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>
92 lines
3.8 KiB
Python
92 lines
3.8 KiB
Python
"""
|
||
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)",
|
||
)
|