← Back to Dashboard

Findings

Single source of truth for all findings, fixes, pending validations, and production evidence. The dashboard Testing Findings tab renders from this file via /api/architecture-findings. Do NOT create finding entries in other files. See memory.md for the documentation rule.

Status Legend

Numbering traps (read first)

Findings

1) Entry + stop atomicity
- Severity: Critical
- Status: done
- Implementation: Complete in code; verified in live trading Feb 2, 2026.
- Proposed change: Use atomic IBKR order placement for entry+stop (preserve NYSE stop-limit behavior), verify stop placement before confirming entry.
- Files: src/ibkr/client.py - Live Test Result (Feb 2, 2026 09:34 AM ET): - PLTR entry filled @ $148.70, stop placed @ $144.23 within 1 second - Stop Order ID 84904 confirmed in IBKR - Batch completed: Total: 1, Success: 1, Failed: 0

2) Batch stop placement safety
- Severity: High
- Status: done
- Implementation: Complete in code; verified in live trading Feb 2, 2026.
- Proposed change: Treat stop placement failures as critical (retry, alert, and/or fail batch) so no filled entry is left unprotected.
- Files: src/earnings/batch_order_manager.py - Live Test Result (Feb 2, 2026 09:34 AM ET): - Batch ID 319b8adb executed successfully - Stop placement confirmed for PLTR immediately after entry fill - Total batch time: 4467ms (submit: 1778ms)

3) Duplicate order prevention / idempotency
- Severity: High
- Status: done
- Implementation: Complete in code; verified in live trading Feb 2, 2026.
- Proposed change: Add per-symbol lock or DB idempotency key before order placement to prevent race duplicates.
- Files: src/earnings/trading_orchestrator.py - Live Test Result (Feb 2, 2026 09:34 AM ET): - AAPL correctly skipped: "Existing positions (will skip): {'AAPL'}" - Also verified via test_duplicate_prevention.py at 04:40 AM ET

4) Market hours gating / holiday handling
- Severity: High
- Status: done
- Implementation: Complete (IBKR liquidHours gating + calendar fallback).
- Proposed change: Gate entry/exit orders to regular market hours using IBKR market hours; allow stop-loss/emergency exits during extended hours.
- Files: src/earnings/order_executor.py and scheduling layer - Live Test Result (Feb 2, 2026 04:40 AM ET): - Pre-market test: Entry correctly blocked (market_status: pre_market) - Instant exit confirmed always allowed - Tested via test_market_hours_gating.py

5) IBKR error blocking
- Severity: Medium
- Status: done
- Implementation: Complete. trading_block.txt persistence, 1100 transient (cleared on reconnect), 1300 critical (blocks trading). Unit tests in test_ibkr_error_block_policy.py.
- Files: src/ibkr/client.py

6) Collector retries/backoff
- Severity: Medium
- Status: done
- Implementation: Complete. Exponential backoff implemented in all 9 collector files. Running stable for 2+ months.
- Files: src/data_tracker/collectors/continuous_collector.py, src/data_tracker/collectors/live_trades_collector.py, src/data_tracker/collectors/earnings_market_fetcher.py, src/data_tracker/collectors/gamma_collector.py, src/data_tracker/collectors/api_health_checker.py, src/data_tracker/collectors/prices_backfill_service.py, src/data_tracker/collectors/sync_earnings_markets.py, src/data_tracker/collectors/dome_client.py, src/data_tracker/collectors/polymarket_client.py

7) DB-level trade dedupe
- Severity: Medium
- Status: done
- Implementation: Added trade_dedupe table with global unique trade_id; collectors insert into dedupe first, then trades.
- Files: src/data_tracker/collectors/live_trades_collector.py, src/data_tracker/collectors/backfill_trades.py, src/data_tracker/migrations/017_trade_dedupe.sql

8) Backfill state atomic writes
- Severity: Medium
- Status: done
- Implementation: Atomic state writes via temp + fsync + rename.
- Files: src/data_tracker/collectors/prices_backfill_service.py, src/data_tracker/collectors/backfill_trades.py, src/data_tracker/collectors/orderbook_backfill_service.py, src/data_tracker/watchdog_v3.py, src/ibkr/client.py

9) Reconciliation window hybrid schedule
- Severity: Medium
- Status: done
- Implementation: Daily reconciliation timer + monthly full-history timer enabled.
- Files: deploy/systemd/polymarket-trade-reconcile.timer, deploy/systemd/polymarket-trade-reconcile-full.*, deploy/install_services.sh

10) Dashboard slippage weighting + staleness warnings
- Severity: Low
- Status: done
- Implementation: Weighted slippage from trade notional + stale warnings in UI.
- Files: earnings_dashboard/app.py, earnings_dashboard/templates/dashboard.html

11) Fractional-share display consistency
- Severity: Low
- Status: done
- Implementation: Preserve fractional shares display with consistent formatting.
- Files: earnings_dashboard/app.py, earnings_dashboard/templates/dashboard.html

12) Closed-market instant exit safety
- Severity: Critical
- Status: done
- Implementation: When market is fully closed, instant exit uses GTC LIMIT only (no MARKET fallback).
- Files: src/earnings/order_executor.py, docs/STRATEGY.md, docs/EDGE_CASES.md

13) Orderbook collector partial commit safety
- Severity: High
- Status: done
- Implementation: Commit per-market snapshots to avoid full-batch rollback.
- Files: src/data_tracker/collectors/main_collector.py

Testing Gaps (recommended additions) Unit tests for entry+stop atomicity (including failure cases). Integration tests for batch orders to ensure stop placement success. Reconciliation tests covering old trades and partial fills. Dashboard P&L tests covering fractional shares and stale pricing.

14) Reconciliation Disconnection Guard (Finding #106) - Severity: Critical - Status: done - Implementation: Complete (Feb 2, 2026). Guards, sanity checks, and external position stop_price population. - Root Cause: Reconciliation ran while IBKR was disconnected, incorrectly closing AAPL as "orphaned", then re-adding it as external position without stop_price. - Fixes: 1. Added IBKR connection guard at start of _reconcile_positions_with_ibkr() 2. Added sanity check: abort if IBKR returns 0 positions but DB has open positions 3. DB repair: restored original AAPL position (ID 48), deleted fake external (ID 54) 4. External position creation now populates stop_price from IBKR stop orders - Files: src/earnings/trading_orchestrator.py

15) Stale Conviction Data - Gamma API Batch Reliability (Finding #107) - Severity: High - Status: done - Implementation: Complete (Feb 2, 2026). Two-phase fetch: batch + individual fallback + 0% display fix. - Root Cause: Gamma API batch endpoint (/markets?condition_ids=...) silently drops markets from responses. Requesting 25 condition_ids returns only ~20. Affected markets show "STALE" in dashboard. - Additional Bug Found: Template used e.conviction and ... which is falsy for 0.0, causing 0% convictions to show as "50%". - Fixes: 1. Phase 1: Batch fetch (unchanged, fast) 2. Phase 2: Identify missing markets, fetch each individually (reliable) 3. Log warnings when markets are still missing after both phases 4. Fixed template to use e.conviction is not none instead of e.conviction and - Files: earnings_dashboard/app.py, earnings_dashboard/templates/dashboard.html

Tests left to be done post implementation - Finding 12) Trigger instant exit when market is fully closed (8 PM - 4 AM ET): confirm GTC LIMIT only, no MARKET fallback. (Pending - will test if instant exit needed during 8 PM - 4 AM window)

Tests completed - Finding 1) Live entry+stop test - PASSED Feb 2, 2026 09:34 AM ET: PLTR entry filled, stop placed within 1 second (order ID 84904). - Finding 1) Second live test - PASSED Feb 2, 2026 03:58 PM ET: PEP @ $155.12 + stop @ $152.01, PFE @ $26.64 + stop @ $26.11 (Batch 34c25f75). - Finding 2) Live batch entry test - PASSED Feb 2, 2026 09:34 AM ET: Batch 319b8adb completed with stop placement confirmed. - Finding 2) Second batch test - PASSED Feb 2, 2026 03:58 PM ET: Batch 34c25f75 - 2/2 success, 0 failed, stops placed for both. - Finding 3) Duplicate prevention test - PASSED Feb 2, 2026 04:40 AM ET: AAPL correctly skipped due to existing position. - Finding 3) Second duplicate test - PASSED Feb 2, 2026 03:58 PM ET: PLTR correctly skipped (existing position) in batch. - Finding 4) Market hours gating test - PASSED Feb 2, 2026 04:40 AM ET: Pre-market entry blocked, instant exit allowed. - Finding 5) Simulated critical IBKR error + block file removal using scripts/verify_retry_backoff_and_block.py. - Finding 6) Simulated 429/5xx retries across collectors/backfills using scripts/verify_retry_backoff_and_block.py. - Finding 14) Code review VERIFIED Feb 2, 2026: IBKR connection guard (line 935) + zero position sanity check (line 1001) confirmed in _reconcile_positions_with_ibkr(). - Finding 15) Stale conviction fix VERIFIED Feb 2, 2026: All markets now show fresh data from gamma_api, 0% convictions display correctly. - Finding 16) IBKR Connection Resilience VERIFIED Feb 3, 2026: IBC auto-login fixed, alert rate limiting added.

16) IBKR Connection Resilience - IBC Auto-Login & Alert Fatigue (Finding #108) - Severity: High - Status: done - Implementation: Complete (Feb 3, 2026). Fixed IBC Java path for auto-login, added alert rate limiting. - Root Cause: Two issues identified: 1. IBC gatewaystart.sh used system OpenJDK (/usr/lib/jvm/java-17-openjdk-amd64/bin) which lacks JavaFX, causing NoClassDefFoundError: javafx/scene/Parent. IB Gateway restarts without auto-login. 2. Reconciliation skip alert (Finding #106 guard) fired every 5 minutes when IBKR disconnected, causing 50+ alerts overnight. - Fixes: 1. Updated /opt/ibc/gatewaystart.sh line 33: JAVA_PATH=/opt/i4j_jres/Oda-jK0QgTEmVssfllLP/17.0.10.0.101-zulu/bin (bundled Zulu Java with JavaFX) 2. Switched from ibgateway.service to ibgateway-ibc.service (enabled, tested auto-login + 2FA) 3. Added _last_disconnect_alert_time tracking in trading_orchestrator.py 4. Alert rate limited: only first alert per disconnect episode, then every 30 min 5. Log at DEBUG level for suppressed alerts (issue still visible in logs) - Files: - /opt/ibc/gatewaystart.sh (server) - /etc/systemd/system/ibgateway-ibc.service (enabled) - src/earnings/trading_orchestrator.py (rate limiting)

17) Hardware/Software Stop Race Condition & Exit Retry Loop (Finding #109) - Severity: Critical - Status: done - Implementation: Complete (Feb 3, 2026). IBKR position check before software exit + exit-in-progress tracking. - Root Cause: Two bugs caused PFE LONG 12 shares to spiral into SHORT 108 shares: 1. Race Condition: Hardware stop #93950 filled at 11:42:49 (sold 12, position flat). At 11:42:51, software stop backup checked _check_hardware_stop_status() which only looks for active stop orders (PreSubmitted/Submitted), not whether position is still open. Since stop had filled (status=Filled), it returned False. Software executed another SELL, creating SHORT position. 2. Exit Retry Loop: Exit verification failed (saw -12 shares instead of 0). Position stayed in active_positions. Main loop (1s interval) kept triggering software stop backup. Each iteration placed another SELL, growing SHORT from -12 to -108 shares. - Fixes: 1. Added _exit_in_progress set to track symbols with pending exits - prevents retry loops 2. Added _get_ibkr_position_shares() helper to check current IBKR position before software exit 3. Modified check_stop_loss_backup(): - Skip if symbol already in _exit_in_progress - Before executing: verify IBKR position is not flat (prevents race with hardware stop) - Verify position direction matches expected (prevents selling into SHORT) - Add to _exit_in_progress BEFORE executing to prevent loops 4. Added _handle_hardware_stop_fill() to clean up when hardware stop already closed position 5. Clear _exit_in_progress only on successful verified exit 6. Corrected PFE database record: direction=LONG, shares=12, entry=$26.73, exit=$26.13, pnl=-$7.20 - Files: - src/earnings/trading_orchestrator.py (lines 258-266, 3130-3185, 3396-3445, 3203, 3213) - Database Fix: - PFE position ID 57: Corrected from SHORT/108/$26.00 to LONG/12/$26.73 - P&L corrected from -$31.32 to -$7.20 - Prevention: - Software backup now checks IBKR position before acting - Exit attempts are tracked to prevent infinite loops - Direction mismatch triggers critical alert, not automatic action

18) Scheduled Exit DB Update Failure & Early Exit Trigger (Finding #110) - Severity: High - Status: done - Implementation: Complete (Feb 3, 2026). Fixed pg8000 type casting + exit timing threshold. - Root Cause: Two bugs caused PEP scheduled exit data to be corrupted: 1. DB Update Failed: _persist_position_close_with_exit_details() failed with "invalid input syntax for type integer" when passing Python float slippage values to PostgreSQL numeric columns via pg8000. Position remained "open" in DB. 2. Early Exit Trigger: check_scheduled_exits() used <= 120 threshold (2 minutes). At 9:30:00 AM, time_to_exit=120 and 120<=120 was TRUE, triggering BMO exits 2 minutes early (9:30 instead of 9:32). 3. Reconciliation Overwrite: Because position was still "open" after failed DB update, reconciliation closed it as "orphaned_position" with wrong data ($158.98 exit instead of actual $155.96). - Fixes: 1. Added to_decimal() helper to cast float values to Decimal before DB insert/update 2. Applied to all slippage parameters: exit_slippage, exit_slippage_pct, exit_limit_price, exit_actual_slippage, entry_slippage, entry_slippage_pct, entry_limit_price 3. Changed check_scheduled_exits() threshold from <= 120 to <= 10 (10-second grace window) 4. Changed _execute_batch_exit() threshold from <= 120 to <= 10 for consistency 5. Added critical alert when DB update fails (instead of silent error log) 6. Corrected PEP database records: exit_price=$155.96, pnl=$1.68, reason=scheduled_exit - Files: - src/earnings/trading_orchestrator.py (lines 1997-2010, 2129-2134, 3332-3339, 3924-3928, 1908-1922) - Database Fix: - PEP position ID 56 (trading_positions): exit_price=155.96, pnl=1.68, reason=scheduled_exit - PEP position ID 22 (trade_audit_log): exit_price=155.96, pnl=1.68, reason=scheduled_exit - Prevention: - Float-to-Decimal conversion prevents pg8000 type mismatches - 10-second threshold ensures exits happen at scheduled time, not 2 minutes early - Critical alerts on DB failures prevent silent data corruption

19) Auto-Fix Data Corruption & Duplicate Shutdown Alerts (Finding #111) - Severity: Critical - Status: done - Implementation: Complete (Feb 3, 2026). Converted auto-fix to detection-only + idempotent shutdown + hardware stop DB fix. - Root Cause: Three issues identified during Feb 3, 2026 incident analysis: 1. Auto-Fix Data Corruption: _reconcile_positions_with_ibkr() automatically "fixed" discrepancies between DB and IBKR by: - Closing "orphaned" positions (position in DB but not IBKR) with calculated P&L using current price - Syncing share counts to match IBKR This caused data corruption in two incidents: - PFE: Synced 12 shares to 108 (from erroneous exit loop SHORT) - PEP: Closed as "orphaned" with wrong $7.72 P&L (actual was $1.68) The auto-fix was dangerous because: - It didn't know if DB update was just pending (position recently closed correctly) - It trusted IBKR even when IBKR had erroneous positions (from bugs) - Calculated P&L used current price, not actual exit price 2. Duplicate Shutdown Alerts: Signal handler called shutdown() then sys.exit(0), but sys.exit() triggers the finally block in run() which also called shutdown(). Result: 2-3 shutdown alerts per shutdown. - Fixes: 1. Detection-Only Reconciliation: - Duplicates: Log CRITICAL + alert, no auto-merge - Orphaned positions: Log CRITICAL + alert, no auto-close - Share mismatches: Log CRITICAL + alert, no auto-sync - External positions: STILL auto-added (safe operation - doesn't overwrite data) 2. Alert for Manual Fix: Issues detected send Telegram alert with list of discrepancies requiring manual review 3. Idempotent Shutdown: Added _shutdown_called flag to EarningsTrader.__init__(). shutdown() now checks flag and returns early if already called. Single alert per shutdown guaranteed. - Files: - src/earnings/trading_orchestrator.py (lines 916-1325: reconciliation function rewritten) - run_earnings_trader.py (lines 146, 361-376: idempotent shutdown) - Additional Bug (AMD): - _handle_hardware_stop_fill() called non-existent method _close_position_in_db() - Caused AttributeError, DB not updated, then reconciliation "fixed" with wrong P&L - Fix: Changed to call _persist_position_close_with_exit_details() instead - AMD record corrected: exit_price=$244.84, P&L=-$5.00 (not -$6.66) - Database Fixes: - PFE position ID 57: direction=LONG, shares=12, exit=$26.13, pnl=-$7.20 - PEP position ID 56/22: exit=$155.96, pnl=$1.68, reason=scheduled_exit - AMD position ID 58/24: exit=$244.84, pnl=-$5.00, reason=stop_loss:hardware - Prevention: - Auto-fix can no longer overwrite correct data with wrong calculated values - Discrepancies require human review before fixing (check IBKR activity log) - Shutdown sends exactly one alert regardless of how shutdown is triggered - Hardware stop cleanup now uses correct DB method

20) IBKR Connection Reliability Overhaul (Finding #112) - Severity: Critical - Status: done - Implementation: Complete (Feb 4, 2026). Removed dual state tracking, fixed race conditions, added health verification. - Root Cause: IBKR Gateway disconnected at 11:25 PM ET on Feb 3 and never reconnected. 7 AMC signals (QCOM, GOOGL, F, etc.) missed execution. Three issues identified: 1. Dual State Tracking: _connected flag and _connection_state enum could diverge. Comment at line 172 said _connected was removed, but it was still being used at lines 279, 302-303, 313, 1595. 2. Race Condition: Heartbeat loop checked _reconnect_in_progress without holding _reconnect_lock, allowing competing reconnect attempts. 3. No Health Verification: After reconnect, connection was considered good if ib.connect() succeeded, but connection could be degraded or non-functional. - Fixes: 1. Removed all _connected flag references - now using only _connection_state enum 2. Added lock acquisition before checking _reconnect_in_progress in heartbeat loop 3. Added _verify_connection_health() method that performs functional checks after reconnect: - Verifies ib.isConnected() returns True - Verifies accountValues() returns data (functional check) - Verifies managedAccounts() returns account ID 4. Reconnect loop now retries if health check fails after successful connect - Files: - src/ibkr/client.py (lines 279, 302-303, 313, 455, 1595, new _verify_connection_health method)

21) Ticker Extraction Reliability (Finding #113) - Severity: Medium - Status: done - Implementation: Complete (Feb 4, 2026). Added fallback patterns and logging for skipped markets. - Root Cause: QRVO missing from earnings_markets because single regex pattern \(([A-Z]{1,5})\) failed when ticker wasn't in parentheses. Markets silently skipped with no logging. - Fixes: 1. Added fallback ticker extraction patterns: - Pattern 1: Standard parentheses (TICKER) - Pattern 2: "ticker SYMBOL" or "ticker: SYMBOL" - Pattern 3: All-caps word at start of question - Pattern 4: Quoted ticker 'TICKER' or "TICKER" - Pattern 5: Last resort - any standalone 1-5 letter uppercase word (filtered) 2. Added warning logs when markets are skipped due to missing ticker or condition_id - Files: - src/data_tracker/collectors/earnings_market_fetcher.py (_extract_ticker_from_question method, extract_markets_from_events method)

22) Exit Reason Display Normalization (Finding #114) - Severity: Low - Status: done - Implementation: Complete (Feb 4, 2026). Standardized exit reason display in dashboard. - Root Cause: Exit reasons displayed inconsistently: - Raw values like instant_exit:wrong_prediction, stop_loss:hardware, execution_ledger_reconciliation - Template had incomplete switch logic that didn't cover all variations - Fixes: 1. Added normalize_exit_reason() function to dashboard app.py 2. Converts internal exit reasons to user-friendly format: - scheduled_exit, scheduled_batch → "Scheduled Exit" - instant_exit:wrong_prediction → "Instant Exit (Wrong Prediction)" - stop_loss:hardware → "Stop Loss (Hardware)" - execution_ledger_reconciliation → "Ledger Reconciliation" 3. Updated dashboard template to use normalized display with appropriate icons/colors - Files: - earnings_dashboard/app.py (normalize_exit_reason function, trade history formatting) - earnings_dashboard/templates/dashboard.html (exit reason display)

23) Exit Retry Loop Prevention - Complete Overhaul (Finding #115) - Severity: CRITICAL - Status: done - Implementation: Complete (Feb 4, 2026). Comprehensive fix for exit retry loop that caused CME SHORT -38 shares. - Root Cause: Finding #109's fix was incomplete. Multiple compounding issues: 1. _exit_in_progress only applied to stop_loss_backup - NOT to scheduled exits, instant exits, or batch exits 2. No IBKR position verification before exit - Orders placed without checking position still exists in expected direction 3. DB persist failure didn't block cleanup - active_positions cleared even when DB update failed 4. Exit verification used abs() - Couldn't distinguish "position still open" from "position flipped to opposite direction" 5. pg8000 type casting issue - COALESCE with integer literal 0 caused slippage values to fail - Incident: CME position Feb 4, 2026: - Original: LONG 1 share @ $293.46 - First scheduled exit executed successfully, DB update failed - Service restart reloaded CME as "open" from DB - Main loop kept triggering scheduled exits - Each iteration placed another SELL order - Result: CME SHORT -38 shares (CATASTROPHIC) - Fixes: 1. Added _exit_in_progress to ALL exit paths: - check_scheduled_exits() - Skip if symbol in _exit_in_progress - execute_scheduled_exit() - Add symbol BEFORE order, clear only on DB success - execute_instant_exit() - Add symbol BEFORE order, clear only on DB success - _execute_batch_exit() - Add all symbols BEFORE batch, clear only on DB success - check_positions_for_exit() - Skip if symbol in _exit_in_progress - _process_instant_exit_queue() - Skip queued items if already in progress 2. Added IBKR position verification before exit: - New _verify_ibkr_position_for_exit() method - Checks position exists in expected direction (LONG vs SHORT) - Blocks exit if position is flat, opposite direction, or can't verify - Sends critical alert on direction mismatch 3. DB persist now returns bool: - _persist_position_close_with_exit_details() returns True/False - Callers check return value before cleanup - If False: keep in active_positions, keep in _exit_in_progress - This prevents service restart from re-triggering failed exits 4. Exit verification enhanced to detect direction flip: - Uses signed position value (not abs()) - Detects if position flipped to opposite direction - Logs CRITICAL with specific direction flip warning - Sends separate alert for direction flip vs incomplete exit 5. Fixed pg8000 type casting: - Changed COALESCE defaults from 0 to 0::numeric - Prevents integer/numeric type inference issues - Files: - src/earnings/trading_orchestrator.py: - Line 265: _exit_in_progress set initialized - Lines 3350-3360: check_scheduled_exits() - added skip check - Lines 3130-3145: execute_scheduled_exit() - added marking and verification - Lines 3197-3213: execute_instant_exit() - added marking and verification - Lines 3337-3362: execute_instant_exit() - DB persist check before cleanup - Lines 4073-4098: _execute_batch_exit() - added marking - Lines 4114-4156: _execute_batch_exit() - persist before cleanup, check return - Lines 4193-4208: _process_instant_exit_queue() - skip if in progress - Lines 3413-3421: check_positions_for_exit() - skip check - Lines 3680-3714: _handle_stop_order_fill() - check persist return - Lines 2260-2320: New _verify_ibkr_position_for_exit() method - Lines 1986-2002: _finalize_position_exit() - check DB persist return - Lines 2205-2225: _persist_position_close_with_exit_details() - return bool - Lines 2170: Fixed COALESCE with 0::numeric - src/earnings/order_executor.py: - Lines 1148-1195: place_instant_exit() - enhanced verification with direction check - Lines 1375-1420: place_scheduled_exit() - enhanced verification with direction check - Acceptance Test: Position exit must be blocked if: - IBKR shows position in opposite direction - DB persist fails (position stays in active_positions) - Same symbol already has exit in progress

24) Trade Data Integrity and Exit Reason Standardization (Finding #116) - Severity: Medium - Status: done - Implementation: Complete (Feb 4, 2026). Fixed three data integrity issues. - Root Cause: Multiple issues: 1. CME showed wrong P&L: Dashboard displayed bug artifact (ID 62, -$19.60) instead of real trade (ID 60, -$3.85) 2. Exit reasons not standardized: EFX/NYT showed "Ledger Reconciliation" when they were actually stop losses (had stop_order_id) 3. Unverified DB writes: External position sync auto-added IBKR positions without verification, causing bug artifacts to pollute strategy data - Fixes: 1. Exclude recovery records from trade log: - Added filter exit_reason NOT LIKE 'manual_recovery:%' to 3 queries - Bug artifacts no longer skew strategy P&L calculations 2. Detect stop_loss in reconciliation: - Added stop_order_id to DB position query - If position had stop_order_id, set exit_reason to 'stop_loss:hardware_reconciled' - Future reconciled stops correctly identified 3. External positions: detection-only: - Removed auto-INSERT for external IBKR positions - Now logs warning and sends alert for manual review - Prevents unverified data from entering database 4. Updated normalize_exit_reason(): - Added 'hardware_stop' pattern matching - 'stop_loss:hardware_reconciled' displays as "Stop Loss (Hardware)" 5. Corrected existing records: - EFX (ID 61) and NYT (ID 59) exit_reason updated to 'stop_loss:hardware_reconciled' - Files: - earnings_dashboard/app.py: Lines 1041, 2927, 3445 (filters), 3353-3358 (normalize) - src/earnings/trading_orchestrator.py: Lines 1007-1027 (query), 1118-1124 (exit reason logic), 1238-1268 (detection-only) - Acceptance Test: - CME shows LONG 1 share, -$3.85, "Scheduled Exit" - EFX/NYT show "Stop Loss (Hardware)" - External positions no longer auto-added to database

25) Add Missing Thread Locks (Finding #117) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Added thread locks for shared state. - Root Cause: active_positions, _pending_batch_entry, and _reserved_capital_for_bmo were accessed without locks, creating potential race conditions when background threads (EarningsDetector callback) accessed shared state simultaneously with main thread. - Fixes: 1. Added three new locks in __init__: - _positions_lock: Guards active_positions dict - _batch_lock: Guards _pending_batch_entry - _capital_lock: Guards capital reservation values 2. Created thread-safe accessor methods: - _get_position(), _set_position(), _remove_position(), _has_position() - _get_all_position_symbols() - _get_reserved_capital(), _set_reserved_capital(), _add_to_reserved_capital() - Files: - src/earnings/trading_orchestrator.py: Lines 251-255 (locks), 321-366 (accessor methods) - Acceptance Test: - No race conditions under concurrent access - Thread-safe methods available for future refactoring

26) Fix Division by Zero Guards (Finding #118) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Added guards to prevent ZeroDivisionError. - Root Cause: Several division operations in risk_manager.py could throw ZeroDivisionError if total_capital, daily_starting_capital, or target values were 0 (e.g., on startup). - Fixes: 1. Line 684: new_exposure_pct = new_exposure / self.total_capital if self.total_capital > 0 else 1.0 2. Line 877: Added elif total_target > 0 before scale-down calculation 3. Line 1052-1053: Added early return if daily_starting_capital <= 0 4. Line 897: Added guard for position percentage display logging - Files: - src/earnings/risk_manager.py: Lines 684, 877, 897, 1052-1053 - Acceptance Test: - No ZeroDivisionError with zero capital - Graceful handling on startup/edge cases

27) Add Missing _send_alert Method (Finding #119) - Severity: Medium - Status: done - Implementation: Complete (Feb 5, 2026). Added _send_alert method to OrderExecutor. - Root Cause: OrderExecutor called self._send_alert() at lines 1058, 1177, 1182, 1187, 1401, 1406, 1411 for critical errors (direction flips, position sync issues) but the method did not exist, causing AttributeError. - Fixes: 1. Added _send_alert(self, message: str) method to OrderExecutor class 2. Method uses fire-and-forget pattern to send CRITICAL level alerts 3. Wraps async AlertManager calls in try/except for safe execution - Files: - src/earnings/order_executor.py: Lines 210-238 (new method) - Acceptance Test: - Critical alerts sent for direction flips - No AttributeError when calling _send_alert

28) Add Entry Verification (Finding #120) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Added IBKR position verification after entry. - Root Cause: After entry orders filled, we tracked position in memory but never verified IBKR actually had the position. If order silently failed, DB/IBKR desync would occur. - Fixes: 1. Added entry_verified and ibkr_position_at_entry fields to ExecutionResult 2. After entry+stop completes, verify IBKR has the position 3. Check direction matches expected (BUY->LONG, SELL->SHORT) 4. Allow 1 share tolerance for fractional share rounding 5. Send alert on direction mismatch - Files: - src/earnings/order_executor.py: - Lines 117-118 (new fields in ExecutionResult) - Lines 139-140 (to_dict updated) - Lines 834-889 (entry verification logic in place_entry_with_stop) - Acceptance Test: - Entry success logs "(verified)" when IBKR position confirmed - Entry logs "(UNVERIFIED)" if position not found in IBKR - Alert sent on direction mismatch

29) Bound Instant Exit Queue (Finding #121) - Severity: Medium - Status: done - Implementation: Complete (Feb 5, 2026). Queue bounded to prevent memory leak. - Root Cause: _instant_exit_queue was unbounded queue.Queue(). If exits were queued faster than processed, queue could grow unbounded causing memory issues. - Fixes: 1. Changed queue.Queue() to queue.Queue(maxsize=100) 2. Changed put() to put_nowait() at both call sites 3. Added try/except queue.Full handling with CRITICAL log 4. Full queue logs message with current queue size - Files: - src/earnings/trading_orchestrator.py: - Line 230 (maxsize=100) - Lines 604-610 (crash recovery put with Full handling) - Lines 3481-3487 (earnings detection put with Full handling) - Acceptance Test: - Queue does not grow beyond 100 items - CRITICAL log when queue full (operational alert)

30) Standardize Error Handling with Result Pattern (Finding #122) - Severity: Medium - Status: done - Implementation: Complete (Feb 5, 2026). Created Result pattern type. - Root Cause: Methods used mixed patterns (None, exceptions, bool) for error handling, making error propagation inconsistent and hard to debug. - Fixes: 1. Created src/earnings/types.py with generic Result[T, E] type 2. Added Ok() and Err() convenience constructors 3. Added is_ok(), is_err(), unwrap(), unwrap_or() methods 4. Documented usage pattern for gradual migration - Files: - src/earnings/types.py (new file) - Acceptance Test: - Result type importable: from src.earnings.types import Result, Ok, Err - New code can use Result instead of None/exceptions - Note: Existing methods not refactored to minimize risk; gradual migration recommended

31) Fix IBKR Connection State Management (Finding #123) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Atomic state transitions. - Root Cause: _connection_state was modified without locks, causing race conditions between heartbeat thread, error handler, and connect/disconnect methods. - Fixes: 1. Added _state_lock for atomic state transitions 2. Created _set_state(), _get_state(), _transition_if() helper methods 3. Updated all state transition sites to use atomic helpers 4. Connected property now uses atomic state access - Files: - src/ibkr/client.py: - Line 212 (_state_lock initialization) - Lines 224-246 (atomic helper methods) - All state transitions updated to use helpers - Acceptance Test: - No race conditions during connection state changes - State machine transitions are atomic

32) Standardize Database Transactions (Finding #124) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Thread-safe engine and transaction helper. - Root Cause: Engine initialization had race condition (check-then-act without lock). Transaction patterns were inconsistent across codebase. - Fixes: 1. Added threading.Lock() for _engine_lock (was None before) 2. get_engine() now uses double-checked locking pattern 3. Added get_transaction() context manager for atomic write operations 4. Documented pattern: use get_transaction() for writes, get_connection() for reads - Files: - src/db/connection.py: - Line 44 (threading import and lock initialization) - Lines 80-138 (thread-safe get_engine with double-checked locking) - Lines 182-200 (new get_transaction helper) - Acceptance Test: - Concurrent engine access is thread-safe - Transactions auto-commit on success, auto-rollback on exception

33) Add Stop Order Verification (Finding #125) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Verify stop in open orders after placement. - Root Cause: After placing stop order, we logged success but didn't verify the order actually appeared in IBKR's open orders list. Rejected orders could go unnoticed. - Fixes: 1. After placing stop, call reqAllOpenOrders() to get current orders 2. Search for our order ID in open trades list 3. Log verification status (verified or warning if not found) 4. Return trade object regardless (caller can check orderId tracking) - Files: - src/earnings/order_executor.py: - Lines 1647-1662 (stop order verification after placement) - Acceptance Test: - Stop order placement logs "✅ Stop order verified in IBKR" - Warning logged if order placed but not found in open orders

34) Fix Earnings Detector Thread Safety (Finding #126) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Lock around monitored positions dict. - Root Cause: _monitored_positions dict was accessed from main thread (add/remove) and monitor thread (_check_all_positions) without synchronization. - Fixes: 1. Added _positions_lock in init 2. add_monitored_position() uses lock when adding 3. remove_monitored_position() uses lock when removing 4. _has_active_positions() takes snapshot under lock 5. _check_all_positions() takes snapshot under lock before iteration - Files: - src/earnings/earnings_detector.py: - Line 150 (_positions_lock initialization) - Lines 757-759 (add with lock) - Lines 766-769 (remove with lock) - Lines 807-809 (snapshot for _has_active_positions) - Lines 821-824 (snapshot for _check_all_positions) - Acceptance Test: - No race conditions when adding/removing positions during monitoring - Iteration uses snapshot to avoid concurrent modification

35) Alert System Retry and Circuit Breaker (Finding #127) - Severity: Medium - Status: done - Implementation: Complete (Feb 5, 2026). Added retry with backoff and circuit breaker. - Root Cause: Alert sending did single attempt, could fail silently during network issues. No protection against alert channel outages flooding with retries. - Fixes: 1. Added CircuitBreaker class with CLOSED/OPEN/HALF_OPEN states 2. Added _retry_with_backoff() helper (3 retries, exponential delay) 3. Per-channel circuit breakers (_telegram_circuit, _webhook_circuit) 4. CRITICAL message deduplication (rate limit same message per 30s) 5. Send method now uses circuit breaker before retry - Files: - src/earnings/alerts.py: - Lines 49-118 (CircuitBreaker class and retry helper) - Lines 377-380 (circuit breaker initialization) - Lines 468-520 (send with circuit breaker and retry) - Acceptance Test: - Failed sends retry with exponential backoff - Circuit opens after 5 consecutive failures - Same CRITICAL message not sent within 30s

36) Stop Order Race Condition Fix (Finding #129) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Lock around stop order placement. - Root Cause: Race between checking self.stop_orders and placing new order. Concurrent threads could both pass check and place duplicate stops. - Fixes: 1. Added _stop_order_lock in init 2. _place_stop_order() acquires lock and delegates to _place_stop_order_locked() 3. All stop order logic runs under lock - Files: - src/earnings/order_executor.py: - Line 209 (_stop_order_lock initialization) - Lines 1557-1562 (lock acquisition and delegation) - Acceptance Test: - Concurrent stop placements for same symbol serialized - No duplicate stop orders

37) Configuration-Driven Thresholds (Finding #130) - Severity: Medium - Status: done - Implementation: Complete (Feb 5, 2026). Centralized config with env overrides. - Root Cause: Magic numbers scattered across codebase without documentation. Changing thresholds required code changes and deployment. - Fixes: 1. Created src/earnings/config.py with all thresholds 2. Each value documented with rationale 3. Environment variable override support 4. Categories: Order execution, limits, risk, IBKR, database, alerts - Files: - src/earnings/config.py (new file) - Acceptance Test: - Config values importable: from src.earnings.config import Config - Override via env: ENTRY_FALLBACK_TIMEOUT=60 - Note: Existing code not refactored to use Config class (gradual migration)

38) Exit Verification Retry with Backoff (Finding #131) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Exit verification retries 3x with backoff. - Root Cause: Single exit verification attempt could fail if IBKR slow to update. Position reported as not flat even when exit actually succeeded. - Fixes: 1. Retry loop: 3 attempts with exponential backoff (3s, 6s, 12s) 2. Breaks early on success (position flat) 3. Logs retry attempts for debugging 4. Final state analysis after all retries 5. Applied to both instant exit and scheduled exit - Files: - src/earnings/order_executor.py: - Lines 1238-1305 (instant exit verification with retry) - Lines 1483-1543 (scheduled exit verification with retry) - Acceptance Test: - Exit verification retries before reporting failure - Logs show retry attempts with delays - Direction flip detection still works after retries

Note: Finding #128 (Parallel Earnings Detection) deferred - requires async refactor

39) Cancel Stop Order Before Exit (Finding #132) - Severity: Critical - Status: done - Implementation: Complete (Feb 5, 2026). Cancel active stop before exit to prevent IBKR rejection. - Root Cause: Instant exit for ARES failed with IBKR rejection. IBKR rejected the SELL order because an active stop-limit order (stop_order_id 103984) was already in place for the same symbol, and IBKR prevents conflicting orders. - Fixes: 1. Added cancel_order() method to IBKRClient for canceling active orders 2. Modified process_instant_exits() to cancel stop order BEFORE placing exit order: - Check if position has stop_order_id - If yes, cancel stop via IBKRClient.cancel_order() - Wait for cancel confirmation (up to 5 seconds) - Then place exit order - If exit fails, restore stop order for protection 3. Added restore_stop_order() helper for re-placing stop if exit fails - Files: - src/ibkr/client.py: Lines 1545-1600 (cancel_order method) - src/earnings/trading_orchestrator.py: Lines 2675-2750 (cancel-before-exit logic) - Acceptance Test: - Instant exit succeeds when stop order is active - Stop order restored if exit order fails - No IBKR rejection for conflicting orders

40) Enhanced Execution Data Capture (Finding #133) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Enhanced get_recent_executions() + order type helper. - Root Cause: Stop-order fills were missing critical data: - Commission not captured from fill.commissionReport - realizedPNL not captured from fill.commissionReport - No way to retrieve order type (STP LMT vs STP) for filled orders - Fixes: 1. Modified get_recent_executions() to include: - commission: From fill.commissionReport.commission - realized_pnl: From fill.commissionReport.realizedPNL 2. Added get_order_type_for_order(order_id) method: - Searches ib.trades() for matching order ID - Returns order.orderType (e.g., 'STP LMT', 'STP', 'LMT') - Used by reconciliation to capture exit order type - Files: - src/ibkr/client.py: - Lines 1450-1468 (get_recent_executions with commission/realizedPNL) - Lines 1515-1545 (get_order_type_for_order helper) - Acceptance Test: - get_recent_executions() returns commission and realized_pnl - get_order_type_for_order() returns correct order type for filled orders

41) Reconciliation Full Data Capture (Finding #134) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Reconciliation populates all exit fields + ibkr_executions. - Root Cause: Stop-order fills discovered via reconciliation were missing data: - exit_order_type NULL in trading_positions - exit_commission NULL in trade_audit_log - total_commission not calculated - Execution not recorded in ibkr_executions table - Fixes: 1. Enhanced reconciliation exit processing to extract all data: - exit_commission, exit_order_id, exit_exec_id, exit_exchange from exit_exec - exit_order_type via get_order_type_for_order() - Calculate total_commission (entry + exit) 2. Update trading_positions with exit_order_type 3. Update trade_audit_log with full exit data: - exit_order_type, exit_order_id, exit_commission - total_commission, gross_pnl, ibkr_exit_exec_id, exit_execution_exchange 4. INSERT execution into ibkr_executions table: - Records all stop-order fills in execution table - ON CONFLICT DO NOTHING prevents duplicates - Source marked as 'reconciliation' - Files: - src/earnings/trading_orchestrator.py: - Lines 1163-1280 (enhanced reconciliation exit processing) - Acceptance Test: - Reconciled exits have exit_order_type populated - Commission captured from IBKR and stored in DB - Stop-order fills appear in ibkr_executions table

42) Correct Slippage for Stop-Limit Orders (Finding #135) - Severity: Medium - Status: done - Implementation: Complete (Feb 5, 2026). Dashboard uses limit_price vs fill_price for STP LMT orders. - Root Cause: Dashboard showed misleading slippage for stop-limit orders: - ARES showed "2.00% slippage" which was actually the NYSE_STOP_LIMIT_BUFFER - Calculation used stop_price (trigger) vs fill_price - For STP LMT, should use exit_limit_price (limit) vs fill_price - Actual slippage was 0% (filled at limit price) - Fixes: 1. Added exit_limit_price to dashboard trade query 2. Enhanced slippage calculation logic: - If exit_order_type contains 'LMT' AND exit_limit_price exists: Use limit_price as reference (our worst acceptable price) - Else if stop_price exists (pure STP order): Use stop_price as reference (trigger price) 3. Slippage = reference_price - fill_price (for LONG) 4. This shows TRUE slippage (worse than our limit) - Files: - earnings_dashboard/app.py: - Line 2923 (added exit_limit_price to query) - Lines 2976-3010 (enhanced slippage calculation) - Acceptance Test: - STP LMT orders show slippage relative to limit price - Fill at limit = 0% slippage (correct) - Fill below limit = negative slippage (filled better than limit)

43) Exit Reason Mislabeling in Reconciliation (Finding #136) - Severity: High - Status: done - Implementation: Complete (Feb 5, 2026). Reconciliation now compares order IDs to determine exit type. - Root Cause: Reconciliation assumed any position with stop_order_id was closed via stop loss. This was wrong when: 1. The stop order was CANCELLED (not filled) 2. The position was closed by a DIFFERENT order (scheduled/instant exit)

 TW, ICE, CI were scheduled limit exits at profit but labeled as "stop_loss:hardware_reconciled"
 because they had stop_order_ids. The stop orders were cancelled after the limit exits filled.

44) ibkr_executions Missing Unique Constraint (Finding #137) - Severity: Medium - Status: done - Implementation: Complete (Feb 5, 2026). Added UNIQUE constraint on exec_id. - Root Cause: Reconciliation INSERT used ON CONFLICT (exec_id) DO NOTHING but the ibkr_executions table only had an INDEX on exec_id, not a UNIQUE constraint. PostgreSQL requires a UNIQUE constraint for ON CONFLICT specification.

 This caused CI reconciliation to fail repeatedly with:
 "there is no unique or exclusion constraint matching the ON CONFLICT specification"

Phase 3D: Critical Bug Fixes (Feb 7, 2026 Holistic Audit)

Numbering trap (docs-sweep, Sep 26 2026): list items 45–50 below reused Finding #132–#137, which were already assigned on Feb 5 (list items 39–44). Those Feb 5 entries remain PRIMARY. These Feb 7 bugs are aliases of the reused numbers, not new IDs — cite the line (list 45–50) when referring to them. The dashboard parser first-write-wins on (Finding #N), so the Testing Findings tab shows the Feb 5 titles for #132–#137.

45) fixes_applied NameError in reconciliation (Finding #132 reused Feb 7 — PRIMARY is list 39 / Feb 5 Cancel Stop Order Before Exit) - Severity: Critical - Status: done - Implementation: Complete (Feb 7, 2026). Initialized fixes_applied = [] at line 1133 alongside issues_detected. - Root Cause: Variable referenced at line 1340 (execution ledger auto-close) before initialization at line 1377. - Files: src/earnings/trading_orchestrator.py

46) cancelled_stop_info undefined in scheduled exit (Finding #133 reused Feb 7 — PRIMARY is list 40 / Feb 5 Enhanced Execution Data Capture) - Severity: Critical - Status: done - Implementation: Complete (Feb 7, 2026). Added cancelled_stop_info = None initialization in place_scheduled_exit(). - Root Cause: Variable only defined in place_instant_exit() but referenced in place_scheduled_exit() stop restore logic. - Files: src/earnings/order_executor.py

47) Direct active_positions access bypasses thread locks (Finding #134 reused Feb 7 — PRIMARY is list 41 / Feb 5 Reconciliation Full Data Capture) - Severity: Critical - Status: done - Implementation: Complete (Feb 7, 2026). Replaced all 10+ direct self.active_positions[symbol] accesses with thread-safe wrappers (_get_position, _set_position, _remove_position). All iteration uses with self._positions_lock: snapshots. - Root Cause: Finding #117 added locks and wrappers but many call sites still used direct dict access. - Files: src/earnings/trading_orchestrator.py (12 locations fixed)

48) Division by zero in batch sizing (Finding #135 reused Feb 7 — PRIMARY is list 42 / Feb 5 Correct Slippage for Stop-Limit Orders) - Severity: Critical - Status: done - Implementation: Complete (Feb 7, 2026). Added if total_weight <= 0: return {} guard. - Root Cause: proportion = weights[symbol] / total_weight crashes if all signal weights are 0. - Files: src/earnings/batch_order_manager.py

49) State desync - position removed before DB commit verified (Finding #136 reused Feb 7 — PRIMARY is list 43 / Feb 5 Exit Reason Mislabeling in Reconciliation) - Severity: Critical - Status: done - Implementation: Complete (Feb 7, 2026). Wrapped cleanup steps (remove position, cancel stops, close in risk manager) in try/except. Sends CRITICAL alert if cleanup fails after DB persist. - Root Cause: If _persist_ibkr_execution() threw exception, position was removed from memory but execution not recorded. - Current state (#408): live persist is PositionManager.persist_ibkr_execution (fail-open). Orchestrator _persist_ibkr_execution deleted. Historical root-cause name above is the then-live twin. - Files: src/earnings/trading_orchestrator.py

50) Queue full drops instant exits silently (Finding #137 reused Feb 7 — PRIMARY is list 44 / Feb 5 ibkr_executions Missing Unique Constraint) - Severity: Critical - Status: done - Implementation: Complete (Feb 7, 2026). Added CRITICAL alert via _send_alert_async() when queue is full. - Root Cause: Queue was bounded (#121) but only logged when full, no alert sent. Positions left unprotected. - Files: src/earnings/trading_orchestrator.py

51) Execution ledger 48h lookback too short (Finding #138) - Severity: Critical - Status: done - Implementation: Complete (Feb 7, 2026). Extended find_exit_execution() lookback from 48h to 168h (7 days). - Root Cause: CBOE/MCHP incident (Feb 6, 2026) - stop fills unrecoverable if bot was down >48h. - Files: src/ibkr/client.py

52) Positions unmonitored during IBKR disconnection (Finding #139) - Severity: Critical - Status: done - Implementation: Complete (Feb 7, 2026). Added reconnection callback system to IBKRClient. TradingOrchestrator registers _on_ibkr_reconnect() which triggers immediate reconciliation after IBKR reconnects. - Root Cause: After IBKR reconnection, no reconciliation was triggered. Positions closed by stops during downtime went undetected until next scheduled reconciliation (5 min). - Files: src/ibkr/client.py, src/earnings/trading_orchestrator.py

Phase 3E: High Priority Hardening (Feb 7, 2026 Holistic Audit)

53) Missed scheduled exits on restart (Finding #140) - Severity: High - Status: done - Implementation: Added _check_missed_scheduled_exits() on startup. Checks all open positions for scheduled_exit_time < now and executes immediately with alert. - Current state (#408): live startup callee is ExitManager.check_missed_scheduled_exits (from orchestrator __init__ after _load_persisted_positions). Orchestrator _check_missed_scheduled_exits deleted. Historical implementation name above is the then-live twin. - Files: src/earnings/trading_orchestrator.py

54) _exit_in_progress never cleared on failure (Finding #141) - Severity: High - Status: done - Implementation: Added _exit_in_progress_times dict tracking timestamps. Auto-clears stale entries after 5 minutes in check_scheduled_exits(). Prevents permanent blocking from transient failures. - Files: src/earnings/trading_orchestrator.py

55) Stop order tracking outside lock (Finding #142) - Severity: High - Status: done - Implementation: Moved self.stop_orders[symbol] = stop_order_id inside _stop_order_lock. - Files: src/earnings/order_executor.py

56) Hardcoded market holidays expire 2027 (Finding #144) - Severity: High - Status: done - Implementation: Added 2027-2028 US market holidays and early close days. - Files: src/earnings/market_calendar.py

57) Audit log files can fill disk (Finding #145) - Severity: High - Status: done - Implementation: Uses single rotating file audit_log.jsonl. Rotates at 50MB with 1 backup. - Files: src/earnings/audit_logger.py

58) Config values lack bounds validation (Finding #146) - Severity: High - Status: done - Implementation: Added Config.validate() classmethod. Checks percentage fields (0-1 range) and integer fields (reasonable bounds). Call on startup. - Files: src/earnings/config.py

59) DB connection pool timeout already configured (Finding #149) - Severity: High - Status: done (already implemented) - POOL_CONFIG['pool_timeout'] = 30 already exists in src/db/connection.py.

60) Signal generator DB queries unhandled (Finding #150) - Severity: High - Status: done - Implementation: Wrapped _get_earnings_markets_from_db() query in try/except, returns empty list on failure. - Files: src/earnings/signal_generator.py

61) get_previous/next_trading_day() returns wrong date (Finding #151) - Severity: High - Status: done - Implementation: Extended search window from 10 to 30 days. Handles holiday clusters. - Files: src/earnings/market_calendar.py

62) Earnings timing fallback hardcoded path (Finding #152) - Severity: High - Status: done - Implementation: Uses os.path.dirname(__file__) relative path with env var override EARNINGS_TIMING_CACHE_PATH. - Files: src/earnings/earnings_timing_fallback.py

63) Missing alert when price sources fail (Finding #154) - Severity: High - Status: done - Implementation: Sends warning alert when both IBKR and yfinance fail for a symbol. - Files: src/earnings/trading_orchestrator.py

64) No startup safe mode (Finding #155) - Severity: High - Status: done - Implementation: Added _reconciliation_completed flag. execute_entry() blocks until first reconciliation succeeds. Prevents new entries during inconsistent state. - Files: src/earnings/trading_orchestrator.py

65) Deferred to Phase 4A: P&L calculation dedup (Finding #143) - Severity: High - Status: deferred - Reason: Requires refactoring trading_orchestrator.py (Phase 4A scope)

66) Deferred to Phase 4A: N+1 query in conviction history (Finding #147) - Severity: High - Status: deferred - Reason: Performance optimization, not a crash risk

67) Deferred to Phase 4A: Blocking IBKR market data calls (Finding #148) - Severity: High - Status: deferred - Reason: Requires async refactor

68) Deferred to Phase 4A: Split place_instant_exit() (Finding #153) - Severity: High - Status: deferred - Reason: 436-line function needs careful extraction during orchestrator split

Phase 4A: Split Trading Orchestrator (Feb 7, 2026)

69) Split TradingOrchestrator into 6 focused modules - Severity: Architecture - Status: done - Implementation: Complete (Feb 7, 2026). Split 4878-line monolith into 6 modules using composition with full dependency injection. Each manager receives only its specific dependencies via init. - New modules created (3,326 total lines): - trading_utils.py (464 lines): 18 utility methods (market time, pricing, scheduling) - position_manager.py (815 lines): 14 methods (position CRUD, DB persistence, stop orders) - exit_manager.py (792 lines): 13 methods (scheduled/instant/batch exits, stop backup) - capital_manager.py (671 lines): 14 methods (capital reservation, signals, entries) - reconciliation_manager.py (586 lines): 3 methods (IBKR/DB reconciliation, table sync) - TradingOrchestrator updated: Creates all managers in init, delegates to them. Original methods remain for backward compatibility. External callers unchanged. - Files: src/earnings/trading_utils.py, src/earnings/position_manager.py, src/earnings/exit_manager.py, src/earnings/capital_manager.py, src/earnings/reconciliation_manager.py, src/earnings/trading_orchestrator.py, src/earnings/__init__.py - Note: Method location references in earlier findings (e.g., #14, #17, #18, #19, #23, #25, #39, #41, #43) still reference trading_orchestrator.py. The methods now exist in both the original file AND the new manager modules (identical code). The original methods will be removed once gradual migration to managers is complete.

Phase 4B+4C: IBKR as Source of Truth (Feb 7, 2026)

70) IBKR as source of truth for live position state - Severity: Architecture - Status: done - Implementation: Complete (Feb 7, 2026). IBKR is now the definitive source for live position state. Database remains source of truth for historical data. - New components: - position_state.py: PositionState enum (PENDING/OPEN/CLOSING/CLOSED/FAILED) with state machine validation and transition rules - ibkr_position_cache.py: Thread-safe TTL cache (5s) wrapping ib.positions() with disconnection detection - Changes: - position_manager.py: Added IBKR cache integration methods - trading_orchestrator.py: Main loop blocks ALL trading when IBKR disconnected - memory.md: Principle #2 updated to reflect IBKR as source of truth - DB migration 018: Added position_state column to trading_positions - Key behavior: When IBKR disconnected, ALL trading operations blocked. Hardware stops remain active in IBKR regardless.

