feat(metrics): Fix 3 — DB-computed metrics, stateless tracker, resolution tracking
CI/CD / build-and-push (push) Successful in 1m47s

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>
This commit is contained in:
chemavx
2026-04-21 17:34:48 +00:00
parent 9b62636a3e
commit 9a5be27532
5 changed files with 268 additions and 160 deletions
+51 -26
View File
@@ -89,43 +89,68 @@ async def get_trades(limit: int = 50, status: str = "open"):
@app.get("/api/summary")
async def get_summary():
"""Dashboard summary card data."""
history = await db.get_metrics_history(days=1)
open_trades, all_trades, inverted, legacy_count = await asyncio.gather(
"""Dashboard summary card data.
All portfolio counts (total_trades, open_trades_count, total_deployed,
cash_available) are computed live from the DB on every request.
PnL and performance metrics come from the latest metrics_daily snapshot,
which is written by the bot every cycle via MetricsTracker.update_daily_summary().
After Fix 3, that snapshot is also DB-computed — not dependent on pod restarts.
"""
latest_metrics, open_trades, all_trades, inverted, legacy_count = await asyncio.gather(
db.get_metrics_history(days=1),
db.get_recent_trades(limit=500, status="open"),
db.get_recent_trades(limit=500),
db.get_recently_closed_inverted(hours=24),
db.get_legacy_incomplete_count(),
)
latest = history[0] if history else {}
latest = latest_metrics[0] if latest_metrics else {}
paper_bankroll = float(os.getenv("PAPER_BANKROLL", "10000"))
total_deployed = sum(t.get("net_cost", 0) for t in open_trades)
return {
"paper_mode": os.getenv("PAPER_MODE", "true") == "true",
"paper_bankroll": paper_bankroll,
"total_trades": len(all_trades),
"open_trades_count": len(open_trades),
"closed_trades_count": len(all_trades) - len(open_trades),
"total_deployed": total_deployed,
"cash_available": max(0.0, paper_bankroll - total_deployed),
"legacy_incomplete_count": legacy_count,
"reentry_guard_blocks_24h": len(inverted),
# Metrics from latest metrics_daily snapshot (computed by MetricsTracker).
# total_pnl: estimated unrealized PnL for open trades in the current bot
# session — uses edge_net × net_cost (model edge on deployed
# capital). Resets to 0 on pod restart until Fix 3 is applied.
# calibration_score: null until market resolution data is available
# (requires close_price / outcome per closed trade).
"total_pnl": latest.get("total_pnl", 0),
"win_rate": latest.get("win_rate", 0),
"sharpe_ratio": latest.get("sharpe_ratio", 0),
"calibration_score": latest.get("calibration_score"), # null if unavailable
# ── Portfolio state (live from DB) ──────────────────────────────────
"paper_mode": os.getenv("PAPER_MODE", "true") == "true",
"paper_bankroll": paper_bankroll,
"total_trades": len(all_trades), # exact, from DB
"open_trades_count": len(open_trades), # exact, from DB
"closed_trades_count": len(all_trades) - len(open_trades), # exact
"total_deployed": total_deployed, # exact, from DB
"cash_available": max(0.0, paper_bankroll - total_deployed), # exact
"legacy_incomplete_count": legacy_count, # exact, from DB
"reentry_guard_blocks_24h": len(inverted), # exact, from DB
# ── P&L (from latest metrics_daily snapshot) ────────────────────────
# unrealized_pnl_est: open positions, edge_net × net_cost fee.
# Estimated — uses model signal, not live price. Source: open trades.
# realized_pnl: closed positions with known resolution.
# Exact — computed from (resolution entry_price) × shares.
# total_pnl: sum of both.
"unrealized_pnl_est": latest.get("unrealized_pnl_est") or 0,
"realized_pnl": latest.get("realized_pnl") or 0,
"total_pnl": latest.get("total_pnl") or 0,
# ── Performance metrics (from latest metrics_daily snapshot) ─────────
# win_rate: fraction of resolved closed trades where close_pnl > 0.
# null if fewer than 5 resolved trades. Source: closed+resolved trades.
# sharpe_ratio: 0.0 — requires daily-return time series (not yet tracked).
# calibration_score: 1 Brier score on resolved trades (higher = better).
# null if fewer than 10 resolved trades. Source: closed+resolved trades.
"win_rate": latest.get("win_rate"), # null if < 5 resolved
"sharpe_ratio": latest.get("sharpe_ratio") or 0, # 0.0 until tracked
"calibration_score": latest.get("calibration_score"), # null if < 10 resolved
# ── Counters from snapshot ───────────────────────────────────────────
"resolved_count": latest.get("resolved_count") or 0,
# ── Promotion gate ───────────────────────────────────────────────────
# All thresholds must pass; null metrics count as not-ready.
"promotion_ready": (
latest.get("sharpe_ratio", 0) >= 0.5
and latest.get("win_rate", 0) >= 0.52
and (latest.get("calibration_score") or 0) >= 0.7 # null → not ready
(latest.get("sharpe_ratio") or 0) >= 0.5
and (latest.get("win_rate") or 0) >= 0.52
and (latest.get("calibration_score") or 0) >= 0.7
and len(all_trades) >= 50
),
}