Commit Graph
18 Commits
Author SHA1 Message Date
momentum devandClaude Fable 5 e7c34f298f operator-plane: guarded command bus (flatten / pause kill switch)
Adds OperatorCommandEvent + a closed command vocabulary (flatten_all,
flatten_symbol, pause_entries, resume_entries). POST /api/command emits it
(token-required: 503 when DASHBOARD_TOKEN unset — commands never run
unauthenticated); the live loop's new 5s "command" step consumes them via
OperatorCommandConsumer with three replay-safety layers (in-session nonce set,
persisted operator_command_done reload, 10-min staleness window).

flatten_all routes through close_session DIRECTLY (never step_eod_flatten) so it
cannot trip plan 003's daily-restart exit. Pause is checked at the top of BOTH
request_approvals_for_ready_signals AND submit_breakout_now (missing either
leaks pause on that path). qabot gains an authorized-chat-only /flatten slash
command (prose "flatten" still refuses via _ACTION_WORDS). Snapshot exposes
broker truth + command_bus + a defensive experiment ledger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:15:00 -04:00
momentum devandClaude Fable 5 fbb84ae166 operator-plane: cheapen SSE change detector (shared once/sec signal)
Every SSE client polled COUNT(*) on a fresh EventStore connection each ~1s
(re-applying the 473-line schema unless PG_SKIP_SCHEMA=1). Replaced with one
process-wide _ChangeSignal: a single long-lived connection reads max(timestamp)
(idx_events_timestamp tail seek) at most once per second GLOBALLY behind a lock,
so N clients cost one query/sec. EventStore.max_timestamp() backs it; reader is
injectable for the query-budget test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:05:08 -04:00
momentum devandClaude Fable 5 07570fb0e4 operator-plane: fix dashboard POST NameError (import os/hmac)
Every POST endpoint (approve/reject/exit/watch) crashed with NameError:
do_POST references os.environ + hmac.compare_digest but neither was imported.
Adds the imports, a non-loopback regression test for the token gate (no token
-> routes; token set -> 401 without/with-wrong header, 200 with correct), and
plumbs DASHBOARD_TOKEN + PG_SKIP_SCHEMA=1 into both compose dashboard services
(+ .env.example docs for DASHBOARD_TOKEN / COMMAND_BUS_ENABLED).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:04:01 -04:00
momentum devandClaude Opus 4.8 d54a2a930c Review sweep: money-path fixes + security + delete 805MB dead weight
Consolidated 4-tool review (2 correctness agents + Codex + over-eng audit).

Money-path (regression-tested):
- _flatten_all: moved the fresh-broker close from close_session INTO the
  shared function + added a broker-held guard on entry cancellation, so
  the daily-loss CIRCUIT BREAKER (which called _flatten_all raw) no
  longer strips a just-filled bracket's legs or leaves a sync-gap fill
  naked. One guard, all callers.
- restart state rebuild: _fresh_entries cap, _requested_symbols dedup,
  and losing benches now rebuild from today's events on boot — a
  mid-session restart no longer resets the cap to 0 or re-fires a
  resting order into a duplicate (the 2026-07-06 incident).
- POST /orders no longer retries a lost-response URLError (was
  re-sending duplicate live orders); GET/DELETE still retry.
- expire_stale_entries now @_locked (raced the trigger thread).
- _session_open: ET conversion instead of a hardcoded 13:30-13:33Z
  window that only matched EDT — the fast path armed nothing all winter.

Security:
- admin panel fail-CLOSED on empty ADMIN_TOKEN (was fail-open on a panel
  fronting the docker socket); header-only token, constant-time compare.
- admin API responses redact secret override VALUES (live keys would
  have echoed after a paper->live switch).
- dashboard: removed wildcard CORS (cross-origin drive-by could drive
  trade actions); optional DASHBOARD_TOKEN gate on POST.

Deletions (~805MB + dead code shipped in the prod image): momentum-
rebuilt.zip, archive venv/htmlcov, and schwab/ core/ telemetry/ models/
runtime/providers/ storage/replay.py controlroom/ + three dead
evaluation stubs + package.json — all verified zero-importer.

