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>
total_pnl now uses edge_net × net_cost instead of (0.5 - entry_price) × shares.
The old formula overestimated BUY_NO trades at low entry prices by 3–10× because
buying at price 0.158 yields 3164 shares — any exit-at-0.5 assumption produced
$1072 PnL on $500 deployed. edge_net × net_cost is bounded by net_cost per trade
and uses the model's own signal, giving $122 for the same position.
calibration_score is now None (null in API) instead of 1 - 2×|avg_edge|. That
formula was not a real calibration: it requires knowing market resolutions
(YES=1/NO=0) which we do not store yet. Returning null is more honest than
returning 0.0 or a meaningless proxy. Fix 3 will compute it from closed trades.
check_promotion_thresholds updated to handle None calibration (null → not ready).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- db: update_family_key() persists corrected family slugs for open trades
- db: get_recently_closed_inverted() returns markets closed for inversion
within N hours; used as reentry guard in the trading loop
- db: get_recent_trades() accepts status=open|closed|None and adds a
computed "status" field to every row
- bot/main.py: legacy scan now computes family_key from stored question
alone (dummy Market) when a position's market is no longer active —
fixes NULL family_key on legacy trades like Ken Paxton (562186)
- bot/main.py: legacy scan (Step 2.5) persists corrected family_keys in
DB so family conflict guards work correctly on next restart
- bot/main.py: positions with NULL edge_net and no live market are tagged
legacy_incomplete instead of OK; counted separately in scan summary
- bot/main.py: reentry_guard blocks re-entering any market closed for
inversion bug within 24h; logs reentry_guard_triggered per skip
- api/main.py: /api/trades now accepts ?status=open|closed|all (default
open) and includes status_filter in response
DB fix (applied directly): 629558 family_key politics-2026 →
ohio-gubernatorial-2026; 562186 family_key NULL → texas-republican-2026
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>