Created: 2026-01-06
Priority: Critical
The database must be future-proof and reliable. All queries must work correctly without workarounds. The database will be used for years.
tag=Earnings is unreliable)Earnings Market Data:
- earnings_markets table is the source of truth for earnings market data
- Contains properly extracted: ticker, earnings_date, earnings_time, market_cap_category (Large / Mid / Small)
- Synced by the hourly Gamma earnings_market_fetcher; earnings_time (BMO/AMC) is resolved in Python from NASDAQ when explicit, else earnings_timing.py DEFAULT_TIMINGS + timing_data/overrides.json only — not yfinance/web fallback (those remain runtime-only in SignalGenerator; Finding #220; amended by #349: a ≥2-vote calendar consensus from the multi-source verifier may also correct date+timing, and verifier-confirmed rows — date_confidence='confirmed' — are locked against ALL Polymarket-derived sync overwrites; changes audited in earnings_date_audit). Market cap from KNOWN_MCAPS, then Yahoo/Finnhub via yfinance_limiter, persisted by backfill_upcoming_null_mcaps() after each live fetch (Finding #219). Finding #480 retires run_full_sync / sync_from_markets_table and the manual wrapper before DB access because that legacy writer could change a date without the mandatory audit row.
Live Position State (Phase 4B+4C - Feb 7, 2026):
- IBKR is the source of truth for live position state (is a position currently open?)
- IBKRPositionCache (5s TTL) provides thread-safe cached access
- trading_positions table is the source of truth for historical data (P&L, audit trail, crash recovery)
- All DB writes remain as complete audit trail - they are never removed
- When IBKR is disconnected, ALL trading operations are blocked
Outcome resolution writers (Finding #227 — Apr 21-22; coverage updates Apr 26):
- earnings_markets.actual_beat / eps_actual / revenue_actual / stock_price_* / stock_return_* — single writer is src/data_tracker/collectors/outcome_resolver.py (run by polymarket-resolver.timer daily 06:00 ET). The trading orchestrator may pre-fill actual_beat on instant-exit detection (faster path); the resolver back-fills the rest and is idempotent (COALESCE(... IS NULL)).
- actual_beat source cascade (Finding #227 update Apr 22; #244/#246 update Apr 26):
1. EPS surprise (eps_actual >= eps_estimate) — Yahoo → Finnhub → Polygon /vX/reference/financials
2. Post-event Polymarket yes_price from price_snapshots >12h after earnings_date (≥0.95/≤0.05)
3. Pre-event Polymarket prob cascade (prob_30min_before → prob_1h → ... → prob_24h)
Coverage: 73% (Apr 22) → 84.1% (Apr 26 #244 — --force bypasses the 6-hour updated_at guard the hourly sync was tripping) → 98.1% (Apr 26 #246 — post-event price_snapshot backfill via Polymarket data-api) → ~95-99.8% current (Finding #321, May 2026 — 948/961 past-settled resolved) → decayed back to 76.0% by Aug 16, 2026 (the #244 lockout was never structurally fixed; the hourly sync kept re-locking unresolved rows — populations.md §1.2) → structurally fixed Aug 21, 2026 (Finding #371, A-2): the guard now keys on the resolver-OWNED resolver_attempted_at (migration 033), which no other writer touches; backlog self-heals on normal timer fires. Remaining unresolved are bounded by post-event PM-trade availability + ambiguous mid-band; this is no longer a fixed "14 irreducible" set (the earlier "14 irreducible" figure was superseded by #321).
- earnings_markets.actual_earnings_time (NEW Apr 26 — Migration 026, Finding #247) — populated from SEC EDGAR submissions/{cik}.json acceptedDate. Coverage 90.9% (667/734 past-settled). Refines simulator's ±90-min release-window heuristics. Backfill: scripts/backfill_press_release_times_from_sec.py. Weekly self-heal in polymarket-data-backfill.timer (5th ExecStart pass).
- skipped_signals.actual_outcome / would_have_won / hypothetical_pnl[_pct] / hypothetical_stop_hit — single writer is the resolver (Pass 2). Inserts come from signal_generator.log_skipped_signal.
- polymarket_opportunity_log.hypothetical_* — single writer is the resolver (Pass 3); operator can also run scripts/compute_counterfactual_pnl.py for ad-hoc batches. Now also wired into polymarket-data-backfill.timer weekly so new rows get counterfactual P&L automatically (Finding #238, Apr 26).
- polymarket_opportunity_log.snapshot (JSONB) — pm_v2 schema since Apr 26 (Finding #243): captures pm_best_bid, pm_best_ask, pm_spread, pm_bid_depth, pm_ask_depth, snapshot_schema='pm_v2'. Note: schema label fires on every new row, but value-add fields are NULL on signal_emitted rows pending Phase 3 fix (Finding #273, documented Apr 27 evening).
- trade_audit_log.prediction_correct / actual_earnings_result — derived by resolver Pass 4 by joining to earnings_markets.actual_beat once that's known. Also backfilled one-shot Apr 27 evening via SQL (Finding #264 — VZ + 22 historical trades).
- trade_audit_log.regime_at_entry / regime_details / vix_at_entry / sp500_change_at_entry — written by position_manager._persist_position_open on entry using the shared MarketRegimeProvider (mandatory non-null on new rows starting Apr 22, 2026; historical rows backfilled by scripts/backfill_regime_for_audit_log.py). Bug fix Apr 26 — Finding #234 — restored the orchestrator INSERT path that was silently dropping these fields for 5/116 trades.
- trade_audit_log.conviction_at_exit — added on close path Apr 26 (Finding #237). Forward-only — historical NULL.
- trade_audit_log.hold_duration_hours — Apr 27 morning (#256) added writer; Apr 27 evening (#263) discovered the writer's pg8000 syntax error and re-fixed with CAST(... AS timestamptz). Backfilled VZ #120 + 22 trades via SQL.
- trade_audit_log.volume_24h_at_entry — Polymarket Gamma's volume24hr field has been intermittently NULL since ~Feb 16, 2026 (Finding #272 RCA Apr 27). signal_generator.get_market_volume() now falls back to most-recent positive price_snapshots.volume_24h within last 6h. Backfilled 114/117 (97%) historical rows.
- polymarket_opportunity_log / skipped_signals intended_position_value / intended_shares / intended_stop_price / intended_flex_applied / regime_at_entry / vix_at_entry / sp500_change_at_entry — written by signal_generator at insert time (forward strategy-replay capture, no later reconstruction needed).
BUG_EXIT convention on trade_audit_log.notes (Finding #276, Apr 27, 2026):
When a trade was force-exited by a since-fixed bug, the exit_reason column
is left intact as the historical record of WHAT the trader did, but the
notes column is annotated with a structured prefix so future analyses can
exclude these polluting rows from exit-reason aggregations:
BUG_EXIT (Finding #N): <one-line explanation>...exit_reason='instant_exit:earnings_miss'. The bug
is fixed (Finding #264), but the trade row stays in trade_audit_log
with a BUG_EXIT (Finding #264): ... annotation in notes so the
dashboard renders it as 🐛 BUG-EXIT · Instant Exit (Earnings Miss)
and any aggregation grouping by exit_reason can WHERE notes NOT ILIKE
'BUG_EXIT%' to exclude it.prediction_correct and
actual_earnings_result are backfilled to reflect the TRUE outcome (per
earnings_markets.actual_beat), since the trader's prediction_correct
derivation may have used the wrong-signal exit reason.exit_reason should add AND notes NOT ILIKE 'BUG_EXIT%'
to its WHERE clause unless the analysis specifically wants the bug-driven
rows.The dashboard trade-history serializer (earnings_dashboard/app.py:get_trade_history)
detects this prefix, prefixes exit_reason_display with 🐛 BUG-EXIT ·,
and surfaces a bug_exit_flagged: bool field on the API response so JS /
analyses can filter without parsing the prefix string.
src/data_tracker/collectors/earnings_market_fetcher.py, hourly via polymarket-earnings-sync.timer; includes post-fetch NULL-mcap backfill.scripts/sync_earnings_markets.py, src.db.earnings_sync.run_full_sync() and sync_from_markets_table() exit before DB access. Do not restore them without an earnings_date_audit write in the same transaction.src/db/earnings_sync.py keeps backfill_upcoming_null_mcaps(), backfill_upcoming_null_earnings_time(), resolve_earnings_time_for_sync() for supported targeted callers.scripts/backfill_ticker_earnings_timing.py — updates NULL/TBD/empty only (DATABASE_URL required)polymarket-earnings-sync.timer (earnings_market_fetcher) — runs MCAP backfill after each Gamma sync. Polygon mcap added as 2nd-tier fallback Apr 26 (Finding #249).polymarket-data-backfill.timer (Sun 04:00 ET, 6 ExecStart passes) heals markets.volume_total for resolved markets (Finding #229), backfills missing prob_*_before via Polymarket data-api (#230), backfills post-event price_snapshots for outcome resolution (#246), and pulls SEC EDGAR press-release times (#247)./var/log/polymarket/earnings_sync.log, /var/log/polymarket/data_backfill.log. Service exit codes are watched by the integrity monitor's systemd_last_run:* rules (Finding #268, Apr 27 evening).get_polymarket_earnings_from_db() functionCREATE TABLE earnings_markets (
id SERIAL PRIMARY KEY,
condition_id VARCHAR(66) NOT NULL UNIQUE,
ticker VARCHAR(10) NOT NULL,
company_name VARCHAR(255),
market_cap_category VARCHAR(20), -- 'Large', 'Mid', 'Small'
earnings_date DATE NOT NULL,
earnings_time VARCHAR(10), -- 'BMO', 'AMC', 'TBD'
fiscal_quarter VARCHAR(10),
fiscal_year INTEGER,
-- ... probability snapshots for analysis ...
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE INDEX idx_earnings_date ON earnings_markets(earnings_date);
CREATE INDEX idx_earnings_ticker ON earnings_markets(ticker);
idx_earnings_date - Fast date range queriesidx_earnings_ticker - Fast ticker lookupscondition_id - Prevents duplicates┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ markets table │────>│ EarningsMarketSync│────>│ earnings_markets │
│ (raw Polymarket)│ │ (parse & enrich) │ │ (clean, indexed) │
└─────────────────┘ └──────────────────┘ └──────────────────┘
│
v
┌──────────────────────────────┐
│ Timing: NASDAQ → BMO/AMC │
│ else DEFAULT_TIMINGS/override│
│ (no web → DB; Finding #220) │
└──────────────────────────────┘
│
v
┌──────────────────────────────┐
│ Yahoo + Finnhub (limiter) │
│ mcap / company fallbacks │
└──────────────────────────────┘
│
v
┌──────────────────┐
│ backfill NULL │
│ market_cap_* │
│ (Finding #219) │
└──────────────────┘
│
v
┌──────────────────┐
│ backfill NULL/ │
│ TBD earnings_time│
│ (Finding #220) │
└──────────────────┘
polymarket_opportunity_log (Migration 021, Finding #221)Append-only log of every Polymarket-linked earnings candidate evaluated on the BMO/AMC signal path (SignalGenerator), including rejects (timing, depth, conviction, generate_signal, priority filter, calendar misses). Rows include live-rule entry_datetime_scheduled, exit_datetime_scheduled, and stop_loss_pct when BMO/AMC and earnings datetime are known—so counterfactual P&L jobs can run over all stages without re-deriving the calendar.
src/earnings/polymarket_opportunity_log.py (fail-soft inserts).src/earnings/counterfactual_pricing.py + scripts/compute_counterfactual_pnl.py — Polygon 1m (preferred) or yfinance daily fallback; fills hypothetical_* on polymarket_opportunity_log. Canonical narrative: Finding #221 in docs/FINDINGS.md.If markets are missing from dashboard:
1. Check earnings_markets table: SELECT COUNT(*) FROM earnings_markets WHERE earnings_date >= CURRENT_DATE
2. Start the live oneshot: systemctl start polymarket-earnings-sync.service
3. Check logs: tail -f /var/log/polymarket/earnings_sync.log
polymarket-earnings-sync.timer (earnings_market_fetcher); see docs/OPERATIONS.md. Weekly self-heal via polymarket-data-backfill.timer (Findings #229-#231).systemctl start polymarket-earnings-sync.service; the legacy manual script/module are retired (#480).volume24hr field is intermittently NULL (Finding #272 — Apr 27); signal_generator.get_market_volume() falls back to price_snapshots.volume_24h when needed.Polymarket creates earnings markets 1-2 weeks before the earnings date. This is expected behavior - markets will appear as dates approach.
# Run supported sync immediately
systemctl start polymarket-earnings-sync.service
# Check sync logs
tail -f /var/log/polymarket/earnings_sync.log
# Verify earnings sync timer (hourly systemd; NOT crontab)
systemctl list-timers polymarket-earnings-sync.timer --no-pager
# Check database stats
python3 scripts/check_earnings_db.py
Before this fix (pre-2026-01-06):
- Relied on Polymarket's tag=Earnings API filter
- This filter was broken and returned sports/crypto markets
- Dashboard code had complex workarounds parsing question text
- Data was inconsistent and unreliable
After this fix:
- Dedicated earnings_markets table with proper schema
- Automated sync process keeps data fresh
- Dashboard uses clean database queries
- No workarounds, no tech debt