feat(metrics): Fix 3 — DB-computed metrics, stateless tracker, resolution tracking
CI/CD / build-and-push (push) Successful in 1m47s
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:
+51
-26
@@ -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
|
||||
),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user