chore: cleanup duplicate trade save, misleading cycle counters, and /api/summary inconsistencies
CI/CD / build-and-push (push) Successful in 7s

Bug #5: metrics.record_trade() only delegated to save_trade(), which
executor.execute() already calls — every trade was written twice (deduped
only by ON CONFLICT DO NOTHING). Remove the redundant call and the now-dead
method. RealExecutor.execute() raises NotImplementedError, so real mode is
unaffected.

Bug #6 (CYCLE SUMMARY): manifold accepted/rejected counters only increment
on the active-signal path, so with MANIFOLD_SIGNAL_ENABLED=false they always
printed 0/0 — print 'manifold_signal: disabled' instead.
family_conflicts_prevented duplicated blocked_by_family (same counter
printed twice); removed. gnews_cap was a dead variable with a misleading
comment; removed.

Bug #7 (/api/summary): total_trades was len() over a LIMIT-500 query —
capped once history grows; counts now come from COUNT(*) via
compute_metrics_from_db. cash_available was reimplemented in the API;
extract cash_available() in paper.py (same formula, unchanged) and feed it
from get_open_position_data() — the exact source/helper
PaperExecutor.initialize() uses. Test asserts API and executor report
identical cash for the same DB state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
chemavx
2026-06-11 17:21:32 +00:00
co-authored by Claude Fable 5
parent 02cbfc0b94
commit 7ebb87aede
5 changed files with 147 additions and 25 deletions
+12 -1
View File
@@ -36,6 +36,17 @@ def _notify_in_background(coro) -> None:
task.add_done_callback(_background_tasks.discard)
def cash_available(bankroll: float, total_net_cost_open: float) -> float:
"""Cash left after the net cost (fees included) of all open positions.
Single source of truth for the cash figure, shared by
PaperExecutor.initialize() and the /api/summary endpoint so both always
report the same number for the same DB state.
total_net_cost_open comes from Database.get_open_position_data().
"""
return max(0.0, bankroll - total_net_cost_open)
@dataclass
class Trade:
id: str
@@ -121,7 +132,7 @@ class PaperExecutor:
positions_value = sum(positions_size.values())
self._portfolio.positions = positions_size
self._portfolio.cash = max(0.0, self._portfolio.cash - total_net_cost)
self._portfolio.cash = cash_available(self._portfolio.cash, total_net_cost)
total_value = self._portfolio.cash + positions_value
exposure_pct = positions_value / total_value if total_value > 0 else 0.0
+19 -9
View File
@@ -11,7 +11,12 @@ from bot.data.polymarket import PolymarketClient, Market, market_family_key
from bot.data.external import ExternalDataClient
from bot.data.news import NewsClient
from bot.data.manifold import ManifoldClient
from bot.strategy.bayesian import BayesianStrategy, gnews_priority, MAX_NEWS_QUERIES_PER_CYCLE
from bot.strategy.bayesian import (
BayesianStrategy,
gnews_priority,
MAX_NEWS_QUERIES_PER_CYCLE,
MANIFOLD_SIGNAL_ENABLED,
)
from bot.risk.manager import RiskManager
from bot.executor.paper import PaperExecutor
from bot.metrics.tracker import MetricsTracker
@@ -199,7 +204,6 @@ async def run_trading_loop(
# 7. Execute (paper)
trade = await executor.execute(order)
if trade:
await metrics.record_trade(trade)
log.info("Trade executed: %s", trade)
# Block this family for the rest of the cycle (Phase 2)
occupied_families.add(signal.family_key)
@@ -221,7 +225,17 @@ async def run_trading_loop(
if denom == 0:
return "0% (0/0)"
return f"{n * 100 // denom}% ({n}/{denom})"
gnews_cap = strategy._news_queries_this_cycle # already updated by reset below
# The accepted/rejected counters only increment on the active-signal
# path, so with the signal disabled they always print 0/0 — say
# "disabled" instead of pretending the matcher found nothing.
if MANIFOLD_SIGNAL_ENABLED:
manifold_summary = (
f" manifold_matches_accepted: {stats['manifold_matches_accepted']}\n"
f" manifold_matches_rejected: {stats['manifold_matches_rejected']}"
)
else:
manifold_summary = " manifold_signal: disabled"
log.info(
"[CYCLE SUMMARY]\n"
@@ -239,9 +253,7 @@ async def run_trading_loop(
" gnews_queries_used: %d/%d\n"
" reentry_guard_blocked: %d\n"
" legacy_incomplete_seen: %d\n"
" family_conflicts_prevented: %d\n"
" manifold_matches_accepted: %d\n"
" manifold_matches_rejected: %d",
"%s",
n_total,
n_uncertainty,
stats["max_edge_gross"],
@@ -256,9 +268,7 @@ async def run_trading_loop(
stats["gnews_queries_used"], MAX_NEWS_QUERIES_PER_CYCLE,
reentry_guard_count,
legacy_incomplete_count,
stats["skip_family"],
stats["manifold_matches_accepted"],
stats["manifold_matches_rejected"],
manifold_summary,
)
# 9. Update daily metrics
-6
View File
@@ -21,7 +21,6 @@ import logging
from datetime import datetime, UTC
from bot.data.db import Database
from bot.executor.paper import Trade
log = logging.getLogger(__name__)
@@ -30,11 +29,6 @@ class MetricsTracker:
def __init__(self, db: Database) -> None:
self._db = db
async def record_trade(self, trade: Trade) -> None:
"""Persist a trade to the DB. No in-memory accumulation."""
await self._db.save_trade(trade)
log.info("Trade recorded: %s", trade)
async def update_daily_summary(self) -> None:
"""Compute metrics from DB and write a metrics_daily snapshot.