Files
polymarket-bot/bot/metrics/tracker.py
T
chemavx 9a5be27532
CI/CD / build-and-push (push) Successful in 1m47s
feat(metrics): Fix 3 — DB-computed metrics, stateless tracker, resolution tracking
schema.sql
  trades:        + close_pnl, resolution (market outcome storage)
  metrics_daily: + unrealized_pnl_est, realized_pnl, open/closed/resolved_count

db.py
  close_paper_position(): accepts resolution; computes close_pnl in SQL
    BUY_YES: (resolution − entry_price) × shares
    BUY_NO:  ((1 − resolution) − entry_price) × shares
  save_daily_metrics(): persists new columns
  compute_metrics_from_db(): single DB query for all metrics; no in-memory state

tracker.py — complete rewrite (stateless)
  Removed self._trades, self._daily_returns, compute_metrics(), _compute_sharpe(),
  check_promotion_thresholds(), _empty_metrics()
  update_daily_summary() now reads compute_metrics_from_db() every cycle
  Safe across pod restarts: always reflects full DB history

paper.py
  close_position(): passes resolution to close_paper_position()

api/main.py  /api/summary
  Added unrealized_pnl_est (estimated, open trades) and realized_pnl (exact,
  closed+resolved) as separate fields alongside total_pnl
  win_rate: null if < 5 resolved trades (was proxy on entry_price < 0.5)
  calibration_score: Brier-based, null if < 10 resolved trades
  resolved_count exposed as field
  Each field annotated with: exact/estimated, source, null conditions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 17:34:48 +00:00

98 lines
4.1 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
from bot.executor.paper import Trade
log = logging.getLogger(__name__)
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.
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)",
)