+4 regression tests. 99 money-path/trigger/exit tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 13:00:10 -04:00
momentum devandClaude Opus 4.8 befcec2965 Post-migration DB health: healthcheck DDL storm + broken-pipe fixes
The loop healthcheck opened a fresh connection every 60s with schema
auto-apply enabled; the CREATE-IF-NOT-EXISTS lock bursts cancelled
minute_bars autovacuum every single cycle (visible as 'canceling
autovacuum task' each minute post-migration), so the 19M-row table
never got analyzed, snapshot queries crawled, browsers timed out, and
the dashboard log filled with double BrokenPipeError tracebacks.

- healthchecks now set PG_SKIP_SCHEMA=1 (both compose files)
- api/main.py _send_json swallows BrokenPipe/ConnectionReset (client
  hung up; nothing to tell it)
- live DB: ALTER SYSTEM max_wal_size=4GB, shared_buffers=1GB,
  effective_cache_size=3GB, checkpoint_completion_target=0.9 (the
  checkpoint-every-24s warning under migration load on NAS storage)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:41:12 -04:00
momentum devandClaude Opus 4.8 6c36d04f7a Component attribution: per-stage scorecard (CLI + /api/attribution + dashboard + board pulse)
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>
2026-07-02 12:18:57 -04:00
momentum devandClaude Opus 4.8 16c759e334 Daily Performance: per-day equity curve to compare days
Adds a day-over-day view so the bot's per-day record is comparable at a glance.
query_daily_performance returns one row per NYSE trading day {date, day_pnl
(broker equity delta), cum_pnl, realized, trades, win_rate, equity} — account
summaries (EOD equity) joined to the closed-trade journal; weekend/holiday
mark-drift on held positions is filtered out (trading days only). Wired into the
snapshot as `daily`.

Dashboard panel renders it as a waterfall: the blue line is the cumulative P&L
(the equity curve), each bar is that day's step (green up / red down) on one
shared scale. Hover shows date/day-P&L/trades/win%; click a day to open its full
view; the currently-viewed day is highlighted.

Tests: trading-days-only filter + cumulative math. Headless-verified (4 days
render, click-to-open switches the view); no JS errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:39:14 -04:00
momentum devandClaude Opus 4.8 45e403e89f Per-day journal backfill + all-time score + Trades panel
You wanted to compare the bot's success rate per market day, plus an all-time
total. Three pieces:

- Backfill (backfill_journal.py + storage/journal_backfill.py): reconstruct every
  completed round-trip from Alpaca's full filled-order history (pair entries with
  exits per symbol into episodes, read exit reason + bracket stop from the
  orders), and emit position_closed events timestamped (ET) at the exit fill so
  each lands on its own market day. Idempotent (re-run replaces prior backfill);
  live-reconciled closes deduped by symbol+minute. Run once: it journaled 22
  round-trips across 6/16-6/18, so each day's win-rate/avg-R/trades now populate
  retroactively (6/17: 57% win, 6/18: 43%).

- All-Time score (query_alltime_score + dashboard panel): cumulative across every
  closed round-trip — total realized, win rate, trades, avg R, trading days,
  best/worst day. Always shown, not date-scoped.

- Per-day Trades panel: the individual round-trips for the viewed day (symbol,
  color-coded exit reason, R, P/L, time) with a net/win-rate header — so you can
  see exactly what happened each day. LIVE now scopes the journal to TODAY (was
  unbounded, which would have shown the all-time list once the journal filled).

Tests: round-trip reconstruction (stop/target/scale-out/short/open/legs),
all-time aggregation, live=today scoping. Headless-verified both panels live and
on 6/18. No JS errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 08:28:40 -04:00
momentum devandClaude Opus 4.8 45ba264534 Fix P&L mislabelling + add a Catalyst Engine feed so the LLM is legible
Two things the operator couldn't see were wrong/opaque:

1) P&L attribution bug. query_session_pnl read each position's unrealized as
   `unrealized_pl`, but the live snapshot stores it as `unrealized_pnl` (see
   alpaca_paper/sync.py). So unrealized always read $0 and the ENTIRE broker
   equity-delta was dumped into "Realized" — a real +$330 realized / -$67
   unrealized day showed as "$263 realized, $0 unrealized, 0 closed". Fixed to
   prefer the stored key, accept the alpaca-native alias, else derive from
   qty*(price-cost). The existing test used the same wrong key (which is why it
   never caught this) — corrected, plus a regression test reproducing the exact
   broker-delta day and a derive-from-qty test.

   (Still surfaced separately: closed round-trips emit no position_closed event,
   so realized P&L is broker-derived and the trade list / win-rate read 0. That
   journaling gap is noted, not fixed here.)

2) Catalyst Engine feed. The Ollama portion only showed totals; you couldn't see
   WHAT it read or WHY it ruled. Added query_catalyst_feed (the raw stream from
   news_catalyst_cache, newest first, with a `vetoed` flag = dilutive AND
   conviction >= the 0.60 floor) -> snapshot `catalyst_feed` -> a new dashboard
   panel under Armed Triggers: time (ET), symbol, colored read (🟢/🔻/🔴 +
   type), conviction, headline, and the model's own rationale, with a red ⚠ VETO
   badge on confirmed dilution. Makes the engine legible at the open — and
   immediately exposed the 7B over-tagging one "SpaceX bear ETF" headline as
   dilutive across a dozen unrelated large-caps (harmless: none are targets).