Phase 4D: Event-Driven Architecture (Feb 7, 2026)

71) Event-Driven Architecture with real-time stop fill detection - Severity: Architecture - Status: done - Implementation: Complete (Feb 7, 2026). Created EventBus for decoupled communication and IBKR fill callbacks for real-time stop order fill detection. - New components: - event_bus.py: Thread-safe publish/subscribe with 20 event types, JSONL persistence - ibkr/client.py: Added on_fill() callback + _on_order_status() handler - trading_orchestrator.py: Added _on_ibkr_fill() handler for stop fill detection - Key achievement: Solves CBOE/MCHP problem. Stop fills now detected in real-time via ib_insync orderStatusEvent, rather than relying on periodic reconciliation. - Events published: ORDER_FILLED, STOP_FILLED (triggers position cleanup), IBKR_DISCONNECTED, IBKR_RECONNECTED, etc.

Phase 4E: Medium Priority Fixes (Feb 7, 2026)

72) 20 medium priority fixes (#156-#175) - Severity: Medium - Status: done - Implementation: Complete (Feb 7, 2026). All 20 findings from the holistic audit addressed. - Concurrency (#156-#159): - #156: Lock ordering documented (positions -> capital -> batch) - #157: Capital lock already consistent in capital_manager.py (verified) - #158: Added _positions_lock to risk_manager.open_positions - #159: Circuit breaker state reset on restart (documented as intentional) - Error Handling (#160-#164): - #160: Added debug logging to bare except clauses - #161: cancel_order() now polls for cancellation verification (3s) - #162: Already guarded (total_depth > 0 check exists) - #163: Override JSON validated on load (structure + timing values) - #164: dispose_engine() wrapped in _engine_lock - Config (#165-#168): - #165: earnings_datetime timezone validated (auto-converts naive to ET) - #166: Polling intervals configurable via env vars - #167: Already done in Phase 3E (2027-2028 holidays) - #168: Trading block file uses atomic remove (no TOCTOU race) - Monitoring (#169-#172): - #169: Alert delivery confirmation logged (Telegram success/failure) - #170: Daily position summary at 8 PM ET - #171: Already exists (/api/health endpoint in dashboard) - #172: Reconciliation abort alert failure now logged - Code Quality (#173-#175): - #173: No obsolete commented-out code found - #174: Dead method _is_earnings_announcement() removed - #175: Strategy doc already documents 10x vs 2x/6x leverage correctly

Feb 9, 2026 Critical Fixes

76) IBKR connectivity loss auto-recovery (Finding #176) - Severity: Critical - Status: done - Root Cause: Error 1100 (connectivity lost) was in CRITICAL_BLOCK_ERRORS, persisting trading_block.txt that survived restarts. Errors 1101/1102 (connectivity RESTORED) were treated as disconnections instead of recovery events. - Impact: Bot blocked for 33+ hours (Feb 8-9) until manual file deletion. - Fixes: 1. Removed 1100 from CRITICAL_BLOCK_ERRORS (transient, not critical) 2. Errors 1101/1102 now clear trading block, verify health, restart heartbeat 3. Health check auto-clears connection-related blocks on success - Files: src/ibkr/client.py

77) Stale earnings dates in DB (Finding #177) - Severity: Critical - Status: done - Root Cause: earnings_markets table populated by sync job every ~6h. Companies shift earnings dates but DB doesn't re-verify. ON had Feb 10 in DB, actual was Feb 9. - Impact: ON trade would have been missed (signal generator uses DB date). - Fixes: 1. Added verify_earnings_dates() to signal_generator.py - cross-checks DB vs EarningsCalendar before each signal check (AMC 10:30 AM, BMO 3:50 PM) 2. Auto-corrects mismatches and sends Telegram alert 3. Fixed 5 stale dates: ON, CRSP, NET, GRAB, PANW - Files: src/earnings/signal_generator.py, src/earnings/trading_orchestrator.py

78) Duplicate order from market fallback (Finding #178) - Severity: Critical - Status: done - Root Cause: batch_order_manager._apply_market_fallback accessed result.filled_price but OrderResult has avg_fill_price. AttributeError caused batch to think order failed, triggering market fallback = duplicate (ON: 6 shares instead of 3). - Same pattern as PFE (#17) and CME (#23): retry/fallback without checking IBKR position. - Fixes: 1. Fixed filled_price -> avg_fill_price attribute bug 2. Added IBKR position guard (_check_ibkr_position) before EVERY market fallback: - batch_order_manager._apply_market_fallback - order_executor.place_guaranteed_fill If position already exists, ABORT the fallback. Defense-in-depth. 3. Manual recovery: sold 3 excess ON shares, placed stop loss. (Corrected #353, Jul 31 2026: the original entry claimed the recovery legs were "recorded in DB" — they never were. The excess-share legs exist only in IBKR; the DB holds the corrected strategy trade. Now represented as an UNQUANTIFIED placeholder row in the trading_errors ledger — finding '#178', realized_loss 0.00, quantifiable from IBKR Flex on demand.) - Files: src/earnings/batch_order_manager.py, src/earnings/order_executor.py

79) Earnings detector false exit from PRNewswire wrong EPS (Finding #179A) - Severity: Critical - Status: done - Root Cause: Two bugs in earnings_detector.py: 1. Polymarket confirmation state was popped after confirmation (line 322), creating 30-second vulnerability windows where secondary sources could override. 2. No contradiction guard: PRNewswire parsed wrong EPS ($0.06 vs actual $0.64), concluded MISS, overrode Polymarket's 99.95% BEAT signal. - Impact: ON position exited prematurely (still profitable, but missed further gains). - Fixes: 1. Keep Polymarket confirmation state (don't pop after confirmation) 2. PRNewswire/BusinessWire that contradict confirmed Polymarket outcome are logged but ignored 3. Yahoo (authoritative) can still override if needed - Files: src/earnings/earnings_detector.py

80) NASDAQ stop orders don't trigger in extended hours (Finding #179B) - Severity: Critical - Status: done - Root Cause: IBKR ignores outsideRth on pure STP orders for NASDAQ stocks routed via SMART (Warning 2109). Every NASDAQ STP in our history got 2109. Stops only trigger during RTH (9:30 AM - 4:00 PM ET). - Impact: HOOD dropped from $87.83 to $78.00 after hours (-11.2%), but stop at $85.20 (STP) never triggered. (Clarified #353, Jul 31 2026: the -11.2% was the UNREALIZED trough while the stop was blocked; the REALIZED loss on the closed trade was -$5.26 = -3.0%, i.e. the design stop distance — price recovered before the exit filled. The original "Actual loss: -11.2%" wording conflated the two.) - Evidence: CVS, KO, NET, RDDT, RL stops (all STP LMT) triggered correctly in pre/post market. HOOD, DDOG, MAR (all STP) got Warning 2109. - Fix: ALL exchanges now use STP LMT with 2% buffer (not just NYSE). Removes the is_nyse conditional entirely. Same 2% buffer approach that was already proven to work for NYSE. - Files: src/earnings/order_executor.py, src/ibkr/client.py - Docs updated: STRATEGY.md (sections 5.5, 7.3, 7.17), EDGE_CASES.md, OPERATIONS.md, TESTING_FINDINGS_PENDING.md

