Add MANIFOLD_MATCHER_VERSION="v3_outcome_guard" tag persisted to
manifold_match_audit.matcher_version so metrics can isolate current-matcher
stats from pre-versioning records, whose accepted matches the outcome
guard would now reject.
- schema: add matcher_version column + index; idempotent startup backfill
tagging NULL rows as legacy_pre_outcome_guard (no outcome types) or
v2_outcome_guard_no_version (has outcome type, version not persisted)
- save_manifold_audit: write matcher_version on every new record
- get_manifold_matches: split summary into current_version / all_time /
legacy; recent_matches now carry matcher_version
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds alpha attribution by dominant signal feature — which feat_*_lo had
the largest absolute log-odds value on each trade.
Changes:
- _dominant_feature() helper in api/main.py: picks the winning feature
from signal_components (threshold 0.0001, same as "triggered" in
/api/metrics/features)
- _enrich_trade() refactored to single exit point; adds dominant_feature
field to every open trade in /api/trades
- compute_attribution_from_db() in db.py: VALUES subquery finds dominant
feature per trade in SQL, then aggregates trade_count/avg_edge_net/
unrealized_pnl_est/realized_pnl/resolved_count/win_rate per group
- /api/metrics/attribution endpoint: returns attribution dict + total_attributed_trades
No schema changes, no strategy changes. Pure observability.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds feat_fg_lo / feat_mom_lo / feat_news_lo / feat_mfld_lo / feat_btc_dom_lo
to every trade, all normalized to log-odds contribution for direct comparability.
- fg / mom / btc_dom: raw probability-delta × 2 → log-odds
- news / mfld: already log-odds (LOGODDS_WEIGHT already applied), no scaling
- btc_dom tracked separately in bayesian.py instead of bundled in total_adj
- reasoning string updated to fg_lo= / mom_lo= notation for self-documentation
Schema: 5 new DOUBLE PRECISION columns + 2 partial indexes
Stack: TradingSignal → Order → Trade → save_trade all carry feat fields
Startup: backfill_feature_columns() recovers fg/mom/news/mfld from old
reasoning strings (×2 applied to fg/mom); btc_dom_lo stays NULL for legacy
API: /api/metrics/features — triggered/material split per feature with
two-level thresholds (0.05 for fg/mom/btc_dom, 0.10 for news/mfld)
API: /api/trades/legacy — exposes pre-Phase-1 trades (edge_net IS NULL)
API: _enrich_trade backward-compat: reads DB columns first, falls back to
reasoning regex with unit conversion for pre-Phase-6 trades
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>