← Back to Dashboard

Edge Cases & Handling

Version: 1.8 | Last Updated: April 27, 2026 | Status: ✅ APPROVED


Critical Timing Change

⚠️ AMC Exit Time: 3:57 PM (not 3:58 PM)

This provides a 1-minute buffer before BMO entries at 3:58 PM to prevent order execution conflicts.


Category 1: Market Schedule

# Edge Case Handling Alert
1.1 Market Holidays (NYSE closed) Skip trading, check calendar None
1.2 Early Close Days (1 PM close) Exit at 12:57 PM (AMC) / 12:58 PM (BMO entry) None
1.3 Late Open Days (delayed open) Wait for open + 2 min for BMO exit None
1.4 Trading Halts (stock-specific) Alert, retry on resume ⚠️ WARNING

US Market Holiday Calendar 2026

Date Holiday Market Status
Jan 1 New Year's Day CLOSED
Jan 20 MLK Day CLOSED
Feb 17 Presidents Day CLOSED
Apr 3 Good Friday CLOSED
May 25 Memorial Day CLOSED
Jun 19 Juneteenth CLOSED
Jul 3 Independence Day Eve EARLY CLOSE 1 PM
Jul 4 Independence Day CLOSED
Sep 7 Labor Day CLOSED
Nov 26 Thanksgiving CLOSED
Nov 27 Day after Thanksgiving EARLY CLOSE 1 PM
Dec 24 Christmas Eve EARLY CLOSE 1 PM
Dec 25 Christmas CLOSED

Category 2: Weekend/Holiday Holds

# Edge Case Handling Alert
2.1 3+ day weekend hold Trade with 50% position size (canonical — signal_generator.py symbol long_weekend_multiplier = 0.5 if is_long_weekend else 1.0 (cite the symbol, not a line number — line refs rot); reconciled Apr 26, 2026 — was 75% in this doc, but live bot has always used 50% per STRATEGY.md §5.2.1) None
2.2 Earnings date moved after entry Exit at market immediately ⚠️ WARNING

Category 3: Order Execution

