Purpose: Single entry document for a new AI or human operator. It maps all project documentation, runtime layout, rules, and where to look first. Update this file when onboarding expectations change.
Repository: https://github.com/eumarius/earnings-trading-bot-.git (default branch: master)
Finding #479 DB activity logging DEPLOYED Sep 26, 2026 20:55 UTC (deploy #8 614a29d; the post-deploy evaluator defect (security:db_client_activity could not evaluate, so the foreign-client page was inactive 20:58–21:33 UTC) is fixed DEPLOYED Sep 26, 2026 21:30 UTC (deploy #10 f28c0da); the rule reads ok):
code-derived DB entrypoint identities, a
query-free 15-second all-database pg_stat_activity sampler, rotated host
evidence, a persistent foreign-client latch, and one CRITICAL page per new
foreign address. DigitalOcean extension and off-cluster sink steps are in the
Project-store reliability runbook.
| Order | Document | Why |
|---|---|---|
| 1 | .cursor/memory.md (repo root; may require git show if .cursor/ is gitignored locally) |
Mandatory fix checklist, deployment rule, trade prohibition, findings rule, cross-interface rules (§M volume/gates), user preferences. |
| 2 | docs/CONTEXT.md |
Architecture, key file paths, directives (IBKR vs DB truth, no hardcoded capital, dashboard parity). |
| 3 | docs/STRATEGY.md |
Trading rules, schedules, position sizing, volume filter (§2.4), stops, exchanges. |
| 4 | docs/OPERATIONS.md |
Server access, systemd services, deploy, logs, troubleshooting. |
| 5 | docs/FINDINGS.md |
Canonical narrative for every finding/fix (per memory §J). Dashboard “Testing Findings” tab reads this via API. |
| 6 | docs/DATA_ARCHITECTURE.md |
Tables (markets, earnings_markets, price_snapshots, etc.), collectors, freshness expectations. |
| 7 | docs/DATABASE_DIRECTIVES.md |
Schema/migration discipline, earnings sync rules. |
docs/)| File | Contents |
|---|---|
| STRATEGY.md | Strategy, BMO/AMC schedule, conviction bands, caps, volume (effective DB-or-Gamma, SIGNAL_MIN_VOLUME_TOTAL), risk. |
| OPERATIONS.md | Droplet [hidden], SSH, [hidden], services, logs, backfill, env. |
| CONTEXT.md | Long-form context: modules, IBKR cache, reconciliation, config overview. |
| FINDINGS.md | All numbered findings; only place for canonical finding text (§J). |
| DATA_ARCHITECTURE.md | DB tables, collector sources (Gamma, CLOB, Dome), hypertables. |
| DATABASE_DIRECTIVES.md | DB authority rules, migrations, earnings_time (Finding #220). |
| DATA_INTEGRITY.md | Integrity checks, reconciliation expectations. |
| API_REFERENCE.md | Polymarket/Dome API fields (verify against live Gamma when debugging). |
| EDGE_CASES.md | Known failure modes (volume fields, shorts filter, etc.). |
| IBKR_CONTINUITY.md | Weekly 2FA, ibgateway-ibc.service, IBC config, restart procedure. (May 31 text is partly stale — recycle 2FA and #426–#428 are missing; use the Sep 2 briefing below.) |
| IBKR_SETUP.md | Gateway ports, VNC, connectivity. (May 10: paper described as ready-to-wire; paper is active. Capital figures drift.) |
| packs/2026-09-02-ibkr-vps-briefing/ | IBKR-on-VPS operating manual (PDF/HTML/MD): process chain, secrets locations, client IDs, watchdogs, 2FA, recycle, greenfield replica. Probed 2 Sep 2026. |
| STATE_MACHINES.md | Position/order state machines. |
| POSTGRES_OPERATIONS.md | DB ops, retention, memory. |
| PAPER_TRADING_TEST_PLAN.md | Paper testing checklist. |
| BMO_AMC_TIMING_DEEP_DIVE.md | Timing edge cases. |
| OPS_TIMING_VERIFICATION.md | Ops timing checks. |
| HOLISTIC_REVIEW_PROMPT.md | Large review prompt template. |
| CODE_AUDIT_RECONCILIATION_2026_08.md | Aug 22 code-audit reconciliation SSOT (TIER 0/1/2 queue; #393–#410). TIER 2 do-wave #401–#405; deferred T2.2 T2.6 T2.8 T2.11 T2.13 T2.15 ON-HOLD (#406). T2 leftover HTTP quota closed as #407. T2 leftover orchestrator helpers closed as #408. DONE-CLAIM/C10/C11 is #409. Aug 21–22 ship letter-audit is #410. |
| POSITION_SIZING_ANALYSIS.md / FINANCIAL_PROJECTIONS*.md | Analysis (may be dated; verify against code). |
| STRATEGY_IMPROVEMENTS.md | Improvement backlog / notes. |
| SYSTEM_REVIEW_2026_01_06.md | Point-in-time review. |
| HYPERLIQUID_API.md | If touching Hyperliquid (sniper path). |
| File | Note |
|---|---|
| AUDIT_FIX_QUEUE.md | Deprecated per FINDINGS.md / memory §J. |
| TESTING_FINDINGS_PENDING.md | Deprecated redirect. |
docs/archive/Historical specs, old Cloud deployment, superseded strategy docs. Do not treat as current unless cross-checking STRATEGY.md / code. Start at docs/archive/INDEX.md — the SSOT index of every archived/frozen doc (what it was, why historical, what superseded it; drift-audit C9 keeps it complete). It also points to the historical-DATA counterpart (the OPS-1 orderbook archive runbook in OPERATIONS.md).
| Item | Value |
|---|---|
| Server | root@[hidden] (DigitalOcean) |
| App root | [hidden] |
| Python venv | [hidden]/venv/bin/python |
| Env | [hidden]/.env (secrets; not in git) |
| Deploy script | [hidden]/deploy/deploy_from_github.sh — fast-forward to ONE pinned commit (DEPLOY_TARGET_SHA, HEAD asserted; #476 Round 4) + restart listed systemd units if present; launched detached by bash deploy/remote_deploy.sh [--sha <sha>] (F-8) |
| Dashboard URL | http://[hidden]:8080 |
Default workflow after code changes: commit → git push origin master → from the operator machine: bash deploy/remote_deploy.sh (detached systemd-run deploy, polled to DEPLOY_EXIT; OPERATIONS §8.A1) → verify earnings-trader + polymarket-dashboard active (see memory §H / §I).
Services commonly involved: earnings-trader, polymarket-dashboard, polymarket-collector (continuous_collector.py), ibgateway-ibc, ibkr-monitor, polymarket-earnings-sync.timer (hourly), polymarket-resolver.timer (daily 06:00 ET — outcome / counterfactual / regime, Finding #227), plus backfill units in deploy_from_github.sh.
| Area | Path | Notes |
|---|---|---|
| Earnings trader | src/earnings/ |
Orchestrator, signal generator, risk, orders, config.py, volume_policy.py (shared volume gate), market_regime_provider.py (shared regime/VIX, Finding #227), opportunity log. |
| Outcome resolver | src/data_tracker/collectors/outcome_resolver.py |
Daily learning-loop fill (Finding #227): actual_beat, eps_actual, stock_price_*, hypothetical_pnl, prediction_correct. Run by polymarket-resolver.timer. |
| IBKR | src/ibkr/ |
Client, gateway monitor, fees. |
| Data collection | src/data_tracker/collectors/ |
continuous_collector.py — orderbooks + Gamma market sync (~15m for markets.volume_total bulk sync). |
| Earnings ↔ DB sync | src/data_tracker/collectors/earnings_market_fetcher.py, src/db/ |
Hourly live writer + supported backfills; legacy manual entry points retired by #480. |
| Dashboard | earnings_dashboard/app.py, templates/dashboard.html |
FastAPI + Jinja; some metrics duplicated in inline JS — grep both when changing gates (Finding #225). |
| Tests | tests/unit/ |
Pytest; run with project venv / Python 3.11+ where zoneinfo exists. |
| Migrations | src/data_tracker/migrations/ |
SQL migrations. |
.cursor/memory.md — Mandatory checklist (§A–§M), trade execution prohibition (no orders without CONFIRMED), FINDINGS only in docs/FINDINGS.md, deploy after push unless user opts out.earnings_time / trusted timing: Finding #220, DATABASE_DIRECTIVES.md; do not persist yfinance as authoritative.src/earnings/volume_policy.py + SignalGenerator.get_market_volume — both go through the SAME helper (memory §M consolidation per Finding #282, Apr 30 2026). Effective cumulative = max(markets.volume_total, Gamma cumulative) since cumulative is monotonically non-decreasing. Earlier "DB else Gamma" semantic per Findings #222–#226 was changed because once-per-15-min DB sync became stale within seconds for active markets (silently rejected tradeable MRNA #2026-05-01 at $6.28 < $2k while live Gamma showed $3,734).# SSH
ssh root@[hidden]
# Trader + dashboard + gateway
systemctl status earnings-trader polymarket-dashboard ibgateway-ibc
# IBKR API port (live)
ss -tlnp | grep 4001
# Collector
systemctl status polymarket-collector
journalctl -u polymarket-collector -f
IBKR 2FA: systemctl restart ibgateway-ibc.service — approve on mobile within timeout; see IBKR_CONTINUITY.md.
src/earnings/config.py + .env)DATABASE_URL — SQLAlchemy DSN; production may use postgresql+pg8000://... (psycopg2 needs plain postgresql:// for raw scripts).SIGNAL_MIN_VOLUME_TOTAL — Default 1000 USD (lowered $2k→$1k May 30, 2026 per Finding #311 SHIP); 0 disables; aligned with volume_policy / get_market_volume.FINNHUB_API_KEY — Optional; Yahoo rate limits (earnings / mcap).trading_mode.txt on server; see OPERATIONS.md.pytest tests/unit/ (from repo root with PYTHONPATH or installed package).scripts/verify_retry_backoff_and_block.py runs on push/PR (see README.md).docs/FINDINGS.md only (severity, status, root cause, fix, files). Cross-reference STRATEGY.md / OPERATIONS.md / CONTEXT.md only as pointers.| File | Purpose |
|---|---|
| README.md | Quick start, repo link, high-level structure. |
archive/PROJECT_STATUS.md |
Historical snapshot (archived; may be stale — verify live). |
.env.example |
Non-secret env template. |
deploy/deploy_from_github.sh |
Production deploy + service restarts. |
requirements.txt |
Python deps. |
polymarket-deploy-20261004T120553Z-812717079, SHA 7c9d6b0, 12:05 UTC: NASDAQ calendar GET timeout=8 and a 45s budget; orderbook size FAIL names a finished drain; trader 12:06 UTC, integrity monitor 12:08 UTC; Gateway not restarted). Sep 26, 2026 — Sep 27, 2026 — Findings #481/#482 DEPLOYED (deploy #14 28b572b 00:33 UTC: #481 quarantines a permanently failing live-trade row once and marks it seen, market-data poison is WARN only; deploy #15 860af84 02:43 UTC: #482 migration 042 widens trades.token_label to varchar(64), the typmod probe is scoped to 042, and the three quarantined payloads — two distinct trades — were released, spool empty; the deploy-#14 dashboard restart paged once again, fix PR #9 pending; no trading change). Reliability Batch B3 — Findings #454/#455 and migration 039 — DEPLOYED Sep 26, 2026 23:29 UTC (deploy #13 00607c9, the exact approved SHA, pure fast-forward: paced IBKR in-process recovery with reset/latch truth; a durable DB-write spool for entry/exit/execution/reconciliation/live-trade writes; 039 idempotency keys; the live-trades poison-row loop (trades.token_label varchar(20) vs a 22-character outcome; 26 CRITICAL alerts 23:32:47–23:37:46 UTC; polymarket-live-trades stopped 23:39:08 by operator decision) was fixed by #481 (deploy #14 28b572b, Sep 27 00:33 UTC: poison rows quarantined once and marked seen, market-data poison WARN only, collector restarted) and #482 (deploy #15 860af84, 02:43 UTC: migration 042 widened token_label to varchar(64); the quarantined trades were released and the spool was empty at 02:51); the #454 soak (next 04:13 reset and 06:00 AutoRestart) is pending; no strategy change). The #479 sampler-grace race hardening DEPLOYED Sep 26, 2026 22:39 UTC (deploy #12 2ecb02c: one systemctl show snapshot per evaluation, a restart-loop FAIL that overrides the grace, the untrusted-address WARN checked before the stale grace; the deploy's own dashboard restart still raced the new monitor's first cycle and paged once, resolved five minutes later — follow-up OPEN; no trading change). Finding #467 and the #479 sampler restart-grace fix DEPLOYED Sep 26, 2026 22:19 UTC (deploy #11 3138aca: dashboard response secret-redaction filter; FastAPI auto-docs off and the three inert write endpoints removed; the IBKR account, wallet and server identifiers masked on public pages, docs included (one open gap: /api/btc5m/docs/doc_map still shows the raw WALLET_NAGI value, the six-character prefix of the third-party benchmark maker's wallet (not a full address, not Marius's wallet); a code follow-up is OPEN); the sampler rules tolerate a deploy restart for 120 s; no trading change). Findings #477/#480 DEPLOYED Sep 26, 2026 21:56 UTC (deploy #9 0e985dd: guarded data migrations 040 then 041 applied — #477 one P&L definition for closes and corrections plus a reconciler P&L-consistency check, BBW 274/307 −121.52 → −282.10 and SNOW 75 net −9.96 → −1.16; #480 pg8000 covered-channel lookup typed, the unaudited legacy sync retired, the four false leak rows relabelled timing_channel_mismatch; the Phase-1 net from the DB is now −$922.28 (n=13), matching the record; no strategy or admission change). Findings #457/#478/#479 DEPLOYED Sep 26, 2026 20:55 UTC (deploy #8 614a29d: #457 committed secrets scrubbed from HEAD + hash tripwire, the doadmin reset DONE 20:49:21 UTC before the deploy; #478 C-Q2/G-RS count trigger, 2/12; #479 per-service application_name + DB activity sampler — the post-deploy evaluator defect (security:db_client_activity could not evaluate, so the foreign-client page was inactive 20:58–21:33 UTC) is fixed DEPLOYED Sep 26, 2026 21:30 UTC (deploy #10 f28c0da); the rule reads ok; no live trading behaviour change). Findings #473/#474/#475 reliability follow-ups + docs sweep DEPLOYED Sep 26, 2026 19:05 UTC (deploy #7 a695050: #473 requirements pinned to the prod venv — the deploy never runs pip, prod pip freeze equals constraints.txt; #474 migration-baseline check — prod --check-baseline 5/5, the deploy ran no DDL; #475 C-Q2 detection replay, offline research tooling; docs sweep — Phase-1 P&L record, dashboard findings parser, drift-audit C10/C12, comment-only code edits; no live trading behaviour change). Finding #468 prod checkout hygiene DEPLOYED Sep 26, 2026 15:15 UTC (deploy #5 f130113; .gitignore + OPERATIONS §8.A2 + test only, no runtime change; prod git status --porcelain empty; prod moves DONE 14:59 UTC). Reliability Batch A DEPLOYED Sep 26, 2026 (deploy #1 884e528 10:18 UTC; #445 fix + F-7/F-8 deploy #2 3d7f1b1 11:48 UTC; apt-daily-upgrade hold ended 11:27 UTC): Finding #453 Monday 28 Sep stall stopgap (300 s dated watchdog budget, self-expiring Tue 00:00 ET); Finding #445 dashboard hypertable SQL bounded + Sep 25 read-only incident record (7-day coverage cached 6 h; per-statement transactions; 60 s statement timeout); Finding #444 deploy migration ledger (029 never re-runs; DDL under lock_timeout; output logged); Finding #443 needrestart covers the live xvfb.service/x11vnc.service (deploy-time coverage check; apt timer hold ended operator-attended at 11:27 UTC). Previous production baseline was #442 (DEPLOYED Sep 13, 2026). Finding #442 earnings-date join + 21-day verifier lookahead + timeline DATE SPLIT DEPLOYED Sep 13, 2026 (CBRL Wed 23 no longer wears Sep 14; verify 5→21 days; votes persist on confirm-in-place; #349/#424 leftover). Finding #441 1102 no nested util.run; attach-bounce after IB() reset DEPLOYED Sep 9, 2026 (04:14 1102 then 5h reset loop; farm sticky blocked bounce; #440 leftover). Finding #440 same-thread req after 2FA DEPLOYED Sep 7, 2026 (heartbeat/reconnect must not util.run on another loop; #87 leftover; 03:57 RESTORED then 40s Mode B). Finding #439 weekly last-resort 2FA budget DEPLOYED Sep 5, 2026 (one Python recycle per Sunday week; 7d cooldown; Sunday 06:00–18:00 UTC quiet; #438 leftover). Finding #438 last-resort recycle DEPLOYED Sep 5, 2026 (P1 req first; P2 2FA only Sunday or after IB() reset failed; #435 age/cooldown; #437 leftover). Finding #437 Saturday stay-connected DEPLOYED Sep 5, 2026 (recycle-off periodic IB() reset; heartbeat keeps probing; weekday AutoRestart reattach no bounce; attach down_since continuous; #436 leftover). Finding #436 weekly-only Sunday 2FA DEPLOYED Sep 3, 2026 (recycle-off default; Mode A/B keep probing; Mon–Sat session preserve; Sunday/crash attach-only; #434 leftover). Finding #435 recycle-once DEPLOYED Sep 3 (Mode B in-place first; no 2FA while Gateway young / cooldown / late-watch; 06:27/06:48/07:14 leftover). Finding #434 stay connected first DEPLOYED Sep 3 (recycle-on even with a live book; farm window still no-2FA; #430 leftover). Finding #433 keep probing when weekly 2FA holds recycle off DEPLOYED Sep 3 (Mode B exhaust-and-stop has no next step; header follows request_path_ok; 3 Sep 05:16 leftover). Finding #432 Mode A daily reconnect without 2FA DEPLOYED Sep 3 (1100 sticky farm probe until durable success; 900s timer no longer exhausts; Mode B leftover AMENDED by #433; recycle stays off). Finding #431 channel-window already-evaluated DEPLOYED Sep 2 (AEO C-1 generate_signal covers; any non-timing row; true timing-only still FAIL). Finding #430 weekly 2FA contract DEPLOYED Sep 2 (standing Gateway recycle off; midweek ibgateway-ibc restart was the extra 2FA; Sunday 06:00 stays; true needs GUARD-OVERRIDE-UNTIL). Finding #429 late-login quiet + Mode B window DEPLOYED Sep 2 (2FA resets 1800s quiet; 8 heartbeat fails/600s exhaust). Finding #428 late-login attach DEPLOYED Aug 30 (2FA abort keeps watching IBC+:4001; systemd-run bounce; flat-book re-check in that call). Finding #427 farm-recovery + standing recycle DEPLOYED Aug 29 (1100 keeps 900s probe window — no 61s zombie-exhaust; no-1100 TCP-up still #426 exhaust; flat-book auto-recycle Gateway then trader; IBC 06:00 is not recycle). Finding #426 Gateway zombie DEPLOYED Aug 28 (durable-only healthy; TCP-up trip exhausts not 300s loop; no client 99 on dead path; recycle CONFIRMED 11:22 UTC). Finding #425 etfstack VIX fallbacks DEPLOYED Aug 28 (Yahoo chart KeyError → history → Cboe → last-good cache). Finding #424 ASO official IR DEPLOYED (Sep 1 disputed → Sep 9 BMO confirmed). Finding #423 trade-reconcile BBW#274 DEPLOYED (live log + LMT fill + IBKR ingest). Finding #422 Trader Session READY DEPLOYED (Connected, not Unknown). ** Finding #421 IBKR one session machine DEPLOYED (TCP-up always in-place; connect/1102 clears REAL_DROP; bind errorEvent before connect; 1102 probes in place; STILL DISCONNECTED only if TCP down; PATH_DEAD/EXHAUSTED win the header; empty live book no ghost last-good stops; #420 1100-misclassify leftovers AMENDED; trader bounce — book flat). Finding #420 IBKR nightly-1100 leftovers DEPLOYED (CancelledError swallow; 1100 skips in-place exhaust; skip VIX/snapshot flood; UNREADABLE not on live book; last-good stops kept; recon-guard 1800 after exhaust; #419 AMENDED; trader bounce — BBW/DG hardware stops stay). Finding #419 IBKR in-place reconnect escape hatch DEPLOYED (quiet heartbeat while TCP up; one IB() reset then stop; no infinite 60s; leftover timeout cancel; UNREADABLE not on live book; #418/#416 AMENDED; trader bounce — HPQ hardware stop stays). Finding #418 IBKR reconnect amplifier DEPLOYED (keep TCP session on durable-health fail; last-good book; skip 412 reload; exit still attempts bounded send; #415 AMENDED; trader bounce — BBWI hardware stop stays). Finding #417 needrestart + overnight trades freshness DEPLOYED (live-trades + integrity-monitor excluded; overnight max_age 180 min / RTH 60 min). Finding #416 unverifiable IBKR ≠ missing DEPLOYED (IBKR UNREADABLE vs IBKR MISSING; overlay skip). Finding #415 IBKR latch leftover DEPLOYED (re-trip only inside durable probe; 300s reconnect while dead; get_quote skip; #413 AMENDED; trader bounce — BBWI hardware stop stays). Finding #414 channel-window leak already-admitted suppress DEPLOYED (ignore timing_channel_leak when same condition_id already signal_emitted/admitted; DKS Aug 25 false #384; trader + integrity bounced). Finding #413 IBKR request-path latch hysteresis DEPLOYED (3 successes incl. qualify to clear; 5 min reconnect cooldown; no ibkr_disconnect on #412 trips; re-trip leftover AMENDED by #415). Finding #412 IBKR request-path latch DEPLOYED (3 consecutive bounded_request failures → trading block 412 + Telegram ibkr_request_path + dashboard request_path_dead + integrity trader:ibkr_request_path; clear/reconnect/alert AMENDED by #413). Finding #411 orderbook-archive dual-limit pacing DEPLOYED (18/run, TimeoutStartSec=21600, 00:35+06:35 UTC). Trader not bounced (mid-session; no trading-path change). Finding #410 ship-verification letter-audit (record only; no trading-logic change). Finding #409 DONE-CLAIM + C10/C11 DEPLOYED Aug 22, 2026 15:23:13 UTC SHA defe78f. Finding #408 leftover-orchestrator twins DEPLOYED Aug 22, 2026 14:49:44 UTC SHA 4e4ffa1. One persist helper + one missed-exit/scheduled-exit helper. Trader process is defe78f (ActiveEnterTimestamp 2026-08-22 15:23:13 UTC); #410 did not bounce the trader. This docs-stamp is a later SHA. Previous: Finding #407 HTTP quota + five lean memory.md §F/§H clauses DEPLOYED Aug 22, 2026 13:39:29 UTC SHA 7fbfe05. T2 leftover HTTP quota closed. Previous TIER 2 do-wave Findings #401 #402 #403 #404 #405 #406 DEPLOYED Aug 22, 2026 07:53:22 UTC SHA 88014f4. Reservation credits 10:32 exits; CapitalManager gone; INTRADAY skip; Telegram log + fake schedule gone; one PDT constant; deferred T2.2 T2.6 T2.8 T2.11 T2.13 T2.15 ON-HOLD on the Action Queue. See FINDINGS for status markers. Previous: Aug 22, 2026 night — #400 get_positions fail-closed DEPLOYED Aug 21, 2026 23:00:26 UTC in the trader process at SHA e857f5e (systemd ActiveEnterTimestamp, NRestarts=0). Do not write “Aug 22 ~23:00 UTC”. This docs-stamp commit is a later SHA than the trader process — origin/master after this stamp is NOT e857f5e. #393–#399 remain live from the earlier 4292bdb restart (2026-08-21 21:43:10 UTC). T1.6 CLOSED on the wire Aug 22 ~22:38 UTC (POLYGON_KEY rotated + REST probe 200; burned default is only in rotate bak files). Prior implementer WRONG on UTC (wrote Aug 22 ~21:43 UTC), WRONG on 'must ROTATE' / 'prod .env still holds the burned default', and WRONG on 'fully memory.md'. Previous: Aug 21, 2026 15:17 UTC — THE QUEUED MARKET-SAFE TRADER RESTART EXECUTED (operator-approved, Friday ~11:15 ET inside the safe corridor): the trader now runs origin/master 7340dd5 — #378 + #379 + #380-code + #382-detector + #384 are LIVE in the trader process. Pre-check all green (0 open positions DB+IBKR, 0 breakers, 0 stops today, no signals in flight; snapshot /root/deploy_snapshots/pre_restart_378-384_20260821T151542Z); pull was a no-op (server already at origin/master — today's earlier targeted deploys had synced code, only the running process was stale); full deploy_from_github.sh run (contract check PASS, dashboard HTTP 200); post-restart NRestarts=0, reconciliation "DB and IBKR in sync", scheduler armed (10:30 AMC / 15:50 BMO / 15:55 exits), startup shows prnewswire (corroboration-only, #379) + zero RuleB lines, GUARD_REGISTRY clean (16 entries), integrity 72 rules / same 2 known FAILs (storage drain + FIVE disputed date) / rule set identical pre-post, §G probe proved the #384 closed=true fallback live on DE's closed market; #378/#384 epochs (Aug 23) unchanged — conservative enforcement start; inert RULEB_* prod-.env lines removed with a dated comment per the #380 plan. Earlier — Class-A queue day: A-1 DONE (#372 deep dive — operator REDIRECTED A-1 to "always use the market's own benchmark", census 1,307 markets: TRUE mislabels 141/1,239 = 11.4% + 12 poisoned trade flags + 34 free backfills — then APPROVED IN FULL → #382 IMPLEMENTED + §F backfill LANDED the same day: actual_beat now written by the 3-tier system (the market's own UMA resolution, Gamma degenerate-only, write-once-LOCKED pm_resolution_v1 > basis-matched benchmark EPS vs the market's stated strike parsed at discovery into benchmark_basis/benchmark_comparator (migration 034; strict >, tie→NO, abstain-on-unparseable, NEW shared src/earnings/benchmark_contract.py) > w357 sustained-crowd proxy, dashboard-marked ~provisional); pre-event conviction cascade + single-snapshot 0.95/0.05 RETIRED as label sources; resolver Pass 4 corrective; backfill verified on prod: 1,273 resolutions stamped / 141 relabels / 29 fills (5 of the census 34 self-filled between census and run, all agreeing) / 85 trade corrections / 2 condition_id repairs (CME #29 + XOM #19) / 0 conflicts / a1_resolution_compare probe 0 mislabels + 0 poisoned + 0 fillable / second run plans ZERO; displayed prediction accuracy 87.3% vs predicted ~88 (flips nearly cancel); audit CSVs scripts/analysis_357/backfill_372_audit{,_v2}.csv; the NEW labels:benchmark_parse drift rule caught REAL drift on its FIRST cycle — 6 whole-dollar-strike markets (no pt fraction, invisible to the legacy eps parser too) → slug parser v2 (PARSE_VERSION v2); resolver/dashboard/integrity DEPLOYED (migration 034 applied, targeted restarts, no positions held); the earnings_detector basis guard (GAAP: Yahoo street claims → UNKNOWN; wire claims refused persistence-alone) is TRADER-side — DEPLOYED Aug 21, 2026 15:17 UTC at the market-safe trader restart (with #378/#379/#380); commits 79122bd (implementation; jointly carried the in-flight A-9 detector change to keep the shared suite green) + 7d622a5 (parser v2) + the records commit; FINDINGS #372 (design) + #382 (implementation)); A-2 DONE (#371 — resolver 6-hour lockout fixed structurally: retry guard re-keyed from the shared updated_at the hourly sync kept touching to a resolver-OWNED resolver_attempted_at, migration 033; decayed actual_beat backlog self-heals on normal timer fires); A-3 DONE (#375 — the 5-part volume-integrity bundle: monotonic GREATEST guard on all 3 markets.volume_total writers; closed-market Gamma fetches fixed via a closed=true RETRY — the param is an EXCLUSIVE filter, verified empirically, the literal "add closed=true" would have blinded every open-market fetch; real events-sync pagination (the API silently caps at 100/page — limit=500 was a no-op beyond page 1); entered/past dashboard rows show ADMISSION-TIME volume (@adm) with ⛔ below-gate-AT-DECISION vs ⓘ below-gate-NOW icons; 24h~ marker on the rolling fallback; stored finals re-baseline FIX-FORWARD per #365; deploy = collector + dashboard restarts only, trader untouched); A-5 DONE (#378 — admitted-stage opp-log rows restored: they NEVER flowed live, all 369 historical admitted rows were the May-29 synthetic backfill of pre-Apr-22 markets and the census misread the backfill's Apr-8 coverage edge as a live stop; NEW _log_admitted_signal at batch scheduling in both cycles + consistency:opp_log_admitted_coverage HIGH integrity rule; DEPLOYED Aug 21, 2026 15:17 UTC at the market-safe trader restart — the rule epoch Aug 23 had assumed a weekend restart and stays as a conservative enforcement start); A-9 DONE (#379 — PRNewswire demoted to corroboration-only: NEW frozen CORROBORATION_ONLY_SOURCES = {'prnewswire'} makes the #342 arbiter REFUSE persistence-alone admission of a solo PRNewswire contra claim (the residual solo path on non-GAAP markets; crowd-agree + second-source corroboration stay open, pending stays armed, veto reset unchanged) + the BusinessWire poller DELETED (_check_businesswire gone — 842k failed/error log lines, ZERO detections ever, truth-audit §5); changelog #379 per §O; DEPLOYED Aug 21, 2026 15:17 UTC at the same trader restart as A-5); A-12 DONE (#380 — operator chose REMOVE: RuleB apparatus deleted entirely (module + orchestrator hooks + RULEB_* knobs + GUARD_REGISTRY entry + tests; phase3 refutation evidence KEPT); trader-log contradiction resolved live — the 2.33 GB unrotated log is REAL at [hidden]/logs/ (unit append: target, outside #274 logrotate), the 0-B file was the legacy /var/log twin, journald was 3.9 GB unbounded; logrotate extended + journald bounded 2G/14d INSTALLED LIVE, code deletion DEPLOYED Aug 21, 2026 15:17 UTC at the same trader restart); A-13 DONE (#381 — counterfactual window-truncation fixed: the CF cron had priced trades MID-FLIGHT (1,100/3,355 stored prices computed before the simulated exit; DELL +32.1% stored as +2.2%) — the pricing library now REFUSES unsettled (+30 min buffer) or bar-gapped (>5 min, stop-hit exempt) windows via never-persisted cf_refused_* versions, the cron SELECT excludes unsettled exits, resolver Pass 2 gains both guards, NEW HIGH rule consistency:cf_window_truncation, and --requeue-truncated re-queued the backlog through the fixed job; deployed LIVE, collector-side, no trader restart); A-14 DONE (#384 — the FINAL Class-A item, queue COMPLETE 14/14: silent coverage bugs fixed — (1) conviction-fetch failures (TMUS/AXP/PYPL class): Polymarket CLOSES earnings markets ~9:00 ET on earnings day BEFORE the 10:29 ET AMC conviction check and the #333 live-Gamma fallback lacked closed=true (the #375 exclusive-filter trap) → [] for exactly the closed markets it existed to save; fixed with a closed=true retry on empty + a 2-attempt transient-HTTP retry + WARNING-level failure logs (the silent debug swallow hid the class for months); (2) channel-window leaks (MA/WWD class): inconsistent timing resolution made BOTH channels say "not mine" until the right channel's entry moment passed; NEW classify_timing_mismatch stamps skip_reason='timing_channel_leak' + ERROR when the resolved channel's entry moment (AMC 10:39 ET; BMO midnight ET) has passed; TWO HIGH integrity rules coverage:conviction_fetch + coverage:channel_window_leak (explicit marker + timing-only structural sweep), epoch Aug 23; DEPLOYED Aug 21, 2026 15:17 UTC at the market-safe trader restart (conviction_calculator + signal_generator in-process, with #378/#379/#380/#382-detector) — §G probe proved the closed=true fallback live on DE's closed market). Earlier 07:10 UTC — OPS-4/OPS-5 CLOSED (#369/#370, deployed 06:56 UTC); earlier same day: DEPLOYED: PKG-357 (#366) + Phase-1 (#367) are LIVE. Operator "deploy all" executed in the overnight market-safe window (00:34 ET, market closed, 0 open positions, 0 breakers): server at 3465087; prod .env PDT_RESERVE_CAPITAL=17450 + stale SIGNAL_LONG_MAX_THRESHOLD 0.95→0.90 fixed; migrations 031/032 applied BEFORE the restart; close-persist contract check PASS; trading capital $10,054.38 live (PDT floor intact); integrity 0 FAIL incl. the new b5_pilot_auto_revert + open_position_release_window rules; Phase-1 pill + "Trades: 0" + frozen testing count 193 verified; monitors M-C1/K-S2C/K-B4/K-RES/K-B5 ARMED, D-6/D-7/D-9 RETIRED. Earlier — 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 the single Config.PHASE1_START_ET boundary (default 2026-08-21; deployer sets/verifies at deploy): blue "Phase 1" header pill, "Trades: N" counter (Phase-1 only), Test Progress cell removed, History Trading Capital ROI re-cut to three stacked windows (All-time / Testing in Production frozen at 193 trades / Phase 1), "Levrage"→"Leverage" typo fixed; changelog #367 + chart marker; NOT DEPLOYED — rides the PKG-357 deploy, checklist extended. Earlier — Finding #366: PKG-GO given and the FULL PKG-357 package IMPLEMENTED in code — 9 implementation commits + ship mechanics (A-7 and B-5 own commits): $10k capital step (repo lockstep; prod .env line at deploy) + S2c equal-split sizing with $9k activation gate + B-4 3-tier priority table + C-1 LONG band re-closed 0.90 + RES M1+M5 tonight-only reservation/A-10 + WAVE-RT + CLB-1/A-6 (consecutive-loss breaker retired, daily-loss 5% sole brake, persisted) + B-5 small-cap pilot (auto-revert ENCODED) + M-VOL V-BLEND shadow; migrations 031/032; full unit suite green py3.11. NOT DEPLOYED — the live bot runs pre-#366 code until the market-safe window; DEPLOY CHECKLIST in IMPLEMENTATION_PLAN_2026_08.md. Earlier — Finding #365: the BABA apparent volume-gate breach ($930 shown vs the $1,000 gate) deep-dived: DISPLAY ARTIFACT, zero true breaches (admission volume $2,145.74, gate PASS legitimate); A-3 EXPANDED into a 5-part volume-integrity bundle — records only, NO code shipped. Earlier — Finding #364.b: FUND-1 re-specified as a pure CONFIG/CODE change (verified live: equity $27,465.52 ALREADY in the account; PDT_RESERVE_CAPITAL 25000→17450 → trading capital $10,015.52; NO money movement; PDT floor margin $2,465.52) + the P1_SURFACES census-grade re-audit of the #363/#364 package additions (census 148→161, flags N14–N23, §11a addendum replaced by full inventories) + the binding memory.md execution-discipline note — records only. Earlier same day — Findings #363 + #364: circuit-breaker audit + operator decisions RECORDED (records only; NO trading-code changes, NO deploys, production DB untouched). #363: the commissioned breaker audit found the consecutive-loss breaker's count semantics BREAK under S2c (70% of 3-loss runs same-wave; 49 firings/47wk package replay, 28 blocking nothing; money ≈ $0 outlier-noise) → verdict RETIRE the counter, keep the daily-loss 5% brake; 33-control inventory 24 SAFE · 6 NEEDS-RETUNE · 3 REDESIGN; D-9 armed, D-4 extended. #364 (later same day): operator APPROVED CLB-1 exactly as recommended (counter display-only; daily-loss 5% = SOLE loss brake, restart-proof with true-ET resume + DB-anchored day P&L; hidden ≥3/≥5-loss sizing halving re-keyed to daily-loss tiers; A-6 elevated into the ship set), CONFIRMED the ONE-PACKAGE PKG-357 manifest (A-7 → C-Q1 capital step → S2c+activation gate → B-4 12-surface lockstep incl. the P1_SURFACES 148-surface inventory → C-1 → RES M1+M5 breaker-independent + A-10 → WAVE-RT BATCH-1/EQ-1/MSP-1 → CLB-1+A-6 → B-5 pilot → M-VOL shadow → arm M-C1/K-S2C/K-B4/K-RES/K-B5, retire D-6/D-7/D-9 → ONE changelog entry + supersession flips; 4 missed items flagged and added: the C-Q1 capital step, MSP-1, D-6/D-7 retirement, ship mechanics) and RECORDED the pending-decisions register (FUND-1 gates S2c activation; PKG-GO = the ONLY decision missing for the build to start; Class A queue A-1/A-2/A-3/A-5/A-9/A-11/A-12/A-13/A-14 = per-commit NOW work, corrected from "post-package"; A-12 fix-or-REMOVE flagged; OPS-1 CLOSED (#383 Aug 21 — option C lossless archive pipeline live, backlog drains overnight); OPS-2 CLOSED (#373); OPS-3 CLOSED (#377); C-Q2 ~Sep 13; C-Q3 before ~Oct 12). The consecutive-loss breaker STAYS LIVE and D-9 stays armed until CLB-1 ships. Records: FINDINGS #363/#364, IMPLEMENTATION_PLAN_2026_08.md (manifest + register sections), IMPLEMENTATION_PLAN_P1_SURFACES.md §11 addendum, STRATEGY_REVALIDATION Part I/§II.7/§II.8/App §15, dashboard rows breaker-retire-363 + wave-scale-retunes-363 + CLB-1 decision row → APPROVED-AWAITING-IMPLEMENTATION (all four template encodings already carry the #361 vocabulary), package row scope = the full manifest, both canvases. Previous: Aug 18, 2026 — Finding #362: VIK entered a DAY EARLY (Mon Aug-17 for a Wed Aug-19 BMO release) — root cause chain: #333 end_time OR-branch selection → #349 resolved a Tuesday entry → NO entry-DATE guard (#359 only rejects LATE) → run_bmo_cycle's "All BMO have same entry time" batch assumption. Fix package SHIPPED: entry-date invariant wrong_entry_date at all 4 admission loops + batch scheduling + execution moment; post-resolution window re-check schedule_outside_window in the signal generator (closes #333↔#349); integrity rule consistency:open_position_release_window (HIGH; VIK Aug-17 allowlisted as known incident); multi-day BMO reservation dedup; dashboard ET timestamps + WRONG-ENTRY-DAY red highlight; ON/NET date-drift recorded as a measurement caveat (PLAYBOOK §5), NO historical rows repaired. VIK LEFT OPEN per operator decision (stop live, exits Wed Aug-19 10:32 ET; tonight's cycle cannot re-enter it — already-have-position check). 1058 unit tests green. Previous: Aug 17, 2026 — Finding #361 + #361.b (corrective sweep + DEPLOY: the #361 records are now LIVE on the dashboard; stale-pending text on 8 surfaces fixed; status-vs-description tripwire added): operator APPROVED all three open Part II decisions — C-1 (re-close the LONG cap at 0.90), RES (the M1+M5 reservation mechanism, multi-day component dropped), B-5 (the small-cap LONG 0.80–0.95 pre-registered pilot). RECORDS ONLY: nothing implemented, nothing deployed — the three carry the NEW status APPROVED-AWAITING-IMPLEMENTATION (distinct from open AND from implemented-pending-deploy), wired into build_revalidation_357() / build_action_queue() (rows reservation-m1m5-357, smallcap-long-b5-357; relax-202-9095 annotated) and ALL FOUR dashboard template encodings (meta/filter/summary-chip/sort + decStatusColor, the #334 tripwire). The #360 action plan is FINALIZED (every #357-program decision is made): decisions_done → implementation queue (capital/reserve step → the package awaiting the operator's EXPLICIT go — S2c+B-4+M1/M5+C-1 re-close, B-5 rides or ships separately → Class A fixes, A-7 first). Operator-requested plain-language reservation explainer added: doc §II.2.1 + plain_language field on the RES queue row. The #361 records ride the NEXT operator-triggered deploy. See FINDINGS #361. Prior: Aug 16, 2026 late — Finding #360: the dashboard Strategy tab RECONCILED to the re-issued decision record (records/dashboard only, NO trading behavior): the #357 section now renders the Part I/II decision states (C-Q1/S2c/B-4 DECIDED-NOT-IMPLEMENTED with the HARD-PDT red callout — equity ≥$25k, reserve ≈$17,450 NOT withdrawn, no day-trade counting in code; B-3/A-8 implemented as #359/#358 — DEPLOYED Aug 16, 2026 20:22 UTC; C-2 SUPERSEDED by the decided B-4 table; C-1/RES/B-5/C-Q2/C-Q3 open with recommendations), the ORDERED ACTION PLAN (decide C-1 + M1/M5 + B-5 now → deploy #358+#359+#360 → capital/reserve step → the S2c+B-4+M1/M5 package with A-7 prerequisite → Class A fixes) and the consolidated 16-row §II.7 monitor table. Action Queue reconciled: Audit re-validation → IMPLEMENTED (executed as #357); vix-2025-truesim-357 → D-2 DISCHARGED (shadow monitor); drop-5k-volume-bump-357 → RETRACTED (folded into B-4); #314.e SKIP candidates RETIRED; v3 D folded into B-4 / v3 E refreshed to D-5/B-008 criteria; NEW rows package-10k-s2c-b4-357 (rank 1, new status DECIDED-NOT-IMPLEMENTED), class-a-fixes-357 (rank 2), reservation-m1m5-357, smallcap-long-b5-357. Tests 25/25 + full unit suite green. DEPLOYED Aug 16, 2026 20:22 UTC — #360 shipped with #358+#359 in a single deploy_from_github.sh run at efebe45; the live tab renders the decision record (verified: PDT callout + 16-row monitor table + rank-1 package row served). See FINDINGS #360. Earlier same day: #357 RE-ISSUED (same filename): docs/STRATEGY_REVALIDATION_2026_08.md restructured into Part I decision record (DECIDED Aug 16: C-Q1 capital → $10,000 — inside a ≥$25k-equity account, the PDT constraint callout §I.1; reserve ≈$17,450, NOT withdrawn; S2c sizing; the B-4 3-tier table as fill-ordering/safety infrastructure, $0 profit claim; B-3 = #359; A-8 = #358) · Part II open decisions awaiting operator sign-off (C-1 close the 90–95 band — recommended CLOSE + band shadow monitor; M1+M5 reservation mechanism; B-5 small-cap LONG pilot; 12 remaining Class A fixes — A-7 is now an S2c prerequisite; ALL armed monitors/triggers/contracts consolidated in ONE table, §II.7) · Part III updated verdict register (51 HOLDS · 9 WEAKENED · 5 REFUTED · 10 INSUFFICIENT-DATA · 5 DORMANT; B-006 HOLDS→WEAKENED per §12; superseded rows retired explicitly) · Part IV study library — appendices §0–§3 and §7–§14 preserved verbatim with ORIGINAL section numbers (old §4 → Part III, §5 → Parts I–II, §6 → §I.4; every existing "§N" citation still resolves). Canvas rebuilt to match. #358 + #359 DEPLOYED Aug 16, 2026 20:22 UTC (single operator-triggered deploy with #360). Same day: Finding #359: stale-signal guard at ADMISSION shipped (operator-approved B-3 of the #357 queue): signals whose intended entry time is >5 min past are rejected as born_dead_schedule BEFORE consuming a slot/capital (new Config.STALE_SIGNAL_MAX_ENTRY_LATE_MINUTES, all 4 admission loops, W6 Bug-B _log_exposure_skip date-field fix rode along = A-4 done); 995 unit tests green; DEPLOYED Aug 16, 2026 20:22 UTC together with #358. Same day: Finding #358: the A-8 print-density collapse is ROOT-CAUSED and FIXED in code (three collector bugs: a markets-table lifecycle leak throttling the poller to ~1 print/5–9 min per market, a WebSocket collector with a frozen subscription list living outside the deploy pipeline, and a WS writer storing order-book LEVEL prices of EITHER token — NO uninverted — as yes_price; plus the #351 stale-median geometry still live on the PRIMARY detection path, now fixed with a fresh-window median + live-Gamma fallback). 976 unit tests green. DEPLOYED Aug 16, 2026 20:22 UTC (deploy/deploy_from_github.sh from the server; first reaper run closed exactly 1,288 stale rows). Historical WS-signature prints (volume_24h/liquidity NULL) are order-level prints, NOT market prices — exclude them from analyses; the #336/#339 "noisy stream" phenomena were largely this bug. See FINDINGS #358 + docs/STRATEGY_REVALIDATION_2026_08.md §10. Same day, earlier: Finding #357: FULL STRATEGY RE-VALIDATION at n=183 COMPLETE — ZERO trading changes shipped; all candidates queued for operator decision. The 9-workstream program (80-row assumption register under scripts/analysis_357/, reports w1–w9 + Phase-3 synthesis) delivered docs/STRATEGY_REVALIDATION_2026_08.md: 52 HOLDS · 8 WEAKENED · 5 REFUTED · 10 INSUFFICIENT-DATA · 5 DORMANT. Headlines: (1) core premise REFUTED as stated — the +$120.80 net book = stop-mechanics (+$123/+$321 vs no-stop) + named-5 outliers (+$400.92 > total net) + selection cells (70–75 band +$408/18 gauntlet-PASS); pre-release drift is generic (non-traded markets drift too), ~73% survivorship-conditioned, ≈0 post-Jul — falsifiable KILL monitor armed (Sep+Oct universe drift ≤0 AND book net ≤0 ⇒ regime edge dead); (2) resolver truth bug (W7 §0, 64/648 mislabeled, GAAP-vs-adjusted one-sided): corrected calibration Brier 0.1760→0.1432, overconfidence gaps ~halve, the "post-June high-band deterioration" REVERSES; 14 canonical trades carry poisoned prediction_correct (§F repair queued Class A-1); W9's no-conviction-gradient conclusion survives; (3) 0.90–0.95 reopened band FAILED its contractual re-gauntlet on real fills: −$1.90/trade vs +$6.19 promised (18 fills, ex-top-2 −$163) → OPERATOR DECISION C-1 pending: re-close the LONG cap at 0.90 (action-queue row relax-202-9095 → NEW status FAILED-REGAUNTLET); priority REFUTED as P&L ranker (ρ=−0.06), #313 volume gradient GONE (≥$5k tier worst) → C-2 pending: drop the +2@$5k bump; (4) volume-collapse narrative re-based: markets.volume_total rewritten DOWNWARD on 44% of markets — entry-time pass rates 51–83% all year (Jul/Aug best); May–Jun was the real (seasonal) trough; Oct season: slots bind, not universe; (5) all defensive/mechanical rules survived (stops 3/4/5 ~optimal, RuleB still FAILED-GAUNTLET at n=186, flat-25 OVER-delivered +$17.4/wk vs +$7.4, instant-exit apparatus net-NEGATIVE −$157 lifetime — kept as tail insurance). Queue: 12 Class A data/code fixes (resolver EPS-tier fix + §F backfill, volume monotonic guard, breaker persistence −$58, LLY admission backfill, PM snapshot density, PRN demotion, …) + 3 Class B operator decisions (C-1, C-2, stale-signal guard) + 3 Class C strategic questions (≥$10k capital, detection funding, Oct capacity) + 8 Class D armed triggers ($2k-gate re-price at n≈240, VIX 20–25 true-sim replay, monthly drift-decay monitor, …). Dashboard: NEW Strategy-tab section "2026-08 Full Re-validation (#357)" (build_revalidation_357()) + FAILED-REGAUNTLET status + 3 new queue rows. NO strategy_changelog entry (nothing strategy-affecting shipped). Prior entry — Finding #356 SHIPPED: guard-override tripwire — disabled live defenses police their own restoration. The #355 session-breaker override was restored Aug-9 instead of the planned Aug-5 night (the restore died with an interrupted agent session; zero behavioral impact verified — Aug-6 peaked at 3 stops, Aug-7 at 0). Structural fix: any .env guard-weakening line now requires # GUARD-OVERRIDE-UNTIL: YYYY-MM-DD (inclusive through end-of-day UTC); NEW integrity rule consistency:guard_overrides (HIGH, 55th rule) is OK while an override is future-dated (listed in detail), FAILS the moment the expiry passes or immediately when undated. GUARD_REGISTRY in integrity_monitor.py: SESSION_STOP_BREAKER_ENABLED (true), SESSION_STOP_BREAKER_THRESHOLD (≤10 — catches neutering), MAIN_LOOP_STALL_TIMEOUT_SEC (≤600), RULEB_MODE (known values); absent=safe default; unparseable=violation. Pure core evaluate_guard_overrides unit-tested against the literal Aug-5 override shape (correctly FAILS: undated). 17 NEW tests (947 green); OPERATIONS.md convention + memory.md §F line. Also Aug-9: Sunday IBKR 2FA rotation handled (gateway restart → operator push → reconnected, net liq $27,345); #355 commit landed (487dbac) + deployed. Prior entry — Finding #355: operator override of the session stop breaker (⚠️ RE-ENABLE PENDING) + breaker-alert display fix + COMPLETE-STATE readiness rule. Aug-5 morning: 4 of last night's 5 BMO entries stopped out (UBER/CRCL/CVS/KHC, −$83.76; DIS +$17.04 scheduled) → consecutive-loss circuit breaker (08:57:48 ET, 4h) + session stop breaker (4 stops ≥ threshold 4) both tripped; the 10:30 AMC wave was fully skipped (not recoverable). Operator explicitly ordered the block overridden for tonight's 15:58 ET BMO-for-Aug-6 wave: SESSION_STOP_BREAKER_ENABLED=false in prod .env (dated comment, .env.bak-355) + trader restart; the circuit breaker self-expired at 12:57:48 ET. ⚠️ FOLLOW-UP REQUIRED: re-enable the session stop breaker after tonight's session (remove the .env override) — it is a gauntlet-validated defense (#312/#335.b) and must not stay off silently. Also: breaker alert showed "4h pause / pause_hours: 0" (trigger path passes resume_at, alert rendered pause_hours or 0) → hours now derived from resume_at (+1 test); NEW memory.md "COMPLETE-STATE CHECKS" section after the agent answered "ready to trade" from connectivity alone while both brakes were active (operator directive: "always check everything! no shortcuts!"). Prior entry — Finding #354 SHIPPED: 15:58 ET batch freeze — hard deadlines on ALL IBKR requests + batch-prep deadline. Operator asked "is IBKR connected? why no trades Monday?" — IBKR was healthy all day (live probe; the Sunday-2FA scenario did NOT occur), but the 19:58 UTC batch of 6 BMO signals (PFE/CAT/MRK/FIS/MPC/APTV, ~$3,566) froze on symbol 3: get_quote's ib.qualifyContracts() runs the ib_insync event loop with NO timeout and blocked forever on a half-dead gateway socket. The 120s main-loop watchdog forced a restart at 20:00:03; the re-derived signals hit the (correct) market-hours gate at 16:00:47 ET — zero entries, no orders leaked. Second same-class stall that day (06:06 UTC, gateway daily auto-restart window, stage=run_scheduled_cycles). FIXES: (1) NEW IBKRClient.bounded_request(awaitable, what, timeout_sec) — every ib_insync request-response call runs under util.run(..., timeout=IBKR_REQUEST_TIMEOUT_SEC=10) (asyncio.wait_for; cancels on expiry; verified on prod ib_insync 0.9.86); timeout → None + honest ⏱️ log; callers DEGRADE (skip symbol / abort order), never hang. qualify_contract_bounded() wraps the worst offender. (2) §E sweep — ALL 15 request-response sites bounded: 7 qualifyContracts in client.py (incl. the 4 order paths — unqualifiable contract now ABORTS the order), reqExecutions (async, post-sleep removed), 2 qualifyContracts + 4 sync reqAllOpenOrders in order_executor (stop management + #352 zombie checks); tripwires assert no unbounded site returns. (3) NEW BATCH_PREP_DEADLINE_SEC=45: execute_batch_entries' prep loop stops when the deadline expires and SUBMITS WHATEVER IS PREPARED — partial entry beats zero entry; worst case per-request timeouts keep 6 symbols under it. (4) get_account_info's connection guard passed the literal "place_market_order" (copy-paste) — routine account polls during disconnects logged as blocked ORDER attempts and misled this incident's review; fixed + tripwired. DELIBERATELY unchanged: the 120s watchdog (true backstop now) and the market-hours gate that refused the post-restart retry (16:00:47 is after the close — the fix is to never lose the window, not to enter after it). 14 NEW tests (test_finding_354_ibkr_request_timeouts.py — bounded_request paths, get_quote skip-on-timeout = the literal Aug-3 site, batch-deadline matrix against the REAL execute_batch_entries with a fake clock, §E tripwires); 929 green; changelog #354. Prior entry — Finding #353 SHIPPED: History-tab Error Log window + error-ledger backfill + SBUX #349 reclassification + twin repair + 5 doc corrections (all operator-approved; accounting/display only, NO strategy-behavior change). A research pass inventoried ALL past operational error losses; operator decisions: backfill V #83 (−$8.08) + XOM #89 (−$2.84) into trading_errors; MOVE the SBUX #349 born-dead trade out of strategy stats into the ledger (−$0.78 net = −$0.06 gross + $0.7169 commissions — the documented −$0.67 was wrong); add UNQUANTIFIED placeholders (realized_loss 0.00) for the ON #178 recovery legs + PFE #109 unwind legs (never in the DB; quantifiable from IBKR Flex on demand); KEEP all false-detection exits + conviction-bug entries counted; LEAVE the VZ +$32.64 reconciliation gain where it is. Ledger now 6 rows, −$248.79 all-time. Mechanics: NEW error:% exclusion class in TRADE_METRICS_FILTER_SQL/_T (+ the strategy-analysis script's twin filter + the 2 frozen-JSON deep-dive scripts, §M); SBUX relabeled error:born_dead_schedule_349 in BOTH tables (originals preserved in the audit note). Verified stats delta: 162 → 161 closed trades, net P&L +$114.77 → +$115.55 (+$0.78), gross +$373.82 → +$373.88 (+$0.06). Twin repair: trading_positions id 45 (SBUX Jan-28) said reconciliation:not_in_ibkr/$0.00 while trade_audit_log id 11 (the stats source) says stop_loss_software_backup/−$15.07 — id 45 aligned to the audit row (STAYS counted; stats unchanged). UI: get_trade_history() now fetches the ROW-LEVEL ledger in ONE query (totals derived in Python; #352 single-FROM trading_errors tripwire green) → NEW collapsed "⚠ Error Log" <details> section on the History tab (between P&L Summary and the Progression chart; Jinja-only, all-time, fail-soft). Doc corrections: the "stats derive from trading_positions" wording was WRONG everywhere it appeared (7 surfaces — FINDINGS #352, migration 030, app.py, dashboard tooltip, tripwire comment, CONTEXT, this file): stats derive from trade_audit_log via TRADE_METRICS_FILTER_SQL; the invariant is "no stats code reads trading_errors". Also corrected: FINDINGS #178 "recorded in DB" claim (ON legs never were); #179B HOOD −11.2% was the unrealized trough (realized −$5.26 = −3.0% design). NO strategy_changelog entry (accounting/display change, not strategy behavior — noted in FINDINGS #353). 8 NEW tests (test_finding_353_error_log.py); 915 green (was 907). Prior entry — Finding #352.b SHIPPED: the #352 dedup guard made MARKETABILITY-AWARE (operator caught the flaw the same day #352 shipped). Operator asked "are we risking not being able to exit if SL hits outside regular hours?" — hardware stops are fine (STP-LMT outsideRth=True GTC), but the instant-exit falling-market scenario was NOT: pre-market miss exit → stop cancelled → SELL LIMIT at quote−1% → price keeps falling BELOW the limit → the limit sits above the market unfilled, nothing protects the position (stop deliberately not restored per #352), and the #352 guard — a dumb counter — deferred to that stale limit FOREVER, including in RTH where it no-op'd both the re-fired instant exit AND the 10:32 ET scheduled exit (pre-#352 the accidental "guarantee" was the queued MARKET filling at the open; #352 removed it without a deliberate replacement). FIX: NEW _working_exit_orders_marketable(symbol, side, tolerance_pct) (+ _get_working_exit_order_details carrying limit prices) — SELL exit LIMIT counts only if limit_price ≤ bid×(1+tol), BUY exit if ≥ ask×(1−tol), tol = NEW Config EXIT_ORDER_MARKETABILITY_TOL_PCT (default 0.002, env-tunable); quote unavailable → FAIL-SAFE defer (never churn blind); working MARKET in RTH inherently marketable, found OUTSIDE RTH (must not exist post-#352) → cancelled belt-and-braces with honest CRITICAL log. Both guards (place_instant_exit + place_scheduled_exit): marketable → defer as before (exit_orders_already_working, anti-stacking preserved); STALE → _cancel_working_exit_orders + PROCEED (fresh marketable limit at the current quote / place_guaranteed_fill's RTH limit→market ladder). Outside RTH this is a bounded ~5-min limit chase (#141 re-arm): each cycle cancels-and-replaces ONE order, never stacking; all #352 invariants intact (never MARKET outside RTH, ≤1 working order, cancel-before-restore, no stop beside the working limit — the chase is what makes that safe). 17 NEW tests (test_finding_352b_exit_marketability.py: rule matrix + tolerance edge + quote fail-safe + the operator scenario end-to-end + marketable-defer + queued-MKT-outside-RTH cancel); 2 #352 guard tests pinned marketable; 907 green (was 890); changelog #352.b. Prior entry — Finding #352 SHIPPED: VIRT −77 NAKED SHORT (worst operational loss to date, −$237.09) + working-order-aware exit engine + error-loss ledger. Operator spotted an untracked VIRT SHORT 77 @ $54.46 on the live IBKR panel. Chain (all verified): the (correct) VIRT pre-market instant exit at 10:46Z placed SELL 11 LIMIT → IBKR Error 399 "will not be placed until 09:30 ET" (a WARNING — the order QUEUES, it does not die) → 5s timeout placed SELL 11 MARKET (also queues; IBKR MKT never fills extended-hours) → verification saw 11 shares → "restore stop" but left both queued sells alive → #141's 5-min stale-clear re-armed → 8 cycles, ~15 queued sells (#199's duplicate check is POSITION-based, blind to working orders) → one filled pre-market 11:01:50 ($55.60, recorded; DB closed; trader forgot VIRT) → 13:30:00.0Z: the seven surviving MARKET orders filled in the same second @ $54.49 → naked SHORT −77, no stop, invisible to accounting → reconciler screamed CRITICAL every 5 min from 13:34 but only the operator's eyeball caught it → flattened on operator instruction 15:47Z (BUY 77 MKT @ $57.53, clientId 87, verified flat, zero working orders). Error loss −$237.09 recorded in the NEW trading_errors ledger (migration 030) — visible on the History tab "⚠ Error Losses (excluded from stats)" card, EXCLUDED from P&L/win-rate/ROI/PF/DD by construction (corrected #353: all stats derive from trade_audit_log via TRADE_METRICS_FILTER_SQL — not trading_positions as originally written; the holding invariant is "no stats code reads trading_errors", and the tripwire asserts app.py has exactly one trading_errors read). The legit VIRT LONG (−$26.73, correct miss exit per #351) stays a normal trade. FIXES (4 prevention + 1 detection): (1) working-order dedup — NEW _get_working_exit_orders (ACTIVE non-stop exit-side orders via openTrades); both exit paths refuse to stack (exit_orders_already_working, unverified → exit_manager keeps position open + waits), top up only the uncovered difference; (2) entry clamp: exit size ≤ actual signed IBKR position; already-flat → NO order + zombie exit orders AND stops cancelled; (3) NO MARKET outside RTH (extends #90C to extended hours; BOTH fallback sites) — the marketable GTC LIMIT stays working as THE exit, stop deliberately NOT restored beside it (double-exposure); (4) cancel-before-restore on RTH verification failure; (5) NEW integrity rule consistency:external_ibkr_position (HIGH, 54th rule; reads ibkr_snapshot portfolio vs DB open rows, skips when snapshot stale). Auto-flatten of external positions considered and REJECTED (reconciler false positive would close real positions) — documented in FINDINGS. Deploy hook generalized to a migrations list (029+030). Lineage honestly recorded: #58/#83 (entry dups) → #199 (GPRO exit dups, direction-aware but position-based) → #352 (working-order-aware — the piece every prior defense lacked). 17 NEW tests (test_finding_352_exit_order_stacking.py, incl. the literal 7×11 stack + both extended-hours sites); 890 green; changelog #352. Prior entry — Finding #351 SHIPPED: Hyatt false "earnings miss" exit — sparse-market crowd-witness fix + consensus-fallback fix + §F repair. Operator asked whether the two Jul-30 BMO "Instant Exit (Earnings Miss)" rows (H, VIRT) were correct. VIRT: CORRECT (Virtu pre-announced Jul-14 and delivered exactly in-line $1.82 Normalized Adjusted EPS vs $1.82 estimate → the market's beat criterion resolves NO; crowd pinned 0.0005 sustained; #341 contra path exited by design). H: FALSE DETECTION — Hyatt BEAT ($1.12 adjusted vs $0.91; crowd pinned 0.9995 from 11:04:45Z) but PRNewswire parsed the GAAP $0.38 from Hyatt's dual-format headline ("Diluted EPS was $0.38 and Adjusted Diluted EPS was $1.12") → false MISS claim, the VZ/#264 GAAP-vs-adjusted class. The #342 arbiter refused the immediate exit and started its 300s persistence clock, but its crowd witness (_live_polymarket_median) took the median of the last 5 prints REGARDLESS of time spread — on H's sparse market (~11 snapshots/hr vs VIRT's 712) the ONE fresh 0.9995 was outvoted by four 23-40-min-old pre-release 0.77-0.795 prints → "neutral 0.79" → the veto that was factually earned 3 minutes before the clock expired never fired → false MISS admitted at 11:07:47, exit filled 11:09:56 (−$18.63; H sold the news anyway, so P&L impact vs the scheduled exit is likely small — the poisoned labels were the real damage). SECONDARY BUG in the same log: exit_manager/orchestrator periodic checks pass the in-memory position's consensus_eps, which loads 0.0 when the DB row was NULL (open-persist never writes it) → _determine_outcome($0.38, $0)=BEAT flip-flop ("beat admitted (informational)" vs the monitor loop's MISS) — harmless for LONGs but fabricates contra BEAT claims for SHORTs. FIXES: (1) NEW pure select_fresh_median — crowd median counts only prints ≤CONTENT_CROWD_MAX_AGE_SEC (900s), requires ≥CONTENT_CROWD_MIN_FRESH_PRINTS (NEW knob, default 3); (2) NEW _live_gamma_yes_price fallback when the DB stream is too sparse (the #325 bake-off's best source; ACCEPTS resolved 0/1 prints unlike the #333 conviction path — resolution IS the signal here); with these, H would have been VETOED at 11:04:45; (3) check_position_for_exit recovers zero/None consensus from the monitored registry (#339-recovered); (4) §F repair (both tables, #344 taxonomy): H rows → instant_exit:false_detection, outcome beat, actual_eps 1.12, prediction flags corrected, originals in audit note; earnings_markets.actual_beat was never touched (#342 held) and the #344 exit_label_vs_resolution rule would have self-detected on resolution. Deliberately NOT changed: _check_polymarket_price's own median (its sparse-market failure mode is a MISSED exit — fail-safe; widening without replay evidence = #330-class risk). 12 NEW tests (test_finding_351_sparse_crowd_witness.py, incl. the literal H print pattern); 873 green; changelog #351 (execution). Prior entry — Finding #350 SHIPPED: Polygon plan-downgrade incident + #348 root-cause correction. The Polygon/Massive subscription silently dropped to the FREE tier (5 req/min) between Jul-27 10:07 and Jul-28 ~10:00 UTC: etfstack-signal.service hard-failed (exit 3 — SPY 429 after 3 retries; its (2,5,12)s ≈ 19s backoff never escaped the 60s quota window) and the whole account throttled (371 resolver + 52 equity-bars 429s on Jul-29). Empirical proof: 10-request probe = exactly 5× 200 then 429 "upgrade your subscription". #348's root cause was CORRECTED — the schedule collision was real but latent-and-harmless for months (Jul-26/27: zero 429s); the plan downgrade was the trigger (correction note in FINDINGS #348). Operator restored the subscription (verified 12/12 rapid 200s). Hardening shipped (operator points 2+3): (1) NEW src/utils/polygon_rate_limiter.py — cross-process flock'd token bucket (POLYGON_MAX_RPS default 10 combined, POLYGON_BURST 10, fail-open ALWAYS) wired into all scheduled/live consumers: counterfactual_pricing shared fetch (both resolvers + equity-bars + compute), etfstack data_provider (guarded import, subtree stays standalone), resolver financials, dashboard trade-chart ×2, caching.py mcap, weekly strategy-analysis; manual one-off scripts + dormant resolution_sniper deliberately NOT wired. (2) Plan-limit 429 bodies (classify_quota_downgrade) write logs/polygon_quota_downgrade.json; NEW integrity rule consistency:polygon_plan_downgrade (HIGH, 53rd rule) alarms BY NAME; flag self-clears on next successful request. (3) etfstack backoff (2,5,12)→(15,30,65)s — escapes an exhausted minute. (4) etfstack live run_daily now passes allow_etf_yfinance_fallback=True (operator point 3): signal continuity > single-source purity on the paper account; WARNING-logged, whole-series fallback, no splicing. NEW tests/unit/test_finding_350_polygon_limiter.py (21 tests: bucket math, exact-prod-body classifier, flag round-trip, integrity evaluator, tripwires incl. wired-consumers list). Jul-29 etfstack signal row is missing (release passed) — the Jul-30 21:03 UTC run generates fresh; paper account held risk_off regardless. Prior entry — Finding #349 SHIPPED: earnings-date consensus verification + admission guards (SBUX instant-exit war fix). SBUX was entered Jul-28 19:58:11 UTC and closed 20 SECONDS later labeled "Scheduled Exit" (−$0.67 — corrected #353: −$0.06 gross / −$0.78 net; since #353 an error-ledger entry, not a strategy trade): the Feb-9 single-source verifier (NASDAQ, which was RIGHT: Jul-29) and the hourly Polymarket-derived sync (WRONG: Jul-28, from the market's own end_time/slug) fought over earnings_markets.earnings_date for two days — the verifier corrected Jul-28→Jul-29 FOUR times (incl. 19:50:02 Jul-28, seconds before the entry wave), the sync clobbered it back every hour. The trade was SELECTED via the corrected Jul-29 date but SCHEDULED from the wrong Jul-28 end_time (_get_signals_for_timing derived earnings_dt from end_time) → scheduled exit 10:32 ET Jul-28 = 5.5h in the PAST at entry → exit manager sold on its next tick; the intended Jul-29 AMC trade was missed. Fixes (Layer 1 root cause + Layer 2 guards, all shipped together): (1) multi-source consensus verifier (NEW earnings_date_verifier.py + rewritten verify_earnings_dates): DB row + NASDAQ + Finnhub (+ yfinance tiebreak) each get one vote; unique ≥2-vote winner ⇒ date_confidence='confirmed' (+ timing corrected on consensus, amending #220); no majority ⇒ 'disputed'; single-source auto-correct REMOVED. (2) Confirmed dates LOCKED against all 3 Polymarket-derived writers (fetcher/scraper/earnings_sync CASE guards — this ends the war). (3) NEW earnings_date_audit + 2 integrity rules: consistency:earnings_dates_disputed (WARN) + consistency:earnings_date_churn (HIGH — ≥3 changes/7d = writer fighting the verifier). (4) Layer-2 admission guards in signal_generator: disputed rows skip (earnings_date_disputed); unconfirmed date-vs-end_time mismatch skips (earnings_schedule_mismatch); confirmed mismatch trades on the VERIFIED day; entry-time invariant refuses any signal whose computed exit is <30 min in the future (scheduled_exit_not_future). (5) Dashboard ⚠ DATE? badge on disputed rows (Jinja + mobile JS + API, §M). (6) Migration 029 (date_confidence, date_verified_at, audit table); §F: SBUX corrected to Jul-29 AMC confirmed per official IR. 26 NEW tests (test_finding_349_earnings_date_war.py; 840 green — 5 older fixtures updated whose stubbed "exit=now" the new invariant correctly refuses). Prior entry — Finding #348 SHIPPED: Polygon 429-storm fix. equity-bars collector failed (30/35 jobs 429'd, first failure since ≥Jun 1): its fixed 10:00 UTC slot collided with the daily resolver's 06:00 ET (= 10:00 UTC in EDT) on the shared Polygon key at 20 req/s each; no retry/backoff existed; 429s were mislabeled "likely delisted/OTC". Fixes (operator-approved Tier A): (1) fetch_polygon_minute_bars retries 429/5xx with 2s/8s/30s backoff + NEW raise_on_transient/PolygonTransientError (§M — all consumers inherit; legacy default unchanged); (2) collector classifies honestly (delisted only on clean-200-zero-bars; transient= counted/reported separately; >10% threshold still exits 2); (3) timer 10:00 → 11:35 UTC; (4) --polygon-sleep 0.05 → 0.25 in equity-bars + both resolver units. 5 NEW tests (815 total green); rerun backfilled the 30 missed jobs; integrity green. Tier B (cross-process token-bucket limiter) explicitly deferred unless more Polygon consumers are added. Same day — Finding #347 SHIPPED: 150-trade TESTING_PROD auto-pause removed (operator passed 150 DB trades and chose to continue; the pause was latent anyway — in-memory counter reset on every restart; guard test added). Jul 22 — Finding #346: BMO-reservation override threshold was 0.90 on the LIVE orchestrator path (#333 lockstep missed the twin) → 0.95 + twin tripwire test. Jul 22 — Finding #345: backup service wedged 7 days (cleanup ran AFTER pg_dump → disk-full death spiral, 27 GB stale local backups on a 48 GB disk) → cleanup-first + 2-day local retention; Spaces off-site remains operator-choice-absent. Prior entry — Finding #344 SHIPPED: honest exit labels — operator-caught discipline failure in the #342 repair. The #342 §F repair deliberately kept PNC's instant_exit:earnings_miss label ("historical fact of why we exited" + audit note) — WRONG call: notes never enter SQL aggregations, so every "how many PM predictions missed" query would count the parse-bug exit as a real miss, violating the #328 (KR) honest-labels precedent; operator: "we also dealt with this before and you did not fix it... no shortcuts ever." Full §E audit of ALL 13 outcome-labeled exits, each cross-checked against resolver actual_beat AND the market's own RESOLVED price (last snapshot ≥12h post-earnings — actual_beat itself was detector-writable pre-#342) AND external EPS where ambiguous: (1) 5 FALSE-DETECTION exits relabeled instant_exit:false_detection in BOTH tables with audit notes preserving the original label — ON Feb-10 (PRNewswire $0.06 vs $0.64, #264 class), ZM Feb-25 (beat-exit 10:59 ET = hours PRE-release AMC), IONQ Feb-25 (miss-exit 15:51 ET pre-release; IONQ actually beat −$0.20 vs −$0.48 adj), VZ Apr-27 (#264 Yahoo GAAP), PNC Jul-15 (#342 dividend parse); 6 genuine outcome exits confirmed unchanged (TXN/COP/AMZN/MAR/SNOW/KR). (2) 2 poisoned actual_beat corrected to the market's RESOLVED outcome (the market's criterion is what we trade): AXP Jan-30 True→False (resolved NO 0.0005; resolver's EPS-tie 3.53>=3.53 heuristic said beat), XOM Jan-30 False→True (resolved YES 0.9995; GAAP 1.54-vs-1.69 said miss). (3) Prediction flags corrected on 6 rows in both tables (ON/ZM/IONQ/VZ → correct; AXP/XOM → wrong) — resolver Pass 4 only fills NULL flags, stale values never self-heal. (4) NEW self-detecting integrity rule consistency:exit_label_vs_resolution (HIGH): FAILS whenever an earnings_miss/beat exit label contradicts the market's resolution, naming offenders — the class no longer waits for an operator to eyeball a history row. Taxonomy: instant_exit:false_detection renders "Instant Exit (False Detection)" via the existing generic branch; excluded from %stop% and %earnings_miss% predicates, still counted as an instant exit (honest). 4 NEW tests (807 green); rule verified OK on prod post-repair. Prior entry — Finding #343 SHIPPED: intraday outcome-resolver cadence + display-only crowd read (completes #342's single-writer model). Operator caught JNJ/CTAS beats (PM pinned 0.9995, aligned with our LONGs) showing NO dashboard badge. Root cause (3 stacked): (1) #341 silenced same-side PM detections BY DESIGN — they were the position-row badge feed AND the actual_beat poison class; (2) PRNewswire chronically timing out + Yahoo slow → no content detection either; (3) the EPS resolver — #342's SOLE actual_beat writer — ran once daily at 06:00 ET, BEFORE same-day BMO releases (Jul-15: ran 06:01, JNJ released ~06:45) → same-day outcomes settled ~24h late. Fix A: NEW polymarket-resolver-intraday.{service,timer} light pass (--earnings-limit 50 --skipped-limit 0 --opportunity-limit 0 --lookback-days 3, 10-min timeout) on 06..10:15,45:00 + 16..18:15,45:00 America/New_York (:15/:45 offsets clear the 06:00 daily full pass which remains the completeness backstop; calendar strings systemd-analyze-validated on prod; wired into deploy_from_github.sh). Fix B: positions API computes pm_outcome_display/pm_outcome_yes_price for open positions with NULL earnings_outcome via the SAME _compute_outcome_status helper the Upcoming table uses (§M single definition — fresh ≤12h post-release PM extreme ≥0.99/≤0.01); rendered in Jinja desktop + JS mobile as "📊 BEAT/MISS · PM" with an explicit crowd-read-not-settled-truth tooltip. NOT done: re-enabling same-side detection writes (would reopen #337/#341 corruption). Side note: JNJ stopped out 9:03 ET (−$15.22, stop_loss:hardware) on a correct BEAT — sell-the-news, same as JPM Jul-14; flags correct. Prior entry — Finding #342 SHIPPED: UNIFIED EXIT ARBITER — no detection source may exit a position uncorroborated, settle actual_beat, or stop monitoring. Trigger: PNC beat clearly ($4.81/$4.85 adj vs $4.41) but PRNewswire's EPS parser extracted the DIVIDEND ("Increased quarterly common stock dividend... to $2.00 per share") as EPS → $2.00 vs $4.41 = false MISS → instant exit of a CORRECT LONG at 6:34:31 ET (−$11.14, ~$14-15 vs holding; PNC pre-market ~$253 an hour later) while the live crowd median sat at 0.91 BEAT — then wrote actual_beat=False and stopped monitoring. Root cause is the AUTHORITY MODEL: check_earnings was first-to-detect-wins, giving whichever source spoke first unilateral power (same defect as ON $0.06-parse, VZ #264 GAAP scrape, JPM #337 pre-release pin, WFC #341 pins — one bug in five costumes). Operator directive: "single integrated mechanism... bulletproof," whole package in one. SHIPPED: (1) _arbitrate_content_claim — every content-source (PR/BW/Yahoo) CONTRA claim is cross-examined against the live PM crowd median (_live_polymarket_median, median-of-5, fresh ≤900s): agrees (≤0.25/≥0.90) → admit immediately (two witnesses, FASTER than the 5-min PM-only path); neutral/stale → 2nd independent source or 300s persistence (CONTENT_EXIT_CORROBORATION_SEC) required; contradicts (≥0.90 vs MISS claim / ≤0.10 vs BEAT) → ARBITER VETO + persistence-clock reset (a recurring bad parse can never time in). Same-side claims informational. Feb-10 _confirmed_polymarket sticky cache + #264 (a)/(b) shields replaced by the arbiter. PM source unchanged from #341. (2) NO detection signal writes earnings_markets.actual_beat — removed from BOTH writers (trading_orchestrator + position_manager _persist_earnings_detection, §E); the EPS resolver (#227) is the sole settler. (3) Monitoring is immortal to signals: _check_all_positions never removes; callbacks dedup per outcome (last_reported_outcome, survives the reconciler's 5-min re-registrations + restarts via already_reported_outcome seed) and RE-FIRE on flips; BOTH reconciler registration sites now register even when an outcome exists (closes the #337 restart gap that left JPM unprotected). (4) _extract_eps_from_text rebuilt 2-tier: number-first "$4.81 Diluted EPS" parses; bare "$X per share" excluded within 80 chars of dividend/buyback/repurchase/book-value/offering context; negatives (−$0.25 / ($0.12)) parse. 4 new Config knobs. NEW tests/unit/test_finding_342_exit_arbiter.py (22 tests: PNC/VZ/ON incident replays, arbiter matrix, monitoring lifecycle, parser corpus, SET-actual_beat source tripwire); 2 #341 tests re-pinned; 803 unit tests green. §F: PNC rows repaired (actual_beat False→True, actual_eps 2.00→4.81, outcome miss→beat, prediction flags corrected; exit_reason string kept as historical fact + note). Monitor: first live ARBITER VETO/WAIT/admitted lines; actual_beat stays NULL until the resolver runs. Prior entry — Finding #341 SHIPPED: Polymarket price detection becomes EXIT-ONLY (contra-position signals, 5-min sustained confirmation + 60s grace, never settles the record). The pre-deploy acceptance replay of #339 (scripts/detection_latency_replay.py --truth market --positions, all 13 resolved markets with ≥200 prints incl. the WFC/JPM day) found: (a) 30s confirmation lets 0.1-4 min post-release whipsaws (IMAX/OKTA class) exit CORRECT positions — 3 false exits; (b) same-side "resolution" pins are NOT truth — WFC pinned ≥0.98 for ~1.5h before flipping to its real MISS, 5 more markets did the same; under the pre-#341 design those settled actual_beat + STOPPED monitoring (the #337 poisoning class) from signals that exit nothing. Shipped: _check_polymarket_price emits ONLY contra-position signals (LONG→MISS side / SHORT→BEAT side, both grades; nothing without a direction — live callers all pass it); every PM signal requires 300s SUSTAINED consensus (POLYMARKET_DECISION_CONFIRMATION_SEC=300) with 60s grace for brief median pops (POLYMARKET_CONFIRMATION_GRACE_SEC=60 — without grace, print noise kept resetting the clock and median save latency ballooned 22m→73m); PM signals carry EarningsResult.grade ('resolution'/'decision'), never write earnings_markets.actual_beat (orchestrator gates on source — EPS resolver #227 owns ground truth), never remove the position from monitoring (the instant-exit flow does that on close), and never arm _confirmed_polymarket — the PR/BW/Yahoo contradiction shield now lazily uses _live_polymarket_extreme_outcome (self-correcting after a crowd flip). Replay scorecard: saves 2→4, false exits 3→2 (both remaining had the crowd decisively wrong 5+ sustained minutes — now cost only an early exit, not corrupted truth), median save latency +22m from crowd flip (vs +104m pre-#339). 19 detector tests re-pinned to contra-only semantics (4 NEW: same-side pin suppressed / no-direction emits nothing / grace clock / PM keeps monitoring); 781 unit tests green. Prior entry — Finding #340 SHIPPED: #339 blast-radius cleanup + structural prevention. The 2 failing integrity rules (conviction_at_exit_post_fix 6/23 NULL, hold_duration_post_fix 3/23) were the #263-era detection net firing exactly as designed on #339's fallback-path closes (5× Jul-14 + VZ Apr-27). Fixes: (1) §F data repair — conviction_at_exit backfilled point-in-time on all 6 rows (median ±30min of exit; last-60min-pre-exit median for WFC/GS/BAC whose PM markets closed at 9:00 ET before their exits: VZ 0.9995 / JPM 0.8400 / C 0.9995 / WFC 0.9870 / GS 0.9400 / BAC 0.9490); (2) fallback close-persist WIDENED — now carries exit_commission, total_commission (numeric-cast), conviction_at_exit, hold_duration_hours, prediction_correct (previously dropped despite being in-scope; only margin-ledger aggregates remain primary-only); (3) NEW scripts/close_persist_contract_check.py + deploy hook — runs the REAL PositionManager close-persist end-to-end against prod PG on every deploy (synthetic __SQLCHK__ row, fractional commission = the #339 22P02 trigger shape, asserts primary-path success via reconciliation_status, deletes residue) so the #263/#339 class of PG-type-resolution bugs is caught at deploy time instead of on the first live close. NEW test_position_manager_fallback.py (3 tests, 795 pass). Prior entry — Finding #339 SHIPPED: WFC miss-detection lag (48 min → stop bleed) + Jul-14 fee-capture loss — 3 compounding bugs fixed. WFC: release ~7:00 ET, PM collapsed 0.99→~0.12 at 8:39 ET (decisive MISS) but NO detection → hardware stop 9:27 (-$20.58, ~$7-10 avoidable). Bugs: (1) #336's all-of-5-prints unsatisfiable on real mixed bid/ask/trade streams → decision statistic now MEDIAN of last 5; (2) resolution thresholds 0.98/0.02 unreachable on a dying book → NEW decision-grade thresholds post-typical-release (BMO 7:00 ET / AMC 16:05 ET; median ≤0.25 MISS / ≥0.90 BEAT; env-tunable; #337 gate + 30s confirmation still in front; same-direction signals benign for held positions); (3) consensus_eps silently $0.00 on 4/5 positions (yfinance calendar failed, no DB fallback; _determine_outcome(actual, 0) = Yahoo could never exit a LONG) → add_monitored_position now falls back to earnings_markets.eps_estimate. FEES: COALESCE(:param, 0) bare-0 typed the param INTEGER → first fractional Tiered commission ($0.3555) crashed the primary close persist (22P02) → fallback path dropped commissions/gross_pnl/hold_duration; fixed with 0::numeric + CAST in BOTH writers (position_manager + orchestrator reconciliation, §E); §F backfill of JPM/WFC rows from logged commissionReport values (0.3555/0.3652). JPM was NOT a bug: real BEAT detected 6:56 ET, prediction correct, 7:07 stop = sell-the-news (by design). PRNewswire RSS chronic timeouts noted, not actioned. 6 NEW incident-replay tests. Prior entry — Finding #338 SHIPPED: canonical chronological Strategy Changelog (operator-directed). Operator: July's changes were scattered / "extremely difficult to follow chronologically" / "use a standardized format... also in the future". NEW src/earnings/strategy_changelog.py = single source of truth (26 entries launch→#337; schema: date/finding/category/title/change/why/expected_impact/verification/monitor/status/superseded_by/marker_label; supersession chains encoded) → feeds the NEW Strategy-tab 📜 Strategy Evolution timeline (top of tab, newest-first, month-grouped, status filters) AND app.py's History-chart markers (hardcoded list replaced by get_history_chart_markers(), verified byte-identical to the prior 7 markers incl. the Jul-6 merged-label convention). Enforced by NEW tests/unit/test_strategy_changelog.py (9 tests incl. content-depth minimums + marker continuity) + NEW memory.md §O (mandatory same-commit entry for every strategy change) + STRATEGY_ANALYSIS_ARCHITECTURE.md §0.14. Also fixed in-audit: stale phase-status banner (Last shipped: May 28 → points at the changelog). Prior entry — Finding #337 SHIPPED: release-window gate on the price-based earnings detector (JPM false-BEAT incident). JPM's Polymarket book spiked to 0.997 for ~3 minutes at 8:26 PM ET Jul-13 (~10.5h before its BMO release; 6,974 prints ≥0.98 overnight) — sustained enough to LEGITIMATELY pass #336's all-of-5-prints check + the 30s confirmation → detector confirmed BEAT, wrote earnings_markets.actual_beat=True + trading_positions.earnings_outcome='beat', stopped monitoring JPM; after 2 overnight trader crashes (asyncio "cannot enter context" on IBKR reconnect ~04:14Z — pre-existing, needs own RCA, systemd auto-recovered) the reconciler re-registered only outcome-less positions → JPM left WITHOUT instant-exit protection into its real release. FIX: _release_window_open gates _check_polymarket_price — BMO window opens 4:00 AM ET on the earnings date (preserves KR 6:52 AM early-release class), AMC 3:30 PM ET, derived from earnings_markets (cached 10 min), FAIL-OPEN on gaps; content-based sources (PR/BW/Yahoo) deliberately ungated. Neither signal.earnings_datetime (BMO = midnight-UTC = 8 PM ET prior evening — would NOT have blocked this) nor MonitoredPosition.expected_release_time (reconciler degrades it to now() on restart) was usable as the gate. DB §F-corrected (actual_beat NULL + position detection fields NULL, 1+1 rows, audit event kept) → JPM re-registers on the deploy restart. Flagged not actioned: asyncio reconnect crash RCA; reconciler's expected_release_time=now(); consensus_eps=$0.00 at registration. 4 NEW tests (779 pass). Prior entry — Finding #336 SHIPPED: false pre-event "BEAT - PM" outcome badges + earnings-detector single-print hardening (eve of the Jul-14 bank wave). Operator caught JPM/GS badged BEAT the night before earnings while conviction showed 93-94%. Root cause: price_snapshots thin-book noise (JPM 621/11,137 prints ≥0.99 in 12h, ask-side prints interleaved with 0.75 bid-side in the same second) + both _compute_outcome_status (#280) and the trader detector's PM reads (#264) trusting the SINGLE latest print. Entry path unaffected (no outcome input; median conviction robust) — the banks trade normally tonight. Latent trader risk closed: a noise-sustained false BEAT would have poisoned _confirmed_polymarket (blocks real MISS detection) and PERSISTED wrong earnings_markets.actual_beat via _persist_earnings_detection. Fixes: dashboard PM-extreme branch gated on _is_past_release_time; _check_polymarket_price + _live_polymarket_extreme_outcome now require ALL of the last POLYMARKET_EXTREME_MIN_PRINTS=5 prints at the extreme (env-tunable; 30s confirmation window unchanged on top). NEW tests/unit/test_outcome_status.py + 4 detector tests incl. the exact JPM print pattern (774 pass). Data-quality note flagged, not actioned: the snapshot stream mixes bid/ask/trade prints within the same second. Prior entry — Finding #335 SHIPPED: busy-week capital bundle (operator-directed, ahead of the Jul-14 bank-earnings wave — busiest week on record expected: 7-14 entries vs ~2/wk avg). (1) Exposure cap MAX_DAILY_EXPOSURE 1.40→1.50: flat-25% sizing (#314.f) fit only 5 concurrent positions (125%) under 1.40 — 6th always rejected at 150%>140%, stranding ~$406/night; 1.50 fits exactly 6 (targeted granularity fix; #315's declined 2.0 raise was a different, broad question; margin-safe vs enforced 2.0× Reg-T gate ≈$5.4k). (2) ADMISSION freed-capital fix: can_open_position(capital_freed_from_exits=) nets out about-to-exit positions (10:32 BMO exits before 10:34 AMC entry; 15:57 AMC exits before 15:58 BMO entry) — pre-fix a full overnight book auto-rejected EVERY next-morning AMC signal (Jan-28-2026 fix covered batch SIZING only, not admission; would have silently killed Wed UAL / Thu NFLX after 5-position nights). Wired: orchestrator check_amc (300s helper) + check_bmo (600s helper) + capital_manager lockstep twins. (3) BMO-reservation admission re-based: get_actual_available_capital returned RAW IBKR day-trade buying power (~4×net_liq ≈$110k on $2.7k trading capital) minus reservation → never bound; now returns the strategy budget (MAX_DAILY_EXPOSURE × total_capital − effective exposure, freed-aware) bounded above by BP; dashboard available_capital now shows ~$4k not $110k. Also: build_phase3_sim_capacity cap resolved from Config at build time (was hardcoded 1.40); replay_exposure_cap_sweep DEFAULT_CAPS += 1.50 with LIVE marker; stale 30/22/15 + "70% exposure" + 2/3/4%-stop docstrings in risk_manager corrected; STRATEGY.md ×10; Action Queue row busy-week-capital-335; tests re-pinned + NEW TestBusyWeekCapitalFixesFinding335 (764 pass). SESSION BREAKER (#312) — threshold RAISED 3→4 (#335.b, operator choice): counterfactual on all 9 historical ≥3-stop days — only 4 would-be-blocked entries, net −$15.78 (breaker SAVED money; 7/9 days the 3rd stop fired post-20:00Z blocking nothing); agent recommended keep-as-is, operator chose 4 for busy-week correlated-stop tolerance (SESSION_STOP_BREAKER_THRESHOLD=4; skip-reason string session_3_stop_breaker kept for analytics continuity; only 4/9 historical breaker days reached 4+ stops). NOTE: TESTING_PROD 150-trade pause counter is in-memory only (resets each restart; currently 0/150) — never fires as documented; flagged, not fixed. Prior entry — Finding #334 SHIPPED: priority re-tiered 0-10 for the reopened 0.90-0.95 band (operator-directed; completes #333, which had MISSED priority.py — a 0.92 LONG would have passed the relaxed config gate but scored 0 and been silently dropped at priority_filter). New base table: Large 70-80/80-90/90-95 = 8/7/6 · Mid = 5/4/3 · Small 70-80 = 2 (90-95 LOWEST per mcap group; small caps stay 70-80 LONGs only; SHORT miss-conf ≥90% stays SKIP) + #313 volume bump → 0-10 scale (MAX_PRIORITY_SCORE=10, new LONG_MAX_CONVICTION=0.95). All consumers audited (orchestrator /6 + JS mobile /6 leftovers fixed); dashboard legend rebuilt (+2 NEW rows, all /8→/10, stale "LONG 90%+"/"70-89%" texts fixed); STRATEGY.md §3 rewrite; Action Queue + FINDINGS + PLAYBOOK + CONTEXT; tests re-pinned + new #334 band test. §M lesson: the LONG cap is encoded in BOTH Config.SIGNAL_LONG_MAX_THRESHOLD AND priority.py — change together. Prior entry — Finding #333 part 2 SHIPPED: #202 LONG cap RELAXED ≥0.90 → ≥0.95 (operator-confirmed outright relax). SIGNAL_LONG_MAX_THRESHOLD default + prod .env → 0.95; §M lockstep: is_high_conviction, simulator DirectionPolicySpec (prob_threshold_70_30_with_95_skip), capital_manager._bmo_reservation_threshold 0.90→0.95 (would otherwise have been silently overridden by every reopened-band signal; this threshold IS the mechanism behind the "AMC needs 90%+ if BMO tomorrow" rule), dashboard app.py+Jinja+JS gating (badges ✗95+/70-94%; JS stale $2k fallback → $1k per #311), Live Config Direction row, History P&L marker, STRATEGY.md ×8, Action Queue rows (relax-202-9095 IMPLEMENTED), 2 guard tests (741 pass). ≥0.95 skip REMAINS. Monitor: first ~20 real fills in the 0.90-0.95 band vs +$6.19/trade counterfactual; re-gauntlet on REAL fills at trade #150 or 20 band-fills. Prior entry — Finding #333 exclusion-funnel coverage leaks FIXED + gap-aware 0.90-0.95 band replay (relax decision PENDING-OPERATOR). Leak fixes shipped: (1) get_earnings_markets end_time::date OR-window with ±1-day guard vs earnings_date (PXLW channel-window hole — stale earnings dates made markets invisible to both channels); (2) NEW ConvictionCalculator._get_conviction_from_gamma_live last-resort fallback, ≤24h-target-gated to keep point-in-time replays look-ahead-free (GME Jun-9 $22.8k conviction_unavailable class); (3) timing PRIORITY 4.5 — infer BMO/AMC from market end_time hour ≤14Z/≥20Z before the web fallback (GD/UBSI timing_unknown class). NEW scripts/analyze_90_95_bucket.py (gap-aware stops per #330 + Fixed/Tiered commissions + flat-25% sizing): 0.90-0.95 band n=78 → +$482.94 net, ex-top-2 +$135.39 PASSES pooled (sub-slices outlier-carried — large/mid/pre-May fail ex-top-2; ~2/3 of convictions pre-date the #325/#327 estimator fixes; no slippage; instant-exit rescue unmodeled = conservative); 0.95+ n=34 → −$6, ex-top-2 −$228 → KEEP skipping. Action Queue row relax-202-9095 = PENDING-OPERATOR (relax cap to 0.95 / half-size 12.5% pilot until ~20 real fills / keep + re-run at trade #150). 8 NEW tests, 741 pass. OPEN operator decisions: relax-202-9095 + #314.e SKIP rules at 30+ trades. Prior entry — Finding #314.f SHIPPED: FLAT 25/25/25 sizing (operator-confirmed real-money change; was 30/22/15 per #211) — basis +$7.40/wk gauntlet-PASS ×2 at the live 1.40 cap; lockstep config + simulator rules.py (jul6_flat25 + production_strategy_jul6()) + dashboard + STRATEGY.md + memory.md + 3 guard tests; monitor small-cap stop-outs (25% in the 5%-stop tier) + re-validate at trade #150+. Same day — Finding #332 Tiered-commission alignment audit + History-tab P&L upgrade. Operator switched IBKR Fixed → Tiered (change processes by Friday per IBKR); no-shortcuts audit of the commission chain fixed: exit-commission INT-ROUNDING before persist (would corrupt Tiered legs $0.42→$0 — the critical one), $0.00-commission falsy checks in client.py, gross_pnl writer inconsistency (+97-row DB correction; realized_pnl IS gross price P&L). Verified aligned: fill-summed commissionReport capture (partial-fill-correct), #329 Flex weekly reconcile, fee_calculator (already models Tiered), #331 net displays, PROJECTIONS $1/side (frozen benchmark). History tab: NEW Gross P&L + Commissions Paid cards + launch-to-date cumulative gross/NET progression chart (LightweightCharts) with strategy-change markers (app.py:STRATEGY_CHANGE_MARKERS, sync with PLAYBOOK §2) + per-rule-era performance table. OPEN operator decisions: #314.f sizing-up variant (flat 25/25/25 +$7.40/wk vs 35/25/18 +$6.08/wk vs KEEP — both gauntlet-PASS). Prior entry — Finding #330 RuleB FAILED-GAUNTLET: the simulator's window arm was gap-fill OPTIMISTIC → gap-aware fix flips RuleB from +$224/100 to −$209/100; Phase-2 active flip CANCELLED (NO live trading change). Operator-directed full strategy review found simulate_window_only filled window-blocked stops at the STOP PRICE even when the price was still through the stop at rearm — unobtainable under the as-shipped #305 cancel-and-rearm mechanism (re-placed STP-LMT triggers instantly, fills near market; = FA_hard's rearm_gap_open, the arm that retracted FA_hard in #228). Gap-aware fix (first post-window bar OPEN fill; signature unchanged so the phase3_simulator reference-adapter contract holds; RuleG inherits) + prod re-run n=129: RuleB −$209/100 headline / −$194/100 ex-top-2 / −$227/100 ex-repeat — FAIL every §K check (optimism bias ≈ $535); RuleG −$30/100. Live corroboration: all 4 June in-window stops (LULU/ADBE/ACN/KR) were CORRECT stops — RuleB-active would have worsened each (ACN −$26 actual vs ~−$134 held). NO stop-loss rule variant currently survives the gauntlet. Consequences: Action Queue Sim RuleB → FAILED-GAUNTLET; v3 D/E RuleB dependencies REMOVED (pure selection rules at trade #150+ / SHORT n≥30); #323.b "0.92-0.99 conditional on RuleB" lever dead; May-28 PR-1 live_baseline_gauntlet PASS/SHIP marked SUPERSEDED. Phase-1 dry-run stays deployed (logging only) + its log spam fixed (8,026 duplicate lines → once per position). 6 NEW unit tests. Same review also produced the ranked improvement list: (1) idle-cash yield on the $25k PDT reserve — operator: reserve becomes trading capital post-testing, NO action; (2) IBKR Fixed→Tiered commission plan (~55% of gross P&L currently goes to commissions at test-size positions; 106/127 legs = the $1 Fixed minimum) — informational, operator decides; (4) capacity-aware sizing re-run for the parked #314.c candidates — DONE (Finding #314.f: flat 25/25/25 Δ+$7.40/wk + 35/25/18 Δ+$6.08/wk BOTH gauntlet-PASS at the live 1.40 cap on 578 signals/39wks incl. June; STRENGTHENED vs May 31; Action Queue row sizing-up-2026-07 = PENDING-OPERATOR-APPROVAL — historical: resolved Jul 6, 2026, #314.f shipped flat 25/25/25; later E-001 contractual PASS with S2c as decided successor per #360); (5) LONG 0.85-0.90 skip candidate (n=31, −$92 net, ex-top-2 −$210 §K-robust) — WAIT for fresh post-#327 data per operator; (6) P&L display fix (net-of-commissions) — SHIPPED same session as Finding #331 (all Total P&L displays net via shared PNL_EFFECTIVE_SQL definition; Strategy-tab analysis stays gross with a NET companion line; labels/tooltips renamed). Remaining open items from the review: Fixed→Tiered commission plan (operator decides, account setting), idle-cash yield (operator declined — reserve becomes trading capital post-testing), LONG ≥0.85 skip candidate (WAIT for fresh post-#327 data), sizing-up ship decision (#314.f PENDING-OPERATOR-APPROVAL — historical: resolved Jul 6, 2026, flat 25/25/25 shipped). Audit-process lesson #7 (STRATEGY_AUDIT_v3 §16): a TRUE simulator must model the REARM path of any cancel-and-rearm rule, not just the silence. Prior entry — Finding #329 Commission-capture completeness + IBKR Flex ground-truth reconcile (data-integrity + ops). Follow-up to the P&L accounting RCA. The live commissionReport path (ibkr_executions source=live/live_stop_fill, 205 rows) IS ground truth for most trades, but the IBKR_FLEX_TOKEN had EXPIRED (err 1012) — Flex ran once at setup (Jan 7-29) then lapsed and was never scheduled. NULL-commission trades (31 exit + 5 entry) = early no-exec trades + commission-less recovered fills (log_backfill/manual_recovery). statement_csv rows store commission NEGATIVE (sign convention; 2 XOM dupes) → backfill uses ABS(). Fixes: (1) backfilled the 7 recoverable NULL sides (+$7) + recomputed total_commission (staleness $206.37→$229.44); (2) refreshed Flex token (→2027-06-06) + new Activity Flex Query id 1563851 (Trades, Last 365 Calendar Days, XML); (3) NEW weekly FAIL-SOFT polymarket-flex-reconcile.{service,timer} (Sun 13:30 UTC) + scripts/run_flex_reconcile.sh → pulls official IBKR statement, gap-fills exact-charged commissions (ABS), never cry-wolfs (#297/#326). Full-history backfill PENDING IBKR new-query propagation (transient 1001); detached retry probe + weekly timer complete it. P&L DISPLAY fix (Total P&L omits commissions) is separate + pending operator go-ahead. Prior entry — Finding #328 KR exit MISLABELED "Scheduled Exit" → crash + honest reconciliation labels + durable crash logs FIXED (PRODUCTION FIX). Operator: KR recorded as a Scheduled Exit but exited 6:52 AM ET (scheduled 10:32) — RCA whether scheduled or stop. It was NEITHER: KR reported a pre-market earnings MISS → the detector fired an instant_exit:earnings_miss (SELL 1122823 filled 9@$60.02, 10:52:23 UTC, −$16.83); 13 s later the trader crashed persisting the close → DB never updated → orphaned → on restart reconciliation recovered the fill but blanket-labeled it scheduled_exit:reconciled. Bug 1 (root cause = the recurring exit 1): _persist_position_close_with_exit_details used self.position_manager; the real attribute is self.position_mgr → AttributeError on EVERY instant-exit persist (fixed). Why CI missed it: test_close_path_routing built its mock with the same wrong name position_manager, passing against the bug → corrected to position_mgr (now guards it). Bug 2: order_executor.py imported get_engine from non-existent src.db.database → src.db.connection. Bug 3 (honest labels): BOTH reconcilers (orchestrator _reconcile_positions_with_ibkr + reconciliation_manager, §E) now timing-aware — fill >5 min before scheduled_exit_time → instant_exit:reconciled, else scheduled_exit:reconciled (added scheduled_exit_time to both reconcile queries; dashboard already renders instant_exit:*; stop-rate %stop% analyses unaffected). Bug 4 (durable logs): tracebacks WERE captured but buried in 362 MB of ib_insync INFO → quieted ib_insync→WARNING + added sys/threading excepthooks (full flushed traceback for any uncaught main/thread exception) + fatal handler now logger.exception. DB corrected: KR trade_audit_log id=137 + trading_positions (§F) → instant_exit:earnings_miss, exit_time → 10:52:23 (actual fill), note appended; P&L/exit price were already correct. 8 unit tests pass; committed + deployed + §G. Prior entry — Finding #327 a #325 REGRESSION fixed (PRODUCTION FIX — trader conviction path). Operator asked to check the open KR position's entry conviction + ensure the calculator works. KR logged 0.885 (traded Mid LONG 80-90%) but EVERY source at entry = 0.92 (→ #202 ≥0.90 skip). Root cause: #325 raised the SHARED PRICE_SNAPSHOT_ROBUST_LIMIT 9→10000, which broke the no-time-window _get_latest_from_price_snapshots "latest conviction" path (live BMO/AMC entries via _get_latest_conviction): limit 10000 medianed over the market's WHOLE history (KR 4021 snapshots, 0.50-0.95 → 0.885) instead of recent. Affected 2 of 3 post-#325 trades — KR + ACN (both Jun 17, both should've been #202-skipped); SWBI (time-windowed path) fine. Fix: window the latest path to the last 60 min of available data (anchored to max(time)). Verified prod: KR 0.885→0.92, ACN 0.895→0.905, both → #202 SKIP. KR + ACN positions OPEN (operator decision per ADBE #325). 3 unit tests pass. Prior entry — Finding #326 api-health cry-wolf FIXED (ops/integrity; NO trading change). Operator flagged systemd_last_run:polymarket-api-health.service failing >1 day. Ran clean Jun 3-12, first failed Jun 13 08:03 UTC; ONLY the Polymarket website check errored ("Could not find buildId") — Gamma/CLOB/data-api/price-accuracy all HEALTHY (ZERO trading impact). Root cause: Polymarket migrated Next.js Pages Router → App Router (buildId + /_next/data gone; data now inline via self.__next_f RSC). No self-recovery because: persistent upstream change + daily timer no-retry + an INFORMATIONAL-ONLY check escalating to exit(1) (cry-wolf, #297). Fix (operator A+C): rewrote check_polymarket_website_api App-Router-aware (GET /finance, verify finance-markets+dehydratedState markers) + demoted all website failures to 'warning' (exit 0 per #297); removed unused re. Verified healthy (finance_markets=151), deployed, service re-run → integrity green. Prior entry — Finding #325 Conviction under-sampling bug FIXED — production trader conviction path (committed + deployed). Operator flagged an open ADBE LONG the dashboard marked SKIP (97.5% ≥90% → #202). The trader's ConvictionCalculator price-snapshot fallback computed 0.789 vs true ~0.95 because (1) the 0.02<yes_price<0.99 filter (3 sites) deleted the genuine high-conviction prints (ADBE's most-common reading 0.999) and (2) PRICE_SNAPSHOT_ROBUST_LIMIT=9 sampled 9 of ~1,700-per-±5min → noise-dominated median. 0.789 fell in the 70-90% band → #202 never fired. The dashboard was correct via a SEPARATE live-Gamma latest-yes_price path (app.py:2074). Scope audit (127 trades): ≥2 #202 violations — ADBE (open) + LULU (−$25.54, stopped); SNOW direction-INVERTED. Bake-off (408 markets vs actual_beat): Gamma is the BEST source (78% acc), not faulty — the estimator was. Fix (conviction_calculator.py, 5 edits): drop filter (0<v<1), LIMIT 9→10000 so the median spans the full ±30min window, reliability gate min-max RANGE>0.35 → robust IQR>0.25 (recover the real conviction, do NOT skip flickering markets — operator constraint). Before/after (378 markets): is_reliable-skips 24→19, 0 new tradeable-band skips. §G prod probe caught a fix-to-the-fix: first deploy used LIMIT 600 → ADBE still 0.863 (600-closest collapses to ~±2min on the high-freq Gamma stream); raised to 10000 → ADBE 0.912, reliable → #202 SKIP, is_reliable count (1) + ≥0.90 band membership (27/82) identical at 600 vs 10000 (surgical). 3 unit tests pass (+1 new). Refines #197; interacts with #283/#319 gate. ADBE held per operator; dashboard display path left as-is (correct). NEW scripts/conviction_source_bakeoff.py. Prior entry — Finding #323.b RuleB re-test AMENDS #323 — the LONG ≥0.90 skip (#202) flips POSITIVE under RuleB (offline; NO production change). Operator's insight confirmed: ~62-67% of stop-hits fire in the ±90min earnings window (vol-driven, conviction-independent), so #323's baseline-stop "keep ≥0.90" was confounded. NEW scripts/phase3_simulator/replay_deadzone_ruleb.py (3 regimes via canonical analyze_finding_a_full sims): LONG ≥0.90 under RuleB = +$853 / ex-top-2 +$614 / 47.7% win (n=65), RuleB > no-stop > baseline. Dead-zone 0.60-0.70 LONG ~breakeven even unstopped (weak edge, KEEP); small-cap/short KEEP. Action Queue conviction-band-revalidation. ⚠️ CORRECTED (replay_202_ruleb.py): the ≥0.90+RuleB "flip" does NOT hold on the real #202 trades — the 26 REAL closed ≥0.90 LONG trades (real fills) are ACTUAL −$112/15.4% WR → RuleB −$88/15.4% WR → NO-STOP −$154; RuleB can't fix the 85%-wrong direction. The +$853 was post-#202 SKIPPED signals (counterfactual, later period, ~$33/26 bar-pricing optimism). #202 HOLDS even with RuleB; Action Queue row back to KEPT; ≥0.90-relax lead downgraded. RuleB's value (Phase 2, #305) is the vol-spike-stop cases generally, independent of ≥0.90. Prior entry — Finding #323 conviction-band exclusion re-validation → ALL HOLD + Finding #324 counterfactual-% units cleanup (offline/dashboard; NO production change). #323: operator asked if the bot excludes profitable trades; comprehensive §K-gauntlet check (stop-aware opp_log counterfactual + universe proxy, de-duped + ex-top-2 + real per-tier scale + per-mcap) → raising the ≥0.90 cap, trading the 0.30-0.70 dead zone (0.60-0.70-as-LONG = −$54.65/FAIL once stops modeled, the #228 trap), and loosening small-caps/shorts ALL fail — production bands UNCHANGED; KEPT row in Action Queue. NEW scripts/phase3_simulator/replay_deadzone.py (+ 3 analysis scripts). #324: standardized counterfactual P&L on % (canonical hypothetical_pnl_pct, PERCENT); fixed a 100× "Avg %" dashboard display bug + honestly labeled the $10k-notional dollars; flagged a separate suspected regime/VIX realized_pnl_pct fraction-display bug for follow-up. Prior entry — Finding #322.f Pre-Monday trading-readiness audit → GO (test-only fixes; NO prod behavior change). After the week's #311-#322.e changes, full systems+code audit per memory.md before Monday markets. Live prod ALL GREEN: config effective values all match ground truth; 6/6 services active; integrity 47/47 OK; IBKR connected net_liq $27,548, 0 open positions, no breakers, 0 trader errors; prod HEAD==origin/master. Full unit suite caught 7 stale-test failures (verified NOT prod bugs per INFERENCE DISCIPLINE): 5× from #322.b scale 245→550, 1× exposure test stale vs #317 uniform-140%, 1× pre-existing dashboard cache flake — all fixed (test-only) → 726 passed. ⏭️ REMINDER (Sat Jun 6, 2026): review RuleB dry-run logs (RuleB[dry_run] in trader journal / Strategy Analysis tab) → decide Phase-2 RULEB_MODE=active flip (#322.e). Prior entry — Finding #322.e RuleB Phase 1 DRY-RUN actually ENABLED in prod (logging only; NO live order change) — pre-flight for an operator "push RuleB to prod" request found RuleB had NEVER run (RULEB_ENABLED default False + no prod .env override → main-loop hook never fired, 0 log lines in 7d; the dashboard "IMPLEMENTED-DRY-RUN" was code-only). Operator chose dry-run-first + tracked config default: RULEB_ENABLED False→True, RULEB_MODE stays dry_run (cancel/rearm only LOGGED, no IBKR side effects). NOT the Phase-2 active flip (separate confirmed ship after reviewing dry-run logs; known edge cases: restored-position earnings_dt, clock-skew on cancel→rearm). py_compile + 68 tests pass; deployed (trader restart). Phase 2 (RULEB_MODE=active) pending operator review of dry-run logs. Prior entry — Finding #322.d Phase 5: ROI re-baseline (analysis/dashboard/docs; NO trading change) — completes the final roadmap item. Re-baselines strategy ROI as a 3-epoch ladder (frozen pre-launch PROJECTIONS → frozen 87-trade → current ~117-trade + simulator) + net-new metrics (5-KPI, capacity lift, §K outlier dependence) + a hybrid forward projection (live vs sim side-by-side). Caution-first (operator choice): leads with the outlier alarm (top-5 +$427 vs ex-top-5 −$205 = the whole realized edge is ~5 outliers), tiny/frozen sample (117 trades, drought since ~Mar 2026), and live +$12.32/wk < sim +$18.11/wk; forward ~+25.6%/+37.7% on $2,500 shown but LOW CONFIDENCE. NEW PROJECTIONS_87TRADE + build_roi_rebaseline() (reuses KPI/capacity; no recompute) → report roi_rebaseline; NEW dashboard "📊 ROI Re-baseline" + renderRoiRebaseline(); NEW docs/FINANCIAL_PROJECTIONS_2026_05.md (old 87-trade doc frozen as Epoch 2). py_compile + jinja + node + builder unit-test clean; deployed (report regen + dashboard restart). Phase-3 strategy-simulator project COMPLETE end-to-end (PR 1-5 + Phase 4 + Phase 5). Prior entry — Finding #322.c Audit follow-ups (dashboard + config; NO trading behavior change): wired the dashboard risk panel via the trader's ibkr_snapshot.json risk_state (read-only serialization of risk_manager.get_risk_state(); single source of truth, no §M duplication); added a data-api.polymarket.com dependency probe + System-tab row (#289); confirmed MAX_POSITION_SIZE_PCT vestigial (no consumer; Large sizes 30% unclamped) → re-aligned 0.15→0.30 + documented, mirroring #315. py_compile + test_config (0.30) + test_risk_manager 27 PASS. Deployed; trader restarted (operator-CONFIRMED, Sunday) → snapshot now carries live risk_state, reconnected clean (net_liq $27,548.36). Prior entry — 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 fixed (docs + dashboard render code; NO trading change). Operator caught #322 as a shortcut — it claimed "every word and line" but only covered dashboard.html static HTML, skipping the Python that generates the dashboard (strategy_performance_analysis.py 4,815 LoC, app.py 6,252 LoC) + all 26 docs/*.md. The complete re-do: universe gate $2K→$1K (SQL+labels+sweep), P&L→+$212/~117, cockpit capital/sizing $2,450/10%/$245→$2,500/22%/$550, PR2-banner→RESOLVED, priority 0-6→0-8, dead watchdog removed, max_trades 100→150, position-sizing→30/22/15% by mcap; 18 docs synced (Phase 3 PR1-5 + Phase 4 COMPLETE; service topology; equity-bars 47k→1.9M; rules→~47; watchdog→integrity-monitor; stops/sizing/exposure; $2k→$1k; IBKR Xvfb:99/IBC; FINANCIAL_PROJECTIONS superseded banner). History preserved; no numbers fabricated. py_compile + jinja clean; deployed. Prior entry — Finding #322 Full dashboard staleness audit (dashboard-only; NO trading change)) = operator-directed exhaustive line-by-line audit of every dashboard tab (Trading/System/Database/Findings/History/Strategy + header) vs current production config; ~30 stale static facts fixed. Trading: priority 0-6→0-8 (legend/badges/clamps/labels, #313), volume gate $2,000→$1,000 (tooltip + 3 fallbacks, #311), stops STOP-LIMIT, AMC exit comment 3:58→3:57. System: exposure "rare 60%/140%"→uniform 140% (#317), backfill 4→6-pass, "Phase 0/1/2"→Phase 0-4 COMPLETE, long-weekend 75%→50% (matches live signal_generator.py:1364), 2FA "VNC"→IBKR app push. Strategy: gate-retune "current $2,000"→$1,000 post-#311 + cockpit sizing exposure→uniform + #316 panel "superseded by #317". Database +#307 citation; History 5-outlier net "$159"→+$212. Verified node --check (both <script> blocks) + jinja parse + 0 mojibake + global residual-token sweep clean. Frontend-only (dashboard.html); deployed (dashboard restart). Also earlier this session — Finding #321 follow-up SHIPPED: self-healing counterfactual retry in outcome_resolver (daily timer reprices transient price_data_unavailable gaps once bars exist; bounded by bars-exist gate + 3-day cooldown). Earlier — Finding #321 Strategy-analysis data completeness: Polygon backfill + full simulator re-validation (data backfill; NO code/strategy change)) = operator directive to ensure COMPLETE Polygon-sourced data before trusting the simulator. Audit found 472 PAST counterfactual-P&L gaps in polymarket_opportunity_log (price_data_unavailable rows whose equity_bars_1m already existed — original fetch failed transiently + compute_counterfactual_pnl.py never retries computed rows) + ~48 earnings_markets outcome/return gaps. Backfilled via compute_counterfactual_pnl.py (counterfactual P&L 1,288→1,755 priced; 30 remain = 25 future + 5 delisted) + outcome_resolver.py --force (stock_return 99.8% of past markets; actual_beat 948/961, 13 awaiting EPS publication). Validation batch repriced 20/20 via Polygon first. Full re-run on COMPLETE data — every conclusion HOLDS: capacity #314.b $1k gate KEEP +$18.11/wk (was +$16.40); regime #318 KEEP compound_v1 (identical); depth #319 KEEP gate (identical, 22 binding kills, net +$22.54). Backfill VALIDATED the shipped KEEP decisions rather than overturning them. Key insight: the _CF_SQL simulator universe is filter-limited (volume≥$1k drops 713 + conv≥0.90/small-cap drops 369 → 590 raw → 441 post-dedup), NOT data-limited. Corrected #320's Universe View label (regenerates on every report build via analyze_universe_hit_rate, not a 4h manual job). NO code/strategy change (DB backfill via existing resolver + compute script). Follow-up flagged: add a periodic retry of price_data_unavailable rows whose bars now exist. Earlier same day — Finding #320 Phase 4 (remainder): Unified Simulator dashboard consolidation (dashboard-only; NO trading change)) = completes the Phase-3-dependent half of Phase 4 (un-gated now Phase 3 PR 1-5 is done). NEW 🧪 Unified Simulator umbrella on the Strategy tab (#unified-simulator-section) framing ONE canonical engine over its arms: live stop-loss (Finding-A, relabeled), capacity-aware (Phase 3 Capacity-Aware Sweeps + Gate Retune), selection-rule research (static regime #318 + depth #319 KEEP cards, #phase3-selection-research-section), universe signal-quality (Universe View, folded + honest Apr-25-baseline label). Archived Q3-2025 Backtest Scenario Comparison + BMO/AMC Timing Matrix off the tab (producers+JSON retained; dispatcher calls removed). Consolidated 5 overlapping breakdowns → 2 canonical (removed Live Performance Snapshot; kept Direction+Timing Matrix + Performance Breakdowns). Operator AskQuestion: unify-and-preserve (not delete-all-3) + static regime/depth cards + include breakdowns. Frontend-only; verified node --check (both <script> blocks) + Jinja parse + div balance preserved + removed-DOM-ids only referenced by 3 now-dead (never-called) render fns (renderScorecard/renderBacktestTable/renderCohortTimingTable — documented dead, safe to delete in follow-up). Deployed (dashboard restart only; NO trader restart). Completes Phase 3 + Phase 4 (history at May 31 ship: Phase 5 ROI re-baseline was still outstanding; Phase 5 SHIPPED same day — Finding #322.d). Earlier same day — Finding #319 PR 5: Depth-gate v2 widened reconstruction — break-even low-information-signal filter → KEEP (offline-only; closes Finding #283; Phase 3 simulator PR 1-5 COMPLETE)) = NEW scripts/phase3_simulator/depth_gate_variants.py (pure decomposition + remove_all / carveout_real_trade / spread_gate_only / depth_gate_only variants) + replay_depth_gate_v2.py (replays the REAL ConvictionCalculator.get_conviction_at_entry_time point-in-time over the full 915-market universe → 22 BINDING depth kills [gate fired AND clears volume + #202/#207 skips], recovers raw orderbook + reliability_reason, prices counterfactual equity P&L via run_counterfactual_for_row). Operator's question "has the gate ever saved us from a bad trade?" → YES: 13 losers BLOCKED (−$142.56), 9 winners KILLED (+$165.09), net removal +$22.54 over ~7 months (≈ +$1/trade = noise). Kill cause 18/22 snapshot_reliability (hollow book → sparse/unstable price-snapshot conviction), 1 spread, 3 no-orderbook → low-information-signal filter, not the pure spread gate #283 implied. The n=4 "only costs us / blocked 0 losers" was a small-sample mirage; widening (operator-requested) reversed it. Variants: remove_all +$22.54 / carveout +$22.78 (≈ identical) → adds variance for ~$0 EV. Operator-confirmed KEEP; production conviction_calculator/signal_generator depth check UNCHANGED. 395 tests PASS (+20). NO PRODUCTION CHANGE, NO deploy. This was the last Phase 3 deferred-bundle item — Phase 3 strategy-simulator (PR 1-5) is COMPLETE. Earlier same day — Finding #318 PR 4: Regime classifier v2 sweep — faster detectors add no robust ROI → KEEP compound_v1 (offline-only; closes Finding #278)) = NEW scripts/phase3_simulator/regime_classifiers.py (pure-Python point-in-time SpyDailyHistory prior-completed-bar lookup + 6 classifiers: compound_v1 baseline + compound_or_dd5 [#278 option B] + below_ema20 + below_ma50 + dd_3pct + compound_symmetric; VIX deferred to Phase 4) + replay_regime_v2.py (re-classify regime point-in-time → re-apply Rule #1 [#304] → recompute P&L via run_sizing_sweep/apply_sizing_gauntlet vs compound_v1; sizing held at production so credited == raw realized P&L). Operator AskQuestion: classifier set = equity family of 6; universe = BOTH (live-only primary + capacity-aware 1.40 cross-check); point-in-time = prior-close (agent); gauntlet-floor = full-pop spillover primary (agent). Prod result (568 signals = 127 live + 441 counterfactual): below_ema20 (fastest; skips ~40% of LONGs vs compound's 17%) gains +$70.31 PASS on live-only but −$17.02 FAIL-EX-TOP-2 on the capacity-aware universe at the live 1.40 cap — the live-only win is selection luck concentrated in actually-traded outcomes (whipsaws out winners on the broader universe; same reversal as #315 "don't look in isolation"). compound_or_dd5 + below_ma50 inert; dd_3pct + compound_symmetric FAIL. Only effect surviving both universes = better worst-week tail (5p −$98.60→−$57.08) at ~breakeven P&L (downside trade-off, not a money-maker). Methodology validated: PIT compound_v1 reproduces the live recorded regime 95.3%. Operator-confirmed KEEP compound_v1; production src/earnings/market_regime_provider.py UNCHANGED. 375 tests PASS (+22 new). NO PRODUCTION CHANGE, NO deploy (offline analysis). Earlier same day — Finding #314.e PR 3.b.3: 4 deferred SKIP-rule candidates — real-scaled + bias gauntlet (ANALYSIS-ONLY, ship deferred to operator) = NEW scripts/phase3_simulator/replay_skip_rules.py re-evaluates the 4 Finding #313 deferred SKIP cells REAL-SCALED (not hypothetical $10k-notional) + capacity-aware. Real-scaling corrected 2 cells (Large+SHORT+$2k-$5k only n=3 not 12; Large+LONG[.80,.90)+$1k-$2k net-POSITIVE +$0.93 avg, hyp said −$20). Gauntlet: skip Large+LONG+≥$10k +$2.04/wk PASS; skip Mid+LONG+$1k-$2k +$1.12/wk PASS; skip Large+SHORT+$2k-$5k FAIL (n=3); skip Large+LONG+$1k-$2k −$0.82/wk FAIL (cell positive); combined skip-all-4 +$5.42/wk PASS (maxDD −$220, WR 39.1%). Operator ship-bar=C analysis-only → NOTHING shipped; per-candidate ship pending operator. 313/313 tests PASS (+10). NO PRODUCTION CHANGE. Earlier same day — Finding #314.d PR 3.b.5 SHIPPED: Hybrid seasonal gate variant — KEEP uniform $1k) = evaluated operator's "$2k peak-weeks / $1k off-peak" gate. Verified the academic peak-week classifier tracks PM data first (peak windows hold 57.7% of 977 markets / 60% of vol≥$1k). NEW scripts/phase3_simulator/replay_gate_hybrid.py (hybrid = uniform $1k gate + peak-week sub-$2k skip; reuses run_sizing_sweep). Prod run (568 signals): hybrid removes 144 peak-week $1k-$2k signals → +$8.08/wk vs uniform $1k +$16.86/wk (Δ −$8.78/wk, FAIL-SPILLOVER), nearly reverting to uniform $2k (+$6.70/wk). Operator premise "$1k is noise in peak weeks" REFUTED (those markets are net-positive per #314.b; removing them gives back most of #311's lift; cap-reallocation doesn't help — cap-skips 58→5 but freed slots not refilled). Honest note: pre-build diagnostic under-counted footprint (31 via cumulative volume vs 144 in simulator). DECISION: KEEP uniform $1k (#311). 303/303 tests PASS (+7). NO PRODUCTION CHANGE. Earlier same day — Finding #314.c PR 3.b.4 SHIPPED: Sizing variants sweep — KEEP 30/22/15) = first sweep on the size-aware capital simulator. simulate_one_variant() gained scale_pnl_with_size flag (default OFF → #314.b + SKIP sweeps byte-identical; design doc §12 PR3) + realized_pnl_credited on AdmissionDecision; NEW gauntlet.apply_sizing_gauntlet() + NEW scripts/phase3_simulator/replay_sizing_variants.py (8 variants, per-period + per-bucket + gauntlet vs baseline + Pareto). Prod run (568 signals / 36 wk / $2.5k × 2.0 = $5k max gross): operator's "lower sizing → more positions → more P&L" hypothesis REFUTED — lowering sizing admits more (20/15/10 → 437 vs baseline 409; capacity-skips 58→30) but total P&L DROPS −$6.80/wk (FAIL-SPILLOVER — marginal capacity-freed trades are low-EV; shrinking good positions costs more). Only positive-Δ variants (flat 25/25/25 +$1.41/wk, 35/25/18 +$0.75/wk) FAIL the gauntlet (single-name / outlier dominated) + worsen maxDD/5p. DECISION: KEEP production 30/22/15 (Finding #211 unchanged). 296/296 phase3_simulator PASS (+14). NO PRODUCTION BEHAVIOR CHANGE. Earlier (May 30 LATE LATE evening — Finding #314.b PR 3.b.2 SHIPPED: Capacity-aware re-validation of Finding #311) = NEW scripts/phase3_simulator/replay_capacity_aware.py (~410 lines) — first sweep on capital simulator. Prod run on 568-signal universe (127 live + 441 counterfactual, 36 weeks): post-#311 ($1k gate) admits 409 / total +$607 / +$16.86/wk; pre-#311 ($2k gate) admits 295 / total +$241 / +$6.70/wk; Δ +$10.16/wk (HALF the optimistic +$21/wk Phase γ+δ projection — capacity contention is the gap: 58 capacity-rejects at $1k vs 5 at $2k). $1k-$2k admitted bucket POSITIVE (n=132, avg +$3.20, total +$423). max_capital_used=$4,949 (99% of cap; binding). DECISION: KEEP $1k gate. Earlier same day (LATE LATE evening) — Finding #314 PR 3.b.1 SHIPPED: Capital simulator foundation) = NEW scripts/phase3_simulator/capital_simulator.py (~365 lines, all offline-only): CapitalSimSignal (live+counterfactual) + SizingVariant (mcap_pct + extra_skip_predicates + gate_volume_min) + AdmissionDecision + CapacitySimResult + simulate_one_variant() per-event heap-based simulator + signal_from_live_trade() + signal_from_counterfactual_market() (latter applies per-tier empirical scaling). 30 NEW unit tests; 282/282 phase3_simulator PASS local. NO PRODUCTION BEHAVIOR CHANGE. PR 3.b.2 NEXT: re-validate Finding #311 ($1k gate) capacity-aware; PR 3.b.3 → 4 deferred SKIP rule candidates with bias gauntlet; PR 3.b.4 → sizing variants sweep. Earlier same day (LATE LATE evening) — Finding #311 Re-validation Note (doc-only): operator-requested re-run; methodology audit caught my PM-volume / IBKR-slippage conflation; live n=10 within 1σ of simulator projection — sampling noise. KEEP $1k gate; revisit after 30+ live trades. Earlier same day (LATE LATE evening) — Finding #313 SHIPPED: PR 3.a — Volume-tier priority bump + capital cap raise + NYSE dead-code cleanup) = src/earnings/priority.py rewrite (scale 0-6 → 0-8, base 0-6 mcap×conv ordering preserved per Mar 9 + new volume bump +1 at $2k +2 at $5k); src/earnings/config.py MAX_TOTAL_EXPOSURE_PCT 1.40 → 2.00 (Reg T overnight ceiling); src/earnings/order_executor.py is_nyse dead branch dropped (post Finding #185 unification); signal_generator.py passes volume_total to priority; dashboard legend + clamp updated. 617/618 unit tests PASS. NEW MAX_PRIORITY_SCORE=8, VOLUME_BUMP_TIER_1_USD=2000, VOLUME_BUMP_TIER_2_USD=5000. Operator follow-up next: re-validate Finding #311 ($1k gate) against new volume data — $1k-$2k bucket was historically -$0.77 avg / WR 30% (n=10) → may need gate adjustment if simulator + new data agree. 4 candidate SKIP rules deferred. Earlier same day (LATE evening) — Finding #312 SHIPPED: v2 #6 Daily 3-stop session breaker + DirectionPolicySpec doc-bug fix = NEW src/earnings/session_stop_breaker.py pure-decision module (~155 lines: SessionBreakerAction enum + SessionBreakerDecision frozen dataclass + decide_session_stop_breaker() pure function + COUNT_STOPS_TODAY_SQL constant). UPDATED src/earnings/signal_generator.py: NEW _count_todays_stops() helper (fail-soft on DB error → 0) + skip block in _build_trading_signal after Queue Rule #1 long_downtrend → returns (None, "session_3_stop_breaker") when ≥ Config.SESSION_STOP_BREAKER_THRESHOLD (default 3) stops fired today (ET-day). UPDATED src/earnings/config.py: 2 new env vars (SESSION_STOP_BREAKER_ENABLED default True, SESSION_STOP_BREAKER_THRESHOLD default 3). Plus production_strategy_may30() direction-policy correction: long_threshold 0.55 → 0.70, short_threshold 0.45 → 0.30, NEW long_max_threshold=0.90, conviction_fn "abs_distance_x2" → "raw_prob". Dashboard Live Config "Direction" + "Selection" rows updated. 79 tests PASS (22+8+38+11). Earlier same day (LATE evening) — Finding #311 SHIPPED: gate_uniform_$1000 production change (first real-money strategy ship since Mar 9, 2026)) = src/earnings/config.py SIGNAL_MIN_VOLUME_TOTAL default lowered $2,000 → $1,000. production_strategy_may30() factory created. Earlier (May 30) — Phase α+β+γ+δ+ζ shipped (10 commits): unified Strategy framework with 9-component composable object + 5-KPI engine + replay-based candidate sweep + Pareto frontier + simulator-driven Top-5 in cockpit. Earlier (May 30 morning) — Phase ε.1-5: Operator Cockpit (4 layers) + Section Index + Universe View staleness banner + auto-regen on deploy. NO INTEGRITY-MONITOR RULE COUNT CHANGE (47/47 preserved). Earlier (May 29 — Finding #310.c shipped: Phase 3 simulator PR 2.c FINAL) = NEW scripts/phase3_simulator/gate_retune_sweep.py (~280 lines) with ShipEligibility / VariantSweepResult / SweepResult frozen dataclasses + evaluate_conservative_ship_eligibility() 3-condition Q3 (C) check + run_gate_retune_sweep_on_universe() deterministic orchestrator + run_gate_retune_sweep() SQL-loading runner. NEW gauntlet.apply_admission_gauntlet(variant_id, admission_results, period_filter) entry point reusing existing 5-check helpers via duck-typed AdmissionResult (added @property symbol + @property entry_time). NEW output.write_sweep_results_json() schema = phase3_sim_gate_retune.v1 (deterministic, sort_keys + trailing newline). Extended runner.py with --run-gate-retune-sweep flag + --sweep-output arg + _run_gate_retune_sweep_mode() (~80 lines). Added strategy_performance_analysis.build_phase3_sim_gate_retune() helper that loads scripts/phase3_simulator_gate_retune_sweep.json + falls back to status='not_run' placeholder. Added new dashboard 🧪 Phase 3 — Gate Retune Sweep section (per design doc §7.4) below the existing 🧪 Phase 3 Simulator Status section: status header (boundary date / universe / eligible-count) + per-variant table (sorted: ship-eligible first, then per-100 desc) + per-variant collapsible details with ship-eligibility reasons + per-period gauntlet table + eligible-variants summary + renderPhase3SimGateRetune() JS function (~150 lines). 25 NEW unit tests across 6 classes (TestAdmissionResultDuckTypeProperties + TestApplyAdmissionGauntlet incl. synthetic H-rule shape + TestConservativeShipEligibility incl. each FAIL path + TestRunGateRetuneSweepOnUniverse + TestWriteSweepResultsJson incl. 0-byte-diff reproducibility + TestSerialization). 250/250 PASS local Python 3.11 (208 phase3_simulator + 42 prior signal-test files); no regressions. NO production behavior change. PR 2 COMPLETE — operator AskQuestion per qualifying variant is the next step. PR 3 (sizing + priority + exposure-cap) begins next session. Earlier (May 29) — Finding #310.b shipped: Phase 3 simulator PR 2.b — counterfactual_admission.py (load opp_log low-volume markets + per-variant admission filter) = NEW scripts/phase3_simulator/counterfactual_admission.py (~230 lines) with CounterfactualMarket frozen dataclass + load_counterfactual_markets() SQL loader + evaluate_admission(variant, market) per-variant filter + evaluate_variant_admissions() per-variant scoring + AdmissionResult dataclass + summary_dict() quick aggregate. §A pre-coding INFERENCE DISCIPLINE catch (6th this session-arc): original PR 2.b design assumed counterfactual replay would re-fetch bars + re-compute stops + re-execute the stop-loss simulator (substantial new code). Pre-coding schema inspection revealed polymarket_opportunity_log already has hypothetical_entry_price + hypothetical_exit_price + hypothetical_stop_hit + hypothetical_pnl + hypothetical_pnl_pct + intended_* fields pre-populated by compute_counterfactual_pnl.py (Finding #308 re-run). Massive scope reduction. Critical filter: USABLE_PRICES_SOURCES=('polygon.io_1m_close',) excludes 278/540 rows with source='none' ($0 placeholder; Polygon fetch failed). Pre-coding evidence: 262 usable counterfactual markets (all in collapse period; max volume $1,953 → no production-gate admission); $1k admits 96, $500 admits 146, $250 admits 181, $100 admits 196, $0 admits 262; aggregate +$3,180 if all admitted (avg +$12.14/trade). Strong signal pending PR 2.c gauntlet vetting. 20 NEW unit tests in 6 classes; 225/225 PASS local Python 3.11. NO production behavior change. PR 2.c FINAL (next): gauntlet per-variant×per-segment + output schema + runner --variant-set + 3-condition conservative-ship check + phase3_sim_gate_retune dashboard section + run on prod + AskQuestion ship decisions. Earlier (May 28-29) — Finding #310 shipped: Seasonality preflight + structural-collapse discovery + PR 2.a declarative scaffolding (gate_variants.py + 11 variants + period segmenter + week-of-month classifier) = OPERATOR-REQUESTED preflight before PR 2 coding REFUTED the pure-seasonality hypothesis (Dec 2025 off-season was healthy at 98.1% gate-pass + $14,522 median volume; Mar+2026 off-season collapsed to 27.8% + $0 median → real STRUCTURAL collapse starting March 2026, NOT seasonality). Operator AskQuestion 3-question pattern expanded PR 2 scope: Q1 = (B) SEGMENTED (3 segments: healthy < 2026-03-01 / collapse / full); Q2 = (B) THRESHOLD + SEASONAL AXIS (11 unique variants = 6 thresholds × 2 modes uniform/seasonal_week with off-peak factor 0.5, dedupe $0/$0); Q3 = (C) SHIP CONSERVATIVE (ship only if gauntlet PASS on full-pop + ≥20 admitted markets in collapse + ex-top-2 stays positive). PR 2 split into 3 sub-commits: PR 2.a SHIPPED (this; declarative scaffolding + 51 unit tests + doc updates; 205/205 PASS local); PR 2.b NEXT (counterfactual_admission.py — synthesize Trade from polymarket_opportunity_log skip_reason='low_volume' rows + delegate to replay_trade); PR 2.c FINAL (gauntlet aggregator extension + output schema + runner --variant-set + 3-condition ship check + phase3_sim_gate_retune dashboard section + run on prod + AskQuestion ship decisions). NO production behavior change. Earlier (May 28 LATE evening): Finding #306.c shipped: Phase 3 simulator gauntlet.py + output.py + runner.py --run-gauntlet (fifth PR-1 ship of the day; THIRD of 3 sub-commits for #306 — PR 1 COMPLETE) = NEW scripts/phase3_simulator/gauntlet.py (5-check bias gauntlet per memory §K: n_adequate + ex_top_2 + ex_repeat_tickers + full_population_spillover + five_outlier; verdict dispatcher in priority order FAIL-N-TOO-LOW → FAIL-EX-TOP-2 → FAIL-EX-REPEAT-TICKERS → FAIL-SPILLOVER → WARN-OUTLIER-DOMINATED → PASS; true_simulator_compliant=True baked in; VariantStats + CheckResult + GauntletResult frozen dataclasses) + NEW scripts/phase3_simulator/output.py (deterministic JSON schema per design doc §7.2: DataSnapshot + RulesetEvaluation + ShipRecommendation + RunOutput; write_results_json with sort_keys + trailing newline; mask_volatile_fields for reproducibility diffs; derive_ship_recommendation verdict→SHIP/DEFER/DROP mapping; get_code_git_sha defensive subprocess wrapper) + UPDATED scripts/phase3_simulator/runner.py (added --run-gauntlet flag for full pipeline + --baseline-rule + --variant-rules + --output + --no-replay-detail + --polygon-sleep; removed "#306.a stops here" footer staleness). 33 NEW tests across 2 new files; 154/154 PASS local Python 3.11 (112 phase3_simulator + 42 prior signal-test regression). §10.3 H-rule self-validation per design doc PR 1 acceptance gate: PASS (synthetic v2 H-rule shape returns FAIL-EX-TOP-2 as required; if it ever flips to PASS, gauntlet is broken — block PR 2). §10.2 reproducibility per design doc PR 1 acceptance gate: PASS (2 runs same input produce byte-identical JSON after masking generated_at). PR 1 IS NOW COMPLETE: foundation (#306.a) + replay+cost-model+cross-check (#306.b) + gauntlet+H-rule+reproducibility+JSON (#306.c) + Finding #273 conditional-label fix (#307) + Bug #6 hyp_pnl re-run (#308) all shipped. All 4 PR 1 acceptance gates pass (§10.1 cross-check PASS per #306.b §G probe = 100% match within $1 on all 7 rules; §10.2 reproducibility; §10.3 H-rule self-validation; Phase 0 §0.6 corrected per #308). NO production behavior change. PR 2 (volume-gate retune sweep, highest-leverage per May-28 PM volume-collapse evidence) can begin next session per design doc §12. Earlier (LATE evening): Finding #306.b shipped: Phase 3 simulator cost_model + replay engine + reference_adapter + cross_check_baseline script (fourth PR-1 ship of the day; second of 3 sub-commits for #306) = NEW scripts/phase3_simulator/cost_model.py (CostEstimate dataclass + estimate_cost with proxy-flat-1pct default per Q2 + latent pm_v2_orderbook path for PR 2+ counterfactual admission) + NEW scripts/phase3_simulator/reference_adapter.py (re-exports fetch_bars/run_trade/simulate_/TradeSim from scripts.analyze_finding_a_full per Q1 HYBRID + _assert_reference_module_stable() signature-drift guard fired at import time with memory.md §M-pointing diagnostic) + NEW scripts/phase3_simulator/replay.py (ReplayContext + ReplayResult dataclasses + replay_trade(trade, ruleset, ctx) delegating to reference's run_trade + replay_population progress-logging loop) + NEW scripts/phase3_simulator/cross_check_baseline.py (operator-runnable script implementing design doc §10.1 acceptance gate: load N trades, run replay_population, INNER JOIN against scripts/finding_a_full.csv, per-rule drift breakdown, PASS if match_pct >= 95% within $1 on every rule, writes JSON evidence file). §A pre-coding INFERENCE DISCIPLINE catch (fifth this session-arc): Trade.scheduled_exit_dt(cal) was MISSING from population.py — the reference's run_trade calls it, so the delegation in #306.b would AttributeError immediately. My #306.a TestReferenceParity only checked dataclass FIELDS (via dataclasses.fields()), not METHODS. Fixed both as part of #306.b: added scheduled_exit_dt to population.py + new TestReferenceParity.test_method_names_match_expected + test_scheduled_exit_dt_consistency_amc tests. Caught BEFORE writing #306.b code. Code shipped: 4 new package files + 1 updated (population.py) + 3 new test files + 2 tests added to test_population.py. 37 NEW tests; 121/121 PASS local Python 3.11 (79 phase3_simulator + 42 prior signal-test regression). NO production behavior change (simulator package remains offline-only). The §10.1 cross-check is now RUNNABLE — execution is part of #306.b §G probe runway (post-deploy on prod, expect verdict PASS within $1 on every rule since Q1 HYBRID delegation calls the same simulate_ functions; any drift would indicate (a) reference CSV stale vs current reference code or (b) Polygon bar cache-vs-fresh divergence). Earlier (LATE evening): Finding #306.a shipped: Phase 3 simulator foundation skeleton (third PR-1 ship of the day; first of 3 sub-commits for #306) = NEW scripts/phase3_simulator/ package skeleton (__init__.py + population.py Trade+load_trades duplicated per Q1 HYBRID + rules.py declarative RuleSet dataclasses + baseline_ruleset factory + runner.py CLI stub) + NEW tests/unit/phase3_simulator/ test scaffold (42 NEW tests: 18 population including 4 CHANGE-CONTRACT cross-check tests vs analyze_finding_a_full.Trade + 24 rules including serialization round-trip). 84/84 PASS (42 new + 42 prior signal-test regression, no regressions). NO production behavior change (offline-only simulator package). 3 operator AskQuestion decisions resolved BEFORE coding: Q1 = (C) HYBRID (duplicate Trade/load_trades, delegate stop-loss simulate_ via future reference_adapter.py in #306.b); Q2 = (A) FLAT 1% proxy spread (lands in #306.b cost_model.py); Q3 = (B) THREE INCREMENTAL MILESTONES (this sub-commit + #306.b cost_model+replay+cross-check + #306.c gauntlet+H-rule self-validation+reproducibility+output+dashboard). Earlier (May 28 LATE evening): Finding #308 shipped: Phase 3 simulator PR 1 second commit (ops-only) = compute_counterfactual_pnl.py re-run on prod (78 truly-pending rows processed; generate_signal coverage 67.5% → 69.1%) + structural-ceiling discovery documented in design doc §3 Gate 4. INFERENCE DISCIPLINE catch #4 this session: the 480 generate_signal + 39 depth rows with NULL hyp_pnl are STRUCTURALLY un-simulatable (all direction_would_be='NONE' = conviction-deadband; no direction means no counterfactual P&L). The Phase 0 audit's "75% coverage target" was a framing artifact — actual achievable ceiling is ~69% / ~28% on those stages, not 75%. The signal_emitted subset (100% coverage) is sufficient for Phase 3 simulator's primary input; no Phase 3 work blocked. Earlier (May 28 LATE evening): Finding #307 shipped: Phase 3 simulator PR 1 first commit = snapshot_schema='pm_v2' conditional-label fix at signal_generator.py:1041 + integrity rule update in lockstep (integrity_monitor.py:541 null_rate:opp_log:pm_v2_post_deploy col changed from snapshot_schema → method to handle the new conditional-label semantic) + 6 new unit tests + design doc updates reflecting INFERENCE DISCIPLINE catch (Bug #2/#3 ALREADY FIXED Apr 26 — Findings #234/#237 — so revised PR 1 scope drops their forward-fix work; only Bug #6 hyp_pnl re-run remains in #308). 42/42 tests PASS pre-commit (6 new + 36 prior). This is the LOCKSTEP fix for the original Finding #273 (deferred Apr 27 + re-confirmed May 28 afternoon). Closes #273 Phase 1. The 1,055 pre-fix mislabeled rows remain as historical baseline (not retroactively unlabeled per memory.md §C surgical-changes + audit-trail discipline); cost model in Phase 3 simulator handles both pre-fix and post-fix rows uniformly via proxy fallback. Earlier (May 28 evening): Phase 3 unified simulator design doc shipped at docs/PHASE_3_SIMULATOR_DESIGN.md (867 lines). Operator chose all-4-bundle scope via AskQuestion LATE afternoon → design doc concretizes 5-PR ship plan (~3-5 weeks): PR 1 foundation + Finding #273 fix lockstep + Bug #2/#3/#6 backfill; PR 2 volume-gate retune sweep (highest leverage per Finding #279 May-28 amendment); PR 3 sizing + priority + exposure-cap variants; PR 4 regime classifier v2; PR 5 depth-gate v2 decomposition. Phase 3 gates from Phase 0 doc (STRATEGY_ANALYSIS_ARCHITECTURE.md §0.6) VERIFIED MET as of May 28 (equity_bars_1m 185 days/418 symbols, actual_beat 94.9%, opportunity_log hyp_pnl 100% on signal_emitted subset, Bug #5 stop_triggered fixed; remaining sub-gaps on sp500/conviction_at_exit/depth-stage-hyp_pnl addressed in PR 1 scope). 8 open operator-decision points in design doc §15 must resolve before PR 1 starts. Earlier (May 28 LATE afternoon): Finding #305 shipped: RuleB (±90 min no-stop window) Phase 1 DRY-RUN — SECOND Phase-3 strategy rule landed ~3 hours after Rule #1 ship. Hardware cancel-and-rearm pattern (NOT soft-gate per Finding #228 lesson). Phase 1 RULEB_MODE='dry_run' default + RULEB_ENABLED=false default (no production behavior change until operator explicitly enables); Phase 2 active-mode flip pending operator's review of dry-run logs over ~few days. 25 new unit tests in tests/unit/test_ruleb_window_gate.py, 36/36 PASS pre-commit (25 RuleB + 7 Rule #1 + 4 prior signal-test regression). Bias-gauntlet PASS robustly on May-28 fresh simulator n=121: ex-top-2 +$127/100 STRENGTHENED from Apr-25 +$123/100 baseline. Structural inconsistency caught during pre-coding (INFERENCE DISCIPLINE + NO SHORTCUTS): the original Playbook §6.7 spec described a soft-gate/bookkeeping-defer approach that would have delivered $0 EV (same failure mode as FA_soft retracted in Finding #228); operator chose Path (A) hardware cancel-and-rearm via AskQuestion after agent surfaced the inconsistency with comparison matrix + recommendation. State is DERIVED per-cycle (no DB column, no migration) per memory.md "IBKR is source of truth for live state" + §C surgical-changes. Phase 1 known limitation flagged: restored positions (signal=None per position_manager:1155) cannot derive earnings_release_dt → graceful SKIP_NO_EARNINGS_DT; Phase 2 may add restart-safe lookup if dry-run logs show this happens in practice. Earlier (May 28 afternoon): Finding #304 shipped: Queue Rule #1 (Skip LONGs in downtrend regime) — FIRST Phase-3 strategy rule landed ~50h after the May-26 un-gate. Per memory.md NO SHORTCUTS + INFERENCE DISCIPLINE: 7 new unit tests added in lockstep (tests/unit/test_signal_regime_gate.py), all PASS pre-commit on prod's Python 3.12 venv along with 4 existing signal-test regressions (11/11). Rule fires on direction==LONG AND _safe_get_regime() == "downtrend"; fail-soft on every regime-fetch failure mode. Bias-gauntlet PASS robustly on fresh May-28 data: 22 LONG-downtrend trades / 5W / −$136.30 net (ex-top-2 −$175, more negative than headline → robust); full-population spillover positive (uptrend +$133/41tr, sideways +$89/39tr, downtrend SHORT +$172/3tr preserved). Sequence-of-work choice on Finding #273: operator AskQuestion option (E) DEFER per the original FINDINGS.md lockstep approach (with Phase 3 simulator), after evidence surfaced #273's bug surface is 16× bigger than originally documented + the fix scope is non-trivial without simulator schema knowledge. Earlier (May 28 afternoon): Finding #279 Phase B amendment shipped (PM earnings-market volume collapse evidence folded in). First piece of Phase 3 strategy-review work landed today. Doc-only change — no code, no integrity rule changes, no live-trading change. Triggered by operator's "next session we will deal with phase 3 strategy review" direction + AskQuestion option (B) "fold #304 evidence into #279" (vs standalone Finding #304 / both / defer). Sequence-of-work plan: (D then A) — ship Finding #273 (pm_v2 NULL bid/spread data-capture fix) next, then IN-QUEUE Rule #1 (Skip LONGs in downtrend regime), then re-evaluate remaining Phase 3 options. Today's pre-amendment data-gathering pass: re-ran scripts/strategy_performance_analysis.py (116 clean trades, +$212.21 net, WR 37.07 %, stop rate 51.72 %, 0 new clean closures since May 7 = 21-day strategy-execution drought May 6→27); re-ran scripts/analyze_finding_a_full.py (n=121 fresh, RuleB strengthened to ex-top-2 +$127/100, RuleG headline higher but ex-top-2 +$105 weaker, FA_hard retraction reasoning still holds via rearm_gap_open arm at -$1,545/100); direct SQL on earnings_markets LEFT JOIN markets USING (condition_id) quantified the PM volume collapse: Jan 2026 100.0 % gate-pass at avg $33,475 → May 2026 1.9 % gate-pass at avg $306, ~109× avg drop / ~1,847× median drop / 52× gate-pass-rate drop, NO Mid-cap market in all of May crossed the $2k gate (0/74), the 3 May gate-passers were AMPL Small (vol $2,441 / 12 % conv / Finding #207 SHORT skip), NVDA Large (vol $9,358 / 97 % conv / Finding #202 LONG skip), LOW Large (vol $2,253 / 89 % conv / no entry — sub-question for Phase B planning). HPQ entry yesterday crossed the gate via Finding #282 Gamma-cumulative fallback path (DB volume_total $23.84 < $2k but volume_threshold_met = True recorded; data-capture gap flagged not actioned per memory §C). Earlier: Finding #303 shipped (deploy_from_github.sh pre-pull hygiene for recurring working-tree drift, RESOLVED). Closes the operational-hygiene issue surfaced by Finding #302's deploy: 3 pull-abort rounds had to fall back to memory.md §H scp single-file hotfix. New git_hygiene_cleanup() function before the git pull --ff-only line: removes untracked files byte-identical to origin/master (case a, ~12 files today), resets modified files byte-identical to origin/master (case b, ~5 files today), stashes modified files that DIFFER from both HEAD and origin/master with timestamped message (case c, ~2 files today; operator reviews via git stash list). Operator-approved divergent-handling = stash-preserve (not abort-deploy or auto-drop). Scope = script-only (no docs/integrity-rule additions). Earlier (May 28 LATE morning): Finding #302 shipped (polymarket-backup.timer collision with apt-daily-upgrade.timer, RESOLVED). Operator-approved fix-path (A) "move backup timer to 05:00 UTC" via AskQuestion after I traced the full root-cause chain per the new INFERENCE DISCIPLINE rule. Today's missing May-28 backup filled via manual systemctl start polymarket-backup.service post-deploy; backup file successfully created, integrity rule flipped FAIL → OK. The IBKR Gateway disconnect from earlier this morning had the same root cause (network restart killed its TCP connection); Gateway auto-recovered separately and doesn't need a fix. Earlier (May 28 morning): memory.md 🚨 INFERENCE DISCIPLINE rule codified as companion to NO SHORTCUTS. Triggered by today's HPQ-scheduling-bug-that-wasn't false alarm (agent declared a +24h scheduling bug on a correctly-running AMC position; operator caught it instantly because the code's _calculate_scheduled_exit_time_from_db literally calls get_next_trading_day(entry_time) and the file-level docstring says "AMC Exits: Execute at 3:57 PM (next day)" — agent had never traced the exit-scheduling code, only the entry-sizing path). Rule + the May-17 2FA-inference + May-28 AMC-scheduling anchoring incidents now live in .cursor/memory.md between NO SHORTCUTS and TRADE EXECUTION PROHIBITION; §A pre-coding checklist updated with single-line cross-reference. No code changes; doc-only commit. Also today's IBKR live disconnect at 04:12 UTC — auto-recovered without manual intervention (port 4001 came back via systemd auto-restart at 04:17:16; trader reconnected at 04:18:17). Total downtime 6m 17s. Earlier (May 27 morning): factual correction shipped to the May-17 and May-24 reconnect entries below: I had incorrectly inferred "IBKR allowed session resume without re-2FA" on both Sundays from the fast (~16-20s) port-up time, but the operator clarified that every Sunday reconnect DOES require operator 2FA approval on phone — the IBC server-side logs are blind to the human-side push approval, and fast port-up is consistent with quick mobile approval, NOT with session resume. Both reconnect entries now reflect the actual workflow (operator-approves-push). The downstream reasoning chain that claimed "live Sunday automation may be feasible as fully unattended" was also unfounded and has been removed. Earlier (May 24 morning): IBKR Sunday 2FA reconnect handled manually (09:18 UTC restart → operator approved 2FA push on mobile → port 4001 open in ~20s → trader reconnected at 09:19:56 with account U2…13 / net_liq $27,537.50). Integrity-monitor: 41 / 45 OK + 4 FAIL — 1 transient (systemd:earnings-trader "activating" cache lag, self-resolves next cycle), 2 etfstack-subtree-scope (freshness:etfstack_equity_curve ~8.8d stale; systemd_last_run:etfstack-execute.service exit code 1) deferred to etfstack handoff, 1 main-project (artifact:strategy_analysis_report 350h stale vs 336h threshold, 14h past — would regenerate via cd [hidden] && source .env && export DATABASE_URL && venv/bin/python3 scripts/strategy_performance_analysis.py per memory.md §K). Surfaced for operator decision; not silently regenerated per NO SHORTCUTS. Earlier (May 17 morning): Finding #297 shipped (api_health_checker WARN-but-exit-0 cry-wolf fix) — closes the pre-existing systemd_last_run:polymarket-api-health.service FAIL surfaced during the §G probe of #296. Operator approved option (b) WARN-but-exit-0 via AskQuestion (vs widen-threshold / leave-as-is / build-age-aware-fix). Today's prior op also completed: IBKR Sunday 2FA reconnect handled manually (06:48 UTC restart → port 4001 open in ~16s — faster than May-10's 90s; IBKR allowed session resume without re-2FA → trader reconnected at 06:49:01 with account U2…13 / net_liq $27,534.56). Integrity-monitor: 45 / 45 OK (expected back to baseline after #297 §G probe). Earlier (May 11 evening): Finding #296 (_check_systemd_unit_last_result branch reorder) shipped as the lowest-risk close-out of the 3 May-10 "observed-but-not-actioned" maintenance items; the other 2 (Sunday-2FA partial-automation + Sunday-aware ibkr_snapshot threshold) remain explicitly deferred per AskQuestion. Today's 2FA reconnect data point (operator was awake-and-available; an automation would have put the trader back online ~43 min earlier) recorded for future operator decision on item #1. Project remains in steady-state observation mode for ~9 days until May 26, 2026* when the Phase 3 Strategy Revamp un-gates (Finding #240's 30-day equity-bars window completes). Two new modular subtrees (strategies/etfstack/ LIVE on paper account DU…91 since May 11 14:13 UTC; strategies/pm_earnings_arb/ research-only) are isolated by design and have dedicated handoff prompts.)IMPLEMENTED-PENDING-DEPLOY. Both copies require FUNDER_ADDRESS and refuse the retired proxy and the retired signer. Not in the collector restart set. Do not deploy or restart units for this change. Canonical: FINDINGS #488.DEPLOYED Oct 4, 2026 12:05 UTC (unit polymarket-deploy-20261004T120553Z-812717079, SHA 7c9d6b0; trader 12:06 UTC, integrity monitor 12:08 UTC; Gateway not restarted). #486 puts timeout=8 on our NASDAQ earnings-calendar GET and a 45s budget on the 21-day loop so a hung calendar degrades to silence instead of a 120s watchdog death. #487 keeps storage:orderbook_snapshots FAIL at 12 GB after grace expiry and, when no live chunk is older than 90 days, says the drain finished instead of saying the drain failed. trader:restart_tripwire was ok after that restart (NRestarts=0). Canonical: FINDINGS #486 and #487.RESTART before last-resort 2FA — IMPLEMENTED-PENDING-DEPLOY (flag default off). Stacked on master 543fa5d (B1). Paper P4 + live L7 proved token reuse. Loopback client never sends STOP. Monday 13:30–20:15 UTC freeze and weekday entry/exit waves refuse while the flag is on. Do not enable, deploy, or restart Gateway/trader in this change. Canonical: FINDINGS #484.WALLET_NAGI leaked on /api/btc5m/docs/doc_map — IMPLEMENTED-PENDING-DEPLOY. After #467 deployed (master 3138aca, deploy #11), a post-deploy scan of the live dashboard found /api/btc5m/docs/doc_map still serving the raw WALLET_NAGI value (facts.py's benchmark-maker wallet prefix, [REDACTED]) in doc prose, even though /api/btc5m/state already masks the same constant per-route. Root cause: doc-serving routes render prose with no per-field call site to mask at, so #467.D relied entirely on secret_redaction._collect_known_identifier_literals() — which collected btc5m's wallet/host constants from a hand-maintained tuple of attribute NAMES ("WALLET_MAIN", "WALLET_TITUS", "EXECUTION_HOST", "EXECUTION_HOST_IP", "EXECUTION_TREE_PATH") that nobody updated when WALLET_NAGI was added to facts.py — a second, separate list a human had to remember to extend. Fixed centrally per operator direction ("mask identifiers centrally for every public response, JSON APIs included, rather than per route"): that function now DISCOVERS every WALLET_* constant (plus the three EXECUTION_HOST*/EXECUTION_TREE_PATH names) by naming convention, so a future addition to facts.py needs zero other code changes. Per-route structural masking (_mask_wallet, mask_identifier) is unchanged — it still runs first for the nicer partial-reveal UX; central redaction is the backstop that catches a response surface (like a doc route) with no per-field masking of its own. Built the requested close-out test (tests/unit/test_finding_483_identifier_leak_gap.py, 26 tests): enumerates EVERY public GET route, including the ones a scan with no varied inputs can't reach — /api/trade-chart-data (fixture source+id query params), etfstack's /backtest/{run_id}/equity+/metrics (fixture run_id) — plus both doc-serving routes at every valid slug, and asserts none of the known account id, known server IP/path, or ANY WALLET_*/EXECUTION_* value in facts.py (discovered dynamically, not hand-listed in the test either) appears anywhere. That sweep found a SECOND hit the same way (not from facts.py): DU…91, etfstack's earlier paper account id, on five main-dashboard docs — fixed by adding it to identifier_masking.KNOWN_ACCOUNT_IDS (read by both the doc masking and the central backstop). Widened two existing #467 tests that had the identical blind spot. Also folded in (B3 review): a #467 test that HANGS on a fresh cloud VM — it planted a fake DATABASE_URL at a private-looking address (10.20.30.40) and hit every real route; a VM whose network silently drops (rather than rejects) traffic to that address left the real DB connect() call blocked for the OS's full TCP timeout with no PGCONNECT_TIMEOUT set (confirmed: passes with PGCONNECT_TIMEOUT=1, hangs the same way on pure master). Fixed hermetically (not by tuning a timeout): a no_db_network fixture patches the ONE shared engine factory every dashboard/btc5m/etfstack route uses (src.db.connection.create_db_engine) to return a bare object() — any DB call raises AttributeError immediately, no socket ever opens, and every call site already treats that as its existing "DB unavailable" fallback; the fixture's literal address is now the RFC 2606-reserved db.invalid, never a real host/IP. Verified hermetic directly (socket.socket.connect instrumented: zero calls). Full unit suite and drift audit rerun clean.Sep 26 — Finding #467: public dashboard defense in depth DEPLOYED Sep 26, 2026 22:19 UTC (deploy #11 3138aca; GPT round-3 approved; one open gap: /api/btc5m/docs/doc_map still shows the raw WALLET_NAGI value — FINDINGS #483 follow-up IMPLEMENTED-PENDING-DEPLOY on master c4e8a32) (awaiting cross-family review; no deploy this session). Root cause: Sep 14 an internet scanner harvested the live DATABASE_URL because /docs/operations served docs/OPERATIONS.md verbatim and the doc had the connection string pasted into it (committed-secret cleanup is separate, #457/reliability/cred-scrub). Three-part runtime guard, same finding: (1) earnings_dashboard/secret_redaction.py — global ASGI middleware redacts every response (HTML/JSON/plain text/404/error/unhandled-exception) by value (env-collected secrets, snapshotted at install time) and by shape (postgres URL creds, DigitalOcean AVNS_, Telegram tokens); a catch-all exception handler closes the one real Starlette bypass (ServerErrorMiddleware sits outside all user middleware — reproduced live via this repo's own /docs/{doc_name} route when markdown isn't installed). (2) FastAPI's /openapi.json / /docs / /docs/oauth2-redirect / /redoc disabled via the constructor; three always-inert unauthenticated write endpoints deleted (/api/testing-prod/confirm, /api/testing-prod/reset, /api/set-trading-mode — get_orchestrator() always returns None), plus the now-dead save_persisted_mode(). (3) earnings_dashboard/identifier_masking.py masks IBKR account/order ids, wallet addresses, and server hostnames/IPs/paths at each payload's assembly site (performance/position data untouched); an exhaustive grep for the same identifier classes across the two OTHER modular dashboard subtrees (btc5m, etfstack) found real wallet-address, execution-host, and paper-account exposures in both their JSON endpoints AND their static tab templates — fixed with LOCAL, dependency-free mask helpers in each subtree (never importing from earnings_dashboard, preserving each subtree's "modular, separately-extractable" guarantee). tests/unit/test_finding_467_dashboard_hardening.py — 74 tests, including 4 "trap route" tests proving the filter covers a route added after this session, a real planted-secret test against docs/OPERATIONS.md and the btc5m plaintext docs route, and a full 56-route sweep for both a planted secret and every known identifier. Full unit suite 2297/2297 PASS, scripts/doc_drift_audit.py clean. Prod untouched (read-only this session; no restarts, no deploy). Cross-family review (71dd002) returned CHANGES REQUIRED — 4 blockers, all fixed same finding: (1) a chunk-boundary split-before-redact leak, reproduced with the reviewer's exact repro, fixed by redacting the whole combined window BEFORE splitting it (margin now sized from the actual literal/pattern lengths in play, not a fixed 4 KiB); (2) public /docs/{doc_name} pages still served the real account number/server IP/deploy path in prose — #457 scrubs committed secrets, not identifiers, and decision D covers docs too — fixed with exact-value masking on the rendered copy (U2…13 / [hidden]), source .md files untouched; (3) short/URL-encoded DATABASE_URL passwords weren't collected at all — fixed with no minimum length + token-boundary anchoring for short literals + raw/decoded/re-encoded URL-password variants; (4) the full-route sweep passed with the filter removed (reviewer's own mutation check: 44/44 routes) — fixed with a redact_bytes call-count instrumentation test plus an explicit mutation test reproducing that exact result. Bonus (non-reviewer-flagged) fix: the catch-all exception handler's response was found to bypass SecretRedactionMiddleware entirely (Starlette hands Exception/500 handlers to ServerErrorMiddleware, outside all user middleware) — stayed safe only because the handler redacts before building its response; docs corrected, dedicated test added. Post-review: 125 tests in the #467 file, full suite 2348/2348 PASS, drift audit clean. Round 2 (05c02ed) returned CHANGES REQUIRED — 1 blocker, confirmed the exception-path bonus fix valid: a secret made of multibyte/Unicode characters still leaked across a streaming chunk boundary — reviewer's exact repro was "🔐" * 1500 (6000 UTF-8 bytes) split into a 5000-byte and a 1000-byte chunk. Two stacked bugs: the carry margin was sized from Python's len(v) (character count), undersizing it for any multi-byte literal (1500 "characters" vs. the real 6000 bytes); and redact_bytes() decoded each window to str with errors="replace" before matching, which permanently destroys an incomplete trailing UTF-8 sequence before the rest of it ever arrives in the next chunk — no margin size fixes a corruption that already happened. Fixed by matching secrets as UTF-8 bytes throughout the streaming path end to end (_build_literal_pattern_bytes, _GENERIC_PATTERNS_BYTES, _SecretStore.redact_bytes — never decoding a window at all) and sizing the margin from each literal's len(v.encode("utf-8")); redact_text (str-based, used only by the exception handler, which has no chunk boundary) is unchanged. New TestStreamingMultibyteBoundaryRegression (10 tests): direct margin-in-bytes assertions for 2-/3-/4-byte characters, the reviewer's exact repro run against the real un-overridden margin, an offset sweep generalizing the round-1 ASCII sweep to all three byte widths (every offset including mid-character splits), and two targeted mutation checks (margin-formula-only, decode-only) each confirmed to reproduce a real unredacted leak. Mutation-checked against the actual pre-round-2 file (git show HEAD:...): 3 of the 10 new tests correctly fail against it; file restored byte-identical. Post-round-2: 135 tests in the #467 file, full suite 2619/2619 PASS, drift audit clean (12 checks).
Aug 21 later evening — Finding #392: K-B5 lost its special autonomous status (operator: "we treat everything the same"). consistency:b5_pilot_auto_revert (#366) only ever ALERTED — verified no code path flips B5_PILOT_ENABLED or blocks pilot admissions (env-read-only flag) — so the change is semantic: the rule is now contract:k_b5_revert_decision, the eleventh #389-pattern contract reminder (category contract, WARNING, 📋 telegram once-then-rearm). Revert conditions unchanged verbatim (net ≤$0 ex-top-2 at 25-fills/Dec-15 re-score · >3 gap-throughs · breaker with ≥2 pilot stops); a trip = "K-B5 REVERT DECISION DUE… Alert-only — outcome returns to the operator". Nothing acts autonomously anywhere now. Sweep: integrity_monitor + config comment + changelog monitor field + builder texts + 6 dashboard tooltips + Rev (§II.7 header/row, §II.2/§II.3, §12.5 annotation, timeline) + plan + STRATEGY.md + PLAYBOOK + #366/#389 amendment notes. Tests red-proven (old code = 12 fails); no trader-side deletion needed — the trader never carried an autonomous path. Deploy: targeted integrity-monitor restart + report regen; trader untouched.
renderPhase3SimulatorStatus + build_phase3_simulator_status builder + report key) · Gate Retune Sweep collapsed into a 📦 <details> archive (calendar table kept) · regime snapshot → 1-row strip under the Cockpit (same ids, renderRegime unchanged) · Performance-vs-Projections removed (ladder coverage verified) · Stop Loss + Stop Reversal merged into "Stops (descriptive)" (3 summary cards dropped, aggregates in the table title) · selection-research cards folded into the Unified Simulator bullet · ROI-target card removed · P&L tooltip glossary single-sourced. Strategy 32 → 28 section divs; exhaustive greps clean (#368 discipline), Jinja + node --check + div-scan clean (the pre-existing stray </div> died with the regime section), suite green (no new failures), drift audit CLEAN. Dashboard-only deploy, NO trader restart.confirmed (write-locked) + operator_ir_check audit row; consistency:earnings_dates_disputed back to ok. storage:orderbook_snapshots made DRAIN-AWARE: size FAIL → WARN only while archive:orderbook_backlog is OK AND today ≤ ORDERBOOK_DRAIN_GRACE_UNTIL (2026-08-29); self-reverts to FAIL if the drain stalls or the date passes; 12 GB threshold unchanged.contract:* integrity rules (category contract, WARNING, telegram_reminder) compute the K-S2C / K-B4 / K-RES contracts, the D-1 / D-2 / D-4 / D-5 / D-8 triggers and the C-Q2+G-RS (Sep 13) / C-Q3 (Oct 12) date reminders LIVE and send a 📋 Telegram reminder once on the ok→due transition (re-arms per the monitor table). Alert-only — every contract outcome returns to the operator; only K-B5's #366-encoded auto-revert acts autonomously (stale by the same evening — #392 removed K-B5's special status, bullet above). The operator's C-Q3 reframe landed in the same pass (Rev §II.5 + §II.7 + plan + dashboard builder): no hard slot limit under S2c+shrink-to-fit+A-7 — the Oct question is "accept per-trade dilution on peak nights, or add capital before the season." Non-trader deploy (targeted polymarket-integrity-monitor.service restart — its own unit; trader untouched).avg_ret_pct now true percent; template tripwire test) · F-298 effective_cumulative_volume_at_entry (migration 036, both persist twins bind signal.volume_total; backfill evaluated + SKIPPED — 96/201 coverage, wrong-time semantics; trader-side write QUEUED for the next safe-window restart) · F-321 bounded direct CF retry (migration 037 cf_retry_count, 5 attempts / 3-day cooldown / terminal price_data_unavailable_final_v1; the EXISTS-bars gate dropped — the pricer fetches Polygon directly, so bar-cache state was the wrong signal).renderBacktestTable/renderCohortTimingTable/renderScorecard + their 7 dead DOM-id lookups). Judgment classes (7 CONSOLIDATE + 3 REMOVE candidates, mostly Strategy tab) await operator review in docs/DASHBOARD_CLEANUP_PROPOSAL_2026_08.md. Template-integrity 92/92 + full suite green; dashboard-only deploy, NO trader restart.regt_2x, max effective leverage 2.0) + the 1.50× total-exposure cap unchanged. Code-verified: both gates compute against TRADING CAPITAL (risk_manager.py:601 equity base = net-liq − reserve; block at :1162), so the 1.50× cap binds strictly before the 2.0× margin gate (a 0.50×-headroom backstop). Nothing ships. SSOT: revalidation §I.2 row LEV-1; record: FINDINGS #366 update note.docs/*.md must be reachable from .cursor/memory.md (BFS memory → tier-2 → tier-3; verified: 13 tier-2 + 19 tier-3, 0 orphans; archive/+packs/ exempt per R4). NEW docs/archive/INDEX.md is the SSOT for historical docs (20 archived + 7 banner-frozen-in-place entries, superseded-by pointers, plus the OPS-1 orderbook data-archive counterpart); 3 docs got their missing R4 frozen banners. Repo-side only; synced to the server at the Aug 21 15:17 UTC trader-restart deploy..cursor/memory.md leaned 695 → 316 lines (−55%) per the operator-approved proposal (scripts/analysis_357/memory_lean_proposal.md). All binding rules survive (verbatim / merged / pointer); module API reference moved to CONTEXT.md §Module quick reference; drift fixed (stale Apr-25 §K numbers → Playbook §1 lookups, archived STRATEGY_IMPROVEMENTS claim removed, §F #354 line extended per #369). Repo-side only — the server's memory.md copy synced at the Aug 21 15:17 UTC trader-restart deploy (rode that window as planned).28b572b DEPLOYED Sep 27, 2026 00:33 UTC; deploy #15 860af84 DEPLOYED Sep 27, 2026 02:43 UTC)artifact:db_write_spool WARN, trader quarantine stays FAIL + CRITICAL. Verify: tests/unit/test_finding_481_live_trades_poison.py.trades.token_label to varchar(64) (typmod probe scoped to ALTER TYPE migrations); artifact:db_write_spool pages on FAIL and on RESOLVED, not on poison WARN. The incident's 3 quarantined payloads (2 distinct trades) were released per OPERATIONS; spool empty, one RESOLVED page 02:51:25. Verify: tests/unit/test_finding_482_token_label_widen.py.polymarket-deploy-20260927T003319Z-514718516 (00607c9..28b572b, rc 0; polymarket-live-trades restarted, RESOLVED 00:37:12); #15 unit polymarket-deploy-20260927T024258Z-2459628687 (28b572b..860af84, rc 0; 042 applied). Gateway untouched in both. OPEN: the dashboard-restart false page recurred in #14 (00:33:57 → RESOLVED 00:37:09); its fix, PR #9, is pending. Records: FINDINGS #481/#482; Project store ops logs internal/ops-log-2026-09-27-deploy-14.md / -15.md.DEPLOYED Sep 26, 2026 23:29 UTC: deploy #13 00607c9)DEPLOYED Sep 26, 2026 23:29 UTC: paper 4002 proved immediate repeated reqCurrentTime calls caused the 0/37 recovery result. The recovery path now spaces owner-thread probes by 2.0 s, preserves reset-budget truth, cancels late owner jobs, classifies owner-pump misses as main-loop stalls, routes completed 2FA logins correctly, and persists a 10-minute failed-reset restart-tripwire marker while request-path follows the recovered process. Pre-review 1.1 s paper reset recovered read-only in 4.082 s; final 2.0 s fake-Gateway test passes, while the paper recheck is UNVERIFIED because paper emitted 2110 and initial connect failed before the probe. No live 4001 attach and no Gateway restart. Opus review #454 mechanism PASS; follow-up fixes await re-review.DEPLOYED Sep 26, 2026 23:29 UTC: pending fills hydrate memory/risk/earnings monitoring and keep admission closed through reconciliation. Migration 039 + advisory locks make entry/close/execution replay idempotent; prod duplicate preflight returned 0 rows. Trader/collector locks and capacity are separate. Exit execution precedes close; committed closes and stopout-before-restart self-clear. Transients back off forever; poison/corrupt rows quarantine with a tested audited release command. An unspooled fill creates a sticky/emergency entry gate without touching IBKR's exit controls. Integrity is read-only over the spool. Any failed pre-restart deploy gate restores the pre-pull checkout. Earlier four-case real-Postgres suite passed locally; final nine-case suite (including over-long exec-id 22001+spool) awaits the requested cloud-VM run under the no-Mac-installs rule. Historical 98-print backfill remains a separately approved write.2ecb02c..00607c9 (the exact approved SHA); unit polymarket-deploy-20260926T232940Z-285816666 23:29:43 → 23:33:38 UTC, DEPLOY_EXIT rc=0; 039 applied, 040/041 current; Gateway untouched. Incident: the live-trades poison-row loop (trades.token_label varchar(20) vs a 22-character outcome; 26 CRITICAL alerts 23:32:47–23:37:46 UTC; polymarket-live-trades stopped 23:39:08 by operator decision) was fixed by #481 (deploy #14 28b572b, Sep 27 00:33 UTC: poison rows quarantined once and marked seen, market-data poison WARN only, collector restarted) and #482 (deploy #15 860af84, 02:43 UTC: migration 042 widened token_label to varchar(64); the quarantined trades were released and the spool was empty at 02:51); artifact:db_write_spool was FAIL (12 quarantined) until deploy #15 860af84 (ok 02:51:25); #454 soak pending; Sep-25 98-print backfill not run. Incident record: Project store internal/incident-2026-09-26-live-trades-spool-loop.md; post-deploy record: FINDINGS #455.DEPLOYED Sep 26, 2026 22:39 UTC: deploy #12 2ecb02c)systemctl show snapshot (ActiveState, StateChangeTimestampMonotonic, NRestarts), so a recovered active sampler can't be misread mid-restart; three or more automatic restarts since the last manual start with a state change in the last 5 min FAIL even inside the 120 s grace; ambiguous-address evidence WARNs before the stale grace.3138aca..2ecb02c (no-ff merge of PR #8 094a00f); unit polymarket-deploy-20260926T223934Z-2361616640 22:39:37 → 22:43:08 UTC, DEPLOY_EXIT rc=0; sampler active, NRestarts=0, restart_loop: false; 0 sampler pages. OPEN: the deploy's final dashboard restart raced the new monitor's first cycle — one CRITICAL TRIGGERED (22:42:48) and RESOLVED (22:47:54) for systemd:polymarket-dashboard.service. Record: FINDINGS #479.DEPLOYED Sep 26, 2026 22:19 UTC: deploy #11 3138aca)/openapi.json, /docs, /redoc off; POST /api/testing-prod/confirm, /api/testing-prod/reset, /api/set-trading-mode removed; the IBKR account (U2…13), wallets and server IP/deploy path masked on public pages, docs included, plus the btc5m and etfstack tabs — one open gap: /api/btc5m/docs/doc_map still shows the raw WALLET_NAGI value, the six-character prefix of the third-party benchmark maker's wallet (not a full address, not Marius's wallet); a code follow-up is OPEN (FINDINGS #467 Known gap). Verify: tests/unit/test_finding_467_dashboard_hardening.py.0e985dd..3138aca (no-ff merges of PR #4 d576b7d and PR #7 295fe28); unit polymarket-deploy-20260926T221858Z-2508114717 22:19:01 → 22:22:24 UTC, DEPLOY_EXIT rc=0; migrations current=7 applied=0; Gateway stack and root crontab untouched; nothing paged. Post-deploy record: FINDINGS #467.DEPLOYED Sep 26, 2026 21:56 UTC: deploy #9 0e985dd)CHANNEL_ALREADY_COVERED_SQL explicitly casts every repeated bind, preserving #414/#431 semantics. The legacy run_full_sync / sync_from_markets_table writer and scripts/sync_earnings_markets.py are retired; only the live earnings_market_fetcher plus supported NULL-field backfills remain. The repo-wide tripwire scans SQL fragments and ambiguous date_trunc / EXTRACT inputs; real-pg8000 3/3 passed. Migration 041 relabels only ids 9381/9382/9383/9387. #477 is merged: DATA_MANIFEST is numeric 040 then 041 and both ship in one deploy. Full narrative: FINDINGS #480.schema_migrations had no 040/041 row. After 041 the four rows read timing_channel_mismatch, so the query now returns 0.SELECT count(*) FROM polymarket_opportunity_log WHERE skip_stage='timing' AND skip_reason='timing_channel_leak' AND (
(id=9381 AND evaluated_at='2026-09-23 14:30:11.403726+00' AND condition_id='0x2ffe6e26095d2a088f6674ce4cfd54d3f862937f07ef069be6908e123d95f92c' AND symbol='GIS' AND timing_channel='AMC' AND earnings_timing_resolved='BMO' AND earnings_datetime='2026-09-23 20:00:00+00') OR
(id=9382 AND evaluated_at='2026-09-23 14:30:11.701571+00' AND condition_id='0x43deba5d45a469c11be88dbf6769ce958d107f2b7f5dfe73403603e00b4aac1c' AND symbol='CTAS' AND timing_channel='AMC' AND earnings_timing_resolved='BMO' AND earnings_datetime='2026-09-23 13:00:00+00') OR
(id=9383 AND evaluated_at='2026-09-23 14:30:11.915809+00' AND condition_id='0x63e296422fbc8aa3cd7b57cc8147c3acd6668bd0804367cd50706411420771bf' AND symbol='PAYX' AND timing_channel='AMC' AND earnings_timing_resolved='BMO' AND earnings_datetime='2026-09-23 13:00:00+00') OR
(id=9387 AND evaluated_at='2026-09-23 19:50:09.672826+00' AND condition_id='0x2ffe6e26095d2a088f6674ce4cfd54d3f862937f07ef069be6908e123d95f92c' AND symbol='GIS' AND timing_channel='AMC' AND earnings_timing_resolved='BMO' AND earnings_datetime='2026-09-23 20:00:00+00'));
src/earnings/pnl_fields.py is the one definition of realized/gross/net P&L for closes and corrections; rewrite_exit_price also writes the trading_positions twin. trade_reconciler flags gross/net/twin disagreement (> $0.01). Migration 040 changes BBW audit 274 gross/net −121.52 → −282.10; position 307 exit 37.25 → 34.66, realized −121.52 → −282.10, pct −4.998725 → −11.604183; SNOW 75 net −9.96 → −1.16. Exit commission stays NULL; exact before-value pre-checks passed.Finding #477 040 and four Finding #480 041 NOTICE lines, then applied=2; schema_migrations has both ids (21:56:30, 0e985dd); BBW 274/307 read −282.10 and SNOW 75 −1.16; the four opportunity rows read timing_channel_mismatch; the #477 P&L check over the full history finds 0 mismatches (legacy MTB#4/TXN#9/T#10 only); Phase-1 net −$922.28 (n=13); 0 42P18 / covered-channel warnings; integrity 85 ok / 2 warn, nothing paged. The next scheduled trade-reconcile run is still to confirm on its own (UNVERIFIED). Record: FINDINGS #480 Post-deploy.f28c0da..0e985dd (no-ff merge of PR #5 d61d8af); detached unit polymarket-deploy-20260926T215623Z-326053163 21:56:26 → 21:59:54 UTC, DEPLOY_EXIT rc=0; migrations current=5 bootstrap=0 applied=2 (040, 041); Gateway stack and root crontab untouched; trader ready, 0 breakers.DEPLOYED Sep 26, 2026 20:55 UTC, deploy #8 614a29d; doadmin reset DONE Sep 26, 2026 20:49 UTC (#457); trusted sources DONE Sep 26, 2026 16:06 UTC (#457); prod Finnhub key and the other rotations OPEN)doadmin password was committed in 20 tracked files and the cluster accepted any IP. The 4 prod scripts/ now resolve the URL via src.db.connection.get_database_url() and exit when it is missing; the 15 archive/ files and the OPERATIONS pooling snippet read os.environ["DATABASE_URL"]. No git history rewrite (OD9), so the old password stays in history until the reset.ip_addr:[hidden] only, added 16:06:17 UTC after a 15-min pg_stat_activity sample (182 samples) showed that address as the only customer client. All 7 DB services kept their sessions (0 restarts); the Mac's direct TCP to :25060 now fails. Workstations and agents reach the DB over SSH only (OPERATIONS §2 "Database Connection")."" defaults; docs/archive/POLYMARKET_KNOWLEDGE.md is redacted; scripts/earnings_timing_5y_api_study.py and the 2 fill_earnings_* scripts need the key in the environment (the fill_* default was the live prod FINNHUB_API_KEY, so its rotation is OPEN for the operator); the burned Polygon literal is now its sha256 prefix 90ba0a59ae1b.:8080 /docs/operations served the live DATABASE_URL and was harvested twice by scanners (Sep 14 10:06 UTC and Sep 26 02:20 UTC). DB access can be neither proven nor excluded (no pg_stat_statements, no commit timestamps, no session history); nothing else was found. See FINDINGS #457 root cause (4); #467 adds the dashboard response-redaction guard on its own branch. The repo-side guard is the #457 denylist test (13 sha256 prefixes, every tracked file).doadmin reset: DONE Sep 26, 2026 20:49 UTC (#457) — unit od8-doadmin-reset-20260926T204837Z started 20:48:44 UTC on the operator's go, RESET COMPLETE at 20:49:21 UTC, rc 0. The 7 long-running DB clients restarted and reconnected; the old password is rejected; ibgateway-ibc was not touched. The scrub then shipped in deploy #8 614a29d (not with B1).tests/unit/test_finding_457_no_live_db_url.py (DB password, bot token, credential getenv defaults, burned Polygon key) and tests/unit/test_finding_399_recon_polygon_timing_priority.py; doctl databases firewalls list b28d3f40-480e-4931-a38d-68c7ef428546 shows one rule.a695050..614a29d (no-ff merges of reliability/deployed-sweep-a695050 532246e, reliability/cred-scrub cfb3c62, cursor/cq2-count-trigger-642f 1b45809, cursor/db-activity-audit-7529 b0b77f8); detached unit polymarket-deploy-20260926T205457Z-3090020335 20:55:00 → 20:58:21 UTC, DEPLOY_EXIT rc=0; migrations current=5 bootstrap=0 applied=0; Gateway stack and root crontab untouched; trader ready, 0 breakers; /docs/* 0 secret hits. Post-deploy record: FINDINGS #479; the #479 evaluator fix shipped in deploy #10 f28c0da (DEPLOYED Sep 26, 2026 21:30 UTC).DEPLOYED Sep 26, 2026 20:55 UTC: deploy #8 614a29d)614a29d): contract:cq2_grs_detection_rejudgment is no longer the overdue Sep 13 date reminder. It counts wrong-held positions (canonical, resolved, open at the release gate, contra; since the #358 deploy) and WARNs at 12 or on 2026-12-15 — dashboard/integrity panel only, no Telegram (src/data_tracker/contract_cq2_count.py). 2/12 at the deploy (BOX, BBW), so the standing WARN cleared to ok.tests/unit/test_finding_478_cq2_count_trigger.py; the integrity panel showed the rule ok "2/12 wrong-held positions (backstop 2026-12-15, 80 days out)" after deploy #8 614a29d.DEPLOYED Sep 26, 2026 19:05 UTC: deploy #7 a695050, with the docs sweep)requirements.txt pins every direct dependency to the prod venv (read-only freeze, Python 3.12.3); constraints.txt is the full freeze; install with pip install -r requirements.txt -c constraints.txt. The deploy never runs pip. Verify: tests/unit/test_finding_473_pinned_requirements.py.deploy/migrate.py bootstrap baseline now pins column types with length/precision, nullability and defaults, PK/UNIQUE/FK/CHECK constraints, index definitions and seed rows by key with post-UPDATE values (captured read-only from prod); a partially applied file is refused before restarts. New read-only --check-baseline (prod Sep 26: 5/5 match) and --capture-baseline. The two btc5m.live_samples seed rows are not required (table truncated in the Sep 25 cleanup). Verify: tests/unit/test_finding_474_bootstrap_baseline.py.scripts/detection_latency_replay.py:simulate is a poll-grid port of the live _check_polymarket_price (#337 gate, #358 fresh-window median via select_fresh_median, Gamma proxy hook, #341 contra grades + confirmation/grace), reading every number from live config; w7_arbiter_sweep.py uses it. --truth market = pm_resolution; --positions = time-aware live is_valid (#366 split at the scheduled entry). Research tooling only. Parity with the live method on 48 random streams + targeted cases; identical to the C-Q2 v2 simulator on the v2 cohort (crowd 69/69, live gate 26/26); entry-conviction window half-open. Verify: tests/unit/test_finding_475_detection_replay_parity.py.facb218..a695050 (no-ff merges of reliability/docs-sweep 5ac9c23 and reliability/followups 1633a17); detached unit polymarket-deploy-20260926T190538Z-2875818611, DEPLOY_EXIT rc=0; migrations current=5 bootstrap=0 applied=0 (no DDL); Gateway stack and root crontab untouched; trader ready, 0 breakers. Post-deploy record: FINDINGS #475.DEPLOYED Sep 26, 2026 15:15 UTC: deploy #5 f130113; prod moves DONE Sep 26, 2026 14:59 UTC (#468)).gitignore covers what the server writes inside the checkout: data/api_health/, trading_block.txt, login_attempt_state.json, logs/ibkr_last_recycle.json, logs/*.log.*, the generated scripts/phase3_* outputs, scripts/_sec_ticker_to_cik.json (comment moved off the pattern line) and .env.bak.*. strategies/btc5m/src/umpire/ and *.py.test are never ignored. OPERATIONS §8.A2: never run git clean on prod (-x/-X would now delete the IBKR safety state); move, never clean./var/lib/polymarket/archive/legacy-logs-2026-08-21.tar.zst holds the 42 legacy logs (originals removed after verification);/root/secret-backups/ holds the 2 .env.bak.* (0700/0600; delete after OD8);/root/btc5m-umpire-2026-09-26/umpire/ holds the umpire tree (Mac copy in ~/Backups/polymarket-btc5m/umpire-2026-09-26/; btc5m owner decides whether to commit it);/var/lib/polymarket/archive/checkout-hygiene-2026-09-26/ holds the dumps, scratch copies and evidence.tests/unit/test_finding_468_checkout_hygiene.py. git -C [hidden] status --porcelain must stay empty; it was empty after deploy #5 (15:22 UTC), with the safety-state files unchanged and the Gateway untouched. Any new untracked path on prod is new server-written state or a stray file: move it, never git clean (OPERATIONS §8.A2).DEPLOYED Sep 26, 2026: deploy #1 884e528 10:18 UTC; deploy #2 3d7f1b1 11:48 UTC)deploy/migrate.py + migration 038; steady-state deploys issue 0 DDL; --dry-run is the read-only pre-flight (prod dry-run Sep 26: 038 would apply, 029/030/etfstack 030/btc5m 031 would record as bootstrap, baseline matched). Review fixes: all migrations fatal, checksum drift fails closed, reviewed bootstrap baseline, conviction-tracker DDL gated per kind and fail-closed. Rollback is patch-forward only (never revert #444). Verify: tests/unit/test_finding_444_migration_ledger.py. F-7 (branch reliability/batch-a-2): the deploy restarts earnings-conviction-tracker after a read-only --check-schema passes. F-8: every deploy runs detached — bash deploy/remote_deploy.sh (systemd-run unit, log /var/log/polymarket/deploy-<id>.log, polled to DEPLOY_EXIT rc=N); deploy #1 ran attached and lost its tail steps. Verify: tests/unit/test_finding_444_detached_deploy.py.approximate_row_count; 60 s statement timeout; per-statement transactions (_PerStatementReads). Holds recorded: btc5m recorder paused 19:13:46 UTC Sep 25 (still disabled), dashboard held 20:08:53–21:06:22 UTC. Review fixes: §O reporting entry, single-flight coverage refresh, global last-updated via the newest chunk. Delta fixes: 15 min backoff after a failed coverage refresh; last-updated walks past an empty newest chunk (≤8 chunk-bounded probes). Deploy #1 (884e528, Sep 26 10:18:18 UTC) acceptance failed on one check: the 90-day per-market "Market availability" query timed out 4/4 passes (DB-protection criteria all passed; no hold). Fix on reliability/batch-a-2: 7-day grouped counts, cached like coverage; prod plan ≈ 16.5 s cold / < 1 s warm. Rollback: ops-hold the dashboard + patch forward (never redeploy the old query). Verify: tests/unit/test_finding_445_dashboard_bounded_sql.py; post-deploy temp_bytes < 100 MB per pass.xvfb(-.*)? / x11vnc(-.*)?; deploy/needrestart/gate.sh (prerequisites + coverage over needrestart.conf + every conf.d/*.conf) and the compare-only deploy/check_host_reference.sh (live xvfb/x11vnc/ibgateway-ibc units + apt drop-ins committed under deploy/host-reference/) both run before migrations and restarts and fail immediately. Hold history: apt-daily-upgrade.timer disabled Sep 25 19:25:51 UTC, re-enabled Sep 26 11:27 UTC operator-attended (DONE Sep 26, 2026 11:27 UTC (#443)). Verify: tests/unit/test_finding_443_needrestart_live_units.py.IMPLEMENTED-PENDING-DEPLOY (section below). Original note: after the delta re-review; the Batch A implementation 8ba88d0 was reverted by f8c972c. FINDINGS #450 (APPROVED-AWAITING-IMPLEMENTATION) carries the B1 scope and the prod threshold values..env change. History: retired by #470 (Batch B2, IMPLEMENTED-PENDING-DEPLOY), which fixes the cause. Verify: tests/unit/test_finding_453_stall_monday_stopgap.py; trader log Main loop watchdog budget 120s -> 300s Mon 04:00 UTC.DEPLOYED Sep 26, 2026 19:05 UTC, deploy #7 a695050) requirements.txt pins every direct dependency to the prod venv and constraints.txt is its full freeze (prod pip freeze matched it after deploy #7). History (pre-#473): requirements.txt omitted pg8000 and sqlalchemy (both imported by every service; the prod venv had SQLAlchemy 2.0.45 / pg8000 1.31.5) and left fastapi, pytest-asyncio (and transitive Starlette) floor-only — the cross-family reviewer's clean venv needed pg8000 + FastAPI 0.115.12 / Starlette 0.46.2 / pytest 7.4.4 / pytest-asyncio 0.21.2 to run the suite; the follow-up was to pin them in a dedicated change with a venv rebuild test. F-2 C10 now requires live canvases to name #393/#400 and the newest DEPLOYED #N (doc_drift_audit.check_c10_audit_package_r2; all five LIVE_EARNINGS_CANVASES stamped). Docs sweep DEPLOYED Sep 26, 2026 19:05 UTC (deploy #7 a695050). F-3 delete the dead scripts/check_earnings_today.py (queries the non-existent earnings_markets.symbol; stale 70/30 + $2,000 gate; FINDINGS #450 §E). F-6 (candidate) EarningsDetector._check_prnewswire backoff defaults a never-failed feed to "failed at monotonic 0", so the PRN check skips for the first 5 min after a host boot. F-7 / F-8 — DEPLOYED Sep 26, 2026 11:48 UTC (deploy #2 3d7f1b1; FINDINGS #444). F-9 (candidate) ibkr-monitor.service (run_gateway_monitor.py) is not restarted by deploys either; its restart semantics are an operator decision. F-10 (candidate) get_high_conviction_markets()'s 7-day DISTINCT ON over open Earnings markets spills one ~11.3 MB incremental sort per stats pass (≈ 1.2 GB temp writes/hour; 281 ms); a per-market LIMIT 1 lookup would remove the sort. OD17 prod cleanup DONE Sep 26, 2026 11:56 UTC — the three untracked scripts/*btc5m* copies moved to /root/btc5m-od17-moved/ after a sha256 match with strategies/btc5m/tools/. F-4 the stale "Phase 0 stub exits 78" btc5m recorder comment in deploy_from_github.sh (plan OD11, btc5m-only commit). F-5 (→ #474 DEPLOYED Sep 26, 2026 19:05 UTC, deploy #7 a695050) (delta re-review Minor 1, #444) extend deploy/migration_baseline.json + probe_sql beyond column types and the two btc5m id = 1 seed rows to constraints (PK/UNIQUE/CHECK/FK, ~34 in the four bootstrapped files), column defaults (~30), index definitions (15) and every seed key the files insert (btc5m.integrity_rule_catalog rule_ids, the btc5m.live_samples archive rows). Not small: needs a read-only prod catalog capture and normalised comparison of pg_get_constraintdef / column_default / pg_get_indexdef text (sequence and schema-qualified names differ by environment), and a false mismatch would stop the deploy — so it shipped with its own review (#474), not in the Batch A window. History: before #474 a bootstrapped ID proved object existence + column types + those two seed rows only.IMPLEMENTED-PENDING-DEPLOYdeploy/cron/root.crontab is root's crontab: the one Sunday reminder (05:50) and the 04:00 audit, via /bin/bash, both scripts 100755. deploy/install_crontab.py --install runs right after the host gates, before migrations and restarts, as a transaction, and any failure stops the deploy:/var/backups/polymarket/, then an atomic swap, a multiset verify, and an automatic verified restore on failure;monitor_23_45 one-shot is retired only after commit.Pre-flight: deploy/remote_deploy.sh runs the fetched installer's --dry-run before the pull and stops on refusal (OPERATIONS §8.F). Refusal is shell-aware (shlex), the install holds an fcntl lock and re-reads the crontab before committing. Retired: gateway_restart_manager.sh (Gateway PID killer, OD5), ibkr_health_monitor.sh, ibkr_restart_warning.sh, the 05:55 duplicate, --critical, and the dead check_trader_health.sh / exit_reminder.sh lines. cron:schedule_audit (dashboard) runs the audit each cycle.
- Verify: tests/unit/test_finding_476_managed_crontab.py. After deploy: venv/bin/python3 deploy/install_crontab.py --check prints OK; bash scripts/ibkr_schedule_audit.sh PASS.
IMPLEMENTED-PENDING-DEPLOYdeploy/systemd/ibkr-monitor.service (EnvironmentFile, no Gateway coupling), installed + restarted by the deploy; the deploy fails unless a fresh heartbeat appears within 90 s. src/ibkr/gateway_pager.py pages live :4001 once after 180 s (20 min only for the weekly Sun 06:00–06:30 restart with a real 2FA marker, #456), then one RESTORED; it never pages paper and never restarts anything. The monitor is TCP-only (account probe deleted). The heartbeat (0644) attests live_incident {generation, opened_at, deadline, page, classification} and last_resolved {generation, resolved_at, resolve: delivered/owed/dropped} (B2 OD6 cedes RESTORED only on delivered). src/earnings/incident_ledger.py (shared with #449 and B2): claim_page / claim_resolves, the in-memory state authoritative after a failed write plus a /run/polymarket fallback copy, resolve-during-send persisted and owed after 120 s if the sender died, a new outage always a new generation that pages (stale debt deleted), owed debt deleted after 24 h. alerting:ibkr_monitor stands down on a closed port only when the page is attested delivered or not yet due. DeliveryResult.delivered = Telegram HTTP 200. alerting:ibkr_monitor pages if the pager is blind. Healthchecks: one secret HEALTHCHECKS_PING_KEY; slugs earnings-trader / integrity-monitor / prod-host / db-backup; silent when unset.tests/unit/test_finding_448_ibkr_monitor_pager.py, tests/unit/test_finding_448_healthchecks_deadman.py. After deploy: heartbeat /var/log/polymarket/ibkr-monitor-heartbeat.json fresh; alerting:ibkr_monitor ok; no Alert delivery FAILED in journalctl -u ibkr-monitor. After Marius sets the key: config:healthchecks_ping_key ok.IMPLEMENTED-PENDING-DEPLOYdb:read_only CRITICAL; db:sessions_killed (first sample = baseline, review blocker 5); db:disk_plan = all databases + WAL reserve max(max_wal_size, wal_keep_size) vs 120 GiB (blocker 6: Sep 25 = 92.5% FAIL; today ~44%); db:temp_spill, db:long_transaction, storage:btc5m, dashboard:api_reachable, dashboard:db_errors, intraday resolver timer + last run; integrity:meta:db_unreachable when the monitor cannot reach the DB.tests/unit/test_finding_446_db_health.py. After integrity restart: all new rows ok on a healthy cluster, no Telegram.IMPLEMENTED-PENDING-DEPLOY/var/log/polymarket/backup-last-pgdump.err. Log line is a preview plus path and byte count. One retry on read-only/session-kill. Stdout still streamed (#260). The sidecar is best-effort: the child's rc decides, and a sidecar write failure only warns (review Major 5).tests/unit/test_finding_447_backup_stderr.py. After a failed backup the sidecar starts with pg_dump: error:, not pg_dump: er.IMPLEMENTED-PENDING-DEPLOYget_high_conviction_markets no longer DISTINCT ON every 7-day snapshot of every open Earnings market. Per-market JOIN LATERAL … ORDER BY time DESC LIMIT 1, still #445-bounded. Panel filters and LIMIT 15 unchanged.tests/unit/test_finding_471_high_conviction_lateral.py. After dashboard restart: High Conviction panel still ≤15 rows; live EXPLAIN has no incremental sort on this statement.IMPLEMENTED-PENDING-DEPLOYsrc/earnings/ibkr_snapshot.is_stale (cadence × 2), not IBKR_SNAPSHOT_INTERVAL_SEC=10 s. Trader write gate and weekend/holiday loop idle import the same helpers. Sat 15:18 UTC #468 probe (43.4 s, connected: True) was a false disconnect. The trading day is the ET date, as in the trader's loop, so the Sunday-evening UTC rollover stays a 60 s non-trading cadence (review Major 3).tests/unit/test_finding_472_ibkr_snapshot_cadence.py. After dashboard restart: /api/state ibkr_reality.ibkr_connected true while the file is ~40 s old on a weekend.IMPLEMENTED-PENDING-DEPLOYibkr-monitor's pager, every day, page-only, one page per outage. A hang or crash pages after 180 s, including a Sunday-afternoon crash that brings its own 2FA prompt. Only the scheduled weekly restart gets 20 min, on evidence: IBC's Re-starting session inside Sun 06:00–06:30 UTC, a real Second Factor Authentication initiated within 10 min after it, no login since, the outage opened within 10 min of it. A genuine crash in that window pages at 180 s. Latched, so the page says "2FA NOT APPROVED". Pages are information: Detected / Automatic / Fallback lever. The trader's #438 automatic last-resort recycle stays on (operator decision Sat 26 Sep 20:39 UTC; IBKR_GATEWAY_AUTO_RECYCLE default true, false the dated hold): only after the IB() reset failed, #435/#439 guards, never while IBC awaits a 2FA, no DB I/O on the IB owner thread. Every systemctl restart ibgateway-ibc (the trader's last resort or by hand) is a fresh IBKR login and needs a 2FA approval on the operator's phone (#430); only IBC's in-process 06:00 AutoRestart keeps the session (Mon–Sat). The Saturday-only integrity rule is removed.tests/unit/test_finding_456_weekend_autorestart.py. Sat 3 / Sun 4 Oct: login by 06:20 with no Telegram, or exactly one correct page.IMPLEMENTED-PENDING-DEPLOY/var/lib/polymarket/integrity-alert-ledger.json, failed sends retried; 2-cycle hysteresis for systemd:* / unit health / snapshot / dashboard rules; openings deferred in /run/polymarket/deploy-window.json (written by the deploy) and Sun 01:30-03:00 UTC; status writes best-effort (RO still pages); the tripwire reports starts_24h. scripts/send_telegram.sh is exit 0 only on HTTP 200, and its artifact is best-effort (Major 4).tests/unit/test_finding_449_telegram_delivery.py. After deploy: the deploy's own restarts page nothing; telegram-last-send.json ok after the Sunday reminder.IMPLEMENTED-PENDING-DEPLOY on branch reliability/b2_check_prnewswire no longer treats "never failed" as "failed at boot" on the monotonic clock (it skipped PR Newswire for 5 min after every host boot); IBKRPositionCache refreshes when _last_refresh == 0.0 at any uptime. Verify: tests/unit/test_finding_469_monotonic_never_sentinel.py.signals:<bmo|amc>:SYMBOL) with IB owner pumping; stall dump written to logs/stall_dumps/; the #453 dated 300 s override is removed (budget 120 s). Follow-up B2-F2 — closed in review round 2: the three detector reads resolve condition IDs first and read by condition ID in the 7-day lookback under the statement timeout; on failure the crowd witness and the PM price check fall back to live Gamma (whose DB lookup / persist are timed too, round 3), while the extreme-outcome shield returns None; the entry/close reads got the timeout too. Verify: tests/unit/test_finding_470_signal_stall_root_cause.py, tests/unit/test_finding_470_review_timeouts.py.source=ibkr_live, non-stale, populated margin state (Aug 26 placeholder-capital and Sep 8 post-reattach classes silent); one WARNING per breach incident + one resolve via the persisted leverage incident; alert-only. Verify: tests/unit/test_finding_452_leverage_alert_latch.py.src/earnings/alert_policy.py — expected IBKR windows (nightly reset 23:30–01:15 ET, 06:00 UTC AutoRestart, Sunday 2FA) set the page deadline; persisted incidents (logs/alert_incidents.json) give one page + one resolve across the #441 bounce; the lever names ibgateway-ibc when :4001 is down; planned restarts are silent; recon guard pages once after 30 min (5 with a position); integrity skips its trader:ibkr_request_path page only on a delivered trader page of the same incident generation (memory.md reworded). Review round 2: delivered = Telegram HTTP 200 (require_telegram); one ibkr incident per outage; operator actions page through it (≤1 per kind). Review round 3: B1 merged; incidents live in B1's IncidentLedger (alert_policy is a thin adapter); the trader's alert-dispatcher thread sends and retries every page / action / resolve; operator actions use ibkr_operator_action (outside the #413 cooldown); :4001 closed + live ibkr-monitor → the monitor pages, the trader stands down. Runbook OPERATIONS §8.A3. Verify: tests/unit/test_finding_451_trader_alert_policy.py.src/earnings/signal_gates.py is the one conviction-band + depth-gate definition (edges read live from Config.SIGNAL_LONG_THRESHOLD / SIGNAL_SHORT_THRESHOLD / SIGNAL_LONG_MAX_THRESHOLD) for _build_trading_signal, is_valid, priority, ConvictionCalculator and the dashboard; the dashboard fails closed on a missing/one-sided book (PM FALLBACK) and renders 0% conviction; LONG/SHORT edges join GUARD_REGISTRY. Trader identical at prod values (old-logic oracles). B2-F1 closed (review round 2): both surfaces feed the band the tier from src/earnings/market_cap_tier.py (KNOWN_MCAPS → shared 24 h cache logs/market_cap_cache.json → yfinance; unknown = Small), never earnings_markets.market_cap_category; exact 0.0 / 1.0 quotes take the one-sided path on both. Verify: tests/unit/test_finding_450_dashboard_gate_parity.py, tests/unit/test_finding_450_market_cap_and_quote_parity.py.DEPLOYED Sep 13, 2026)(ticker, date); verify + official-calendar fetch 21 days; confirm-in-place writes votes=; NASDAQ+Yahoo pair may rewrite a locked PM date before last-resort CBRL IR (Sep 14→23 BMO); timeline DATE SPLIT when scheduled-exit plan ≠ calendar. Does not move the live CBRL exit. Complements #349/#424.tests/unit/test_finding_442_earnings_date_surfaces.py.DEPLOYED/§B: completed on #367–#408; 13 missing live-behavior changelog keys backfilled (#368 #369 #370 #371 #373 #375 #378 #383 #388 #389 #390 #402 #404); build_revalidation_357() updated= names every token; test_finding_410_ship_verification.py. Record only — no trading-logic change. Dashboard restart after pull; trader not bounced. Closer leftover: #383 changelog + HANDOFF trader SHA defe78f.DEPLOYED Aug 22, 2026 15:23:13 UTC SHA defe78f; #408 DEPLOYED Aug 22, 2026 14:49:44 UTC SHA 4e4ffa1)#N on every R2 surface including build_revalidation_357 + live canvases); C11 (§B: + test_finding_N_*.py or NO TEST:); #408 builder/canvas R2 misses backfilled.PositionManager.persist_ibkr_execution; delete orchestrator persist twin; reroute entry + batch persist. Twin B — __init__ calls exit_mgr.check_missed_scheduled_exits; process_scheduled_actions 'exit' calls exit_mgr.execute_scheduled_exit; delete orchestrator missed-exit / scheduled-exit / finalize / unused verify twins. Leftovers cited: Flex backfill + archive paper_trade_test.ActiveEnterTimestamp=2026-08-22 14:49:44 UTC, NRestarts=0. Server __init__ calls exit_mgr.check_missed_scheduled_exits; no orchestrator persist-twin def. This docs-stamp is a later SHA than the trader process.DEPLOYED Aug 22, 2026 13:39:29 UTC SHA 7fbfe05)finnhub_rate_limiter (40/min, fail-open) on every live Finnhub GET · live yfinance through yfinance_limiter · PRN financial-RSS tripwire (no second cache) · five §F/§H clauses. Changelog integrity entry (live-path throttle, not a trading rule).ActiveEnterTimestamp=2026-08-22 13:39:29 UTC, NRestarts=0. Limiter importable. Dashboard HTTP 200 on :8080. This docs-stamp is a later SHA than the trader process.DEPLOYED Aug 22, 2026 07:53:22 UTC SHA 88014f4)ON-HOLD rows (#406), not markdown-only.strategy_analysis_report.json so the six ON-HOLD rows appear on the Strategy tab.4292bdb / e857f5e).#400 DEPLOYED Aug 21, 2026 23:00:26 UTC)docs/CODE_AUDIT_RECONCILIATION_2026_08.md.pilot_tag persist (#394) · PRN cache + one poller (#395) · cheap get_positions + #369 widen (#396) · aliased exit sets (#397) · batch s2c_split_pct + resize after filter (#398) · recon gate + Polygon default stripped + Jinja timingKnown + priority Config (#399) · #400 get_positions fail-closed (PositionsLookupError on disconnect/exception; honest empty book still []).e857f5e pulled + deploy_from_github.sh; trader ActiveEnterTimestamp 2026-08-21 23:00:26 UTC (do not write “Aug 22 ~23:00 UTC”); NRestarts=0; recon in sync; IBKR connected; 0 positions; 0 breakers; server get_positions raises (no return [] in except); Polygon REST 200 (key masked); integrity 82/0 fail/1 #390 warn; dashboard HTTP 200. This docs-stamp is a later SHA than the trader process — do not claim origin/master == e857f5e.4292bdb at 2026-08-21 21:43:10 UTC (prior stamp “Aug 22 ~21:43 UTC” was WRONG). Disk HEAD after that docs stamp was c9c1d59..env still holds the burned default” was WRONG. Do not write the live key into any repo file.The full PKG-357 package + Phase-1 transition went LIVE per the deploy checklist (IMPLEMENTATION_PLAN_2026_08.md, now marked EXECUTED):
- Window: overnight, 00:28 ET Friday; market closed; trading_positions open = 0 rows; IBKR connected; 0 active breakers; integrity 0 FAIL pre-deploy. Pre-deploy snapshot: /root/deploy_snapshots/pre_366_20260821T042834Z.
- Sequence: migrations 031 (pilot_tag) + 032 (v_blend_score) applied BEFORE any restart (an in-flight read-only analytics query held the trading_positions lock ~4 min and was cancelled to let the ALTER through) → prod .env: PDT_RESERVE_CAPITAL 25000→17450, stale SIGNAL_LONG_MAX_THRESHOLD=0.95 fixed to 0.90 (the checklist's flagged hazard, real), PHASE1_START_ET verified absent (deploy day = default) → deploy_from_github.sh pulled bf16b1c → 3465087, cleared pycache, restarted trader/dashboard/collector/live-trades/websocket, ran the close-persist contract check (PASS) and regenerated the report.
- Verified live: capital $10,054.38 (NetLiq $27,504.38 − 17,450); A-6 day-P&L anchor from DB at startup; no consecutive-loss init; consecutive_losses key gone from /api/state; integrity 57 rules, 0 FAIL (only the pre-existing storage:orderbook_snapshots WARN); b5_pilot_auto_revert OK; DB↔IBKR reconciliation in sync; Phase-1 pill, "Trades: 0", no Test Progress/Levrage, testing frozen at 193, testing+phase1 ≡ alltime; B-4 ranks + 0.90 cap + reservation card rendering; 25000//10/consecutive-loss greps clean on the prod checkout.
- Watch next session: 10:30 ET reservation summary line; 15:50 ET priority=N/3 admission logs; 15:58 equal-split sizing; v_blend_score on new opportunity-log rows; first pilot entry carries pilot_tag + 🧪 badge.
- ~~Outstanding~~ → BOTH CLOSED same day (Aug 21, 2026 06:56 UTC deploy — Findings #369/#370): (1) OPS-4 CLOSED (#369) — the nightly ~04:15 UTC (and ~06:06 UTC) trader deaths were self-inflicted watchdog kills: 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; NOT OOM (148 MB peak), NOT the 04:15 apt neighbor (aggravator only — needrestart bounced the Gateway on library updates; now excluded via /etc/needrestart/conf.d/polymarket.conf). All 15 sites bounded with explicit degrade paths; trader:restart_tripwire integrity rule armed (NRestarts>0 = death outside a deploy → Telegram within one 5-min cycle; baseline 0 as of the 06:50 UTC restart — tonight's 04:13/06:00 windows are the live proof and cannot pass silently). (2) OPS-5 CLOSED (#370) — the failure was three stacked holes: the exit-2 was CORRECT (#251b; 16 real issues: 14 unrecoverable pre-March legacy exit-exec gaps + 2 side false positives from 3 legacy SLD rows, since normalized — documented DB repair), but the unit had no EnvironmentFile (Telegram silently disabled) and was never in BATCH_SERVICES (nobody watched it). Fixed end-to-end: EnvironmentFile on both reconcile units + deploy-managed, exec-linkage epoch (2026-03-07) reports legacy gaps as non-fatal, services:systemd_unit_health dynamic failed-unit sweep (which immediately caught + cleared a 6.5-month ghost polymarket-ibc.service failed state on its first cycle), reconcile-full/flex-reconcile last-run + timer rules. First clean full-history run 06:58 UTC (exit 0, 0 mismatches, 14 named legacy gaps); monthly timer re-armed: next fire Sep 1. Integrity after: 63 rules / 62 OK / 0 FAIL / 1 pre-existing WARN.
The operator closed the "TESTING IN PRODUCTION" era (193 trades) and moved the project to Phase 1 trading with a P&L fresh start, effective at the PKG-357 deploy moment.
- Single-source boundary (#334 lesson): NEW Config.PHASE1_START_ET (env-overridable, default 2026-08-21 ET; Config.phase1_start() the only parser). Every phase surface derives from it; trades split on entry_time, timezone-correct. Deploy-time step: the deployer SETS/VERIFIES PHASE1_START_ET in prod .env if deploying on any day other than Aug 21, 2026 — checklist in IMPLEMENTATION_PLAN_2026_08.md extended (env step + #367 post-deploy probes).
- Dashboard: header pill "TESTING IN PRODUCTION" (yellow) → "Phase 1" (blue badge-phase1); "193/150 trades + progress bar" → "Trades: N" (Phase-1 closed trades from zero, DB COUNT(*) FILTER); Trading Stats "Test Progress" cell REMOVED; System-tab card reworked to "🚀 Phase 1 Trading" (shows the frozen testing-era count); History Trading Capital ROI re-cut to three stacked filter-independent windows — All-time / Testing in Production (start – cutover) / Phase 1 (cutover – ) — via the pure compute_phase_roi_windows + module-level compute_roi_triplet; the meaningless PERIOD day-span ("2,423 days") is gone. "Levrage" typo → "Leverage".
- Records: FINDINGS #367; changelog entry #367 (reporting) with chart marker "TESTING IN PRODUCTION (193 trades) → Phase 1"; STRATEGY.md §4.0 retitled (transition CLOSED-historical, §4.1 naming note); OPERATIONS.md mode/services tables; CONTEXT.md; P1_SURFACES census-drift note; risk_manager.py alert-string fossils updated. Testing-era history (milestones, #347 narrative) stays historical; NO data deleted.
- Tests: NEW tests/unit/test_finding_367_phase1_transition.py — phase-split ROI (synthetic rows both sides of the boundary incl. the ET/UTC edge), counter-from-zero, frozen-count immutability, testing+phase1≡alltime invariant, single-source tripwire, stale-string tripwires (no "Test Progress"/"Levrage"/x-of-150). Full unit suite green (py3.11).
The operator gave PKG-GO and the entire #364 ONE-PACKAGE manifest went in the same day: 9 implementation commits in the planned order + this ship-mechanics commit, each implementation commit carrying its own test_finding_366_* file.
- What changed (headline per item): A-7 affordability admission + greedy backfill (own commit, shared src/earnings/affordability.py helper in all 4 loops) · C-Q1 capital step (PDT_RESERVE_CAPITAL 25000→17450 across every repo encoding incl. the N14 History-ROI hardcode; prod .env at deploy) · S2c equal-split shrink-to-fit ≤25%/name with the buffer-aware $9k activation gate (S2C_MIN_ACTIVATION_CAPITAL; flat-25 below it) · B-4 3-tier table (3 = LONG 70–75 any mcap, 2 = LONG 80–85 large/mid, 1 = rest, 0 = skip; volume bumps + mcap spine deleted; batch multiplier flattened; /10 surfaces swept) · C-1 SIGNAL_LONG_MAX_THRESHOLD → 0.90 + M-C1 shadow tagging · RES M1+M5 tonight-only observed-set reservation (10:30 ET count, breaker-independent per #363) + rank-3 AMC carve-out replacing the deleted ≥0.95 override twins (A-10) · WAVE-RT (deadline min(45+5×(n−6),90)s, per-batch slippage counting, MSP-1 record-only) · CLB-1+A-6 (3/5/7 pauses + sizing-halving + counter DELETED; daily-loss 5% sole brake, DB-derived, persisted, true-ET resume; tiers −2.5%/−4%) · B-5 pilot (own commit; B5_PILOT_ENABLED; ≤2 concurrent; pilot_tag migration 031; auto-revert ENCODED as integrity rule consistency:b5_pilot_auto_revert; K-B5 re-score 25 fills / Dec 15) · M-VOL V-BLEND shadow (src/earnings/v_blend.py, frozen Aug-16 reference populations, migration 032, zero trading impact).
- Ship mechanics: changelog package entry #366 (chart marker) + B-5 entry #366.b5 + supersession flips (#313/#333/#334/#346 SUPERSEDED, #207→#366.b5, #314.f/#335 AMENDED); all dashboard/queue/action-plan/monitor encodings flipped to IMPLEMENTED-PENDING-DEPLOY; FINDINGS #366 + supersession notes on the six affected entries; STRATEGY.md/OPERATIONS.md rewritten in the per-item commits; DEPLOY CHECKLIST added to IMPLEMENTATION_PLAN_2026_08.md (manual prod .env line, migrations 031+032 BEFORE restart, close_persist_contract_check.py on the server, §G probes, market-safe window).
- Monitors: M-C1/K-S2C/K-B4/K-RES/K-B5 IMPLEMENTED — arm at the deploy restart; D-6/D-7/D-9 retire at the deploy restart (the live consecutive-loss breaker keeps running pre-#366 code until then).
The operator saw BABA (entered/traded) displaying $930 against the $1,000 volume gate. Deep-dive verdict: display artifact — BABA's admission volume was $2,145.74 at Tue Aug-19 15:50 ET (opportunity log; gate PASS legitimate); today's $930 is a post-close 24h-ROLLING snapshot substituted into the cumulative slot by the dashboard's last-resort fallback (app.py:2260) after BOTH cumulative sources failed.
- Root causes (3): (1) sync_markets_from_gamma's limit=500 is silently capped at 100 by the API with no pagination (continuous_collector.py:225–238) — BABA fell off the top-100 in the heavy week, volume_total stale at its Aug-16 value; (2) all three Gamma cumulative fetches omit closed=true → closed markets return EMPTY (volume_policy.fetch_gamma_cumulative_single + dashboard batch + per-market); (3) the price-snapshot fallback substitutes 24h-rolling volume into the cumulative display slot with no marker.
- Class audit: 97 trades have admission-time log rows; 93 provably passed the gate; all 43 below-gate-today displays are artifacts (100%); 80% store less volume today than at admission (vs W1's 44% — traded markets evaluate up to resolution). No bypass branches, no unit confusion; PNC/CFG (Apr 14–15) entered under a config-DISABLED gate (by design then).
- Plan change: A-3 EXPANDED into a 5-part bundle (the monotonic guard alone would NOT have prevented this): monotonic GREATEST guard on both writers + closed=true on the three Gamma fetches + sync pagination + admission-time volume display semantics (below-gate-NOW info vs below-gate-AT-DECISION alarm) + mandatory stale/rolling marker on the 24h fallback. Priority stays P2 Class A; the class has cost operator attention twice. Recorded across IMPLEMENTATION_PLAN_2026_08.md (P1_SURFACES has no A-3 reference — verified), the revalidation §II.6/A-002 rows, both dashboard encodings, CONTEXT, this handoff, and the implementation-master-plan canvas. Tests green (python3.11).
Operator clarification verified correct: the account ALREADY holds the money; the "PDT reserve" is blocked in code by us.
- FUND-1 verified + re-specified: IBKR snapshot 19:03:41 UTC / dashboard /api/state 15:05:43 ET both show NetLiquidation $27,465.52, trading capital $2,465.52, reserve 25,000 (prod .env:62, the only operative encoding). Trading capital is computed LIVE as net_liquidation − PDT_RESERVE_CAPITAL at every read (no hardcoded trading-capital constant; nothing counts day trades; nothing moves money). The step: PDT_RESERVE_CAPITAL 25000→17450 → trading capital $10,015.52; PDT floor $25k holds with $2,465.52 margin (equity unaffected by the parameter); S2c gate re-specified buffer-aware ~$9k (satisfied at ship, ≈$1,000 margin). Lockstep: 5 env-default "25000" strings, the app.py:3690 PDT_RESERVE_USD History-ROI hardcode (N14 — env change alone would leave History ROI wrong), 3 fallback literals, 2 Jinja defaults, .env.example, GUARD_REGISTRY add.
- Census-grade re-audit (P1_SURFACES): census 148→161 (§1b NEW: 13 alert/notification surfaces — the original was dashboard-only); §11a thin addendum replaced by full §11 (CLB-1+A-6: 5 dashboard + 2 alert surfaces) and §12 (WAVE-RT: 0 dashboard + 2 alert) inventories; §3 FUND-1 rewritten (10 dashboard + 1 alert + 1 code hardcode); MSP-1 confirmed record-only (0 everywhere). Flags N14–N23; material catches beyond N14/N15: NO consecutive-loss counter renders anywhere today (N17 — §11a's "keeps rendering" was wrong), the Edge-Cases second breaker table (N16), dead CONSECUTIVE_LOSS_LIMIT knob (N19), DAILY_LOSS_LIMIT+tiers must join GUARD_REGISTRY (N20), STRATEGY.md L2050 stale "3 stop-exits" (N21). Prod .env carries NO breaker keys (verified — CLB-1 needs no GUARD-OVERRIDE dance). Manifest ↔ surfaces cross-check clean after the FUND-1/C-Q1 rewrites.
- Execution discipline (operator instruction): binding note added under the manifest — every package item and every Class A commit follows the full memory.md checklist, no exceptions.
Triggered by the morning's 10:03 ET "3 consecutive losses — 4h pause" firing (TGT/LOW/DY — all three from the same Aug-18 evening entry wave; the pause blocked nothing; the day closed +$37.70). The operator commissioned a full breaker/risk-control audit under the P1 package (#363), then approved its verdict and packed everything into one implementation package (#364).
- #363 (analysis + records): package replay (47 wk, §14 harness): the consecutive-loss rule fires 49× with 28 blocking nothing; 70% of 3-loss runs contain ≥2 same-wave losses; money effect is a sign-flip on two outliers (−$823/+$312). The daily-loss 5% brake fires ONCE (the worst day in history), $0 foregone. Full 33-control inventory: 24 SAFE · 6 NEEDS-RETUNE (→ WAVE-RT + CLB-1 riders) · 3 REDESIGN (the counter, its hidden sizing escalation, the reservation-breaker interaction → RES design requirement). New decision CLB-1 (§II.8), monitor D-9 armed, D-4 extended.
- #364 (operator decisions recorded): CLB-1 APPROVED — retire the counter (display-only), daily-loss 5% sole brake (true-ET resume + DB-anchored day P&L), sizing halving re-keyed to daily-loss tiers (−2.5%/−4% proposal), A-6 rides; ships inside PKG-357; breaker LIVE + D-9 armed until then. ONE-PACKAGE manifest confirmed (recorded in IMPLEMENTATION_PLAN_2026_08.md; 4 additions the communicated manifest missed: the C-Q1 capital step, MSP-1, D-6/D-7 retirement-at-ship, changelog/supersession ship mechanics). Pending-decisions register recorded with one correction (Class A queue = per-commit NOW work, not post-package). FUND-1 + the explicit PKG-GO are all that remain before the build starts.
- Sweep per the #361.b lesson: statuses AND description texts flipped together across FINDINGS, both plan docs, the revalidation doc (Part I re-headed Aug 16–19; §II.8 → APPROVED; App §15 annotated), CONTEXT, this handoff, rb_breaker_audit_report.md, build_revalidation_357()/build_action_queue() rows and both canvases; zero remaining "awaiting operator" markers for CLB-1. Tripwire tests updated (+#364 pins); full unit suite green (python3.11).
The operator reported VIK's Active-Trades row (entered "Aug 17, 7:58 PM") contradicting its own timeline plan (Tuesday entry, Wednesday exit). Investigation verdict: yes, a day early — VIK reports Wed Aug-19 BMO (confirmed #349 consensus); the intended entry was Tue Aug-18 15:58 ET; it filled Mon Aug-17 15:58 ET in HD's batch. ("7:58 PM" was a second bug: raw naive-UTC strings re-read as browser-local.)
- Root cause (4 links): stale Polymarket end_time (Tue) pulled VIK into Monday's window via the #333 OR-branch → resolve_schedule_basis correctly built a TUESDAY-entry signal → no guard checks the entry DATE (#359 passes future entries trivially) → run_bmo_cycle scheduled the whole batch at signals[0] (HD's Monday) time.
- Class frequency (193 trades): 4 genuine wrong-date entries — CRSP Feb-10 (−$6.04), SBUX Jul-27 (+$8.91 by luck), SBUX Jul-28 (−$0.06, the #349 born-dead), VIK (open). Net realized +$2.81 = luck, not design. ON Feb-9 / NET Feb-10 are NOT wrong entries — post-hoc DB date drift (measurement caveat added to PLAYBOOK §5; rows deliberately NOT repaired).
- Shipped (all six fixes): (1) entry_is_for_today guard (wrong_entry_date) at all 4 admission loops before can_open_position + _filter_batch_signals_for_today at batch scheduling (the shared-entry-time assumption is DEAD) + last-line refusal in both batch executors; (2) schedule_within_window re-check after resolve_schedule_basis (schedule_outside_window) — a market resolved outside its selecting window is re-picked on its true day; (3) integrity rule consistency:open_position_release_window (HIGH): open positions' entry day + implied release day re-checked daily against the CURRENT confirmed date; KNOWN_WRONG_ENTRY_POSITIONS allowlists ("VIK", Aug-17) as OK-with-annotation; (4) dedupe_reservation_signals in both multi-day BMO reservation builders (VIK was double-counted under Aug-18 AND Aug-19); (5) dashboard: positions API sends ET times (_format_trade_time_et, §M), JS renders verbatim + "ET" suffix (no browser-local reparse), red WRONG-ENTRY-DAY badge + highlighted entry cell when actual entry day ≠ plan; (6) date-drift assessment only, no data repair.
- VIK guard statement: tonight's Tue cycle re-selects VIK (in-window, correct entry day → new guards pass it) and can_open_position rejects it as an existing position — NOT re-entered, NOT force-exited; Wednesday 10:32 ET exit + live stop untouched.
- Verification: NEW tests/unit/test_finding_362_wrong_entry_date.py (44 tests: invariant matrix, window semantics, dedup, admission/scheduling/execution tripwires, integrity shapes incl. VIK/HD, dashboard pins); 1058 unit tests green (python3.11; was 1014).
Operator: "why is this not updated? what else did you miss?" (the #334 missed-surfaces lesson repeating). Two root causes, both fixed:
- #361 was never deployed — the live server still ran the pre-#361 build, so its regenerated strategy_analysis_report.json still carried C-1/RES/B-5 as PENDING-OPERATOR-APPROVAL and the strategy tab rendered the stale [PENDING-OPERATOR-APPROVAL] badges the operator screenshotted. Fixed: deployed (per §H) and verified the live report/tab renders the APPROVED state.
- #361's records sweep missed surfaces (its own repo-side text was mostly correct, but not everywhere): docs/CONTEXT.md had NO #359/#360/#361 entries at all (bootstrap doc — a future agent would bootstrap into the pre-decision world); docs/STRATEGY_PLAYBOOK.md's READ-THIS-FIRST banner still listed C-1/RES/B-5 as "OPEN operator decisions"; docs/STRATEGY_REVALIDATION_2026_08.md §10 + the canvas still said "pending deploy / the deploy is yours" for #358 (deployed Aug 16 20:22 UTC — a b7a162c miss); the canvas §11 table labeled C-1 "pending"; STRATEGY.md carried no pointer that the live 0.95 cap has an approved re-close; stale sizing-up/relax-202-9095 PENDING markers in the CONTEXT/HANDOFF historical accretions lacked resolution pointers. All fixed in this commit; every remaining PENDING-OPERATOR-APPROVAL match is either live code vocabulary, the genuinely-open C-Q2/C-Q3, or explicitly-framed historical narrative (full ledger in FINDINGS #361.b).
- Tripwire (can't recur): test_finding_357_revalidation.py gained a status-vs-description coupling test class — for EVERY build_revalidation_357() decision row and EVERY build_action_queue() item, a pending/approval-needed marker in the row's TEXT is only allowed when the row's canonical status IS PENDING-OPERATOR-APPROVAL; plus template-source assertions that no line ties C-1/RES/B-5 identifiers to a pending marker. Statuses and descriptions can no longer diverge silently.
Operator decision (Aug 17): APPROVE all three open Part II decisions from the #357 program — C-1 (re-close the LONG conviction cap at 0.90; M-C1 arms at ship), RES (the M1+M5 reservation mechanism: observed-set reservation + rank-3 AMC carve-out, multi-day component dropped; K-RES arms at ship), B-5 (the small-cap LONG 0.80–0.95 pre-registered pilot; K-B5 contract, §12.5 auto-revert terms unchanged). Implementation explicitly does NOT start yet.
- New status vocabulary (#360 extension): APPROVED-AWAITING-IMPLEMENTATION — distinct from PENDING-OPERATOR-APPROVAL (no decision), DECIDED-NOT-IMPLEMENTED (the Aug-16 Part I decisions) and IMPLEMENTED (shipped). Wired into all four dashboard.html parallel encodings (AQ_STATUS_META / filter checkbox / summary chip s.approved_awaiting_implementation / sort order) + decStatusColor (checked BEFORE the IMPLEMENTED substring so it can't render green).
- docs/STRATEGY_REVALIDATION_2026_08.md: revision table + reading guide, Part I §I.2 decided-table (3 new rows), §I.3, Part II heading + §II.1/§II.2/§II.3 headings and queue rows → APPROVED Aug 17; §II.7 M-C1/K-B5/K-RES parentheticals; Part III B-005/B-006/E-008 action cells + retired-table rows; §13 header note. NEW §II.2.1 — the operator-requested plain-language reservation explainer (verbatim-substance adaptation of the approved text).
- build_revalidation_357(): C-1/RES/B-5 decision-record entries → APPROVED-AWAITING-IMPLEMENTATION (Aug 17, 2026); updated/ship_note extended; action_plan FINALIZED — finalized banner + decide_now → decisions_done (all three approvals recorded); implementation queue re-sequenced (deploy step DONE → capital/reserve step → the package on the operator's EXPLICIT go, arming K-S2C/K-B4/K-RES/M-C1(+K-B5) → Class A fixes, A-7 first).
- build_action_queue(): reservation-m1m5-357 + smallcap-long-b5-357 → APPROVED-AWAITING-IMPLEMENTATION (+ summary count approved_awaiting_implementation); relax-202-9095 title/next_action → C-1 APPROVED, re-close awaiting implementation; package-10k-s2c-b4-357 SEQUENCE updated (decision set COMPLETE). NEW optional plain_language field on the RES row, rendered as its own block by _renderAQItem.
- No strategy_changelog entries (nothing ships — #338 convention). No deploy: the records ride the next operator-triggered deploy.
- Tests: tests/unit/test_finding_357_revalidation.py extended for the new status/plan/plain-language encodings; full unit suite green (python3.11).
Operator instruction: fully incorporate today's work into the dashboard's Strategy Analysis tab in the established disciplined format, reconciling every old open/postponed item so nothing is lost or duplicated.
- build_revalidation_357() now carries the Part I/II decision record (statuses DECIDED-NOT-IMPLEMENTED / APPROVED-IMPLEMENTED (#359…) / IMPLEMENTED (#358…) / PENDING-OPERATOR-APPROVAL / SUPERSEDED), the pdt_constraint red callout, the action_plan (decide_now / implementation_queue / future_work) and the 16-row monitors table mirroring §II.7. Verdict chips updated to 51/9/5/10/5.
- build_action_queue() reconciled (every disposition explicit — see FINDINGS #360 item 4 for the full ledger) + 4 new rows: the rank-1 package-10k-s2c-b4-357 (new status DECIDED-NOT-IMPLEMENTED — decisions vs implementations distinct per the #338 convention; changelog entries at SHIP time), rank-2 class-a-fixes-357 (A-7 first), reservation-m1m5-357 + smallcap-long-b5-357 (pending sign-off).
- dashboard.html: new status wired into all four parallel encodings (meta/filter/summary-chip/sort — the #334 tripwire); renderer extended with PDT callout + plan lists + monitor table.
- Tests: tests/unit/test_finding_357_revalidation.py 17→25, all green; full unit suite green.
- DEPLOYED Aug 16, 2026 20:22 UTC — shipped with #358+#359 in a single deploy_from_github.sh run; the deploy's Phase ε.1 regen rebuilt strategy_analysis_report.json in the same run (no timer wait). Verified live: pdt_constraint + action_plan + 16-row monitors in the served report; package-10k-s2c-b4-357 rank-1 DECIDED-NOT-IMPLEMENTED row present.
Operator instruction: "re-do the strategy revalidation 357 considering all above points; also structure all findings and proposed fixes cleanly." The document (grown by accretion: original §0–§6 + eight appended deep dives §7–§14) was re-issued under the same filename; no analysis content deleted or compressed.
- New structure: Part I decision record (the decided items with evidence one-liners + implementation status; the PDT ≥$25k-equity constraint as the READ-THIS-FIRST callout §I.1; the #358/#359 items (deployed Aug 16, 2026 20:22 UTC); the former §6 discipline notes as §I.4) · Part II open decisions (C-1 recommended CLOSE, M1+M5 reservation, B-5 pilot, dispositions, C-Q2/C-Q3, the 14-row Class A queue with A-4/A-8 marked done, and §II.7 = every armed monitor/trigger/contract in ONE table: KILL, D-1…D-8, M-C1, M-VOL, K-S2C, K-B4, K-B5, K-RES, G-RS) · Part III the register, updated through every deep dive (B-006 HOLDS→WEAKENED per §12 → totals 51/9/5/10/5) + an explicit "retired and superseded" table · Part IV appendices §0–§3 and §7–§14 verbatim, original section numbers deliberately preserved so existing citations ("§14.6", "§5 Class D-1", "§4 B-005") still resolve (header carries the old→new map).
- Open decision list for the operator: C-1 (close the 0.90–0.95 LONG band) · the M1+M5 reservation mechanism · B-5 (small-cap LONG 0.80–0.95 pilot) · the volatility/VIX/small-cap-SHORT standing dispositions · C-Q2/C-Q3 · the 12 remaining Class A fixes (A-7 first: S2c prerequisite).
- Canvas strategy-revalidation-357.canvas.tsx rebuilt to mirror the parts (PDT callout on top; all existing charts/tables preserved, reordered into the appendix library).
The operator approved B-3 from the #357 revalidation queue (docs/STRATEGY_REVALIDATION_2026_08.md, originally §5 — in the re-issued edition the decided B-3 row lives in Part I §I.2). Implemented as Finding #359.
- The gap (W6 §5.2): the #349 born-dead class was guarded at signal BUILD/entry (exit_schedule_is_actionable) but not at ADMISSION — the stale SBUX leftover (Jul-28) consumed an admission slot (displacing HUM), entered, and died 15s later. ~1 per 4 weeks observed.
- Fix: NEW pure guard entry_schedule_is_admissible (earnings_date_verifier.py) rejects signals whose intended entry_datetime is MORE than Config.STALE_SIGNAL_MAX_ENTRY_LATE_MINUTES (=5) minutes past, wired into all 4 admission loops (orchestrator check_bmo_signals/check_amc_signals + capital_manager §E twins) BEFORE can_open_position. Live-path rejections land in polymarket_opportunity_log (skip_stage admission, reason born_dead_schedule). Knob GUARD_REGISTRY-policed (#356). Aware-ET comparison; prior-evening BMO admissions (entries hours ahead) pass untouched.
- Rode along (same code path): W6 Bug B / #357 queue A-4 — _log_exposure_skip passed entry_datetime where earnings_datetime belongs (skip rows back-shifted a day on BMO). Fixed in both spots; the A-4 queue row was deliberately left to its owner (sibling-concurrency rule), FINDINGS #359 is the canonical record.
- Verification: NEW tests/unit/test_finding_359_stale_signal_guard.py (19 tests); 995 unit tests green (python3.11).
- Deploy: DEPLOYED Aug 16, 2026 20:22 UTC — single operator-triggered deploy_from_github.sh run at efebe45 with #358 + #360. Verified: trader restarted cleanly (NRestarts=0), knob live at 5.0, in GUARD_REGISTRY, integrity clean, zero guard firings post-deploy (market closed — no misfires).
Operator follow-up on #357/W7's "cannot evaluate G-002/3/4, fix A-8 first" verdicts: did we examine the WHOLE detection logic? Answer: W7's framing was a shortcut — beneath the real density collapse sat a data-corruption bug and a detector-side staleness bug.
- Root causes: (1) sync_markets_from_gamma never closes markets that leave Gamma's unarchived window → 2,093 'open' rows, 1,288 dead → collection cycle 5–9 min → per-market cadence = cycle time (W7's 49–117 prints/day). (2) WS collector froze its token list at process start (new markets never subscribed) and lived OUTSIDE the deploy pipeline (unit server-only, not in the restart list). (3) WS writer stored raw order-book LEVEL prices of EITHER token (NO uninverted) as yes_price — only 0.7% of Jul-14's 86,671 WS rows match the true bid/ask mid; plus a dedent bug dropped all but the last entry per batch. (4) Detector: _check_polymarket_price used an unwindowed median-of-5 (the #351 Hyatt geometry on the PRIMARY path).
- Fixes: stale-open reaper (3-day cutoff, STALE_OPEN_REAP_DAYS); WS token refresh on every (re)connect + 15-min set-change watcher; extract_yes_quotes (YES-oriented bid/ask midpoints, NO books inverted, full batch, dedup); YES-only book persistence; NEW deploy/systemd/polymarket-websocket.service + deploy install/restart; detector fresh-window median (select_fresh_median, 900s/3-print knobs shared with the #351 witness) + live-Gamma fallback with persist=True (fallback reads land in price_snapshots, signature: all four extra columns NULL).
- Historical caveat (IMPORTANT for analyses): WS-signature rows (volume_24h/liquidity NULL) are order-level prints, not market prices — unfixable post-hoc (orientation unrecorded). Exclude them; re-check dense-market (JPM/WFC Jul-14 class) adjudications as part of A-1. #336/#339's "noise" characterizations were largely this bug.
- Verification: NEW tests/unit/test_finding_358_print_density.py (14 tests); 976 unit tests green (python3.11).
- Deploy: DEPLOYED Aug 16, 2026 20:22 UTC. First reaper run closed exactly 1,288 stale rows (open markets 2,093 → 805); WS service got its first repo-managed restart, resubscribed 1,386 tokens and connected; per-market print cadence ~291s pre → ~105s in the first 10 min post (trending down as the 3-day grace window drains).
Completes the Phase-3-dependent half of Phase 4 (STRATEGY_ANALYSIS_ARCHITECTURE.md §0.7/§0.11.2), un-gated now that Phase 3 (PR 1-5) is complete. Pure frontend (earnings_dashboard/templates/dashboard.html) — zero trading/capital impact.
- Why: the Strategy tab was fragmented across 23 sections on 3 different populations (live-traded / universe / Q3-2025-archive), none canonical → operator confusion. Phase 3's simulator is now the canonical engine.
- Shipped: NEW 🧪 Unified Simulator umbrella over its arms (live stop-loss = Finding-A; capacity-aware = Phase 3 Capacity/Gate-Retune sweeps; selection-rule research = static regime #318 + depth #319 KEEP cards; universe signal-quality = Universe View folded). Archived Q3-2025 Backtest + BMO/AMC off the tab. Consolidated 5 breakdowns → 2 canonical (removed Live Performance Snapshot). Section Index regrouped; phase banner → "Phase 3 + Phase 4 COMPLETE".
- Operator AskQuestion: unify-and-preserve (not literal delete-all-3, which loses unique live-stop-loss + universe-PM-accuracy evidence); static regime/depth cards (not live sweeps — KEEP decisions, depth needs Polygon ~40s); include the breakdowns consolidation.
- Honest note: a true Universe View data-refresh is a separate ~4h Polygon job (analyze_universe_view.py) — folded + labeled as the Apr-25 accuracy baseline rather than silently burning quota; current-universe P&L is already covered live by the Capacity-Aware arm.
- Verification: both inline <script> blocks pass node --check; Jinja parses; div balance preserved (HEAD −1 / working −1, pre-existing); removed DOM ids referenced only by 3 now-dead never-called render fns (documented; safe follow-up delete).
- Deploy: dashboard service restart only; NO trader restart, NO migration, NO integrity change.
- Phase status: Phase 3 + Phase 4 COMPLETE. Remaining: Phase 5 (re-baseline STRATEGY.md §9 ROI projections vs simulator-derived) + monitor shipped changes (#311 $1k gate, sizing-up #314.c, SKIP-rule #314.e) at 30+ live trades.
PR 5 of the Phase 3 simulator (design §4.3 / §12). Answers the operator's first-principles question: the depth/spread gate has no execution role (we trade equities on IBKR; Polymarket is signal-only) and is ~98% redundant — has it ever saved us from a bad trade?
- Why widen: depth-stage logging only began 2026-04-14 (collapse period) → ~4 unique tradeable kills. The first look (n=4) showed it had blocked 0 losers and only killed winners → looked removable. Small-sample mirage. 659 healthy-period (Oct 2025–Mar 2026) markets were never depth-logged.
- What was built (offline-only): scripts/phase3_simulator/depth_gate_variants.py (pure-Python decompose into min-depth/spread/asymmetry factors + remove_all/carveout_real_trade [#283 hyp-#3]/spread_gate_only/depth_gate_only variants; thresholds imported from ConvictionCalculator per §M) + replay_depth_gate_v2.py (replays the REAL conviction code point-in-time over the full 915-market universe; binding-kill filter = gate fired AND clears volume ≥$1k + #202 ≥0.90/small-≥0.80 + #207 small-cap-SHORT [depth fires BEFORE the volume gate]; recovers raw orderbook from orderbook_snapshots + reliability_reason; counterfactual equity P&L via run_counterfactual_for_row, Polygon 1m, mcap stop; per-market, no repeat-eval inflation).
- Result (22 binding kills, all usable): 13 losers BLOCKED (−$142.56 avoided), 9 winners KILLED (+$165.09), net removal +$22.54 over ~7 months (≈ +$1/trade = noise). So YES it has saved us from bad trades (reversing n=4). Kill cause: 18/22 snapshot_reliability (hollow 1¢/99¢ book → conviction fell back to price-snapshots that were too thin/volatile: insufficient_samples:1<3 / unstable_window:range>0.35), 1 pure spread, 3 no-orderbook → a low-information-signal filter, not the pure spread gate #283 implied. Variants: remove_all +$22.54 / carveout_real_trade +$22.78 (≈ identical — 21/22 price-snapshot-derived).
- DECISION: operator-confirmed KEEP. Removal/carve-out is +$22.54 over 7 months (noise, one trade swings it) while ADDING variance by admitting single-sample / 40–90-pt-unstable convictions — the low-EV/high-variance proliferation the strategy avoids. Production conviction_calculator/signal_generator depth check UNCHANGED. Closes Finding #283 / depth-gate-v2 Action Queue row as KEPT.
- Process note: flipped twice (n=4 "remove, only costs us" → conviction-source "the big win was a lucky hollow-midpoint guess, lean carve-out" → widened n=22 "break-even low-info filter, KEEP"). The widening was decisive; the small-sample answer would have been wrong. n=22 still below §K ideal → research-grade KEEP, not gauntlet-PASS (design §12 PR 5 acceptance).
- Tests: 368/368 phase3_simulator (+20) + 22 risk_manager + 5 config = 395 PASS. NO integrity change, NO migration, NO deploy.
- Phase 3 milestone: #283 was the last open deferred-bundle item (#278 closed by #318, #279 by #311-#317, #283 by #319). The Phase 3 strategy-simulator project (PR 1 foundation → PR 5 depth-gate) is COMPLETE. Remaining forward work is monitoring shipped changes (#311 gate, sizing-up defer-monitor, SKIP-rule candidates) at 30+ live trades.
PR 4 of the Phase 3 simulator (design §4.1 / §12). Tests whether a FASTER downtrend detector improves Rule #1 (Skip LONGs in downtrend, #304) vs the production compound classifier (median 31-trading-day lag from SPY peak — Finding #278).
- What was built (offline-only): scripts/phase3_simulator/regime_classifiers.py — pure-Python (no pandas/yfinance/DB) point-in-time SpyDailyHistory (MA20/50/200 + EMA20 + 60d-high; prior-completed-bar lookup enforcing the design §11 forward-look-bias mandate) + 6 classifiers (compound_v1 baseline mirroring market_regime_provider._classify; compound_or_dd5 = #278 option B; below_ema20; below_ma50; dd_3pct; compound_symmetric = adds close<MA200 fixing the #278 false-positive asymmetry) + build_regime_variants (Rule #1 skip predicate per classifier; sizing/gate held at production). replay_regime_v2.py — fetches SPY daily bars (yfinance), runs both universes via the existing run_sizing_sweep/apply_sizing_gauntlet engines (candidate-vs-compound_v1 union-delta; scale_pnl_with_size off-equivalent since sizing unchanged).
- Design tensions (operator AskQuestion): classifier set = equity family of 6 (VIX-term-structure deferred to Phase 4 per design §15-Q5); replay universe = BOTH (live-only primary + capacity-aware 1.40 cross-check); point-in-time = prior completed daily close (agent decision); gauntlet-floor = full-population spillover is the primary decider (agent decision).
- Result (prod, 568-signal universe; SPY 450 daily bars): the fastest detector below_ema20 gains +$70.31 / PASS on live-only (maxDD −$157.66→−$114.92, 5p −$68.47→−$31.51, WR 38.2%→41.0%, downtrend-LONG cell n=44 / −$159 / ex-top-2 −$277) but −$17.02 / FAIL-EX-TOP-2 on the capacity-aware universe at the live 1.40 cap (headline −$5.6/100, ex-top-2 flips +$35; maxDD identical). It labels ~40% of LONGs "downtrend" (vs compound's 17%) → whipsaws out winners on the broader universe, so the live-only win doesn't generalize. compound_or_dd5 (+$0 / +$13.58) + below_ma50 inert/non-robust; dd_3pct + compound_symmetric FAIL. The only durable benefit across both universes is a better worst-week tail (5p −$98.60→−$57.08) at ~breakeven P&L.
- Methodology validation: point-in-time (prior-close) compound_v1 reproduces the LIVE recorded regime_at_entry on 95.3% of 106 LONG trades (17 PIT downtrends ⊂ 22 recorded; the gap is the live intraday 3:58 PM read vs the prior completed close — conservative, look-ahead-free).
- DECISION: operator-confirmed KEEP compound_v1. Rule #1 optimizes for ROI and faster detection adds no robust ROI (it's a wash on the full opportunity set + whipsaws good LONGs). Also #278 re-test trigger (≥30 live Rule #1 firings) is unmet (~0 since May 28 in the low-volume regime). Closes Finding #278 / regime-detection-v2 Action Queue row as KEPT. Production market_regime_provider.py UNCHANGED.
- Tests: 348/348 phase3_simulator (+22: 18 classifier + 4 replay incl. forward-look-bias + #278-lag-reproduction synthetic) + 22 risk_manager + 5 config = 375 PASS. NO integrity change, NO migration, NO deploy.
Operator high-level review: "do we need rare vs busy? are we overcomplicating? maximize ROI not complex rules." Answer + ship:
- Change: RiskManager.OPPORTUNITY_EXPOSURE_LIMITS rare tier 60% → 140% (= MAX_DAILY_EXPOSURE); all tiers now UNIFORM; removed _RARE_EXPOSURE_RATIO; get_adjusted_max_exposure docstring updated. 3 buckets kept only for observability logging labels.
- Why: Finding #316 showed the rare 60% cap was near-inert (bound 2% of rare days, ~1 trade/36wk); normal==busy already. Simplification, ~zero ROI cost.
- Strategic framing (the deeper answer): "more opportunities" ≠ "more ROI" — looser caps / lower gate / smaller sizing all DILUTE ROI (admit low-EV marginal trades, per #314.c/#315/#314.d). The ROI lever is CONCENTRATION (sizing-up #314.c gauntlet-PASS +$4.6/wk at the live cap), not proliferation. #317 is cleanup, not a ROI move; sizing-up stays defer-and-monitor.
- Production behavior: rare-day (1-2 opps) cap rises 60% → 140% (affects ~2% of rare days). Normal/busy unchanged. Operator-CONFIRMED + deployed (earnings-trader restarted — class attr is import-time).
- Encoding incident: StrReplace corrupted risk_manager.py UTF-8 mid-edit (re-wrote em-dashes/arrows as cp1252; caught when the new test imported the module). Reverted via git checkout + re-applied with a UTF-8-safe Python patch (ASCII-only text). Verified clean before commit. Lesson: avoid StrReplace on files with existing non-ASCII; use a utf-8 read/replace/write script.
- Tests: NEW TestUniformExposureCapFinding317; 22/22 test_risk_manager.py PASS.
MAX_TOTAL_EXPOSURE_PCT re-aligned 2.00→1.40 in config.py (unwired var, ZERO live effect) + comment marking it vestigial; #313's cap-raise re-scoped as inert in FINDINGS #313 + PLAYBOOK (priority-0-8 + NYSE parts of #313 remain validated). Commit d1469f5.scripts/phase3_simulator/replay_exposure_tiering.py (per-day entry-budget model, faithful to #285 greedy-fill; emits CapacitySimResult for gauntlet reuse). Prod (117 trading days): rare-day 60% cap is NON-binding (1/49 = 2.0%, $151 foregone) → operator's "most days rare → 60% over-constrains" premise REFUTED. Real binding = busy-day 140% (48.4%, $41k foregone — risk ceiling working as designed). Loosening rare cap (uniform-140 / raised-rare-100) admits +1 net-loser, total +$926→+$907, both FAIL-EX-TOP-2. DECISION: KEEP 60/140/140. 9 NEW tests; 326/326 PASS. Commit 9df31fa. NO production change.Priority 5 commit 1 (action-queue rows #314.b-e + #315 + Live Config exposure→1.40) SHIPPED + verified live (commit 76dc553). Commit 2 is DEFERRED to a focused next session per operator decision (May 31). Plan locked:
- Approach (operator chose A): scripts/strategy_performance_analysis.py should RUN the capacity sweeps at exposure_cap=1.40 during report build (deterministic, always live-accurate; ~10-15s added). The sweep JSONs are regenerated-per-run + not committed, so this avoids stale pinned artifacts.
- Scope: new dashboard.html section "🧪 Phase 3 — Capacity-Aware Sweeps" (renders #314.b/c/d/e + #315 at the live 1.40 cap) + JS render fn (mirror renderPhase3SimGateRetune pattern) + a build_phase3_sim_capacity() helper in strategy_performance_analysis.py; mark the existing Phase γ+δ section "superseded (pre-capacity, optimistic +$21/wk)"; re-rank the Top-5 cockpit on the corrected live-cap lift (NOT the +$21/wk projection — the $1k-gate lift is +$6.61/wk at the live cap per #314.b re-run).
- Live-cap numbers to surface (all re-run May 31 at exposure_cap=1.40): #314.b $1k gate +$15.61/wk (Δ +$6.61 vs $2k); #314.c flat 25/25/25 +$4.18/wk PASS + 35/25/18 +$4.63/wk PASS (sizing-UP ship-eligible, defer-monitor); #314.d hybrid −$5.24/wk FAIL; #314.e skip-≥$10k +$2.53/wk PASS + combined +$4.28/wk PASS.
Morning baseline be60109 carried 4 operator-approved follow-up findings (#289.a, #289.b, #290, #291) queued in docs/FINDINGS.md from the previous session's Dome→Polymarket migration handoff. By end of day:
| Time (UTC) | Commit | Finding | Summary |
|---|---|---|---|
| ~07:00 | 785b5f1 |
— | .cursor/memory.md NO-SHORTCUTS cross-link in §A and §G checklists (dangling diff from session-start git_status) |
| ~07:30 | a27df6e |
#292 + #288.v3 | Cold-bootstrap of polymarket-trades-backfill.service (operator-decision-D) ran 29-min wall, 152,101 trades saved with new polymarket_data_api_backfill source label. §G probe surfaced two real findings: #292 Polymarket data-api /trades offset hard-cap at 3000 (_OffsetCapReached sentinel + truncated flag in backfill_market's 5-tuple return — graceful end-of-pagination); #288.v3 5-label trade_dedupe.source doc correction (previously-undocumented 'backfill' label with 2.17M rows from Jan 31 bulk import). Timer enabled. |
| ~08:00 | 2053086 |
#290 | Watchdog dead-code deletion — 5 files (watchdog{,_v3}.py + 3 deploy/.service files; 5th caught by §E re-grep) + 7 stale-reference fixes + 4 historical-context forward-pointer annotations. Decommission loop in deploy_from_github.sh:127-128 still idempotent. |
| ~08:30 | eb11136 |
#291 (RE-SCOPED) | Long-running-no-op service defense. Original spec recommended 3 progress:* rules, but live-system analysis showed the named daemons are SOLE writers covered by existing freshness rules. Surfaced via AskQuestion (NO-SHORTCUTS rule); operator chose option (B) state-file mtime defense. New artifact:trades_backfill_state rule + 40-line context comment + deploy/deploy_from_github.sh services-array CHECKLIST + new DATA_INTEGRITY.md §2.5. |
| ~08:45 | ebbd953 |
#291.v2 | Doc-count drift correction (34/35 → 37 rules across 6 surfaces — systemd count was stale at 17 vs actual 19 from #269's trades-backfill timer + last_run additions). |
| ~09:00 | abf544d |
#289.a | api_health_checker silent-failure → systemd-timer migration (operator option B via AskQuestion). Root cause: /bin/sh is dash on Ubuntu, source is bashism; chained && short-circuited; bug existed silently for 14 days. New polymarket-api-health.{service,timer} with bash -c 'source .env && ...' pattern + 3 new integrity rules. §E bonus: same dash-bug also affected sync_earnings_markets.py cron — fixed via SHELL=/bin/bash at top of root crontab (covers both bugs going forward). |
| ~09:30 | 6d634b8 |
#289.b | live_trades_collector pg8000 InterfaceError cascade (~118k errors over 10 days in log file, NOT journal as original Finding entry incorrectly claimed). Root cause: try/except INSIDE with engine.begin() block → inner SQL exception swallowed at DEBUG → connection in failed-tx state → COMMIT fails → cascade. PLUS failed trade_id never added to _seen_trade_ids so SAME bad trade re-attempted every 5s. Fix: try/with INVERSION + cascade closure via _seen_trade_ids.add on both success AND error paths + WARNING-level visibility. §G probe: 1 InterfaceError post-restart (was ~4/min pre-fix); 3 distinct WARNING entries surfaced the underlying VARCHAR(10) data-quality issue → became #294. |
| ~09:45 | c717f53 |
#289.b §G | Post-deploy §G probe close-out + new follow-up findings surfaced for next batch (#293 Polymarket Website 404, #294 VARCHAR(10) overflow, #295 backfill_trades.py same-pattern). |
| ~12:00 | 4da2a1a |
#294 | trades.outcome widened VARCHAR(10) → VARCHAR(64), trades.side 10 → 20 (operator chose option B "defense-in-depth" via AskQuestion). Migration 028 + ORM database.py lockstep. Hypertable-safe (PostgreSQL VARCHAR widening is non-rewriting; 907 chunks; ~3min total ALTER time on prod). §G probe clean. |
| ~12:30 | e62f10a |
#293 | Polymarket Website URL renamed /finance/earnings → /finance (cached 404 with age 18h+ confirmed long-term). Operator chose option (C) via AskQuestion: fixed api_health_checker.py URL + deleted dead EarningsMarketSyncer class (12 KB; zero callers besides retry test) + removed silently-broken cron entry on prod. Wrapper scripts/sync_earnings_markets.py preserved per Finding #220 manual-sync workflow (uses healthy run_full_sync Gamma path). §G probe: api_health Result=success, polymarket_website status=healthy, 94 finance_markets — daily Telegram noise eliminated. |
| ~13:00 | 9eee672 |
#295 | backfill_trades.py:save_trades_batch SAVEPOINT-per-trade fix. Operator chose option (A) via AskQuestion: with conn.begin_nested(): per trade matches existing convention in continuous_collector.py:534 + trading_orchestrator.py:1641. Failed trades isolated; outer batch COMMIT succeeds for all valid trades (pre-fix: single bad trade silently rolled back ENTIRE batch). Plus per-trade WARNING logging (was: only first error per batch at ERROR) + failed counter + summary line. Return signature unchanged so all 4 callers work. §G probe: 432/432 markets succeeded, +7 trades, 0 SAVEPOINT-rollback WARNINGs (consistent with #294 already removing today's trigger). |
| ~14:00 | (manual) | — | IBKR Sunday 2FA reconnect. Initial state: ibgateway-ibc.service shown as active but Java window had exited (IBC returned exit status 143 + Gateway finished); port 4001 not listening. systemctl restart ibgateway-ibc spawned fresh Gateway → 2FA prompt appeared → operator approved (likely via IBKR mobile push — "Login has completed" 11s after prompt). Port 4001 came up at 10:00:32 UTC; trader auto-reconnected at 10:01:33 UTC (90s after gateway restart); snapshot refreshed at 10:01:42 with connected: True, error: None. Account U2…13, no positions (correct for Sunday). Integrity-monitor flipped 2 last non-OK rules → all 40/40 OK. |
All 4 morning-baseline items shipped + 3 newly-discovered side-effect items shipped + IBKR Sunday 2FA handled. Project is in steady-state observation mode.
AskQuestion)In line with the morning's NO-SHORTCUTS codification, three design tensions were surfaced explicitly to the operator via the AskQuestion tool rather than silently interpreted:
1. #291 RE-SCOPE: original spec recommended progress:* rules; live analysis showed those would duplicate existing freshness coverage. Operator chose (B) state-file mtime defense.
2. #289.a fix path: crontab patch (A) vs full systemd-timer migration (B) vs hybrid (C). Operator chose (B); my recommendation came with 5-point reasoning weighted by long-term observability.
3. #293 fix path: URL fix only (A) vs check-flip-to-warn (B) vs URL fix + delete dead code (C). Operator chose (C).
4. #294 fix path: outcome only (A) vs widen both columns (B) vs input truncation (C). Operator chose (B).
5. #295 fix path: SAVEPOINT-per-trade (A) vs one-tx-per-trade (B) vs defer (C). Operator chose (A).
In every case, the operator picked the deeper / more-thorough option. Pattern: when in doubt, ASK — operator overwhelmingly prefers thoroughness over surgical-minimum.
docs/STRATEGY_PLAYBOOK.md §7 deferral bundle)with conn.begin_nested(): SAVEPOINT pattern is now used in 3 active code paths — continuous_collector.py:534, trading_orchestrator.py:1641, backfill_trades.py:save_trades_batch (post-#295). This is the established convention for any future batch-write code.with engine.begin(), NOT vice versa) for single-row writes — used in live_trades_collector.py:save_trade (post-#289.b) + websocket_collector.py (already had it). Established convention for any future per-row INSERT code with pg8000.integrity_monitor.py rule count is now 40 (verified live via curl /api/integrity): freshness=5, null_rate=5, systemd=21 (5 always-on + 8 timer + 8 last_run), artifact=4, storage=4, meta=1.gateway_restart_manager.sh runs Mon-Sat only per design. systemctl restart ibgateway-ibc spawns a fresh Gateway with 2FA prompt; operator approves via IBKR mobile push (or VNC [hidden]:5901 for SMS/hardware-token); trader auto-reconnects within ~90s. Documented in this section for the next agent picking up a Sunday handoff.integrity_monitor.py:_check_systemd_unit_last_result minor latent issue: for never-fired oneshot units, systemctl show -p Result returns success (systemd default for never-ran), causing the rule to report STATUS_OK with "last run succeeded (dead)" — observed in #289.a §G probe before the integrity-monitor's first 5-min cycle had real exit data.~~ RESOLVED May 11, 2026 evening via Finding #296 — branch reorder in _check_systemd_unit_last_result puts the never-ran check (exit_ts in ("", "n/a", "0")) BEFORE the success check, so never-fired units (and bonus: LoadState=not-found units) now correctly return STATUS_UNKNOWN. Live verification on prod confirmed the bug shape via probe oneshot unit; 5-case smoke test (fired/never-ran/failed/not-found/n/a) all pass; §G post-deploy probe confirms 8/8 existing systemd_last_run:* rules continue STATUS_OK with no regression.integrity_monitor.py:artifact:ibkr_snapshot Sunday false-alarm: the 5-min freshness threshold is appropriately tight Mon-Sat but creates an expected weekly false alarm during the IBKR Sunday 2FA window. Not actively harmful (operator handles 2FA manually anyway). Could be widened to 4h on Sundays specifically; future cleanup candidate.Surfaced during the Priority 4 docs audit; operator then directed "understand WHY the max-exposure rule exists + re-run the analysis." NO production change.
MAX_TOTAL_EXPOSURE_PCT (=2.0, the var Finding #313 raised from 1.40) has NO consumer in src/ — live exposure is gated by MAX_DAILY_EXPOSURE=1.40 via OPPORTUNITY_EXPOSURE_LIMITS (rare 0.60 / normal·busy 1.40, risk_manager.py:274-278,587-605). So #313's cap-raise is inert; the #314.b/c/d/e sweeps used exposure_cap=2.0 (live is 1.40 → capacity even more binding; directional conclusions hold).scripts/phase3_simulator/replay_exposure_cap_sweep.py on prod (568 signals): $/wk peaks near 1.0× (+$18.28), flat-to-declining above 1.4× (1.40→+$15.61, 2.0→+$16.86, 3.0→+$15.36); 1.40→2.0 raise = only +$1.25/wk for −$41 worse maxDD. Higher caps admit more but lower-EV trades + deeper DD. (Caveat: simulator admits chronologically, not by priority — directional conclusion robust.)replay_exposure_cap_sweep.py (NEW, 530ff93) + test (+4) + FINDINGS #315 (discovery + resolution) + CONTEXT + this block. scripts/phase3_exposure_cap_sweep_results.json regen on prod (not tracked).Operator Priority 3: evaluate the 4 SKIP-rule candidates deferred in Finding #313 under the capital simulator + bias gauntlet. Offline-only; NOTHING shipped (operator ship-bar = C analysis-only). Originally sized on hypothetical $10k-notional; re-evaluated REAL-SCALED + capacity-aware per the operator's correction.
scripts/phase3_simulator/replay_skip_rules.py: 4 cell predicates + 4 individual SKIP variants + combined "skip all 4" + baseline; real-scaled per-cell diagnostic; reuses run_sizing_sweep (capacity-aware sweep + 5-check gauntlet vs baseline). SKIP-rule gauntlet delta = variant-credited − baseline-credited over union of admitted.d7d7cc0 code+tests; doc commit follows): replay_skip_rules.py (NEW), test_replay_skip_rules.py (NEW), + FINDINGS #314.e + CONTEXT + this block + STRATEGY_PLAYBOOK. scripts/phase3_skip_rules_results.json regenerated on prod (not tracked).Operator Priority 2: evaluate "$2k in peak earnings-weeks / $1k off-peak" vs uniform $1k (Finding #311). Offline-only; NO production change.
earnings_markets ⋈ markets (977 mkts, Sep 2025→Jun 2026) — academic peak-week classifier (weeks 4/17/30/43 ±2) TRACKS PM density: peak windows hold 57.7% of markets + 60% of vol≥$1k. Premise confirmed (peak-week ≥$2k = 419 vs [$1k,$2k) = 31 by cumulative volume). Caveat: weeks 7-9 Q4-reporting cluster sits ~2-3 wk after academic week 4 (slightly under-captured). → implemented A.markets.volume_total; the simulator universe (opp_log/entry volume) shows 144. Different volume field + population; 144 drives the sweep — the hybrid is a SUBSTANTIAL change, not a near-no-op.scripts/phase3_simulator/replay_gate_hybrid.py: hybrid = uniform $1k gate + peak-week sub-$2k skip predicate (reuses classify_earnings_season_segment). 3 variants (uniform_$1k / hybrid_$2k_peak_$1k_off / uniform_$2k). All share 30/22/15 sizing → reuses run_sizing_sweep.a755f45 code+tests; doc commit follows): replay_gate_hybrid.py (NEW), test_replay_gate_hybrid.py (NEW), + FINDINGS #314.d + CONTEXT + this block + STRATEGY_PLAYBOOK. scripts/phase3_gate_hybrid_results.json regenerated on prod (not tracked).Operator Priority 1. Capacity binds at 99% of the $5,000 max-gross cap (Finding #314.b), so the operator hypothesized the 30/22/15 sizing itself is the constraint and lowering it would fit more concurrent positions + lift P&L. This sweep tests that with size-aware P&L + the bias gauntlet.
simulate_one_variant() gained scale_pnl_with_size (default False → gate/SKIP sweeps + #314.b byte-identical). When True, credited P&L scales with each variant's position size (design doc §12 PR3 "P&L = realized_pnl_pct × new_position_value"). AdmissionDecision.realized_pnl_credited added. The prior simulator credited full production-sized P&L regardless of variant sizing — fine for same-sizing sweeps, wrong for sizing variants (INFERENCE DISCIPLINE catch).gauntlet.apply_sizing_gauntlet(): per-trade delta = variant-credited − baseline-credited over the UNION of admitted trades (captures both earns-less-per-trade and fits-more-trades); reuses the 5-check memory §K helpers.scripts/phase3_simulator/replay_sizing_variants.py (~395 lines): DB-free run_sizing_sweep() + DB-backed main(); reuses the #314.b universe loaders. 8 variants (operator AskQuestion option C): 30/22/15 (base) + 25/18/12 + 20/15/10 + 22/16/11 + flat 25/25/25 + flat 18/18/18 + 33/22/15 + 35/25/18.exposure_cap=1.40, not the 2.0 used above) — lowering still FAILS, but flat 25/25/25 (+$4.18/wk) and 35/25/18 (+$4.63/wk) now PASS the gauntlet → RAISING/concentrating is ship-eligible at the real cap (operator decision; caveats re: stale MAX_POSITION_SIZE_PCT + small-cap risk).c5fba22 code+tests; doc commit follows): capital_simulator.py, gauntlet.py, replay_sizing_variants.py (NEW), test_capital_simulator.py (+6), test_replay_sizing_variants.py (NEW), + docs/FINDINGS.md #314.c + docs/CONTEXT.md + this block + docs/STRATEGY_PLAYBOOK.md. scripts/phase3_sizing_variants_results.json regenerated on prod each run (not tracked).First sweep landing on the capital simulator foundation (PR 3.b.1). Re-validates Finding #311 ($1k gate ship) under realistic capacity constraints.
scripts/phase3_simulator/replay_capacity_aware.py (~410 lines) loads full universe (live + counterfactual), runs simulator on 2 variants (production_post_313 vs pre_311_baseline_2k), prints per-volume-bucket breakdown + Δ vs production, writes JSON.scripts/phase3_simulator/replay_capacity_aware.py (already shipped commit 47aaa33)docs/FINDINGS.md — Finding #314.b entry + Finding #311 re-validation note updateddocs/CONTEXT.md — Last-Updated header + What-changed bulletdocs/AGENT_HANDOFF.md — this blockPR 3.b foundation. Per-event timestep simulator that models capacity-constrained admission across overlapping positions — the missing piece for evaluating sizing/SKIP-rule variants under realistic capital constraints (~$2.5k trading capital × 2.0× exposure cap = $5k gross max post Finding #313).
exit_datetime_scheduled is crossed.CapitalSimSignal — frozen dataclass (condition_id / symbol / entry_dt / exit_dt / mcap / direction / conviction / volume / realized_pnl_real / intended_position_value_real / hypothetical_stop_hit / source).SizingVariant — frozen dataclass (id / description / mcap_pct / flex_max / gate_volume_min / extra_skip_predicates). is_skipped_by_rules(sig) -> (bool, str|None) checks gate + extra predicates.AdmissionDecision — audit record per-signal (admitted / skip_reason / capital_reserved / position_size_requested).CapacitySimResult — aggregate (n_total / n_admitted / n_skipped_rule / n_skipped_capacity / total_pnl / win_rate / max_concurrent / max_capital_used / decisions[]).simulate_one_variant(variant, signals, trading_capital, exposure_cap) -> CapacitySimResult — main entry point.signal_from_live_trade(...) + signal_from_counterfactual_market(market) — convenience constructors. Counterfactual constructor applies per-tier empirical scaling per Audit Fix v3 (Large $537, Mid $339, Small $205, Mega = Large) lazy-imported from replay_production_and_candidates.EMPIRICAL_POSITION_BY_TIER.entry_datetime_scheduled.exit_datetime_scheduled.intended_position_value_real) → capacity check (current_reserved + new_size ≤ trading_capital × exposure_cap) → admit or skip with reason.mcap_pct (e.g., 25/25/25 vs production 30/22/15), the simulator scales the signal's intended_position_value_real by variant_pct / production_pct. Production_pct is hardcoded at {large:0.30, mid:0.22, small:0.15} per Mar 9 Finding #211. Defensive fallback to intended_position_value_real if variant has 0% on a tier.CapitalSimSignal from live trades + counterfactual markets → simulate_one_variant() returns CapacitySimResult with admission decisions. Output feeds into Phase β KPI engine for 5-KPI evaluation.scripts/phase3_simulator/capital_simulator.py:1-365; tests/unit/phase3_simulator/test_capital_simulator.py:1-380.scripts/phase3_simulator/capital_simulator.py (NEW, ~365 lines)tests/unit/phase3_simulator/test_capital_simulator.py (NEW, 30 tests)docs/CONTEXT.md (Last-Updated header + What-changed bullet)docs/AGENT_HANDOFF.md (this block)Operator-requested re-validation of PR 2 ($1k gate ship) given the new volume-tier evidence in Finding #313.
docs/FINDINGS.md (Finding #311 re-validation note prepended), docs/CONTEXT.md (header), this entry. NO CODE CHANGE; NO DEPLOY (doc-only commit).THIRD real-money strategy ship of the day (after #311 gate ship, #312 v2 #6 + DirectionPolicySpec).
src/earnings/priority.py rewrite: scale 0-6 → 0-8. Base 0-6 mcap×conviction ordering PRESERVED (Mar 9 / Finding #211 still validated by live data: Large LONG WR 45.7% n=35 > Mid LONG WR 34.3% n=70). NEW volume bump: +1 if vol ≥ $2k, +2 if vol ≥ $5k. Justification: live LONG WR jumps 36% (NULL/old) → 64% ($2-5k) → 83% ($5k+).src/earnings/config.py:132: MAX_TOTAL_EXPOSURE_PCT 1.40 → 2.00. +43% gross capacity (still inside Reg T overnight 2× ceiling). Trading capital base unchanged ~$2.5k (PDT reserve $25k).src/earnings/order_executor.py: dropped dead is_nyse branch (computed but never referenced post Finding #185 Feb 15 unification). primary_exchange still captured for audit log.MAX_PRIORITY_SCORE=8, MAX_BASE_PRIORITY_SCORE=6, VOLUME_BUMP_TIER_1_USD=2000, VOLUME_BUMP_TIER_2_USD=5000.test_dashboard_state_requests_passive_gateway_status) is pre-existing test-order issue, passes in isolation. NEW tests: test_priority_score_volume_bump_finding_313, test_priority_score_max_score_bounds. UPDATED tests: test_priority_score_is_exchange_independent (more robust assertion pattern), test_default_values_loaded (sync stale STOP_LOSS + MAX_TOTAL_EXPOSURE assertions), test_testing_prod_uses_full_base_sizing_for_large_cap (sync stale Mar-9 sizing assertion).priority_score column already supports any int.Config.MAX_TOTAL_EXPOSURE_PCT == 2.0, MAX_PRIORITY_SCORE == 8.calculate_priority_score(None, "large", 0.75, "LONG", volume_total=10000) == 8.src/earnings/priority.py (full rewrite, ~260 lines)src/earnings/config.py (MAX_TOTAL_EXPOSURE_PCT 1.40 → 2.00 + extensive comment)src/earnings/signal_generator.py (call site + gap_risk_category docstring deprecation)src/earnings/order_executor.py (drop is_nyse dead branch + update comment)earnings_dashboard/app.py (call site)earnings_dashboard/templates/dashboard.html (legend + clamp + Live Config rows)tests/test_reconciliation_and_orders.py (+2 new tests, 1 refactor)tests/unit/test_config.py (sync stale assertions)tests/unit/test_position_sizing_2x_rollout.py (sync stale Large-cap sizing)docs/FINDINGS.md (Finding #313 entry)docs/STRATEGY_PLAYBOOK.md (verdict + Action Queue + IMPLEMENTED table)docs/AGENT_HANDOFF.md (this block)docs/CONTEXT.md (Last-Updated header + What-changed bullet)SECOND REAL-MONEY STRATEGY SHIP of the day (after #311 gate_uniform_$1000 evening ship).
src/earnings/session_stop_breaker.py (~155 lines): pure-decision module with SessionBreakerAction 3-value enum (ALLOW / SKIP / DISABLED), SessionBreakerDecision frozen dataclass, decide_session_stop_breaker(enabled, threshold, stop_count_today) pure function, COUNT_STOPS_TODAY_SQL constant.src/earnings/signal_generator.py: _count_todays_stops() helper (fail-soft on DB error → 0) + skip block in _build_trading_signal after Queue Rule #1 long_downtrend.src/earnings/config.py: SESSION_STOP_BREAKER_ENABLED (default True) + SESSION_STOP_BREAKER_THRESHOLD (default 3).polymarket_opportunity_log via existing skip_stage='generate_signal' infrastructure.scripts/phase3_simulator/rules.py:DirectionPolicySpec defaults: long 0.55→0.70, short 0.45→0.30, NEW long_max_threshold=0.90 (Finding #202), conviction_fn "abs_distance_x2"→"raw_prob".production_strategy_may30() factory updated with descriptive id prob_threshold_70_30_with_90_skip.exit_reason ILIKE '%stop%' per Finding #233 (matches stop_loss:hardware (44) + stop_loss:hardware_reconciled (8) + instant_exit:stop_loss_backup (5) = 57 total stop-exits).tests/unit/test_session_stop_breaker.py (22 tests covering ALLOW/SKIP/DISABLED + threshold boundaries + SQL smoke checks + dataclass shape lock-ins) + tests/unit/test_signal_session_stop_breaker.py (8 integration tests via _build_trading_signal).skip_reason='session_3_stop_breaker' when ≥3 stop-exits have already happened today. Monitor over Jul 2026 Q2 earnings season for first live fire.Config.SESSION_STOP_BREAKER_ENABLED == True AND Config.SESSION_STOP_BREAKER_THRESHOLD == 3production_strategy_may30().direction_policy.long_threshold == 0.70 AND .selection_rules_active.includes("session_3_stop_breaker")src/earnings/session_stop_breaker.py (NEW, 155 lines)src/earnings/signal_generator.py (helper + skip block + import)src/earnings/config.py (2 new env vars)scripts/phase3_simulator/rules.py (DirectionPolicySpec + production factory)tests/unit/test_session_stop_breaker.py (NEW, 22 tests)tests/unit/test_signal_session_stop_breaker.py (NEW, 8 integration tests)tests/unit/test_signal_regime_gate.py (stub _count_todays_stops in fixture)tests/unit/test_signal_volume_threshold.py (stub _count_todays_stops)tests/unit/phase3_simulator/test_rules.py (direction-policy lock-in updates)scripts/strategy_performance_analysis.py (Action Queue: v2 #6 IN-QUEUE → IMPLEMENTED)earnings_dashboard/templates/dashboard.html (Direction + Selection live config rows + cockpit-config-asof)docs/FINDINGS.md (Finding #312 entry)docs/STRATEGY_PLAYBOOK.md (verdict + Action Queue + IMPLEMENTED table)docs/AGENT_HANDOFF.md (this block)docs/CONTEXT.md (Last-Updated header + What-changed bullet)FIRST REAL-MONEY STRATEGY SHIP since Mar 9, 2026 (Mar 9 stops + LONG-90% skip per Findings #202 + #211).
src/earnings/config.py:239: SIGNAL_MIN_VOLUME_TOTAL default 2000.0 → 1000.0. .env overrides still respected.scripts/strategy_performance_analysis.py:2907: BOT_VOLUME_GATE_USD constant kept in lock-step (memory.md §M).scripts/phase3_simulator/rules.py:baseline_ruleset(): gate_threshold.id "current_2k" → "current_1k", min_volume_total 2000.0 → 1000.0. NEW production_strategy_may30() factory; old production_strategy_may29() kept as backward-compat alias (returns same content).gate_uniform_$1000 candidate (per-tier empirical scaling Large $537/Mid $339/Small $205): E[wk P&L] +$36.51 (Δ +$20.65/wk), max DD -$212 (Δ -$46; within target), outlier ex-top-5 +$582 (alarm fixed).gate_quarterly_$2000peak_$1000other (would need day-of-year branch in signal_generator.py for ~$0/wk extra lift not worth code surface)gate_quarterly_$1000peak_$500other (more aggressive; deferred to monitor uniform_$1000 effects first)test_volume_gate_is_2000 → test_volume_gate_is_1k + test_production_gate_is_1k references new production_strategy_may30() factory).grep 'SIGNAL_MIN_VOLUME_TOTAL' src/earnings/config.py → 1000.0src/earnings/config.py (1 line)scripts/strategy_performance_analysis.py (1 const + Action Queue updates)scripts/phase3_simulator/rules.py (factory + new alias)tests/unit/phase3_simulator/test_rules.py (lock-in tests)docs/FINDINGS.md (Finding #311 entry)docs/STRATEGY_PLAYBOOK.md (verdict banner refresh + Action Queue update)docs/AGENT_HANDOFF.md (this block)docs/CONTEXT.md (Last-Updated header)docs/STRATEGY.md (gate doc)docs/OPERATIONS.md (gate doc)earnings_dashboard/templates/dashboard.html (Live Config row, banner)Third (FINAL) sub-commit of PR 2 of the Phase 3 simulator project. Per operator AskQuestion incremental-milestones pattern (from #306). NO production behavior change; sweep is offline-only. After this commit, PR 2 is COMPLETE.
scripts/phase3_simulator/gate_retune_sweep.py (NEW, ~280 lines): ShipEligibility (eligible / full_pop_verdict / full_pop_ex_top_2_per_100 / n_admitted_collapse / reasons) + VariantSweepResult (per-variant + per-period gauntlet + ship eligibility) + SweepResult (full sweep + eligible_variants list). evaluate_conservative_ship_eligibility() Q3 (C) 3-condition check (verdict==PASS + n_collapse≥20 + ex_top_2≥0). run_gate_retune_sweep_on_universe() deterministic in-memory + run_gate_retune_sweep() SQL-loading.scripts/phase3_simulator/gauntlet.py UPDATED: ADMISSION_BASELINE_RULE_ID = "gate_status_quo_$2k" constant + apply_admission_gauntlet(variant_id, admission_results, period_filter) new entry point reusing existing _aggregate / _check_* helpers via duck-typed AdmissionResult.scripts/phase3_simulator/counterfactual_admission.py UPDATED: added @property symbol + @property entry_time to AdmissionResult so gauntlet helpers work via duck typing.scripts/phase3_simulator/output.py UPDATED: write_sweep_results_json(sweep_result, path, code_git_sha) — schema = phase3_sim_gate_retune.v1; deterministic (sort_keys + indent=2 + trailing newline + masking volatile fields).scripts/phase3_simulator/runner.py UPDATED: --run-gate-retune-sweep flag + --sweep-output path + new _run_gate_retune_sweep_mode() orchestrator. Wall time ~1-3s on 262-row prod universe.scripts/strategy_performance_analysis.py UPDATED: build_phase3_sim_gate_retune() helper loads scripts/phase3_simulator_gate_retune_sweep.json + falls back to status='not_run' placeholder. Integrated into run_analysis() report dict.earnings_dashboard/templates/dashboard.html UPDATED: NEW 🧪 Phase 3 — Gate Retune Sweep section (placed below 🧪 Phase 3 Simulator Status per design doc §7.4) + renderPhase3SimGateRetune JS function + render-loop integration.tests/unit/phase3_simulator/test_gate_retune_sweep.py (~440 lines, 25 tests in 6 classes): TestAdmissionResultDuckTypeProperties (2) + TestApplyAdmissionGauntlet (6 incl. synthetic H-rule shape) + TestConservativeShipEligibility (6 incl. each FAIL path) + TestRunGateRetuneSweepOnUniverse (5) + TestWriteSweepResultsJson (4 incl. 0-byte-diff reproducibility) + TestSerialization (2).docs/FINDINGS.md Finding #310.c.from scripts.phase3_simulator.gate_retune_sweep → 1 caller (runner.py via lazy import) + 1 self-import in test.apply_admission_gauntlet → 2 sites: definition + lazy-import in gate_retune_sweep + 1 test reference.phase3_sim_gate_retune → 5 sites: strategy_performance_analysis (helper + integration) + dashboard.html (HTML section + render call + JS function name). All intentional.phase3_simulator_gate_retune_sweep.json → 4 sites: gate_retune_sweep / runner / strategy_performance_analysis / dashboard runbook. Consistent.systemctl is-active (11 services) → all active.curl /api/integrity → 47/47 OK preserved.pytest tests/unit/phase3_simulator/ tests/unit/test_pm_v2_conditional_label.py tests/unit/test_ruleb_window_gate.py tests/unit/test_signal_regime_gate.py tests/unit/test_signal_volume_threshold.py tests/unit/test_signal_reliability_gate.py -q → expect 250/250 PASS on prod Python 3.12.runner.py --run-gate-retune-sweep --log-level INFO → ~1-3s wall time, JSON written to scripts/phase3_simulator_gate_retune_sweep.json, per-variant summary printed.strategy_performance_analysis.py → phase3_sim_gate_retune section in report JSON with status='loaded'./api/strategy-analysis returns phase3_sim_gate_retune.status == "loaded" + eligible_variants list populated + dashboard 🧪 Phase 3 — Gate Retune Sweep section renders with per-variant table + per-variant detail collapsibles.sizing_policies.py + capital_simulator.py + per-direction SKIP rule revalidation + position×regime interaction + phase3_sim_sizing dashboard section.scripts/phase3_simulator/gate_retune_sweep.py (NEW, ~280 lines)scripts/phase3_simulator/gauntlet.py (UPDATED)scripts/phase3_simulator/counterfactual_admission.py (UPDATED)scripts/phase3_simulator/output.py (UPDATED)scripts/phase3_simulator/runner.py (UPDATED)scripts/strategy_performance_analysis.py (UPDATED)earnings_dashboard/templates/dashboard.html (UPDATED)tests/unit/phase3_simulator/test_gate_retune_sweep.py (NEW, ~440 lines, 25 tests)docs/PHASE_3_SIMULATOR_DESIGN.md (§12 PR 2 SHIPPED + PR 3 next)docs/STRATEGY_PLAYBOOK.md §1 (verdict banner refresh for PR 2 COMPLETE)docs/FINDINGS.md (new Finding #310.c entry)docs/AGENT_HANDOFF.md §11 (header + this block)docs/CONTEXT.md (Last-Updated header + What-changed bullet)Second sub-commit of PR 2 of the Phase 3 simulator project. Per operator AskQuestion incremental-milestones pattern (from #306). NO production behavior change; offline-only loader + filter.
hypothetical_entry_price + hypothetical_exit_price + hypothetical_stop_hit + hypothetical_pnl + hypothetical_pnl_pct + intended_position_value + intended_shares + intended_stop_price fields populated by scripts/compute_counterfactual_pnl.py (re-run in Finding #308). Massive scope reduction — PR 2.b becomes a ~230-line loader + 20-test suite instead of a multi-module replay extension. But the schema check also surfaced a critical nuance: 278 of 540 skip_reason='low_volume' rows have hypothetical_prices_source='none' (Polygon fetch failed; $0.00 placeholder, NOT a real outcome). Without filtering these out we'd bias the gauntlet downward by counting "ghost markets" as observed neutral outcomes. The loader filters these via USABLE_PRICES_SOURCES=('polygon.io_1m_close',) constant.gate_uniform_$2000 admits 0 (confirms current production never admits these)scripts/phase3_simulator/counterfactual_admission.py (~230 lines):CounterfactualMarket frozen dataclass — 22 fields (identity, trader context, pre-computed outcome, intended sizing, regime)load_counterfactual_markets(engine, limit, usable_sources) SQL loader with the right filters (skip_reason='low_volume' + non-none prices source + direction IN LONG/SHORT + non-NULL required fields)USABLE_PRICES_SOURCES = ('polygon.io_1m_close',) constant (regression-locked)evaluate_admission(variant, market) -> bool per-variant admission via variant.admits()AdmissionResult frozen dataclass (variant_id, market, admitted, delta_vs_status_quo_dollars). .period property delegates to gate_variants.classify_period.evaluate_variant_admissions(variant, markets) -> List[AdmissionResult] one result per marketadmitted_only(results) convenience filtersummary_dict(variant, results) quick aggregate (n_universe / n_admitted / n_admitted_by_period / total_pnl / per_100 / n_winners / n_losers). Heavy-weight gauntlet aggregation (5-check) lands in PR 2.c.tests/unit/phase3_simulator/test_counterfactual_admission.py (~270 lines, 20 tests in 6 classes):docs/FINDINGS.md Finding #310.b.from scripts.phase3_simulator.counterfactual_admission → no callers yet (PR 2.c adds them); 1 self-import in test.polymarket_opportunity_log.*skip_reason='low_volume' → 2 sites: this module's SQL + scripts/compute_counterfactual_pnl.py (original writer). No drift.volume_total field semantic matches Finding #282 max(DB, Gamma) — opp_log stores the effective value the trader's gate check used; same input as variant.admits consumes.systemctl is-active (11 services) → all active.curl /api/integrity → 47/47 OK preserved.pytest tests/unit/phase3_simulator/ tests/unit/test_pm_v2_conditional_label.py tests/unit/test_ruleb_window_gate.py tests/unit/test_signal_regime_gate.py tests/unit/test_signal_volume_threshold.py tests/unit/test_signal_reliability_gate.py -q → expect 225/225 PASS on prod Python 3.12.python3 -c "from sqlalchemy import create_engine; import os; from scripts.phase3_simulator.counterfactual_admission import load_counterfactual_markets, evaluate_variant_admissions, summary_dict; from scripts.phase3_simulator.gate_variants import gate_retune_sweep_variants; e=create_engine(os.environ['DATABASE_URL']); markets=load_counterfactual_markets(e, limit=1000); print(len(markets), 'usable counterfactual markets'); v=next(v for v in gate_retune_sweep_variants() if v.id=='gate_uniform_\\$500'); print(summary_dict(v, evaluate_variant_admissions(v, markets)))" → expect 262 markets + summary_dict with n_admitted=146 + positive total_pnl_dollars.scripts/phase3_simulator/counterfactual_admission.py (NEW, ~230 lines).tests/unit/phase3_simulator/test_counterfactual_admission.py (NEW, ~270 lines, 20 tests).docs/PHASE_3_SIMULATOR_DESIGN.md (§12 PR 2 status — PR 2.b SHIPPED marker added).docs/FINDINGS.md (new Finding #310.b sub-entry).docs/AGENT_HANDOFF.md §11 (header + this block).docs/CONTEXT.md (Last-Updated header + What-changed bullet).OPERATOR-REQUESTED preflight before any PR 2 code change: "start PR 2 now but you need to consider something very important (check it): volume seasonality with the main earnings seasons vs 'off seasons' periods, that might play a part in the current volume drought, consider and check this as well before drawing any conclusions". Per memory.md NO SHORTCUTS + INFERENCE DISCIPLINE 2× for trading-related observations.
earnings_markets LEFT JOIN markets returning per-month gate-pass + avg/median/p90 volume + per-mcap + per-week-of-month breakdowns across the full 9-month data window (Sep 17, 2025 → Jun 4, 2026; 977 markets; 3 reporting seasons observed: Q3 Oct-Nov 2025, Q4 Jan-Feb 2026, Q1 Apr-May 2026; 2 off-seasons observed: Dec 2025, Mar 2026).healthy (earnings_date < 2026-03-01) / collapse (≥ 2026-03-01) / full. Boundary 2026-03-01 per evidence above.uniform (gate = threshold for every market) + seasonal_week (gate = threshold for wk1+wk2 markets; gate = threshold × 0.5 for wk3+ markets). 0.5 factor approximates healthy-regime wk1+wk2 vs wk3+ median-volume ratio.counterfactual_admission.py — synthesize Trade from polymarket_opportunity_log skip_reason='low_volume' row + fetch entry bar from equity_bars_1m cache + compute stop_price from mcap-tier + delegate to existing replay.replay_trade + unit tests with mocked bars. Technically-novel — first time simulator extends beyond replaying existing closed trades.output.py schema extension + runner.py --variant-set gate_retune_sweep flag + 3-condition conservative-ship check per Q3 (C) + new phase3_sim_gate_retune dashboard section in strategy_performance_analysis.py + render in dashboard.html + run on prod + operator AskQuestion per qualifying variant.scripts/phase3_simulator/gate_variants.py (~205 lines): GateVariantSpec frozen dataclass with threshold_for_market(date) -> float + admits(date, effective_volume) -> bool methods + gate_retune_sweep_variants() factory (11 variants) + classify_period(date) segment classifier + classify_seasonal_tier(date) week-of-month classifier + constants (COLLAPSE_BOUNDARY_DATE=2026-03-01 / OFF_PEAK_FACTOR=0.5 / PEAK_WEEK_LAST_DAY_OF_MONTH=14 / MIN_ADMITTED_MARKETS_COLLAPSE_SEGMENT=20 / SHIP_CONSERVATIVE_MIN_EX_TOP_2_PER_100=0.0) all operator-AskQuestion-locked.tests/unit/phase3_simulator/test_gate_variants.py (~220 lines, 51 tests in 6 classes): TestConstants (5 regression-locks) + TestThresholdForMarket (10 parametrize) + TestAdmits (4) + TestGateRetuneSweepVariants (7 — count=11 + unique ids + thresholds + descriptions) + TestClassifyPeriod (9 parametrize — Feb 28 / Mar 1 boundary) + TestSupportedPeriods (1) + TestClassifySeasonalTier (8 parametrize — day 14 / day 15 boundary).docs/FINDINGS.md Finding #310.from scripts.phase3_simulator.gate_variants → no callers yet (PR 2.b + 2.c add them) + 1 self-import in test_gate_variants.py.COLLAPSE_BOUNDARY_DATE | OFF_PEAK_FACTOR | gate_retune_sweep_variants → only the module + tests.effective_volume_usd semantic matches Finding #282 max(volume_total, Gamma cumulative) — same input the live trader's gate check uses; PR 2.b will compute it the same way.systemctl is-active (11 services) → all active.curl /api/integrity → 47/47 OK preserved.pytest tests/unit/phase3_simulator/ tests/unit/test_pm_v2_conditional_label.py tests/unit/test_ruleb_window_gate.py tests/unit/test_signal_regime_gate.py tests/unit/test_signal_volume_threshold.py tests/unit/test_signal_reliability_gate.py -q → expect 205/205 PASS on prod Python 3.12.python3 -c "from scripts.phase3_simulator.gate_variants import gate_retune_sweep_variants; print(len(gate_retune_sweep_variants()), 'variants')" → expect "11 variants" on prod.scripts/phase3_simulator/gate_variants.py (NEW, ~205 lines).tests/unit/phase3_simulator/test_gate_variants.py (NEW, ~220 lines, 51 tests).docs/PHASE_3_SIMULATOR_DESIGN.md (§12 PR 2 section IN-FLIGHT update + PR 2 sub-commit structure 2.a/2.b/2.c documented).docs/STRATEGY_PLAYBOOK.md §1 (verdict banner refreshed for Finding #310 PR 2.a ship).docs/FINDINGS.md (new Finding #310 entry).docs/AGENT_HANDOFF.md §11 (header + this block).docs/CONTEXT.md (Last-Updated header + What-changed bullet).Fifth PR-1 commit today after #307 + #308 + #306.a + #306.b. Closes the multi-week Phase 3 simulator foundation. Adds the bias-gauntlet + deterministic JSON output schema + end-to-end CLI pipeline. All 4 PR 1 acceptance gates now PASS.
_make_run_output() + JSON serialization + mask_volatile_fields diff (no Polygon/DB calls; <1s). Dashboard surface deferred to PR 2 per design doc §12 (sections explicitly attributed to PR 2/3/4/5 in §12 acceptance lists, not PR 1). Runner.py footer staleness fixed in the same commit (NO SHORTCUTS rule — surgical clean-up belongs with the commit that makes it relevant).gauntlet.py (~340 lines): all 5 checks per memory §K (n_adequate / ex_top_2 / ex_repeat_tickers / full_population_spillover / five_outlier) + _verdict() dispatcher in priority order + apply_gauntlet(results, baseline_rule, variant_rule) public entry + compute_per_trade_delta helper + VariantStats / CheckResult / GauntletResult frozen dataclasses with to_dict serializers + FIVE_OUTLIER_SYMBOLS = (GPRO, DELL, SWBI, TXN, STUB) regression-locked + N_FLOOR=12 / N_IDEAL=20 / SPILLOVER_FAIL_THRESHOLD_PER_100=-5.0 constants. Diverges from Q1 HYBRID delegation deliberately: gauntlet AGGREGATION must support arbitrary variant ids for PR 2-5 (not the reference's hard-coded 7 stop-loss rules); stop-loss MATH delegation via reference_adapter still holds.output.py (~210 lines): DataSnapshot + RulesetEvaluation + ShipRecommendation + RunOutput dataclasses per design doc §7.2. write_results_json with sort_keys + indent=2 + trailing newline (POSIX). mask_volatile_fields zeros generated_at for reproducibility diffs. derive_ship_recommendation verdict→SHIP/DEFER/DROP/WARN mapping with human-readable rationale per memory §K. get_code_git_sha() defensive subprocess (timeout=5s; returns 'unknown' if git unavailable).runner.py UPDATED (~80 lines change): added --run-gauntlet flag + --baseline-rule (default A_base) + --variant-rules (nargs+, default RuleB) + --output + --no-replay-detail + --polygon-sleep. New _run_full_pipeline() orchestrates baseline replay → per-variant gauntlet → write JSON → print ship recommendations. Removed "#306.a stops here" footer; updated module docstring to reflect post-#306.c capabilities.test_gauntlet.py (~340 lines, 17 tests in 7 classes): TestComputePerTradeDelta (3); TestVerdictPass (1); TestVerdictNTooLow (3 incl. below-floor / at-floor / between-with-notes); TestHRuleSelfValidation (1 — test_h_rule_synthetic_returns_fail_ex_top_2 = design doc §10.3 PR 1 acceptance gate; 12-trade synthetic with headline +$30 / ex-top-2 -$10 sign flip; asserts verdict==FAIL-EX-TOP-2 with DO-NOT-SHIP-PR-2 diagnostic if it ever returns PASS); TestVerdictExRepeatTickers (1 — 15 trades with most-recent-symbol-dominates flips sign); TestVerdictSpillover (2 — severe fails / mild passes); TestVerdictFiveOutlier (1 — outlier-cluster-flips WARN-OUTLIER-DOMINATED); TestSerialization (2); TestEdgeCases (3 — empty / all-None / zero-headline-neutral-pass).test_output.py (~210 lines, 16 tests in 5 classes): TestRunOutputSchema (4 — top-level keys match §7.2 + DataSnapshot/RulesetEvaluation/ShipRecommendation keys); TestReproducibility (5 — test_two_runs_produce_zero_byte_diff = design doc §10.2 PR 1 acceptance gate; plus sorted-keys + trailing-newline + mask_volatile_fields + no-replay-detail strip); TestDeriveShipRecommendation (5 — PASS→SHIP / WARN→DEFER / FAIL-N-TOO-LOW→DEFER / 3-way parametrize FAIL-*→DROP); TestGetCodeGitSha (1 — returns non-empty string).docs/FINDINGS.md Finding #306.c entry.from scripts.phase3_simulator.gauntlet → 2 callers (runner.py + test_output.py) + 1 self-ref (test_gauntlet.py). All intentional.from scripts.phase3_simulator.output → 2 callers (runner.py + test_output.py). All intentional.FIVE_OUTLIER_SYMBOLS → definition + 1 test using it. Narrative refs in .cursor/memory.md §K + docs/STRATEGY_PLAYBOOK.md §1 are not code consumers.analyze_finding_a_full.py:_aggregate / _ex_top_n / _dedupe_tickers / _split_by / print_gauntlet helpers UNCHANGED — Phase 3 gauntlet is a parallel implementation per design doc §5.1 (intentional divergence from Q1 HYBRID for variant-id flexibility in PR 2-5).systemctl is-active (11 services) → all active.curl /api/integrity → 47/47 OK preserved.pytest tests/unit/phase3_simulator/ tests/unit/test_pm_v2_conditional_label.py tests/unit/test_ruleb_window_gate.py tests/unit/test_signal_regime_gate.py tests/unit/test_signal_volume_threshold.py tests/unit/test_signal_reliability_gate.py -q → expect 154/154 PASS on prod Python 3.12.cd [hidden] && source .env && export DATABASE_URL POLYGON_KEY && venv/bin/python3 scripts/phase3_simulator/runner.py --run-gauntlet --limit 200 --output /tmp/p3sim_baseline.json --log-level INFO → expect documented RuleSet summary + replay completion + per-variant gauntlet summary (RuleB likely PASS per May-28 reference simulator showing ex-top-2 +$127/100 strengthened) + JSON written. Wall time ~5-8 min (cache hot from #306.b §G probe).runner.py --run-gauntlet --output /tmp/run1.json then runner.py --run-gauntlet --output /tmp/run2.json; diff the two JSONs after masking generated_at → expect IDENTICAL.test_two_runs_produce_zero_byte_diff.test_h_rule_synthetic_returns_fail_ex_top_2.gate_variants.py (declarative thresholds $2k→$0) + replay.py extension (counterfactual admission for skip_reason='low_volume' rows; activates the latent opp_log_pm_fields_by_audit_id plumbing for pm_v2_orderbook cost-model on newly-admitted markets) + output.py schema extension + first phase3_sim_gate_retune dashboard section in strategy_performance_analysis.py.scripts/phase3_simulator/gauntlet.py (NEW, ~340 lines).scripts/phase3_simulator/output.py (NEW, ~210 lines).scripts/phase3_simulator/runner.py (UPDATED: --run-gauntlet flag + footer cleanup).tests/unit/phase3_simulator/test_gauntlet.py (NEW, ~340 lines, 17 tests).tests/unit/phase3_simulator/test_output.py (NEW, ~210 lines, 16 tests).docs/PHASE_3_SIMULATOR_DESIGN.md (§12 PR 1: #306.c marked SHIPPED + PR 1 COMPLETE).docs/FINDINGS.md (new Finding #306.c entry).docs/AGENT_HANDOFF.md §11 (header + this block).docs/CONTEXT.md (Last-Updated header + What-changed bullet).Fourth PR-1 commit today after #307 (snapshot_schema conditional-label fix) + #308 (hyp_pnl re-run + structural ceiling) + #306.a (foundation skeleton). Adds the baseline replay path end-to-end via Q1 HYBRID delegation; #306.c will add gauntlet + H-rule self-validation + reproducibility + JSON output + dashboard hook. NO replay/gauntlet variants, NO production behavior change.
Trade.scheduled_exit_dt(cal) was MISSING from scripts/phase3_simulator/population.py. The reference's run_trade calls it (scripts/analyze_finding_a_full.py:621 — sim.sched_exit = t.scheduled_exit_dt(cal)), so the delegation in #306.b would AttributeError immediately when called with our Trade. The #306.a TestReferenceParity only validated dataclass FIELDS (via dataclasses.fields() reflection), not METHODS. Fixed both as part of #306.b: added scheduled_exit_dt to population.py + new TestReferenceParity.test_method_names_match_expected test asserting {release_dt, scheduled_exit_dt, position_value, stop_distance_pct} present on BOTH dataclasses + new TestReferenceParity.test_scheduled_exit_dt_consistency_amc test asserting identical output on identical inputs. Caught BEFORE writing any #306.b code, per NO SHORTCUTS §A pre-coding rule. Same shape as the 4 INFERENCE DISCIPLINE catches earlier in the session arc (#273 16× bigger bug surface; Playbook §6.7 RuleB soft-gate broken spec; Bug #2/#3 already-fixed; #308 deadband-conviction structural ceiling) and the same shape as the #306.b ATR-stop sig-count miscount caught by my own _assert_reference_module_stable() at test-collection time (I expected simulate_combined_window_atr to have 9 params; actual is 8 — assertion fired correctly; fixed expectation in the same commit).scripts.analyze_finding_a_full. Re-exports + _assert_reference_module_stable() signature-drift guard.PROXY_FLAT_SPREAD_PCT = 0.01; half-spread per leg (entry + exit); cost_model_method='proxy_flat_1pct' in JSON output. pm_v2_orderbook path (latent) wired through opp_log_pm_fields_by_audit_id in ReplayContext for PR 2+ counterfactual admission readiness.cost_model.py (~140 lines): CostEstimate frozen dataclass (method / spread_pct / entry_cost / exit_cost / total_cost). estimate_cost(trade, opp_log_pm_fields=None) always returns a CostEstimate. pm_v2_orderbook path fires only when ALL 3 fields non-None (matches Finding #307 conditional-label semantic); otherwise proxy_flat_1pct. Half-spread per leg with abs(shares) defense. cost_estimate_to_dict for JSON output. Module docstring documents §4.4 + §9 + §10.1 + the explicit "cost NOT folded into pnl_d for baseline; held separately for gauntlet aggregation in #306.c" design choice.reference_adapter.py (~120 lines): re-exports TradeSim / fetch_bars / run_trade / simulate_* per Q1. REFERENCE_RULE_IDS = (A_base, FA_soft, FA_hard, RuleB, RuleD, RuleG, RuleF). _assert_reference_module_stable() fires at import + checks inspect.signature param counts against _EXPECTED_PARAM_COUNTS; raises RuntimeError with CHANGE-CONTRACT + memory §M diagnostic on drift. Catches: function missing OR param-count mismatch. (Caught my own off-by-one expectation for simulate_combined_window_atr during test collection — assertion fired correctly; fixed expected count.)replay.py (~210 lines): ReplayContext (polygon_key / cal / sleep_s / optional opp_log_pm_fields_by_audit_id); ReplayResult (audit_id / symbol / direction / timing / entry_time / exit_time / live_pnl / per_rule_pnl_d: Dict[str, Optional[float]] / per_rule_aux: Dict[str, Dict] / cost_estimate / halt_suspect / atr_dollars). replay_trade(trade, ruleset, ctx) delegates to _ref_adapter.run_trade, extracts per-rule pnl_d (kept separate from aux to avoid duplication), computes cost_estimate. replay_population(trades, ruleset, ctx, progress_every=25) loops, logs progress, skips None results with WARNING. replay_result_to_dict for JSON schema. ruleset arg accepted-but-unconsumed for baseline (preserves signature for #306.c + PR 2-5 variant branching).cross_check_baseline.py (~280 lines): OPERATOR-RUNNABLE SCRIPT (NOT a unit test). Implements design doc §10.1 PR 1 acceptance gate. Loads trades + reference CSV → runs replay_population → INNER JOINs by audit_id → per-rule drift breakdown → PASS if match_pct >= 95% within $1 on every rule, else FAIL with outlier dump (first 10 per rule). Writes JSON evidence to scripts/phase3_simulator_baseline_cross_check.json. Exits 0/1/2. CLI: --reference-csv / --population / --limit / --tolerance-dollars / --min-match-pct / --polygon-sleep / --output / --log-level. Standalone — does NOT run as pytest collection (needs POLYGON_KEY + DATABASE_URL + ~1-7 min wall time).population.py UPDATED: added Trade.scheduled_exit_dt(cal) method matching reference + sys.path bump + imports of _scheduled_times_from_timing + MarketCalendar (same pattern as reference). The CHANGE-CONTRACT docstring was already in place from #306.a.test_cost_model.py (~140 lines, 13 tests in 4 classes): TestConstants (2 — regression-lock proxy = 0.01 + method ids); TestProxyFlatPath (5 — no-args + None + empty-dict + 3-way partial parametrize + SHORT abs-shares); TestPmV2OrderbookPath (2 — all-3-populated + spread==0 not treated as NULL); TestSerialization (2 — frozen + dict keys).test_reference_adapter.py (~80 lines, 13 tests in 3 classes): TestReExports (8 parametrize); TestRuleIdRegistry (2 — expected ids + source-grep of TradeSim.run_all); TestSignatureStabilityAssertion (3 — passes-on-current + fails-on-signature-drift via monkeypatch + fails-on-missing-function via delattr, both with CHANGE-CONTRACT diagnostic).test_replay.py (~195 lines, 9 tests in 3 classes): TestReplayTradeWithStubbedAdapter (6 — extract per-rule pnl_d + proxy cost_estimate default + pm_v2_orderbook when opp_log provided + None when reference returns None + per_rule_aux preserved without pnl_d duplication + halt + atr propagation); TestReplayPopulation (2 — skip None + preserve order); TestReplayResultSerialization (1 — JSON round-trip via replay_result_to_dict + json.dumps(default=str)).TestReferenceParity.test_method_names_match_expected (catches the #306.b §A pre-coding gap class) + TestReferenceParity.test_scheduled_exit_dt_consistency_amc (output-parity guard on AMC trade with cal arg).docs/FINDINGS.md Finding #306.b entry.from scripts.phase3_simulator → 5 source files (4 new package files + cross_check_baseline.py) + 7 test files. All intentional.from scripts.analyze_finding_a_full → 2 imports: tests/unit/phase3_simulator/test_population.py:176 (TestReferenceParity, from #306.a) + tests/unit/phase3_simulator/test_reference_adapter.py:51 (rule-id source-grep, NEW). Plus the adapter module's own re-exports (intentional + central per Q1 HYBRID)._scheduled_times_from_timing → 3 sites: outcome_resolver.py (definition) + analyze_finding_a_full.py:93 (reference's import) + phase3_simulator/population.py (NEW lockstep import via #306.b). No drift.systemctl is-active (11 services) → all active (sanity check; no service-restart changes).curl /api/integrity → 47/47 OK preserved (no integrity rule changes).pytest tests/unit/phase3_simulator/ tests/unit/test_pm_v2_conditional_label.py tests/unit/test_ruleb_window_gate.py tests/unit/test_signal_regime_gate.py tests/unit/test_signal_volume_threshold.py tests/unit/test_signal_reliability_gate.py -q → expect 121/121 PASS on prod Python 3.12.cd [hidden] && source .env && export DATABASE_URL POLYGON_KEY && venv/bin/python3 scripts/phase3_simulator/cross_check_baseline.py --limit 200 --log-level INFO → expect verdict PASS with match_pct >= 95.0% on every rule + max_drift <= $1.00. Wall time ~1-7 min depending on Finding #240 cache hit rate. Output: scripts/phase3_simulator_baseline_cross_check.json (NOT committed; operator-runnable evidence).python3 scripts/phase3_simulator/runner.py --population all-closed --limit 200 → expect documented baseline summary + ~120 trades.replay.ReplayResult + replay.replay_population and adds gauntlet + H-rule self-validation + reproducibility test + JSON output + dashboard hook.scripts/phase3_simulator/cost_model.py (NEW, ~140 lines).scripts/phase3_simulator/reference_adapter.py (NEW, ~120 lines).scripts/phase3_simulator/replay.py (NEW, ~210 lines).scripts/phase3_simulator/cross_check_baseline.py (NEW, ~280 lines).scripts/phase3_simulator/population.py (UPDATED: added scheduled_exit_dt method + lockstep imports).tests/unit/phase3_simulator/test_cost_model.py (NEW, ~140 lines, 13 tests).tests/unit/phase3_simulator/test_reference_adapter.py (NEW, ~80 lines, 13 tests).tests/unit/phase3_simulator/test_replay.py (NEW, ~195 lines, 9 tests).tests/unit/phase3_simulator/test_population.py (UPDATED: +2 tests).docs/PHASE_3_SIMULATOR_DESIGN.md (§12 PR 1: #306.b marked SHIPPED).docs/FINDINGS.md (new Finding #306.b entry).docs/AGENT_HANDOFF.md §11 (header + this block).docs/CONTEXT.md (Last-Updated header + What-changed bullet).Third PR-1 commit today after #307 (snapshot_schema conditional-label fix) + #308 (hyp_pnl re-run + structural ceiling). Foundation skeleton intervention only — NO replay, NO gauntlet, NO production behavior change. First of 3 incremental sub-commits per operator AskQuestion Q3 = option B INCREMENTAL MILESTONES.
scripts/analyze_finding_a_full.py (952-line Apr-25 reference behind Finding #228 FA retraction). Options (A) REFACTOR / (B) DUPLICATE / (C) HYBRID. Operator chose (C) HYBRID — duplicate the minimal Trade dataclass + load_trades into population.py for clarity; delegate stop-loss simulate_* via a reference_adapter.py (LANDS IN #306.b) that imports from scripts.analyze_finding_a_full. Preserves the proven stop-loss math anchored in the reference (zero regression risk on the FA-retraction baseline) while avoiding a duplicate copy of trade-loading logic that would silently drift in a year.cost_model_method='proxy_flat_1pct' (LANDS IN #306.b cost_model.py); easy to swap in PR 1.5 if simulator dry-run reveals materially-wrong-vs-live-CLOB-fetch ground-truth.strategy_performance_analysis.py phase3_sim_* sections. After #306.c, PR 1 COMPLETE and PR 2 (volume-gate retune sweep) begins.scripts/phase3_simulator/, all offline-only):__init__.py (~40 lines) — package marker docstring with design-doc references + sub-commit roadmap + Q1/Q2/Q3 decisions recorded.population.py (~200 lines) — Trade dataclass (frozen=False, mirrors analyze_finding_a_full.Trade exactly) + load_trades(engine, population, limit) (SQL byte-identical to reference) + supported_populations() helper + module-level CHANGE-CONTRACT docstring (any change to Trade schema here MUST land in analyze_finding_a_full.py in the same commit and vice versa — same lesson as memory.md §M cross-interface rule).rules.py (~230 lines) — declarative dataclasses per design doc §5.4: RegimeClassifierSpec, SizingPolicySpec, GateThresholdSpec, DepthGateSpec, RuleSet (all frozen=True). baseline_ruleset(id_suffix='may28') factory regression-locks production config (Mar-9 sizing per #211, $2k gate per #282, compound regime classifier, depth 0.50, active selection rules per #304/#207/#202, active stop-loss per #305). spec_to_dict/spec_from_dict round-trip via _kind discriminator (required for design doc §10.2 reproducibility).runner.py (~150 lines) — CLI stub with --variant-id/--population/--limit/--skip-db/--log-level. Loads baseline RuleSet + (optionally) trades; prints summary; exits 0/1/2. Explicit exit 2 if variant_id is anything other than baseline_may28 (sweep ids land in PR 2+). NO replay/NO gauntlet — those land in #306.b/c.tests/unit/phase3_simulator/ — NEW directory, 3 NEW files, 42 NEW tests, 42/42 PASS locally on Python 3.11):__init__.py (1 line package marker).test_population.py (~250 lines, 18 tests in 4 classes): TestPositionValue (2: LONG positive shares + SHORT abs guard); TestStopDistancePct (5: LONG/SHORT/zero-entry/zero-stop/case-insensitive); TestReleaseDt (5: actual_earnings_time wins + BMO 07:30 ET default + AMC 16:15 ET default + NULL date falls back to entry-time date + NULL label+timing defaults BMO); TestReferenceParity (4: field-name match + field-type match + position_value consistency + release_dt consistency vs analyze_finding_a_full.Trade — CHANGE-CONTRACT guards that fail loudly with memory.md §M-pointing diagnostic if either side drifts); TestSupportedPopulations (2: 4 expected filters + cross-source-grep of analyze_finding_a_full.load_trades source for the same population keys).test_rules.py (~210 lines, 24 tests): per-spec construction + immutability (8); RuleSet minimal (1); baseline_ruleset regression-lock (7 tests across regime / sizing / gate / depth / selection / stop-loss); serialization round-trip on every spec class + full RuleSet (5) + unknown-kind raises + missing-kind raises (2).tests/unit/phase3_simulator/ + test_pm_v2_conditional_label + test_ruleb_window_gate + test_signal_regime_gate + test_signal_volume_threshold + test_signal_reliability_gate. No regressions to prior signal tests. (Will re-run on prod Python 3.12 post-deploy.)docs/FINDINGS.md Finding #306.a entry.scripts/phase3_simulator|phase3_simulator → 6 source files (the 4 new package files + 2 new test files) + 3 doc references (CONTEXT / AGENT_HANDOFF / PHASE_3_SIMULATOR_DESIGN). No stale references.from scripts.analyze_finding_a_full → exactly 1 import in tests/unit/phase3_simulator/test_population.py:176 (intentional — TestReferenceParity cross-check). No other simulator engines to consolidate.raise SystemExit(main()) matches scripts/analyze_finding_a_full.py:main exit-code convention.systemctl is-active earnings-trader polymarket-dashboard polymarket-collector polymarket-live-trades polymarket-integrity-monitor → all active (no service-restart-relevant changes; sanity check only).curl /api/integrity → 47/47 OK preserved (no integrity rule changes; sanity check only).cd [hidden] && source .env && export DATABASE_URL && venv/bin/python3 -m pytest tests/unit/phase3_simulator/ tests/unit/test_pm_v2_conditional_label.py tests/unit/test_ruleb_window_gate.py tests/unit/test_signal_regime_gate.py tests/unit/test_signal_volume_threshold.py tests/unit/test_signal_reliability_gate.py -q → expect 84/84 PASS on prod Python 3.12.cd [hidden] && source .env && export DATABASE_URL && venv/bin/python3 scripts/phase3_simulator/runner.py --population all-closed --limit 200 → expect documented baseline RuleSet summary + ~120 loaded trades + most-recent sample (id=163 HPQ as of session start).cd [hidden] && source .env && export DATABASE_URL && venv/bin/python3 scripts/phase3_simulator/runner.py --skip-db → exits 0 with the baseline RuleSet summary alone.src/earnings/ or src/data_tracker/ references it).population.Trade + rules.baseline_ruleset() and adds cost_model.py (proxy-flat-1pct per Q2) + replay.py + reference_adapter.py (delegates stop-loss simulate_* per Q1 HYBRID) + baseline cross-check vs analyze_finding_a_full.py (acceptance per design doc §10.1).scripts/phase3_simulator/__init__.py (NEW, ~40 lines).scripts/phase3_simulator/population.py (NEW, ~200 lines).scripts/phase3_simulator/rules.py (NEW, ~230 lines).scripts/phase3_simulator/runner.py (NEW, ~150 lines).tests/unit/phase3_simulator/__init__.py (NEW, 1 line).tests/unit/phase3_simulator/test_population.py (NEW, ~250 lines, 18 tests).tests/unit/phase3_simulator/test_rules.py (NEW, ~210 lines, 24 tests).docs/PHASE_3_SIMULATOR_DESIGN.md (§12 PR 1: #306 sub-commit-split callout block added + #306.a marked SHIPPED).docs/FINDINGS.md (new Finding #306.a entry).docs/AGENT_HANDOFF.md §11 (header + this block).docs/CONTEXT.md (Last-Updated header + What-changed bullet).Ops-only commit (no code change). Second of three PR 1 ships. Closes the Bug #6 framing from Phase 0 §0.4 with a corrected understanding: the "coverage gap" was structural, not a real gap.
counterfactual_computed_at IS NULL). 480 generate_signal + 39 depth rows with NULL hyp_pnl but already processed (marked counterfactual_version='no_price_simulation_v1').direction_would_be='NONE' (conviction in deadband between SHORT_THRESHOLD=0.30 and LONG_THRESHOLD=0.70 → _direction_hypothesis_for_log returns "NONE"). The script's _can_simulate_prices correctly refuses to simulate counterfactual P&L without a direction (you can't compute LONG/SHORT P&L if you don't know which direction). These rows are STRUCTURALLY un-simulatable, not a coverage gap.compute_counterfactual_pnl.py --limit 200. Processed all 78 pending rows in ~90s. Post-run: 0 pending rows across all stages; generate_signal coverage 67.5% → 69.1% (+1.6pp); depth coverage 27.8% unchanged (no pending depth rows; remaining 39 all deadband); signal_emitted 100% unchanged.generate_signal + depth stages is approximately what we have now (~69% / ~28%), NOT 75% or 100%. The Phase 0 75% target was incomplete framing — didn't account for deadband-conviction rows being structurally un-simulatable. Phase 3 simulator's signal_emitted 100% subset is the primary input for all 4-bundle analyses; the structural ceiling on the other stages doesn't block any Phase 3 work.docs/PHASE_3_SIMULATOR_DESIGN.md (§3 Gate 4 rewritten with post-#308 state + structural ceiling clarification).docs/FINDINGS.md (new Finding #308 entry).docs/AGENT_HANDOFF.md §11 (header + this block).docs/CONTEXT.md (Last-Updated + What changed bullet).First code-bearing commit of the Phase 3 simulator project (PR 1). Ships the Option A MINIMAL conditional-label fix per design doc §9 + the integrity rule update that the fix necessitates (caught via memory.md §M cross-interface check). Doc-only design-doc updates also folded in to reflect the INFERENCE DISCIPLINE catch from PR 1 §A pre-coding (Bug #2 + #3 are ALREADY FIXED in code since Apr 26 — Findings #234/#237 — so PR 1 active scope is reduced ~30%).
AskQuestion option (A) after agent's PR 1 §A pre-coding revealed Bugs #2 + #3 are already fixed in code (forward-write paths set the fields correctly since Apr 26; the 111 NULL rows are pre-fix historical baseline, not current bugs). Decisions 2 + 3 in design doc §15 reframed as "already resolved by existing fixes; optional historical backfill deferred."SELECT ... GROUP BY pre/post Apr-26 ... entry_time/exit_time) showed post-Apr-26 closed trades have sp500_change_at_entry 9/9 = 100% populated (Finding #234 fix worked) and conviction_at_exit 9/10 = 90% populated (Finding #237 fix worked). Same pattern as the previous 2 catches this session (#273 16× bigger bug surface; Playbook §6.7 RuleB soft-gate broken spec). Surfaced via AskQuestion; operator chose proceed-with-revised-smaller-scope.docs/FINDINGS.md Finding #307. Highlights: trace at signal_generator.py:1019-1059; 4 failure modes (all 3 None, partial None, all populated, conviction_result None) each covered by dedicated unit test; integrity rule fail-over plan verified (rule's new col=method field is unconditionally populated when enrichment block runs, so null-rate stays at ~0% baseline).snapshot_schema consumers. Found integrity_monitor.py:541 had null_rate:opp_log:pm_v2_post_deploy rule keyed on (snapshot->>'snapshot_schema') with max_null_pct=20.0. Pre-fix this rule had ~0% null rate (because schema_label was unconditionally set on every row with conviction_result). Post-fix, ~95% of signal-stage rows would have NULL schema_label (intentionally) → integrity FAIL on next 5-min cycle. Updated rule in lockstep: changed col from snapshot_schema → method (which is set unconditionally inside the enrichment block at line 1029, regardless of bid/ask/spread). Same intent (verify enrichment ran), unaffected by the new conditional-label semantic. 18 lines updated with 15-line context comment explaining the why.dashboard.html:2791 reference is purely a static UI badge ("✓ Phase 2.1 — opp_log pm_v2 (#243)"); no logic gate; no update needed. Doc references in STRATEGY_ANALYSIS_ARCHITECTURE.md + DATABASE_AUDIT_2026_04_27.md + STRATEGY_AUDIT_v3.md + DATABASE_DIRECTIVES.md are historical narrative; no behavior change needed.src/earnings/signal_generator.py:1028-1058 (17 lines): pm_snapshot dict construction + new conditional that sets snapshot_schema='pm_v2' ONLY when best_bid AND best_ask AND spread are ALL non-None. Conservative gate (all 3 fields, not just bid). 12-line context comment explaining the rationale + cross-references to Finding #228 (FA_soft retraction) + Finding #307 (this entry) + Finding #273 (canonical narrative). Other pm_* fields continue to be set unconditionally (preserves JSONB shape for forward-compat readers).src/data_tracker/integrity_monitor.py:541-577 (18 lines): rule null_rate:opp_log:pm_v2_post_deploy col changed snapshot_schema → method + 15-line context comment.tests/unit/test_pm_v2_conditional_label.py — NEW, ~150 lines, 6 tests, 6/6 PASS pre-commit on prod Python 3.12 venv):test_label_set_when_all_three_fields_populated — control casetest_label_not_set_when_best_bid_is_nonetest_label_not_set_when_best_ask_is_nonetest_label_not_set_when_spread_is_nonetest_label_not_set_when_all_three_are_none — typical fallback case (most common in practice per the 95.6% mislabel rate evidence)test_no_pm_snapshot_block_when_conviction_result_is_none — existing behavior preservedtest_pm_v2_conditional_label + test_ruleb_window_gate + test_signal_regime_gate + test_signal_volume_threshold + test_signal_reliability_gate). No regressions.docs/PHASE_3_SIMULATOR_DESIGN.md):pm_v1 rows.col field of the existing pm_v2_post_deploy rule was updated, not a new rule added).src/earnings/signal_generator.py (17 lines net added)src/data_tracker/integrity_monitor.py (18 lines net updated)tests/unit/test_pm_v2_conditional_label.py (NEW, ~150 lines)docs/PHASE_3_SIMULATOR_DESIGN.md (§3 + §12 + §15 updates)docs/FINDINGS.md (new Finding #307 entry)docs/AGENT_HANDOFF.md §11 (header + this block)docs/CONTEXT.md (Last-Updated + What changed bullet)docs/PHASE_3_SIMULATOR_DESIGN.md, 867 lines)Fifth Phase-3-related artifact today (after #279 amendment, JSON refresh, Rule #1 ship, RuleB ship). Doc-only intervention; no code changes. Companion to STRATEGY_ANALYSIS_ARCHITECTURE.md Phase 0 baseline (Apr 26).
stop_triggered=TRUE on 61/120 = 50.8%, matches exit_reason ILIKE '%stop%' exactly. Bug #6 (hyp_pnl coverage) FIXED for signal_emitted (100%) and PARTIAL for generate_signal (67.5%) + depth (27.8%) → PR 1 will backfill the lower-coverage subsets. Bug #2 PARTIAL: regime_at_entry 0/120 NULL ✅, vix_at_entry 2/120 NULL ✅ (near-complete), sp500_change_at_entry 111/120 NULL ❌ STILL BROKEN — PR 1 backfill via one-shot SQL using regime_at_entry + entry_time + SPY history. Bug #3 (conviction_at_exit) STILL BROKEN at 111/120 NULL — PR 1 forward-only fix.analyze_finding_a_full.py) / Risk analysis (10 risks with mitigations) / 5-PR ship plan (~3-5 weeks total) / Out of scope / Acceptance criteria / 8 open operator-decision points / References.RegimeClassifierSpec, SizingPolicySpec, GateThresholdSpec, DepthGateSpec, RuleSet) not arbitrary Python code → testable, composable, serializable, visible in JSON outputequity_bars_1mpm_v1 rows and the conditional-label-fixed pm_v2 rows uniformly; documents proxy assumption in output for transparencyscripts/phase3_simulator/ package layout matches existing pattern (scripts/analyze_finding_a_full.py, scripts/strategy_performance_analysis.py)verdict field drives dashboard render + ship-recommendationFAIL-EX-TOP-2 or FAIL-SPILLOVER; if it returns PASS the gauntlet is broken and the simulator is blocked from shippinggenerated_at timestamp); tested by diff between two runsAskQuestion per the session patternphase3_simulator/ package skeleton, baseline replay matching analyze_finding_a_full.py cross-check, cost model V1 with proxy fallback, Finding #273 fix (Option A conditional-label per design doc §9), Bug #2 sp500 + #3 conviction_at_exit + #6 hyp_pnl backfills.depth_quality into spread_gate + depth_gate; constraint = sample size of real-signal depth-kills (PR 1 backfill increases n).scripts/phase3_simulator/ / alt src/strategy_analysis/phase3/STRATEGY_ANALYSIS_ARCHITECTURE.md §0.7, STRATEGY_PLAYBOOK.md §7 + §8 cross-references table; AGENT_HANDOFF.md §11 (this block); CONTEXT.md.docs/PHASE_3_SIMULATOR_DESIGN.md (NEW, 867 lines).docs/STRATEGY_ANALYSIS_ARCHITECTURE.md (1 paragraph in §0.7 Phase 3 section cross-referencing the new design doc + verifying gates now met).docs/STRATEGY_PLAYBOOK.md (1 paragraph in §7 intro + 1 row in §8 cross-references table).docs/AGENT_HANDOFF.md §11 (header + this block).docs/CONTEXT.md (Last-Updated header + What-changed bullet).Third Phase-3 commit today after Finding #279 amendment (dc13ff3), strategy report refresh (27501b8), Finding #304 Rule #1 (3f8654d), refresh-post-#304 (6a4cfa2). Code-bearing intervention; hardware-side execution change (cancel + re-place hardware STP-LMT during ±90min earnings window) — but Phase 1 ships in DRY-RUN mode + RULEB_ENABLED=false default, so production behavior is UNCHANGED until operator explicitly enables.
docs/STRATEGY_PLAYBOOK.md §2 (rank-1 IN-QUEUE after Rule #1 ship earlier today).scripts/analyze_finding_a_full.py:simulate_window_only (line 443) revealed the simulator explicitly skips stop-triggering during the window (if w0 <= b["t"] <= w1: continue # inside window: stop silent). The simulator's +$127/100 ex-top-2 EV REQUIRES the live hardware stop to actually be silent — a soft-gate implementation would deliver $0 EV, exactly the FA_soft failure mode that Finding #228 retracted on Apr 25. The Playbook §6.7 caveat "works for RuleB only because the window is narrow enough that the broker's hardware fill + bot's reconciliation-after gives a similar outcome to no-stop-in-window" was INCORRECT — the broker's hardware fill closes the position at the stop-fired price regardless of window narrowness. Per memory.md NO SHORTCUTS + INFERENCE DISCIPLINE, agent did NOT silently code the broken spec; surfaced via AskQuestion with comparison matrix BEFORE writing any code.ruleb_state DB column from the 8-item list is unnecessary — derived-from-current-state (now, hardware_stop_active, release_dt, scheduled_exit) is strictly better per memory.md "IBKR is source of truth for live state" + §C surgical-changes. Skipped the migration; flagged for operator override if disagreement.src/earnings/ruleb_window_gate.py (~180 lines): pure decision function decide_ruleb_action(ctx) -> RuleBAction, RuleBAction enum (8 values: NOOP, CANCEL, REARM_POST_WINDOW, REARM_HARD_FALLBACK, SKIP_LATE_ENTRY, SKIP_NO_EARNINGS_DT, SKIP_NO_SCHEDULED_EXIT, SKIP_NO_STOP_PRICE), RuleBDecisionContext dataclass, derive_earnings_release_dt(position) helper. Module-level docstring explains simulator alignment + Phase 1 limitations + why derived-state-not-DB.src/earnings/trading_orchestrator.py (~220 lines added): _process_ruleb_window_gate() per-cycle dispatcher; _ruleb_log_once() for once-per-(symbol,action) skip-logging (prevents log spam); _ruleb_fire_cancel() cancel side effect (dry-run logs only, active mode calls ibkr_client.cancel_order); _ruleb_fire_rearm() re-place side effect (dry-run logs only, active mode calls order_executor._place_stop_order + HIGH-severity Telegram alert on failure). Main-loop hook added between check_stop_loss_backup and the 1s sleep — runs every orchestrator cycle when Config.RULEB_ENABLED=True AND active positions exist.src/earnings/config.py (~25 lines added): 4 new env vars in a new "STRATEGY RULES" section: RULEB_ENABLED (bool, default False — production default disabled), RULEB_MODE ('dry_run' | 'active', default 'dry_run'), RULEB_WINDOW_MINUTES (int, default 90 — matches simulator), RULEB_HARD_FALLBACK_MIN_BEFORE_EXIT (int, default 30 — safety boundary).tests/unit/test_ruleb_window_gate.py — NEW file, ~225 lines, 25 tests; all PASS pre-commit on prod Python 3.12 venv):derive_earnings_release_dt (4 tests): signal present, naive datetime, no signal (Phase 1 limitation), signal but no earnings_datetime.test_ruleb_window_gate.py + test_signal_regime_gate.py + test_signal_volume_threshold.py + test_signal_reliability_gate.py — no regressions to prior signal tests.docs/FINDINGS.md Finding #305 for full trace including the explicit decision tree (10 cases, first-match-wins), runtime simulation walkthrough for an AMC trade, line numbers, all 8 failure modes, and acceptance test results.run_bmo_cycle, run_amc_cycle, run_exit_cycle, check_stop_loss_backup) all use the same main-loop polling pattern. _process_ruleb_window_gate follows the same shape: per-cycle iteration of active_positions, per-position decision, dispatch on action. No new architectural primitive introduced. Existing _check_hardware_stop_status (line ~4340) is reused as the "is hardware stop active?" source of truth. IBKRClient.cancel_order + order_executor._place_stop_order are well-tested existing entry points; RuleB just calls them at new times.scripts/strategy_performance_analysis.py:build_action_queue): queue rank-2 row (Sim RuleB) flipped status IN-QUEUE → IMPLEMENTED-DRY-RUN; rank: 2 → rank: None; implemented_date: "May 28, 2026 (Phase 1 DRY-RUN)"; implemented_findings: ["#305"]; live_evidence + bias_check + files_to_touch + next_action all rewritten with May-28 fresh sim numbers + Phase-1/Phase-2 rollout notes.docs/STRATEGY_PLAYBOOK.md): §2 — RuleB row moved from 🟢 IN-QUEUE table to ✅ IMPLEMENTED table; remaining IN-QUEUE rules renumbered (3-stop breaker 3→1, trade-#150+ revalidation 4→2). §3.7 verified-finding entry gets a SHIPPED banner with as-shipped narrative + Phase 1 limitations. §6.7 implementation-notes REWRITTEN — the original spec described soft-gate (which would deliver $0 EV); the AS-SHIPPED section documents hardware cancel-and-rearm. Original soft-gate spec preserved below the as-shipped section as "DO NOT IMPLEMENT" historical context (so future agents debugging "why doesn't the code match the original spec?" find the answer).RULEB_ENABLED=false (production default — operator must explicitly enable). When enabled, RULEB_MODE='dry_run' logs every cancel/rearm action it WOULD take, without IBKR side effects.SKIP_NO_EARNINGS_DT for fresh entries (only for restored positions if any).RULEB_MODE='active' (~1 LOC env-var change in operator's prod .env). May also include restart-safe earnings_release_dt lookup IF Phase 1 logs show that's needed in practice.signal=None per position_manager.load_persisted_positions:1155 → cannot derive earnings_release_dt → RuleB skips them with SKIP_NO_EARNINGS_DT + once-per-position warning log. Most positions don't span bot restarts at current operations cadence (HPQ has been the only open position for a few days but its window passed before today's deploy). Three candidate Phase 2 approaches if needed: (a) add condition_id to ActivePosition + query earnings_markets at decision time; (b) add earnings_release_dt_scheduled column to trading_positions (migration); (c) back-derive from scheduled_exit_time + DEFAULT_TIMINGS (fragile).systemctl is-active earnings-trader polymarket-dashboard polymarket-collector polymarket-live-trades polymarket-integrity-monitor → all active.curl /api/integrity → integrity 47/47 OK preserved (no rule changes).pytest tests/unit/test_ruleb_window_gate.py → 25/25 PASS on prod.scripts/strategy_performance_analysis.py regen so dashboard reflects IMPLEMENTED-DRY-RUN status immediately.curl /api/architecture-findings should show Finding #305 (count 213 → 214).RULEB_ENABLED=false default means _process_ruleb_window_gate does NOT run on prod's main loop. Verify by journalctl -u earnings-trader | grep "ruleb_window_gate" → should return empty (the watchdog stage marker is only set when RULEB_ENABLED=True).src/earnings/ruleb_window_gate.py (NEW, ~180 lines).src/earnings/trading_orchestrator.py (~220 lines added: 4 new methods + main-loop hook).src/earnings/config.py (~25 lines added: 4 env vars + comment block).tests/unit/test_ruleb_window_gate.py (NEW, ~225 lines, 25 tests).scripts/strategy_performance_analysis.py (queue rank-2 row IN-QUEUE → IMPLEMENTED-DRY-RUN).docs/FINDINGS.md (new Finding #305 entry).docs/STRATEGY_PLAYBOOK.md (§2 IMPLEMENTED + IN-QUEUE renumbering; §3.7 status banner; §6.7 AS-SHIPPED narrative + original-spec preserved as historical).docs/AGENT_HANDOFF.md §11 (header + this block).docs/CONTEXT.md (Last-Updated header + What changed bullet).scripts/strategy_performance_analysis.py on prod to refresh the JSON with the IMPLEMENTED-DRY-RUN row (matches today's earlier pattern). Operator's separate later session: enable RULEB_ENABLED=true in prod .env, restart trader, monitor dry-run logs.docs/STRATEGY_PLAYBOOK.md §7 deferred items: counterfactual replay of sizing variants, gate-retune candidates, depth-gate-v2 decomposition. Each has existing data infrastructure (Finding #240 equity_bars_1m, Finding #243 PM snapshot JSONB, Finding #246 price_snapshots backfill, Finding #247 SEC EDGAR timestamps).Second piece of Phase 3 Strategy Review work landed today, on top of the Finding #279 Phase B amendment commit (dc13ff3) + dashboard-refresh commit (27501b8). Code-bearing intervention; bias-gauntlet PASS; first selection-rule change in the Action Queue since Mar 9, 2026 (Finding #202 LONG-≥90 % skip). No live-trading change beyond the new skip semantics; no integrity-monitor rule change; no DB migration.
STRATEGY_PLAYBOOK.md §2 Rule #1 has been IN-QUEUE rank 1 since Apr 22 (v3 audit Apr 22 §3 Finding B) — confirmed STRONGER on Apr 25 re-validation (Finding #232). Re-confirmed on May-28 fresh data regen at the start of today's Phase 3 work: same 22 LONG-downtrend trades, same 5W (23 %), same −$136.30 total (no new closures since 2026-05-07 = 21-day execution drought per PM volume-collapse evidence). Bias-gauntlet ALL 4 PASS; bucket is robustly negative ex-top-2 (−$175 — winner-masked); full-population spillover positive. Selection-rule with no execution dependency, smallest implementation surface (~40 lines code + 7 unit tests)._build_trading_signal: (A) before volume gate per Playbook §6.1 spec / (B) after volume gate matching existing skip-rule pattern / (C) with explicit dual-log. Operator chose (B) — match existing pattern for consistency, deviates from Playbook spec but is operationally cleaner (the inflate-vs-realism trade-off on "live evidence ≥30 trades" gauntlet trigger doesn't matter because Rule #1 isn't on a live-evidence ship gate).src/earnings/signal_generator.py):_safe_get_regime() -> Optional[str] at line ~1067 (immediately after _regime_fields_for_log). 17 lines including docstring. Returns the current SPY regime string ("downtrend"/"uptrend"/"sideways"/"unknown") or None on every failure path. Mirrors the existing _regime_fields_for_log pattern: getattr for missing-provider, try/except for get_snapshot() raises, DEBUG-level logging on errors. Re-uses the existing self._regime_provider: MarketRegimeProvider instance (no new dependency injection needed)._build_trading_signal at line ~1213 (immediately after the existing small_cap_long_above_80 check, before calculate_entry_exit_times). 22 lines including bias-gauntlet-summary context comment. Logic: if direction == SignalDirection.LONG: current_regime = self._safe_get_regime(); if current_regime == "downtrend": logger.info(...); return None, "long_downtrend". Skip-reason string "long_downtrend" matches the pattern of existing reject codes (snake_case + scenario-descriptive). Caller (BMO loop line ~1606 / AMC loop line ~2052) handles the opportunity-log emission with skip_stage="generate_signal" + skip_reason="long_downtrend" automatically via existing _emit_pm_opportunity_log call.tests/unit/test_signal_regime_gate.py — NEW file, ~185 lines):test_long_in_downtrend_is_skipped — core rule: LONG + downtrend → (None, "long_downtrend").test_long_in_uptrend_is_not_skipped_by_rule1 — LONG + uptrend → no skip.test_long_in_sideways_is_not_skipped_by_rule1 — LONG + sideways → no skip (sideways LONG +$89/39tr bucket preserved).test_short_in_downtrend_is_not_skipped_by_rule1 — SHORT + downtrend → no skip (downtrend SHORT +$172/3tr direction-asymmetric bucket preserved per Playbook §3.1).test_long_with_unknown_regime_is_not_skipped — fail-soft on classifier "unknown".test_long_with_no_regime_provider_is_not_skipped — fail-soft on missing _regime_provider.test_long_when_regime_provider_raises_is_not_skipped — fail-soft on provider Exception.docs/FINDINGS.md Finding #304 for full trace including line numbers, simulate, failure modes, acceptance test results._build_trading_signal (intraday / long_conviction_max_threshold / conviction_deadband / volume_total_missing / low_volume / small_cap_short / small_cap_long_above_80). Rule #1 follows the post-volume-gate pattern matching small_cap_* rules. Does NOT call log_skipped_signal directly because the upstream _emit_pm_opportunity_log caller already handles the opportunity-log row via skip_stage="generate_signal" — no double-logging.scripts/strategy_performance_analysis.py:build_action_queue): queue rank-1 row status flipped IN-QUEUE → IMPLEMENTED; rank: 1 → rank: None (per the existing schema convention for shipped rules); implemented_date: "May 28, 2026"; implemented_findings: ["#304"]; live evidence + bias check text refreshed against May-28 fresh data; next_action rewritten to "SHIPPED..." + live acceptance test instructions. Will be visible on the dashboard 🎯 Action Queue section after either (a) the Sun 2026-05-31 13:00 UTC weekly timer fires the JSON regen, or (b) a manual python3 scripts/strategy_performance_analysis.py post-deploy run (which I'll do as part of the §G probe so the IMPLEMENTED row shows TODAY rather than Sunday).docs/STRATEGY_PLAYBOOK.md): §2 — Rule #1 row moved from 🟢 IN-QUEUE table to ✅ IMPLEMENTED table; remaining IN-QUEUE rules renumbered (RuleB rank 2→1, 3-stop breaker 3→2, trade #150+ revalidation 4→3). §3.1 verified-finding entry gets a "✅ IMPLEMENTED May 28, 2026 (Finding #304)" status banner + ship-notes blockquote. §6.1 implementation-notes block expanded with AS-SHIPPED line numbers + acceptance-test results.systemctl is-active earnings-trader polymarket-dashboard polymarket-collector polymarket-live-trades polymarket-integrity-monitor → all active.curl /api/integrity → integrity 47/47 OK preserved (no rule change introduced this commit).pytest tests/unit/test_signal_regime_gate.py → 7/7 PASS (pre-commit verification; will re-run post-deploy for confidence).curl /api/architecture-findings should show Finding #304 (count 212 → 213).src/earnings/signal_generator.py (helper + skip block; ~40 lines added including comments).tests/unit/test_signal_regime_gate.py (NEW file; ~185 lines).scripts/strategy_performance_analysis.py (queue rank-1 row update).docs/FINDINGS.md (new Finding #304 entry; supersedes the previous "next-free is #304" placeholder).docs/STRATEGY_PLAYBOOK.md (§2 IMPLEMENTED table addition + IN-QUEUE renumbering; §3.1 status banner; §6.1 implementation notes expansion).docs/AGENT_HANDOFF.md §11 (header + this block).docs/CONTEXT.md (Last-Updated header + "What changed this week" bullet).scripts/strategy_performance_analysis.py on prod to refresh the JSON with the IMPLEMENTED row, commit the regen as chore(reports): refresh ... post-Finding #304 ship (same pattern as today's 27501b8 JSON-refresh commit), push + deploy. Dashboard's Action Queue will then show the moved row immediately rather than waiting for the Sun 2026-05-31 13:00 UTC weekly timer.First piece of Phase 3 Strategy Review work landed today, ~50 hours after the May 26 Phase 3 un-gate. Doc-only intervention; no code, no live-trading change, no integrity-rule change.
finding_a_full.csv was Apr-25 (33 days), PM volume-collapse evidence was chat-only May 19 (no canonical entry). Operator approved the 6-step plan ("start executing all the steps above fully in line with memory.md").stash@{0} from Finding #303's hygiene cleanup contained a strategy_analysis_report.json from generated_at = 2026-05-25T06:30:24-04:00 (fresher than the on-disk committed version which was 2026-04-21T15:30:48-04:00 — the new polymarket-strategy-analysis.timer shipped via 645d666 had not yet fired; first natural fire Sun 2026-05-31 13:00 UTC). Saved to /tmp/strategy_report_2026-05-25_from_stash.json for delta-compare; pm_earnings_arb subtree's h1_longshot_24h.csv stashed file left untouched (deferred to that workstream's handoff per memory §C surgical-changes — fold-in or drop is a parallel-workstream decision).venv/bin/python3 scripts/strategy_performance_analysis.py, ~15 s wall) — fresh numbers matched the May-25 stash byte-for-byte: 116 clean trades (43 W / 73 L, WR 37.07 %, net +$212.21, avg +$1.83/trade); 119 all-trades incl. 3 bug-affected (WR 36.97 %, net +$207.72); stop rate 51.72 %. The strategy has 0 new clean closures since 2026-05-07 (last closure was id=162 MCD; next entry id=163 HPQ entered 2026-05-27 14:34 UTC, still open as of 12:30 UTC May 28). 119-vs-126 gap vs trading_positions is fully accounted for by the analysis-script filter (excludes 7 reconciliation/manual_recovery/duplicate_merged entries, all from Jan 2026).venv/bin/python3 scripts/analyze_finding_a_full.py, 6m 50s wall, equity-bars cache populated for 95 new tickers) — n=121 on all-closed population. RuleB strengthened: headline +$236/100, ex-top-2 +$127/100 (was +$123/100 on Apr-25 baseline of n=51), ex-repeat-tickers +$239/100 — passes all 4 gauntlet checks robustly. RuleG (window+ATR): headline +$242 but ex-top-2 only +$105 — higher headline / weaker robustness than RuleB; and RuleG is materially more invasive (~25 lines for RuleB vs full ATR(14) machinery for RuleG). FA_hard retraction reasoning still holds on fresh data: rearm_gap_open arm n=24 at $-370.90 = -$1,545/100; that one mechanism alone outweighs every other arm. Two halt-suspect trades flagged (ARES 2026-02-05, NYT 2026-02-04) — data-quality observation only, no action.earnings_markets LEFT JOIN markets USING (condition_id) shows Dec 98.1 % → Jan 100.0 % → Feb 95.9 % → Mar 27.8 % (first cliff) → Apr 64.2 % (partial recovery) → May 1.9 % (3 of 154 markets). Jan→May average cumulative volume dropped from $33,475 to $306 (~109×); median from $24,730 to $13.39 (~1,847×). NO Mid-cap market in all of May crossed the $2k gate (0/74); 3 May gate-passers all had specific reasons they didn't produce entries (AMPL conv 12 % → SHORT skip per Finding #207 sub-rule; NVDA conv 97 % → LONG skip per Finding #202; LOW conv 89 % Large LONG → no entry reason not on current investigation surface, sub-question for Phase B planning). HPQ entered yesterday via Finding #282's Gamma-cumulative fallback path (DB volume_total = $23.84 but volume_threshold_met = True recorded at entry → effective max(DB, Gamma) ≥ $2k at gate-decision instant). Data-capture gap surfaced (NOT actioned today per memory §C): the cumulative-volume value the gate actually decides on is not persisted as a trading_positions column; for forensic-grade replay we'd need effective_cumulative_volume_at_entry. Defer to Phase 3 simulator design when the consumer ships.AskQuestion (NO SHORTCUTS rule #1 — surface tradeoffs, don't silently interpret):docs/FINDINGS.md Finding #304. The two events are independent: volume-collapse evidence is folded into #279; Rule #1 ship is #304. This is the cleanest interpretation of operator choice (B) — "fold the EVIDENCE into #279" was about the evidence's home, not about reserving the 304 number indefinitely.) Rationale for the fold-in: the volume-collapse evidence is INPUT to the sizing+priority+gating decision #279 Phase B already owns; folding-in preserves canonical single-source per memory §J; "Finding #298 candidate" chat-label retired in favor of the #279 subsection.docs/FINDINGS.md Finding #279 entry: status line updated (May-28 amendment + scope-expansion note) + new "Phase B framing premise — PM earnings-market volume collapse (May 28, 2026 amendment)" subsection appended with the per-month / per-mcap / per-gate-passer tables + Phase B scope expansion (1-5) + data-capture gap mention + saved-artifacts cross-reference list.docs/STRATEGY_PLAYBOOK.md §1 verdict-banner: new May-28 refresh block at top with fresh trade numbers + RuleB simulator update + Phase 3 first-ship note; status box updated "verified still current as of May 28, 2026 afternoon" + Apr 26 → May 28 intervening-work block now includes May 11-28 operational hygiene rollup + new "May 28 doc-only amendment to Finding #279" line + "next-up Finding #273 then Rule #1" sequence. §7 deferred-bundle row for sizing-priority-v2 rewritten to include the May-28 amendment, the volume-gate retune scope, and the per-month / per-gate-passer evidence summary inline.docs/AGENT_HANDOFF.md §11 last-updated header + this block.docs/CONTEXT.md Last-Updated header + What-changed-this-week bullet.build_action_queue() row added or modified (the existing sizing-priority-v2 row stays DEFERRED-POST-PHASE-3 — the amendment is informational scope-expansion, not a status change). No integrity-monitor rule changes. No live-trading change. No deploy required for behavior — but per memory §I, will still git pull on prod so the dashboard's /api/architecture-findings re-renders FINDINGS.md (dashboard reads the doc directly per memory §J — operator-facing surface refresh).signal_emitted rows) — operator-chosen "D then A" sequence. Pre-coding checklist (memory §A) + §B 5-step + §E same-pattern + §G probe runway live there.Operator-directed fix for the recurring prod-git operational-hygiene problem surfaced in Finding #302's deploy (where 3 successive git pull --ff-only aborts forced fallback to memory.md §H scp single-file hotfix). Operator-approved via AskQuestion: divergent-handling = stash_continue (preserve operator work, don't block deploy, no Telegram alert), scope = script_only.
git pull --ff-only refuses to overwrite the prod working-tree copy with the same content via the incoming commit (error: working tree files would be overwritten by merge). Plus modified-tracked files have the same pattern.deploy/deploy_from_github.sh lines 5+: new git_hygiene_cleanup() function (~80 lines including ~40 lines of context comments) inserted BEFORE the existing git pull --ff-only. Function logic per Finding #303 in docs/FINDINGS.md:rm (pull re-creates)git checkout HEAD -- <file> (pull re-applies same content via incoming commit)git stash push -m "auto-stash before deploy <ISO-timestamp> (Finding #303)" -- <files> + WARNING log + restore/drop commands. Operator reviews on next opportunity.bash -n deploy/deploy_from_github.sh clean ✓ (verified pre-commit)bash deploy/deploy_from_github.sh; observe hygiene function reports actual cleanup counts; observe git pull --ff-only succeeds; observe service restarts complete; observe git stash list shows the 2 divergent files preserved.deploy/deploy_from_github.sh (new git_hygiene_cleanup() function + invocation)docs/FINDINGS.md (#303 entry — full §B 5-step + simulation table)docs/AGENT_HANDOFF.md §11 (this block + last-updated header)Operator-directed root-cause investigation of the systemd_last_run:polymarket-backup.service STATUS_FAIL surfaced via the dashboard banner earlier today. Resolved same session via operator-approved fix-path (A) "move backup timer to 05:00 UTC" through AskQuestion.
🚨 INFERENCE DISCIPLINE rule was applied — agent verified the model against actual code (read service unit + script + journal + DNS state + historical log scan + cross-timer schedules) BEFORE declaring a root cause. The discipline paid off: surface-level reading would have called this a "transient DNS blip"; the actual cause is a deterministic schedule overlap with Ubuntu's nightly unattended-upgrade.apt-daily-upgrade.service started at 04:11:35 UTC; processed a systemd-related package and restarted systemd-resolved + systemd-networkd + systemd-timesyncd at 04:11:54 UTC; polymarket-backup.timer fired at 04:11:53 UTC (a coin-flip into the same RandomizedDelaySec window — both timers configured for 04:00 + 30min jitter on this droplet); pg_dump's DNS lookup hit dead systemd-resolved mid-restart and exit-1'd with could not translate host name "...digitalocean.com" to address: Temporary failure in name resolution; integrity rule flipped STATUS_FAIL.polymarket-backup.timer (OnCalendar=*-*-* 04:00 + 1800s jitter) and apt-daily-upgrade.timer (overridden on this droplet to OnCalendar=*-*-* 04:00 + 30m jitter) have been overlapping every night since both were configured, but a 3-condition alignment is needed for the failure: (1) random delays land the timers within ~20s of each other, (2) today's upgrade payload includes systemd, (3) the package's post-install triggers service restarts. 14-day historical scan of /var/log/polymarket/backup.log.*.gz via zgrep found ZERO prior DNS errors — today was the first instance.deploy/systemd/polymarket-backup.timer OnCalendar=*-*-* 04:00:00 → OnCalendar=*-*-* 05:00:00 (kept RandomizedDelaySec=1800 so jitter remains; new fire window 05:00-05:30 UTC, fully clear of apt-daily-upgrade's latest possible end at 04:30 UTC). Added 16-line context comment to the timer file explaining the collision history (per memory.md §H — context belongs WITH the configuration).scp direct to /etc/systemd/system/ (memory.md §H "single-file hotfix fallback"), NOT the usual git pull + install flow. Reason: prod's deploy_from_github.sh + git pull were BLOCKED by accumulated working-tree state (see "Prod-git operational-hygiene issue surfaced" below). Worked around by direct scp of the timer file + daemon-reload + restart polymarket-backup.timer. Timer file is fully self-contained; this bypass introduces no risk for THIS fix specifically. The broader prod-git issue still requires operator decision (see follow-up section).systemctl start polymarket-backup.service post-install: created polymarket_db_20260528_072747.sql.gz (~2.9 GB), exit 0, Result=success. Local cleanup correctly removed May-20 and May-21 (>7d) per the keep_recent_days=7 + keep_at_least_one=True policy. Subsequently Persistent=true on the new timer triggered a CATCH-UP fire at 07:43:37 UTC (a second backup of the same day; harmless, just adds disk usage; will be cleaned naturally on 7-day retention).systemctl cat polymarket-backup.timer | grep OnCalendar confirms new schedule on prod; systemctl list-timers polymarket-backup.timer shows NEXT as Fri 2026-05-29 05:23:21 UTC; integrity-monitor's systemd_last_run:polymarket-backup.service flipped STATUS_FAIL → STATUS_OK; total integrity-monitor FAIL count dropped from 2 → 1 (the remaining systemd_last_run:etfstack-execute.service is etfstack-subtree scope, not main project).git pull --ff-only aborted THREE TIMES in succession on different categories of working-tree state:earnings_dashboard/templates/dashboard.html, scripts/equity_bars_collector.py, scripts/strategy_analysis_report.json, src/data_tracker/integrity_monitor.py, src/earnings/market_cap_data.py, strategies/pm_earnings_arb/reports/h1_longshot_24h.csv, strategies/pm_earnings_arb/src/data_loader.py) — 5 of 7 byte-identical to origin/master (working-tree had been edited on prod, then those exact edits later committed to remote by parallel workstream; not yet reset locally), 2 differ: scripts/strategy_analysis_report.json (local=271.8KB, remote=185.4KB — local is newer regen) and strategies/pm_earnings_arb/reports/h1_longshot_24h.csv (same size, different content). The 2 differing files were NOT touched per INFERENCE DISCIPLINE (could lose work).git pull --ff-only. This will likely recur on every future deploy until either (a) the subtree's workflow changes to commit-before-prod-edit, or (b) a routine git stash --include-untracked is added to the deploy script's pre-pull step.systemd_last_run:polymarket-backup.service rule briefly showed STATUS_UNKNOWN with message "never ran (Result=success)" while the manual backup was MID-EXECUTION. Root cause: my Finding #296 reorder treats empty ExecMainExitTimestamp as "never ran" — correct for newly-installed units but WRONG for currently-running units (which also have empty ExecMainExitTimestamp because they haven't exited yet). Self-resolves once the run completes. Fix would require also checking ActiveState (e.g., ActiveState in ("activating", "active") + empty exit_ts → STATUS_OK "running" or just skip / leave previous-cycle's status). Not actioned today; flagged for separate operator decision.backup_database.py — generic resilience but not needed given the 14-day scan showed zero non-collision DNS failuresapt-daily-upgrade.timer to Ubuntu default 06:00 — would require understanding why the override was madedeploy/systemd/polymarket-backup.timer (schedule edit + 16-line comment), docs/FINDINGS.md (Finding #302 entry), docs/AGENT_HANDOFF.md §11 (this block + last-updated header).🚨 INFERENCE DISCIPLINE rule codified + IBKR auto-recovery from mid-week disconnect)Two distinct things landed today:
1. New .cursor/memory.md rule: 🚨 INFERENCE DISCIPLINE — VERIFY MODEL BEFORE DECLARING
Triggered by an agent false-alarm earlier today. Operator asked the agent to do an end-to-end readiness check on yesterday's AMC trades (May 27); agent traced position-sizing + priority + exposure-cap code thoroughly but did NOT trace the exit-scheduling code path. As a result, the agent reported AMC as a "~3.5h same-day hold" when the actual strategy is a ~29h next-trading-day-exit hold (Day-1 morning entry → Day-2 afternoon exit, spanning the after-close earnings announcement).
When the agent investigated today's open HPQ position (entered yesterday morning, scheduled to exit today 19:57 UTC per the correct AMC scheme), it declared the scheduled_exit_time = 2026-05-28 19:57 UTC to be a "+24h scheduling bug" and surfaced it as URGENT capital-risk to the operator — proposing "manually flat HPQ now" as a remediation option. Operator caught the error instantly: "why do you think AMC trades are not overnight? this is a major thinking error on your side, AMC trades are opened on day 1 early in the session and closed on day 2 late in the session as per our trading scheme, so there is no error!"
Code confirms operator: src/earnings/trading_orchestrator.py:17 file-level docstring literally says "AMC Exits: Execute at 3:57 PM (next day)"; _calculate_scheduled_exit_time_from_db at lines 1358-1360 explicitly calls self._market_calendar.get_next_trading_day(entry_time). The agent had every opportunity to see this and missed it on the first pass.
Root cause: agent built conclusions on an unverified mental model and propagated to URGENT escalation without ever reading the relevant code. Same logical-trap shape as the May-17 2FA inference error (treating absence of evidence as evidence of absence), but applied to investigation instead of inference about hidden human action. Second instance in 12 days of this category of error → operator approved codifying as a permanent project rule.
What shipped:
- .cursor/memory.md: new 🚨 INFERENCE DISCIPLINE — VERIFY MODEL BEFORE DECLARING section between NO SHORTCUTS and TRADE EXECUTION PROHIBITION. Includes: (a) the rule statement + 3-step verification process, (b) both anchoring incidents in narrative detail (May 17 + May 28), (c) common-shape analysis identifying the unverified-mental-model pattern, (d) 6-item self-audit checklist to run before any anomaly declaration, (e) why-it-matters-for-this-project rationale.
- .cursor/memory.md: §A pre-coding checklist gets a single-line cross-reference to the new section (peer to the existing NO SHORTCUTS pointer).
- Scope decided via AskQuestion: operator chose medium scope (memory.md + this handoff entry, NOT full-cascade to CONTEXT.md + STRATEGY_PLAYBOOK.md like NO SHORTCUTS received) and single-banner visual weight (🚨 prefix, not the triple-banner gravitas of NO SHORTCUTS).
- Doc-only commit; no prod deploy needed.
2. IBKR live mid-week auto-recovery (separate from rule codification)
At 04:12:00 UTC today, IBKR live Gateway dropped the trader's session (Peer closed connection); Gateway exited cleanly (status=0/SUCCESS). Two notable aspects: (a) timing did not match the documented patterns — neither Sunday weekly auth (today is Thursday) nor the daily 06:00 UTC internal-restart window (this was ~1h 50min earlier than that). May indicate an IBKR-side off-schedule push or a network blip. (b) systemd auto-restart attempt #2 (counter=1) brought port 4001 back at 04:17:16 UTC, and the trader auto-reconnected at 04:18:17 UTC without any operator intervention — total downtime 6m 17s. Trader correctly blocked all trading operations during the disconnect window (hardware stops remained active per design). Post-recovery: position reconciliation completed, "✅ IBKR RECONNECTED and verified healthy" alert fired. HPQ position (22 shares @ $24.65 avgCost, stop-loss order 1044546 GTC) survived intact through the disconnect with no exposure change.
Observability gap surfaced (NOT actioned today — for future operator decision): the integrity-monitor's artifact:ibkr_snapshot rule reports STATUS_OK during the disconnect because the trader keeps writing the snapshot file (just with connected: false). The rule checks file FRESHNESS, not file CONTENT. A future rule reading snapshot.connected == True would have caught the disconnect proactively — possible Finding #299 if scoped.
Files (this commit):
- .cursor/memory.md (new INFERENCE DISCIPLINE section + §A cross-reference)
- docs/AGENT_HANDOFF.md §11 (this block + last-updated header)
Second-week-in-a-row standard Sunday 2FA workflow per IBKR_CONTINUITY.md + handoff Decision 13. Operator-requested via "reconnect ibkr now".
ibgateway-ibc.service shown active (running) since Sun 2026-05-17 06:48:08 UTC; 1 week 0 days ago — exactly the weekly cadence. Port 4001 NOT listening; snapshot connected=False, account=None, error="IBKR not connected".systemctl restart ibgateway-ibc → new MainPID 2126135.cf56485 and does NOT generalise to live.)connected=True, account_id=U2…13, net_liquidation=27537.50, buying_power=110116.56, available_funds=27529.14. Trader auto-restarted (new MainPID 2126135, "Active since Sun 2026-05-24 09:23:27 UTC") and detected the IBKR reconnect on its retry loop.artifact:ibkr_snapshot flipped FAIL → OK; total integrity 41/45 OK + 4 FAIL.systemd:earnings-trader.service — "expected=active got=activating" — integrity-monitor saw the trader mid-restart and cached activating. systemctl is-active returns active now. Transient cache lag; self-resolves on next 5-min cycle.freshness:etfstack_equity_curve — last write 12,712m ago (~8.8 days), threshold 4500m (~3.1 days). etfstack subtree scope — belongs to that workstream's separate handoff. NOT actioned here per the "fully isolated" framing in the original handoff addendum. Update: parallel commit cf56485 (Finding etfstack-006, shipped same-day later) automates the paper Gateway Sunday reconnect — root cause was paper Gateway port 4002 silently dead since May-17 weekly auth, causing etfstack-execute to fail every weekday. These two etfstack rules should self-resolve over the next ~few days as etfstack-execute fires successfully on Mon-Fri.systemd_last_run:etfstack-execute.service — last run exit code 1. etfstack subtree scope — same as #2 above; addressed in cf56485.artifact:strategy_analysis_report — 350h stale vs 336h threshold (14h past expiry). Main project scope but a SEPARATE operator decision from the 2FA reconnect — per memory.md §K the report is regenerated via cd [hidden] && source .env && export DATABASE_URL && venv/bin/python3 scripts/strategy_performance_analysis.py (typically ~5-15 min wall time). Surfaced for operator awareness; NOT silently regenerated per NO SHORTCUTS pattern #9 (mirror form: don't add scope without approval).docs/AGENT_HANDOFF.md §11 (last-updated header + this section).Operator-directed standalone close-out of the pre-existing systemd_last_run:polymarket-api-health.service FAIL that surfaced during the §G post-deploy probe of Finding #296 on May 11. Operator-approved fix-path (b) "WARN-but-exit-0" via AskQuestion (vs (a) widen threshold / (c) leave-as-is / (d) build age-aware structural fix).
api_health_checker.py:check_price_accuracy flags a per-check WARNING when even 1 sampled market's DB-snapshot price drifted >5% absolute vs live Gamma — but the warning text the check itself writes is "N prices differ - may be normal market movement". Run-level rollup then propagated WARNING → exit 2 → systemd Result=exit-code → systemd_last_run:* STATUS_FAIL → HIGH-severity Telegram alert. Empirical false-alarm rate ~14% (1/7 daily fires May 10 → May 17) on a small sample; would rise with market volatility / earnings clusters. Training the operator to filter price_accuracy alerts is the cry-wolf antipattern that violates memory.md Principle 5 ("Explicit over implicit").api_health_checker.py:main (exit(2) → exit(0) for the warning branch) + 16-line context comment explaining the (Finding #297) decision + new logger.warning("Overall WARNING result — exiting 0 by design (Finding #297). See data/api_health/latest.json for per-check details.") so the next agent reading /var/log/polymarket/api_health.log doesn't get confused. error → exit 1 and healthy → exit 0 paths UNCHANGED — real failures still alert via systemd_last_run.grep -E '\bexit\(2\)|sys\.exit\(2\)' returned 2 matches: (a) this file (FIXED), (b) deploy/backup_database.py:287 which exits 2 on upload_failed — that's a REAL S3-upload failure that SHOULD continue to alert via systemd_last_run:polymarket-backup.service; NOT the same antipattern (no warning-classification intermediate state). Repo-wide grep -E 'overall_status.*warning|status.*=.*[\"\\x27]warning[\"\\x27]' showed only api_health_checker.py mixes warning-classification with exit-code propagation. Other 9 batch services in integrity_monitor.BATCH_SERVICES (polymarket-backup, polymarket-resolver, polymarket-trade-reconcile, polymarket-equity-bars, polymarket-data-backfill, polymarket-earnings-sync, polymarket-trades-backfill, etfstack-signal, etfstack-execute) — all checked, none has the WARN→exit-non-zero antipattern.docs/FINDINGS.md for full trace. Acceptance tests: (a) AST clean; (b) manual systemctl start polymarket-api-health.service post-deploy exits 0 even with a price-drift WARNING in latest.json; (c) systemd_last_run:polymarket-api-health.service flips FAIL → OK on next 5-min cycle; (d) latest.json content unchanged — drift events still recorded for dashboard/operator review.systemctl start polymarket-api-health.service triggered; result captured; curl /api/integrity confirms systemd_last_run:polymarket-api-health.service STATUS_OK and total flips 44/45 → 45/45 OK.price_snapshots.captured_at lookup + threshold matrix + simulator testing on historical drift. Revisit only if (b) proves insufficient (e.g., a REAL broken-pricing event slips through as "warning"). Possible future Finding #298 — per-warning Telegram alerts via a new latest.json content-reading rule — if operator decides they want explicit price-drift surfacing beyond the current "WARN visible in latest.json + dashboard, no Telegram noise."src/data_tracker/collectors/api_health_checker.py (exit-code rewrite + 16-line context comment + logger.warning) + docs/FINDINGS.md (#297 entry) + docs/AGENT_HANDOFF.md §11 (this section + last-updated header) + docs/CONTEXT.md (Last-Updated header + What changed this week bullet).Operator-requested manual reconnect via the standard weekly Sunday 2FA workflow (per IBKR_CONTINUITY.md + original handoff Decision 13).
ibgateway-ibc.service shown active (running) since Sun 2026-05-10 10:00:12 UTC; 6 days ago (IBC controller wrapper alive, but Java GW process had exited internally during the IBKR weekly auth event). Port 4001 NOT listening. Snapshot connected=False, account=None, error="IBKR not connected". Trader looping IBKR connection attempts every 120s.systemctl restart ibgateway-ibc → new MainPID 1601153 → IBC controller spawned, Gateway launched.connected=True, account_id=U2…13, net_liquidation=27534.56, buying_power=110116.56, available_funds=27529.14, error=null. Trader log shows [ALERT] INFO system_startup: Earnings Trading Bot started + Sunday market-holiday detection → ⛔ Market holiday - skipping trading operations for 2026-05-17 (correct — no trading on Sunday).artifact:ibkr_snapshot fail -- stale (0h old, threshold 0h) — message-format quirk in _check_artifact_mtime (integer-hour formatting for sub-hour ages, both age and threshold render as "0h"). Flipped back to OK at 06:54:02 UTC when integrity-monitor re-evaluated against the fresh snapshot. Cosmetic-only; no false alarms fired during the window.freshness:trades unknown -- evaluator raised OperationalError: out of shared memory — documented Postgres OOM-on-locks per POSTGRES_OPERATIONS.md Finding #216. Self-resolved on next cycle.systemd_last_run:polymarket-api-health.service from the May-16 08:00 UTC daily fire flagging price_accuracy: 1 prices differ - may be normal market movement → script exits code 2 → rule correctly FAIL — NOT related to today's reconnect)._check_systemd_unit_last_result reorder) closed via Finding #296 on May 11. Items #1 (Sunday-2FA partial-automation) + #3 (Sunday-aware ibkr_snapshot threshold) remain explicitly deferred per AskQuestion. Today's reconnect would have benefited from item #1 — operator was awake-and-available; automation would have run the restart at 06:05 UTC and put the trader back online ~43 min earlier. Recording the data point for future operator decision.docs/AGENT_HANDOFF.md §11 (last-updated header + this section)._check_systemd_unit_last_result branch reorder — closes 1/3 of the May-10 maintenance items)Standalone close-out of the lowest-risk item from the 3-item "observed-but-not-actioned" list at the bottom of the May-10 end-of-day session summary. Operator approved option (A) "ship #2 only" via AskQuestion (vs B: ship #1+#2 / C: ship all three / D: defer all three / E: different).
artifact:ibkr_snapshot threshold widening) were deferred per AskQuestion because they change operator workflow + alert semantics and need explicit approval beyond "close the maintenance backlog."Result=success + ExecMainExitTimestamp= (empty) for any unit that has never been started. The previous branch ordering in _check_systemd_unit_last_result (lines 318-326 of integrity_monitor.py) checked the success branch BEFORE the never-ran branch, so never-fired oneshot units returned a misleading STATUS_OK with text "last run succeeded (dead)". Same false-OK applied to LoadState=not-found units.systemctl show -p Result -p ExecMainExitTimestamp for three shapes: (a) fired oneshot (polymarket-api-health.service, fired 08:01 UTC today) → Result=success + non-empty timestamp = correctly STATUS_OK pre AND post fix. (b) Never-fired probe oneshot (created via daemon-reload only) → Result=success + empty timestamp = STATUS_OK pre-fix (BUG), STATUS_UNKNOWN post-fix (CORRECT). (c) Non-existent unit name → identical shape + LoadState=not-found = same false-OK pre-fix, correctly STATUS_UNKNOWN post-fix._check_systemd_unit_last_result is defined ONCE in integrity_monitor.py and used by 8 lambda evaluators in _build_rules(); no external callers; no unit tests exist. Companion helper _check_systemd_timer_recent does NOT have the same bug (already checks empty LastTriggerUSec first). The other 4 rule categories don't depend on systemd defaults. No same-pattern violations elsewhere._check_systemd_unit_last_result (12-line context comment + 4-line move). Never-ran branch (not run_result or exit_ts in ("", "n/a", "0")) returns STATUS_UNKNOWN BEFORE the success branch can claim STATUS_OK on a default-success-empty-timestamp tuple. Failure branch unchanged.docs/FINDINGS.md. 5-case inline smoke test (fired/never-ran/failed/not-found/n/a-timestamp) executed locally pre-commit; all 5 produce the correct status.curl /api/integrity returns 45/45 OK; the 8 systemd_last_run:* rules continue STATUS_OK; no regression. Forward observability: the next time a new oneshot unit is added (e.g., a future Sunday-2FA timer if/when item #1 is ever approved), the brief pre-first-fire window will now correctly show UNKNOWN instead of falsely OK.LoadState=not-found case is now also returned as UNKNOWN (vs the previous false OK). Considered but rejected: explicitly returning STATUS_FAIL for not-found would be more diagnostic but expands scope; would also need an additional systemctl show -p LoadState field. Deferred as a possible #297 candidate IF a misnamed unit ever slips through. Currently no systemd_last_run:* rule references a misnamed unit (verified live).src/data_tracker/integrity_monitor.py (branch reorder + 12-line context comment) + docs/FINDINGS.md (#296 entry) + docs/AGENT_HANDOFF.md §11 (this section + struck-through item in the 3-item list above) + docs/CONTEXT.md (May 11 changelog + last-updated header)._check_systemd_unit_last_result + systemd_last_run produced 0 external callers + 0 same-pattern violations; (c) 5-case smoke test pre-commit; (d) AskQuestion formalized which of the 3 items to ship; (e) struck-through (not deleted) the resolved item in the 3-item list so future agents see the closure history; (f) the other 2 items remain explicitly tagged as deferred-pending-operator-approval per memory.md NO SHORTCUTS pattern #9 (mirror form).Final item in operator-decided plan-(C) sequence (#290 → #291 → #289.a → #289.b).
journalctl -u polymarket-live-trades.service" was WRONG. journalctl shows ZERO occurrences in the entire 14-day journal history. The errors are in /var/log/polymarket/live_trades.log (which the systemd unit writes to via StandardOutput=append:, NOT journal). Verified across 10 daily-rotated log files: ~12-13k InterfaceErrors/day, totaling ~118k InterfaceErrors in 10 days.save_trade (lines 219-293) had try/except INSIDE with self.engine.begin() as conn: block:except catches at DEBUG level (invisible at INFO logs), returns Falsewith block exits NORMALLY (no exception propagated)engine.begin().__exit__ thinks success → tries COMMITInterfaceError: in failed transaction blockCycle error handler in run() → service sleeps 5s and continues_seen_trade_ids.add(trade_id) was only called on success path. The failed trade_id was never added. Next 5s cycle re-fetched the SAME bad trade and re-attempted the same failing INSERT — forever. ~12-13k/day.try wraps with engine.begin(), NOT vice versa.with → engine.begin auto-rollbacks → connection returns to pool clean.try catches at WARNING level (visible) with trade_id + exception type + message for forensics._seen_trade_ids.add(trade_id) is called on BOTH success AND error paths — cascade closure.backfill_trades.py:save_trades_batch has a similar inner-try-inside-engine.begin pattern. Not currently affecting prod (timer-driven weekly, batch-save bounds the cascade), but structurally same risk. Documented as a future-deferred sub-finding under #289.b for refactor when next touched. NOT in #289.b scope per memory.md §C surgical-changes.live_poll writes continue at baseline.pg8000.dbapi.ProgrammingError: value too long for type character varying(10). The trades.outcome and trades.side columns are VARCHAR(10). Some Polymarket trades have outcome labels >10 chars (likely from non-binary markets). Recommended follow-up: ALTER COLUMN to VARCHAR(64) OR add truncation in save_trade. The 3 affected trade_ids are documented in the deploy log for forensic follow-up.docs/FINDINGS.md.All 4 operator-approved follow-up findings from session-start handoff (#289.a + #289.b + #290 + #291) shipped today in this session. Plus the bonus cold-bootstrap close-out work (#292 + #288.v3 + #291.v2). Total 7 commits today; net delta of significant cleanup + structural defense + 5 new integrity rules.
Next operator-decision items remaining on the project (per docs/STRATEGY_PLAYBOOK.md §7 deferral bundle):
- Phase 3 Strategy Revamp (gated to ~May 26, 2026 when Finding #240 30-day equity-bars window completes)
- May 26 deferral bundle: #278 (regime detection v2), #279 + #285 Phase B (sizing + priority + exposure-cap re-eval), #283 (depth-gate v2), #273 (pm_v2 NULL bid/spread on signal_emitted rows — fix in lockstep with simulator ship)
- NEW (surfaced today as side-effect findings — not in original handoff scope):
- ✅ Polymarket Website API earnings JSON 404 → RESOLVED via Finding #293 — operator-approved option (C) "URL fix + delete dead code". Investigation surfaced the route was renamed /finance/earnings → /finance (cached 404 with age 18h+ confirmed long-term, not transient); JSON URL /finance.json (without /earnings) returns 200 with same shape. Fixed api_health_checker.py URL + deleted dead EarningsMarketSyncer class (zero callers besides retry test) + removed silently-broken cron entry on prod. Wrapper scripts/sync_earnings_markets.py preserved per Finding #220 manual-sync workflow. §G probe verified polymarket_website check now STATUS_OK; systemd_last_run:polymarket-api-health.service flipped FAIL → OK; daily Telegram noise eliminated.
- ✅ trades.outcome / trades.side VARCHAR(10) too narrow → RESOLVED via Finding #294 — operator-approved option (B) widen BOTH columns. Migration 028 shipped: outcome 10→64, side 10→20. Hypertable-safe (PostgreSQL VARCHAR widening is non-rewriting; 907 chunks, no chunk rewrite needed). ORM database.py updated in lockstep. §G probe verified no service disruption + 60 live_poll writes/10min continuing healthy + 0 new InterfaceErrors.
- ✅ backfill_trades.py:save_trades_batch SAVEPOINT-per-trade fix → RESOLVED via Finding #295 — operator-approved option (A). Restructured save loop to use with conn.begin_nested(): SAVEPOINT per trade (matches existing convention in continuous_collector.py:534 + trading_orchestrator.py:1641). Failed trades isolated via SAVEPOINT rollback; outer transaction stays healthy → end-of-batch COMMIT succeeds for all valid trades (pre-fix: single bad trade silently rolled back ENTIRE batch). Plus per-trade WARNING-level logging (was: only first error per batch at ERROR) + failed counter + summary log. Return signature unchanged so all 4 callers work without changes. §G probe: AST clean, manual systemctl start polymarket-trades-backfill.service runs cleanly post-deploy.
Third item in operator-decided plan-(C) sequence. Operator approved fix-path (B) systemd-timer migration after AskQuestion surfaced trade-offs vs (A) crontab patch.
/bin/sh: 1: source: not found. Ubuntu's /bin/sh is dash; source is a bash-ism. The cron command cd && source venv && python -m && >> log short-circuits at the dash source failure (exit 127); the python invocation NEVER runs; the >> log redirect (part of the chained command) never executes; mail spool empty (no MAILTO) so the dash error went nowhere. Bug existed for 14 days (Apr 27 → May 10). Confirming control: /bin/bash -c '...' runs the same command writes 1010 bytes to log and exits 1 (script ran fine, exits non-zero because Polymarket Website API has been ERROR-ing on earnings JSON 404 for unknown duration — separately surfaced).0 */6 * * * cd [hidden] && source venv/bin/activate && python3 scripts/sync_earnings_markets.py >> ... had the IDENTICAL latent bug. Fixed in same commit via SHELL=/bin/bash at top of root crontab (covers both bugs going forward; future cron entries that use bashisms will work).deploy/systemd/: polymarket-api-health.{service,timer} with bash -c 'source .env && export DATABASE_URL POLYGON_KEY FINNHUB_API_KEY && python -m api_health_checker' ExecStart. Daily 08:00 UTC + 300s RandomizedDelaySec. Persistent=true (resilient to host downtime). Type=oneshot. Logs to /var/log/polymarket/api_health.log AND journal.deploy_from_github.sh install block (~10 lines) — installs both files + daemon-reload + enable --now timer + restart timer (idempotent).systemd_timer:polymarket-api-health.timer (TIMERS list, 26h slack), systemd_last_run:polymarket-api-health.service (BATCH_SERVICES list, catches non-zero exit), artifact:api_health_latest_json (NEW artifact rule, 26h threshold — belt-and-suspenders for #287 "ran but didn't write" pattern).SHELL=/bin/bash at top of crontab.systemctl is-active polymarket-api-health.timer → active; systemctl list-timers polymarket-api-health.timer shows tomorrow's NEXT; manual systemctl start polymarket-api-health.service writes latest.json cleanly; wc -l /var/log/polymarket/api_health.log shows growing line count; curl /api/integrity shows 3 new rules; crontab -l | grep api_health_checker returns empty; crontab -l | head -1 shows SHELL=/bin/bash.❌ Polymarket Website API: ERROR — Earnings JSON returned 404 — Build ID: build-TfctsWXpff2fKS. The https://polymarket.com/_next/data/<buildID>/... URL returns 404 because the build ID changes when Polymarket redeploys. This has been broken for unknown duration and was hidden by the cron silent-failure. Now visible daily via systemd_last_run:polymarket-api-health.service STATUS_FAIL. NOT in scope for #289.a — surfaced for operator awareness; likely needs build-ID auto-discovery via the Polymarket homepage HTML OR the check should warn not fail on this single API. [RESOLVED by Finding #326, Jun 14 2026 — Polymarket migrated to the Next.js App Router (buildId gone); rewrote the check App-Router-aware + warn-not-fail.]docs/FINDINGS.md.Second item in operator-decided plan-(C) sequence. Originally documented spec recommended 3 new progress:* rules (polymarket-live-trades, polymarket-collector, polymarket-websocket) attributing writes to specific services via DB-query patterns. After reading integrity_monitor.py end-to-end and verifying the live system, all 3 named daemons are the SOLE writers of their respective tables in the current architecture (CLOB trades disabled per ENABLE_CLOB_TRADES=false; the Dome-era redundant-writer pattern doesn't apply). So the existing freshness:* rules already alarm 10-60 min after any of these would silently die. The proposed progress:* rules would have duplicated coverage with attribution noise on illiquid days.
Per memory.md NO SHORTCUTS rule, the design tension was surfaced explicitly to the operator (via AskQuestion) rather than silently interpreted. Operator chose option (B): re-scope to actual blind-spot closure via state-file mtime defense + documentation + deploy-checklist, skip the progress:* rules.
artifact:trades_backfill_state rule in _build_rules() §4 — _check_artifact_mtime on [hidden]/trades_backfill_state.json with 9-day threshold (8-day timer cadence + 1-day slack). Severity HIGH. Catches the §287 frozen-state pattern: state file not advancing despite service appearing active._build_rules() §4 explaining the §E "long-running no-op service" lesson + which services are/aren't in scope (with rationale).deploy/deploy_from_github.sh:services=() array — when adding a new long-running daemon collector, reminder to add a state-file mtime rule if the daemon maintains state.docs/DATA_INTEGRITY.md — "Long-running-no-op service defense — state-file mtime rules (Finding #291)". Documents §287 blind spot, defense pattern, in-scope/out-of-scope decision matrix.curl /api/integrity: artifact=3, freshness=5, meta=1, null_rate=5, storage=4, systemd=19); artifact count 3 (was 2 before #291); systemd count 19 (=5 always-on + 7 timer + 7 last_run; the +2 vs the older 17-claim came from #269 adding systemd_timer + systemd_last_run for polymarket-trades-backfill on May 9, which earlier docs had not propagated).login_attempt_state.json (IBKR, not no-op-relevant), gap_monitor_state.json (gap_monitor.py is dead code per #289 — no service, no process; out of scope for surgical-changes here), *.archived_* (#287 preserved archives, no writers), ibkr_snapshot.json (already covered by existing rule).curl /api/integrity returns the new check artifact:trades_backfill_state with status=ok and detail.age_seconds matching state file mtime (~1h post-cold-bootstrap).docs/FINDINGS.md.First item in operator-decided plan-(C) sequence (#290 → #291 → #289.a → #289.b) following the cold-bootstrap close-out earlier this morning. Pure cleanup; closes the §C "left for future deletion" footnote that originated in #270 May 9 evening and was reaffirmed in #289.v2.
src/data_tracker/watchdog.py (21,853 bytes — note: original Finding entry estimated "~1 KB" which was a documentation drift, actual was 22 KB), src/data_tracker/watchdog_v3.py (31,190 bytes), deploy/polymarket-watchdog.service (674 bytes), deploy/polymarket-watchdog-v3.service (719 bytes), deploy/systemd/polymarket-watchdog.service (477 bytes — second copy in deploy/systemd/ subdir, ExecStart pointed to deleted watchdog.py). Out-of-scope per §C: polymarket_sniper/watchdog.py (Dec 2024, different sub-project process-monitor — left alone).deploy/setup-digitalocean.sh (3 lines: cp + enable + start), deploy/install_services.sh (4 lines: pkill + enable + start + status), tests/system_health_check.py (1 line in expected-services list), tests/comprehensive_system_test.py (1 line in services_to_check list), docs/DATA_INTEGRITY.md §6.1 "Watchdog Logs" → "Integrity Monitor Logs" + cross-reference block, docs/DATA_INTEGRITY.md §7.1 services-overview table, docs/DATA_INTEGRITY.md Appendix file table.docs/FINDINGS.md Finding #270 watchdog rows (lines 2627-2628), docs/FINDINGS.md Finding #289.v2 watchdog mention (line 2808), docs/AGENT_HANDOFF.md line 192 next-session row (above) + line 373 #270 footnote (above).docs/archive/*, earnings_dashboard/app.py:1324 (already cited #260c), src/data_tracker/integrity_monitor.py:6+616 (intentional comparison comments).find [hidden]/src -name "watchdog*" returns empty; find [hidden]/deploy -name "polymarket-watchdog*" returns empty; systemctl is-active polymarket-watchdog.service continues to return not-found; systemctl is-active polymarket-integrity-monitor.service returns active; deploy-script decommission loop runs without error (idempotent on now-not-found units).docs/FINDINGS.md.Operator chose Plan D from session-start (cold bootstrap before any new code), with go-decision (B) on the offset-cap finding mid-session (fix-now rather than defer). Both shipped in this commit chain.
Result=success, ExecMainStatus=0, NRestarts=0. 928 of 929 markets succeeded (1 failed pre-fix = TSLA Q3 2025 due to offset cap; resolved via #292). 152,101 trades saved with new polymarket_data_api_backfill source label — confirms #288 field-mapping migration is functionally correct. State file persists at [hidden]/trades_backfill_state.json (NOT data/trades_backfill_state.json as I initially checked — false alarm worth noting in case future agent makes the same mistake; STATE_FILE path resolves to project root). API budget 968 calls / 929 markets ≈ 1.04/market — confirms warm-delta dominance because existing 2.86M trade_dedupe rows put each market into delta-mode immediately via MAX(time) lookup./trades offset hard-cap at 3000 (RESOLVED). Caught by §G probe of #288 cold bootstrap. TSLA Q3 2025 market with >3500 trades hit HTTP 400 {"error":"max historical activity offset of 3000 exceeded"} at ?offset=3500. Pre-fix: _fetch_trades_page retried 4× and gave up; market marked failed; stuck in permanent retry loop on every weekly timer fire (since not added to completed_markets). Investigation (direct probes against Polymarket data-api) confirmed: cap is hard at 3000 (3500 reachable with limit=500), NO alternative pagination param works (?before=, ?after=, ?since=, ?until=, ?startTime=, ?endTime=, ?max_timestamp=, ?asset= all silently ignored), no alternative endpoint exposes deeper history (/activity requires user, /positions requires user, /holders is not trade history, /trades-history 404, /tradeHistory 404, /openapi.json 404). Fix: new _OffsetCapReached sentinel exception in backfill_trades.py; _fetch_trades_page raises it on the cap-specific 400 (NOT retried); backfill_market catches it, sets truncated=True, success=True, breaks loop; run() adds market to completed_markets (truncation counts as success-variant) and increments n_truncated counter for visibility. Bounded blast radius — TSLA Q3 2025 is the ONE currently-vulnerable market (no pre-#288 Dome coverage); the other 144 markets in DB with >3500 trades all have substantial pre-#288 Dome backfill coverage that preserves their full historical depth. §E same-pattern check: polymarket_client.PolymarketClient.get_trades has the same offset pattern but ZERO active callers (only .get_trades( reference is continuous_collector.py:346 calling the CLOB SDK, different code path) — left per memory.md §C surgical-changes; would patch when a real caller is added.trade_dedupe.source doc correction (RESOLVED). Cold-bootstrap baseline query revealed trade_dedupe has FIVE distinct source labels in coexistence, not 4 as documented across 6 surfaces. Previously undocumented: 'backfill' (2,170,662 rows) — all inserted in a single bulk import at 2026-01-31 10:13:24 UTC (created_at range collapses to one second). Raw_data shape varies — oldest rows (trade_time 2024-05-10) are Dome-shape (condition_id, tx_hash, user, taker, shares, shares_normalized, token_id, token_label, order_hash); newest rows (trade_time 2026-01-31) are Polymarket-native shape (asset, conditionId, proxyWallet, transactionHash, outcome, size). Source script is no longer in the repo (current 'backfill' literal is only conviction_tracker.py:236+284 writing to a different table). Treat as legacy historical inventory; no current writer. Also: 'dome_backfill' (which docs claimed was the pre-#288 backfill_trades label) has 0 rows in DB — was indeed the literal in pre-#288 source code per git diff 04b4ca4, but every row was ON CONFLICT DO NOTHING-skipped against the pre-existing 'backfill' rows from the Jan 31 bulk import. All 6 doc surfaces updated (this section + CONTEXT.md changelog + FINDINGS.md #288 + #289 + DATA_ARCHITECTURE.md schema description + verify_schema.py module docstring).deploy_from_github.sh propagates the backfill_trades.py change. daemon-reload not strictly required for a script change, but harmless. Since the timer was previously DISABLED (per #269 cold-bootstrap-then-enable convention), enable --now polymarket-trades-backfill.timer runs as the FINAL step after the §G probe of the fix.MAX(time) lookup finds the 3500 saved trades) and complete WITHOUT triggering the cap (delta cutoff filters everything out immediately). Run summary should show n_truncated=0 for warm-delta runs. The cap-handling code path remains exercised on any future cold market with >3500 trades.src/data_tracker/collectors/backfill_trades.py (new _OffsetCapReached + offset-cap detection + truncated flag + run() summary line)docs/FINDINGS.md (Finding #292 entry + Finding #288.v3 entry + #288 + #289 source-label-claim updates)docs/AGENT_HANDOFF.md §11 (this section)docs/CONTEXT.md (May 10 changelog: cold-bootstrap close-out + #292 + #288.v3)docs/DATA_ARCHITECTURE.md (5-label correction at trade_dedupe.source description)src/data_tracker/verify_schema.py (module docstring 5-label correction)These were surfaced in the May 10 self-audit at end of session and explicitly approved by the operator for next-session ship. Order of recommended execution (smallest-blast-radius first):
| # | Finding | Scope | Est. effort |
|---|---|---|---|
| 1 | ✅ #290 — Watchdog dead-code deletion (RESOLVED May 10, 2026 LATE morning) | Deleted 5 files (watchdog.py 22 KB, watchdog_v3.py 31 KB, 3 deploy/.service files — deploy/systemd/polymarket-watchdog.service was a 5th duplicate caught by §E re-grep); fixed 7 stale references (setup-digitalocean.sh, install_services.sh, 2 test files, DATA_INTEGRITY.md §6.1+§7.1+Appendix); added forward-pointers in 4 historical-context refs (FINDINGS #270 + #289.v2 + AGENT_HANDOFF §11 + this row). Decommission loop in deploy_from_github.sh:127-128 still idempotent post-deletion. Out-of-scope per §C: polymarket_sniper/watchdog.py (different sub-project process-monitor — left alone). |
DONE |
| 2 | ✅ #291 — Long-running-no-op service integrity defense (RESOLVED May 10, 2026 LATE morning — RE-SCOPED from original spec) | Re-scoped from "3 progress:* rules + helper" to "1 state-file mtime rule + deploy-checklist + documentation" after live-system analysis showed the named daemons are SOLE writers (existing freshness rules already cover them). New artifact:trades_backfill_state rule (9-day threshold, HIGH severity) + 40-line context comment in _build_rules() + CHECKLIST in deploy/deploy_from_github.sh:services + new §2.5 in DATA_INTEGRITY.md. Total integrity rules now 35 (was 34). |
DONE |
| 3 | ✅ #289.a — api_health_checker silent-failure → systemd-timer migration (RESOLVED May 10, 2026 LATE morning — operator-approved option (B)) | Root cause confirmed via reproduction: /bin/sh: source: not found (Ubuntu's /bin/sh is dash, source is bashism, chained && short-circuits, >> log redirect never runs). Migrated to polymarket-api-health.{service,timer} with bash -c 'source .env && python -m' ExecStart. Daily 08:00 UTC + 5-min RandomizedDelay. New 3 integrity rules (systemd_timer: + systemd_last_run: + artifact:api_health_latest_json). §E bonus: same dash-bug also affected sync_earnings_markets.py cron — fixed via SHELL=/bin/bash at top of crontab. Old cron line removed manually post-deploy. Pre-existing Polymarket Website API 404 surfaced for operator awareness — was hidden by the cron silence. |
DONE |
| 4 | ✅ #289.b — live_trades_collector pg8000 failed-tx cascade (RESOLVED May 10, 2026 LATE morning) | Root cause confirmed via code review: inner try/except was INSIDE with engine.begin() block; on inner SQL exception the catch swallowed it at DEBUG level (invisible), connection went into failed-tx state, engine.begin's exit tried to COMMIT, pg8000 raised InterfaceError on the COMMIT, outer Cycle error caught it. PLUS the failed trade_id was never added to _seen_trade_ids so the SAME bad trade re-fetched + re-attempted every 5s — 12-13k InterfaceErrors/day for 10+ days. (Original Finding entry's claim "errors in journalctl" was wrong — they're in /var/log/polymarket/live_trades.log because StandardOutput=append:.) Fix: try/with INVERSION (try wraps with) + WARNING-level visibility + cascade closure via _seen_trade_ids.add(trade_id) on error path. |
DONE |
Total estimated effort: ~3-4 hours for all 4 items. Smaller subset: ~1 hour for items 1-2 (the cleanups).
Each finding has its own canonical entry in docs/FINDINGS.md per memory.md §J,
including §B 5-step plan + §E same-pattern check guidance + §G acceptance probes
for whoever picks them up.
If you're a new agent reading this section to onboard, the most important thing to know about this project is: NO SHORTCUTS, EVER. This is a live trading bot with real capital; shortcuts in docs/code leak into operator confusion, runbook drift, silent tech debt, and — at the limit — into trading-logic mistakes that cost real money.
Anchoring example (May 10, 2026 — Dome→Polymarket migration Tier C):
during the migration's doc cleanup phase, the agent took a "banner-only +
surgical update of highest-impact lines" shortcut on DATA_ARCHITECTURE.md
(45 inline Dome refs) and OPERATIONS.md (24 refs) instead of doing the
full pass the operator had explicitly asked for. The operator caught it
and said: "the shortcuts are extremely disturbing and make me question
everything you did before! we can never take shortcuts for this project!!!!
huge risk to capital. ensure this is clearly saved in all handovers and
never repeated! if there is one thing that you must always know about
this project is this: no shortcuts! ever!"
The shortcut was fixed same-day in Finding #289.v2, but the rule is now
codified in:
- .cursor/memory.md dedicated "🚨🚨🚨 NO SHORTCUTS — EVER 🚨🚨🚨"
section (gravity equivalent to TRADE EXECUTION PROHIBITION) + §A
pre-coding checklist + §G post-fix verification checklist
- docs/CONTEXT.md "MAIN DIRECTIVES (NON-NEGOTIABLE)" §0 (ABOVE
every other directive)
- This section (docs/AGENT_HANDOFF.md §11)
- docs/STRATEGY_PLAYBOOK.md §0 (above the verdict)
Read the full rule in .cursor/memory.md before any work. The TL;DR is:
no banner-only doc updates, no "several refs" without an exact grep count,
no §E same-pattern check that isn't exhaustive across active code + docs,
no "I'll fix it in a follow-up" without explicit operator approval, no
"highest-impact only" interpretation when the user said "all." Train the
discipline on the small stuff (doc cleanup, comment updates) so it's
automatic on the big stuff (trading logic, sizing, stops, signals).
If tempted: STOP and ASK the operator.
What changed in the last working session. Update this section every time a major piece of work lands so the next agent / operator can self-onboard from this doc alone.
Operator screenshot of docs.polymarket.com
revealed: "Dome has been acquired by Polymarket. As such, all Dome APIs
will reach end of life on April 28th, 2026." Yesterday's HTTP 000 / 504
timeouts on api.domeapi.io (Findings #269.v2 + #269.v3) weren't a
transient outage — Dome has been permanently dead for 12 days.
Operator commissioned a comprehensive investigation (Q1-Q7 covering live state of every Dome consumer, replacement-endpoint inventory, data-loss assessment, hidden-bug audit) followed by a Tier A → B → C migration plan.
Tier A shipped this commit (Finding #287):
- Stopped + disabled 3 dead services on prod that had been silently
failing for 12 days: polymarket-orderbook-backfill (no-op cycling
with frozen state file), polymarket-prices-backfill (Gamma endpoint
not in new docs, "saves 0 prices"), polymarket-dome-prices-backfill
(OOM-killed daily — disabled but Restart=on-failure cycling).
- Archived 3 state files on prod (preserved for forensic / Phase 3 simulator
review). Deleted 3 systemd unit files from repo. Removed those services
from deploy_from_github.sh:services array with a multi-line comment
block citing #287 so the next agent knows what was removed and why.
- Manual rm of unit files from /etc/systemd/system/ per §H fallback
(deploy script doesn't auto-clean removed units).
- Zero data loss. Verified: trades / orderbook_snapshots /
price_snapshots all written within 0-2 min of probe. The 4 dead
services produced no useful data; redundant Polymarket-native consumers
(polymarket-collector, polymarket-live-trades, polymarket-websocket)
cover all the surfaces.
- New §E lesson logged: "long-running no-op service" blind spot — service
can be enabled+active+running but doing zero useful work without any
alert firing. Existing freshness rules (Finding #258) catch real data
impact but not wasted-compute / silent-API-death scenarios. Future
long-running daemon collectors should add forward-progress assertions.
Tier B (Finding #288) — SHIPPED same session (May 10 morning):
- Migrated backfill_trades.py from Dome /polymarket/orders → Polymarket
data-api/trades?market=<cid>. Replaced DomeAPIClient with
requests.Session + new _fetch_trades_page helper. Rate-limit gap
dropped 1.2s → 0.05s (free tier 200 req/10s = 20 q/s sustained, 20×
faster than Dome).
- Cold bootstrap estimate ~3h → ~8 min wall time.
Warm weekly delta ~18 min → ~1-2 min.
- Preserved all smart Option A logic verbatim (T1 delta — now client-side
filter on MAX(time) - 1h overlap since Polymarket /trades has no
server-side start_time; T2 smart re-fetch; T3 earnings-only; circuit
breaker; CLI flags; success-flag handling; exit non-zero on circuit-break).
- Field mapping reworked: tx_hash→transactionHash, user→proxyWallet,
condition_id→conditionId, shares_normalized→size, token_label
→outcome. New asset (token_id) field included in trade_id for
uniqueness on multi-fill same-tx orders. trade_dedupe.source label
changed from 'dome_backfill' → 'polymarket_data_api_backfill' for
forensic distinguishing of pre/post-migration data.
- Removed dome_client._make_request + prices_backfill_service._get_with_retry
retry tests from verify_retry_backoff_and_block.py CI script (modules
to be deleted in Tier C).
- Operator decision pending (deferred to after Tier C): when to trigger
cold bootstrap. Likely after Tier C ships to avoid mid-migration noise.
Tier C (Finding #289) — SHIPPED same session (May 10 morning):
- Deleted 8 Dome-dependent files (1 more than originally planned —
prices_backfill_service.py added to deletion list because Q4
investigation showed its Gamma /markets/{cid}/prices endpoint is
not in the new Polymarket API docs and was logging "Saved: 0 prices"
cycling): dome_client.py, main_collector.py, backfill_earnings_trades.py,
backfill_candlesticks.py, backfill_prices.py, dome_prices_backfill.py,
orderbook_backfill_service.py, prices_backfill_service.py.
- Cleaned src/data_tracker/collectors/__init__.py — removed imports of
DomeAPIClient + DataCollector. __all__ now ['PolymarketClient'].
- Cosmetic cleanup of live_trades_collector.py: DOME_URL →
POLYMARKET_DATA_API, fetch_trades_from_dome() →
fetch_trades_from_polymarket(), source='dome' label → 'live_poll',
create_trade_id prefix dome_ → lp_. Header docstring rewritten.
No functional change — service was always hitting
data-api.polymarket.com, just with misleading naming.
- Cleaned earnings_dashboard/app.py — removed dead
prices_backfill_service and orderbook_backfill_service process-running
checks. Stats keys still set to defaults so dashboard template doesn't
KeyError.
- Cleaned gap_monitor.py:check_backfill_processes() — removed
backfill_candlesticks check (file deleted); backfill_trades check
kept but description updated to note it's now timer-driven.
- Updated 6 docs: env.example.txt (removed DOME_API_KEY), README.md
(3 inline refs), docs/DATA_INTEGRITY.md (§3.1 + §3.2), docs/API_REFERENCE.md
(TOC + §4 fully rewritten + §5 + §6), docs/OPERATIONS.md (services diagram
+ trades-backfill runbook + data sources table + backfill status + appendix
+ curl health checks), docs/DATA_ARCHITECTURE.md (top-of-file banner with
full migration table — body left intact as historical context).
- (CI test cleanup verify_retry_backoff_and_block.py was already done
preemptively in #288 — removed dome_client._make_request and
prices_backfill_service._get_with_retry retry tests.)
Open follow-ups from #289 (separate sub-findings, NOT blocking):
- #289.a — api_health_checker cron silent-failure: log file 0 bytes since
Apr 27; cron fires daily but produces no output. Likely cron-env /
.env-not-sourced / module-import issue. Manual run produces output.
- #289.b — live_trades_collector pg8000 failed-tx cascade:
InterfaceError: in failed transaction block every 5s. Service still
functional (3,474 trades/24h written) but emits constant error noise.
Likely a DB error in a prior cycle leaves connection in failed state
without explicit ROLLBACK.
Final command (post-deploy): DOME_API_KEY removed from prod
[hidden]/.env — done as a separate single-command operation
after Tier C deploy verifies clean.
Operator commissioned a housekeeping batch to use the ~17 days until Phase 3 (May 26) productively. Plan: address the 4 deferred operator-decision findings (#269, #270, #273, #274) one at a time per memory.md §A-§N, with each finding shipped as its own commit + push + deploy + §G live probe. Finding #270 was the first one shipped.
StartLimitIntervalSec/Burst in [Service] instead of
[Unit] (RESOLVED May 9, 2026 evening).
Original Apr 27 documentation only flagged polymarket-trades-backfill.service.
Memory.md §E same-pattern check on May 9 surfaced 5 live instances + 1
install-helper + 2 decommissioned-watchdog occurrences. Cross-checked against
prod via systemctl is-active to filter dead code. Affected live services
(all silently using systemd default 5-starts-in-10s rate limit instead of
per-unit intended values): earnings-trader, polymarket-integrity-monitor,
polymarket-trades-backfill, polymarket-collector,
earnings-conviction-tracker. Plus deploy/install_trader_service.sh
inline heredoc template for consistency. The 2 decommissioned watchdogs
(polymarket-watchdog.service, polymarket-watchdog-v3.service) carry the
same bug but are not-found on prod (per Finding #260c) — left untouched per
§C surgical-changes rule. Subsequently DELETED in Finding #290 (May 10, 2026
LATE morning) along with the 2 .py source files; the StartLimit bug is
no longer in the repo.deploy_from_github.sh auto-installs only
trades-backfill + integrity-monitor; the other 3 unit files were
install -m 0644'd manually from the just-pulled repo on prod, then
systemctl daemon-reload. No service restarts triggered for the StartLimit
fix specifically — the directive change only affects future crash-recovery
behavior, which daemon-reload is sufficient to pick up. Trader / collector /
conviction-tracker continued running uninterrupted.journalctl --since "<reload-timestamp>" |
grep "Unknown key name" returns 0 lines (was firing every ~3 minutes
pre-fix). Verified.deploy_from_github.sh does not auto-install earnings-trader.service
/ polymarket-collector.service / earnings-conviction-tracker.service — it
only restarts the first two and is unaware of the third. That's why the §H
scp/install fallback was needed for this fix. Future cleanup candidate:
extend the deploy script to install these three units via the same install -m
0644 ... pattern used for trades-backfill + integrity-monitor + the timer
units. Deferred in this commit to avoid scope creep on a 1-line systemd fix.docs/FINDINGS.md for the full file
list and per-file context.curl / smoke test (introduced
in Finding #262) returned HTTP 503 with
UndefinedError: 'dict object' has no attribute 'roi_tc_total_pct' for ~2
minutes after the dashboard restarted. Pre-existing bug from May 6 —
Finding #284 (Trading Capital ROI panel) added 13 new TC-ROI fields to
the populated summary.update({...}) block in app.py:get_trade_history
but did NOT mirror them into the _empty_summary cold-start fallback
dict at app.py:4359. Every dashboard restart 503'd until the bootstrap
thread finished its DB queries (~2 min). Operator-facing impact was
mostly invisible (self-recovers), but every future deploy in this
housekeeping batch would have tripped the same 503 and aborted the
deploy script with exit 1. Fixed by extending _empty_summary with
all 13 fields (mirroring app.py:3585-3599 letter-for-letter) and
adding |default(0) to the 6 TC ROI {% if %} Jinja conditionals for
defense-in-depth. Lesson reinforced: §E "data-shape contracts" —
when adding fields to a dict consumed across multiple producer paths
(populated + empty-fallback + cached), enumerate every producer in
lockstep, not just the populated path. Same structural lesson as §M
cross-interface rule (Finding #225). Future cleanup candidate:
refactor _empty_summary + summary.update({...}) to share a single
_make_empty_summary() helper or @dataclass so the shape contract
is enforced by the typechecker, not by hand. Not done today (§C
surgical scope).maxsize 500M (NOT size 500M) +
semantics correction caught in pre-flight. Apr 27 audit caught
collector.log at 8.7 GB; original finding recommended size 500M
"or maxsize 500M to keep the daily cadence". May 9 pre-flight
logrotate -d dry-run on prod logrotate 3.21.0 confirmed the
recommendation needed correcting:daily + size 500M → size REPLACES the time-based count → small
logs (backup.log <1 KB/day) would never rotate.daily + maxsize 500M → maxsize ADDS a size early-trigger ON TOP
of the daily cadence — correct belt-and-suspenders semantics.
Operator approved size 500M based on my (incorrect) explanation;
per memory.md §1 ("Don't assume. Surface tradeoffs.") I flagged the
correction before shipping and re-confirmed maxsize 500M. Shipped
with a 12-line comment block in the config file documenting the
size-vs-maxsize gotcha for the next agent. Prod logrotate.timer is
daily-only (next fire Sun 2026-05-10 00:00 UTC, last fire today
00:00); on hourly-timer distros maxsize would trigger intra-day,
which is forward-compatible. §G probe verified via logrotate -d
dry-run before AND after deploy.polymarket_opportunity_log Phase 2.1 schema fires
snapshot_schema='pm_v2' label on every row but leaves pm_best_bid /
pm_best_ask / pm_spread NULL on signal_emitted rows (8/8 since
Apr 26 deploy). Hypothesis: the snapshot-based-conviction branches
(Finding #246's post-event price_snapshots fallback path) bypass the
orderbook fetch, so ConvictionResult.best_bid/ask come back None.
Zero impact today (Phase 3 unified simulator that consumes these fields
hasn't shipped). Per operator: "Pair fix with Phase 3 unified simulator
ship." Status moved from DOCUMENTED → (DOCUMENTED — deferred to
post-Phase-3 — medium) to match the parser convention used by #278 /
#279 / #283 (substring DOCUMENTED → "deferred" on the dashboard
Testing Findings tab; substring DEFERRED-POST-PHASE-3 is NOT in the
parser map per #277's enumeration). Doc-only change — no code touched.
Phase 3 lockstep instructions + acceptance probe SQL added to the
finding entry. NOT added to build_action_queue() because #273 is
data-capture infrastructure, not a strategy rule./orders start_time parameter.
Per-market MAX(time) lookup → only fetch trades after that timestamp
(with 1-hour overlap; trade_dedupe handles duplicates).earnings_date re-fetched even if previously "completed"; old
closed markets stable-skipped. Closes the gap where collector might
miss trades during an earnings window.INNER JOIN earnings_markets,
~5,735 → ~929 markets, 6× reduction). CLI --all-markets override.polymarket-trades-backfill.timer (Sun 06:00 ET, Persistent=true,
RandomizedDelaySec=600) — runs 2h after data-backfill timer to avoid
Dome burst. INSTALL but NOT enable in deploy script — operator must
cold-bootstrap manually first. Integrity-monitor rule
systemd_timer:polymarket-trades-backfill.timer with 8-day freshness
(returns STATUS_UNKNOWN until enabled, no false alarms).--all-markets / --earnings-only (default), --full-resync
(ignore state), --dry-run (count + budget estimate).docs/OPERATIONS.md.6959072 (#270), 81210dc (#286), e7fb28a (#274), 747080f (#273),signal_emitted rows — fix
in lockstep with simulator ship). The first three are strategy rules;
#273 is the only data-capture infrastructure item, but ships in the
same lockstep window because Phase 3 is its consumer.completed_markets should be populated for
all ~929 earnings markets, trades_saved count should have grown
meaningfully). Then enable the weekly timer with systemctl enable
--now polymarket-trades-backfill.timer per the OPERATIONS.md runbook.Operator pushback May 7 on TTD + MCHP being silently rejected at the exposure cap. Investigation surfaced 3 distinct problems and the resulting fix took 2 commits over the day with full memory.md compliance audit at the end.
skip_stage='exposure_cap' (RESOLVED May 9, 2026).
Investigation: TTD (LONG 78.5%, mid, priority 4/6, vol $215) + MCHP (LONG
87.1%, mid, priority 3/6, vol $1,241) both passed selection gates and
generated valid signals at 14:30:14 UTC, then both rejected by the
orchestrator's exposure-cap check (rare-day 60% × 2 mid signals at 22% +
APP carryover at 18% = 62% > 60%). Three problems found:polymarket_opportunity_log rows
ever recorded for exposure rejections — silent rule.polymarket_opportunity_log with
skip_stage='exposure_cap'. Phase B (problem 3 — rule re-evaluation)
deferred to Phase 3 Strategy Revamp May 26 alongside #279
(sizing-priority-v2).capital_manager.py (Phase 4A extracted module, currently dead code) had
the same buggy loop pattern. Patched in lockstep so re-activation can
never resurrect the bug. Added 9 unit tests in
tests/unit/test_risk_manager.py:TestPhase285GreedyFillExposureMath
covering the failure-mode matrix from §B Step 4 — replays today's exact
TTD/MCHP scenario as a regression test. 41/41 unit tests passing on
prod Python 3.12.risk_manager.py (can_open_position adds optional
extra_pending_exposure kwarg, default 0.0 preserves all 8 callers'
semantics); trading_orchestrator.py (BMO + AMC loops + new
_log_exposure_skip helper); capital_manager.py (lockstep patch);
tests/unit/test_risk_manager.py (9 new tests); docs/FINDINGS.md
Finding #285 canonical; docs/STRATEGY_PLAYBOOK.md §7 + docs/AGENT_HANDOFF.md
§11 (this) + scripts/strategy_performance_analysis.py:build_action_queue()
(sizing-priority-v2 row updated).active post-restart, no
errors. Acceptance test fires Monday on first BMO/AMC tick — should
see skip_stage='exposure_cap' rows accumulate. By May 26 ~10-30 rows
expected for the Phase B bias gauntlet.net_liq − $25,000 PDT reserve, falls back to $2,500
when snapshot unavailable. Annualization formula: compound CAGR
(1 + return)^(365/days) − 1. Initial deploy had a missing t. alias
for TRADE_METRICS_FILTER_SQL_T in the all-time query — caught by §G
live probe (lesson from #265.v2 reinforced again — always probe the
changed output, not just review the code), fixed in v2 within 5 min.("size","90+") PRIORITY_TABLE omission
despite the rule never being data-validated on SHORTs — n=18 total
SHORTs, zero at ≤10% conviction. Operator caught on COIN
#2026-05-07 yes_price=0.0755. Deep-dive evidence (n=2 PSKY at -$500
counterfactual each + 87.5% miss rate at conviction ≤10% across 8
past markets) suggests the skip is mechanism-consistent (priced-in
argument symmetric to #202 LONG-side) but n=3 real-signal kills is
below §K gauntlet floor. Deferred to Phase 3 alongside #279 + #285
Phase B + #283.scripts/analyze_regime_detection_lag.py.pre_event / released_pending / released_*_eps
/ released_*_pm) computed from actual_beat (resolver) or live PM
yes_price extreme (≤0.01 / ≥0.99 — same threshold as Finding #264's
_live_polymarket_extreme_outcome). Settled rows dimmed to 55% opacity.GET /api/trade-chart-data?source={position|audit}&id=N&resolution={...}.
10-min auto-refresh on live charts. Hidden anchor line series forces
price scale to include off-bar markers.effective_cumulative_volume_usd from "DB if positive else Gamma" →
max(db, gamma) because once-per-15-min DB sync became stale in
seconds (MRNA #2026-05-01 silently gated out at $6.28 < $2k while live
Gamma cumulative was $3,734). #282.v2 (post-deploy probe caught
≠trader): consolidated dashboard except-fallback to call the same
fetch_gamma_cumulative_single helper the trader uses (memory §M
cross-interface, single shared definition). Both surfaces now agree
to the cent.After the 6-phase Database Integrity Audit + dashboard 500 chain, operator
requested a rigorous §B 5-step audit of every code commit in the last 4
days, then asked for the volume_24h_at_entry regression to be fixed.
Both delivered in one session.
pg8000 mis-parsed :exit_time::timestamptz (HIGH).
The Phase-2 hold_duration_hours "fix" introduced a SQL syntax error
(driver parsed :: as a parameter-prefix continuation) that broke the
primary close path for EVERY trade since Apr 27 morning. Failed UPDATEs
silently fell to the fallback path, leaving hold_duration_hours,
conviction_at_exit, prediction_correct, detection_to_exit_seconds
and all financing aggregates as NULL. Fixed: CAST(:exit_time AS
timestamptz). Same-pattern rg ':\w+::' audit clean across src/.earnings_miss despite Polymarket signalling YES=99.95% (BEAT — the
company beat non-GAAP EPS). Trader had restarted at 13:16 UTC; in-memory
_confirmed_polymarket dict was empty for the next ~30s confirmation
window; Yahoo's $1.20 vs $1.21 GAAP scrape (-1¢) won. Two-layer fix:
(a) Yahoo contradiction now BLOCKS when pm_confirmed exists (matches
PRNewswire/BusinessWire policy); (b) new helper
_live_polymarket_extreme_outcome() reads latest price_snapshots.yes_price
directly so post-restart extreme prices (≥99% / ≤1%) still block Yahoo.
Backfill: VZ #120 + 22 historical trades had NULL actual_earnings_result
/ prediction_correct populated from earnings_markets.actual_beat./api/earnings-calendar
still rendered volume: 1225.63 / volume_ok: false. v2 fixes the fallback
path. Production-verified: VZ now volume: 3920.43 / volume_ok: true
with all four transparency keys (volume_24h, volume_cumulative,
volume_db_total, volume_gamma_raw) surfaced.polymarket_opportunity_log write logic (NOT a bug —
retracted; renumbered as part of #272 investigation).polymarket-backup.service OOM-killed 3 nights running
(HIGH — RESOLVED Apr 27 17:38 UTC). Apr 25 / 26 / 27 04:09 UTC all killed
at ~9 min. Phase 5's streaming-pg_dump fix shipped at 13:43 UTC AFTER
that morning's kill. Validated via 2 successive manual triggers Apr 27
evening: 8m 9s, 2.0 GB memory peak, 0 B swap, 2.3 GB gzipped backup
created. Integrity rule flipped fail → ok; /api/integrity now
reports fail: 0. Until #268 deployed earlier in the evening, ZERO
Telegram alerts had fired on these failures — the gap that motivated
#268.instant_exit:earnings_miss
(Medium — RESOLVED Apr 27 22:10 UTC). Operator-flagged: trade history
showed VZ as "Earnings Miss" exit even though Polymarket resolved YES
(the position was a correct LONG; exit was Finding #264 bug). Established
reusable convention BUG_EXIT (Finding #N): ... prefix on
trade_audit_log.notes. Dashboard trade-history serializer detects the
prefix and renders 🐛 BUG-EXIT · <normalized> so the row is visually
distinct, AND surfaces bug_exit_flagged: bool on the API response so
future stats-aggregations can WHERE notes NOT ILIKE 'BUG_EXIT%'.
Convention documented in docs/DATABASE_DIRECTIVES.md. SQL one-shot
backfilled VZ #120 with the marker; only 1 row in the table currently./api/architecture-findings parser only mapped
DONE/CODE/VERIFIED/PARTIAL. Enumerated every status keyword
actually used in FINDINGS.md headers, then mapped each one explicitly.
No auto-sync mechanism needed — the API already re-reads FINDINGS.md
on every request; the drift was pure parser logic.Volume-display verification — full-population sweep (Apr 27 22:30 UTC,
no fix needed): operator asked whether the upcoming-earnings widget's
"VOLUME" column shows the same cumulative figure the trader gates on,
for every market not just VZ. Audit script (one-shot from prod):
for each of the 148 widget rows, called the trader's actual
signal_generator.get_market_volume(condition_id) and compared to the
dashboard's volume field + volume_ok gate decision. Result:
148/148 markets, 0 volume-value mismatches, 0 gate-decision
disagreements. Spot rows: VZ $3,920 / BP $2,734 / KO $4,942 / SPGI
$3,213 / BKNG $494 — all match exactly. Dashboard shares the
volume_policy.py module (effective_cumulative_volume_usd) with
signal_generator, so the alignment is structural, not coincidental
(Findings #224 / #265.v2). Re-run via the same script under
/tmp/eclndr.json + signal_generator.get_market_volume() if any
future change touches volume logic.
Cold-start populate time — Polygon dependency review (Apr 27 22:55 UTC, decision: keep status quo): operator asked whether the Polygon access added in Finding #249 could let us speed up dashboard cold-start further and reduce the residual yfinance dependency. Live evidence on the current dashboard process: T+10s widget visible (DB bootstrap), T+9 min full enrichment ("Cache background refresh started"). Of 2,185 successful mcap lookups since restart, 98.4% came from Polygon, 1.5% from yfinance (cached/fast large caps), 0.1% from Finnhub — yfinance is no longer the cold-start bottleneck post-#249. The 9-minute full-enrichment window is now Polygon's natural pace at the current 0.1s/call throttle (~540 symbols × ~4 fallback retries).
Five options scoped (see chat transcript for full evidence + trade-off
matrix). Operator chose Option A (status quo) for reliability
preservation — the current yfinance → Polygon → Finnhub chain is
reliability-conservative (yfinance free first, Polygon paid only when
yfinance fails, Finnhub last-resort tail). Ship-readiness rationale:
- The widget is already visible at T+10s via DB-bootstrap (Finding #249
fix B). The 9-min full-enrichment doesn't block operator visibility.
- Reliability has multiple independent fallbacks today — Polygon outage
degrades to yfinance, which degrades to Finnhub. Flipping the chain
order or making Polygon hot-path-required would reduce that depth.
- 98.4% Polygon hit rate is already excellent; further speedups would
deliver minutes of saved time per restart but add concurrency or DB
schema surface area.
Deferred options for future review (do not re-propose without
re-running the live tier-split probe to check if the picture has
changed):
- Option B (low-risk speedup): extend _polygon_get_market_cap
to also cache primary_exchange + name from the same response →
eliminates ~250 separate yfinance round-trips per cold start.
Estimated saving 30-90s. Zero reliability impact.
- Option D (high-value, schema change): persist Polygon-resolved
market_cap_billions to a new column on earnings_markets so
DB-bootstrap reads it instantly — cold-start drops to T+30s for
full enrichment. Requires migration + writer-placement decision.
- Option C (chain-order flip — Polygon first): NOT recommended
without Option D's DB safety net. Makes Polygon a hot-path
dependency without offline cache fallback.
- Option E (parallelize Polygon calls): NOT recommended — adds
concurrency surface area for ~5 min of restart-only savings.
cleanup_local() silently wiped local backup when Spaces
unconfigured (HIGH — RESOLVED Apr 27 17:40 UTC). Caught during the
manual #267 validation: Result=success, ExecMainStatus=0 but
/tmp/polymarket-backups/ was empty post-run. RCA: the original
cleanup ran unconditionally at the end of every backup, deleting the
just-created file when Spaces wasn't configured (operator's local-only
choice per #260b). New cleanup_local(keep_recent_days, keep_at_least_one)
with proper retention semantics. Three branches in main(): Spaces-success
wipes local (Spaces canonical), Spaces-fail keeps last 7d + most recent,
no-Spaces keeps last 7d + most recent. Verified on prod: 2.3 GB backup
retained at /tmp/polymarket-backups/polymarket_db_20260427_173028.sql.gz.systemd:* (always-on) and
systemd_timer:* (timer-fire freshness). Net: 6 batch services
(polymarket-backup, polymarket-resolver, polymarket-trade-reconcile,
polymarket-equity-bars, polymarket-data-backfill,
polymarket-earnings-sync) could fail and no one was told. Fixed: new
helper _check_systemd_unit_last_result() reads `Result + ExecMainStatus, returns FAIL when last run was killed/oom/etc.
6 new rules added (systemd_last_run:*). Production-verified: monitor
now correctly reportspolymarket-backup.service: fail (Result=signal,
ExecMainStatus=9)`. Total integrity rules: 28 → 34.polymarket-trades-backfill.service has no scheduler
(DOCUMENTED only, low). Service unit exists, was changed to
Type=oneshot in Phase 1, but has no .timer and no cron — never auto-runs.
Operator decision needed: intentionally manual or missing scheduler?StartLimitIntervalSec/Burst in wrong systemd section
(DOCUMENTED only, low). Systemd silently ignores the directives.volume_24h_at_entry NULL for 76 consecutive trades since
Feb 16 (Medium — RESOLVED). RCA: Polymarket Gamma API returns
volume24hr=None intermittently (live probe shows ~50% null rate);
collector writes 0 → markets.volume_24h accumulated zeros for 88% of
recently-synced markets → signal_generator.get_market_volume() treats 0
as falsy → signal.volume_24h=None → INSERT writes NULL. Fix: fall back
to most-recent positive price_snapshots.volume_24h within last 6 hours.
Backfill: 114/117 (97%) trades historically populated. VZ #120 now
volume_24h_at_entry = 550.95.signal_emitted rows since deploy have
snapshot_schema=pm_v2 but pm_best_bid/spread NULL. Schema label fires;
the orderbook fields it was meant to capture are missing. Phase 3 unified
simulator (consumer of these fields) hasn't shipped yet — can be fixed
in lockstep. Tracked.size directive (DOCUMENTED
only, low). Daily-only rotation lets collector.log grow unbounded
between cron fires (caught at 8.7 GB). logrotate -f truncated correctly,
proving config is otherwise sound. Recommend adding size 500M.Lesson logged for the agent: §B Step 5 (acceptance test) and §G
(post-fix verification) failed on my original #265 fix — I never opened
the actual VZ row in the API response post-deploy. Going forward, every
fix MUST end with a probe of the changed output (curl /api/...,
journalctl tail, psql SELECT) — not just import/syntax/pattern checks.
Comprehensive audit and rebuild of the data-recording surface area shipped in one session Apr 27, 2026. Operator decisions: Q1=fresh build (Phase 3), Q2=defer retention (Phase 5), Q3=threshold-based (Phase 1).
Phase 0 — Audit baseline (Finding #250): comprehensive read-only audit via four parallel subagents produced docs/DATABASE_AUDIT_2026_04_27.md (9 sections, all SQL evidence). Headline findings cataloged below.
Phase 1 — Silent process-failure fixes (Finding #251 a-f): six services now exit non-zero on per-script failure thresholds so systemd correctly surfaces failures. Fixed: equity_bars_collector.py (10% threshold), trade_reconciler.py (status='error' check), outcome_resolver.py (50% per-tier infrastructure-failure threshold), backup_database.py (Spaces upload failure), polymarket-trades-backfill.service (Type=oneshot), auto_login_gateway.py broken send_telegram_message import (added wrapper to alerts.py).
Phase 2 — Active data-loss patterns (Findings #252-#257): investigation revealed FIVE of six audited "data-loss" patterns were NOT current bugs (forward-only fixes hadn't fired yet because no trades closed since deploy + no signal-evaluation cycles ran since the weekend). One real bug fixed: trade_audit_log.hold_duration_hours 99% NULL — column existed in schema since migration 005 but no code wrote it. Added to position_manager.py close-path UPDATE; backfilled 113 historical rows (1/116 → 114/116 = 98% coverage). Three "no current bug" findings (#252, #254, #255) get integrity-monitor coverage in Phase 3 (alert if expected post-deploy rows still NULL after a weekday cycle).
Phase 3 — Unified integrity monitor (Finding #258): new src/data_tracker/integrity_monitor.py (~700 LOC) replaces v2/v3 watchdogs. Declarative rule config: 28 rules across 6 categories (5 freshness + 5 NULL-rate + 11 systemd + 2 artifact + 4 storage + 1 meta). Migration 027 created integrity_status table. New AlertCategory.DATA_INTEGRITY plugged into existing AlertManager (rate-limit + dedup + circuit breakers + retry inherited free). New systemd unit polymarket-integrity-monitor.service. Decommissioned v2 + v3 watchdogs (deploy hook idempotent). Telegram alerts on HIGH/CRITICAL transitions only (per operator policy). Pre-deploy live test on prod: 28 rules / 6.2s cycle / 0 transitions / clean.
Phase 4 — Database tab redesign (Finding #259): new /api/integrity endpoint (JSON-snapshot-first with DB fallback). Database tab fully redesigned around 4 panes (Freshness / Coverage / Services & Pipelines / Storage & Retention). Removed all hardcoded misleading sections (WebSocket: Connected literal, Watchdog auto-restart HTML literals, Market Info: 100% always-green, frozen Reliability Timeline prose, Database Tables heuristic-size column, Active Earnings Markets via tags). Live 30-second JS polling. Net dashboard.html size: 6562 → 6253 lines.
Phase 5 — Storage hygiene (Finding #260 a-f): investigation surfaced 4 production-critical issues beyond original scope. #260a (CRITICAL): polymarket-backup was OOM-killed every run since DB grew past ~6 GB (loaded entire pg_dump output into memory via process.communicate()). Fixed via streaming pg_dump pipe in 1MB chunks. #260b: Spaces creds not configured (operator's local-only choice — documented). #260c: watchdog v2 + v3 leftover unit files; v3 had been crash-looping with ModuleNotFoundError, generating 291MB of tracebacks. Idempotent cleanup loop in deploy hook. #260d: log rotation — new deploy/logrotate/polymarket (daily, 14d retention, copytruncate). /var/log/polymarket/* was 11GB at audit start. #260e: Timescale job 3 (166 failures) is an Apache-edition limitation — even alter_job() requires community license. Documented as known limitation. #260f: trade_dedupe 4.1GB documented as acceptable bloat.
Phase 6 — Doc refresh (Finding #261, this update): consolidated changelog above + DATA_ARCHITECTURE.md v1.6 (current numbers, undocumented tables added, gap_tracking phantom removed, retention/compression limitations documented) + DATA_INTEGRITY.md §2 rewritten (integrity monitor replaces watchdog) + OPERATIONS.md §8.A integrity-monitor runbook + §8.B service exit-code semantics + §8.C derived-artifact freshness expectations.
integrity_status table populated with 28 rules per cycle; /api/integrity serving live data; Database tab rendering 4 panes from JSON snapshot.AlertCategory.DATA_INTEGRITY for HIGH/CRITICAL transitions; first alert (RESOLVED) fired Apr 27 08:35 UTC during deploy.policy_job_stat_history_retention) will keep failing 1×/day on Apache edition. Accepted; documented in OPERATIONS §8.A.markets.winning_outcome 100% NULL — phantom column, no live consumer, kept in schema for compatibility (Finding #257).trades/orderbook_snapshots/price_snapshots will continue to grow unbounded. Integrity monitor's storage:* rules are the safety net. Revisit when growth pattern + simulator data needs are clearer.docs/FINDINGS.md — Findings #250-#261 (DB Integrity Audit), #262 (dashboard 500 chain), #263-#265 + #265.v2 + #267-#274 (Apr 27 LATE evening rigorous audit + #272 RCA).docs/DATABASE_AUDIT_2026_04_27.md (frozen snapshot of pre-remediation state — see Phases 1-6 outcomes appendix at the bottom for what shipped against it).http://[hidden]:8080/#database or curl /api/integrity (now serves 34 rules — was 28; 6 new systemd_last_run:* rules added Apr 27 evening per Finding #268).src/data_tracker/integrity_monitor.py.polymarket-backup.service correctly flagged fail (Apr 27 04:09 UTC OOM kill); other 5 batch services ok. Backup OOM streaming fix awaits first proven success at Apr 28 04:00 UTC.docs/DATABASE_AUDIT_2026_04_27.md — the frozen baseline that subsequent phases (1-6) remediate against. Headline findings: (1) polymarket_opportunity_log and skipped_signals haven't received new rows since Apr 24 (3 days idle while prices/trades stream); (2) polymarket_opportunity_log.snapshot is 88 % empty {} with NO schema_version field anywhere despite Finding #243 claiming pm_v2; (3) trade_audit_log.conviction_at_exit 100 % NULL despite Finding #237; (4) sp500_change_at_entry 100 % NULL despite Finding #234; (5) six services exit 0 despite per-row failures (systemd can't see them); (6) deployed watchdog is watchdog.py (v2) but doc describes watchdog_v3.py; data_gaps is entirely tombstoned; (7) no Timescale retention/compression on 27 GB of hypertables, DB tripled from doc's 9.3 GB to 32 GB; (8) Database tab has hardcoded misleading sections (WebSocket: Connected literal, Market Info: 100 % literal, frozen Reliability Timeline prose). Six-phase remediation roadmap defined; Phase 0 ships now (this doc + Finding #250 + AGENT_HANDOFF refresh + Strategy-Architecture cross-reference); Phases 1-6 require per-phase operator approval. Canonical: docs/FINDINGS.md Finding #250 + docs/DATABASE_AUDIT_2026_04_27.md.📅 Upcoming Earnings widget at Live 0 rows · DB 148 rows ~10 min after the Phase-4-partial deploy. RCA: pre-existing slow-cold-start where _refresh_cache() takes ~40 min to first-populate earnings_calendar because it serially looks up market cap for ~540 Yahoo-calendar symbols and yfinance was rate-limited (~6.8 retries per symbol). Two stacked fixes:src/utils/caching.py (chain becomes yfinance → Polygon → Finnhub). Pre-deploy live probe: 98 % Polygon hit rate at ~150 ms per call across 50 symbols including the worst-case 12-retry tail (TRP, TPG, NWG, IMO, IESC, ING resolved). Single shared module per memory §M — populate_mcap_cache, _fetch_single_mcap, discover_unknown_mcaps, signal_generator.get_market_cap_for_symbol all benefit transitively.earnings_dashboard/app.py. New _bootstrap_cache_from_db() runs BEFORE the slow _refresh_cache() and populates earnings_calendar, db_stats, trade_history, high_conviction, recent_trades, dependencies from DB only (no yfinance, no Yahoo earnings calendar, no Gamma batch). New db_only=True parameter on get_earnings_with_polymarket() + get_polymarket_earnings_from_db(). mcap_tier read from earnings_markets.market_cap_category per memory §L (Finding #219).docs/FINDINGS.md Finding #249.docs/STRATEGY_ANALYSIS_ARCHITECTURE.md §0.11:#phase-status-banner): explicit reference to STRATEGY_ANALYSIS_ARCHITECTURE.md, STRATEGY_PLAYBOOK.md, .cursor/memory.md. Phase progress chips (0/1/2/2.1/2.2/2.x ✓ · Phase 3 ⏳ gated · Phase 4 🛠 in progress · Phase 5 ○ post-Phase-3). Two-column "what's done / what's left" grid with live Apr-26 numbers (98.1 % actual_beat, 90.9 % actual_earnings_time, 21 symbols / 47.5k bars Day 0). 5-outlier curse + 4-check gauntlet + true-simulator pinned reminder.findings-legacy-cards block — was display: none, predates findings-dynamic-container + /api/architecture-findings. Per memory §J the canonical narrative lives only in docs/FINDINGS.md.<table id="stop-reversal-scenarios-DELETED"> + JS populator + stop-rev-savings card populator (no DOM consumer) all removed./api/strategy-analysis contract).docs/FINDINGS.md Finding #248 + docs/STRATEGY_ANALYSIS_ARCHITECTURE.md §0.11.equity_bars_1m (migration 025), collector, cache helper, Finding-A simulator wired, daily timer, deploy hook. Day-0: 21 symbols, 47,510 bars. 30-day capture window completes ~May 26, 2026 — the only open Phase 3 gate.pm_v2 schema in JSONB captures full PM book + conviction-window on every signal going forward.--force flag); turned out the resolver's natural floor was a hidden bug, not a data-source ceiling.actual_beat coverage 84.1% → 98.1% via data-api /trades historical backfill. +103 markets newly resolved, 5,697 new snapshot rows.earnings_markets.actual_earnings_time column (migration 026) populated via SEC's submissions/{cik}.json acceptedDate. 0% → 90.9% coverage (667 / 734 past-settled markets); precise timestamps refine the simulator's ±90-min release-window heuristics.polymarket-data-backfill.service extended to 6 ExecStart passes; new markets get post-event snapshots + SEC timestamps every Sunday automatically.docs/STRATEGY_ANALYSIS_ARCHITECTURE.md — the single permanent reference mapping every layer (operative rules / forward SoT / production data / scripts / dashboard) with status flags on every assumption, a data-gap matrix, and a decision-usefulness verdict on unified simulation ("conditional YES, only after Phase 1 + 2 land"). 868 lines, no code changes.renderRecommendations ReferenceError on Strategy tab load (HTML invoked deleted function). Fixed.stop_triggered writer-side fix (deferred from Finding #233 Apr 25). Now derived from exit_reason at write time on both primary and fallback close paths. 55 historical rows backfilled. Field now matches exit_reason ILIKE '%stop%' 57/57.conviction_at_exit writer added on close path (was 116/116 NULL despite column existing). Unblocks deferred Hypothesis H8.compute_counterfactual_pnl.py added to polymarket-data-backfill.timer (Sun 04:00 ET) so new opportunity_log rows get counterfactual P&L weekly without manual intervention.signal_generator.py:1168); EDGE_CASES.md 75% reconciled. Trade-count transition documented as 100→150 (current target)→200 fallback decision tree per operator clarification.docs/archive/ with redirect headers (FINANCIAL_PROJECTIONS, POSITION_SIZING_ANALYSIS, SYSTEM_REVIEW_2026_01_06, STRATEGY_IMPROVEMENTS). Stale-warning headers added to BMO_AMC_TIMING_DEEP_DIVE (uses old 2/3/4% stops) and PAPER_TRADING_TEST_PLAN (says "STOP" not "STP-LMT"). HOLISTIC_REVIEW_PROMPT refreshed with current capital + sizing + stops references.STRATEGY_ANALYSIS_ARCHITECTURE.md §0.6 conditions.docs/FINDINGS.md Finding #232.trade_audit_log.stop_triggered field broken (Apr 25 LATE evening). Discovered during Finding #232 re-validation: only 2 of 116 closed trades have stop_triggered=true, but 57 of 116 have exit_reason ILIKE '%stop%'. The flag undercounts at 1.7 % while real stop frequency is 49 %. All historical analyses depending on stop_triggered are wrong. Forward-fix: every analysis must use exit_reason ILIKE '%stop%' until the audit-log writer is patched. Canonical: docs/FINDINGS.md Finding #233./trades (Apr 25 evening). Universe view was capturing only 31 % of all earnings markets (279/900) — the "missing" 549 had NULL prob_24h_before. Root cause: orderbook collector (continuous_collector) starts sampling many markets only a few hours before the earnings event, so the 24h/12h/6h-before snapshot windows fall outside the sampled timespan. New scripts/backfill_conviction_from_trades.py pulls the full pre-event trade history from Polymarket's data-api and reconstructs probabilities. Recovered 384 of 393 past markets (98 %); in-band universe rose 279 → 568 markets (+104 %) (now 604 with organic growth + timer fire). Canonical: docs/FINDINGS.md Finding #230.continuous_collector.sync_markets_from_gamma queried Gamma with ?active=true&archived=false, which silently excluded the (active=true, closed=true, archived=false) state — Polymarket's "settled but not archived" — that every recently-resolved earnings market sits in. Fix: two-pass sync (active + closed-not-archived) in the collector + one-off scripts/backfill_markets_volume.py. Recovered 319 markets / $5.7M of volume; markets passing the bot's $2K gate jumped 111 → 546. Canonical: docs/FINDINGS.md Finding #229.scripts/analyze_finding_a_full.py) that closes 8 gaps the v3 audit did not model: gap-open slippage at RTH re-arm, hardware vs software gating distinction, earnings_detector cap on wrong-prediction trades, halt detection, full-population spillover, and the bias gauntlet on RuleB / RuleD / RuleG / RuleF. Result: rule as written (FA_soft) is inert — 0 % impact — because hardware STP-LMT keeps outsideRth=True per Finding #179. The only effective form (FA_hard) is −$418 / 100 trades, opposite sign from the audit's claim. Findings D / G also fail the 4-check gauntlet on the simulator's clean baseline. Only RuleB (±90 min window) survives at +$123 / 100. New canonical narrative: docs/FINDINGS.md Finding #228. Forward-looking source of truth: docs/STRATEGY_PLAYBOOK.md. Audit retraction recorded in docs/STRATEGY_AUDIT_v3.md §10.polymarket-resolver.timer (06:00 ET) + MarketRegimeProvider shared module. actual_beat coverage on past earnings_markets rose from 25 % → 73 % via a 4-tier source cascade (Yahoo → Finnhub → Polygon /vX/reference/financials → Polymarket post-event yes_price). Full narrative: docs/FINDINGS.md Finding #227. Operations runbook: docs/OPERATIONS.md.docs/STRATEGY_AUDIT_v3.md with §10 retraction..env — burst-tested 50 req/5 s with 0 × 429s. Default --polygon-sleep 0.05 everywhere.Aug 19, 2026: the complete, criticality-ordered implementation plan for everything open after the #357–#362 revalidation cycle (the approved C-1/S2c/B-4/RES/B-5 package, the Class A queue, all 16 monitors/contracts with dates, operational items, and the items found in no queue) lives in
docs/IMPLEMENTATION_PLAN_2026_08.md. Read that first; the table below is a historical Apr-25 snapshot.Single source of truth:
scripts/strategy_performance_analysis.py:build_action_queue()regenerates this list on every report run. The dashboard 🎯 Action Queue tab anddocs/STRATEGY_PLAYBOOK.md§2 are programmatic mirrors. Edit the function → both surfaces refresh.
| # | Rule | Status | Source | Est. lift / 100 | Risk |
|---|---|---|---|---|---|
| 1 | Skip LONGs in downtrend regime | 🟢 IN-QUEUE — ship next | v3 Finding B / v2 #1 · Apr 25 re-val (Finding #232) | +$140 to +$200 (revised UP) | LOW — STRONGER bias check (22/22 distinct tickers; ex-top-2 = −$175, more negative because SWBI was masking; 8+ distinct losers) |
| 2 | RuleB — ±90 min no-stop window around release | 🟢 IN-QUEUE — PROMOTED rank 4 → 2 | Apr 25 simulator (Finding #228 §10.5) · Apr 25 re-val (Finding #232) | +$123 | LOW — only stop-loss rule that survived; stop bleed is strategy's biggest leak (−$676/57 stops) per Finding #233 |
| 3 | Daily 3-stop session breaker | 🟢 IN-QUEUE | v2 #6 · Apr 25 re-val (8 historical ≥3-stop sessions confirmed) | tail-risk + ~$50 historical | LOW (defensive; real fire frequency confirmed) |
| 4 | Re-validate full audit at trade #150+ via the 4-check gauntlet | 🟢 IN-QUEUE | memory §K | meta | meta — currently at 116, trigger at 150+ |
| ⏸️ | v3 D — Skip Mid-cap LONGs at conviction ≥ 80 % | ⏸️ ON-HOLD pending RuleB | v3 audit + Apr 25 universe view + Apr 25 re-val (REINFORCED — Mid 90-95 % has 87 % stop rate) | +$110 | n/a — stops-eat-winners hypothesis vindicated; RuleB resolution highly likely. Note: Large 80-90 % degraded from +$22 → −$3, possible future widening to "ALL LONGs ≥ 80 %" |
| ⏸️ | v3 E — Skip non-downtrend SHORTs | ⏸️ ON-HOLD pending RuleB + ≥30 SHORT trades | v3 audit + Apr 25 operator decision + Apr 25 re-val (REINFORCED — sideways SHORT 83 % stop rate) | +$60 | n/a — total SHORT n=18 too small; KEEP set GPRO+STUB-dominated |
| 🛑 | v3 F — BMO + VIX-bucket sizing | 🛑 RETRACTED Apr 25 LATE evening (Finding #232) | v3 audit (claim) + Apr 25 deep bias gauntlet (refutation) | n/a — claimed +$60, modeled +$36, scale-UP arm fails ex-top-3 | n/a |
| 🛑 | v3 C — drift filter | 🛑 RETRACTED Apr 25 (option 3 — drop and rely on B+D+E) | v3 audit (claim) + universe view (refutation) | n/a | n/a — universe view at 5-day baseline showed skip-bucket net positive; live n=13 too small to override |
| 🛑 | v3 A — Block stops outside RTH | 🛑 RETRACTED Apr 25 (Finding #228) | v3 audit (claim) + simulator (refutation) | n/a | simulator: −$418/100 (hard) or $0 (soft, inert) |
Combined realistic lift if all IN-QUEUE rules ship: +$190–$310 / 100 trades. Slightly UP from the prior +$160-$280 estimate (Finding B's bias check came out stronger; v3 F drop offset by RuleB promotion + 3-stop breaker confirmation). Flips the current run rate of +$1.44/trade to +$3.30 to +$4.50/trade average.
Implementation order matters: ship #1 first (Skip LONGs in downtrend) — cleanest mechanism, no execution dependencies, ~10 lines in signal_generator using existing MarketRegimeProvider. Then ship RuleB (#2) — addresses the stop bleed at root and unblocks v3 D + v3 E re-evaluation (both ON-HOLD pending RuleB; bias_check fields explicitly state "if RuleB closes the live stop bleed → close as RESOLVED-BY-OTHER-RULE").
Per-rule implementation traces (5-step verification per memory §B): docs/STRATEGY_PLAYBOOK.md §6.
docs/STRATEGY_AUDIT_v3.md §6 has 5 lessons from the v3 audit; §10 added the true-simulator rule (Finding #228 lesson — proxies and observational tables are NOT counterfactual evidence); §13.6 added the single-source-of-truth rule (live production tables are canonical, not derived artifacts); §14 added the canonical-field-validity rule (Finding #233 lesson — never trust convenience flags like stop_triggered without cross-checking against the source-of-truth field like exit_reason).
The most important inherited rule (renamed Apr 25 — was 3-outlier through Apr 25 morning): the GPRO + DELL + SWBI + TXN + STUB quintet carries ~$430 of the strategy's $167 net P&L on 116 trades. Ex-top-5 the strategy is −$263 across 111 trades. Any slice that includes them looks great; any slice that excludes them is structurally negative. Always report numbers ex-top-2 alongside the headline; for any slice containing 2+ of the named 5 outliers, also report ex-them. The 4-check bias gauntlet is mandatory for every new finding (memory §K).
| Source | State | Self-healing |
|---|---|---|
markets.volume_total |
5,622+ markets with positive volume; in-band 90.4 % covered (546 / 604) | ✅ polymarket-data-backfill.timer (Sun 04:00 ET) re-queries Gamma with closed=true for newly-resolved markets |
earnings_markets.prob_*_before |
698+ of 900 populated; 54 past markets still NULL (genuinely no PM activity) | ✅ Same timer chains conviction_tracker --backfill + data-api /trades fallback |
earnings_markets.actual_beat |
70 % past markets resolved (528 / 752) | ✅ polymarket-resolver.timer (daily 06:00 ET) — 4-tier source cascade |
| Universe view (LONG > 60 % + SHORT < 30 %, resolved) | 604 markets (was 568 in last handoff, 279 in morning) | ✅ Auto-refreshes every report run |
trade_audit_log.stop_triggered |
✅ FIXED Apr 26, 2026 (Finding #236) — derived from exit_reason at write time; 57/57 closed stop-exits match canonical |
✅ Both writer-side + analysis-side now agree |
trade_audit_log.exit_reason |
✅ Canonical stop indicator. 49 % real stop frequency on 116 trades. | ✅ Always populated by order_executor on exit |
trade_audit_log.regime_at_entry / vix_at_entry / sp500_change_at_entry |
✅ FIXED Apr 26, 2026 (Finding #234) — orchestrator INSERT path now mirrors PositionManager; 100% / 98% / forward-only coverage post-backfill | ✅ Forward-only enrichment via regime_provider snapshot at entry |
trade_audit_log.conviction_at_exit |
✅ FIXED Apr 26, 2026 (Finding #237) — close path now reads live Polymarket conviction. Forward-only (cannot reconstruct historical) | ✅ Will populate on every close starting Apr 26 |
polymarket_opportunity_log.hypothetical_pnl |
✅ Triaged Apr 26 (Finding #238) — 39% completion is correct (61% legitimately non-simulatable); weekly self-heal added to backfill timer | ✅ polymarket-data-backfill.timer now includes compute_counterfactual_pnl.py |
polymarket_opportunity_log.snapshot |
✅ Verified Apr 26 morning (Finding #241) — 1064/1064 (100%) populated. ✅ ENRICHED Apr 26 evening (Finding #243) — pm_v2 schema captures full PM book + conviction-window on every new signal; IBKR L1 still deferred to Phase 3 |
✅ Captured live by signal_generator._emit_pm_opportunity_log |
earnings_markets.actual_beat (past-settled coverage) |
✅ 71.6% → 84.1% (Finding #244 6-hour-guard fix) → 98.1% (Finding #246 post-event snapshot backfill). 14 remaining unresolved are genuinely irreducible (6 with zero post-event PM trades + 8 PM-ambiguous in 0.20-0.80 range). | ✅ Daily resolver + weekly post-event-snapshot backfill in polymarket-data-backfill.timer |
earnings_markets.actual_earnings_time (SEC 8-K precision) |
✅ NEW Apr 26 (Finding #247) — 0% → 90.9% (667 / 734 past-settled). Precise to within minutes from SEC's 8-K acceptedDate. Refines simulator's ±90-min release-window heuristics on non-traded markets. | ✅ Weekly SEC EDGAR backfill in polymarket-data-backfill.timer (5th pass; 6 total passes now) |
equity_bars_1m (NEW Apr 26 — Finding #240) |
🟢 21 symbols / 47,510 bars / 0 failures on Day 0 backfill. Daily timer accumulates; 30-day window completes ~May 26 | ✅ polymarket-equity-bars.timer (daily 10:00 UTC ≈ 06:00 ET, before market open) |
11 hypotheses deferred to next audit pass — need better data capture or larger n. Full list with re-test triggers: docs/STRATEGY_AUDIT_v3.md §9. Re-trigger the audit after the next 50 closed trades (currently 109; trigger at 150+, or earlier if Action Queue items #1–#4 ship and we want to measure impact).
One follow-on observation worth tracking (not in queue, see Finding #230 §13.3): LONG 95%+ bucket emerged at universe scale as the strongest LONG segment (n=27, +$192/trade). Currently skipped by Mar 9 Finding #202 (skips ≥ 90 %). Live n at 95%+ is just 2 trades — far below gauntlet floor. Re-test at trade #150+ if signal persists.
Apr 28-30, 2026 — three larger items parked for post-Phase-3 review (operator decisions Apr 28-30 — all saved in docs/FINDINGS.md + docs/STRATEGY_PLAYBOOK.md §7 "DEFERRED-POST-PHASE-3" + Action Queue under DEFERRED-POST-PHASE-3 filter on the dashboard):
MarketRegimeProvider compound rule lags SPY peaks by median 31 trading days (2018-2026 empirical, 8 peaks; 2nd-slowest in the SMA family — only close<MA200 is slower; missed the Sep 2020 −8.1% drawdown entirely). Industry-consensus faster alternatives ranked by integration cost: SMA→EMA, drawdown OR clause, VIX term structure (most-watched leading vol-regime indicator on institutional vol desks; we already capture VIX, VIX3M is one extra yfinance call), breadth, credit spreads, FCI composites. Asymmetry to fix: downtrend rule does NOT require close<MA200 (uptrend does). Why deferred: Action Queue rule #1 was sized on 22 historical downtrend-LONG trades identified by EXACTLY the current lagging classifier — switching detectors retroactively shrinks the bucket and may drop below the n≥12 gauntlet floor (memory §K). Re-runnable baseline: scripts/analyze_regime_detection_lag.py (committed Apr 28). Re-test trigger: Phase 3 simulator shipped + rule #1 live ≥30 trades + lift within ±$30 of estimate → build v2 candidate, re-run gauntlet on every regime-using rule, ship only if ≥+30% improvement.POSITION_SIZE_PCT_LARGE=0.30 / MID=0.22 / SMALL=0.15 (config.py:141-143) and 0-6 tier system (priority.py) were set Mar 9, 2026 against ~50 trades + ~$25k capital. Today: 116 trades, +$167 net, stops are biggest leak; Mid LONG 80-90% has 52% stop rate, Mid 90-95% has 87%; LONG 95%+ universe-bucket is +$192/trade but currently SKIPPED (#202); SHORT ≤ 10% bucket is ALSO SKIPPED via symmetric ("size","90+") omission in PRIORITY_TABLE despite the rule never being data-validated on SHORTs — n=18 total SHORTs, zero at ≤10% conviction. Operator caught May 6 on COIN #2026-05-07 yes_price=0.0755; 0-6 tier system has dead values (tier 1 unused, tier 2 has n<5). Finding #218 quintile data exists but informs no rule. Sizing-by-mcap currently treats different EV regimes as identical. NEW May 9, 2026 (Finding #285 Phase B): rare-day exposure cap (60% on 1-2 opp days vs 140% normal) caught silently rejecting valid signals on TTD + MCHP May 7. Phase A shipped — greedy-fill in BMO + AMC loops + opportunity_log emission for skip_stage='exposure_cap'. Phase B (rule re-evaluation) deferred-here per operator (May 9 evening): "most days become rare days as our bottleneck is not no of markets is no of markets with volume above $2k usd" — 22/34 last days = "rare". Rule's premise doesn't hold when low opp-count is structural. Phase A logging will accumulate ~10-30 cases by May 26 → enough for §K bias gauntlet on Phase B rule change. Why deferred: Action Queue selection rules #1 + #2 directly change which trades get sized + stopped — measuring sizing changes BEFORE those rules ship would conflate sizing-effect with rule-effect. Phase 3 unified simulator (with equity_bars_1m from #240) is needed to replay sizing variants under realistic execution cost. Re-test trigger: Phase 3 simulator + rule #1 + RuleB live ≥30 trades each + trade #150+ audit gauntlet pass → comprehensive re-evaluation under unified simulator with current sizing as baseline, flat-equal as null, 4+ candidate redistributions, gauntlet per variant._calculate_depth_quality (conviction_calculator.py:588-628) collapses to binary 0.30/0.75 in production; 0.50 threshold = hard skip at signal_generator.py:1568. Investigation: 37 of 38 historical depth-skips were spread-dominated (bait orders at 1¢/99¢) — gate is misnamed (renamed WIDE SPREAD on dashboard Apr 30 — UX-only, behavior unchanged). 35 of 38 had conviction = 0.500 default → would have been skipped at conviction gate anyway → gate is redundant for 92% of its kills. Only 3 historical kills were real signals: ICE 2026-04-28 LONG 89.9% counterfactual P&L +$158.22 (real missed winner), MPC 2026-04-29 LONG 74.5% counterfactual $0.00 (wash), and MRNA 2026-05-01 SHORT 18.5% today (about to be the 4th real-signal kill — $4,210 cumulative volume + 18.5% conviction from real trades; bid/ask 1¢/99¢ is bait). Why deferred: n=3 is well below n≥12 gauntlet floor. Phase 3 simulator + counterfactual P&L on wider universe needed. Re-test trigger: Phase 3 simulator + ≥10 additional depth-skipped opportunities with computed counterfactual P&L → decompose into min_depth_gate + spread_gate named separately; evaluate "skip gate when conviction is from last_trade_price/price_snapshots"; re-tune 0.50 threshold; ship only if v2 ≥+30% improvement on gauntlet.# All 7 services should be active (added polymarket-equity-bars.timer in Phase 2)
ssh root@[hidden] 'systemctl is-active \
earnings-trader polymarket-dashboard polymarket-collector \
polymarket-resolver.timer polymarket-data-backfill.timer \
polymarket-equity-bars.timer ibgateway-ibc'
# Timers' next-fire times (3 timers post Phase 2)
ssh root@[hidden] 'systemctl list-timers \
polymarket-resolver.timer polymarket-data-backfill.timer \
polymarket-equity-bars.timer --no-pager'
# Equity 1m bar cache size (Phase 2 infrastructure — Finding #240)
ssh root@[hidden] 'source [hidden]/.env && export DATABASE_URL && \
psql "$(echo $DATABASE_URL | sed s/+pg8000//)" -c \
"SELECT count(DISTINCT symbol) AS symbols, count(*) AS bars, \
pg_size_pretty(pg_total_relation_size(\\\"equity_bars_1m\\\")) AS size \
FROM equity_bars_1m"'
# Expected (Day 0): 21+ symbols, 47k+ bars; should grow daily as the timer fires.
# Universe + actual_beat coverage (post Findings #229 + #230 — should be ≥600 in-band)
psql "$(ssh root@[hidden] 'grep DATABASE_URL [hidden]/.env | cut -d= -f2-' | sed s/+pg8000//)" \
-c "SELECT
count(*) FILTER (WHERE prob_24h_before > 0.60 OR prob_24h_before < 0.30) AS in_band,
count(*) FILTER (WHERE earnings_date < CURRENT_DATE AND actual_beat IS NOT NULL) AS resolved_beat,
count(*) FILTER (WHERE earnings_date < CURRENT_DATE) AS past_total
FROM earnings_markets"
# Stop-rate sanity check (Finding #233 — confirms exit_reason path, not stop_triggered)
ssh root@[hidden] 'source [hidden]/.env && export DATABASE_URL && \
psql "$(echo $DATABASE_URL | sed s/+pg8000//)" -c \
"SELECT count(*) FILTER (WHERE stop_triggered) AS broken_flag_count, \
count(*) FILTER (WHERE exit_reason ILIKE \\\"%stop%\\\") AS canonical_count, \
round(100.0 * count(*) FILTER (WHERE exit_reason ILIKE \\\"%stop%\\\") / count(*), 1) AS real_stop_rate \
FROM trade_audit_log WHERE status = \\\"closed\\\" AND realized_pnl IS NOT NULL"'
# Expected: broken_flag_count ~2, canonical_count ~57, real_stop_rate ~49%
# Backfill timer last-run log
ssh root@[hidden] 'tail -30 /var/log/polymarket/data_backfill.log'
# Dashboard universe chips (manual check)
open http://[hidden]:8080/#strategy
# Expect: 🎯 In conviction band: ~600 · ✅ Resolved: ~570 · 💰 With positive volume: ~550 (≥90 %)
This handoff document is maintained for operator/agent continuity. Last structural pass (Sep 27, 2026 — #481/#482 DEPLOYED, deploys #14 28b572b / #15 860af84; B3 #454/#455, deploy #13 00607c9; #479 grace hardening, deploy #12 2ecb02c; #467 + #479 restart grace, deploy #11 3138aca; #477/#480, deploy #9 0e985dd; #457/#478/#479, deploy #8 614a29d; earlier docs sweep + #473–#475, deploy #7 a695050) aligns with .cursor/memory.md (through §DOC R1–R7 + C12), docs/FINDINGS.md (through #482), docs/STRATEGY_AUDIT_v3.md (through §14), and docs/STRATEGY_PLAYBOOK.md §2 (generated from build_action_queue(); do not hand-edit the Action Queue). History: Apr 27, 2026 LATE evening pass documented through #274 logrotate size and then-current Playbook §2.