Verified headless: strip reads total/realized/unrealized correctly; feed renders
40 reads with rationale; no JS errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 20:21:30 -04:00
momentum devandClaude Opus 4.8 0dfd6a262b Phase-2 ON + dashboard catalyst visualization
Turn on the catalyst score blend + dilution veto, and make the engine VISIBLE so
it can be watched live at the open:
- api/main.py: snapshot now carries catalyst_engine {enabled, model, score_blend,
  dilution_veto, advisories, dilutive, vetoes_today} (config read from .env +
  live counts incl. how many entries the veto blocked today).
- dashboard: a catalyst-engine status chip in the Armed Triggers header
  (model · N read · dilutive · veto · score · vetoes-today), and each armed
  gapper now shows the LLM's READ instead of just the raw headline — colored
  sentiment dot, catalyst type, and a red "⚠ VETO" badge on a confirmed dilution
  at/above the conviction floor (or "dilut?" below it).

Verified headless: engine chip renders live (230 advisories, veto+score on); a
seeded bullish gapper shows "fda approval", a dilutive one shows the VETO badge;
no JS errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:56:05 -04:00
Claude e3cb2b8e5f Add local-LLM (Ollama) news/catalyst scoring
Leverage a local Ollama model to interpret news catalysts for the small-cap
momentum strategy — distinguishing bullish catalysts (FDA approval, M&A) from
dilutive offerings the technicals alone can't see. Advisory-first, then automate.

Phase 1 (advisory; no change to trade behavior):
- research/ingestion/news_enrichment.py: stdlib urllib call to Ollama with
  structured JSON output, classifying each headline into a catalyst type,
  sentiment, conviction and dilution flag. Graceful degradation (None on any
  failure) so trading is never blocked.
- news_catalyst_cache table + populate the previously-dead news_events.sentiment.
- Runs on the existing interval Scheduler (off the hot path), gated by
  OllamaConfig; deduped by the raw_news_items payload hash.
- Surface the advisory on the dashboard approval queue (projection + api + UI).

Phase 2 (automation; ships gated OFF):
- Optional catalyst_score blended into calculate_setup_quality (None reproduces
  the exact legacy weights); pure pass-through via evaluate_setup.
- Dilution veto in TradingExecutionService (slow + fast paths) emitting a
  catalyst_dilution_veto risk event, behind NEWS_DILUTION_VETO_ENABLED.

Config (OllamaConfig.from_env), .env.example docs, and tests for parsing,
persistence/dedupe, the projection, the score blend, the veto, and an
end-to-end enrich->advisory flow (Ollama mocked). Default model moves to
qwen2.5:7b-instruct (best small-model JSON, fits a 12GB GPU).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5HTMkaadGDnAPtXe2hWwg
2026-06-19 17:29:24 +00:00
momentum devandClaude Opus 4.8 506ff253ec Dashboard: make LIVE not depend on the SSE stream
LIVE rendered ONLY from the EventSource snapshot handler — and that handler
swallowed render/parse errors (catch {}). If EventSource didn't establish in the
user's browser (proxy, extension, connection cap), LIVE stayed blank while the
date selector — a plain fetch — worked fine. Exactly the reported symptom.

- startStream() now does an immediate pollOnce() (the same fetch path the date
  selector uses) so LIVE shows data even if SSE never connects; SSE then layers
  on for low-latency pushes, with onerror -> interval polling as before.
- Surface render errors (console + conn status) instead of hiding them.
- Serve the dashboard HTML with no-store/no-cache so pushed fixes actually reach
  the browser (a stale cached shell was part of why "I see nothing" persisted).

Verified headless: with EventSource forcibly disabled, LIVE still populates
(10 armed rows, pnl, risk) via the polling fallback — stamp shows "polling ·".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:56:19 -04:00
momentum devandClaude Opus 4.8 9e730a3726 Remove DuckDB remnants (datastore is Postgres)
The duckdb library is already gone (not a dependency, not importable, no
`import duckdb` anywhere) — these were leftover text references that misled.

- Delete storage/schema.py: 418 lines of DuckDB DDL with zero importers. The
  Postgres schema is created by db_pg._apply_schema + event_store._ensure_schema.
- Neutralise stale default db_path strings (./data/momentum*.duckdb) in
  event_store, config, run_live_paper, dashboard_api — the connection layer
  ignores the path (DATABASE_URL is the datastore), so these phantom file paths
  only created confusion. Dropped the now-pointless os.makedirs for them.
- research/multi_schema: the "three DuckDB files" are one Postgres DB now (all
  resolve to the same connection); doc + constants updated to match reality.