# Edge Case Handling Alert
3.1 No shares available to short Skip trade, log reason ⚠️ WARNING
3.2 Extended hours SL/instant exit Accept slippage for safety None
3.3 Entry/exit during market closed Entry/scheduled exit: gate to regular hours (IBKR liquidHours); instant exit: GTC LIMIT only when fully closed ⚠️ WARNING
3.4 Fractional shares not available Fall back to whole shares (round down) None
3.5 Stock not on eligible exchange Use whole shares, log reason None
3.6 Exit order not filled (low liquidity) GTC LIMIT 5s → MARKET fallback (Jan 11 fix) None
3.7 Exit verification failed Keep DB open, alert for manual close 🚨 CRITICAL
3.8 External trade detected Log to external_trades table, alert ⚠️ WARNING
3.9 Orphaned DB position Detect on startup, alert for resolution 🚨 CRITICAL
3.10 Position mismatch (DB vs IBKR) Log discrepancy, alert for investigation ⚠️ WARNING
3.11 Stop-loss in extended hours (Finding #59, UPDATED #179) Use STOP-LIMIT for ALL exchanges (not just NYSE). Pure STP gets IBKR Warning 2109 on NASDAQ (outsideRth ignored, won't trigger after hours). HOOD lost 11% vs 3% intended. STP LMT respects outsideRth on all exchanges. None
3.12 Timezone-naive datetime comparison (Finding #60) All datetimes must be timezone-aware; ensure_timezone_aware() helper converts naive to ET None
3.13 Stop fill detection (Phase 4D) Real-time via IBKR orderStatusEvent fill callbacks through EventBus. Periodic reconciliation (5 min) serves as backup for missed callbacks or downtime. Solves CBOE/MCHP incident. None
3.14 Duplicate order from market fallback (Feb 9, 2026) Before ANY retry/fallback order, check IBKR position first. If position already exists at expected size, ABORT the fallback. Defense-in-depth guard in batch_order_manager._apply_market_fallback and order_executor.place_guaranteed_fill. Solves PFE/CME/ON duplicate pattern. None
3.15 Stale earnings date in DB (Feb 9, 2026; rebuilt Jul 29, 2026 — Finding #349; lookahead/#442 Sep 13, 2026) verify_earnings_dates() runs before each signal check (AMC 10:30 AM, BMO 3:50 PM). Votes across DB row + NASDAQ + Finnhub (+ yfinance tiebreak): unique ≥2-vote consensus ⇒ correct + stamp date_confidence='confirmed' (locks the date against Polymarket-derived sync overwrites); no majority ⇒ 'disputed' (row untradeable). Telegram alert on corrections/disputes; changes audited in earnings_date_audit including confirm-in-place vote sets (#442). Window and official-calendar fetch are 21 days (#442 — a 5-day window confirmed CBRL Sep 14 while NASDAQ listed Sep 23). NASDAQ+Yahoo pair may rewrite a locked PM date; hardcoded IR map is last-resort. Upcoming PM join is (ticker, date). Timeline DATE SPLIT when plan ≠ calendar. The old single-source NASDAQ auto-correct is gone — it lost a 2-day overwrite war with the hourly sync on SBUX (instant entry/exit). Rows with no external calendar coverage stay unverified (NULL confidence) and trade on end_time-consistent dates only (earnings_schedule_mismatch guard).
3.16 IBKR connectivity loss blocking trading (Feb 9, 2026) Error 1100 no longer persists trading block to disk. Errors 1101/1102 (connectivity restored) now properly clear the block. Health check auto-clears connection-related blocks on success. None
3.17 Stop triggered but fill lags (Feb 15, 2026) Track first stop-breach timestamp; if hardware stop remains unfilled past timeout, escalate to instant_exit:stop_timeout_escalation. 🚨 CRITICAL
3.18 Extended-hours stop fills + RTH gap risk (Apr 25, 2026 — Finding #228) Hardware STP-LMT carries outsideRth=True (Finding #179) so stops fire in pre/post-market when hit. The Finding #228 simulator quantified this on 51 stopped trades: blocking the broker fill outside RTH (the proposed v3 Finding A "FA_hard" variant) would force the position to gap-open at the worse RTH-open price 22 of 51 times, costing −$417 vs. live. The 9 recoveries (+$204) don't outweigh it. Operative behaviour: keep extended-hours stops. RuleB (±90 min no-stop window) initially survived the 4-check bias gauntlet but was REFUTED gap-aware (#330, Jul 5 2026) and its apparatus REMOVED (#380, Aug 21 2026) — NO stop-loss rule variant currently survives. Do NOT change extended-hours stop policy without re-running scripts/analyze_finding_a_full.py. None
3.19 Halt-suspect cases in extended hours (Apr 25, 2026) The Finding #228 simulator flags any extended-hours bar gap >30 min combined with a >3 % price jump as halt-suspect. Two real cases observed: ARES Feb 5 (134-min gap, +3.71 %) and NYT Feb 4 (41-min gap, +3.92 %). Current behaviour: hardware STP-LMT may fill at the gap-through price. No software intervention required — the structural protection is the STP-LIMIT order's limit price (Finding #179). Future research: tighter halt detection if frequency increases. None (informational)

Category 4: Gateway/System (CRITICAL)

# Edge Case Handling Alert
4.1 Gateway disconnected Attempt reconnect, check 2FA 🚨 CRITICAL
4.2 Sunday 2FA (weekly) Approve on IBKR app at 10:00 AM Abu Dhabi ⚠️ WARNING
4.3 Session invalidated 2FA required - approve on phone 🚨 CRITICAL
4.4 Server reboot systemd auto-start, IBC handles login ⚠️ WARNING
4.5 Database connection lost Fallback to local state 🚨 CRITICAL
4.6 Mon-Sat restart IBC in-process AutoRestart keeps the session (#437); ibkr-monitor pages if :4001 stays down >180 s (#448); the old gateway_restart_manager.sh is retired (#476) None

Session Invalidation Causes: - Gateway process killed externally (kill, systemctl stop) - Login from IBKR Portal, TWS, or Mobile app (same username) - VNC service crash affecting Gateway - Server reboot without IBC auto-start


Category 5: Circuit Breakers

(SSOT: src/earnings/risk_manager.py class docstring + src/earnings/session_stop_breaker.py. Updated Aug 21, 2026 per CLB-1/A-6, Finding #366: the consecutive-loss breaker — old row 5.5, "5+ consecutive losses" — is RETIRED; daily loss is the sole loss brake, with sizing tiers re-keyed to day P&L.)

# Edge Case Handling Alert
5.1 Daily loss -5% (DAILY_LOSS_LIMIT) Sole loss brake — stop for the day; day P&L DB-anchored; resumes at the true next ET market open; survives restarts (A-6) ⚠️ WARNING
5.2 Drawdown 30%+ Reduce position sizes 50% (40%: 75%; 50%: full pause) 🚨 CRITICAL
5.3 VIX > 35 No new entries (VIX > 30: sizes ×0.50) ⚠️ WARNING
5.4 S&P -5% intraday No new entries, hold existing 🚨 CRITICAL
5.5 Day P&L ≤ −2.5% / ≤ −4% Sizes ×0.50 (ELEVATED) / ×0.25 (HIGH) — re-keyed from the retired loss-streak tiers None
5.6 4 stop-exits in one ET day (session-stop breaker, session_stop_breaker.py) Pause signal generation for the rest of the session (#312; threshold 3→4 per #335.b) ⚠️ WARNING

Category 6: Position Management

# Edge Case Handling Alert
6.1 Position stuck (no exit after expected time +15 min) Force market exit 🚨 CRITICAL
6.2 Multiple orders at same time Execute exits first (3:57 PM), then entries (3:58 PM) None

Timing Schedule Summary

Event Time (ET) Notes
AMC Signal Check 10:29 AM Verify conviction (5 min before entry)
BMO Exit 10:32 AM 1h after open (captures first-hour gains)
AMC Entry 10:34 AM 1h 4 min after open
BMO Signal Check 3:53 PM Verify conviction
AMC Exit 3:57 PM 1 min buffer before BMO entry
BMO Entry 3:58 PM 2 min before close

Alert Summary

🚨 CRITICAL Alerts (Immediate Action Required)

⚠️ WARNING Alerts (Monitor)


Category 7: Code/System Bugs

# Edge Case Handling Alert
7.1 Missing logger import All modules must have import logging + logger = logging.getLogger(__name__) 🚨 POST-MORTEM
7.2 Silent exception in signal generation Caught exception causes 0 signals - check logs for "Error processing market" 🚨 CRITICAL
7.3 API rate limiting Retry with exponential backoff, cache data ⚠️ WARNING
7.4 Database schema mismatch Run migrations before deploy 🚨 CRITICAL
7.5 Wrong volume field in threshold check Use volume_total (cumulative) NOT volume_24h (24hr rolling) 🚨 POST-MORTEM
7.5b Trader vs dashboard volume gate drift One SIGNAL_MIN_VOLUME_TOTAL from Config; dashboard upcoming Polymarket uses DB volume_total + same config — Finding #222 🚨 POST-MORTEM
7.6 Dashboard filtering ignores SHORT signals When filtering tradeable signals, check BOTH ≥70% (LONG) AND ≤30% (SHORT) 🚨 POST-MORTEM
7.7 Conviction query for future entry time When entry time is in future, use _get_latest_conviction() for most recent data 🚨 POST-MORTEM
7.8 Schema mismatch in conviction query Conviction is in price_snapshots.yes_price, NOT markets.yes_price 🚨 POST-MORTEM
7.9 Missing stocks in market cap lookup Always have yfinance fallback + cache for 24h. Apr 26, 2026 (Finding #249): chain is now yfinance → Polygon → Finnhub. Polygon Stocks Starter ($29/mo) gives ~98 % hit rate with separate quota from Yahoo + Finnhub. 🚨 POST-MORTEM
7.10 pg8000 mis-parses :name::cast (Finding #263, Apr 27, 2026) pg8000 driver parses :: as a parameter-prefix continuation in SQL like :exit_time::timestamptz, raising syntax error 42601. Always use CAST(:name AS type) instead. Phase-2's hold_duration_hours writer used :exit_time::timestamptz and silently broke the entire primary close path for every trade, falling through to a fallback UPDATE that didn't write hold_duration_hours, conviction_at_exit, prediction_correct, or financing aggregates. Same-pattern audit: rg ':\w+::' src/ earnings_dashboard/ clean. 🚨 POST-MORTEM
7.11 Yahoo's GAAP scrape can override confirmed Polymarket non-GAAP signal (Finding #264, Apr 27, 2026) VZ earnings BMO: Polymarket said YES=99.95% (BEAT — company beat non-GAAP), trader had restarted ~30s before Yahoo polled, in-memory _confirmed_polymarket dict was empty during confirmation window, Yahoo's $1.20 vs $1.21 GAAP scrape (-1¢) won. Trader exited a CORRECT LONG position. Two-layer defense in earnings_detector: (a) Yahoo contradiction now BLOCKS when pm_confirmed exists (matches PRNewswire/BusinessWire); (b) new _live_polymarket_extreme_outcome() reads price_snapshots.yes_price directly so post-restart extreme prices (≥99% / ≤1%) still block Yahoo regardless of in-memory state. 🚨 POST-MORTEM
7.12 Dashboard volume display vs trader gate misalignment (Finding #265 + #265.v2, Apr 27, 2026) Trader gate uses effective_cumulative_volume_usd(db_volume_total, gamma_volume) (cumulative). Dashboard widget had been using gamma_volume directly (24h rolling per Polymarket Gamma's wonky labelling). VZ rendered volume: 1226 / volume_ok: false / SKIP while the trader correctly entered with cumulative $3,920. Both branches of get_polymarket_earnings_from_db (gamma success + except-fallback) now use effective_cumulative_volume_usd. New transparency keys (volume_24h, volume_cumulative, volume_db_total, volume_gamma_raw) surface all four metrics on the API response. Step-5 lesson: v2 fix was needed because the original v1 fix only patched the success branch; agent didn't curl /api/earnings-calendar to verify VZ post-deploy. Going forward every fix MUST end with a probe of the changed output. 🚨 POST-MORTEM
7.13 pg_dump OOM on growing DB (Finding #267, Phase 5 fix, Apr 27, 2026) Old code: process.communicate() loaded the entire pg_dump output (~32 GB raw) into Python bytes before writing to gzip → OOM-killed every run after DB grew past ~6 GB on a 7.8 GB host. Fixed: stream pg_dump.stdout in 1 MB chunks directly into gzip.open(). Memory footprint now O(chunk_size) regardless of DB size. polymarket-backup.service was OOM-killed Apr 25 / 26 / 27 04:00 UTC before the fix shipped 13:43 UTC; first proven success Apr 28 04:00 UTC. 🚨 POST-MORTEM
7.14 Polymarket Gamma volume24hr field intermittently NULL (Finding #272, Apr 27, 2026) Live probe: volume24hr=None on every market in /events/pagination response. volume, volume1wk, volume1mo, volume1yr remain populated. Our continuous_collector was writing m.get('volume24hr') or 0 to markets.volume_24h → 88 % of recently-synced markets had volume_24h=0 → signal_generator.get_market_volume() treated 0 as falsy → signal.volume_24h=None → INSERT wrote NULL. 76 consecutive trades since Feb 16, 2026 had volume_24h_at_entry=NULL before the fix. signal_generator.get_market_volume() now falls back to most-recent positive price_snapshots.volume_24h within last 6 hours. Backfilled 114/117 (97%) historical rows. Note: trader's volume gate uses volume_total (cumulative) which is unaffected — only the 24h companion was lost. 🚨 POST-MORTEM
7.15 Integrity monitor must watch batch-service exit codes, not just timer fires (Finding #268, Apr 27, 2026) Phase 1 made polymarket-backup, polymarket-resolver, polymarket-trade-reconcile, polymarket-equity-bars, polymarket-data-backfill, polymarket-earnings-sync exit non-zero on real failure. Phase 3's monitor watched systemd:* (always-on) and systemd_timer:* (timer-fire freshness only). Net: polymarket-backup was OOM-killed 3 nights running with zero Telegram alerts. Fix: new _check_systemd_unit_last_result() helper reads Result + ExecMainStatus + ExecMainExitTimestamp so a unit's most-recent failed exit is monitored even when currently inactive. 6 new systemd_last_run:* rules. 🚨 POST-MORTEM

Post-Mortem: Finding #29 (Jan 12, 2026) - conviction_calculator.py used logger without importing - caused ALL signal generation to silently fail - Impact: Missed 3 BMO trades (JPM, BK, DAL) for Jan 13 earnings - Fix: Added import logging and logger = logging.getLogger(__name__) - Prevention: Code review checklist must include logger import verification

Post-Mortem: Finding #30 (Jan 13, 2026) - Signal generator checked volume_24h instead of volume_total for the volume threshold (the gate was $2,000 at the time of this post-mortem; the current live volume gate is $1,000 per Finding #311) - volume_24h was stale/zero for many markets (DAL=$0, BK=$0, WFC=$0) - volume_total (cumulative market volume) was correctly populated ($32K, $11K, $23K) - Impact: Multiple trades incorrectly skipped due to "$0 volume" when they had sufficient volume - Fix: Changed signal_generator.py line 874 to use volume_total for threshold check - Prevention: Always verify field semantics (24h rolling vs cumulative total) match business logic

Post-Mortem: Finding #31 (Jan 13, 2026) - Dashboard "Pending Opportunities" section only filtered for conviction ≥70% (LONG signals) - SHORT signals (conviction ≤30%) like WFC were completely ignored in the timeline - Signal generator, priority calculator, and risk manager all correctly handle SHORTs - Impact: WFC SHORT signal (30% conviction) not displayed in timeline, could cause missed trades - Fix: Updated Jinja2 template to filter for BOTH ≥70% (LONG) AND ≤30% (SHORT) - Prevention: When filtering tradeable signals, always consider BOTH directions

Post-Mortem: Finding #32 (Jan 13, 2026) - THREE CRITICAL BUGS Pre-trade simulation discovered 3 bugs that would have blocked ALL BMO trades:

  1. Conviction Calculator Entry Time Bug
  2. get_conviction_at_entry_time() queried for future entry time (e.g., tomorrow 3:53 PM)
  3. No orderbook data existed for future times → returned None
  4. Fix: Added _get_latest_conviction() to return most recent data when target is in future
  5. File: src/earnings/conviction_calculator.py

  6. Trading Orchestrator Schema Mismatch

  7. _get_current_conviction_and_volume() queried markets.yes_price
  8. But yes_price column is in price_snapshots table, NOT markets
  9. Fix: Query price_snapshots for conviction, markets for volume
  10. File: src/earnings/trading_utils.py (moved from trading_orchestrator.py in Phase 4A)

  11. Market Cap Returns None for Major Banks

  12. MARKET_CAPS dict missing BAC, C, WFC, GS, MS, and many others
  13. Market cap returned None → defaulted to MID cap classification
  14. Fix: Added 50+ missing stocks + yfinance fallback with 24h cache
  15. File: src/earnings/signal_generator.py

  16. Discovery: Pre-trade readiness simulation at T-12 hours

  17. Impact: ALL BMO trades would have failed (0 signals generated)
  18. Prevention: Always run comprehensive pre-trade simulation before market open

Last updated: April 27, 2026 (added Cat 7.10-7.15 from Apr 27 evening rigorous §B audit — Findings #263 / #264 / #265.v2 / #267 / #268 / #272)