(a) learn_params.per_setup_report: permanent per-setup P&L/earliness breakdown
in the harness (forces all-setups so the ORB-vs-rest comparison is visible).
(b) nightly_tune.py + nightly_tune.sh: backfill latest bars, sweep ORB params
over accumulated gapper sessions, write the best PROFITABLE config to
data/learned_params.json. run_live_paper reads it on boot and applies the
tuned ready_score / min_bars (explicit env still wins); won't overwrite with
a losing or thin-sample config. Cron-scheduled after the close.
Verified: 75/75 tests; boot logs the learned params and applies them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sweep over 172 real gapper sessions (16 days) with a per-setup breakdown found
the opening-range breakout is the ONLY profitable setup (+0.25R); every slow
pattern loses (bull_flag -$78k @ ~42min is the worst). Running ORB-ONLY flips
the book from -$95k to +$7.4k AND enters ~11 min earlier (23 vs 34 min avg).
- classify_setup prefers the ORB when valid and honors STRATEGY_SETUPS env
(comma-sep setup values) to restrict active setups; .env defaults to
STRATEGY_SETUPS=opening_range_break (ORB-only).
- BacktestEngine Trade.setup_type captured for per-setup attribution.
- learn_params load_sessions filters to gapper sessions (mimics live scanner) so
the backtest trades the same universe the system would.
75/75 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- structure.detect_opening_range_breakout: break of the first ~5 RTH bars' high
on >= OR-average volume; entry=OR high, stop=OR low. Placed first in
classify_setup so it fires minutes after the open instead of waiting for a
pullback to form. Verified: RGNT triggers at bar 6 (~9:36 ET) q=1.0.
- evaluate_setup counts OPENING_RANGE_BREAK toward the structure criterion.
- backfill_history.py: pull ~3 weeks of minute + 60d daily bars for the sub-$20
universe so learn_params optimizes over real sessions.
75/75 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- learn_params.py: grid-search the strategy timing/threshold knobs over stored
sessions, ranking by total P&L AND 'entry_min' (minutes after the open the
entry fires) — a learning method to find params that enter earlier while
staying green.
- evaluate_setup(min_bars=...) + BacktestEngine(ready_score_pct/criteria/min_bars)
now tunable; watcher passes WATCHER_MIN_BARS through (was a no-op: evaluator
hard-coded 10).
Sweep finding: even the earliest params enter ~28-30 min after the open
(baseline ~33) because the first-pullback setup waits for a pullback to FORM.
Cadence/min_bars are not the bottleneck — the setup design is. Genuine earliness
needs a fast opening-breakout trigger + more historical sessions to learn from.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HIGH:
- Circuit breaker fails CLOSED: a repeated unreadable equity (API 401/429/5xx or
missing field) now halts new entries after 3 consecutive failures instead of
silently reporting 'no breach' and continuing to trade through a drawdown.
- execute(): map order statuses. Terminal-fail (rejected/expired/...) -> ok=False
(never armed as a resting entry); record ANY fill (partial too), not only an
exact 'filled'; require a real broker order id. approve_order only arms
genuinely OPEN statuses.
- sync: account/positions/orders failures now emit a BrokerHealthChangedEvent
(degraded) and step_sync surfaces 'DEGRADED', instead of a success-shaped None
that left risk gates reading stale snapshots as current.
MEDIUM:
- _flatten_all records a position closed only on a CONFIRMED close; the breaker
event reports 'halted_flatten_incomplete' + the unclosed symbols when a close
fails, instead of claiming the book is flat.
- submit_exit_order prefers close_position (handles short sign + fractional qty;
a plain sell of abs(qty) would double a short).
LOW:
- get_orders(nested=true) so bracket child legs aren't dropped from the snapshot.
+ tests/unit/test_execution_safety.py (4 regressions). 75/75 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- discover_active_symbols: distinguish AUTH/entitlement failures (HTTP 401/403,
logged as ERROR, 'not no-movers') from transient/empty in the screener except.
- run_live_paper: the boot watchlist event records an explicit error when it
falls back (screener empty at boot) instead of looking like 'market quiet'.
- discovery.news_candidates: log DB faults instead of swallowing to [].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The live 'symbols' list only ever grew (discover appends every 5 min, dashboard
injects add more) and was comma-joined into ONE bars request; once >~100 symbols
Alpaca 400s the whole batch -> ALL minute-bar ingestion silently stops mid-session.
- client.get_minute_bars / get_daily_bars / get_latest_trades batch symbols in
chunks of 100 (cap-safe) and merge; get_daily_bars also paginates now.
- run_live_paper bounds the watchlist (WATCHER_MAX_UNIVERSE, default 80) so it
rotates toward current movers instead of growing all day.
Found by the silent-failure audit workflow. Verified _chunked(250)->[100,100,50],
live calls intact. 71/71 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- discover_active_symbols logs the screener error instead of swallowing silently.
- run_discovery records an explicit error when the universe is empty (surfaces in
the discovery event + Grafana).
- momentum doctor now FAILs when the latest discovery universe is 0 (the morning's
dead screener would have shown red within a minute).
- tests/unit/research/test_discovery.py: pins get_most_actives top<=100 and the
ETF/price-band filtering.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- discover_active_symbols requested get_most_actives(top*3); once screen_universe
used top*2 headroom for the ETF filter (top=20 -> 40 -> get_most_actives(120)),
this exceeded Alpaca's hard cap of 100 -> HTTP 400, swallowed by the bare
except -> discovery returned EMPTY every cycle. The live loop therefore traded
the static fallback list (non-gappers) instead of real movers all morning.
Fix: cap at min(top*3, 100). Verified: screen_universe now returns 20 real
sub-$20 names, ETF-filtered.
- Add ./momentum launcher (project venv) so 'momentum watch' works without the
full venv path ('python' on the box is the Windows pyenv shim, no deps).
67/67 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- grafana/: docker-compose + provisioned Postgres datasource + 'Momentum — Live'
dashboard (evaluation board with status colours, ready signals, risk events,
bars + equity timeseries) reading the event store via JSONB extraction.
Run: cd grafana && docker compose up -d -> http://localhost:3000.
- RSS news-catalyst discovery wired: extract_tickers also matches $CASHTAGS;
run_discovery pulls tickers from recent news into the universe when NEWS_FEEDS
is set (no-op / safe otherwise).
- docs: .env.example NEWS_FEEDS; GO_LIVE monitoring adds 'momentum watch' + Grafana.
67/67 tests green. Live board (momentum watch) verified rendering the evaluation
board with real determinations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full-screen auto-refreshing board (rich.Live) of what the system is evaluating
in real time: per-symbol status (ready/late/blocked), score, gap%, RVOL and the
blocking reason, ranked by score — plus ready signals, open positions, and risk
events. 'momentum watch' (refresh) or '--once' (single frame). Reads the
Postgres event store.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the daily-loss breaker trips it now also flattens the book: cancels
unfilled (armed) entries and market-closes every open position via
client.close_position (which cancels resting bracket legs). Configurable via
TRADING_FLATTEN_ON_BREACH (default on). Best-effort per symbol; emits a
'halted_and_flattened' risk_rule_triggered event with the flatten summary.
Verified: trip at -4% closes 2 seeded positions + cancels 1 armed entry. 67/67 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- trading_execution: implement the daily-loss circuit breaker (account equity vs
prior close; latches once tripped; fail-open on a data hiccup) — was
advertised in the docstring/settings but never implemented.
- discovery: filter leveraged ETFs (SOXS/TZA/TSLL/NVD/BITO/...) out of the
universe; retry the screener on a transient empty (rate-limit) result.
- run_live_paper: fallback watchlist is now liquid sub-$20 names, not mega-caps
(mega-caps just get filtered by the watcher band -> nothing to trade).
Verified live: forced ready signal -> auto-approve -> real Alpaca bracket order
(pending_new, then cancelled); breaker trips at -3% (simulated). 67/67 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Market-open runbook: start command (observe mode), the 9:30-10:35 ET trading
window (10:30 entry cutoff), monitoring commands (doctor / inspect signals /
criteria / bars), how to enable paper execution, env tuning, honest known limits.
Verified this session: continuous loop stable with graceful Ctrl-C; full
pipeline (discover -> ingest -> evaluate) produces ready signals on real
2026-06-15 morning data; 67/67 tests green throughout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- SetupEvaluationResult.criteria_detail: per-criterion {name,passed,reason}.
- watcher CriteriaEvaluatedEvent now carries gap_pct, relative_volume, status,
reason, and the per-criterion detail (was only passed/failed names).
- CLI: 'inspect criteria SYMBOL' shows status + gap/rvol + each criterion reason;
new 'inspect signals' = ready board + per-symbol gap/rvol/status.
Verified on real 2026-06-15 bars: RGNT shows 9/9 with reasons (rvol 26.2x),
'late' explained by the 10:30 ET cutoff. 67/67 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- research/ingestion/discovery.py: screen sub-$20 most-actives, backfill daily,
rank gappers, persist scanner snapshot, and EMIT a discovery event.
- run_live_paper: watchlist defaults to the screener universe (was a hardcoded
mega-cap list); add a periodic 'discover' task; emit bars-collected
(per-symbol counts) and discovery events every pass.
- IngestionResult.per_symbol: per-symbol row counts (incl 0) so 'are bars
collected?' is answerable per symbol, distinguishing market-closed from broken.
Verified live vs Postgres: watches SOXS/RGNT/GRAB/SOFI/ACHR/... (sub-$20),
820 daily bars backfilled, discovery+ingestion events land in the event store.
67/67 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Drop duckdb dependency (pyproject, requirements) -> psycopg2-binary.
- run_live_paper: boot log shows the real Postgres target (masked); add
_db_target_desc() helper.
- .env.example: document DATABASE_URL / PG* vars; mark WATCHER_EVENT_DB_PATH legacy.
- evaluation/storage.py: INSERT OR REPLACE -> INSERT (setup_id is a fresh uuid).
Verified: 67/67 tests green on Postgres with duckdb UNINSTALLED; app boots
against the live endpoint (127.0.0.1:5432 proxy) and wrote 205 daily bars.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- storage/db_pg.py: thin psycopg2 connection shim (?->%s translation,
autocommit, .df(), and per-test isolated schema for ':memory:').
- storage/db.py: delegate get_connection/get_memory_connection to db_pg.
- storage/pg_schema.sql: Postgres schema, applied on connect.
- research/query.py runs UNMODIFIED on Postgres via the shim.
- ingestion upserts: INSERT OR REPLACE -> INSERT ... ON CONFLICT DO UPDATE.
- event_store: drop invalid btree-on-JSON-text index.
Full logic suite: 67/67 green against Postgres (was DuckDB).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Snapshot before the DuckDB->PostgreSQL migration. data/, archive/, the 232MB
rebuild zip, the Windows .venv, and .env are excluded via .gitignore.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>