- Fix stale "DuckDB handle" comments in api/main.

Kept the DuckDB mentions in storage/db_pg.py + storage/db.py — they document
why the Postgres facade mimics DuckDB's ? placeholders and :memory: semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 22:43:29 -04:00
momentum devandClaude Opus 4.8 9392e0d4d3 Dashboard: fix snapshot timeout (O(1) reads) + date picker + .env/host
The /api/snapshots endpoint was timing out (the screen showed all '—'): several
projections did query_events(type, limit=None), loading an event type's ENTIRE
history just to take the latest. Timed: watch_states 112s, account_orders 78s,
risk 80s -> ~280s total.

- Latest-state reads (account orders/positions/summary, session_pnl, risk_state)
  now use a DESC-LIMIT-1 index seek (_latest_event_payload) — O(1). Live snapshot
  4.7s -> 1.5s; the heavy account_orders payload no longer loads all history.
- Date picker: /api/snapshots?date=YYYY-MM-DD reviews a past session (windowed
  queries via _evt_window). Default LIVE; header <select> populated from
  available_dates; live-only panels (triggers/watch/approval/ready) skip for a
  historical view (watch_states was the 112s killer + a live concept). Frontend
  setView()/loadHistorical() switch between live stream and a one-shot date load.
- dashboard_api.py now loads .env (DATABASE_URL resolves to the configured
  Postgres) and binds 0.0.0.0 / port 8765 so it's reachable from outside WSL.

Verified: LIVE 1.5s, HISTORICAL(2026-06-18) 0.75s with full P&L/equity/fills.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 21:59:31 -04:00
momentum devandClaude Opus 4.8 80a2a0bde6 Dashboard: intraday equity sparkline + per-position R bars; sync stop_price
Two more visualizations + the bug building them exposed:
- Equity-curve sparkline in the P&L strip: query_equity_curve returns the day's
  total_equity series + prior-close baseline (dashed); rendered as an inline SVG
  (green/red vs baseline) so the day's P&L shape reads at a glance.
- Per-position R bar in Open Positions: shows current open profit/loss in R units
  (-1R = the stop, 0R = breakeven), green right / red left, with a per-row ⚠ when
  a position has no resting protective stop. Falls back to % when no stop known.
  Also fixed the P/L column to compute from entry/last (was reading a None field).
- BUG: sync_orders dropped stop_price (kept only limit_price), so the synced
  snapshot showed stop=None on real stop orders — breaking the R bar and spark
  stop-line. Now captured. (The naked alarm was unaffected — it counts qty
  coverage, not the level.)

Verified live: equity curve renders the -1,886 day (94,124 -> 92,238); R bars
render (fall back to % until the loop re-syncs stop_price). Tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 21:29:07 -04:00
momentum devandClaude Opus 4.8 f0d1c3f106 Web dashboard: live armed-trigger proximity radar
The web Watcher ignored the armed-trigger book entirely — the single most
actionable view for an ORB strategy (which gapper is about to break) lived only
in the TUI, despite 25k module_tick(triggers) events in the store. Add
query_armed_triggers (bounded 15-min scan, cheap) -> /api/snapshots, and a top
"Armed Triggers" panel rendering each loaded gapper as a proximity bar: the fill
shows where the LIVE price sits between the stop (left) and the breakout trigger
(the marker line); crossing the marker = fired. Rows sort closest-to-fire on top,
color-coded by state (armed/fired/filled/weak/waiting), with gap%/rvol, the
distance-to-trigger %, and any news catalyst. Armed names within 1% of firing get
a highlighted track.

Verified live: 13 triggers render (NEOV fired +27%, SPRO armed -8%, CRVO -15.6%),
projections suite passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 21:13:00 -04:00
momentum devandClaude Opus 4.8 b79377f41e Web dashboard: portfolio risk strip + naked-position alarm
Closes the audit's remaining web-UI gaps. New query_risk_state projection bundles
the conditions that silently cost money this session: concentration vs the caps
(open/max, gross %), day P&L vs the daily-loss limit, broker-sync data freshness,
and any UNPROTECTED (naked) long. Wired into /api/snapshots.

dashboard.html: a pulsing red UNPROTECTED banner the instant a held long lacks
stop coverage (would have shown the moment APWC/NOWL went naked), plus a risk
strip — ok/warn/critical status dot, positions n/max, gross%/cap, day%/limit, and
a freshness chip (amber >30s, red >90s). Verified end-to-end: the API returns the
risk block (status=critical on today's 3 unprotected overnight positions) and the
page renders it.

Tests: projections suite passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 19:23:03 -04:00
momentum devandClaude Opus 4.8 007cd95c79 Baseline before Postgres migration (WS0)
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>
2026-06-15 23:39:29 -04:00