81) IBKR reconnection failures + zero alerts + stale dashboard (Finding #180) - Severity: Critical - Status: done - Root Cause: Three failures from the same pattern (3 incidents in 5 days, Feb 8-12): 1. Gateway daily restart drops bot connection. Reconnection loop retries but gateway keeps rejecting clientId=1 (stale session state, "remove Client 1"). 2. No Telegram alerts during 37h outage: alerts only sent from orchestrator main loop, which never starts when initial connection fails. 3. Dashboard shows "IBKR Connected" from stale snapshot file (no staleness check). - Impact: 37 hours downtime, missed trading signals for Feb 11-12 earnings. - Fixes: 1. Send Telegram alert immediately when reconnection starts + every 30 min (from IBKRClient._schedule_reconnect, not dependent on main loop) 2. Clean disconnect() before each reconnect attempt (clears stale clientId) 3. Dashboard checks snapshot age > 2 min → show "DISCONNECTED" 4. ibgateway-ibc.service: Restart=on-failure → Restart=always (gateway exits with code 0 on daily restart, on-failure didn't restart it) - Files: src/ibkr/client.py, earnings_dashboard/app.py, deploy/systemd/ibgateway-ibc.service (server updated directly)

82) IBKR weekly 2FA schedule drift hardening (Finding #181) - Severity: Critical - Status: done - Implementation: Complete (Feb 15, 2026). Added single source-of-truth schedule + drift audit and aligned all operational scripts. - Root Cause: Timing/service drift across scripts/docs caused missed 2FA expectation window and inconsistent operational behavior. - Fixes: 1. Added canonical schedule file scripts/ibkr_schedule.env (restart/reminder/manager times) 2. Updated gateway_restart_manager.sh and ibkr_restart_warning.sh to read canonical schedule values 3. Added missing sunday_2fa_reminder.sh with Sunday guardrails 4. Added ibkr_schedule_audit.sh to detect cron/service drift daily 5. Updated diagnostics and deploy flow to verify ibgateway-ibc.service and canonical cron entries 6. Updated continuity docs with canonical schedule source and audit cron - Files: - scripts/ibkr_schedule.env - scripts/gateway_restart_manager.sh - scripts/ibkr_restart_warning.sh - scripts/sunday_2fa_reminder.sh - scripts/ibkr_schedule_audit.sh - scripts/ibkr_diagnostics.py - deploy_ibkr_monitoring.sh - docs/IBKR_CONTINUITY.md

83) Holistic hardening batch: trading/reconciliation/ops/dashboard consistency (Finding #182) - Severity: Critical - Status: done - Implementation: Complete (Feb 15, 2026). Added guardrails against race conditions, strengthened IBKR ops readiness checks, and standardized dashboard trade-quality filtering. - Root Cause: Multiple cross-component assumptions allowed partial false-positive health states, fragile restart timing, and inconsistent reporting filters. - Fixes: 1. Exit race hardening in order_executor.py: - Skip stop restore when IBKR position is already flat - Re-check live position immediately before market fallback order - Handle disconnect during exit verification retries 2. Reconciliation guardrails in reconciliation_manager.py: - Auto-close only when execution side/shares/time invariants pass - Closed-table consistency checks (counts + NULL exit price detection) 3. Capital estimate safety in capital_manager.py: - Use current-price estimate (fallback to entry notional) 4. Ops hardening: - ibkr_health_monitor.sh now includes login-pending detection, command fallbacks, durable state file - gateway_restart_manager.sh uses bounded polling window with PID kill verification - ibkr_schedule_audit.sh validates cron with normalized matching and checks IBC AutoRestartTime 5. Dashboard consistency: - Unified filter for reconciliation/manual recovery/external trades - Explicit COALESCE totals and exit_price IS NOT NULL guard in closed-trade summary - Files: - src/earnings/order_executor.py - src/earnings/exit_manager.py - src/earnings/reconciliation_manager.py - src/earnings/capital_manager.py - src/ibkr/client.py - scripts/ibkr_health_monitor.sh - scripts/gateway_restart_manager.sh - scripts/ibkr_schedule_audit.sh - scripts/ibkr_diagnostics.py - deploy_ibkr_monitoring.sh - earnings_dashboard/app.py - docs/OPERATIONS.md - docs/IBKR_CONTINUITY.md - docs/STRATEGY.md - docs/TESTING_FINDINGS_PENDING.md

84) Health monitor false-unhealthy correction after deploy (Finding #183) - Severity: High - Status: done - Implementation: Complete (Feb 15, 2026). Fixed gateway-running detection to use ibgateway-ibc.service status as primary signal. - Root Cause: Process regex was too strict and marked Gateway=false while API was live, causing false unhealthy state. - Fix: 1. ibkr_health_monitor.sh now checks systemctl is-active ibgateway-ibc.service first 2. Falls back to broader process patterns only if service check unavailable - Files: - scripts/ibkr_health_monitor.sh

85) Reconciliation missing exec_id hardening + one-time repair (Finding #184) - Severity: Critical - Status: done - Implementation: Complete (Feb 15, 2026). Hardened stop-fill persistence and added deterministic log-based reconciliation repair path. - Root Cause: 1. Hardware stop-fill cleanup path could close trades without persisting ibkr_exit_exec_id / exit_order_id 2. Reconciliation auto-fix only used ibkr_executions, so historical gaps remained unresolved 3. Delayed IBKR execution history retrieval was not reliable for historical repair in this environment - Fixes: 1. exit_manager.py now captures recent stop-fill execution payload from IBKR in-memory trades and persists: - exit_exec_id, exit_order_id, exit_execution_exchange, exit_commission - matching ibkr_executions row (source=live_stop_fill) 2. earnings_detector.py now supports stop_monitoring(symbol) (symbol-scoped remove), preventing runtime cleanup exceptions 3. trade_reconciler.py adds log fallback parser for fill events in earnings_trader.log - inserts deterministic synthetic IDs (logfill.<symbol>.<order_id>.<epoch>) into ibkr_executions - backfills ibkr_entry_exec_id / ibkr_exit_exec_id when DB-only matching fails 4. Added one-time repair utility: scripts/repair_trade_exec_mismatches.py - Files: - src/earnings/exit_manager.py - src/earnings/earnings_detector.py - src/data_tracker/collectors/trade_reconciler.py - scripts/repair_trade_exec_mismatches.py - tests/test_reconciliation_and_orders.py - docs/STRATEGY.md - docs/TESTING_FINDINGS_PENDING.md

85b) Reconciliation missing_exec_id hardening + deterministic backfill (Finding #184; duplicate list index of 85 — same finding, not a new number) - Severity: Critical - Status: done - Implementation: Complete (Feb 15, 2026). Hardened stop-fill persistence path and added deterministic reconciliation backfill for legacy gaps. - Root Cause: 1. Hardware stop cleanup could close DB rows without durable ibkr_exit_exec_id / exit_order_id 2. Cleanup path called stop_monitoring(symbol) against a method that accepted no symbol, throwing post-fill errors 3. Reconciliation auto-fix searched only ibkr_executions, so historical gaps remained unresolved when execution rows were never persisted - Fixes: 1. exit_manager.py now captures recent stop-fill details from IBKR in-memory trades and persists: - exit_exec_id, exit_order_id, exit_execution_exchange, exit_commission - matching ibkr_executions row (source=live_stop_fill) 2. earnings_detector.py now supports stop_monitoring(symbol) as symbol-scoped removal (backward-compatible with full stop) 3. trade_reconciler.py now has log-based deterministic fallback: - Parses fill lines from /var/log/polymarket/earnings_trader.log - Generates stable synthetic IDs (logfill.<symbol>.<order_id>.<epoch>) - Inserts missing ibkr_executions rows and links trade exec IDs idempotently 4. Added one-time repair utility scripts/repair_trade_exec_mismatches.py - Files: - src/earnings/exit_manager.py - src/earnings/earnings_detector.py - src/data_tracker/collectors/trade_reconciler.py - scripts/repair_trade_exec_mismatches.py - tests/test_reconciliation_and_orders.py - docs/STRATEGY.md - docs/TESTING_FINDINGS_PENDING.md

86) Unified NYSE/NASDAQ execution policy + stop trigger timeout escalation (Finding #185) - Severity: Critical - Status: done - Implementation: Complete (Feb 15, 2026). Removed exchange-specific execution branching, normalized priority scoring to cap+confidence, and added stop-trigger timeout escalation. - Root Cause: 1. Legacy NYSE/NASDAQ branching made behavior and docs diverge over time. 2. Hardware stop could remain active after breach without quick fill, leaving unresolved downside window. 3. Priority scoring encoded exchange bias that no longer matched hardened execution policy. - Fixes: 1. Unified instant-exit logic in order_executor.py: - all exchanges use LIMIT (GTC, 1%) -> MARKET fallback when market is open - fully closed market remains GTC LIMIT only 2. Added stop-trigger timeout escalation in exit_manager.py: - tracks first breach timestamp - escalates to instant_exit:stop_timeout_escalation after configurable timeout 3. Normalized priority.py scoring to market cap + confidence only (exchange-independent) 4. Updated dashboard priority legends/order matrices and strategy/operations/edge-case docs for consistency - Files: - src/earnings/order_executor.py - src/earnings/exit_manager.py - src/earnings/config.py - src/earnings/priority.py - src/earnings/signal_generator.py - earnings_dashboard/templates/dashboard.html - tests/test_reconciliation_and_orders.py - tests/unit/test_order_executor_stops.py - tests/comprehensive_system_test.py - docs/STRATEGY.md - docs/OPERATIONS.md - docs/EDGE_CASES.md - docs/TESTING_FINDINGS_PENDING.md

87) 2x sizing intent alignment: full testing profile + 140% exposure cap (Finding #191) - Severity: High - Status: done - Implementation: Complete (Feb 23, 2026). Updated sizing logic to match requested behavior: double current testing profile without ambiguity, while retaining leverage guardrails. - Root Cause: 1. User intent ("2x sizing vs current testing behavior") differed from prior implementation semantics. 2. Existing testing_prod sizing multiplier remained at 50% of base Option-B sizing. 3. Exposure budget defaults and opportunity tiers were still tuned around a 70% ceiling. - Fixes: 1. Config.TESTING_PROD_SIZE_MULTIPLIER introduced and set to default 1.0 (from prior hardcoded 0.50). 2. Exposure defaults updated to 1.40: - MAX_DAILY_EXPOSURE=1.40 - MAX_TOTAL_EXPOSURE_PCT=1.40 3. Opportunity exposure tiers doubled proportionally in RiskManager: - rare 0.60, normal 1.40, busy 1.40 4. Kept leverage cap unchanged (LEVERAGE_MAX_EFFECTIVE_LEVERAGE=2.0). 5. Dashboard now reflects configured exposure cap dynamically (max_exposure_pct=140.0). 6. Added unit coverage for new sizing/exposure behavior. - Verification Evidence (production): 1. Runtime check after deploy: - TESTING_PROD_SIZE_MULTIPLIER 1.0 - MAX_DAILY_EXPOSURE 1.4 - LEVERAGE_MAX_EFFECTIVE_LEVERAGE 2.0 - large-cap size on $10k in testing_prod => $2,500 - exposure tiers => rare=0.6, normal=1.4, busy=1.4 2. /api/state shows max_exposure_pct=140.0 3. Live HIMS row confirms leverage snapshot persistence remains intact (trade_audit_log.id=65) - Files: - src/earnings/config.py - src/earnings/risk_manager.py - earnings_dashboard/app.py - tests/unit/test_config.py - tests/unit/test_position_sizing_2x_rollout.py - docs/TESTING_FINDINGS_PENDING.md

88) Close persistence permanence hardening after AS recurrence (Finding #193) - Severity: Critical - Status: done - Implementation: Complete (Feb 24, 2026). Eliminated nullable-parameter type ambiguity and added fallback close persistence path. - Root Cause: 1. pg8000 raised 42P18 ("could not determine data type of parameter") during rich close update when trade_audit_log.notes append path received nullable values. 2. The failed DB transaction left broker-closed positions marked open in DB, creating repeated orphan reconciliation alerts. - Fixes: 1. Reworked close-note update to use type-safe, non-null append semantics in persist_position_close_with_exit_details(). 2. Added fallback close persistence path that writes essential close fields to both trading_positions and trade_audit_log if the rich update fails. 3. Added high-confidence reconciliation auto-close override to safely resolve strict stop-fill matches even when global auto-close is disabled. 4. Added closed-trade metadata completeness check for exit_order_type, exit_order_id, and ibkr_exit_exec_id. - Files: - src/earnings/position_manager.py - src/earnings/reconciliation_manager.py - src/earnings/trading_orchestrator.py - src/earnings/config.py - earnings_dashboard/app.py - docs/OPERATIONS.md - docs/TESTING_FINDINGS_PENDING.md

89) Upcoming earnings dashboard live-visibility hardening (Finding #195) 90) IBKR reconnect/reconciliation holistic hardening (Finding #196) - Root cause: Reconnect callback race: reconciliation observed reconnect_in_progress=true during expected transition and raised critical position-sync noise for guard skips. - Fixes: - Clear reconnect flags before firing callbacks in IBKRClient. - Reclassify fail-closed guard skips to warning reconciliation_guard category. - Add bounded retry-on-ready after guard skip. - Add guard alert cooldown and watchdog thread-dump/stage forensics. - Verification: - Unit test: reconnect flags are idle before callback executes. - Unit test: guard retry scheduler is bounded + deduped. - Unit test: disconnected-guard path emits warning guard alert. - Impacted files: - src/ibkr/client.py - src/earnings/reconciliation_manager.py - src/earnings/alerts.py - src/earnings/trading_orchestrator.py - src/earnings/config.py - tests/unit/test_ibkr_error_block_policy.py - tests/unit/test_reconciliation_guard_retry.py - tests/unit/test_config.py

91) Conviction robustness + unified close/reconciliation recovery (Finding #197) - Severity: Critical - Status: done - Implementation: Complete (Feb 15, 2026). Added robust conviction sampling with reliability gates, unified close persistence routing, and broadened high-confidence reconciliation auto-close for broker-confirmed exits. - Root Cause: 1. Conviction fallback could rely on a single outlier price snapshot in volatile windows, producing wrong direction. 2. Reconciliation high-confidence override was stop-order-centric, leaving recoverable non-stop exits stuck in EXEC_LEDGER_ONLY loops. 3. Trading orchestrator retained a legacy close persistence implementation separate from hardened PositionManager path. - Fixes: 1. conviction_calculator.py: robust price-window median estimator with sample/range reliability checks and reason codes. (Refined in Finding #325, Jun 11 2026: the reliability gate changed from min-max range>0.35 to robust IQR>0.25, the dense-sample cap raised from 9→10000 snapshots, and the 0.02<yes_price<0.99 filter removed — the old range+filter under-counted high-conviction markets and let an excluded ADBE LONG through.) 2. signal_generator.py: fail-closed skip for unreliable conviction windows with reason logging. 3. trading_orchestrator.py: close persistence routed to position_manager.persist_position_close_with_exit_details(...). 4. reconciliation_manager.py: high-confidence execution-ledger override generalized for non-stop exits under strict guardrails; execution-ledger close now uses hardened position-manager close path. 5. Added targeted tests for conviction robustness, reliability gating, reconciliation high-confidence criteria, and close-path routing. - Verification: - pytest -q tests/unit/test_conviction_robustness.py tests/unit/test_reconciliation_guard_retry.py tests/unit/test_close_path_routing.py - pytest -q tests/unit/test_signal_reliability_gate.py - All tests passed; targeted lints clean. - Production evidence (Feb 26, 2026): - EXEC_LEDGER_ONLY for SNOW eliminated (was recurring every 5 min pre-deploy) - Reconciliation metadata complete: SNOW#75 + ICUI#62 repaired with full exit linkage - Reconciliation all-clear: "DB and IBKR are in sync" on every post-deploy cycle - Conviction robustness log evidence: blocked (no entry candidates since deploy) - Follow-up fix (commit 6cb8830): - Alert deduplication for CLOSED TRADE EXEC METADATA INCOMPLETE check (1h cooldown, row-ID tracking) - SNOW data repair (Feb 26, 2026): - Corrected exit from stop_loss@$170.85 to instant_exit@$166.45 (42P18 caused original persist failure) - PnL corrected from -$9.96 to -$1.16 - position_state fixed from 'open' to 'closed' - Files: - src/earnings/conviction_calculator.py - src/earnings/signal_generator.py - src/earnings/reconciliation_manager.py - src/earnings/trading_orchestrator.py - tests/unit/test_conviction_robustness.py - tests/unit/test_signal_reliability_gate.py - tests/unit/test_reconciliation_guard_retry.py - tests/unit/test_close_path_routing.py - docs/OPERATIONS.md - docs/STRATEGY.md - docs/TESTING_FINDINGS_PENDING.md

92) NaT EPS crash loop in Yahoo earnings check (Finding #198) - Severity: Critical - Status: done - Implementation: Complete (Feb 27, 2026). Fixed crash loop from pandas NaT value in Yahoo earnings data. - Root Cause: 1. Yahoo Finance returned pandas NaT for EB (Eventbrite) reported_eps field. 2. _check_yahoo_earnings() set result.is_released = True BEFORE float(reported_eps). 3. float(NaT) threw TypeError, caught by except, but is_released was already True with actual_eps=None. 4. Caller check_earnings() crashed on f"${yahoo_result.actual_eps:.2f}" — None has no __format__. 5. Unhandled in main loop → exit → systemd restart → 69+ restarts over 20 hours. - Fixes: 1. Wrapped float(reported_eps) in try/except; set is_released AFTER successful conversion. 2. Same guard on eps_estimate float conversion. 3. Guarded ALL f-string EPS formatting across file (8 instances, memory.md Section E same-pattern check). - Impact: 3 open positions (DELL, REAL, EB) unmonitored for ~20 hours. Hardware stops remained active in IBKR. - Files: src/earnings/earnings_detector.py

93) Direction-inverted duplicate prevention blocks scheduled exit fallback (Finding #199) - Severity: Critical - Status: done - Implementation: Complete (Mar 6, 2026). Made market-fallback duplicate check direction-aware and fixed IBKR market hours parser. - Root Cause: 1. place_guaranteed_fill() duplicate check at line 680 used abs(p.shares) which ignores position direction. 2. For EXIT orders (BUY to cover SHORT), finding shares means the exit DIDN'T work — but the check concluded "likely filled" and skipped the market fallback. 3. GPRO SHORT -397 shares: limit BUY at $0.77 didn't fill in 45s. Market fallback was blocked by inverted duplicate check. Position left open +$111 unprotected over weekend. 4. Secondary: IBKR market hours parser crashed on YYYYMMDD:HHMM format (int("0306:1600")), blocking all post-failure retry attempts with "Market not in regular hours." - Fixes: 1. order_executor.py: Duplicate check now compares p.shares sign against side. BUY+LONG=duplicate, BUY+SHORT=exit-not-done (proceed). SELL+SHORT=duplicate, SELL+LONG=exit-not-done (proceed). 2. client.py: _parse_ibkr_hours strips YYYYMMDD: date prefix from time components before parsing. 3. batch_order_manager.py: Same direction-aware fix in _apply_market_fallback (memory.md Section E same-pattern check). - Impact: GPRO closed manually after hours at $0.7752, P&L +$111.08. No capital loss, but position was unprotected for ~15 minutes after scheduled exit failure. - Files: - src/earnings/order_executor.py - src/ibkr/client.py - src/earnings/batch_order_manager.py


Live Trading Validation Tests

Migrated from TESTING_FINDINGS_PENDING.md (consolidated Mar 7, 2026).

Global Rules

Live Test Results

1) Finding 1 — Entry + stop atomicity PASSED - Result (Feb 2, 2026): PLTR entry @ $148.70, stop @ $144.23 within 1 second. PEP/PFE batch also passed.

2) Finding 2 — Batch stop placement safety PASSED - Result (Feb 2, 2026): Batch 319b8adb - 1/1 success. Batch 34c25f75 - 2/2 success.

3) Finding 3 — Duplicate order prevention PASSED - Result (Feb 2, 2026): AAPL correctly skipped (existing position). PLTR skipped in batch.

4) Finding 4 — Market hours gating PASSED - Result (Feb 2, 2026): Pre-market entry blocked, instant exit allowed.

5) Finding 12 — Closed-market instant exit PENDING - Will test if instant exit needed during 8 PM - 4 AM window.

6) Finding 14 — Reconciliation disconnection guard VERIFIED (Code Review) - Guards confirmed in reconciliation_manager.py. Phase 4D event-driven architecture provides real-time backup.

7) Finding 15 — Stale conviction data fix VERIFIED - Two-phase Gamma fetch + 0% display fix deployed and verified.


Unit/Integration Test Gaps

1) Entry + stop atomicity (unit tests) — include success and failure cases 2) Batch order stop placement (integration) — validate retry logic and fallback 3) Reconciliation tests — cover old trades and partial fills 4) Dashboard P&L tests — fractional share calculations and stale price warnings 5) Heartbeat degradation (unit) — ADDED (tests/unit/test_ibkr_state.py) 6) IBKRPositionCache invalidation (unit) — ADDED (tests/unit/test_ibkr_position_cache.py) 7) EarningsDetector precedence + confirmation (unit) — ADDED (tests/unit/test_earnings_detector.py) 8) Stop order type by exchange (unit) — ADDED, UPDATED Feb 10 (Finding #179): ALL exchanges use STP-LMT


Pending Production Validation

Finding 182 — Holistic hardening follow-up (DONE)

Finding 184 — Exec ID mismatch hardening + repair (DONE)

Finding 185 — Unified NYSE/NASDAQ order policy + stop timeout escalation (DONE)

Finding 186 — Trading mode persistence consistency (DONE)

Finding 187 — Monitor entrypoint + actionable closed-trade reconciliation (DONE)

Finding 188 — History tab trade log query interpolation guard (DONE)

Finding 189 — IBKR blocking policy + disconnect alert path (DONE)

Finding 190 — Trader session vs gateway status separation (DONE)

Finding 192 — Orphan reconciliation hardening (DONE)

Finding 193 — Close persistence permanent fix (AS 42P18) (DONE)

Finding 194 — Upcoming earnings visibility fail-safe (DONE)

Finding 195 — Upcoming earnings table live-refresh (DONE)

Finding 196 — IBKR reconnect/reconciliation holistic hardening (DONE)

Finding 197 — Conviction robustness + close/reconciliation recurrence (DONE)

Finding 198 — NaT EPS crash loop in Yahoo earnings check (DONE)

Finding 199 — Direction-inverted duplicate prevention (DONE)


Leverage Phase Test Tracker


94) Dashboard event loop blocking from sync calls in async handlers (Finding #200) - Severity: High - Status: done - Implementation: Complete (Mar 7, 2026). Converted all route handlers from async def to def so FastAPI runs them in a thread pool. Removed inline blocking fallbacks for earnings calendar and trade history. - Root Cause: 1. All FastAPI route handlers were declared async def but called only synchronous blocking functions (DB queries, yfinance HTTP, NASDAQ calendar API). 2. In FastAPI, async def handlers run on the main event loop. Any blocking call freezes the entire loop, making ALL endpoints unresponsive. 3. The main dashboard route / had an inline fallback: when the cache was empty, it called get_earnings_with_polymarket(14) which makes 14+ HTTP requests to NASDAQ, plus Gamma API and yfinance calls. This single call could block the event loop for 30+ seconds. 4. Same pattern for get_trade_history("all") fallback — heavy DB query on cache miss. - Fix: 1. Changed all route handlers from async def to def. FastAPI automatically runs def handlers in a thread pool via anyio.to_thread.run_sync, preventing event loop blocking. 2. Removed get_earnings_with_polymarket(14) inline fallback from dashboard route — now always serves from cache (background thread populates within 30s). 3. Removed get_trade_history("all") inline fallback — same pattern, serves from cache or empty list. 4. Kept async def only for: lifecycle hooks (startup_event, shutdown_event), pure cache reads (api_earnings_calendar), and minimal-computation endpoints (api_health). - Same Pattern Check: All 25 route handlers audited. Every handler that calls blocking sync functions converted to def. - Files: earnings_dashboard/app.py

95) Strategy parameter changes: conviction cap at 90% and stop widening to 3%/4%/5% (Finding #202) - Severity: Critical - Status: done - Implementation: Complete (Mar 9, 2026). Two strategy changes based on analysis of 87 live trades. - Change 1 — LONG conviction cap at 90%: - Root Cause: 90%+ conviction LONG trades have 14.3% WR, -$116 P&L, 78% stop rate across 29 trades. The stock has already priced in the expected outcome at these conviction levels. - Fix: Added Config.SIGNAL_LONG_MAX_THRESHOLD = 0.90. Signal generator rejects LONG signals with conviction >= 90%. SHORT signals remain uncapped. - Same-pattern check: Updated signal_generator.py (generate_signal + is_valid), priority.py, conviction_calculator.py, dashboard app.py. - Change 2 — Stop loss widening to 3%/4%/5%: - Root Cause: What-If analysis using Polygon actual exit prices shows moderate stops (3%/4%/5%) improve total P&L from +$51 to +$166 (+$115 improvement). - Fix: Changed Config defaults: Large 2%->3%, Mid 3%->4%, Small 4%->5%. TradingSignal.post_init now reads from Config instead of hardcoded values. - Files: src/earnings/config.py, src/earnings/signal_generator.py, src/earnings/priority.py, src/earnings/conviction_calculator.py, earnings_dashboard/app.py - Documentation: Created docs/FINANCIAL_PROJECTIONS_REVISED_87_TRADES.md as comprehensive reference.

97) Double VIX reduction in position sizing (Finding #203) - Severity: Critical - Status: done - Implementation: Complete (Mar 9, 2026). Removed duplicate VIX reduction. - Root Cause: When VIX >= 30, _calculate_risk_level() returns ELEVATED (50% cut at line 929), THEN a separate VIX check at line 933 applies ANOTHER 50% cut. Result: 25% * 0.5 * 0.5 = 6.25% instead of intended 12.5%. ORCL got $153 (1 share) instead of ~$306 (2 shares). - Fix: Removed the duplicate VIX check at line 933. Risk level ELEVATED already handles VIX >= 30. - Files: src/earnings/risk_manager.py

98) ORCL limit+market double fill race condition (Finding #204) - Severity: High - Status: done (Finding #205 fixes the root cause) - Root Cause: Limit order filled at 14:34:05 but order status remained "PreSubmitted". At 14:34:17 (12s batch timeout), bot saw order as unfilled and placed market fallback. Market order filled at 14:34:20. IBKR=2 shares, DB=1. The duplicate prevention check reads IBKR positions, but the position cache hadn't updated with the limit fill in time. - Fix: See Finding #205.

99) Race condition root cause: time.sleep prevents IBKR event processing (Finding #205) - Severity: Critical - Status: done - Implementation: Complete (Mar 9, 2026). Replaced all time.sleep() with ib.sleep() in order monitoring and fallback paths. - Root Cause: time.sleep() blocks the Python thread but does NOT process the ib_insync event loop. Fill events arrive from IBKR gateway but sit in the event queue unprocessed. Order status, positions, and fills remain stale. Previous fixes (#3, #178, #199) addressed symptoms (wrong field names, direction checks) but never fixed the root cause: the event loop was never given a chance to process fills. - Fix: Replaced time.sleep() with self.order_executor.ibkr.ib.sleep() in: 1. batch_order_manager.py _monitor_orders() polling loop (line ~753) 2. batch_order_manager.py _apply_market_fallback() pre-position-check (new ib.sleep(1)) 3. batch_order_manager.py _apply_market_fallback() post-cancel delay (line ~834) 4. order_executor.py place_guaranteed_fill() monitoring loop (line ~651) 5. order_executor.py place_guaranteed_fill() post-cancel delay (line ~712) 6. order_executor.py instant exit monitoring loop (line ~1124) 7. order_executor.py instant exit fallback delays (lines ~1113, ~1188) - Why this is definitive: ib.sleep() processes all pending IBKR events. After each call, order status, positions, and fills are fully current. The duplicate prevention check now sees actual data, not stale cache. - Same-pattern check: All time.sleep() in order monitoring/fallback paths across both files replaced. - Files: src/earnings/batch_order_manager.py, src/earnings/order_executor.py - Mitigation: DB manually reconciled to 2 shares. Stop order covers only 1 share — needs manual fix. - Files: src/earnings/batch_order_manager.py, src/earnings/order_executor.py

100) verify_earnings_dates() silently failed due to self._get_engine() bug (Finding #206) - Severity: Critical - Status: done - Implementation: Complete (Mar 9, 2026). Fixed broken method call + added startup log. - Root Cause: verify_earnings_dates() called self._get_engine() which does not exist on SignalGenerator. The AttributeError was caught by a broad try/except and silently returned empty. The function should use self.engine (which exists and works). This meant earnings date cross-validation against NASDAQ calendar NEVER ran, despite the EarningsCalendar and finance_calendars being properly installed and available. - Impact: ORCL entered a day early (March 9 instead of March 10) because the DB date from Polymarket slug was wrong and verify_earnings_dates() would have caught and corrected it. - Fix: Changed self._get_engine() to self.engine. Added startup log confirming "NASDAQ calendar available for date cross-validation" so silent failures are immediately visible. - Files: src/earnings/signal_generator.py

101) Enable small cap longs at 70-80% conviction (Finding #207) - Severity: Medium - Status: done - Implementation: Complete (Mar 9, 2026). Changed small cap LONG rule from "require 90%+" to "allow 70-80% only". - Root Cause: Previous rule required 90%+ conviction for small cap longs, but Finding #202 blocked 90%+ entirely. This made small cap longs impossible. Backtest analysis (Q3 2025, proper MCAP classification) showed: - 70-80% conviction: 6 trades, 66.7% WR, EV +$30.68 (strong positive) - 80-90% conviction: 10 trades, 30.0% WR, EV +$0.40 (near-zero, not worth the risk) - Fix: Allow small cap longs at 70-80% only (block 80%+). Full position sizing (10% of capital). Priority score 2/6. - Files: src/earnings/signal_generator.py, src/earnings/priority.py, earnings_dashboard/app.py, earnings_dashboard/templates/dashboard.html

102) Dual MCAP classification systems caused KSS to bypass small cap rules (Finding #208) - Severity: Critical - Status: done - Implementation: Complete (Mar 9, 2026). Centralized market cap data into single source of truth. - Root Cause: Signal generator had its own MARKET_CAPS dict (~100 symbols) separate from the dashboard's _KNOWN_MCAPS (265 symbols). KSS ($3B) was not in the signal generator's dict. yfinance lookup failed (rate-limited), so classify_market_cap(None) defaulted to MID. As Mid cap, KSS at 86.5% conviction LONG passed all filters. As Small cap (correct), it should have been blocked (80%+ LONG not allowed for small caps). - Fix: 1. Created src/earnings/market_cap_data.py — single source of truth for all market cap classifications (265+ symbols). Used by signal generator, dashboard, and backtest scripts. 2. Signal generator's get_market_cap_for_symbol() now reads from KNOWN_MCAPS as primary source, yfinance as fallback. 3. Changed classify_market_cap(None) default from MID to SMALL (most restrictive — unknown stocks get small cap safety rules). 4. Added discover_unknown_mcaps() auto-enrichment: queries upcoming earnings symbols, looks up unknowns via yfinance, persists tier to DB, adds to runtime dict. Runs during dashboard deep refresh (every 2h). 5. Dashboard _KNOWN_MCAPS now imports from the shared module (was inline 285-line dict). - Same Pattern Check: Searched for MarketCap.LARGE defaults in DB loading code — found 4 instances in reconciliation/position/orchestrator, but these are for existing positions where the column is already populated at entry. Not the same pattern. - Impact: KSS entered as LONG at 86.5% with Mid cap rules ($430 position, 4% stop). Should have been blocked entirely. - Files: src/earnings/market_cap_data.py (new), src/earnings/signal_generator.py, earnings_dashboard/app.py

103) Dashboard earnings calendar always shows "Refresh error" (Finding #209) - Severity: Medium - Status: done - Implementation: Complete (Mar 10, 2026). Increased JS abort timeout from 15s to 30s and added resp.ok check. - Root Cause: The JS AbortController timeout was 15s. On cold start, the server thread pool is saturated by the slow / page render (~55s), causing the /api/earnings-calendar fetch to timeout. Additionally, the fetch did not check resp.ok before calling resp.json(), so 500 errors with HTML bodies caused parse failures shown as generic "Refresh error". - Fix: Increased timeout to 30s. Added if (!resp.ok) throw new Error('Server returned ' + resp.status) before resp.json() for clearer error messages. - Files: earnings_dashboard/templates/dashboard.html

104) Main loop stall during BMO/AMC scheduled cycles causes forced restarts (Finding #210) - Severity: High - Status: done - Implementation: Complete (Mar 10, 2026). Added intermediate progress marks and NASDAQ timeout. - Root Cause: The main loop watchdog (120s timeout) attributed ALL work inside run_bmo_cycle() and run_amc_cycle() to a single progress stage run_scheduled_cycles. These cycles legitimately take >120s due to: (1) verify_earnings_dates() making 5 NASDAQ API calls with no timeout, (2) signal generation doing yfinance lookups per symbol, (3) multi-day BMO reservation calculation. The watchdog saw no progress for >120s and killed the process. - Fix: (A) Added self._mark_main_loop_progress() calls inside run_bmo_cycle() and run_amc_cycle() after verify_dates and after signal generation, so watchdog sees forward progress. (B) Wrapped EarningsCalendar.get_upcoming_earnings() in verify_earnings_dates() with a 30s ThreadPoolExecutor timeout to prevent hung NASDAQ API from blocking indefinitely. - Same Pattern Check: Other get_earnings_by_date() calls in earnings_timing_fallback.py are per-symbol (not bulk) and have individual try/except — lower risk. The bulk call in verify_earnings_dates() was the only unbounded one. - Files: src/earnings/trading_orchestrator.py, src/earnings/signal_generator.py

105) Position sizing increase: 25/18/10 to 30/22/15 (Finding #211) - Severity: Medium - Status: done - Implementation: Complete (Mar 10, 2026). Increased position sizes based on 95-trade analysis. - Root Cause: The 90%+ conviction filter (Finding #202) removed ~36% of trades (26 out of 81 LONG trades). Fewer concurrent positions means more capital available per trade. Analysis of position size quintiles showed Q5 (largest positions, avg $490) generated best absolute returns (+$139, $7.70/trade avg) while Q1-Q2 (smallest, $157-$226) were net losers (-$40 combined). The "kept" trades (70-89% conviction) have 40% WR vs 15% for filtered 90%+. - Fix: Changed Config defaults — Large 25%→30%, Mid 18%→22%, Small 10%→15%. On $2,450 capital: Large $735 (+$123/trade), Mid $539 (+$98/trade), Small $367 (+$122/trade). Max concurrent exposure ~65-90% of capital with fewer positions. - Same Pattern Check: Updated all references in risk_manager.py (4 locations), STRATEGY.md (3 tables), FINANCIAL_PROJECTIONS_REVISED_87_TRADES.md, dashboard.html (System tab). - Files: src/earnings/config.py, src/earnings/risk_manager.py, docs/STRATEGY.md, docs/FINANCIAL_PROJECTIONS_REVISED_87_TRADES.md, earnings_dashboard/templates/dashboard.html

96) Volume_24h_at_entry never populated in trade_audit_log (Finding #201) - Severity: Medium - Status: done - Implementation: Complete (Mar 8, 2026). Added volume_24h_at_entry to all 4 INSERT INTO trade_audit_log statements. Backfilled 41/94 trades. - Root Cause: All INSERT statements omitted the column despite signal.volume_24h being available in scope. - Files: src/earnings/trading_orchestrator.py, src/earnings/position_manager.py, src/earnings/reconciliation_manager.py

107) Chinese ADRs and other stocks missing from COMPANY_TIMING_LOOKUP — missed trades (Finding #213) - Severity: High - Status: done - Implementation: Complete (Mar 18, 2026). Added 27 tickers to DEFAULT_TIMINGS in earnings_timing.py. - Root Cause: PDD, BABA, NIO, XPEV, LI, WB, JD and other stocks were not in the COMPANY_TIMING_LOOKUP table. When the Gamma API endDate fell in the ambiguous 13-19 UTC window or midnight (00:00), the sync service stored earnings_time = NULL. The signal generator's determine_earnings_timing() then checked: (1) DB=NULL, (2) COMPANY_TIMING_LOOKUP=not found, (3) EarningsCalendar=time-not-supplied for some, (4) yfinance=rate limited. Result: timing returns None, trade is SKIPPED. - Impact: PDD (6/6 priority, Large cap, 77% conviction LONG) would have been skipped entirely. BABA was correctly resolved by EarningsCalendar but others like CTAS and DBI were not. - Fix: Added 27 tickers to DEFAULT_TIMINGS covering Chinese ADRs (BMO), payroll/business services (BMO), retail (BMO), and others. All classifications verified against historical DB data. - Added: BABA, PDD, JD, NIO, XPEV, LI, WB, BIDU, BEKE, TME, BILI, IQ (Chinese ADRs BMO), PAYX, ADP, ACN (payroll BMO), DBI, CHWY, KSS, BBW, ANF, AEO, URBN, ASO (retail BMO), WSM, CTAS (BMO), FIVE, GME, NMAX (AMC). - Files: src/earnings/earnings_timing.py

106) yfinance rate-limited on server — eliminate dependency for mcap auto-discovery (Finding #212) - Severity: High - Status: done - Implementation: Complete (Mar 16, 2026). Routed auto-discovery through yfinance_limiter with Finnhub fallback, added IBKR-first VIX/SPY, reduced dashboard yfinance calls. - Root Cause: Production server IP permanently rate-limited by Yahoo Finance (429 on every call). discover_unknown_mcaps() called yf.Ticker() directly (no fallback). Dashboard made uncoordinated direct yfinance calls for VIX/SPY, company names, dependency checks, and batch mcap. All returned zero data, leaving symbols as "Unknown" in the dashboard. - Fix: 1. Added 6 missing symbols to KNOWN_MCAPS: BABA ($200B Large), CHWY ($12B Mid), CTAS ($80B Mid), NMAX ($2B Small), PAYX ($50B Mid), DLTR ($15B Mid). 2. Routed discover_unknown_mcaps() through yfinance_limiter.get_market_cap() which has automatic Finnhub fallback when yfinance returns 429. 3. Dashboard _fetch_market_health_data() now checks IBKR snapshot for VIX/SPY before falling back to yfinance. 4. Dashboard get_company_name() checks KNOWN_MCAPS tuple[2] before calling yfinance. 5. Dashboard check_dependencies() no longer makes a yfinance API call (just checks module import). 6. Dashboard populate_mcap_cache() skips symbols already in KNOWN_MCAPS. - Same Pattern Check: Remaining direct yf.Ticker() calls in dashboard were routed through yfinance_limiter (Mar 22, 2026 — Finding #217). Finnhub fallback behavior for market cap was tightened in Finding #219 (empty Yahoo marketCap + MEDIUM priority during Yahoo backoff). - Files: src/earnings/market_cap_data.py, earnings_dashboard/app.py

108) Production timing verification — PDD/BABA BMO (Finding #214) - Severity: Low (ops / evidence) - Status: done - Evidence: docs/OPS_TIMING_VERIFICATION.md. Production logs show PDD: Timing from database: BMO and BABA: Timing from database: BMO (Mar 18–19, 2026). Trades executed and exited per plan (stops filled Mar 19). - Files: docs/OPS_TIMING_VERIFICATION.md

109) Dashboard cold start — heavy _fetch_db_stats_from_db on first paint (Finding #215) - Severity: Medium - Status: done - Root Cause: First HTTP request to / ran the full DB stats query (category coverage joins, per-market availability, data_gaps) before background cache warmed. - Fix: _fetch_db_stats_from_db(full=False) on cache miss + background thread upgrades to full=True; reuse state.positions for timeline to avoid duplicate positions query. - Files: earnings_dashboard/app.py

110) PostgreSQL “out of shared memory” / lock pressure on managed DO (Finding #216) - Severity: Medium - Status: deferred (monitor) - Root Cause: Transient lock spikes during heavy dashboard queries; max_locks_per_transaction not adjustable on DigitalOcean managed Postgres. - Mitigation: Avoid many simultaneous dashboard hard-refreshes; rely on dashboard caching; see docs/POSTGRES_OPERATIONS.md. - Files: docs/POSTGRES_OPERATIONS.md

111) Dashboard yfinance — route fallbacks through limiter (Finding #217) - Severity: Medium - Status: done - Fix: Replaced remaining direct yf.Ticker() usage in dashboard with yfinance_limiter (get_market_cap, get_price, get_ticker_info, get_exchange, VIX/SPY already via limiter after IBKR). - Files: earnings_dashboard/app.py

112) Strategy Analysis — position sizing quintiles in report + tab (Finding #218 / memory.md §K) - Severity: Low - Status: done - Fix: compute_position_sizing_quintiles() in scripts/strategy_performance_analysis.py; table + renderPositionQuintiles() in Strategy Analysis tab; print_summary() prints quintiles when present. Re-run analysis script on prod (with DATABASE_URL) after deploy to refresh strategy_analysis_report.json. - Files: scripts/strategy_performance_analysis.py, earnings_dashboard/templates/dashboard.html

113) Dashboard upcoming table — mass “Unknown” MCAP despite #212/#217 (Finding #219) - Severity: High - Status: done - Root Cause: (1) YFinanceRateLimiter.get_market_cap() only called Finnhub on specific Yahoo exception text; Yahoo often returns empty info/no marketCap without raising, so Finnhub never ran. (2) For Priority.MEDIUM (dashboard batch), when Yahoo was in backoff, the method returned None immediately — Finnhub was skipped entirely. (3) populate_mcap_cache could treat mixed-case tickers as uncached vs get_cached_mcap lookups. (4) Deep-refresh discover_unknown_mcaps used a 60s thread timeout vs dozens/hundreds of sequential lookups. (5) DB persist used exact ticker = :symbol match; casing mismatches skipped updates. (6) Hourly earnings_market_fetcher inserted earnings_markets rows without market_cap_category, so the dashboard showed NULL/Unknown until a rare manual sync. - Fix: Always attempt Finnhub when Yahoo yields no cap (and when Yahoo is rate-limited, before giving up on non-CRITICAL); Finnhub calls throttled ~1.05s apart for free-tier quota; uppercase-normalized dashboard mcap cache + UPPER(TRIM(ticker)) in persist; discovery timeout 300s. EarningsMarketSync.get_market_cap_category() checks KNOWN_MCAPS then limiter; backfill_upcoming_null_mcaps() runs from run_full_sync() and after each EarningsMarketFetcher.run_full_sync(); API gap-fill inserts include company + mcap when possible. YFinanceRateLimiter no longer passes requests.Session into yf.Ticker (Yahoo requires curl_cffi session). - Future-proof: Keep FINNHUB_API_KEY on prod; optional IBKR fundamentals if Yahoo+Finnhub fail; monitor limiter + Finnhub 429s in logs. - Files: src/utils/caching.py, earnings_dashboard/app.py, src/earnings/market_cap_data.py, src/db/earnings_sync.py, src/data_tracker/collectors/earnings_market_fetcher.py, scripts/sync_earnings_markets.py - Documentation (cross-refs only — canonical narrative stays here per memory.md §J): docs/CONTEXT.md, docs/STRATEGY.md §7.8, docs/DATABASE_DIRECTIVES.md, docs/OPERATIONS.md, .cursor/memory.md §L. Finding #220 extends the same docs for BMO/AMC DB sync + dashboard gating.

