The owner's modularity/monitoring ask: rate each pipeline component so what's earning (and
what's cutting) is visible instead of inferred. Everything derives from the existing event
stream — no pipeline changes.
- research/attribution.py: compute_attribution(days) -> per-day per-stage metrics:
DISCOVERY (symbols found + precision = fraction that ever go ready), SIGNALS (ready split
EARLY-vs-LATE against the confirmation window — the only +EV ones), GATES (risk_rule
histogram: what each gate cut), EXECUTION (submissions vs cancels = fill efficiency),
OUTCOMES (realized P&L, win rate, per-close detail). CLI: python -m research.attribution.
All queries filter the native timestamp column first (payload casts on the 676k-row events
table time out).
- api/main.py: GET /api/attribution?days=N.
- dashboard.html: "Component Attribution" panel, lazy-loads on open, per-day table with
gate-histogram hover.
- momentum_cli board header: live pulse line — today's early/late signal split + top gate
cuts (two tiny date-scoped queries).
Verified live: CLI scorecard over 7 days, endpoint returns real ratings (e.g. 2026-07-02:
discovery 45%, early 38%, fill 39%, win 50%, P&L +$29), board renders the pulse line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Focused review pass on the session's safety-critical additions, applying the safe wins:
- exit_manager: drop the dead `_Tracked.last_stop` field (written, never read) and its
assignment; guard `_rest_protective_stop` against a degenerate qty<1 (a 0-qty submit
is rejected and would churn) + document the cur-falsy->breakeven fallback; reword the
synthetic-R comment (the branch is also hit on a healthy trade once breakeven ratchets
the stop to entry, not only the stripped-stop case).
- exits: surface default_trail_r_pct in ExitConfig.describe() (boot-log observability)
and clamp it to [0, 0.95] in from_env (a >=1 / negative synthetic R would put the stop
at/above entry).
- db_pg: comment the gate-vs-try-order distinction of _LAN_DB_HOSTS (superset incl.
localhost) vs _LAN_DB_FALLBACKS so a future edit can't break the non-LAN-DSN guard.
- momentum_cli: drop an unused `import sys`.
Behavior-preserving; 29/29 exit/exits tests green. Deliberately did NOT extract a shared
flatten-vs-rest helper for the catastrophe/naked blocks — the explicit blocks read
clearer for this safety code and the tests guard against drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From a design pass (CLI spec + dashboard spec + an adversarial glance-safety review)
that read the real output of both surfaces. Principle: each safety/state number appears
ONCE on the first screen, every list has a bounded height capped by relevance (active/open
first), and load-bearing signals (the UNPROTECTED banner, heartbeat, day P&L, circuit
breaker) are never trimmed or hidden.
CLI board (momentum_cli.py _render_board): drop the static 2-line strategy subtitle (never
changed, redraws every 3s); trim the heartbeat to "Ns"; armed table shows actionable
(armed/fired/long) rows first then the top-4 scanning by rvol, dropping corrupt >1000%-gap /
0-volume rows with a "+N more scanning" footer; remove the redundant ready-signals panel
(restated the armed table) and the day-P&L echo in the trades title; trades float open lots
to the top (never capped off) and cap closed at 6; risk collapses consecutive identical
fires into one "×N" row (the 12 duplicate KEEL lines -> one) and is promoted ABOVE trades.
Dashboard (api/static/dashboard.html): cap the long "runs" — ready-signals to top 15 by
quality (also kills ~80 per-refresh chart fetches), activity feed 20, catalyst feed 20, order
lifecycle shows only actionable orders (raw 1000s incl rejected/cancelled read as an alarm),
watch-states hide the score-gate 'blocked' rows; drop redundant strip cells (Positions dup of
pnl Open/Closed, Gross) and the Account panel's Buying-Power/Synced; demote Account to a
collapsed half-width panel; drop the ~constant ready-signals "Setup" column. JS syntax
validated, no dangling refs, serves 200.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the multi-agent audit (ship-now, low-overfit-risk cluster):
INGESTION RESILIENCY
- client._request: 429/5xx now retry with Retry-After + exponential backoff
(previously hard-failed, feeding silent-stale ingest).
- SIP backfill feed (settings.backfill_feed='sip'): live realtime stays IEX, but
backtest/backfill ingestion uses the full consolidated tape — IEX sees only
~2-5% of true volume (verified: AAPL 22,797 vs 452,704), which was silently
corrupting every rvol/$-volume decision and backtest. eod_replay wired to it.
- step_ingest freshness watchdog: emits a STALE event + board note when the
newest bar is older than INGEST_STALE_SECONDS during RTH (45% of passes
historically returned 0 rows with no error). Optional hard arm-block
(INGEST_STALE_BLOCK, off by default).
RISK
- Portfolio gross-notional cap (TRADING_MAX_GROSS_NOTIONAL_PCT=0.60): both sizing
paths now cap each entry to the remaining gross budget, so 6 x 16% can't pile
the book into ~96% gross of correlated small-caps. Blocks/shrinks late entries.
- Fixed the duplicate TRADING_MAX_CONCURRENT_POSITIONS in .env.example.
MONITORING (UI)
- Naked/unprotected-position alarm: _unprotected_positions joins the latest
position + order snapshots (order sync now flattens legs) and the TUI board
shows a red UNPROTECTED banner the instant a held long lacks stop coverage —
the exact blind spot when APWC/NOWL went naked. Verified it flags today's
3 overnight positions.
Tests: gross-notional cap blocks when budget full; 50 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The journal/board reconstructs from broker timestamps (UTC) and printed the raw
UTC time, so a 10:12 ET entry showed as 14:12 on a US-market journal. Add _to_et
and convert both the today-filter and the displayed time to America/New_York.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header's "matched" subtotal used the FIFO round-trip sum, which doesn't
equal the broker's average-cost day P&L while positions are partially open — so
"matched + open" never tied to the headline day P&L (e.g. -1340 + open vs the
broker's -627).
Show broker-authoritative realized instead: matched = day P&L - open unrealized,
so matched + open == day P&L by construction. Per-trade rows stay FIFO-accurate
(each is a real fill pair); only the header subtotal is broker-truth. Factor the
open-unrealized sum into a shared helper used by both the live board and the
`journal` close capture. Falls back to the FIFO sum when no equity snapshot
exists.
Verified live: board + journal both reconcile (matched + open == day P&L).
Tests: 3 new (reconciliation invariant + FIFO fallback).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A long-lived `momentum watch` connection got closed by Postgres on idle/restart
overnight, and the next query raised OperationalError and crashed the board.
PgConnection now reconnects ONCE on OperationalError/InterfaceError (skipped for
isolated test schemas); the watch loop also catches transient errors and shows
'reconnecting…' instead of dying. Hardens the live loop's event store too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
momentum journal prints the day's trades (time/symbol/qty/entry/exit/P&L) +
broker-accurate day P&L + win summary. journal.sh appends it to
data/trade_journal.log; cron 16:02 weekdays keeps a durable daily record.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- momentum watch: replace 'open positions' with TODAY'S trade history (FIFO
round-trips from the broker orders snapshot: time/symbol/qty/entry/exit/P&L +
open lots), resets each day. Headline day P&L is broker-accurate (equity -
last_equity, now carried on AccountSummaryUpdatedEvent), shown in the header
and the panel title. sync_orders limit 50->500 so the log captures the whole
day across restarts.
- SYMBOL_WEIGHTS: optional manual per-symbol priority nudge applied to the
armed-trigger rank score (small; doesn't override gap/volume logic).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
News (you were right it's relevant): recent_news_map() pulls fresh catalysts per
ticker from the ingested RSS feeds; step_arm tags each armed candidate and BOOSTS
catalyst-backed gappers up the rank (NEWS_RANK_BOOST) — a fresh PR/FDA/offering
is why a small-cap runs, so it surfaces the hotter name earlier and higher. The
board shows a 📰 marker + the headline. Feeds verified live (PR Newswire /
Business Wire / GlobeNewswire) — pulling real catalysts (FDA approvals, offerings).
Forward-tested only (news isn't OOS-backtestable).
eod_replay.sh + crontab: replay at 16:05 (grow dataset), tune at 17:30.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
So you can confirm the bought price next to the unrealized P&L.
Profit-lock tiers verified out-of-sample (lock_mid: 8:3,15:9,25:18,40:30 on the
prior-low trail) — test avgR 0.127 vs 0.123 baseline, win 55% vs 46%, not
overfit; already the .env default. The flashy lock_only_mid (+0.40R in-sample)
was rejected as overfit (collapsed to 0.187 test).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- strategy/exits.py: percentage profit-lock tiers (TRADING_EXIT_PROFIT_TIERS,
e.g. 8:3,15:9,25:18,40:30). Once a trade has been up gain%, the stop is pinned
to entry*(1+lock%) so a winner can only give back to a locked-in minimum gain
and a fade sells you out IN PROFIT. Shared by backtest + live, active-trades
only (ratchets the broker stop up via replace, never cancel).
- momentum_cli.py: open-positions uPnL was reading 'unrealized_pl' but the sync
stores 'unrealized_pnl' -> always blank. Read the right key (+ green/red).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes found by running the manager against the live book:
- _stop_leg queried status="open", but a bracket's protective stop is a 'held'
child of the FILLED entry, so it was never found -> stops never trailed.
Query status="all" and match active leg statuses.
- when the trailed stop would sit ABOVE current price (price fell back through
the prior swing low), a sell-stop there is rejected; that IS the trail firing,
so exit at market instead.
Also drop the hardcoded "6" in the board title (armed set is now 10).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "always late / miss the move" problem was structural: entries waited for a
1-min bar to CLOSE above the breakout then walked the ingest->watch->execute
cadence (~2 min), and rested a limit AT the trigger that a runner never fills.
Add a top-6 armed-trigger book (runtime/triggers.py): keep the most-promising
gappers ready with a pre-computed opening-range trigger/stop and FIRE the
instant LIVE price crosses, via TradingExecutionService.submit_breakout_now —
a marketable order (capped above the trigger) that actually fills — reusing the
same risk gates (daily-loss breaker, max-concurrent, 1%-risk sizing, dedup).
- structure.py: opening_range() helper (shared with the ORB detector)
- run_live_paper.py: step_arm (rank + arm, 20s) + step_trigger (price-cross
fire + snapshot, 4s)
- momentum_cli.py: rewrite the watch board — market clock + countdown, loop
heartbeat, the 6 armed triggers with live price / gap / rvol / trigger /
distance / stop, and a plain-English "what it's hunting" line
- tests: opening range, trigger book state machine, submit_breakout_now gates
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>
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>
- 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>