Files
polymarket-bot/bot/metrics/tracker.py
T
chemavxandClaude Fable 5 7ebb87aede
CI/CD / build-and-push (push) Successful in 7s
chore: cleanup duplicate trade save, misleading cycle counters, and /api/summary inconsistencies
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>
2026-06-11 17:21:32 +00:00

92 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Metrics Tracker — computes and persists trading performance metrics from the DB.
All metrics are derived directly from the `trades` table on every cycle call.
No in-memory trade state is kept: the tracker is stateless across pod restarts.
Metric definitions
──────────────────
unrealized_pnl_est Estimated PnL for OPEN positions: edge_net × net_cost fee.
Source: open trades with edge_net. Estimated (model signal).
realized_pnl Exact PnL for CLOSED positions: computed from resolution.
Source: closed trades with known resolution. Exact.
total_pnl unrealized_pnl_est + realized_pnl.
win_rate Fraction of resolved closed trades with close_pnl > 0.
NULL if fewer than 5 resolved trades.
calibration_score 1 AVG((final_prob resolution)²) on resolved trades.
Brier score (higher = better calibration). NULL if < 10 resolved.
sharpe_ratio 0.0 — requires a daily-return time series, not yet tracked.
"""
import logging
from datetime import datetime, UTC
from bot.data.db import Database
log = logging.getLogger(__name__)
class MetricsTracker:
def __init__(self, db: Database) -> None:
self._db = db
async def update_daily_summary(self) -> None:
"""Compute metrics from DB and write a metrics_daily snapshot.
Called every cycle by the trading loop. Safe after pod restarts:
reads the full trade history from DB, not from in-memory state.
"""
raw = await self._db.compute_metrics_from_db()
if not raw["total_trades"]:
return
open_count = int(raw["open_count"] or 0)
closed_count = int(raw["closed_count"] or 0)
resolved = int(raw["resolved_count"] or 0)
wins = int(raw["wins_realized"] or 0)
unrealized = float(raw["unrealized_pnl_est"] or 0)
realized = float(raw["realized_pnl"] or 0)
total_deployed = float(raw["total_deployed"] or 0)
total_fees = float(raw["total_fees"] or 0)
total_pnl = unrealized + realized
# win_rate: only over resolved closed trades; null if sample too small
win_rate = (wins / resolved) if resolved >= 5 else None
# calibration: Brier score from DB; null if sample too small
calibration = (
float(raw["calibration_score"])
if raw["calibration_score"] is not None and resolved >= 10
else None
)
avg_edge = total_pnl / total_deployed if total_deployed > 0 else 0.0
metrics = {
"timestamp": datetime.now(UTC),
"total_trades": int(raw["total_trades"]),
"open_count": open_count,
"closed_count": closed_count,
"resolved_count": resolved,
"total_deployed": total_deployed,
"total_fees": total_fees,
"unrealized_pnl_est": unrealized,
"realized_pnl": realized,
"total_pnl": total_pnl,
"win_rate": win_rate,
"avg_edge": avg_edge,
"sharpe_ratio": 0.0, # requires daily-return series (not yet tracked)
"calibration_score": calibration,
"paper_mode": True,
}
await self._db.save_daily_metrics(metrics)
log.info(
"Daily metrics | trades=%d (open=%d closed=%d resolved=%d) | "
"unrealized=$%.2f realized=$%.2f total=$%.2f | "
"win_rate=%s calibration=%s",
metrics["total_trades"], open_count, closed_count, resolved,
unrealized, realized, total_pnl,
f"{win_rate:.1%}" if win_rate is not None else "n/a (<5)",
f"{calibration:.3f}" if calibration is not None else "n/a (<10)",
)