114) Trusted earnings_time DB enrichment, upcoming TRADE gating, sync transaction fix, UNTY default (Finding #220) - Severity: High (data integrity + operator clarity) - Status: done - Implementation: Complete (Apr 2026). - Root Cause: 1. TBD / NULL earnings_time: NASDAQ calendar often returns non-parseable strings; sync_from_markets_table only stored calendar output. Tickers not in DEFAULT_TIMINGS/overrides.json stayed NULL/TBD. Dashboard showed ✓ TRADE on conviction/volume/depth alone while BMO/AMC was unknown — misleading vs batch logic (SignalGenerator skips when determine_earnings_timing() returns None unless runtime fallback resolves). 2. sync_from_polymarket_api INSERT omitted earnings_time, leaving gaps until a later path filled rows. 3. SQLAlchemy 2: with engine.connect() plus nested with conn.begin() on the same connection per row caused “Transaction already initialized” — hundreds of sync errors, zero effective updates per run on affected deployments. - Fix: 1. resolve_earnings_time_for_sync() — Order: NASDAQ calendar only if explicit BMO/AMC; else get_timing_manager().get_timing() (same trusted tier as signal generator priority 2). Does not persist yfinance/web fallback into DB (reliability: no speculative timing stored as calendar truth). 2. backfill_upcoming_null_earnings_time() — Fills upcoming rows where earnings_time is NULL, empty, or TBD. Wired into run_full_sync() after mcap backfill. Logs as earnings_time_backfill in scripts/sync_earnings_markets.py. 3. API gap-fill INSERT now includes earnings_time when resolvable. 4. Per-row with self.engine.begin() for market-table sync writes; initial SELECT uses a short-lived connect() only. 5. Dashboard upcoming table: ✓ TRADE requires known BMO or AMC; if edge/liquidity OK but timing unknown → NEEDS TIMING badge (tooltip notes bot may still use runtime fallback). 6. UNTY (Unity Bancorp): Added UNTY → BMO to DEFAULT_TIMINGS (regional bank, pre-market). One-off DB fixes: scripts/backfill_ticker_earnings_timing.py (--ticker / --timing) for NULL/TBD/empty only. - Same Pattern Check: Signal generator unchanged for fallback order; DB layer aligned to trusted sources only. - Files: src/db/earnings_sync.py, scripts/sync_earnings_markets.py, earnings_dashboard/templates/dashboard.html, src/earnings/earnings_timing.py, scripts/backfill_ticker_earnings_timing.py - Documentation: docs/DATABASE_DIRECTIVES.md, docs/OPERATIONS.md, docs/CONTEXT.md, docs/STRATEGY.md §7.8, .cursor/memory.md (cross-refs).

115) Polymarket opportunity log + counterfactual queue for all reject stages (Finding #221) - Severity: Low (research / analytics) - Status: done - Root Cause: Post-hoc analysis needed a single append-only log for every Polymarket earnings candidate on the signal path (not only executed trades or skipped_signals volume skips), with live-rule entry/exit/stop timestamps for offline P&L simulation. - Fix: 1. Migration 021_polymarket_opportunity_log.sql: Table polymarket_opportunity_log with skip_stage / skip_reason, conviction/volume/depth/mcap/priority, scheduled entry/exit + stop_loss_pct, JSON snapshot, nullable counterfactual columns. 2. SignalGenerator: _emit_pm_opportunity_log + _hypothetical_schedule_fields populate BMO/AMC schedules and stop % (from Config by cap tier) for all stages where timing + earnings datetime are known (including depth/timing/generate_signal rejects and calendar paths). 3. src/earnings/counterfactual_pricing.py + scripts/compute_counterfactual_pnl.py: With POLYGON_KEY, loads Polygon 1-minute bars between scheduled entry/exit, fills at bar close near schedule (research proxy for limit fills), applies stop on bar low (LONG) / high (SHORT) vs logged stop_loss_pct. Without Polygon, yfinance daily close-to-close on entry/exit calendar days (yfinance_daily_approx_v1, no intraday stop). hypothetical_pnl uses fixed USD 10,000 notional (COUNTERFACTUAL_NOTIONAL_USD env override). Script marks non-simulatable rows no_price_simulation_v1 and UPDATEs price columns for simulatable rows. - Files: src/data_tracker/migrations/021_polymarket_opportunity_log.sql, src/earnings/polymarket_opportunity_log.py, src/earnings/signal_generator.py, src/earnings/counterfactual_pricing.py, scripts/compute_counterfactual_pnl.py, tests/unit/test_counterfactual_pricing.py - Documentation (cross-refs per memory.md §J): docs/DATABASE_DIRECTIVES.md, docs/DATA_ARCHITECTURE.md, docs/OPERATIONS.md, docs/CONTEXT.md, docs/STRATEGY.md

Finding 220 — Trusted earnings_time sync + dashboard TRADE gating + SQLAlchemy sync fix (DONE)

Finding 222 — Volume threshold: trader vs dashboard mismatch + NULL volume_total (DONE)

Finding 223 — Dashboard showed $0 volume after #222 (DB volume_total = 0 vs Gamma) (DONE)

Finding 224 — MIN VOLUME column all ⚠ + LOW VOL despite displayed $>2K (DONE)

Finding 225 — Volume rollout: incomplete interface audit (process / regression chain) (DONE)

Finding 226 — Dashboard VOLUME column felt stale (DB 15m vs Gamma ~30s) (DONE)

Finding 227 — Broken learning loop: counterfactual + outcome resolution + market regime capture (DONE)

Finding #227 — Update (Apr 22, 2026): Polygon EPS + Polymarket cascade

After the initial Track A backfill, actual_beat coverage was only 25% (179/715) — Yahoo's earnings_history and Finnhub's free-tier /stock/earnings both have severe coverage gaps for mid/small-caps. Two additive fixes raised this to 73% (524/715):

Tier 3 — Polygon /vX/reference/financials (commit 926f967): - Probed Polygon Stocks Starter ($29/mo): the financials endpoint is unlocked (the screenshot's "Financials & Ratios ✗" gate refers to the realtime ratios endpoint, not historical SEC filings). - New _polygon_earnings_actual helper queries with a ±120 day window, picks the filing whose end_date is closest to target within 30 days, prefers diluted EPS over basic. - Also captures revenue_actual from Polygon's revenues field (was 0 rows before). - Coverage lift: actual_beat 25% → 51%, eps_actual 15 → 210 (14×), revenue_actual 0 → 197.

Tier 4 — Polymarket cascade (commit a86058f): - The remaining 49% gap was almost entirely "we have eps_actual from Polygon but no eps_estimate from anywhere → can't compute BEAT." The Polymarket data was always there; we weren't using it properly. - New cascade in _pm_resolution_actual_beat: 1. Post-event yes_price (latest snapshot from price_snapshots >12h after earnings_date) — markets converge to 0.000 / 1.000 within hours of resolution; this is essentially ground truth. Sample of 15 unresolved tickers showed every one converged cleanly (TFC 1.000, STT 0.001, JNJ 0.820 → 0.999, etc). 2. Pre-event prob cascade (prob_30min_before → prob_1h → prob_6h → prob_12h → prob_24h) — falls through nulls to find the most recent conviction read. - Threshold relaxed from 0.99/0.01 to 0.95/0.05, calibrated against our own resolved-EPS rows: yes_price ≥0.95 → BEAT with 93.75% accuracy (n=112); yes_price ≤0.05 → MISS with 93.6% accuracy (n=47). The old strict thresholds remain available via strict=True for callers that want maximum precision. - Coverage lift: actual_beat 51% → 73% (366 → 524). The 158-row gain came almost entirely from price_snapshots post-event yes_price.

Final actual_beat source mix on the 524 resolved rows: - EPS surprise (eps_actual ≥ eps_estimate): primary for ~30% (where Yahoo or Polygon supplied both numbers) - Post-event Polymarket yes_price: primary for ~50% - Pre-event prob cascade: primary for ~20%

Remaining 191 unresolved rows (~27%): these are markets where neither Yahoo / Finnhub / Polygon EPS exists AND no Polymarket conviction snapshot was ever taken (the conviction_tracker service didn't have those condition_ids in scope when their earnings ran). Most are pre-Jan-2026 markets predating reliable conviction collection. The daily polymarket-resolver.timer will keep retrying; new tickers as they're added to the conviction collector will be picked up automatically.

Final test count: 51 unit tests in test_outcome_resolver.py + test_market_regime_provider.py, including the 9 new tests for the Polymarket cascade (post-event vs pre-event, default vs strict thresholds, NULL cascade, missing-key tolerance for older schema rows).

Polygon plan note: The user upgraded to Stocks Starter ($29/mo). Verified via burst test (50 req in 5s with zero 429s — Starter is unlimited API calls, only restriction is 15-min-delayed real-time). The default --polygon-sleep 0.05 in scripts/backfill_learning_loop.py and the polymarket-resolver.service ExecStart was tuned accordingly. Earlier docs that cited "100 req/min" for Starter were corrected in commit 9324af5.

Finding 228 — v3 audit Finding A retracted via comprehensive simulator (DONE — no code change)

Rule Δ vs A_base per-100 better worse Bias-gauntlet verdict
FA_soft (rule as written) $0.00 $0.00 0 0 INERT — software gate cannot change behavior while hardware fills outsideRth
FA_hard (hardware also gated) −$213.23 −$418.10 9 23 FAIL — net negative, opposite sign from audit's claim
RuleB (±90 min window only) +$62.97 +$123.47 4 1 PASS — only stop-loss rule that survives
RuleD (ATR k=2.0) +$99.23 +$194.57 22 27 FAIL — ex-top-2 collapses to −$109/100
RuleG (window + ATR) +$102.32 +$200.63 22 27 FAIL — ex-top-2 collapses to −$103/100
RuleF (no stop ever) −$12.77 −$25.04 24 25 net-zero, very high variance

Finding 286 — Dashboard cold-start race: _empty_summary missing 13 fields added by Finding #284 → / returned HTTP 503 with UndefinedError: 'roi_tc_total_pct' for ~2min after every restart; deploy_from_github.sh smoke test exited 1 (RESOLVED May 9, 2026)

Finding 285 — Rare-day exposure cap (60% on days with 1-2 opportunities) silently rejected BOTH valid signals when one would fit; rejection wasn't logged anywhere (Phase A: greedy-fill + opportunity_log emission RESOLVED May 9, 2026; Phase B rule re-evaluation DEFERRED to Phase 3 strategy-revamp May 26)

Finding 284 — P&L Summary needed tooltips + a Trading Capital ROI panel (period + all-time, total / monthly / annualized) so the operator can see annualized-on-actual-trading-capital, not just per-dollar-deployed (RESOLVED May 6, 2026)

Finding 283 — Polymarket "depth quality" gate (badge "LOW OB") is functionally a SPREAD gate; 37/38 historical kills were spread-dominated; UX rename + post-Phase-3 deferral (RESOLVED May 31, 2026 via Finding #319 — KEEP gate)

Finding 282.v2 — Same-day post-deploy live probe caught dashboard ≠ trader misalignment ($2,224 vs $4,210 for MRNA) because Gamma batch chronically drops some markets and the except-fallback used cached_volume_24h (rolling, not cumulative); now both surfaces share the SAME single-shot Gamma fetcher (RESOLVED Apr 30, 2026)

Finding 282 — effective_cumulative_volume_usd returned stale DB value when live Gamma cumulative was much higher; trader silently rejected MRNA #2026-05-01 at $6.28 < $2k while live Gamma was $3,734 (RESOLVED Apr 30, 2026)

Finding 281.v3 — Resolution selector (auto / 1m / 15m / 1h / 1d) on trade-chart modal; pre-Apr-26 trades no longer locked to daily-only (RESOLVED Apr 29, 2026)

Finding 281.v2 — Same-day operator-flagged bugs on the first ship: live chart "stops at entry"; SOFI exit marker positioned at $18.67 instead of the $17.56 fill price (RESOLVED Apr 29, 2026)

Finding 280 — Upcoming Earnings widget had no post-event status; today's BMO rows rendered as "SKIP / -- / 0% / NO EDGE" hours after release (RESOLVED Apr 29, 2026)

Finding 278 — Market regime detection is structurally lagging (median 31 trading days from SPY peak); evaluate faster regime alternatives post-Phase-3 (RESOLVED May 31, 2026 via Finding #318 — KEEP compound_v1)

Detector medians: compound (current) 31d [22 historical downtrend-LONG trades sized against this] below_50 12d faster, classic SMA breach below_ema20 8d fastest, EMA smoothing dd_3pct 23d drawdown filter dd_5pct 28d "correction" textbook def below_200 40d slower dd_10pct 49d slowest `` Two takeaways: (1) our compound 4-AND uptrend / 3-AND downtrend rule is the **second-slowest** detector in the family — onlyclose < MA200is slower; (2) our rule **missed an entire drawdown** (Sep 2020 −8.1%) because the move wasn't deep enough to invert MA20<MA50. - **Why this is the right design today (do NOT change before Phase 3):** - Action Queue rule #1 (Skip LONGs in downtrend) was sized on the **22 historical downtrend-LONG trades** identified by exactly this classifier. Switching to a faster detector retroactively re-classifies those trades and may shrink the bucket below the n≥12 gauntlet floor (memory §K). The +$140-$200 / 100-trades estimate is *causally tied to the current classifier's lag*. - Per memory §K + §N, every regime-rule change requires a true-simulator end-to-end re-run on the actual data (not just lag analysis). That re-run is a meaningful piece of work and should be done as a deliberate "regime v2" project after rule #1 has accumulated live evidence. - The lag is a **selection problem (we miss some good downtrend-skips)**, not a **damage problem (the rule itself works on the trades it does catch)** — opportunity cost, not loss. - **Industry-consensus options (ranked by cost-of-integration, surveyed Apr 28):** 1. **SMA → EMA on the same windows** — median 8d vs our 31d; drop-in replacement; no new data; trade-off is more sideways↔uptrend whipsaw. 2. **Drawdown circuit-breaker as OR clause** —downtrend = (compound) OR (close ≤ 95% of 60-day high); 28d median; catches the Sep 2020 case our rule missed; textbook "correction" definition. 3. **VIX term structure (VIX vs VIX3M)** — backwardation = stress; the single most-watched leading vol-regime indicator on institutional vol desks; typically flips 5-15 days BEFORE the price-MA breakdown. We already capture VIX — VIX3M is one extra yfinance call (^VIX3M). 4. **Breadth deterioration** — % of S&P 500 stocks below 50-DMA; leads index by ~10 days; needs universe data we don't currently have. 5. **Credit spreads** — HYG/LQD ratio or HY OAS; leads equity by 5-20 days; free from FRED. 6. **Composite financial-conditions index (FCI)** — Goldman / Bloomberg / Chicago Fed NFCI; full institutional standard. Heavier integration cost. 7. **Hidden Markov Models / Markov-switching** — academic-grade; probably overkill for our purpose. - **Asymmetry to flag in any redesign:** the current downtrend rule does NOT requireclose < MA200. So a sustained bull-market dip that briefly breaks MA50 + inverts MA20<MA50 can flip us to "downtrend" while SPY is still above MA200 — a slight false-positive risk. Symmetric construction would also requireclose < MA200. Worth raising during the post-Phase-3 redesign even if all other choices are unchanged. - **Re-test triggers (when to revisit):** 1. Phase 3 unified simulator has shipped (~May 26, 2026 — equity-bars 30-day capture window completes, see Finding #240). 2. Action Queue rule #1 has been live for ≥30 closed trades AND its measured lift is within ±$30 of the +$140-$200 / 100 estimate (i.e. the current-classifier baseline holds). 3. THEN: buildMarketRegimeProvider v2candidate (likely option B above — compound rule ORdd_5pctORVIX > VIX3M for ≥3 days), re-run the full Action Queue analysis with v2 labels, apply the 4-check gauntlet (memory §K) to every rule that uses regime, ship only if v2 shows ≥+30% improvement and survives the gauntlet. - **Saved artifacts (per operator request "ensure everything is clearly saved"):** -scripts/analyze_regime_detection_lag.py— read-only, re-runnable, committed Apr 28. Re-run with:[hidden]/venv/bin/python3 scripts/analyze_regime_detection_lag.py. Output is the table above + aggregate medians. - This Finding (canonical narrative per memory §J). -docs/STRATEGY_PLAYBOOK.md§7 (forward-looking deferral entry per memory §N). -docs/AGENT_HANDOFF.md§11 DEFER queue. - Action Queue entryregime-detection-v2with statusDEFERRED-POST-PHASE-3(mirrored to dashboard viabuild_action_queue()per memory §K). - **Files (current implementation, no changes):** -src/earnings/market_regime_provider.py— single source of truth for regime classification (memory §M). -scripts/strategy_performance_analysis.py:get_spy_regime_at_date— historical analysis mirror (must stay in sync with_classifyif the rule ever changes). - **Acceptance test (when this finding is eventually closed):** post-Phase-3, after building v2, re-runscripts/analyze_regime_detection_lag.pyANDscripts/strategy_performance_analysis.py` AND the 4-check gauntlet. Lag table should show v2 median ≤ 15d. Action Queue rule #1 evidence under v2 must still pass the gauntlet. Then update this finding to RESOLVED and the Playbook §7 entry.

Finding 279 — Position-sizing percentages and 0-6 priority-tier system have not been re-validated against the current strategy data; defer comprehensive re-evaluation to post-Phase-3 (DOCUMENTED — deferred to post-Phase-3; May 28, 2026 amendment: Phase B scope expanded to include the $2k SIGNAL_MIN_VOLUME_TOTAL gate retune after PM earnings-market volume collapse was quantified)

Phase B framing premise — PM earnings-market volume collapse (May 28, 2026 amendment)

This subsection was added May 28, 2026 (was a chat-only observation from May 19, escalated to canonical narrative here per operator's AskQuestion "option B — fold #304 evidence into #279 Phase B" decision — i.e. the PM volume-collapse evidence got folded INTO this #279 subsection rather than creating a separate Finding entry for the collapse itself). Note for chronology: the Finding #304 number itself WAS subsequently used the same afternoon — assigned to the Queue Rule #1 ship (Skip LONGs in downtrend regime) per docs/FINDINGS.md Finding #304. The two events are independent: the volume-collapse evidence remains folded in here under #279; the next-numbered ship (Rule #1) took #304 in the natural sequence. This subsection captures the volume-environment shift that any Phase B sizing/priority/gating decision must now account for.

Month Markets Avg vol Median vol Max vol $2k gate pass % pass
Dec 2025 52 $16,438.41 $14,521.84 $51,573.51 51 / 52 98.1 %
Jan 2026 54 $33,475.47 $24,729.72 $145,386.59 54 / 54 100.0 %
Feb 2026 170 $33,387.08 $8,823.12 $850,539.22 163 / 170 95.9 %
Mar 2026 72 $3,958.39 $0.00 $58,561.02 20 / 72 27.8 % ← first cliff
Apr 2026 120 $7,031.82 $4,306.62 $59,683.42 77 / 120 64.2 % ← partial recovery
May 2026 (May-1 → today, May-28) 154 $305.98 $13.39 $9,358.05 3 / 154 1.9 % ← catastrophic

Finding 272 — volume_24h_at_entry NULL for 76 consecutive trades since Feb 16 — Polymarket Gamma API drops volume24hr field intermittently (RESOLVED)

Finding 274 — Phase 5 logrotate config had no size early-trigger; runaway logs could balloon multi-GB between daily fires (RESOLVED May 9, 2026 — maxsize 500M, NOT size 500M, per pre-flight dry-run on prod)

Finding 273 — Phase 2.1 PM-snapshot enrichment fires schema label but leaves new fields NULL on signal_emitted rows (DOCUMENTED — deferred to post-Phase-3 — medium)

Finding 270 — StartLimitIntervalSec/Burst in [Service] instead of [Unit] across 5 live systemd unit files (RESOLVED May 9, 2026)

Finding 323 — Conviction-band exclusion re-validation (raise the 90% cap? trade the dead zone? loosen small-caps/shorts?) → ALL exclusions HOLD (RESOLVED Jun 1, 2026; offline analysis, NO production change)

Finding 324 — Counterfactual P&L units cleanup: hypothetical_pnl_pct (PERCENT) is canonical; fixed a 100× "Avg %" dashboard display bug + honestly labeled the $10k-notional dollars (RESOLVED Jun 1, 2026; dashboard/docs, NO trading change)

Finding 323.b — RuleB re-test of the excluded bands AMENDS #323: ~62-67% of stops are in the ±90min earnings window (vol-driven, conviction-independent) → the LONG ≥0.90 skip (#202) flips POSITIVE under RuleB (RESOLVED Jun 1, 2026; offline; NO production change)

Finding 323.c — small-cap LONG 80-90% skip (#207) re-validated real-vs-counterfactual (the #202 playbook applied): counterfactual is a 3-NAME MIRAGE; universe is flat; KEEP (RESOLVED Jun 2, 2026; offline; NO production change)

Finding 323.d — Entry/exit-aligned analysis dataset (the real strategy clock, not "24h before") built + exclusions re-validated with per-tier stops: every exclusion HOLDS (RESOLVED Jun 11, 2026; offline; NO production change)

Finding 348 — equity-bars collector taken down by a Polygon 429 storm: two daily jobs (equity-bars @ fixed 10:00 UTC + daily resolver @ 06:00 ET = 10:00 UTC in EDT) collided on the shared key at 20 req/s each, no retry/backoff existed anywhere, and rate limits were mislabeled "likely delisted/OTC" (Jul 29, 2026)

Finding 488 — dormant sniper refuses a missing or retired funder (Oct 14, 2026)

Finding 487 — storage:orderbook_snapshots stays FAIL at 12 GB after the drain finishes; the page stops saying the drain failed (Oct 3, 2026)

Finding 486 — NASDAQ calendar GET cannot outlast the trader watchdog (Oct 3, 2026)

Finding 484 — IBC Command Server RESTART before last-resort 2FA (heal-1234 step 3) (Sep 27, 2026)

Finding 483 — Post-deploy #467 docs review gap: WALLET_NAGI leaked on /api/btc5m/docs/doc_map; central identifier masking, comprehensive route sweep (Sep 27, 2026)

Finding 470 — Main-loop stalls: the signal scan's unbounded latest-snapshot read blocked the IB owner thread; bounded reads, per-ticker progress, dump to file; #453 stopgap retired (Sep 26, 2026)

Finding 452 — Leverage alerts: page only on broker-reported, populated margin state; latch (one page + one resolve) (Sep 26, 2026)

Finding 451 — Trader IBKR alert policy: expected events page only past their SLA, one page + one resolve, correct lever, OD6 delivery-gated integrity dedupe (Sep 26, 2026)

Finding 469 — PR Newswire check silently skipped for 5 min after a host boot; monotonic "never" sentinel (Sep 26, 2026)

Finding 482 — widen trades.token_label to match outcome (Sep 27, 2026)

Finding 481 — live-trades #455 poison row spool loop + CRITICAL Telegram storm (Sep 26–27, 2026)

Finding 480 — pg8000 untyped binds: covered-channel lookup always 42P18; legacy writer retired; repo tripwire (Sep 26, 2026)

Finding 477 — Exit-price corrections left gross_pnl / net_pnl_after_financing and the trading_positions twin stale (BBW#274, SNOW#75) (Sep 26, 2026)

Finding 467 — Public dashboard defense in depth: secret-value response filter, FastAPI attack surface removed, IBKR/wallet/path identifiers masked (Sep 26, 2026)

Cross-family review (71dd002, docs/reliability/review-467-dashboard.md): CHANGES REQUIRED — 4 blockers, all fixed same finding: 1. Streaming chunk-boundary leak (high, reproducible). _ResponseBodyRedactor._emit_chunk split the RAW window into an emit/carry pair BEFORE redacting it — a secret straddling that split point had each half scanned independently, so neither half matched the full literal and it shipped raw (the reviewer's exact repro: _MAX_BUFFER_BYTES=16, overlap=8, one 24-byte chunk b'xxxxxxxxxxxxSecretValue1'). Fixed: redact the WHOLE combined window first, THEN split the already-safe result; the carry-back margin is max(longest current literal, 788 [longest shape-pattern match], 4096) via new _SecretStore.max_match_length() — an in-progress match's visible-so-far portion is, by construction, always shorter than the margin, so it's never in the emitted half. Regression: TestStreamingChunkBoundaryRegression sweeps every junk_len around the exact danger band (margin − secret_len, margin) using the reviewer's own margin-shrinking technique; mutation-checked against a reverted split-before-redact implementation (14 of 32 parametrized cases correctly fail against it, 0 against the fix). 2. Public /docs/{doc_name} pages exposed identifiers (high). #457 removes committed secrets, not non-secret identifiers, and decision D covers every public page — docs included; deferring the account number to #457 was the wrong scope boundary. Fix: new identifier_masking.mask_known_public_identifiers() — exact-value replacement (never a path-shaped regex, so Markdown links/code fences survive) of the real IBKR account number (→ U2…13) and the droplet IP + [hidden] (→ [hidden]) on the raw doc text, applied before Markdown conversion in serve_documentation() (source .md files on disk stay untouched) and on btc5m's plaintext docs route via a local, dependency-free duplicate (modularity). Same 3 values also joined the response filter's known-identifier literal set as a backstop. Tests parametrize over the review's own reported routes (operations/agent-handoff/readme/strategy) plus every doc_map slug (26, discovered from the route's own 404 body) and every btc5m doc slug (12). 3. Bare/short/URL-encoded DATABASE_URL passwords (medium). The 8-char minimum literal length silently dropped a short password entirely, and only the URL-as-appears (still percent-encoded) form was ever collected. Fixed: no minimum length for a value with known provenance (name-matched or URL-password-extracted) — literals under 8 chars are anchored to token boundaries ((?<![A-Za-z0-9])...(?![A-Za-z0-9])) instead of being dropped, so they can't flood false-positives as an unanchored substring match either; new _url_password_variants() collects the raw/as-appears form, the URL-decoded (bare) form, and a canonical re-encoded form. 4. The full-route sweep was not mutation-sensitive (medium). It only checked whether a planted secret happened to leak — nothing in this app's NORMAL (no live DB/IBKR in tests) request handling embeds one, so the test passed with the filter removed (reviewer's own mutation check: 44 of 44 routes still zero-leak with SecretRedactionMiddleware deleted from the live app). Fixed: test_every_route_response_traverses_the_redactor instruments redact_bytes directly (call-count before/after each request) to prove the POSITIVE the leak-scan can only assume — every route's response actually reaches the redactor — plus an explicit mutation test using the reviewer's exact method (remove the middleware, rebuild the stack), which reproduces the same "44 of 44 never traverse" result.

Bonus finding made while building the blocker #4 instrumentation (not reviewer-flagged): the catch-all Exception handler's response does NOT pass through SecretRedactionMiddleware — Starlette's build_middleware_stack() hands any handler registered for Exception/500 straight to ServerErrorMiddleware, outside all user middleware, sent via the original send. The module docstring previously claimed the opposite. Corrected the docs; the path stays safe because the handler redacts BEFORE building its response (single-point, not defense-in-depth, for this one path) — added a dedicated test proving both facts (zero redact_bytes calls, zero leak).

Post-review: full unit suite 2348/2348 PASS (125 tests in the #467 file, +51 from this round); scripts/doc_drift_audit.py clean.

Cross-family review round 2 (05c02ed, docs/reliability/review-467-dashboard.md): CHANGES REQUIRED — 1 blocker, fixed same finding: 1. Multibyte/Unicode secrets still leaked across a streaming chunk boundary. Round 1's fix sized the carry-back margin from len(v) — Python character count — and redact_bytes() decoded each window to str with errors="replace" before matching. Both are wrong for a non-ASCII literal: (a) a 1500-character emoji secret is 6000 UTF-8 bytes, so a margin of "1500" was 4x too small to hold its visible-but-incomplete tail, and independently (b) decoding a window that ends mid-way through a multi-byte UTF-8 sequence permanently replaces that sequence's trailing bytes with U+FFFD before the rest of the character ever arrives in the next chunk — no margin size fixes a corruption that already happened. Reviewer's exact repro: "🔐" * 1500 (6000 bytes) split into a 5000-byte chunk and a 1000-byte chunk — old code reported max_match_length()==1500, leaked the raw secret, no [REDACTED] anywhere in the output. Fixed by matching secrets as UTF-8 bytes throughout the streaming path, never decoding a window at all: new _build_literal_pattern_bytes(), _GENERIC_PATTERNS_BYTES, _redact_url_credentials_bytes(), _apply_generic_patterns_bytes(), and _SecretStore.redact_bytes() (bytes in, bytes out, no str round-trip); _SecretStore._max_len_bytes is now max(788, len(v.encode("utf-8")) for v in literals) — measured in encoded bytes, not characters. redact_text() (str → str, used only by the exception handler and other complete-in-memory-string call sites, which have no chunk boundary to split) is unchanged. _emit_chunk's redact-before-split invariant from round 1 is unchanged; only what it redacts with, and how it sizes the margin, changed. Regression: new TestStreamingMultibyteBoundaryRegression (10 tests) — direct assertions that the margin is byte-sized for 2-/3-/4-byte characters (é/€/🔐), the reviewer's exact 1500×🔐/5000-1000-byte-split repro run against the REAL (un-overridden) margin, an offset sweep generalizing TestStreamingChunkBoundaryRegression's ASCII sweep to all three character widths (every byte offset around the danger band, including offsets that land mid-character), and two targeted mutation checks — one reverting only the margin formula to character-count, one reverting only redact_bytes to the old decode-based implementation — each confirmed to reproduce a real, unredacted leak. Mutation-checked against the actual pre-fix file (git show HEAD:earnings_dashboard/secret_redaction.py, i.e. the round-1-only version): 3 of the 10 new tests correctly fail against it (test_margin_is_measured_in_encoded_bytes_not_characters, the 🔐-width case of test_margin_scales_with_byte_width_for_each_character_class, test_reviewer_exact_repro_1500_emoji_split_mid_stream); file restored byte-identical afterward (diff confirmed). Post-round-2: full unit suite 2619/2619 PASS (135 tests in the #467 file, +10 from this round); scripts/doc_drift_audit.py clean (12 checks, 0 violations).

Finding 479 — Database access was unattributed and non-durable; add per-service identities plus query-free activity evidence (Sep 26, 2026)

Finding 478 — C-Q2 / G-RS decided (keep all four detection values and the Polymarket price source); the overdue Sep 13 date reminder is replaced by a dashboard-only count trigger (Sep 26, 2026)

Finding 476 — Cron alert scripts silently dead since Feb 15; root crontab unmanaged; a Gateway PID-killer on cron (Sep 26, 2026)

Finding 475 — C-Q2 detection replay is a true simulator of the live detector again (#358 fresh window, #337 gate) (Sep 26, 2026)

Finding 474 — Migration bootstrap baseline verifies full schema effects, not object names (Sep 26, 2026)

Finding 473 — Python requirements pinned to the prod venv; constraints file from its read-only freeze (Sep 26, 2026)

Finding 457 — Live doadmin password and committed API secrets scrubbed from HEAD; DB trusted sources = droplet only (OD8 option (a)) (Sep 26, 2026)

Finding 448 — Every sender can deliver and proves it: ibkr-monitor SLA pager, one definition of "delivered", Healthchecks.io dead-man (Sep 26, 2026)

Finding 446 — Cluster DB-health integrity: read-only, session kills, DB+WAL vs plan, spill, long transactions, btc5m, dashboard, monitor blind (Sep 26, 2026)

Finding 447 — Backup pg_dump stderr captured in full (no pg_dump: er truncation) (Sep 26, 2026)

Finding 471 — High-conviction dashboard query: per-market LATERAL LIMIT 1, not 7-day DISTINCT ON (Sep 26, 2026)

Finding 472 — Dashboard ibkr_reality treated a healthy ~60 s IBKR snapshot as stale after 10 s (Sep 26, 2026)

Finding 456 — AutoRestart login watchdog (page-only, every day, 2FA grace only on evidence of the weekly 06:00 restart) + the trader's last-resort Gateway recycle kept ON by operator decision (Sep 26, 2026)

Finding 449 — Integrity alert policy (per-rule limiter, retry, hysteresis, deploy/maintenance deferral) + Telegram last-send artifact (Sep 26, 2026)

Finding 454 — IBKR in-process recovery: pace current-time probes; preserve reset/latch truth (Sep 26, 2026)

Finding 455 — Filled positions and collector/reconciliation writes survive DB rejection (Sep 26, 2026)

Finding 468 — Prod checkout hygiene: server-written files ignored, SEC cache rule fixed, never git clean on prod (Sep 26, 2026)

Finding 453 — Main-loop stall vs slow signal work: Monday 28 Sep 2026 stopgap, 300 s dated budget (Sep 26, 2026)

Finding 450 — Dashboard TRADE? / API gate is the trader's gate code: one band definition, Config edges, fail-closed depth (§M) — Batch B2 (Sep 26, 2026)

Finding 445 — Dashboard hypertable SQL bounded; short transactions; Sep 25 read-only incident record (Sep 26, 2026)

Finding 444 — Deploy migrations through a ledger: 029 never re-runs; DDL under lock_timeout; output logged (Sep 26, 2026)

Finding 443 — needrestart missed the LIVE xvfb.service / x11vnc.service: apt bounced Xvfb and took the Gateway down (Sep 26, 2026)

Finding 442 — Earnings-date join, 21-day verifier lookahead, timeline DATE SPLIT (CBRL) (Sep 13, 2026)

Finding 441 — 1102 must not nested util.run; attach-bounce after IB() reset (Sep 9, 2026)

Finding 440 — Same-thread req* after 2FA: heartbeat must not util.run on another loop (Sep 7, 2026)

Finding 439 — One last-resort 2FA per week; 7d cooldown (Sep 5, 2026)

Finding 438 — Last-resort recycle after IB() reset: P1 stay-connected; P2 2FA only Sunday or if needed for P1 (Sep 5, 2026)

Finding 437 — Saturday stay-connected: periodic IB() reset; weekday AutoRestart reattach (Sep 5, 2026)

Finding 436 — Weekly-only Sunday 2FA: recycle-off default; attach on new IBC login (Sep 3, 2026)

Finding 435 — Recycle once: in-place first; no 2FA while Gateway is young / cooldown / late-watch (Sep 3, 2026)

Finding 434 — IBKR connect-first: recycle to restore req*; 2FA never outranks a dead path (Sep 3, 2026)

Finding 433 — Weekly-2FA keep probing; header follows request_path_ok (Sep 3, 2026)

Finding 432 — Mode A daily reconnect without 2FA: sticky farm probe after 1100 (Sep 3, 2026)

Finding 431 — Channel-window leak already-evaluated suppress: any non-timing row covers (AEO Sep 2 C-1 skip) (Sep 2, 2026)

Finding 430 — Weekly IBKR 2FA contract: standing Gateway recycle off (Sep 2, 2026)

Finding 429 — IBKR late-login quiet resets on 2FA; Mode B heartbeat window (Sep 2, 2026)

Finding 428 — IBKR late-login attach after recycle 2FA abort; systemd-run bounce (Aug 30, 2026)

Finding 427 — IBKR farm-recovery window after 1100; standing flat-book Gateway recycle (Aug 29, 2026)

Finding 426 — IBKR Gateway zombie: durable-only healthy; TCP-up req* dead exhausts; no extra client on a dead path (Aug 28, 2026)

Finding 425 — etfstack VIX: Yahoo chart KeyError → Ticker.history → Cboe CSV → last-good cache (Aug 28, 2026)

Finding 424 — ASO official IR: 2026-09-01 disputed → 2026-09-09 BMO confirmed (Aug 28, 2026)

Finding 423 — trade-reconcile BBW#274: live log path + LMT fill + IBKR ingest (Aug 28, 2026)

Finding 422 — Trader Session READY + path_ok + connected is Connected, not Unknown (Aug 28, 2026)

Finding 421 — IBKR one session machine: TCP-up always in-place; 1102 clears REAL_DROP; PATH_DEAD wins the header; empty live book no ghost stops (Aug 28, 2026)

Finding 420 — IBKR nightly 1100 leftovers after #419: CancelledError killed heartbeat; 1100 took the in-place exhaust door; VIX/snapshot flood + UNREADABLE + stop wipe (Aug 27, 2026)

Finding 419 — IBKR in-place reconnect had no escape hatch: heartbeat + durable probe wedged the loop; one reset then stop (Aug 26, 2026)

Finding 418 — IBKR reconnect amplifier: one 1100 became hours of self-disconnect; keep the TCP session and last-good book (Aug 26, 2026)

Finding 417 — needrestart missed live-trades + integrity-monitor; overnight freshness:trades 60 min is a false FAIL (Aug 26, 2026)

Finding 416 — unverifiable IBKR book ≠ missing: empty snapshot during request-path dead showed IBKR MISSING (BBWI Aug 26) (Aug 26, 2026)

Finding 415 — IBKR request-path latch leftover: RESTORED then leftover singles re-trip in 21s; 1100 reconnect ignores the 300s cooldown (Aug 26, 2026)

Finding 414 — Channel-window leak false #384: ignore leak when the same market already signal_emitted / admitted (DKS Aug 25 AMC probe) (Aug 25, 2026)

Finding 413 — IBKR request-path latch hysteresis: #412 clear-on-one-probe + reconnect-every-trip paged ~330 Telegrams (Aug 25, 2026)

Finding 412 — IBKR request-path latch: send-dead socket looked "connected" all day; DKS 15:58 batch 0/0 (Aug 24, 2026)

Finding 411 — orderbook archive dual-limit pacing after systemd killed a healthy drain (Aug 24, 2026)

Finding 410 — Aug 21–22 ship verification: record completed to letter; no behavior change (Aug 22, 2026)

Finding 409 — DONE-CLAIM: closer must walk memory.md before saying done; C10/C11 tripwire so a green drift audit is not a memory.md close (Aug 22, 2026)

Finding 408 — collapse leftover-orchestrator "one helper, two copies" twins (IBKR fill ledger + missed-exit startup / scheduled-exit body) (Aug 22, 2026)

Finding 407 — generalize quota-limited HTTP protection (Finnhub shared limiter + Yahoo live wiring + PRN tripwire) + five lean memory.md §F/§H clauses (Aug 22, 2026)

Finding 406 — TIER 2 deferred items registered as Action Queue ON-HOLD rows (T2.2 / T2.6 / T2.8 / T2.11 / T2.13 / T2.15) (Aug 22, 2026)

Finding 405 — one Config.PDT_RESERVE_CAPITAL + integrity always-on list matches Restart=always collectors + unread Config knobs deleted (T2.9 / T2.10 / T2.5 · F18 / F16 / F36 / F9 / F15) (Aug 22, 2026)

Finding 404 — recon Telegram failures logged; fake setup_schedule / schedule.every deleted (T2.14 / T2.7 · F19 / F31 / F38) (Aug 22, 2026)

Finding 403 — INTRADAY / unknown must not become AMC (T2.12 · F33) (Aug 22, 2026)

Finding 402 — CapitalManager deleted after lockstep tests retargeted the live function (T2.1 · F12) + dead orchestrator ExitManager copies gone (Aug 22, 2026)

Finding 401 — one freed-capital helper; 10:30 reservation credits 10:32 / overnight BMO exits (T2.3 / T2.4 · F13 / F5) (Aug 22, 2026)

Finding 400 — get_positions fail-closed: PositionsLookupError on not-connected / exception; successful empty book is still [] (T0.3 leftover · F26 hole the #393 abort could not see) (Aug 22, 2026)

Finding 399 — recon gate on the live batch + committed Polygon default stripped + Jinja TRADE? includes timingKnown + priority LONG cap reads Config lazily (T1.4/T1.6/T1.7/T1.8 · F6/F27/F28/F29) (Aug 22, 2026)

Finding 398 — batch sizing now uses admission s2c_split_pct and recomputes AFTER the #362 day-filter (T1.2/T1.5 · F4/F32) (Aug 22, 2026)

Finding 397 — one _exit_in_progress set: orchestrator aliases ExitManager; queue/batch route through exit_mgr.execute_* (T1.1 · F1) (Aug 22, 2026)

Finding 396 — get_positions is cheap (shares/avgCost, no reqMktData+0.5s); #369 FILES widened to the exit path (T0.6/T0.7 · F10/F2) (Aug 22, 2026)

Finding 395 — PRNewswire RSS fetched once per 30s; detector thread is the one poller (T0.5/T1.3 · F8/F7) (Aug 22, 2026)

Finding 394 — live _persist_position_open now writes pilot_tag (T0.4 · IH-PILOT) (Aug 22, 2026)

Finding 393 — exit fail-open cluster: batch restore-on-fail, #352 lookup fail-closed, MARKET fallback abort on position-check exception (T0.1/T0.2/T0.3 · F25/F3/F26) (Aug 22, 2026)

Finding 392 — operator decision amendment (Aug 21 evening): K-B5 loses its special autonomous status ("we treat everything the same") — the B-5 pilot's revert conditions become the ELEVENTH alert-only contract reminder (contract:k_b5_revert_decision, was consistency:b5_pilot_auto_revert HIGH/FAIL); conditions unchanged verbatim, only the autonomy framing goes (Aug 21, 2026)

Finding 391 — dashboard clean-up phase 2: the operator APPROVED the #387 census's judgment classes IN FULL — all 7 CONSOLIDATE + all 3 REMOVE candidates shipped (Strategy tab 32 → 28 section divs; completed-project chrome gone, history collapsed behind one archive block) (Aug 21, 2026)

Finding 390 — the two live integrity FAILs dealt with honestly: FIVE's disputed earnings date resolved per official IR (both machine sources were WRONG — true date Sep 2 AMC, not Aug 26 or Aug 25) + storage:orderbook_snapshots made DRAIN-AWARE (FAIL→WARN only while the #383 backlog drain is provably active AND a dated GRACE-UNTIL holds; self-reverting, no threshold change) (Aug 21, 2026)

Finding 389 — the §II.7 monitor table made SELF-ALERTING: ten contract:* integrity rules compute every K-contract, D-trigger and scheduled re-judgment condition LIVE and send a 📋 Telegram reminder when due — ALERT-ONLY (every contract outcome returns to the operator; only K-B5's encoded auto-revert acts autonomously [stale — superseded the same evening by the #392 amendment: K-B5 is alert-only too, nothing acts autonomously]); plus the operator's C-Q3 REFRAME recorded across all four surfaces (Aug 21, 2026)

Finding 388 — the four P4 tails closed in one pass: SCRIPT-1 broken import fixed + proven live · F-324 regime/VIX ×100 unit bug VERIFIED REAL and fixed at the producer · F-298 effective_cumulative_volume_at_entry persisted on both entry-persist twins (migration 036; backfill evaluated and SKIPPED with evidence) · F-321 counterfactual price_data_unavailable rows now retry DIRECTLY with a bounded budget and a terminal state (migration 037) (Aug 21, 2026)

Finding 387 — whole-dashboard census (210 surfaces): 8 stale-text FIXes shipped + the #320 tail formally closed (3 never-called render functions + 7 dead DOM-id lookups deleted); CONSOLIDATE/REMOVE judgment candidates delivered as an operator-review proposal (Aug 21, 2026; dashboard-only, NO trading behavior change)

Finding 386 — documentation tree closed top-down: drift-audit C8 (doc-tree orphan check — every live docs/*.md must be reachable from .cursor/memory.md) + C9 (docs/archive/INDEX.md is the new SSOT index for historical docs, mechanically complete in both directions) — 20 archived + 7 banner-frozen docs indexed, 3 missing R4 banners added, the OPS-1 data-archive counterpart linked (Aug 21, 2026; repo-side only — NO trading behavior change)

Finding 385 — memory.md leaned 695 → 316 lines (−55%) per the operator-approved lean-down proposal — every binding rule preserved (verbatim, merged, or pointer); duplicates/drift/superseded content removed with per-item justification; ops/API reference moved to CONTEXT.md/OPERATIONS.md pointers (Aug 21, 2026; docs-only — NO trading behavior change)

Finding 384 — A-14 SHIPPED: both silent-coverage bug classes root-caused and fixed (+$465 demonstrated missed capture) — (1) conviction-fetch failures: Polymarket CLOSES earnings markets ~9:00 ET on earnings day, BEFORE the 10:29 ET AMC conviction check, and the #333 live-Gamma fallback queried without closed=true (the #375 exclusive-filter trap) so it got [] for exactly the markets it existed to save (TMUS/AXP/PYPL class); (2) channel-window leaks: inconsistent timing resolution made BOTH channels say "not mine" until the right channel's entry moment passed (MA: AMC one scan, BMO the next; WWD: only ever seen by the wrong channel) — and every one of these died at DEBUG/WARNING with a generic skip_reason (Aug 21, 2026)

Finding 383 — OPS-1 CLOSED via operator-approved option C: orderbook_snapshots chunks >90 d are now losslessly archived to DigitalOcean Spaces behind a MANDATORY per-chunk byte-identity proof gate, then dropped from the live hypertable — acceptance-proven end-to-end on the TTD/RKLB/SG baseline (archive→drop→ArchivedRangeError→restore→replay byte-match→re-drop), nightly automation armed, and the operator's binding documentation condition satisfied with a full runbook (Aug 21, 2026)

Finding 382 — A-1 IMPLEMENTED (operator approved #372 in full): resolution-as-truth SHIPPED — the 3-tier label system is live, the §F backfill landed and verified (1,273 resolutions stamped, 141 relabels, 29 fills, 85 trade corrections, 2 condition_id repairs, 0 conflicts, second run plans ZERO), and the new drift rule caught REAL template drift on its first production cycle (6 whole-dollar-strike markets → parser v2) (Aug 21, 2026)

Finding 381 — A-13 SHIPPED: counterfactual window-truncation fixed with a layered never-again guard — the CF cron priced trades MID-FLIGHT (1,100 of 3,355 stored prices carried counterfactual_computed_at < exit_datetime_scheduled; DELL 5/28 stored +2.2% vs +32.1% true — a $200 outlier under a $14 label): the job selected any unpriced row regardless of whether the simulated exit had happened and _pick_exit_bar silently returned the last available bar; the resolver's skipped-signals pass had the same hole for yesterday's AMC rows (they exit TODAY 15:57 ET, priced at the 06:00 pass) (Aug 21, 2026)

Finding 380 — A-12 SHIPPED: RuleB apparatus REMOVED entirely (operator option (b)) + the "2.3 GB unrotated trader log" contradiction resolved — BOTH prior claims were half-right about DIFFERENT files: the 2.33 GB unrotated, still-growing earnings_trader.log is real at [hidden]/logs/ (systemd append: target, OUTSIDE the #274 logrotate coverage), while the A-11 "0 B since May 9" observation was the legacy /var/log/polymarket/ twin that stopped receiving writes; journald additionally held 3.9 GB with NO retention bound (Aug 21, 2026)

Finding 379 — A-9 SHIPPED: PRNewswire demoted to corroboration-only (never a sole exit trigger, at any grade) and the BusinessWire poller REMOVED — the #342 arbiter's persistence path could still admit a solo PRNewswire contra claim on non-GAAP markets (CONTENT_EXIT_CORROBORATION_SEC un-vetoed with a neutral/stale crowd); a frozen structural set now refuses persistence-alone admission for corroboration-only witnesses, and BusinessWire (842k failed/error log lines, ZERO detections in the entire history — truth-audit §5) is deleted outright (Aug 21, 2026)

Finding 378 — A-5 SHIPPED: admitted-stage opportunity-log rows restored — root cause: they NEVER flowed live. All 369 historical skip_stage='admitted' rows were the May-29 synthetic backfill of pre-Apr-22 markets (backfill_opp_log_pre_apr22.py, timing_channel='backfill'; coverage ends Apr 8, 2026), so the W2 trade census read the backfill's coverage edge as a "silent April 2026 stop". The live funnel now writes a POSITIVE admitted row at batch scheduling, and a coverage integrity rule makes any future silence a named FAIL (Aug 21, 2026)

Finding 377 — OPS-3 server hygiene executed end-to-end: the 71-deep git stash pile audited (every entry inspected — 65 pure regenerated-report auto-stashes, 6 with other content ARCHIVED as patches before pruning), strategy_analysis_report.json untracked + gitignored so the pile can never regrow (the weekly regen timer + dashboard + integrity rule all read it from DISK — unaffected), 3 stray .env backups key-audited and archived out of the repo dir, the deleted-probe-script cruft resolved, and the orphan MAX_TOTAL_EXPOSURE=500 env line removed with a dated comment (Aug 21, 2026)

Finding 376 — OPS-1 BLOCKED at the platform: DigitalOcean managed Postgres ships the APACHE-licensed TimescaleDB, which excludes native columnstore compression — the approved lossless 90-day compression policy for orderbook_snapshots (21.7 GB, WARN since Jul 27) cannot be implemented transparently on the current instance; every fallback benchmarked (TOAST tuning = zero gain, measured), decision pack delivered, NOTHING lossy improvised per the operator's standing instruction; fidelity-proof harness committed with the pre-change baseline captured (Aug 21, 2026)

Finding 375 — A-3 SHIPPED: the 5-part volume-integrity bundle (#365's expansion) — monotonic GREATEST guard on all three markets.volume_total writers; closed-market Gamma fetches fixed via a closed=true RETRY (the closed param is an EXCLUSIVE filter — verified empirically, an unconditional param would have blinded every OPEN-market fetch); real pagination for the events sync (API silently caps at 100/page; the old limit=500 single-shots were no-ops beyond page 1); entered/past dashboard rows now display the ADMISSION-TIME volume from the opportunity log with alarm-vs-info icon semantics; the 24h-rolling fallback carries a visible 24h~ marker (Aug 21, 2026)

Finding 374 — A-11 CLOSED with a binding documentation-discipline framework: a repo-wide doc-drift census (every docs/.md, every scripts/analysis_357/.md report, every factual code comment in src/, config constants vs their comments, the dashboard record builders, memory.md itself) found 159 places where a written claim disagreed with code/data reality; ALL fixed, root causes diagnosed, and the discipline made structural — NEW binding docs/DOCUMENTATION_DISCIPLINE.md (rules R1–R7) + NEW scripts/doc_drift_audit.py (7 mechanical checks) wired into the unit suite so known drift classes now FAIL the build (Aug 21, 2026)

Finding 373 — OPS-2: the freshness:trades integrity evaluator intermittently died with out of shared memory — increase max_locks_per_transaction — root-caused to unbounded MAX(time) planning that locks EVERY hypertable chunk + its indexes in one transaction (measured: 7,079 locks for trades' 1,010 chunks × 7 relations vs a 6,400-entry shared lock table); fixed BOTH sides — evaluators now bound their scan (30 locks measured) AND max_locks_per_transaction raised 64→512 via the DigitalOcean config API, computed from chunk counts with 18-month growth headroom (Aug 21, 2026; DB parameter APPLIED 07:37 UTC in the overnight window, ~7 s restart, trader untouched)

Finding 372 — A-1 REDIRECTED + deep dive delivered: our ground truth must be the Polymarket market's OWN benchmark/resolution, not our EPS convention — the true mislabel count is 141 of 1,239 (11.4%, double the old ~64–71 estimate) + 12 poisoned trade flags (not 14); 3-tier resolution-as-truth proposal APPROVED IN FULL and IMPLEMENTED by Finding #382 (Aug 21, 2026)

Finding 371 — A-2: the resolver's 6-hour retry guard keyed on the SHARED updated_at column, which the hourly earnings-sync timer touches on every sync — unresolved rows were re-locked every hour, the guard never released, and actual_beat label coverage silently DECAYED (76.0% measured Aug 16 vs the documented ~95-99.8%); fixed structurally with a resolver-OWNED resolver_attempted_at column (migration 033) (Aug 21, 2026)

Finding 370 — OPS-5: polymarket-trade-reconcile-full.service failed since Aug 1 with ZERO alerts for 3 weeks — the exit-2 was CORRECT (#251b semantics; 16 real trade issues), but the unit had no EnvironmentFile so Telegram was silently disabled, AND no integrity rule watched it; fixed end-to-end + services:systemd_unit_health dynamic failed-unit sweep closes the meta-hole class (Aug 21, 2026; DEPLOYED Aug 21, 2026 06:56 UTC, first clean full-history run 06:58 UTC, timer re-armed Sep 1)

Finding 369 — OPS-4: nightly trader deaths at ~04:15 UTC AND ~06:06 UTC (2/night, daily since ≥Aug 15) — NOT OOM, NOT a neighbor job: unbounded ib_insync request-response calls (11 reqAllOpenOrders + 3 qualifyContracts + 1 reqContractDetails sites the #354 sweep MISSED) hung the main loop on the half-dead socket left by the nightly IBKR server reset (~00:13 ET) / IBC 06:00 UTC Gateway AutoRestart; the 120s watchdog then force-killed the process. All sites bounded + needrestart trading-stack exclusions + trader:restart_tripwire integrity rule (Aug 21, 2026; DEPLOYED Aug 21, 2026 06:56 UTC)

Finding 368 — Dashboard tab switching dead after the #367 deploy: Jinja autoescape corrupted a quoted JS literal in the System-tab mode map into &#39; — a SyntaxError that killed the ONE main script block, so showTab was never defined (Aug 21, 2026; dashboard-only fix, targeted dashboard-service restart — trader NOT restarted)

Finding 367 — Phase-1 transition encoded: "TESTING IN PRODUCTION" → Phase 1 with a P&L fresh start and a from-zero trade counter, all derived from ONE config boundary (PHASE1_START_ET); History-tab ROI re-cut to three stacked windows (All-time / Testing in Production / Phase 1); "Levrage" header typo fixed (Aug 21, 2026; records/dashboard only — NO trading-behavior change; DEPLOYED Aug 21, 2026 04:34 UTC riding the #366 deploy)

Finding 366 — PKG-357 IMPLEMENTED: the full ONE-PACKAGE manifest (#364) shipped in code on the operator's PKG-GO — $10k capital step (C-Q1/FUND-1 lockstep) + S2c equal-split sizing with $9k activation gate + B-4 3-tier priority table + C-1 LONG band re-closed at 0.90 + RES M1+M5 tonight-only reservation with rank-3 carve-out + A-10 twin deletion + WAVE-RT retunes + CLB-1/A-6 daily-loss sole brake + B-5 small-cap pilot (own commit) + M-VOL V-BLEND shadow — 9 implementation commits + ship mechanics, full unit suite green (Aug 20, 2026; DEPLOYED Aug 21, 2026 04:34 UTC)

Finding #366 — Update (Aug 21, 2026): the leverage question CLOSED — keep 2× Reg-T margin + the 1.50× total-exposure cap unchanged (operator decision LEV-1; NO code change, NO live-behavior change)

Finding 365 — Operator false-alarm audit (BABA displayed $930, below the $1,000 gate, on an ENTERED market): display artifact, not a breach — the dashboard's last-resort fallback substitutes a post-close 24h-ROLLING volume into the cumulative-volume slot after BOTH cumulative sources fail (earnings-feed sync silently capped at 100 markets by the API; all three Gamma cumulative fetches omit closed=true so closed markets return empty); 97-trade class audit clean (93 provably passed the gate; all 43 below-gate-today displays are artifacts); A-3 EXPANDED from a monotonic write guard into a 5-part bundle (Aug 20, 2026; records only — NO code shipped, NO deploys, production DB read-only) (header line reconstructed Aug 21, 2026 — the original was mangled in a rebase; body below is untouched. The 5-part bundle SHIPPED as Finding #375.)

Finding 364.b — FUND-1 re-specified as a pure CONFIG/CODE change (verified live: the equity is ALREADY in the account — no money movement) + the P1 surface audit re-run at census grade for the #363/#364 package additions (CLB-1 + A-6 · WAVE-RT · MSP-1 · FUND-1): census extended 148 → 161 surfaces, flags N14–N23, two v1 claims corrected + one undercount fixed; binding execution-discipline note added to the plan (Aug 19, 2026, latest; records only — NO trading-code changes, NO deploys, production DB read-only)

Finding 364 — Operator decisions recorded (Aug 19, 2026, late): CLB-1 APPROVED as recommended (retire the consecutive-loss breaker at package ship — counter display-only, daily-loss 5% becomes the SOLE loss brake, hidden sizing-halving re-keyed to daily-loss tiers, A-6 restart-persistence elevated into the same ship set); the ONE-PACKAGE PKG-357 manifest CONFIRMED and recorded (4 scope items the communicated manifest had missed were added: the C-Q1 capital step, MSP-1, D-6/D-7 retirement-at-ship, and the changelog/supersession ship mechanics); the pending-decisions register VERIFIED and recorded (1 correction: the Class A queue is per-commit NOW work, not post-package) (Aug 19, 2026; records only — NO trading-code changes, NO deploys, production DB untouched)

Finding 363 — Commissioned circuit-breaker & risk-control audit under the P1 package (triggered by today's 10:03 ET "3 consecutive losses — 4h pause" firing): the consecutive-loss breaker's count semantics BREAK under S2c (70% of 3-loss runs contain ≥2 same-wave losses; 49 firings/yr in package replay, 28 blocking nothing, money value ≈ $0 outlier-noise) — verdict RETIRE the counter, retain the daily-loss 5% breaker as the sole loss brake (1 firing/47wk, $0 foregone), persist breaker state (A-6 into the package ship set); full 33-control inventory: 24 SAFE · 6 NEEDS-RETUNE · 3 REDESIGN (Aug 19, 2026; analysis + records only — NO trading-code changes, NO deploys)

Finding 362 — VIK entered ONE DAY EARLY (Mon Aug-17 for a Wed Aug-19 BMO release): the #333 end_time OR-branch selected it into Monday's window, #349 correctly resolved a TUESDAY entry, NO guard checks the entry DATE (#359 only rejects LATE), and run_bmo_cycle's "All BMO have same entry time" assumption executed it at Monday's batch time — entry-date invariant + window re-check + integrity rule + reservation dedup + dashboard ET/divergence fixes shipped (Aug 18, 2026)

Finding 361.b — Corrective amendment to #361: the "recorded across every encoding" sweep was INCOMPLETE and #361 was never deployed — the operator caught stale [PENDING-OPERATOR-APPROVAL] C-1/RES/B-5 rows on the LIVE dashboard ("why is this not updated? what else did you miss?" — the #334 missed-surfaces lesson repeating); full stale-state re-sweep + status-vs-description tripwire + deploy (Aug 17, 2026; records/display only — NO trading-behavior change)

Finding 361 — Operator APPROVED the three open Part II decisions of the #357 program (C-1 re-close the LONG cap at 0.90 · RES the M1+M5 reservation mechanism · B-5 the small-cap LONG 0.80–0.95 pre-registered pilot): new status APPROVED-AWAITING-IMPLEMENTATION recorded across every encoding, the #360 action plan FINALIZED, and the operator-requested plain-language reservation explainer added to doc + dashboard (Aug 17, 2026; records only — NOTHING implemented, NOTHING deployed)

Finding 360 — Strategy-tab decision-state reconciliation after the #357 re-issue: the dashboard's Re-validation section + Action Queue brought into lockstep with the Parts I–IV decision record (C-Q1/S2c/B-4 DECIDED-not-implemented with the HARD PDT constraint, B-3/A-8 implemented-pending-deploy, C-2 superseded by B-4, D-2 discharged), plus the ordered action plan and the consolidated 16-row §II.7 monitor table rendered on the tab (Aug 16, 2026 late; records/dashboard only — NO trading behavior)

Finding 359 — Stale-signal guard at ADMISSION (approved B-3 of the #357 revalidation queue): a signal whose intended entry time is already >5 min past is rejected as born_dead_schedule BEFORE it consumes an admission slot or capital; the W6 Bug-B _log_exposure_skip date-field bug fixed in passing (same code path) (Aug 16, 2026)

Finding 358 — Print-density collapse (A-8) ROOT-CAUSED: three collector bugs (markets-table lifecycle leak throttling the poller to ~1 print/5–9 min per market; WebSocket collector with a frozen subscription list living OUTSIDE the deploy pipeline; WS writer storing order-book LEVEL prices of EITHER token — NO uninverted — as yes_price) + the #351 stale-median geometry still live on the PRIMARY detection path (Aug 16, 2026)

Finding 357 — FULL STRATEGY RE-VALIDATION at n=183 (9-workstream program + synthesis): core premise refuted as stated, 0.90–0.95 band FAILED its contractual re-gauntlet, resolver truth bug corrected the calibration story, volume-collapse narrative re-based — ZERO trading changes shipped; all candidates queued for operator decision (Aug 16, 2026)

Finding 356 — Guard-override tripwire: a disabled live defense now polices its own restoration — undated or past-expiry .env guard overrides FAIL integrity by name (Aug 9, 2026)

Finding 355 — Aug-5 double-breaker morning: operator override of the session stop breaker for tonight's BMO wave + breaker-alert pause_hours: 0 display bug + COMPLETE-STATE readiness rule (Aug 5, 2026)

Finding 354 — Trader froze mid-batch at 15:58 ET on an un-timed-out IBKR request: 6 BMO entries (~$3,566) lost while IBKR was healthy; every ib_insync request-response call now runs under a hard deadline, and batch prep submits what's ready when its own deadline expires (Aug 3, 2026)

Finding 353 — History-tab Error Log window + error-ledger backfill (all past operational error losses inventoried into trading_errors) + SBUX #349 reclassification + twin-table repair + 5 accounting/doc corrections (Jul 31, 2026)

Finding 352 — VIRT −77 NAKED SHORT: eight pre-market instant-exit cycles stacked queued LIMIT+MARKET sell pairs that NYSE held until the open — seven surviving MARKET orders fired in the same second at 09:30:00 ET, selling 77 shares against a flat book (−$237, operational error) (Jul 30, 2026)

Finding 352.b — AMENDMENT (same day, Jul 30, 2026 evening): the #352 dedup guard could deadlock on a STALE limit in a falling market — made marketability-aware (cancel-and-reprice)

Finding 351 — Hyatt false "earnings miss" exit: the #342 arbiter's crowd witness was blind on a sparse market — an unwindowed median-of-5 let one fresh 0.9995 BEAT pin be outvoted by 25-minute-old pre-release prints, so a GAAP-vs-adjusted parse error exited a CORRECT LONG (Jul 30, 2026)

Finding 350 — Polygon/Massive subscription silently dropped to the free tier (5 req/min): five services degraded over two days, etfstack-signal hard-failed, and #348's root cause was corrected; fixed with a cross-process token-bucket limiter, plan-downgrade detection that alarms BY NAME, quota-window-escaping backoff, and a live yfinance fallback for etfstack (Jul 29, 2026 late)

Finding 349 — SBUX instant entry/exit: an earnings-date WAR between the single-source verifier and the Polymarket-derived sync produced a trade whose scheduled exit was 5.5h in the PAST at entry; fixed with multi-source consensus verification, confirmed-date write locks, admission guards, and an entry-time invariant (Jul 29, 2026)

Finding 347 — 150-trade TESTING_PROD checkpoint: operator passed 150 DB-counted trades and chose to CONTINUE — auto-pause removed (it could never have fired anyway: the counter was in-memory and reset to 0 on every restart) (Jul 29, 2026)

Finding 346 — BMO-reservation override threshold was 0.90 on the LIVE admission path: #333's lockstep raised capital_manager._bmo_reservation_threshold to 0.95 but MISSED the trading_orchestrator twin — the copy check_amc_signals actually reads (Jul 22, 2026)

Finding 345 — polymarket-backup.service failing daily since Jul 16: cleanup ran AFTER pg_dump, so once the disk filled the failure became PERMANENT (create → ENOSPC → exit before cleanup → stale 3.4 GB backups never deleted) (Jul 22, 2026)

Finding 344 — FALSE-DETECTION exits were labeled instant_exit:earnings_miss/beat — corrupting every future "how many PM predictions missed" statistic; operator caught that the #342 repair deliberately KEPT PNC's wrong label ("historical fact" + note) violating the #328 honest-labels precedent → 5 rows relabeled instant_exit:false_detection, 2 poisoned actual_beat values corrected, and a self-detecting integrity rule added (Jul 15, 2026)

Finding 343 — Same-day outcomes went dark after #342: JNJ/CTAS beat (crowd pinned 0.9995) but the dashboard showed nothing — the resolver (now the SOLE actual_beat writer) only ran daily at 06:00 ET, BEFORE BMO releases → intraday resolver light pass + display-only crowd read on position rows (Jul 15, 2026)

Finding 342 — PNC false MISS exit 5 minutes after a real BEAT: PRNewswire parsed the DIVIDEND ("to $2.00 per share") as EPS while the crowd sat at 0.91 → the LAST unilateral-authority source class (content/wire) is closed with a UNIFIED EXIT ARBITER — no source may exit uncorroborated, settle actual_beat, or stop monitoring (Jul 15, 2026)

Finding 341 — Replay of #339's decision-grade thresholds over ALL 13 high-liquidity resolved markets exposed a whipsaw false-exit class AND proved same-side Polymarket "resolutions" untrustworthy (WFC pinned ≥0.98 for 1.5h before flipping to its true MISS) → the Polymarket price source is now EXIT-ONLY: contra-position signals only, 5-min sustained confirmation with 60s grace, never settles actual_beat, never stops monitoring (Jul 14, 2026)

Finding 340 — Integrity rules caught #339's blast radius exactly as designed (6 rows NULL conviction_at_exit / hold_duration from fallback-path closes) → data repaired, fallback path widened to stop losing recoverable fields, and a deploy-time close-persist SQL CONTRACT CHECK added so PG-type-resolution bugs can never again wait for the first live close (Jul 14, 2026)

Finding 339 — WFC miss went undetected for 48 minutes (bled to the stop) + BOTH Jul-14 stop closes lost their exit commissions: three compounding bugs — the all-of-5 print rule was unsatisfiable on real mixed streams, consensus_eps was silently $0.00 (Yahoo detection disabled), and a COALESCE integer-cast crashed the primary close-persist on the first fractional Tiered commission (Jul 14, 2026)

Finding 338 — Canonical chronological Strategy Changelog (operator-directed): July's changes were scattered across Live Config prose / Action Queue notes / History markers with no chronological thread → ONE standardized, test-enforced changelog module now feeds both the new Strategy-tab 📜 Strategy Evolution timeline and the History-chart markers; format is MANDATORY for all future strategy changes (memory.md §O) (Jul 14, 2026)

Finding 337 — JPM false BEAT "detection" 11 hours BEFORE its release: a SUSTAINED pre-release Polymarket price spike (0.997 for ~3 min) legitimately cleared every #336 price-robustness check → detector confirmed BEAT at 8:26 PM ET the night before, WROTE earnings_markets.actual_beat=True, and STOPPED monitoring the position — the missing dimension was the CLOCK, now added as a release-window gate on the price-based source (Jul 14, 2026)

Finding 336 — False pre-event "BEAT - PM" outcome badges (JPM/GS the night BEFORE earnings) — single noisy price_snapshots print ≥0.99 read as "market resolved"; dashboard badge gated on release window + earnings detector hardened to require ALL of the last N prints at the extreme (Jul 13, 2026)

Finding 335 — Busy-week capital bundle (operator-directed, pre-Jul-14 wave): exposure cap 1.40 → 1.50 (exactly 6 flat-25% slots — 1.40 stranded 15%/night) + AMC/BMO ADMISSION now nets out about-to-exit positions (a full overnight book auto-rejected EVERY next-morning AMC at "would exceed cap" although real exposure at entry was ~0) + BMO-reservation check re-based from raw IBKR day-trade buying power (~$110k — never bound) to the strategy budget (Jul 11, 2026)

Finding 334 — Priority tiers re-numbered for the reopened 0.90-0.95 LONG band (operator-directed): base 0-6 → 0-8 with the 90-95 band ranked LOWEST within each mcap group → full scale 0-10 with the #313 volume bump; the #333 rollout had MISSED priority.py entirely — 90-95 signals would have scored 0 and been silently skipped at priority_filter despite the relaxed cap (Jul 11, 2026)

Finding 333 — Exclusion-funnel coverage leaks FIXED (channel-window hole + conviction/timing fallback hardening) + gap-aware 1m-bar replay of the #202-skipped 0.90-0.95 LONG band: +$483/78 markets net-of-commissions, ex-top-2 POSITIVE overall but sub-slices outlier-carried → relax decision PENDING-OPERATOR (Jul 10, 2026)

Finding 332 — Tiered-plan commission alignment audit: exit-commission was INT-ROUNDED before persist (would corrupt Tiered legs), $0.00 commissions treated as missing, and gross_pnl had inconsistent semantics between writers (97 rows corrected) + History-tab P&L upgrade (gross/commissions cards + launch-to-date progression chart with strategy-change markers + rule-era table) (Jul 6, 2026)

Finding 331 — P&L display was commission-blind: every "Total P&L" surface showed gross price P&L while IBKR commissions (ground truth per #329) consume ~55% of it at test-size positions → all Total P&L displays now NET of commissions via ONE shared SQL definition (Jul 5, 2026; dashboard/display — NO trading change)

Finding 314.f — Sizing-up re-validation at the live 1.40 cap (Jul 5, 2026): flat 25/25/25 (+$7.40/wk) and 35/25/18 (+$6.08/wk) BOTH gauntlet-PASS and STRENGTHENED vs May 31 → SHIPPED FLAT 25/25/25 Jul 6, 2026 (operator-confirmed via AskQuestion)

Finding 330 — RuleB simulator arm was structurally OPTIMISTIC (gap-fill bug): window-blocked stops were "filled" at the STOP PRICE even when the price was still through the stop at rearm → gap-aware fix flips RuleB from +$224/100 to −$209/100 (FAILED-GAUNTLET); RuleB Phase-2 active flip CANCELLED + dry-run log-spam fixed (Jul 5, 2026; simulator + decision reversal — NO live trading behavior change)

Finding 329 — Commission-capture completeness + IBKR Flex ground-truth reconcile: live commissionReport covers most trades, but the Flex token had EXPIRED (only Jan captured) → refreshed token + new 365-day query + weekly FAIL-SOFT flex-reconcile timer; backfilled recoverable gaps + fixed a total_commission staleness ($206→$229) (Jul 5, 2026; data-integrity + ops — full-history backfill pending IBKR query propagation)

Finding 328 — KR exit MISLABELED "Scheduled Exit": the instant-exit (earnings MISS) FILLED, then the trader CRASHED on a self.position_manager typo before persisting → orphaned → reconciliation blanket-labeled it scheduled_exit:reconciled (RESOLVED Jun 18, 2026; PRODUCTION FIX — crash + honest labels + durable logs)

Finding 327 — #325 REGRESSION: the dense LIMIT broke the "latest conviction" path (no time window) → it medianed over a market's WHOLE history → KR + ACN traded that #202 should have SKIPPED (RESOLVED Jun 18, 2026; PRODUCTION FIX — trader conviction path)

Finding 326 — api-health cry-wolf: Polymarket Pages→App Router migration broke the website buildId scraper → daily service FAILURE (ZERO trading impact); rewrote the canary App-Router-aware + demoted to non-fatal (RESOLVED Jun 14, 2026; ops/integrity, NO trading change)

Finding 325 — Conviction under-sampling bug: trader entered an ADBE LONG that #202 should have SKIPPED (true ~0.97, computed 0.79); fixed with dense robust median + IQR reliability gate (RESOLVED Jun 11, 2026; PRODUCTION FIX — trader conviction path)

Finding 322 — Full dashboard staleness audit: every tab checked line-by-line; ~30 stale static facts corrected (RESOLVED May 31, 2026; dashboard-only, NO trading change)

Finding 322.b — Full repo-wide staleness re-audit: ALL 29 files (3 dashboard render sources + 26 docs, ~47k lines) read line-by-line; ~110 stale current-state claims corrected (RESOLVED May 31, 2026; docs + dashboard render code, NO trading change)

Finding 322.c — Audit follow-ups: wire dashboard risk panel + data-api dependency probe + MAX_POSITION_SIZE_PCT vestigial re-align (RESOLVED May 31, 2026; NO trading behavior change; trader restarted to activate)

Finding 322.d — Phase 5: ROI re-baseline (3-epoch ladder + net-new metrics + hybrid forward projection; caution-first) (RESOLVED May 31, 2026; analysis/dashboard/docs; NO trading change)

Finding 322.e — RuleB Phase 1 DRY-RUN actually ENABLED in production (was shipped-but-never-run) — RULEB_ENABLED False→True, MODE stays dry_run (RESOLVED May 31, 2026; logging only, NO live order change)

Finding 322.f — Pre-Monday trading-readiness audit (after the week's #311-#322.e changes) → GO; fixed 7 stale tests caught by the full suite (RESOLVED May 31, 2026; test-only fixes, NO prod behavior change)

Finding 321 — Strategy-analysis data completeness: backfill Polygon counterfactual-P&L + outcome gaps, then full simulator re-validation — all KEEP conclusions HOLD (RESOLVED May 31, 2026; data backfill, NO code/strategy change)

Finding 320 — Phase 4 (remainder): Unified Simulator dashboard consolidation — replace Universe View + Finding-A + Backtest with one canonical umbrella + archive Q3 sections + 5 breakdowns → 2 (RESOLVED May 31, 2026; dashboard-only, NO trading change)

Finding 319 — PR 5: Depth-gate v2 widened reconstruction — the "unreliable orderbook" skip is a roughly break-even low-information-signal filter → KEEP (RESOLVED May 31, 2026; offline-only, NO production change; operator-confirmed KEEP; closes Finding #283)

count counterfactual P&L (real $, per-tier scaled)
losers BLOCKED (gate saved us) 13 −$142.56 avoided
winners KILLED (gate cost us) 9 +$165.09
net effect of removing the gate 22 +$22.54 over ~7 months (≈ +$1/trade, noise)

Finding 318 — PR 4: Regime classifier v2 sweep — faster detectors add no robust ROI → KEEP compound_v1 (RESOLVED May 31, 2026; offline-only, NO production change; operator-confirmed KEEP)

classifier LIVE-ONLY (primary) Δ vs compound_v1 CAPACITY-AWARE 1.40 (cross-check) Δ
compound_or_dd5 (#278 rec.) +$0.00 PASS (inert) +$13.58 FAIL-EX-TOP-2
below_ema20 (fastest) +$70.31 (+$3.5/wk) PASS (ex-top-2 +$136/100 strengthens) −$17.02 FAIL-EX-TOP-2 (headline −$5.6/100, ex-top-2 flips +$35)
below_ma50 +$1.01 PASS (inert) −$24.90 FAIL-EX-TOP-2
dd_3pct −$40.39 FAIL-SPILLOVER −$82.05 FAIL-EX-TOP-2
compound_symmetric −$54.47 FAIL-SPILLOVER (skips fewer → keeps losers) +$0.00 PASS (inert)

Finding 317 — SHIP: Simplify daily exposure tiering to a single uniform cap (RESOLVED May 31, 2026; operator-CONFIRMED production change)

Finding 316 — Exposure daily-tiering re-eval (#285 Phase B): rare-day 60% cap is non-binding → KEEP 60/140/140 (RESOLVED May 31, 2026; offline-only, NO production change)

Finding 315 — MAX_TOTAL_EXPOSURE_PCT has NO live consumer → Finding #313's capital-cap raise is inert in production (FLAGGED May 31, 2026; capital-relevant; awaiting operator decision; NO production change made)

Finding 314.e — PR 3.b.3: 4 deferred SKIP-rule candidates, real-scaled + bias gauntlet (ANALYSIS COMPLETE May 31, 2026; offline-only, NO production change — ship decision: operator chose defer-and-monitor)

Cell Hypothetical (Finding #313) Real-scaled
Large + LONG[0.80,0.90) + vol≥$10k n=16, 25% WR, −$124 avg n=15, 26.7% WR, −$6.00 avg, total −$90.03
Large + SHORT + vol[$2k,$5k) n=12, 0% WR n=3, 0% WR, −$10.74 avg, total −$32.23
Mid + LONG[0.80,0.90) + vol[$1k,$2k) n=62, 19% WR, −$27 avg n=64, 20.3% WR, −$0.90 avg, total −$57.32
Large + LONG[0.80,0.90) + vol[$1k,$2k) n=20, 10% WR, −$20 avg n=22, 18.2% WR, +$0.93 avg (POSITIVE), total +$20.39

Two cells materially changed under real scaling (validating the operator's correction): Large+SHORT+$2k-$5k is only n=3 on real data (not 12 — below the gauntlet floor), and Large+LONG+$1k-$2k is actually NET-POSITIVE real (the hypothetical's −$20 sign was wrong). - Capacity-aware sweep + gauntlet (568 signals / 36 wk / $2,500 × 2.0 = $5,000 max gross; baseline production_post_313 = +$607.06 / +$16.86/wk / maxDD −$274.61):

SKIP variant admits total $ Δ/wk maxDD gauntlet ship-eligible?
skip Large+LONG[.80,.90)+≥$10k 396 +$680.28 +$2.04 −$245.17 PASS YES
skip Mid+LONG[.80,.90)+$1k-$2k 373 +$647.36 +$1.12 −$274.61 PASS YES
skip Large+SHORT+$2k-$5k 408 +$648.59 +$1.16 −$249.20 FAIL-EX-REPEAT-TICKERS NO (n=3, single-name)
skip Large+LONG[.80,.90)+$1k-$2k 400 +$577.39 −$0.82 −$274.61 FAIL-EX-TOP-2 NO (cell is positive — skipping hurts)
skip ALL 4 combined 345 +$802.17 +$5.42 −$219.76 PASS YES (best total + best maxDD)

Finding 314.d — PR 3.b.5 SHIP: Hybrid seasonal gate variant (RESOLVED May 31, 2026; offline-only analysis, NO production behavior change — KEEP uniform $1k gate)

Variant admits rule-skips cap-skips total $ per-wk E[wk] maxDD WR ex-5-outlier Δ/wk vs base gauntlet
uniform_$1k (baseline) 409 101 58 +$607.06 +$16.86 +$22.48 −$274.61 36.4% +$544.30 — (baseline)
hybrid_$2k_peak_$1k_off 318 245 5 +$290.80 +$8.08 +$11.18 −$314.67 38.4% +$228.04 −$8.78 FAIL-SPILLOVER
uniform_$2k (anchor) 295 268 5 +$241.22 +$6.70 +$10.05 −$314.67 38.6% +$178.46 −$10.16 FAIL-SPILLOVER

Finding 314.c — PR 3.b.4 SHIP: Sizing variants sweep (RESOLVED May 31, 2026; offline-only analysis, NO production behavior change — KEEP 30/22/15 sizing)

Variant admits cap-skips total $ per-wk E[wk] maxDD 5p ex-5-outlier Δ/wk vs base gauntlet
30/22/15 (baseline) 409 58 +$607.06 +$16.86 +$22.48 −$274.61 −$98.60 +$544.30 — (baseline)
25/18/12 424 43 +$443.21 +$12.31 +$16.41 −$238.47 −$81.48 +$391.21 −$4.55 FAIL-SPILLOVER
20/15/10 437 30 +$362.06 +$10.06 +$13.41 −$196.68 −$66.43 +$319.91 −$6.80 FAIL-SPILLOVER
22/16/11 432 35 +$397.75 +$11.05 +$14.73 −$212.99 −$72.03 +$351.84 −$5.81 FAIL-SPILLOVER
flat 25/25/25 405 62 +$657.74 +$18.27 +$24.36 −$330.27 −$98.60 +$599.37 +$1.41 FAIL-EX-REPEAT-TICKERS
flat 18/18/18 428 39 +$457.43 +$12.71 +$16.94 −$249.48 −$70.99 +$415.40 −$4.15 FAIL-SPILLOVER
33/22/15 407 60 +$581.29 +$16.15 +$21.53 −$314.58 −$103.90 +$514.24 −$0.71 FAIL-EX-REPEAT-TICKERS
35/25/18 397 70 +$634.08 +$17.61 +$23.48 −$339.55 −$113.65 +$561.46 +$0.75 WARN-OUTLIER-DOMINATED

Finding 314.b — PR 3.b.2 SHIP: Capacity-aware re-validation of Finding #311 (RESOLVED May 30, 2026 LATE LATE evening)

Finding 314 — PR 3.b.1 SHIP: Capital simulator foundation (RESOLVED May 30, 2026 LATE LATE evening; offline-only foundation, NO production behavior change)

Finding 313 — PR 3.a SHIP: Volume-tier priority bump + capital cap 1.40→2.00 + NYSE dead-code cleanup (RESOLVED May 30, 2026 LATE evening; third real-money strategy ship of the day)

Finding 312 — v2 #6 SHIP: Daily 3-stop session breaker + DirectionPolicySpec doc-bug fix (RESOLVED May 30, 2026 LATE evening; second real-money strategy ship today + simulator-doc correctness fix)

Finding 311 re-validation note (same number as the ship entry below — NOT a second assignment; RESOLVED May 30, 2026)

Re-validation Note (May 30, 2026 LATE LATE evening — PR 3.b.2 capacity-aware sweep update): Operator-requested re-run of PR 2 analysis. Two updates:

  1. Methodology audit (operator-led correction): Initial re-validation conflated Polymarket platform volume with IBKR equity-execution slippage — these are independent. PM volume signals prediction-market efficiency; IBKR slippage depends on the underlying stock's liquidity (NYSE/NASDAQ spread). The hypothetical_pnl methodology already captures prediction-quality differences via per-bucket WR (e.g., $1k-$2k WR 27.4% vs $5k+ WR 40.9%). Equity slippage is roughly constant across PM volume buckets (depends on stock not PM market). The "$2.64/trade slippage on $1k-$2k" framing was incorrect.

  2. Capacity-aware re-validation (PR 3.b.2 / Finding #314.b ran on prod May 30): Using the new capital simulator (Finding #314 / PR 3.b.1) on the FULL universe (127 live + 441 counterfactual = 568 signals over 36 weeks):

  3. Pre-#311 ($2k gate): 295 admits, total +$241 → +$6.70/wk baseline.
  4. Post-#311 ($1k gate): 409 admits, total +$607 → +$16.86/wk.
  5. Δ vs $2k baseline = +$10.16/wk (about HALF the optimistic +$21/wk Phase γ+δ projection — gap is capacity contention: 58 capacity-rejected signals at $1k vs 5 at $2k).
  6. $1k-$2k bucket admitted (n=132): avg +$3.20, WR 31.8%, total +$423 → POSITIVE contribution. Operator's earlier concern (based on live n=10) is NOT confirmed at scale.
  7. max_total_capital_used = $4,949.70 (99% of $5,000 cap). Capacity is binding.

Decision: KEEP $1k uniform gate as shipped (Finding #311). The capacity-aware re-validation confirms it's a real +$10/wk lift on $2.5k capital (~21% return-on-capital boost annualized). Less than the original +$21/wk projection but still material. NO CODE CHANGE.

Finding 311 — Volume-gate SHIP $2,000 → $1,000 (RESOLVED May 30, 2026; PR 2 fully shipped to production)

Finding 310.c — Phase 3 simulator PR 2.c FINAL: gate-retune sweep machinery + dashboard section + admission gauntlet (RESOLVED May 29, 2026; PR 2 COMPLETE)

Finding 310.b — Phase 3 simulator PR 2.b: counterfactual_admission.py (load opp_log low-volume markets + per-variant admission filter) — RESOLVED May 29, 2026 (second sub-commit of PR 2)

Finding 310-amendment — PR 2.d.1/PR 2.d.2 retraction + correction (May 29, 2026 LATE morning)

Finding 310 — Seasonality preflight + structural-collapse discovery + PR 2 declarative scaffolding (gate_variants.py + 11 variants + period segmenter + week-of-month classifier) — RESOLVED May 28, 2026 LATE evening / May 29, 2026 (first sub-commit of PR 2). AMENDED May 29, 2026 LATE morning — see Finding #310-amendment above.

Finding 306.c — Phase 3 simulator: gauntlet.py (5-check bias gauntlet) + output.py (deterministic JSON schema) + runner.py --run-gauntlet (RESOLVED May 28, 2026 LATE evening — fifth PR-1 ship; foundation incremental commit #3 of 3; PR 1 COMPLETE)

Finding 306.b — Phase 3 simulator: cost_model + replay engine + reference_adapter + cross_check_baseline script (RESOLVED May 28, 2026 LATE evening — fourth PR-1 ship; foundation incremental commit #2 of 3)

Finding 306.a — Phase 3 simulator foundation skeleton (scripts/phase3_simulator/ package + RuleSet dataclasses + CLI stub) (RESOLVED May 28, 2026 — third PR-1 ship; foundation incremental commit #1 of 3)

Finding 308 — Bug #6 hyp_pnl re-run + structural-ceiling discovery (RESOLVED May 28, 2026 LATE evening — second PR-1 ship; ops-only)

Finding 307 — Finding #273 lockstep fix Phase 1: snapshot_schema='pm_v2' conditional-label fix at _emit_pm_opportunity_log (RESOLVED May 28, 2026 evening — first PR-1 ship of the Phase 3 simulator project)

Finding 305 — RuleB ship Phase 1 DRY-RUN: ±90 min no-stop window around earnings release (RESOLVED May 28, 2026 LATE afternoon — second Phase-3 strategy rule shipped)

Rule Headline /100 Ex-top-2 /100 Ex-repeat-tickers /100 Gauntlet verdict
RuleB (±90 min window) +$236 +$127 +$239 ✅ PASS all 4 checks
RuleG (window + ATR) +$242 +$105 +$245 ⚠️ ex-top-2 weaker than RuleB; more invasive
RuleD (ATR k=2.0) +$182 +$44 +$183 ❌ ex-top-2 fragile
RuleF (no stop ever) +$191 +$53 +$192 n/a (reference only)
FA_hard (Finding A) +$13 +$8 +$9 ❌ rearm_gap_open arm n=24 at -$1,545/100 (retraction holds)

Finding 304 — Queue Rule #1 ship: skip LONGs in downtrend regime (RESOLVED May 28, 2026 afternoon — first Phase-3 strategy rule shipped)

regime direction n wins avg pnl total pnl
downtrend LONG 22 5 (23 %) −$6.20 −$136.30 ← Rule #1 target bucket
sideways LONG 39 14 (36 %) +$2.29 +$89.28
uptrend LONG 41 20 (49 %) +$3.25 +$133.20
downtrend SHORT 3 3 (100 %) +$57.51 +$172.52 ← preserved by rule (LONG-only)
sideways SHORT 7 1 (14 %) −$3.94 −$27.60
uptrend SHORT 8 1 (12 %) −$3.28 −$26.22

Finding 303 — deploy_from_github.sh pre-pull hygiene for recurring working-tree drift (RESOLVED May 28, 2026 LATE morning)

Finding 302 — polymarket-backup.timer collision with Ubuntu apt-daily-upgrade.timer window (RESOLVED May 28, 2026)

Finding 301 — data-api/trades hard 5-year history limit on Polygon-class trade endpoints (DOCUMENTED May 17, 2026)

Finding 300 — 184 unresolved-past earnings markets — root cause locked-out resolver (RESOLVED May 17, 2026)

Finding 299 — orderbook_snapshots.spread_yes/spread_no capture stub auto-MM quotes ($0.01/$0.99), not real tradeable spreads (RESOLVED May 17, 2026 evening)

# Cohort-level realistic NO-side spread for H1b-eligible markets # in the 24h pre-earnings window (matches Phase 1.5 #1 methodology) result = aggregate_realistic_spread( engine, condition_ids=h1b_eligible_cids, start=actual_earnings_time - timedelta(hours=24), end=actual_earnings_time - timedelta(hours=1), outcome="NO", ) print(f"Median NO-side spread: {result['median_realized_spread']:.3f}") # Use this for cost_model.DEFAULT_SPREAD_HALF / 2 calibration in pm_earnings_arb `` - **Future work (operator-decision; NOT in scope for this finding):** - **Fix-the-capture-pipeline (option b)** is still on the table for if/when the operator wants to makeorderbook_snapshots.spread_*columns trustworthy directly. Would benefit dashboards / live-trading code that doesn't have the per-window flexibility of the trades-based helper. Estimated effort: collector validator + tests + a one-shot historical re-compute job for the 7.79M existing rows. - **refresh_pm_realized_spreadscron job** could pre-compute per-market median spread for the 24h pre-ER window of every resolved earnings market and store it in a new column. Saves repeated SQL passes for analysis code that wants the same window repeatedly. Defer until usage patterns justify it. - **Files (this commit):** -src/utils/orderbook_spread.py(new) -tests/unit/test_orderbook_spread.py(new, 13 tests, all pass on prod) -docs/DATA_ARCHITECTURE.md(orderbook_snapshots caveat already shipped May 17 morning; add helper-usage hint in this commit) -docs/FINDINGS.md` (this entry — updated to RESOLVED)

Finding 298 — earnings_markets.market_cap_category contains 4 inconsistent value formats due to two pipeline writers (DOCUMENTED May 17, 2026; canonical helper SHIPPED, source-data migration PROPOSED)

Finding 297 — api_health_checker WARN-but-exit-2 cry-wolf antipattern on price_accuracy drift (RESOLVED May 17, 2026 morning)

Finding 296 — _check_systemd_unit_last_result false STATUS_OK on never-fired units (RESOLVED May 11, 2026 evening)

Finding 295 — backfill_trades.py:save_trades_batch SAVEPOINT-per-trade fix (RESOLVED May 10, 2026 early afternoon)

Finding 293 — Polymarket Website API URL update + sync_earnings_markets dead-code deletion (RESOLVED May 10, 2026 early afternoon)

Finding 294 — trades.outcome and trades.side widened from VARCHAR(10) to VARCHAR(64)/VARCHAR(20) (RESOLVED May 10, 2026 LATE morning)

Finding 292 — Polymarket data-api /trades offset hard-cap at 3000 (graceful end-of-pagination handling) (RESOLVED May 10, 2026)

Finding 288.v3 — Cold-bootstrap close-out for Tier B + 5-label trade_dedupe.source discovery (RESOLVED May 10, 2026)

Finding 291 — Long-running-no-op service integrity-monitor defense via state-file mtime rules + deploy-checklist + documentation (RESOLVED May 10, 2026 — re-scoped from original spec)

Finding 290 — Watchdog dead-code deletion (watchdog.py + watchdog_v3.py + 2 unit files) + same-pattern stale-reference cleanup (RESOLVED May 10, 2026)

Finding 289.b — live_trades_collector.py pg8000 InterfaceError failed-tx cascade (RESOLVED May 10, 2026 — root cause was inner-try-swallows-exception fooling engine.begin into failed-tx COMMIT)

Finding 289.a — api_health_checker cron silent failure (RESOLVED May 10, 2026 — systemd-timer migration via operator-approved option (B))

Finding 289.v2 — Same-day comprehensive same-pattern cleanup of remaining active-code Dome references (RESOLVED May 10, 2026)

Finding 289 — Dome EOL Tier C: codebase + doc cleanup, delete 8 dead Dome-dependent files, cosmetic rename of live_trades_collector.py, doc surface refresh (RESOLVED May 10, 2026)

Finding 288 — Dome EOL Tier B: migrate backfill_trades.py from Dome /polymarket/orders → Polymarket data-api/trades (RESOLVED May 10, 2026)

Finding 287 — Dome API EOL (Apr 28, 2026) — Tier A: stop dead Dome-era services + state-file archival + repo cleanup (RESOLVED May 10, 2026)

Finding 269.v3 — Circuit breaker for upstream-API-down scenarios + integrity-monitor exit-code rule (RESOLVED May 9, 2026 LATE evening)

Finding 269.v2 — Same-day post-deploy fixes caught by §G probe of the cold bootstrap (RESOLVED May 9, 2026 LATE evening)

Finding 269 — polymarket-trades-backfill.service had no scheduler — RESOLVED via smart Option A: weekly timer + delta backfill + earnings-only scope (RESOLVED May 9, 2026)

Finding 268 — Integrity monitor doesn't watch batch service exit codes — Phase 1 work is silent (RESOLVED Apr 27, 2026 evening)

Finding 277 — /api/architecture-findings parser missed RESOLVED / DOCUMENTED / NOT-A-BUG status keywords → dashboard showed closed findings as open (RESOLVED Apr 27 LATE evening)

Finding 276 — VZ #120 trade log mis-classified as instant_exit:earnings_miss despite Polymarket resolving YES (RESOLVED Apr 27 LATE evening)

Finding 275 — cleanup_local() silently wiped local backup when Spaces unconfigured (RESOLVED Apr 27 LATE evening)

Finding 267 — polymarket-backup.service OOM-killed on 3 consecutive nights Apr 25-27 (RESOLVED Apr 27 LATE evening)

Finding 265.v2 — same-day audit caught my own Finding #265 incomplete fix (RESOLVED)

Finding 265 — Dashboard volume column shows 24h while trader gates on cumulative (RESOLVED)

Finding 264 — Yahoo overrode confirmed Polymarket signal during process-restart window (RESOLVED)

Finding 263 — pg8000 mis-parsed :exit_time::timestamptz cast → primary close path failed silently for ALL trades since Phase-2 deploy (RESOLVED)

Net effect: the "fix" from Finding #256 silently broke a far wider set of fields, including the very field it was trying to fix. The integrity monitor caught it. Operator caught it via the integrity monitor. Without these signals it would have been undetected for weeks or months. - Smoking gun in /var/log/polymarket/earnings_trader.log: 13:17:20 - Failed to update position close VZ: pg8000.dbapi.ProgrammingError {'S':'ERROR','C':'42601','M':'syntax error at or near ":"','P':'1665'} 13:17:20 - Attempting fallback close persistence for VZ after primary failure 13:17:20 - ✅ Fallback close persistence succeeded The CRITICAL log is technically fired, but it appears as an INFO-level "succeeded" message after a WARNING-level "fallback used". Easy to miss in normal review. - Fix shipped: - src/earnings/position_manager.py — replaced :exit_time::timestamptz with CAST(:exit_time AS timestamptz) and refactored to use two separate EXTRACT calls. Equivalent semantics, pg8000-safe. - SQL backfill: UPDATE trade_audit_log SET hold_duration_hours = ... WHERE status='closed' AND hold_duration_hours IS NULL — backfilled VZ #120 (65.32h) and one historical edge case. - Same-pattern audit: ran rg ':\w+::' src/ earnings_dashboard/ — only the comment in position_manager.py remains. No other :name:: antipatterns in the codebase. - Memory §B 5-step verification: - Step 1 trace: trader.close → persist_position_close_with_exit_details() → primary UPDATE (line 541). Old: SQL syntax error 42601 → except block → fallback UPDATE (line 710) → succeeds with limited fields. New: primary UPDATE succeeds, all fields populated. - Step 2 simulate: next close (mock or real) executes the primary UPDATE without error. hold_duration_hours, conviction_at_exit, prediction_correct all populate. - Step 3 lines: src/earnings/position_manager.py lines 564-585 (the rewritten clause). - Step 4 failure modes: (a) different SQL driver in future — CAST(... AS timestamptz) is portable ANSI SQL, works everywhere. (b) entry_time might be NULL — already handled by EXTRACT returning NULL → ROUND(NULL) → NULL → no row failure. (c) clock skew between Python datetime.now(timezone.utc) and DB entry_time — mathematically possible to get a negative duration; ROUND handles it but it'd be a data quality flag. - Step 5 acceptance: integrity rule null_rate:trade_audit_log:hold_duration_post_fix transitions from WARNING fail (1/1 NULL) to OK pass. Verified: closed_after_fix=1, with_hold=1 after backfill. - Memory §E same-pattern check: zero other instances of :name:: in src/ or earnings_dashboard/. Cleared. - Lessons: - Driver-specific SQL needs explicit testing. The :name::cast pattern is valid in psycopg2 but breaks in pg8000. Should not have shipped without exercising the close path. - "Silently using fallback path" is a tier-1 signal. Whenever a fallback fires, it should LOUDLY indicate that the primary failed. The current logger.critical is correct, but the secondary logger.info("Fallback close persistence succeeded") makes the failure look benign in log review. Future improvement: keep fallback CRITICAL-level until 24h of success on primary. - Integrity rules with a min_rows threshold are working as intended. The hold_duration_post_fix rule fired exactly when expected (1 row arrived after the deploy and was NULL). - Files: - src/earnings/position_manager.py (the SQL fix) - docs/FINDINGS.md (this entry)

Finding 262.v6 — Real root cause: schema mismatch in market_availability dict (RESOLVED)

Finding 262.v5 — Defensive ibkr_reality + render-error fallback (DONE; incident 4 forced fix)

Finding 262 — Dashboard 500 ImportError after multi-phase deploy (post-mortem) (DONE)

Finding 261 — Phase 6 of Database Integrity Audit: doc refresh (DONE)

Finding 260 — Phase 5 of Database Integrity Audit: storage hygiene + watchdog cleanup + backup OOM fix (DONE — minimal scope per Q2=defer)

#260a — backup OOM kill (CRITICAL): investigation found that polymarket-backup.service was OOM-killed today at 04:09 UTC. Kernel log: Killed process 4182595 (python) total-vm:9582800kB, anon-rss:6871164kB. Root cause: deploy/backup_database.py:91-104 called process.communicate() which loads the entire pg_dump output into a single Python bytes before writing. With DB now 32 GB raw / ~5 GB gzipped and only 7.8 GB RAM on the prod host, this triggers global OOM every run. Older successful backups (Apr 6 / Apr 9) were 2 GB compressed — within RAM budget. Today's larger DB pushed past the limit. - Fix: streaming the pg_dump stdout pipe in 1 MB chunks directly into gzip.open(...).write(). Memory footprint now O(chunk_size) ~= 1 MB regardless of DB size. Cleanup on failure (delete partial backup file). Drain stderr only after stdout EOF. - Per memory §B: traced via OOM kernel log, lines deploy/backup_database.py:91-138. Failure modes: streaming chunk-by-chunk handles arbitrary DB size; partial-file is deleted on exception; pg_dump rc != 0 → return None which Phase 1 #251d's caller already handles (no Spaces upload attempted). - Acceptance: tomorrow 04:00 UTC backup runs without OOM; integrity_monitor's polymarket-backup.timer rule (which fires WARNING if last trigger > 26 hours) stays green.

#260b — Spaces not configured (operator decision noted): the prod .env lacks SPACES_ACCESS_KEY / SPACES_SECRET_KEY. backup_database.py correctly skips Spaces upload + cleans up local file → no off-site backup happening. Phase 1 #251d only enforces "exit non-zero IF creds set". Per memory §C surgical changes, I'm NOT changing the policy here — operator chose local-only when the env was set up. Documented in this finding for visibility; integrity_monitor's artifact:strategy_analysis_report rule provides Phase 5 coverage proxy.

#260c — watchdog v2 + v3 leftover units: Phase 3's deploy hook tried to rm -f the v2 unit file but the daemon-reload order made it ineffective in the first deploy. v3 had its own unit file (NOT in deploy/systemd/) and was crash-looping with ModuleNotFoundError: No module named 'src' (PYTHONPATH issue) — generating 291 MB of error tracebacks in /var/log/polymarket/watchdog_v3.log since deploy. Fix: extended deploy/deploy_from_github.sh to handle BOTH unit files via a for legacy in polymarket-watchdog.service polymarket-watchdog-v3.service loop. Idempotent.

#260d — log rotation (no config existed): /var/log/polymarket/*.log had grown to: - collector.log 8.2 GB - earnings_trader.log 1.7 GB - earnings_sync.log 293 MB - watchdog_v3.log 291 MB (just removed via #260c) - Total /var/log/polymarket/ was over 11 GB.

Fix: new deploy/logrotate/polymarket config: daily rotation, 14 days compressed retention, copytruncate (no SIGHUP needed since services use StandardOutput=append:). Installed via deploy hook. After first nightly run, expected disk reclaim: ~9 GB.

#260e — Timescale job 3 (Apache-edition limitation, ACCEPTED): 166 consecutive failures of policy_job_stat_history_retention job. Investigation: the failure message is functionality not supported under the current "apache" license. Even alter_job(3, scheduled => false) is blocked on Apache edition. Conclusion: this job will keep failing 1×/day on Apache build. The _timescaledb_internal.bgw_job_stat_history table will grow slowly (~1 row/day per job). Documented as known limitation; no fix without upgrading to Timescale community/cloud edition. The integrity_monitor will eventually have a Timescale-job-health rule (deferred to a future phase).

#260f — trade_dedupe 4.1 GB documented as acceptable: investigation showed 2.8M rows × ~1 KB raw_data JSONB + 641 MB indexes + 291k dead tuples (10 % bloat). The dedupe table stores full Polymarket trade payloads for replay analysis. Oldest row Jan 31, newest today — 3 months of dedupe data. Could be pruned to 7-14 days (the dedupe purpose is "did we already insert this trade?" which only matters within a small window) to reclaim ~3.5 GB, BUT that's a behavior change requiring operator decision (would alter analysis-on-historical-data semantics). Documented as acceptable bloat for now; Phase 5 storage rules in integrity_monitor flag it if it crosses 5 GB. Optional VACUUM FULL would reclaim ~410 MB.

Finding 259 — Phase 4 of Database Integrity Audit: Database tab redesign + /api/integrity (DONE)

Finding 258 — Phase 3 of Database Integrity Audit: unified integrity monitor (DONE)

Finding 257 — markets.winning_outcome 100% NULL — phantom column, unused in live code (DOCUMENTED)

Finding 256 — trade_audit_log.hold_duration_hours never written (DONE)

Finding 255 — trade_audit_log.sp500_change_at_entry 100% NULL post-#234 (NO CURRENT BUG)

Finding 254 — trade_audit_log.conviction_at_exit 100% NULL post-#237 (NO CURRENT BUG)

Finding 253 — polymarket_opportunity_log + skipped_signals idle since 2026-04-24 (NOT A BUG — weekend/window timing)

Finding 252 — polymarket_opportunity_log.snapshot 88% empty / no pm_v2 rows yet (NO CURRENT BUG)

Finding 251 — Phase 1 of Database Integrity Audit: silent process-failure fixes (DONE)

#251a — scripts/equity_bars_collector.py (line 380-403) - Threshold: exit non-zero when total_failed / attempted > 0.10 where attempted = len(jobs) - total_skipped (cache-hits don't count as attempts). - Below threshold: transient Polygon flake on a few illiquid symbols, expected. - Above threshold: likely Polygon outage or DB connectivity issue. Exit 2. - Matches the original docstring claim of "exit 2 for unrecoverable Polygon errors" (which the implementation never honoured).

#251b — src/data_tracker/collectors/trade_reconciler.py (line 1156-1186) - Refactored _persist_job_result to return its computed status ("ok" or "error"). - main() now captures job_status and exits non-zero whenever status='error' was persisted (i.e. any trade/position/stop issue detected during the run). - if __name__ == "__main__": sys.exit(main() or 0) so systemd sees the exit code.

#251c — src/data_tracker/collectors/outcome_resolver.py (line 1037-1080) - Distinguishes per-row miss (current fail-soft, exit 0) from infrastructure failure. - Threshold: exit 2 when ANY tier (earnings / opportunity / skipped) has failed / attempted > 50 % AND attempted ≥10 (so a tiny batch of bad rows doesn't trip it). - Hits when all 4 EPS sources unreachable on most rows, or DB connectivity is intermittent.

#251d — deploy/backup_database.py (line 186-204) - When SPACES_ACCESS_KEY + SPACES_SECRET_KEY are both set, Spaces upload failure now exits 2. - When Spaces creds are absent, exit stays 0 (operator chose local-only). - Off-site backup is the entire purpose of the service; "backup file created locally but never reached Spaces" is no longer a silent success.

#251e — deploy/systemd/polymarket-trades-backfill.service (line 6-22) - Type=simple → Type=oneshot + RemainAfterExit=no. - The script iterates all markets and EXITS when finished — it is a batch job, not a daemon. Type=oneshot is the correct semantic. - Restart=on-failure retained so a crash mid-backfill triggers a restart, but successful completion no longer leaves the unit in a confusing "inactive (dead)" state forever.

#251f — scripts/auto_login_gateway.py broken import (src/earnings/alerts.py line 587-606) - auto_login_gateway.py:158, 376 imports send_telegram_message from src.earnings.alerts — that function did not exist (only send_alert and a local send_telegram inside AlertManager.send). - Any IBKR login-lockout warning or 2FA-required notification would ImportError at line 158/376 before reaching Telegram. - Fix: added a real async def send_telegram_message(message, level=WARNING, category="system_notification") to alerts.py as a thin wrapper around send_alert(...). Preserves the legacy call sites without further script refactoring.

Finding 250 — Database integrity audit (Apr 27, 2026) — comprehensive baseline (IN_PROGRESS)

Finding 249 — Polygon as 2nd-tier mcap fallback + DB-first dashboard cache cold-path (DONE)

Finding 248 — Phase 4 dashboard simplification (partial ship — Phase-3-independent items only) (DONE)

Finding 247 — SEC EDGAR 8-K press-release timestamp backfill (DONE)

Finding 246 — Post-event price_snapshots backfill from Polymarket data-api /trades (DONE)

Finding 245 — Ticker-variant retry for class shares in outcome_resolver (DONE)

Finding 244 — Outcome resolver 6-hour guard silently locked out by hourly earnings sync (DONE)

Finding 243 — opportunity_log.snapshot enriched with PM book + conviction-window fields (DONE)

Finding 242 — actual_beat coverage drive: 71.6% → 84.1% (UPDATED Apr 26 evening)

Finding 241 — opportunity_log.snapshot: 100% coverage, JSON shape sufficient for Phase 3 (VERIFIED + EXTENDED via #243)

Apr 26 evening update: this finding's "PM book + IBKR L1 deferred" position was superseded by Finding #243 (PM-book enrichment shipped same day). IBKR L1 specifically remains deferred to Phase 3 due to signal-hot-path risk (see #243). Current schema is pm_v2 going forward.

Finding 240 — Equity 1-minute bar collector + cache (Phase 2 of Strategy Analysis Revamp) (DONE)

Finding 239 — Doc consolidation: long-weekend sizing 50% canonical, trade-count transition 100→150→200 (DONE)

Finding 238 — Forward-fix: weekly self-heal of polymarket_opportunity_log.hypothetical_pnl (DONE)

Finding 237 — trade_audit_log.conviction_at_exit writer added (DONE)

Finding 236 — trade_audit_log.stop_triggered writer fix + 55-row backfill (DONE)

Finding 235 — Dashboard renderRecommendations ReferenceError on Strategy tab load (DONE)

Finding 234 — TradingOrchestrator._persist_position_open was missing regime/VIX/SP500 columns (DONE)

Finding 233 — trade_audit_log.stop_triggered field undercounts stops by 28× (DONE — analysis-side fix Apr 25; writer-side fix shipped Apr 26 as #236)

stop_triggered exit_reason n
true stop_loss:stp_lmt_realtime_fill 1
true stop_loss:hardware_reconciled 1
false stop_loss:hardware 40
false stop_loss:hardware_reconciled 7
false instant_exit:stop_loss_backup 5
false stop_loss_software_backup 1
false hardware_stop 1
false stop_loss_triggered 1

So 55 of 57 stop-exits (96 %) have the flag wrong. - Strategy impact (analyses that were wrong): - v2 #6 (3-stop session breaker) was claimed in earlier docs to have ZERO historical sessions with ≥3 stops → actually 8 sessions (Feb 19 had 5 stops). The rule has real defensive value, not theoretical. - v3 D ON-HOLD reasoning ("stops eat winners on Mid-cap, RuleB will fix this") was tentative in earlier docs because stop rates appeared low → actually strongly vindicated: Mid 80-90 % has 51.6 % stop rate, Mid 90-95 % has 86.7 % stop rate. - v3 E ON-HOLD reasoning rested on claimed sideways-SHORT 67 % stop rate → actually 83.3 % (corrected). - RuleB urgency was understated → corrected stop bleed is −$676 across 57 stops (avg −$11.86/trade), erasing ~80 % of the +$843 contribution from non-stops. Promoted rank 4 → 2 in queue. - Mar 9 #202 wider stops impact was misjudged: pre-Mar-9 stop rate 50 %, post-Mar-9 45.5 % (frequency dropped 10 % only). But avg stop loss rose from −$9.66 → −$22.23 (each stop costs more because stop distance is wider). RuleB targeting the ±90-min window is a better fix than further widening. - Fix (analysis-side, immediate): - Updated scripts/strategy_performance_analysis.py:build_action_queue() to compute live evidence using exit_reason ILIKE '%stop%'. - Updated docs/STRATEGY_PLAYBOOK.md §1 (verdict) + §3 (per-rule evidence tables) + §6 (#5 implementation notes — explicit warning to use exit_reason). - Updated docs/AGENT_HANDOFF.md §11 verification block with stop-rate sanity-check query. - Updated docs/STRATEGY_AUDIT_v3.md §14 (re-validation pass) + §14.7 (lesson #7 — canonical-field-validity rule). - Future agents: any WHERE stop_triggered = true query is suspect — replace with exit_reason ILIKE '%stop%'. - Deferred (writer-side, longer fix): - Patch every stop-exit code path in src/earnings/order_executor.py + src/earnings/position_manager.py + the IBKR fill-event handler to also set stop_triggered = true. Then backfill the 55 historical rows where exit_reason ILIKE '%stop%' AND stop_triggered = false. - Ideal forward-fix: trigger the boolean from the textual reason in a DB-level CHECK or via a unified _persist_exit() helper that derives the boolean from the reason. Removes the drift surface entirely. - Tracked in docs/STRATEGY_AUDIT_v3.md §15 next-pass to-do. - Same-pattern check (memory §E): Are there other audit-log boolean flags that may have drifted from their canonical textual counterparts? Audit candidates: prediction_correct vs actual_earnings_result, is_external_trade vs entry_reason, used_market_fallback vs entry_order_type. Spot-check deferred to next pass. - Process learning (added to STRATEGY_AUDIT_v3.md §14.7 as lesson #7): Canonical-field-validity rule. Never trust convenience flags like stop_triggered without cross-checking against the source-of-truth field like exit_reason. Audit-log boolean fields can fall out of sync silently when writer code paths drift; the textual exit_reason is the canonical record. When in doubt: query both, compare counts, investigate the gap. - Acceptance test: - SELECT count(*) FILTER (WHERE stop_triggered) AS broken, count(*) FILTER (WHERE exit_reason ILIKE '%stop%') AS canonical FROM trade_audit_log WHERE status = 'closed' → should show broken ~2 / canonical ~57 (will rise as more trades close). - scripts/strategy_performance_analysis.py re-run produces an action_queue with v2 #6 in IN-QUEUE and live_evidence strings showing real stop rates (not 0-3 % phantoms). - Files: - scripts/strategy_performance_analysis.py (live-evidence + bias_check fields rewritten to use exit_reason) - docs/STRATEGY_PLAYBOOK.md (§1 verdict + §3 evidence tables + §6 implementation note) - docs/STRATEGY_AUDIT_v3.md (§14 + §15) - docs/AGENT_HANDOFF.md (§11) - docs/FINDINGS.md (this entry) - .cursor/memory.md (no change needed — §F project-specific checks already mention reconciliation and audit-log integrity)

Finding 232 — Action Queue re-validation against fresh universe data + v3 F retracted (DONE)

Metric Handoff (Apr 25 morning) Apr 25 LATE evening Δ
Closed trades 109 116 +7
Net P&L +$61 +$167 +$106
Win rate 33 % 33.6 % ≈ same
Universe in-band 568 604 +36 (organic + backfill timer fire)
Volume coverage on in-band 91 % positive 90.4 % at $2K gate comparable
- Per-rule re-validation results (full table in docs/STRATEGY_AUDIT_v3.md §14.3):
# Rule Old rank New rank Change
1 v3 B — Skip downtrend LONGs 1 1 KEEP — bias check STRONGER (ex-top-2 = -$175, more negative because SWBI was masking; 22/22 distinct tickers)
2 RuleB — ±90 min stop window 4 2 PROMOTED — corrected stop classification (Finding #233) revealed live stop bleed is the strategy's biggest leak (-$676/57 stops)
3 v2 #6 — 3-stop session breaker 2 3 DEMOTE rank but UPGRADE evidence: 8 historical ≥3-stop sessions confirmed (was: claimed zero)
4 Audit re-validation @ #150+ 5 4 DEMOTE — F drop clears a slot
— v3 F — BMO + VIX sizing 3 🛑 RETRACTED DROPPED — see retraction below
⏸️ v3 D — Mid-cap 80%+ LONG skip ⏸️ ⏸️ KEEP ON-HOLD; reasoning REINFORCED (Mid 90-95% has 87% stop rate)
⏸️ v3 E — Skip non-downtrend SHORTs ⏸️ ⏸️ KEEP ON-HOLD; reasoning REINFORCED (sideways SHORT 83% stop rate)
- v3 F (BMO + VIX sizing) — RETRACTION DETAILS:
Original claim was +$60/100 with both ends robust per audit's ex-top-3 check. Apr 25 LATE deep gauntlet split verdict:
Arm Bucket n Headline Ex-top-2 Ex-top-3 Verdict
0.6× scale-DOWN BMO+VIX 18-22 30 −$54 −$135 (more negative) n/a ✅ PASS — robust
1.2× scale-UP BMO+VIX <18 20 +$100 n/a −$1.58 ❌ FAIL bias #2

The +$100 winner bucket is concentrated in STT +$37 / VZ +$33 / ALLY +$32 = +$102 — drop those 3 outliers and 17 trades sum to −$1.58 (essentially flat). Direct multiplier-on-realized-pnl modeling gives total per-100 lift of +$36 (NOT +$60), and that's optimistic. Operator dropped both arms — scale-DOWN-only ~$20/100 not worth implementation + variance + capital-constraint risk at current ~$2.4k capital. - 5-outlier curse rename (was 3-outlier): TXN (+$92, Apr 22 — LONG large NULL-regime, NEW big winner) and STUB (+$58, Mar 4 — SHORT mid downtrend, NEW) join GPRO + DELL + SWBI as dominant outliers carrying ~$430 of $167 net P&L on 116 trades. Ex-top-5 the strategy is −$263 across 111 trades. Memory §K + STRATEGY_AUDIT_v3 §14.4 + STRATEGY_PLAYBOOK §1 + §5 updated. - Combined realistic lift, revised: +$190 to +$310 / 100 trades (slightly UP from prior +$160-$280 because B's bias check came out stronger and v2 #6 is no longer purely defensive; offset by v3 F retraction). Flips the strategy from current +$1.44/trade to +$3.30 to +$4.50/trade average. - Acceptance test: - Run scripts/strategy_performance_analysis.py on prod; new action_queue should have 4 IN-QUEUE (B, RuleB, 3-stop breaker, audit-reval), 2 ON-HOLD (D, E), 8 RETRACTED (now includes v3 F). - Dashboard 🎯 Action Queue tab refreshed — RuleB shows at rank 2, v3 F at the bottom under RETRACTED with detailed bias-gauntlet narrative. - Bias gauntlet section shows "5-outlier rule" not "3-outlier". - Files: - scripts/strategy_performance_analysis.py — build_action_queue() rewritten + four_check_gauntlet_doc() 5-outlier rename - earnings_dashboard/templates/dashboard.html — header text "5-outlier reminder" + JS label "5-outlier rule" - docs/STRATEGY_PLAYBOOK.md — §1 verdict + §2 queue table + §3 per-rule evidence + §4 v3 F retraction + §5 5-outlier rule + §6 implementation notes - docs/STRATEGY_AUDIT_v3.md — §14 (re-validation pass) + §15 (deferred to-do) - docs/AGENT_HANDOFF.md — §11 current state + Action Queue table + verification block + closing footer - docs/FINDINGS.md — this entry + Finding #233 - .cursor/memory.md §K — 5-outlier rename - Re-validation evidence (kept in repo for audit trail): scripts/_revalidate_queue_evidence.sql, _revalidate_f_and_ruleb.sql, _revalidate_corrected.sql

Finding 231 — Forward-fix: weekly self-heal of markets.volume_total + earnings_markets.prob_*_before (DONE)

Finding 230 — earnings_markets.prob_*_before snapshots missing for 401 of 900 markets (orderbook collector started too late) (DONE)

Metric Pre-backfill Post-backfill Δ
In-band Universe LONG (>60%, resolved) 253 529 +276 (+109 %)
In-band Universe SHORT (<30%, resolved) 26 39 +13
Total operational universe 279 568 +289 (+104 %)
Coverage of all 900 markets 31 % 63 % +32 pp
NULL prob_24h_before — past earnings 401 54 −347
NULL prob_24h_before — future earnings 148 148 (will fill as earnings approach)
LONG ALL >60% sum P&L at $10K notional +$17,831 +$26,506 +$8,675
LONG 80-90% bucket size 95 223 +128 (+135 %)
LONG 95%+ bucket size 13 27 +14 (+108 %) — emerges as strong (+$192/trade)
SHORT 20-30% verdict "negative -$985 / 12 markets" +$2,870 / 25 markets sign FLIPPED (the original was a small-sample artifact)
PM prediction accuracy LONG >60% 75 % (n=249) 71 % (n=412) tempered with more data
PM prediction accuracy SHORT <30% 88 % (n=26) 81 % (n=27) unchanged

Finding 229 — markets.volume_total silently 0 for resolved markets (collector excluded closed=true) (DONE)

Legacy Findings (#1-#105)

These findings were originally documented as hardcoded dashboard cards before the FINDINGS.md consolidation (Mar 7, 2026). All are resolved/done.

Finding #1: IBKR Session Persistence - Weekly 2FA Achieved ✅ (Updated Jan 12, 2026) - Severity: Medium - Status: done - Summary: Original Issue: Current Configuration (Option A: IBC-Managed): ⚠️ CRITICAL: Session Invalidation Rules: 📊 Broker Comparison for Our Strategy: *Note: IBKR now configured for weekly 2FA only (Sundays at 11 AM Abu Dhabi). Session token preserved Mon-Sat. Recommendation: Continue with IBKR for testin...

Finding #2: Polymarket Conviction Changes Before Earnings Release (Jan 8, 2026) - Severity: Medium - Status: done - Summary: IMPORTANT OPEN Potential Solutions: Decision Needed: What should the system do if Polymarket conviction drops significantly after entry but before earnings? Current behavior: Hold until earnings release, then instant exit if wrong

Finding #3: JEF Trade Timeline Analysis (Jan 8, 2026) - Severity: Medium - Status: done - Summary: OBSERVATION MONITORING JEF Trade Facts (First Live Test):

Finding #4: Fractional Shares - IMPLEMENTED (Resolved Jan 11, 2026) - Severity: Medium - Status: done - Summary: Original Issue: ✅ Resolution (Jan 11, 2026): See Finding #28 for full implementation details.

Finding #5: Polymarket Volume Quality - $2,000 Minimum Threshold (Updated Jan 9, 2026) - Severity: Medium - Status: done - Summary: Original Issue: Threshold Configuration:

Finding #6: Earnings Release Time Uncertainty (Jan 8, 2026) - Severity: Medium - Status: done - Summary: OBSERVATION MONITORING Current Behavior: Future Enhancement: Consider adding more detection sources: SEC 8-K filings, Twitter/X, company IR pages

Finding #7: Extended Hours Liquidity & Slippage (Jan 8, 2026) - Severity: Medium - Status: done - Summary: OBSERVATION MONITORING Observation: Monitoring:

Finding #8: Historical Exchange Asymmetry (Superseded by unified stop/exit logic) (Jan 8, 2026) - Severity: Medium - Status: done - Summary: HISTORICAL CONTEXT SUPERSEDED Initial observation (JEF trade, Jan): 🎯 Strategy Impact Analysis: Unified Order Handling (UPDATED Feb 15, 2026): Key: NYSE and NASDAQ follow the same stop/instant-exit policy. Difference is now session state (open vs fully closed), not exchange. 📊 SIGNAL PRIORITY UPD...

Finding #9: Small Cap 90% Conviction Threshold Enforced (Jan 8, 2026) - Severity: Medium - Status: done - Summary: Backtest Data: Review After 100 Trades: Query skipped_signals to analyze actual outcomes of skipped small cap signals. May adjust threshold if data supports.

Finding #10: Data Integrity: price_snapshots Bug Fixed (Jan 8, 2026) - Severity: Medium - Status: done - Summary: FIXED CRITICAL Root Cause: ✅ FIX APPLIED: Data Reliability Timeline: Lesson Learned: Always go to ROOT CAUSE. Check all dependencies. Never assume data is garbage without investigation.

Finding #11: IBKR Gateway Weekly 2FA - Configuration Details ✅ (Updated Jan 12, 2026) - Severity: Medium - Status: done - Summary: VERIFIED ACTIVE Architecture (Option A: IBC-Managed): ✅ FINAL CONFIGURATION (Jan 12, 2026): Service Configuration Checklist:

Finding #12: Watchdog v3.0 - Robust Service Monitoring (Jan 9, 2026) - Severity: Medium - Status: done - Summary: DEPLOYED ACTIVE Background: ✅ WATCHDOG v3.0 FEATURES: Monitored Services: Service: polymarket-watchdog-v3.service • Interval: Every 5 minutes

Finding #13: Market Discovery - Complete Earnings Coverage (Jan 9, 2026) - Severity: Medium - Status: done - Summary: FIXED FUTURE-PROOF ✅ SOLUTION: Verification - Previously Missing Tickers Now Found: 💡 KEY INSIGHT: API-based discovery is more reliable than web scraping. The tag_id=1013 parameter was discovered by intercepting network requests from Polymarket's frontend using Playwright. See Finding #17 for the...

Finding #14: Priority System Redesign (0-6 ROI-First Scale) (Jan 9, 2026) - Severity: Medium - Status: done - Summary: Previous System: legacy scoring over-weighted very high confidence and was too coarse-grained for ROI optimization. ✅ NEW SYSTEM: 0-6 ROI-First Priority Scale Key Factors (in order): 📦 DATABASE TRACKING: Module: src/earnings/priority.py • Docs: STRATEGY.md §3.1

Finding #15: APT Daily Timer Conflict - CRITICAL ROOT CAUSE - Severity: Medium - Status: done - Summary: Finding #15: 🔴 APT Daily Timer Conflict (ROOT CAUSE) CRITICAL/FIXED 🔍 ISSUE: IBKR Gateway unexpectedly disconnected at 10:50 UTC (2:50 PM Abu Dhabi) despite being configured for auto-restart at 06:00 UTC. ROOT CAUSE IDENTIFIED: 📁 Configuration: Impact: Prevents unexpected Gateway disconnects due ...

Finding #16: IBKR Gateway Final Configuration - WEEKLY 2FA CONFIRMED - Severity: Medium - Status: done - Summary: Finding #16: ✅ IBKR Gateway Final Configuration (WEEKLY 2FA) CONFIRMED/IMPLEMENTED 🔍 ROOT CAUSE OF REPEATED 2FA: Systemd's Restart=always in ibgateway.service was interfering with Gateway's internal auto-restart, invalidating session tokens. KEY DISCOVERY (from IBKR community): ✅ FINAL CONFIGURAT...

Finding #17: Earnings Detection - Single Source of Truth - Severity: Medium - Status: done - Summary: Finding #17: ✅ Earnings Detection - Single Source of Truth CRITICAL/FIXED 🔍 PROBLEM: Dashboard and Signal Generator were using unreliable methods to detect earnings markets, potentially missing trades. ❌ OLD (UNRELIABLE) Methods: Real tag distribution: Hide From New (132), Finance (127), Economy ...

Finding #18: Earnings Timing Source Accuracy Analysis - Severity: Medium - Status: done - Summary: Finding #18: ⏰ Earnings Timing Source - HYBRID Approach (97%+ Accuracy) CRITICAL/IMPLEMENTED 🔍 PROBLEM: Yahoo Finance and Polymarket timing only agreed 79.5% of the time. Deep analysis needed to determine which source is correct. 📊 DEEP DIVE ANALYSIS: ❌ ROOT CAUSE OF 10.8% ERRORS: ✅ SOLUTION: HYB...

Finding #19: Trading Fees Recording - Severity: Medium - Status: done - Summary: Finding #19: 💰 Trading Fees Recording & Display IMPLEMENTED 🔍 REQUIREMENT: Record trading fees directly from IBKR with order types and display in dashboard trade log. ✅ IMPLEMENTATION: 📊 DATABASE SCHEMA: Note: Historical trades show "--" for fees as commission data was not captured before impleme...

Finding #20: IBKR Overnight Trading Deep Dive - Severity: Medium - Status: done - Summary: Finding #20: 🌙 IBKR Overnight Trading & Gap Risk Analysis CRITICAL/AWARENESS 🔍 QUESTION: Is overnight trading (8 PM - 4 AM) possible? How does it affect gap risk and ROI? 📊 IBKR TRADING SESSIONS (Eastern Time): ⚠️ GAP RISK ANALYSIS: ✅ CURRENT IMPLEMENTATION STATUS: 💰 ROI IMPACT ANALYSIS: Conclusi...

Finding #21: Shorts Implementation Deep Dive - Severity: Medium - Status: done - Summary: Finding #21: 📉 SHORT Selling Implementation CORE COMPLETE ✓ 🔍 QUESTION: Do we have full SHORT selling implementation? Is prioritization for shorts at the same level as longs? ✅ WHAT'S WORKING: ⚠️ REMAINING GAPS (Nice-to-Have): 📋 STRATEGY RULES FOR SHORTS (from STRATEGY.md): 🔧 IMPLEMENTATION STATU...

Finding #22: 2x Leverage Implementation - Severity: Medium - Status: done - Summary: Finding #22: 💰 2x Leverage Implementation (Target: 50 Trades) CRITICAL/PENDING 🎯 GOAL: Enable 2x Reg T margin leverage at 50/100 trades in TESTING_PROD phase. 📊 IBKR LEVERAGE OPTIONS: ⚠️ CURRENT STATE - NO LEVERAGE: 🔧 REQUIRED IMPLEMENTATION: 💵 MARGIN COST ANALYSIS (2x Leverage): Conclusion: Marg...

Finding #23: Concurrent Trade Execution - Severity: Medium - Status: done - Summary: Finding #23: ⚡ Concurrent Trade Execution (Multiple Trades Same Time) IMPLEMENTED ✓ 🎯 PROBLEM SOLVED: The system now handles unlimited concurrent trades at the same entry time with fair capital allocation and rapid execution. ✅ IMPLEMENTATION COMPLETE: 📊 PERFORMANCE COMPARISON: 🔄 EXECUTION FLOW: ...

Finding #24: System Health Check Script - Severity: Medium - Status: done - Summary: Finding #24: 🔍 Comprehensive System Health Check Script IMPLEMENTED ✓ 🎯 PURPOSE: A comprehensive testing script that verifies all system components are working correctly. Run before trading sessions to ensure system health. 📋 TESTS INCLUDED: # Quick check (no IBKR): python tests/system_health_che...

Finding #25: Sunday 2FA Fix - Severity: Medium - Status: done - Summary: Finding #25: 🔐 Sunday Weekly 2FA - Root Cause & Fix FIXED ✓ 🎯 ISSUE: Sunday Jan 11, 2026 - User received reminder notifications but 2FA request never came through. ❌ ROOT CAUSE: 📋 NEW BEHAVIOR: 2FA Frequency: Only on Sundays (weekly security token expiry). Mon-Sat restarts are automatic. Files Ch...

Finding #26: State Machine Documentation - Severity: Medium - Status: done - Summary: Finding #26: 📊 Comprehensive State Machine Documentation DOCUMENTED ✓ 🎯 PURPOSE: Complete system state documentation for all components, including states, transitions, and critical paths. 📑 COMPONENTS DOCUMENTED (11 State Machines): ✅ KEY DIAGRAMS: 📋 CRITICAL PATHS DOCUMENTED: File: docs/STATE_MA...

Finding #27: Position Sizing & Batch Order Analysis - Severity: Medium - Status: done - Summary: Finding #27: 💰 Position Sizing & Batch Order Analysis VERIFIED ✓ 🎯 PURPOSE: Verify position sizing flexibility, batch ordering, and capital reservation for concurrent positions. ✅ TEST CASE: 4 Concurrent + 2 Reserved Positions Reserved for BMO: $21,500 (GS + BK) | Total Deviation: -0.4% ✓ 🔍 KEY V...

Finding #28: Fractional Shares Implementation - Severity: Medium - Status: done - Summary: Finding #28: ✨ Fractional Shares Implementation IMPLEMENTED ✓ 🎯 PURPOSE: Eliminate capital waste from integer share rounding by implementing IBKR fractional shares support. ✅ IMPLEMENTATION COMPLETE 📊 CAPITAL EFFICIENCY IMPROVEMENT: ℹ️ HOW IT WORKS: Files: src/ibkr/client.py , src/earnings/batch_...

Finding #29: Critical Logger Bug - Missed Trades - Severity: Medium - Status: done - Summary: Finding #29: 🚨 Critical Logger Bug - Missed Trades FIXED ✓ ❌ IMPACT: 3 BMO trades missed (JPM, BK, DAL) for Jan 13 earnings 🔍 ROOT CAUSE: ✅ FIX APPLIED (Jan 12, 2026 21:07 UTC): # Added to conviction_calculator.py: import logging logger = logging.getLogger(name) ℹ️ POST-FIX VERIFICATION: Less...

Finding #30: Volume Threshold Bug - Wrong Volume Field Used - Severity: Medium - Status: done - Summary: Finding #30: 🔴 Volume Threshold Bug - Wrong Field Used FIXED ✓ ❌ IMPACT: Signal generator was using wrong volume field for threshold check Root Cause: ✅ FIX APPLIED: Lesson: Volume thresholds should use volume_total (cumulative) not volume_24h (24hr rolling). Files Fixed: src/earnings/signal_gene...

Finding #31: SHORT Signals Missing from Timeline - Severity: Medium - Status: done - Summary: Finding #31: ⚠️ SHORT Signals Missing from Timeline FIXED ✓ ⚠️ IMPACT: WFC SHORT signal (30% conviction) not showing in Active Positions Timeline The "Pending Opportunities" section only showed LONG signals (≥70% conviction), ignoring valid SHORT signals (≤30% conviction). Root Cause: ✅ FIX APPLI...

Finding #32: Three Critical Pre-Trade Bugs Fixed - Severity: Medium - Status: done - Summary: Finding #32: 🔴 Three Critical Pre-Trade Bugs - ALL FIXED FIXED ✓ 🔴 IMPACT: Signal generation returned 0 signals for tomorrow's BMO trades Pre-trade readiness check discovered 3 critical bugs that would have prevented all trades from executing. Bug #1: Conviction Calculator Entry Time Bug Bug #2: ...

Finding #33: IBKR Market Data Subscription Missing - RESOLVED - Severity: Medium - Status: done - Summary: Finding #33: ✅ IBKR Market Data Subscription - RESOLVED FIXED ✓ Added IBKR market data subscriptions. Tested SPY, BAC, BLK, GS - all returning valid prices. Original Issue: Resolution (Option A applied): Verified: Jan 15, 2026 at 10:50 AM UTC Test Results: SPY=$692.81, BAC=$52.70, BLK=$1,081.50, ...

Finding #34: TSM Timing Mismatch - RESOLVED - Severity: Medium - Status: done - Summary: Finding #34: ✅ TSM Timing Mismatch - RESOLVED FIXED ✓ TSM and other international ADRs now correctly categorized as BMO. Root Cause: Fixes Applied: Timing Priority Flow (all paths lead to BMO/AMC/INTRADAY/None): Verified: Jan 15, 2026 Test Result: TSM now correctly resolves to BMO via COMPANY_TIM...

Finding #35: CRITICAL - Duplicate Trade Entry on Service Restart - Severity: Medium - Status: done - Summary: Finding #35: 🚨 CRITICAL - Duplicate Trade Entry on Service Restart CRITICAL - TODO 🚨 Service restart during BMO window caused DUPLICATE ENTRIES MTB, STT, PNC were entered 3 times, resulting in 3x intended position size. Manual intervention required to fix. What Happened: Root Cause: Required Fix:...

Finding #36: EarningsDetector.register_position Missing - Severity: Medium - Status: done - Summary: Finding #36: 🔴 EarningsDetector.register_position Missing Method CRITICAL - TODO Fatal error: 'EarningsDetector' object has no attribute 'register_position' This crash caused the service to restart, which triggered Finding #35. Error Context: Required Fix: Priority: CRITICAL - Causes service cras...

Finding #37: Stop-Loss Orders Not Placed Due to PDT - Severity: Medium - Status: done - Summary: Finding #37: ⚠️ Stop-Loss Orders Blocked by PDT Rule MEDIUM - KNOWN LIMITATION PDT rule prevents stop-loss orders when account < $25K and day trade limit reached What Happened: Mitigation: Account Requirement: $25,000 to avoid PDT restrictions Current Balance: ~$2,700

Finding #38: Fractional Shares Check Bug - FIXED - Severity: Medium - Status: done - Summary: Finding #38: ✅ Fractional Shares Check Bug - FIXED FIXED Bug: Wrong variable used in fractional eligibility check causing TypeError Root Cause: # BEFORE (broken): primary_exchange = contract[0].primaryExchange if qualified else None # Error: 'Stock' object is not subscriptable # AFTER (fixed): pr...

Finding #39: Batch Position Sizing Oversized - FIXED - Severity: Medium - Status: done - Summary: Finding #39: ✅ Batch Position Sizing Oversized - FIXED FIXED Bug: Batch sizing distributed entire budget instead of using strategy target sizes What Was Happening: Fixed Logic: # Now correctly uses calculate_position_size() for each signal # Only scales down if total target > available budget MTB...

Finding #40: Dashboard Data Display & IBKR P&L - FIXED - Severity: Medium - Status: done - Summary: Finding #40: ✅ Dashboard Data Display & IBKR P&L - FIXED FIXED Issues: Wrong positions displayed, missing P&L, stale data Problems Found: Fixes Applied: File: earnings_dashboard/app.py - get_active_positions_from_db() Result: Dashboard now shows real-time IBKR P&L: $-9.04

Finding #41: IBKR Position Reconciliation Auto-Fix - ENHANCEMENT - Severity: Medium - Status: done - Summary: Finding #41: ✅ IBKR Reconciliation Auto-Fix - ENHANCEMENT ENHANCED Old behavior: Detect issues and alert. New: AUTO-FIX discrepancies! What It Now Does: Also Added: Files: src/earnings/trading_orchestrator.py Methods: _reconcile_positions_with_ibkr(), _persist_position_open()

Finding #42: Long Weekend / Holiday Trade Handling - ENHANCEMENT - Severity: Medium - Status: done - Summary: Finding #42: ✅ Long Weekend / Holiday Trade Handling - ENHANCEMENT ENHANCED Edge Case: Tuesday BMO after Monday holiday (MLK Day, Presidents Day, etc.) The Problem: Solution Implemented: 2026 Affected Holidays: Files: market_calendar.py, signal_generator.py, risk_manager.py, dashboard.html Docume...

Finding #43: IBKR Gateway Auto-Login on Session Timeout - ENHANCEMENT - Severity: Medium - Status: done - Summary: Finding #43: ✅ IBKR Gateway Auto-Login - ENHANCEMENT ENHANCED Root Cause: IBKR enforces ~24-hour session timeout for Gateway API connections The Problem (Last 3 Days): Solution Implemented: 2FA Still Required: Files: scripts/auto_login_gateway.py, src/ibkr/gateway_monitor.py Service: ibkr-monitor...

Finding #44: Polymarket Price-Based Earnings Detection - ENHANCEMENT - Severity: Medium - Status: done - Summary: Finding #44: ✅ Polymarket Price-Based Earnings Detection - ENHANCEMENT ENHANCED Discovery: Polymarket markets resolve 10-30 minutes FASTER than Yahoo Finance updates The Problem: Solution - First-to-Detect-Wins: Test Results (Jan 16 BMO): Files: src/earnings/earnings_detector.py, src/earnings/tra...

Finding #45: BMO Exit Timing Research - ROI vs Capital Efficiency - RESEARCH - Severity: Medium - Status: done - Summary: Finding #45: 🔍 BMO Exit Timing Research - ROI vs Capital Efficiency RESEARCH FUTURE ENHANCEMENT Original Research Data (EARNINGS_TRADING_STRATEGY.md §6.1-6.7): Key Research Findings: Current Implementation (Exit at 10:32 AM - Feb 2026): Implementation Status: Status: Research complete • Hybrid im...

Finding #46: Scheduled Exit Time Bug on Service Restart - FIXED - Severity: Medium - Status: done - Summary: Finding #46: ✅ Scheduled Exit Time Bug on Service Restart - FIXED CRITICAL BUG FIXED Root Cause: Service restart set scheduled_exit_time = now + 1 day instead of calculating from earnings timing Impact (Jan 16): Fix Implemented: Files: trading_orchestrator.py, trading_positions table Method: _cal...

Finding #47: Trade Data Integrity - Tables Desynchronized - CRITICAL FIX - Severity: Medium - Status: done - Summary: Finding #47: ✅ Trade Data Integrity - Table Desynchronization - FIXED CRITICAL BUG FIXED COMPREHENSIVE AUDIT: trading_positions and trade_audit_log were completely desynchronized Bugs Discovered: Root Cause: Fixes Applied: Files: trading_orchestrator.py, STRATEGY.md Section 7.5-7.6 Methods: _reco...

Finding #48: IBKR API Fractional Shares - cashQty Required - FIXED - Severity: Medium - Status: done - Summary: Finding #48: ✅ IBKR API Fractional Shares - cashQty Required - FIXED CRITICAL BUG FIXED Root Cause: IBKR API does NOT support fractional totalQuantity - must use cashQty (dollar amount) Why Desktop Works but API Doesn't: Fix Implemented: // WRONG (Error 10243) order = LimitOrder('BUY', Decimal('0...

Finding #49: Database Backup & Retention Policy - IMPLEMENTED - Severity: Medium - Status: done - Summary: Finding #49: ✅ Database Backup & Retention Policy - IMPLEMENTED OPERATIONS IMPLEMENTED Issue: trades table grew to 14GB causing "out of shared memory" errors on managed PostgreSQL The Problem: Solution Implemented: Backup Location: [hidden]/backups/ ├── trades_archive_20260118_165503.c...

Finding #50: Database Connection Pooling - IMPLEMENTED - Severity: Medium - Status: done - Summary: Finding #50: ✅ Database Connection Pooling - IMPLEMENTED PERFORMANCE IMPLEMENTED Problem: 36+ idle database connections consuming ~360MB memory Root Cause: Files: src/db/connection.py (new), 13 collector/trader files updated Docs: docs/STRATEGY.md (Section 7.8), docs/OPERATIONS.md (Connection Poo...

Finding #51: Whole Shares Only & Scheduled Exit Fix - FIXED - Severity: Medium - Status: done - Summary: Finding #51: ✅ Whole Shares Only & Scheduled Exit Fix - FIXED CRITICAL BUG FIXED Two Critical Bugs: cashQty unreliable + check_scheduled_exits never called Bug #1: cashQty Unreliable Bug #2: Scheduled Exits Not Triggering Files: src/ibkr/client.py, src/earnings/trading_orchestrator.py, src/earnin...

Finding #52: Smart Restart & 10 AM Abu Dhabi Schedule - IMPLEMENTED - Severity: Medium - Status: done - Summary: Finding #52: ✅ Smart Restart & 10 AM Abu Dhabi Schedule - IMPLEMENTED RELIABILITY IMPLEMENTED Root Cause: IBKR Gateway shuts down at 23:45 UTC daily (IBKR internal behavior) Fixes Implemented: New Schedule (Abu Dhabi Time): Files: /opt/ibc/config.ini, /etc/crontab, scripts/ibc_smart_restart.sh (N...

Finding #53: Position Size Flex (+57%) - IMPLEMENTED - Severity: Medium - Status: done - Summary: Finding #53: ✅ Position Size Flex (+57%) - IMPLEMENTED ENHANCEMENT IMPLEMENTED Enable trading expensive stocks by flexing position size up to +57% when needed Solution: Position Size Flex Impact (with $2,694 capital): Result: 15 of 17 stocks tradeable (88%, up from 65%) Files: src/ibkr/client.py ...

Finding #54: IBC AutoRestartTime Format Fix - ROOT CAUSE FOUND - Severity: Medium - Status: done - Summary: Finding #54: ✅ IBC AutoRestartTime Format - ROOT CAUSE FOUND ROOT CAUSE FIXED Root Cause: AutoRestartTime=06:00 (24-hour format) was being IGNORED by IBC - format must be 06:00 AM (12-hour with AM/PM) Expected Behavior After Fix: Files: /opt/ibc/config.ini (on server) Docs: docs/IBKR_CONTINUITY.m...

Finding #55: Smart Web Fallback for Earnings Timing - IMPLEMENTED - Severity: Medium - Status: done - Summary: Finding #55: ✅ Smart Web Fallback for Earnings Timing - IMPLEMENTED ENHANCEMENT IMPLEMENTED Never miss a trade due to unknown earnings timing - 5-tier lookup with web fallback Solution: 5-Tier Priority System with Web Fallback Files: src/earnings/earnings_timing_fallback.py (NEW), src/earnings/si...

Finding #56: AMC Batch Sizing Capital Reservation Bug - FIXED - Severity: Medium - Status: done - Summary: Finding #56: ✅ AMC Batch Sizing Over-Reservation - FIXED BUG FIXED Valid AMC trades were skipped because batch sizing used full 5-day BMO reservation Symptom: TXN trade at 10:34 AM skipped with "No budget available for batch sizing" Root Cause: Files: trading_orchestrator.py (run_amc_cycle) Docs:...

Finding #57: Stop-Loss Order ID Bug Fix - FIXED - Severity: Medium - Status: done - Summary: Finding #57: ✅ Stop-Loss Order ID Bug - FIXED BUG FIXED Stop-loss orders were not being placed due to attribute access error Error: 'Trade' object has no attribute 'order_id' Root Cause: Files: order_executor.py (line 616), batch_order_manager.py (line 740)

Finding #58: Duplicate Order Prevention (Pre-Order Check) - HAD REGRESSION - Severity: Medium - Status: done - Summary: Finding #58: ⚠️ Duplicate Order Prevention - HAD REGRESSION CRITICAL BUG REGRESSED → See #83 ⚠️ This fix was broken from the start - see Finding #83 for the real fix The code used .get() on PositionInfo (a dataclass, not dict) causing silent exception. Original Symptom: TXN position was 4 shares ...

Finding #59: NYSE Always STOP-LIMIT - FIXED - Severity: Medium - Status: done - Summary: Finding #59: ✅ NYSE Always STOP-LIMIT - FIXED LOGIC GAP FIXED NYSE stop-loss could fail if triggered during extended hours Fix: NYSE Always Uses STOP-LIMIT Files: order_executor.py (_place_stop_order) Docs: STRATEGY.md Section 7.1

Finding #60: Timezone-Naive Datetime Crash - FIXED - Severity: Medium - Status: done - Summary: Finding #60: ✅ Timezone Crash Fix - CRITICAL ROOT CAUSE FIXED Service crashed 4x in 5 minutes during trading - caused 4x TXN position Crash Error: Root Cause: Comprehensive Fix: Files: trading_orchestrator.py, signal_generator.py Docs: STRATEGY.md Section 7.9

Finding #61: Conviction Data Collection Gap - FIXED - Severity: Medium - Status: done - Summary: Finding #61: ✅ Conviction Data Collection - IMPLEMENTED DATA GAP FIXED Conviction data not being captured for strategy refinement Comprehensive Implementation: Backfill Results: Files: conviction_tracker.py, earnings-conviction-tracker.service Tables: earnings_markets (prob_* columns), conviction...

Finding #62: is_connected() Bug - CRITICAL FIX - Severity: Medium - Status: done - Summary: Finding #62: 🚨 is_connected() Bug - ROOT CAUSE of SBUX Stop Loss Failure CRITICAL BUG FIXED Root Cause: Code called is_connected() METHOD but IBKRClient only has connected PROPERTY The Bug (Active since Jan 16, 2026!): Impact - SBUX Trade (Jan 28): Fix Applied: Files: src/earnings/trading_orchest...

Finding #63: Startup Resilience - Retry Loop - Severity: Medium - Status: done - Summary: Finding #63: ✅ Startup Resilience - IBKR Retry Loop ENHANCEMENT IMPLEMENTED When IBKR Gateway restarts, earnings-trader now waits up to 15 minutes instead of failing immediately Solution - Exponential Backoff: Files: run_earnings_trader.py (connect_ibkr method)

Finding #64: VIX/SPY from IBKR - Severity: Medium - Status: done - Summary: Finding #64: ✅ VIX/SPY Data from IBKR (yfinance fallback) ENHANCEMENT IMPLEMENTED Market data for circuit breakers now uses IBKR first (no rate limits), yfinance as fallback Data Source Priority: Files: src/earnings/trading_orchestrator.py (_update_market_data_for_breakers, _get_ibkr_index_quote)

Finding #65: yfinance Rate Limit Tracking - Severity: Medium - Status: done - Summary: Finding #65: ✅ yfinance Rate Limit Tracking & 5min Backoff ENHANCEMENT IMPLEMENTED yfinance usage is now tracked and rate limits trigger automatic 5-minute backoff Monitoring: Files: src/earnings/trading_orchestrator.py (_log_yfinance_usage, _is_yfinance_rate_limited, _set_yfinance_rate_limited)

Finding #66: Hardware Stop Orders - Severity: Medium - Status: done - Summary: Finding #66: ✅ Hardware Stop Orders (STP LMT, outsideRth, GTC) - Updated #179 CRITICAL FIX IMPLEMENTED Stop orders now use STP LMT (STOP-LIMIT) for ALL exchanges with outsideRth=True and GTC (Finding #179 - Feb 10, 2026) Stop Order Configuration: Key Benefit: Files: src/earnings/order_executor.py...

Finding #67: Software Backup Stop Loss - Severity: Medium - Status: done - Summary: Finding #67: ✅ Software Backup Stop Loss (Two-Layer Protection) CRITICAL FIX IMPLEMENTED Two-layer stop loss: Hardware (IBKR) + Software backup (Python) for maximum safety Layer 1: Hardware Stop (IBKR) Layer 2: Software Backup (Python) Verified Working: Files: src/earnings/trading_orchestrator.py...

Finding #68: Centralized Earnings Timing - Severity: Medium - Status: done - Summary: Finding #68: ✅ Centralized Earnings Timing System ENHANCEMENT IMPLEMENTED SBUX showed AMC instead of BMO - earnings timing was hardcoded in 4+ files Solution - Single Source of Truth: Files: src/earnings/earnings_timing.py, timing_data/overrides.json, scripts/check_earnings_timing.py Docs: STRATE...

Finding #69: Database Commission Type Fix - Severity: Medium - Status: done - Summary: Finding #69: ✅ Database Commission Type Fix (float→int) BUG FIX FIXED Files: src/earnings/trading_orchestrator.py (lines 1363, 1464)

Finding #70: Reconciliation P&L Calculation - Severity: Medium - Status: done - Summary: Finding #70: 🚨 Reconciliation Was Recording $0 P&L - FIXED CRITICAL BUG FIXED Reconciliation closed orphaned positions with $0 P&L and generic notes - actual losses were hidden! Problem (SBUX Example): Fix - Now Calculates Real P&L: SBUX Record Corrected: Files: src/earnings/trading_orchestrator....

Finding #71: Dashboard Statistics Fix - Severity: Medium - Status: done - Summary: Finding #71: 🚨 Dashboard Stats Wrong (ROI negative with positive P&L) - FIXED CRITICAL BUG FIXED Dashboard showed 11 trades, -1.93% ROI with +$55.34 P&L, and inconsistent counts across tables Problems Found: Fixes Applied: Corrected Stats: Files: earnings_dashboard/app.py (3 queries updated + sum...

Finding #72: Capital Timing Bug - Severity: Medium - Status: done - Summary: Finding #72: ⏱️ Capital Timing for Position Sizing - FIXED LOGIC BUG FIXED Position sizes calculated before exits don't account for capital freed from imminent exits Example (T position today): Files: src/earnings/risk_manager.py, src/earnings/trading_orchestrator.py

Finding #73: Orphaned Stop Orders - Severity: Medium - Status: done - Summary: Finding #73: 🚨 Orphaned Stop Orders Trigger Unintended Trades - FIXED CRITICAL BUG FIXED When positions closed, stop orders were NOT cancelled → orphaned stops triggered unintended trades SBUX Incident: Files: src/earnings/trading_orchestrator.py

Finding #74: Position Size Flex Not Applied - Severity: Medium - Status: done - Summary: Finding #74: 🚨 Position Size Flex Not Applied - FIXED CRITICAL BUG FIXED The Position Size Flex feature was documented but NEVER actually applied! Root Cause: Verified (Jan 28, 2026): Files: src/ibkr/client.py (POSITION_SIZE_FLEX_MAX = 0.57)

Finding #75: Centralized yfinance Rate Limiter - Severity: Medium - Status: done - Summary: Finding #75: 📊 Centralized yfinance Rate Limiter - FIXED HIGH PRIORITY FIXED 815+ rate limit errors per day from scattered yfinance calls Fix - Centralized YFinanceRateLimiter: Files: src/utils/caching.py (YFinanceRateLimiter), signal_generator.py, trading_orchestrator.py, earnings_detector.py, e...

Finding #76: Dashboard Timing Priority Bug - Severity: Medium - Status: done - Summary: Finding #76: 🚨 Dashboard Timing Priority Wrong (MA Shows AMC Instead of BMO) - FIXED CRITICAL FIXED Dashboard showed MA as AMC, but Yahoo Finance shows BMO - timing priorities were backwards Root Cause (Agent Error): Fix (app.py lines 1605-1629): Files: earnings_dashboard/app.py (lines 1605-1629)

Finding #77: Active Positions Not Showing in Timeline - Severity: Medium - Status: done - Summary: Finding #77: 🚨 Active Positions Not Showing in Timeline - FIXED CRITICAL FIXED IBM and MA positions were not visible in the Active Positions Timeline after earnings release Files: earnings_dashboard/app.py (get_active_positions_from_db), dashboard.html (timeline_positions section)

Finding #78: Yahoo Finance Rate Limit Bypass - Severity: Medium - Status: done - Summary: Finding #78: ⚡ Yahoo Finance Rate Limit Bypass - IMPLEMENTED PERFORMANCE FIXED Dashboard slow (~2 min load) due to Yahoo Finance rate limiting New Stats Tracked: Files: src/utils/caching.py (YFinanceRateLimiter), .env.example (FINNHUB_API_KEY)

Finding #79: P&L Audit and Database Integrity Fix - Severity: Medium - Status: done - Summary: Finding #79: 🔍 P&L Audit & Database Integrity Fix - FIXED CRITICAL FIXED Dashboard P&L numbers inconsistent: History=$60.59, Header=$87.33, Unrealized=$28.74 Root Cause (IBKR as Source of Truth): Fixes Applied: After Fix: Total Realized P&L = $29.29 | Discrepancy = $0.00 Action Required: ...

Finding #80: Earnings Detection Persistence Fix - Severity: Medium - Status: done - Summary: Finding #80: 🎯 Earnings Detection Persistence Fix - FIXED CRITICAL FIXED MA/IBM earnings status not showing on dashboard despite logs showing "BEAT signal" Root Cause: Fixes Applied (trading_orchestrator.py): After Fix: MA & IBM show earnings_outcome='beat', actual_beat=True ✅ Principle: Serv...

Finding #81: NYSE Stop-Loss Order Type Fix - Severity: Medium - Status: done - Summary: Finding #81/#179: 🛡️ ALL Exchanges Stop-Loss Order Type Fix - FIXED CRITICAL FIXED Pure STOP orders are unreliable in extended hours (NYSE MARKET rejection + NASDAQ Warning 2109 outsideRth ignored) Background: Root Cause: Fix Applied: NYSE: STOP-LIMIT | NASDAQ: STOP-LIMIT ✅ Trade-off: STOP-LIMIT ...

Finding #82: Table Synchronization Share Count Fix - Severity: Medium - Status: done - Summary: Finding #82: 🔄 Table Sync Share Count Fix - FIXED DATA INTEGRITY FIXED Dashboard showed V=1 share while IBKR had 2 shares Root Cause: Fix Applied: V now shows 2 shares in both tables ✅ Principle: Both tables must always be synchronized. When fixing one, fix both.

Finding #83: Duplicate Order Prevention Fix (PositionInfo Attribute Access) - Severity: Medium - Status: done - Summary: Finding #83: 🚨 Duplicate Order Prevention Fix (Finding #58 Regression) CRITICAL BUG FIXED 🚨 Finding #58 "fix" was NEVER working - AAPL/V got 2x position size! Service restart at 9:38 AM triggered duplicate orders. Finding #58 check failed silently due to code bug. What Happened: Root Cause - Find...

Finding #84: Earnings Timing Data Source Priority Fix - Severity: Medium - Status: done - Summary: Finding #84: 📅 Earnings Timing Data Source Priority Fix DATA INTEGRITY FIXED DIS showed as BMO when it's actually AMC! External source (EarningsCalendar) returned wrong timing, overriding correct database value. Root Cause: Fix Applied: Principle: "Once verified in database, don't override from e...

Finding #85: Market Health Indicators Fix (VIX/SPX) - Severity: Medium - Status: done - Summary: Finding #85: 📊 Market Health Indicators Fix (VIX/SPX) ENHANCEMENT FIXED VIX showed "--" and SPX showed "0" most of the time yfinance API is rate-limited and unreliable, causing empty responses. Root Cause: Fix Applied: Market health indicators now always show valid data ✅ Files: app.py (get_cache...

Finding #86: Exposure Limit Not Enforced - Severity: Medium - Status: done - Summary: Finding #86: 📈 Exposure Limit Not Enforced CRITICAL FIXED Exposure reached 78.1% when max should be 70% Logs showed "Current exposure: $0" when actual positions were worth $1,170. Root Cause: Fix Applied (4 locations): Exposure now correctly calculated from actual positions ✅ Principle: When the ...

Finding #87: Earnings Detection Not Persisting (ib_insync Thread Safety) - Severity: Medium - Status: done - Summary: Finding #87: 🧵 Earnings Detection Not Persisting CRITICAL FIXED V and AAPL earnings detected but never persisted to database Polymarket showed 99.95% beat signal, callback fired, but dashboard showed no outcome. V detected 20+ times without removing from monitoring. Root Cause: Fix Applied: Earni...

Finding #88: Stop-Loss Tracking and Slippage Analysis - Severity: Medium - Status: done - Summary: Finding #88: 📊 Stop-Loss Tracking & Slippage ENHANCEMENT IMPLEMENTED XOM stop-loss exit: -2.46% actual vs -2.00% designed Needed to track slippage ($0.65 / 0.46%) and understand stop order execution. XOM Analysis: What Happened: Tracking Implemented: Market Session Values: Future trades track sto...

Finding #89: Exit Verification Ignored - Severity: Medium - Status: done - Summary: Finding #89: 🚨 Exit Verification Ignored CRITICAL BUG FIXED XOM position closed in DB but still existed in IBKR! Exit verification detected failure but code ignored it and closed position anyway. What Happened: Root Cause: Code checked if result.success (order submitted) but not result.exit_verif...

Finding #90: Root Cause Analysis - Severity: Medium - Status: done - Summary: Finding #90: 🔍 Stop Order & Market Hours Root Cause ROOT CAUSE FIXED Three root causes discovered in IBKR/DB audit: 90A: Orphaned Stops (IBM) Cancellation used openTrades() which missed PreSubmitted orders. Now uses tracked order ID with 3 retries. 90B: Duplicate Stops (V, AAPL) No duplicate chec...

Finding #91: Stop Orders Dictionary Not Populated - Severity: Medium - Status: done - Summary: Finding #91: 🔄 Stop Orders Not Repopulated on Restart ROOT CAUSE FIXED XOM LIMIT orders kept appearing on service restart stop_orders dictionary was empty after restart → software backup thought no hardware stop existed. Two Gaps Found: Fixes Applied: All positions now have stop_order_id populate...

Finding #92: Dashboard Reality Gap - IBKR Overlay Added (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: ✅ FIX APPLIED: Result: Dashboard now shows IBKR reality alongside DB state.

Finding #93: Findings Log Completeness & Dashboard Doc Sync (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: ✅ FIX APPLIED: Result: Findings are searchable, complete, and consistent with live trading logic.

Finding #94: XOM Reconciliation Drift - History vs IBKR Reality (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: ✅ FIX APPLIED: Result: History reflects only real trades; DB positions converge to IBKR reality automatically.

Finding #95: Trade Log Accuracy - Slippage, Fees, Order Types, Times (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: ✅ FIX APPLIED: Result: Trade log reflects actual execution details and consistent order types.

Finding #96: Historical Trade Backfill - IBKR Executions (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: Older trades were missing commissions, order types, and slippage because those fields were never persisted at execution time. ✅ FIX APPLIED: Result: Historical trade log reflects IBKR execution reality.

Finding #97: Flex Query Backfill - Full Historical Coverage (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: Live IBKR API only returns same-day executions, so older trades could not be fully backfilled. ✅ FIX APPLIED: Result: Historical trade log can be fully aligned to IBKR execution history.

Finding #98: Upcoming Earnings Table Regression - Gamma Batch Fetch (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: MCap blank, priority SKIP for most entries, conviction unreliable (0%/100%). ✅ FIX APPLIED: Result: Conviction, priority, and MCap tiers display correctly with reliable data.

Finding #99: Live Data Freshness & IBKR Position Truth (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: Dashboard treated DB state as live truth and silently used stale snapshot data. ✅ FIX APPLIED: Result: Live state reflects broker truth with explicit freshness controls.

Finding #100: AXP Earnings Timing Correction (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: AXP timing showed AMC in dashboard while NASDAQ listed BMO. ✅ FIX APPLIED: Result: Dashboard timing aligns with NASDAQ for AXP.

Finding #101: IBKR Execution Source‑of‑Truth Pipeline (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: Trade log drifted from IBKR reality (missing commissions, unclear order types, stop-loss slippage not visible). ✅ FIX APPLIED: Result: Trade log aligns to broker‑truth with explicit slippage handling.

Finding #102: Daily Trade Reconciliation Job (Updated Jan 31, 2026) - Severity: Medium - Status: done - Summary: Original Issue: No daily check to confirm trade_audit_log stays aligned with the IBKR execution ledger. ✅ FIX APPLIED: Result: Daily alerts surface any drift between the trade log and broker‑truth.

Finding #103: IBKR Reconciliation Auto-Fix (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: Reconciliation surfaced mismatches but didn’t auto-fix or report combined trade + position status. ✅ FIX APPLIED: Result: Continuous broker‑truth reconciliation with visible auto‑fix status.

Finding #104: Entry + Stop Atomicity (IBKR Parent/Child) (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: Entry orders could be acknowledged even if the attached stop order failed, leaving positions unprotected. ✅ FIX APPLIED: Result: Entries are only confirmed when a valid protective stop is in place.

Finding #105: Batch Stop Placement Safety (Updated Jan 30, 2026) - Severity: Medium - Status: done - Summary: Original Issue: Batch entries could fill without a confirmed stop‑loss if stop placement failed, leaving positions unprotected. ✅ FIX APPLIED: Result: Batch entries are never left unprotected after fills.

Production Evidence Snapshots

2026-02-21

2026-02-23

2026-02-24

2026-02-26