Telemetry: discovery took 6-30s ON the single scheduler, freezing ingest/arm so the trigger read stale bars — the real 'late edge detection'. Now discovery runs on its own daemon thread with a dedicated DB connection; the watchlist it mutates is a lock-guarded Watchlist (list-like drop-in, no torn reads); the scheduler discover step auto-disables (single-writer). Loop is now 3 fast threads (entries/exits/discovery) + slow scheduler for tolerant work. Also: nightly_tune writes a heartbeat every run, so the boot alert distinguishes 'tuner not running' (real) from 'ran, no profitable ORB config, kept params' (expected — the strategy has no edge). Kills the misleading Telegram alarm. 543 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implementation Plans — next enhancement milestone
Generated by the improve skill on 2026-07-13 at commit d54a2a9, from an
8-cluster recon sweep (~95 evidence-cited findings), a 5-frame ideation pass
(48 raw ideas → 14 merged candidates, all 14 verified sound by a fresh-context
spot-check against HEAD), and a design-fit review of the dashboard additions.
Companion design artifacts: docs/ideation/2026-07-13-next-milestone-ideation.html
(ranked ideas + bases), docs/ideation/momentum-architecture-current.{drawio,png},
docs/ideation/momentum-milestone-featureset.{drawio,png}.
Execute in the order below unless dependencies say otherwise. Each executor: read the plan fully before starting, honor its STOP conditions, and update your row when done.
Execution order & status
| Plan | Title | Priority | Effort | Depends on | Status |
|---|---|---|---|---|---|
| 001 | Production safety rails — CI test gate, dead-man's switch, pg_dump backup | P1 | S | — | DONE |
| 002 | Dead-weight deletion — schwab/, grafana/, dead modules/tables/projections | P2 | S | 001 | DONE |
| 003 | Money-path hardening — daily self-restart, positive-evidence contract, reconcile, cage count | P1 | M | 001 | DONE |
| 004 | Learning flywheel — one population module, scheduled eod_replay + nightly_tune, atomic writes | P1 | M | 001 (002 rec.) | DONE |
| 005 | Entry-path unification + decision receipts | P2 | M | 001, 003, 004 | DONE |
| 006 | Experiment OS — pre-registered counterfactual experiments over logged fires (flagship) | P1 | L | 004 (005 rec.) | DONE |
| 007 | Operator control plane — dashboard truth + kill switch + effective config (step 1 = prod bug fix, ship early) | P2 | M | 001, 003 (006 for ledger data) | DONE |
| 008 | Conversion-signal model — measure if the labeled data separates winners | P1 | L | 006, 004 | MEASURED 2026-07-14 (corrected): the 0.514 was the liq1-dominated MIX. Scoped to live v1: CV AUC 0.641 — REAL signal, but it is ~entirely the VWAP gate the bot ALREADY runs (above-VWAP +1R 35.4% vs below 12.7%). Residual within above-VWAP = 0.526 ≈ noise. RVOL-fallback added (read-only test 2026-07-14): 0.526→0.535, within noise — RVOL adds nothing beyond VWAP. Verdict: VWAP is the whole story for current features; no second indicator. Only market-context + catalyst remain untested (forward-accrual only, negative prior). The lever is a different setup (premarket), not more ORB filters. |
| 010 | Premarket shadow entry — prove a FILL-ABLE edge before any live premarket order | P2 | L | 006, 009 | NO-GO (Step 1 measured 2026-07-15): premarket is a paper-fill mirage. pm1+fat-tail-exit expR: −0.50R @0.5% fill, −1.27R @1% (realistic thin book), −2.74R @2%, vs the ORB's −0.11R. The 22-min fat tail can't outrun the 1-2% premarket spread. STOP — do not build the live shadow track. The ORB's 5-min wait exists to trade in penny-spread regular-session liquidity. |
| 009 | Feature-population prerequisite — instrument v1/pm1 (market context, RVOL fallback, catalyst-on), then re-run 008's probe | P2 | L | 006, 008 | code DONE, awaiting supervised rebuild+probe (market context + thin-name RVOL fallback + SPY/QQQ ingest + enrichment-on + committed probe all landed & tested; Steps 4-5 rebuild/AUC run supervised). 2026-07-16: the 07-15 nightly job never fired (docker outage overlapped 20:00 ET) — 07-15 was a data hole (0 setup_events, no SPY ingest, no regime row). BACKFILLED supervised: SIP refetch + 50 v1 setups + 6 pm1 fires + SPY/QQQ ingest + regime row for 07-15. Accrual clock unbroken; verify tonight's 20:00 scheduler fires. |
| 011 | Regime gate — a FREE-data daily signal deciding whether the ORB trades today; metalabel P(our ORB profits this session), proven in the harness (walk-forward) before any live gate |
P1 | L | 006, 004, 009 | BACKTESTED ON PROD (2026-07-15) — VIABLE as a SHADOW damage-control signal, NOT yet a live gate. 266 walk-forward sessions: trade-always −0.111R/session (36% hit) → meta_v1 GATED −0.004R/session (48% hit), trading only the best ~32% of days; delta +0.107R in-sample, +0.128R OOS (recent 80 sessions), improves across 3 folds. threshold_v1 rule gates too little to matter. Leaky full-fit +0.61R ≫ walk-forward +0.078R confirms the walk-forward discipline was load-bearing; OOS holding = the edge is real. Verdict: turns a bleeding strategy roughly FLAT by sitting out bad regimes. FIRE-WEIGHTED harness (the LIVE v1 population — gap≥10%, vol≥300k — 2041 fires): BOTH experiments PASS their pre-registered bars (first PASS of the milestone) — regime_meta_v1 expR_delta +0.0965R, baseline −0.085R/fire → GATED +0.0117R/fire (POSITIVE on the real population, keeps ~33%); regime_threshold_v1 +0.083R but keeps only 26 fires (its pass = 'sit out 99%', thin/ignore). So on the actual tradeable population the gate is mildly POSITIVE, not just flat. DEPLOYED (shadow): scheduler runs build-features+score nightly; admin /api/regime/run button triggers on demand. LIVE-GATED IN PAPER (Step 5 shipped 2026-07-15, commit 0ac09d3): _regime_reject blocks fresh entries on unfavorable sessions, TRADING_REGIME_GATE_ENABLED=1 in both compose, fail-open everywhere. Blocker-1 (tape_regime not pre-open) resolved by DELETING tape_regime — measured 0.000 importance; LIVE feature set = breadth_rvol(vol_upto/avg, non-look-ahead) + spy_lagged + trailing. Takes effect on next deploy. Real-money promotion still a separate owner decision. 2026-07-16 AUDIT + FIX: the shipped live gate was structurally INERT — live_session_favorable read today's breadth_rvol from today's OWN setup_events rows, which the labeler only writes at 20:00 ET, so the fail-open guard returned None every live session (verified: no meta_live score + 1288 all-pass gate receipts on 07-16; the 07-15 'validation block' p=0.080 was computed PRE-guard on a zero-filled feature — an artifact, not a validation). Also found breadth_rvol was UNIT-CONTAMINATED: _session_context pooled all setup versions, and pm1 stores RAW cumulative premarket volume in relative_volume (median 261k) vs ratio tracks (~0.8) — ~240/267 historical sessions were pm1-raw-volume, and plan 009's v1 RVOL fallback was about to flip forward sessions to ratio units (07-15 rebuilt = 0.18), silently breaking the feature. FIX (same-day): breadth_rvol redefined pm1-ONLY (median premarket volume — the dominant historical source, unit-consistent, strictly pre-open knowable); live gate recomputes today's pm1 fires IN MEMORY from premarket minute_bars (pure compute_premarket_setups, no insert); _regime_reject retries fail-open verdicts every ~5 min instead of caching None all day (definite verdicts still once/day). regime_features rebuilt (267 sessions, labels unchanged), scores rewritten, fire-weighted re-validation vs the PRE-REGISTERED bars: regime_meta_v1 expR_delta +0.0570 (bar +0.05, n=2058, PASS — weaker than the contaminated +0.0965 but real), keeps 37%, baseline −0.078→gated −0.021; recent-80-session delta +0.149R/session (always −0.141 → gated +0.008, trading 29% of days). threshold_v1 +0.0766 but keeps 1% (sit-out-everything, ignore as before). Live check 07-16 on fixed code: breadth 612,819, verdict UNFAVORABLE — first genuine live decision. Adversarial review (19-agent, 9 findings) hardened the live path: previous_close falls back to the latest prior session's close when today's daily_bars row is absent intraday (else the gap basis silently flips to the premarket open = different fire population than training); verdicts before 9:50 ET are PROVISIONAL (re-scored ~5 min — ingest lag can truncate the premarket frame) and the first verdict at/after 9:50 is FINAL even when None (caps the scorer at a handful of runs/day instead of ~78 on quiet days — hot-path stall guard); NULL-breadth training rows excluded from the live fit (zero-fill taught a 'zero premarket volume' class the serve path can never produce); throttle datetime arithmetic moved inside the fail-open try. KNOWN ACCEPTED SKEW: live premarket volumes are scaled-IEX estimates vs the nightly SIP tape (up to ~25-50x low on symbols without a volume_ratio row) — regime_scores['meta_live'] vs the EOD label is the standing forward measure of whether this skew flips verdicts; revisit (intraday SIP refetch at 9:46+, 16-min embargo) if agreement is poor. Takes effect on next image deploy. Prior status below. CODE DONE + REVIEW-APPROVED (commit 5cef4aa), deploy-wired + backtested at 092d607. Built: research/regime.py (probe + walk-forward build-features/score), iv_regime_gate harness intervention (keep-or-zero, fail-open), regime_threshold_v1+regime_meta_v1 JSONs, 5 tests incl. the mutate-the-future leakage guard. Reviewer re-ran: 9 new + 35 existing experiments tests green, ruff clean, live paths untouched. HMM rung skipped (statsmodels absent). Research: docs/research/regime_notes.md. SUPERVISED next step (hits production Postgres — NOT done by the executor): python3 -m research.regime probe (Step-0 STOP gate) → build-features → score --model threshold_v1/meta_v1 → register+run+adjudicate the two experiments → record expR_delta + OOS/fold results here. Review findings to fold in at that point: (1) tape_regime = earliest setup's at-trigger (~9:35) reading, not strictly pre-open — fine for the shadow harness, must be lagged before Step-5 live promotion; (2) minor: _load_regime_favorable model_version has no None-guard (both JSONs set it, fail-open if absent). Honest prior unchanged: most promising AND most curve-fit-prone (~150-260 sessions); walk-forward + threshold-floor + OOS decide GO vs "no free-data regime signal at this sample size". |
| 012 | Dashboard order-limits verbosity + dead-column cuts (owner request 2026-07-16) | P2 | S | — | DONE (2026-07-17, review-APPROVED after 2 revision rounds; commits b349518+1f3ddca on advisor/012-dashboard-order-limits in worktree .claude/worktrees/plan012 — awaiting owner merge). Step 3 revised mid-execution: lifecycle events never carried limit/stop, enriched client-side via client_order_id join to d.orders. |
| 013 | Symbol drill-in: 24h candlestick + live buy/sell trade markers (TradingView Lightweight Charts v5.2, vendored) | P2 | M | 012 (soft — reuses its bracket-leg maps) | DONE (2026-07-17, review-APPROVED after 1 revision round; commits b4ad3d3+709e7a4 on worktree-agent-ae2ee0000d5e3a7eb — awaiting owner merge). Headless-browser render-verified. Executor surfaced 2 pre-existing out-of-scope defects: OrderFilledEvent has no side field (worked around via order_submitted sibling join in /api/chart) and GET /api/attribution 500s when DATABASE_URL unset (research/attribution.py hardcodes os.environ lookup). |
| 014 | Live decision trace — gate-receipt feed (value→threshold per gate) + chart reject markers | P2 | M | 013 (chart overlay half; feed panel standalone) | DONE (2026-07-17, review-APPROVED first pass, zero revisions; commit dd81dbb on worktree-agent-acb00535650deac1a — awaiting owner merge). Decision Trace panel (lazy-load + SSE-throttled), /api/decisions (clamped 500, case-insensitive, server-side epoch), collapsed reject markers merged into the chart's single markers primitive. |
| 015 | Backtest replay dashboard (/backtest) — theoretical entries/exits over past sessions under CURRENT settings, via Experiment-OS replay | P2 | L | 013 (chart component) | DONE (2026-07-17, review-APPROVED first pass; commit 8ac222a on worktree-agent-ae4d3f36e18c6e7a5 — awaiting owner merge). replay_window = assembly-only over harness primitives (same _entry_index/adverse-fill/R math); /api/backtest 45-session clamp + 10-min cache behind a dedicated lock; /backtest page with THEORETICAL banner + X-of-Y coverage line + per-trade drill-in (blue entry/purple exit markers). Deviations accepted: /api/chart gained backward-compatible date= anchor (72h cap couldn't reach old sessions); batching idiom taken from regime.py (plan's run() pointer was wrong). |
| 016 | EDGAR dilution enrichment — unbreak silent nightly refresh (UTC current_date bug + missing float_cache DDL) + pre-register dilution_veto_v1 keep-or-zero experiment | P2 | M | 006 | DONE (2026-07-17, review-APPROVED first pass; f6999c7 merged). iv_dilution_gate keep-or-zero (fail-open on unfetched symbols, early results biased toward zero never inflated), dilution_veto_v1 self-registers via the experiments/*.json glob, nightly adjudication live. Prod backfill seeded same day (304 symbols swept, 245 filings / 91 symbols). FIRST ADJUDICATION 2026-07-17: FAIL (n=2044, expR_delta +0.0012 vs +0.05 bar) — but only 64/2069 fires (3%) had filing coverage (backfill covers recent actives only; historical fires read unflagged by fail-open design), AND the flagged fires ran BETTER than unflagged (baseline −0.038 vs −0.377, n=64): early evidence points AGAINST the dilution-trap thesis, not merely underpowered. Bar-design lesson: min_n counted ALL fires not flagged ones. Verdict terminal (no goalpost-moving); filings keep accruing nightly. IF revisited: dilution_veto_v2 with a flagged-fire min_n metric, after ~3-4 weeks coverage. |
| 017 | L2 order-book depth recorder [VERDICT 2026-07-18: order-book overhang is REAL but too weak to trade. n=2191 under the real deployed exit: high-overhang fires -0.676R vs low -0.759R = +0.083R separation, SAME direction as the sniff (held from n=23 to n=2191 — genuinely real, not noise). BUT both buckets deeply negative; gating fails the random-gate null (delta +0.28 was mostly exposure-shrink). It reduces losses, does NOT create profit or beat a random gate. Live gap>=10% band: n=13/74, no signal. FAIL under the pre-registered bar. Same shape as the regime gate: real modest signal on a negative-EV base = less-bad, not positive. depth_features(2202) + code retained for a possible broader-population redesign; NOT wired live. Databento spend ~$16 free credits. REDESIGN LEAD REFUTED 2026-07-18: gap x overhang grid under real exit shows lower-gap is the WORST cell (0-3% -0.831R), not a hidden edge; NO positive cell exists anywhere. Exhaustive search complete — no positive subpopulation in gap-ORB.] — the one untested selection axis; FREE-first (Phase 0-1 = $0 sniff test + recorder), paid feed only after the data earns it, pre-registered verdict before any live use | P1 | L | 006 | TODO (owner-approved to plan, no spend yet) |
| 018 | Sub-second exit management on a dedicated thread | P1 | M | — | DONE (5896d29): exits on own 1s thread + dedicated conn, scheduler step auto-off, idempotent. Also shipped: fast pipeline (ingest 5s/arm 8s/trigger 2s) + emit self-heal. |
| 019 | Move discovery off the scheduler thread | P1 | M | 018 | DONE: discovery on own thread + dedicated conn + thread-safe Watchlist; scheduler discover step auto-off; single-writer. Also: tuner heartbeat fixes the false 'tuner not running' alert (it runs, finds no profitable config = expected). |
Status values: TODO | IN PROGRESS | DONE | BLOCKED (with one-line reason) | REJECTED (with one-line rationale)
Expected drift (read before running any plan's drift check)
Every plan stamps d54a2a9 and its drift check diffs against that commit. Executed
in order, later plans WILL see diffs from earlier ones (compose scheduler/db-backup
blocks from 001/003/004/006, workflow test job from 001, trading_execution/
run_live_paper changes from 003/004/005, telegram_bot from 001). That is EXPECTED
drift: verify only that the plan's quoted code excerpts still exist semantically
(line numbers may shift); STOP only on changes NOT attributable to a completed
plan in this series (check the status table above).
Dependency notes
-
001 first, always: it is the gate that makes every other plan's image push test-verified. 007's step 1 (the POST
NameErrorimport fix) is a production bug fix that may be cherry-picked ahead of everything. -
002 before the refactor-heavy plans (004/005) so they diff against a smaller surface; its production
DROP TABLEsection runs only after 001's backup exists. -
003 before 005/007: both build on its clean-shutdown/flatten semantics. Note the documented interaction: 007's
flatten_allcommand must not trigger 003's daily-restart exit (see 007 STOP conditions). -
004 before 006: every experiment population MUST come from
strategy/population.py— running experiments against a look-ahead population re-creates the exact artifact class (vr1/liq1) this milestone exists to kill. -
005 before 006 is recommended (receipts enrich replay + rank_one_bullet needs rank), but 006's bar-path replay works without it — the rank experiment just starts underpowered.
-
Postgres provisioning (owner): the only new DDL in the milestone is plan 006's
experiments/experiment_runs/experiment_verdictstables (full DDL inplans/006-experiment-os.md). Plans 001–005/007 add no tables. The owner said they will provision the database once the plan is solid — that means: apply 006's DDL (or let the harness's idempotent schema-apply do it on first run), and after 002 lands, optionally run its operatorDROP TABLElist post-backup. -
013 soft-depends on 012: the chart's entry/stop/target price lines reuse the bracket-leg maps (
stopBy/targetBy/entryLimitBy) 012 builds inrender(d). 013 alone must build them itself (STOP condition covers it). -
012/013/014/015 are UI/read-path only — zero overlap with plans 008-011's research/live-gate surface; safe to execute in parallel with any of them.
-
Recommended UI execution order: 012 → 013 → 014 → 015. 014's Decision Feed panel half can land before 013; its chart-overlay half can't. 015 consumes replay.py as-is — any plan that changes exit-sim semantics invalidates its honesty claim and must re-verify it.
-
015's data supply is the nightly setup_events build (plan 009's accrual) — a missed scheduler night is a missing day on /backtest; the page surfaces it via the "sessions replayed X of Y" line.
Queued defects (surfaced by plan 013/015 executors — ALL FIXED 2026-07-17 evening, merged: OrderFilledEvent.side additive field + emit; /api/attribution via storage facade; admin_api load_dotenv moved behind main. Remaining: scheduler regime-step stdout check if tonight's row is missing again.)
-
OrderFilledEvent has no
sidefield (storage/event_schema.py; sole emit sitealpaca_paper/execution.pypasses none). /api/chart works around it by joining the sibling order_submitted row — fix at the emitter so every consumer gets it. Live-path change: needs its own plan + tests, do NOT drive-by fix. -
GET /api/attribution 500s when DATABASE_URL unset —
research/attribution.py:_connect()hardcodesos.environ["DATABASE_URL"], bypassing the storage facade's fallback chain everything else uses. Read-side, small. -
Nightly scheduler's
regime build-featuresstep silently produced no row two nights running (07-15 full-outage, 07-16 step-level: setups+SPY landed, regime row didn't — backfilled supervised both times). Pull the scheduler container's stdout at 20:00 ET tonight; suspect thepython -m research.regimeCLI's store bootstrap (WATCHER_EVENT_DB_PATHdefault) diverging from DATABASE_URL in-container. -
Adjudicator gap (2026-07-17, from orderflow H2): the keep-or-zero expR_delta bar is mechanically beatable on a negative-baseline population — zeroing ANY ~44% of −0.21R/fire fires yields ~+0.09 delta. Add a random-gate null (delta must beat flagged_share x |baseline| by the margin) or a kept-vs-zeroed separation bar before trusting future keep-or-zero PASSes. regime_meta_v1's PASS is unaffected (its evidence was the session-level walk-forward OOS, not just the fire bar).
Findings considered and rejected (do not re-audit)
- Retest/pullback entry logic, scale-out partials, close-above-trigger confirmation from folklore — refuted in
docs/research/entry_conversion_research.md(external evidence + in-house tests); the research doc's "explicit do-nots". - Selection levers (confirmation-speed gate, float gating, liquidity floors, VWAP-reclaim promotion, liquid-breakout, leading-gainer detector, premarket-volume slices) — refuted at year scale per project memory; the validated population stays as-is.
- Reviving the NiceGUI controlroom — owner preference: :8765 is the dashboard.
- LISTEN/NOTIFY for dashboard change detection — deferred, not rejected: the shared cached signal (007 step 2) is the sufficient lazy fix; NOTIFY is the named upgrade path.
- TimescaleDB / pg extensions — not required at current scale (README's own assessment); revisit only if event/bar volume grows past what the hot-path index handles.
strategy/regime.pydeletion — already deleted at HEAD; recon reference was stale. NOTE: plan 011 introduces a NEW, unrelatedresearch/regime.py— an offline metalabel scorer (session-levelP(ORB profits today)), not the deleted live regime module. Different file, different purpose; do not conflate.- Schwab account sync revival for human-vs-bot comparison — the only non-delete rationale for schwab/; owner may veto 002's schwab deletion if that comparison is still wanted (plan 002 grep-gates it either way).