48 Commits
Author SHA1 Message Date
momentum devandClaude Fable 5 40e51c8dd5 feat: morning pick journal — prospective human/LLM forecast experiment
Tests whether a PROSPECTIVE forecaster (owner or local LLM) carries signal
the machine levers never found. Picks land premarket via dashboard panel or
research/llm_picker.py (Ollama); research/pick_scoring.py judges them under
a pre-registered bar (n>=40, >=30 sessions, mean >=+0.5%/pick net, median>0,
both halves positive). Picks after 9:30 ET stored but flagged invalid.
Shadow-only: human_picks is never read by the live loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:52:39 -05:00
momentum devandClaude Fable 5 72a9ecbd0b fix: reasoning panel flicker + stuck loading on live ticks
Every SSE snapshot re-render rebuilt the expanded panel's HTML back to
'loading…' and re-fetched — panels flickered per tick and never looked
settled. Rendered panels now cache per symbol: re-renders repaint from
cache instantly, background refresh at most once/min, single-flight
guard against fetch races. Watch panel and trigger panel share the
cache; toggleCriteria's duplicate direct fetch removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 20:36:31 -05:00
momentum devandClaude Fable 5 a6da02f809 feat: reasoning timeline on armed-trigger rows (click to expand)
The day's relevant names live in ARMED TRIGGERS — the reasoning panel now
opens right there: click a trigger row for the score/rvol timelines,
criteria lanes, and worded reasons, same renderer as the watch panel.
Caret + hover affordance; open panels survive snapshot re-renders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 20:11:02 -05:00
momentum devandClaude Fable 5 2be23673be feat: reasoning-over-time drill-down — score/rvol timelines + criteria lanes + worded reasons
Clicking a watch-state row now shows HOW the verdict evolved, not just the
latest flags:
- setup score across the session vs the 60% gate (SVG line)
- relative volume vs its 2.5x floor
- per-criterion pass/fail lanes over the whole session (the variance of
  each logical step at a glance)
- each criterion's latest reason in words (rvol 4.9 (min 2.5), gap 80.9%,
  price vs breakout ...) instead of bare pass/fail marks

Backend: query_symbol_criteria gains history[] (server-side strided to
~240 samples) + detail[]. The generic LIKE scan took 12s/click over weeks
of criteria rows; now a partial expression index on the extracted symbol
(idx_events_criteria_symbol, built at boot) + 24h window + SQL-side
stride = <1s. Latest-verdict fallback keeps stale symbols and historical
views working (covered by existing projection tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 19:37:35 -05:00
momentum devandClaude Fable 5 0c28cf921f feat: one-click source links on catalyst feed headlines
Every LLM catalyst read's headline now links to the raw article
(raw_news_items.raw_url joined by headline), styled as a distinct dotted
underline per NN/g citation guidance — any AI verdict traces to its
source in one click. Rows without a matching raw item degrade to plain
text. Join verified against prod: 0.29s for the full feed query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:40:49 -05:00
momentum devandClaude Fable 5 81bf1540ef feat: explicit + prefix on positive P&L values (CVD accessibility)
Gain/loss no longer rides on green/red alone — red-green color vision
deficiency affects ~8% of males; the minus sign already printed, now the
plus does too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:36:41 -05:00
momentum devandClaude Fable 5 da17be8969 feat: dashboard readability pass — type hierarchy, P&L masthead, trigger-row cleanup
- Two-texture typography: labels move to the UI sans (11px, brighter muted
  #93a0bd), values stay tabular mono — label/value pairs parse instead of
  dissolving into uniform mono soup
- TOTAL P&L is the one dominant instrument: 34px + green/red state wash and
  accent bar on the cell (CSS :has on the existing pos/neg classes, no JS)
- Equity tile: available funds as a small sub-line (fixes the two-line wrap
  that broke the strip height)
- Armed triggers: single-line ellipsized meta/levels (was a 3-line crumb
  wrap), 15px symbols, ARMED/WEAK state chips, subtle zebra, thinner bars,
  wider levels cell so entry/stop/now never clip
- Verified with headless before/after renders against live data; zero JS
  logic changes, node --check clean

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:19:16 -05:00
momentum devandClaude Fable 5 3d6bc36955 feat: Equity/Avail funds tile in dashboard P&L strip
Shows total_equity and buying_power (settled available cash) from the
latest account snapshot. On the Tradier live cash account these diverge
for days after a deposit or sale (T+1) — the bot sizes off equity but
buys need settled cash, so the gap explains rejected entries at a glance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 13:44:49 -05:00
momentum devandClaude Fable 5 8c62804808 feat: broker-aware factory for reconcile/journal/health tools
eod_reconcile, backfill_journal and the admin health block were hardwired
to AlpacaPaperClient — on a BROKER=tradier stack they would silently
reconcile/report against the wrong broker. make_trading_client() honors
BROKER env with no silent cross-broker fallback (missing Tradier keys =>
skip with reason, never fall back to Alpaca). Admin UI shows the active
broker label; JSON key stays 'alpaca' for compatibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 13:13:01 -05:00
momentum devandClaude Fable 5 52144241b4 fix: dashboard broker badge reflects BROKER env (PAPER · TRADIER etc.)
Badge text was hardcoded to ALPACA; _broker_block now reports the active
broker name and derives paper/live from the Tradier sandbox host when
BROKER=tradier, keeping the base-URL-truth rule from plan 007.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 12:38:37 -05:00
momentum devandClaude Fable 5 48d75816b6 feat: backtest page — live-taken vs not-taken split + per-trade live P&L column
/api/backtest now annotates each replay trade with the live loop's realized
P&L for the same (session, symbol) and returns a live_split summary (taken vs
not-taken n/sumR/avgR). Page grows two summary cells, a Live P&L column with
an amber ≠ marker when sim and live disagree on a trade's sign, and a footer
caveat: minute-bar granularity + adverse fills flip individual rows — trust
aggregates, not rows. Read-only annotation; cached replay result unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 13:58:24 -05:00
momentum devandClaude Fable 5 2e39b756ba dashboard: stale-session alarm banner + rvol_tod in armed triggers
The 07-21 morning outage (loop booted post-EOD on the 20th, survived
midnight, silently blocked every entry while signals went READY) had zero
UI surface. New query_loop_session projection parses the live session id's
embedded date; a red banner fires when it isn't today's — the exact day-2
poison signature — telling the operator to restart the loop. Armed
Triggers rows now also show rvol_tod (the arming signal since 5597250)
next to legacy rvol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 20:09:56 -05:00
momentum devandClaude Fable 5 f10a8033a1 chart: add volume histogram + Bollinger Bands + EXIT markers
Owner asks on the candlesticks:
- VOLUME: histogram series (bottom 20% overlay), coloured by candle direction.
- BOLLINGER BANDS: 20-period 2-sigma, computed client-side from closes; price
  riding the upper band = strong, break back inside = fading (momentum context).
- EXIT markers were NEVER wired — the fills feed carries only entries
  (order_submitted), and the loop fills brackets via position-diffing so no
  order_filled fires. Exits live in position_closed; now surfaced as square
  markers with reason + P&L (green win / red loss). Entry 'expected' (unfilled
  submit) markers labelled distinctly. Fixed a self-inflicted bug: queried the
  event store AFTER closing it — moved the position_closed fetch inside the
  open-store block. 11 chart tests green incl. new exit-marker test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:28:15 -05:00
momentum devandClaude Fable 5 5f04d5584a ui: surface hidden values + UI-REVIEW fixes 1 & 2
Owner: hover-only values were invisible. Now VISIBLE:
- Trades panel: Entry + Exit$ columns (projection now carries entry_price/
  exit_price; were hidden entirely).
- Armed triggers: entry/stop/now levels shown inline (were tooltip-only).
UI-REVIEW fix 1 (color): collapsed 3 greens->1 (#34d17e) and dup reds->
canonical #ff5a60 across CSS AND canvas markers (one --green/--red now).
UI-REVIEW fix 2 (a11y): :focus-visible outlines on all controls + aria-labels
on the money-path buttons (approve/reject/exit/flatten) — keyboard operators
were flying blind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:54:21 -05:00
momentum devandClaude Fable 5 053c927cf7 backtest: explain the regime-gated empty window + toggle to view raw-criteria trades
Owner saw a blank /backtest — it was CORRECT (the regime gate sat out every
recent session) but unexplained. Now: ?regime=0|1 override on /api/backtest,
an 'apply regime gate (as deployed)' checkbox, and an empty-state message
saying WHY there are no trades and what to click. Gated view = what the bot
would do; ungated = the raw simple criteria.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 22:40:03 -05:00
momentum devandClaude Fable 5 d50115dea8 backtest: 1d window button (owner request)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:09:06 -05:00
momentum devandClaude Fable 5 8ac222a4ca research-tooling: /backtest replay dashboard — theoretical trades under current settings
Adds a read-only /backtest page showing how the bot WOULD have traded past
sessions under the CURRENTLY configured live settings: theoretical ORB fires
from setup_events, replayed through the shared exit engine (simulate_exit +
ExitConfig.from_env(), same adverse-fill accounting the exit-experiment
harness uses), screened through the shared population gate and (optionally)
the live regime gate. No new simulation math — replay.replay_window() is pure
assembly of existing pieces (reconstruct_fires_from_setups, _fire_in_population,
_load_regime_favorable, simulate_exit); api/main.py adds the /backtest and
/api/backtest routes plus a dedicated-lock 10-min cache so a slow replay never
blocks the live dashboard's SSE stream. Also extends /api/chart with an
optional date= anchor so the trade drill-in chart modal can reach sessions
older than the existing 72h-from-now window, and adds a one-line BACKTEST nav
link to the live dashboard header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 08:42:43 -05:00
momentum devandClaude Fable 5 dd81dbbda1 operator-plane: live decision trace — gate receipts feed + chart reject markers (plan 014)
Surfaces the entry_gate_evaluated decision receipts (plan 005) that were
write-only until now: a Decision Trace panel (per-gate value->threshold
chips, symbol filter, lazy-load + SSE refresh) and collapsed reject markers
merged into the plan-013 symbol chart's existing markers primitive.

- storage/projections.py: query_gate_receipts (case-insensitive symbol
  filter, newest-first, same _hist_window idiom as query_fills_feed)
- api/main.py: GET /api/decisions (clamped limit<=500, date-scoped);
  DashboardState.decisions() adds a server-computed epoch per row; factored
  chart()'s inline to_epoch into a shared _to_epoch_et helper
- api/static/dashboard.html: Decision Trace panel + refreshChart now also
  fetches /api/decisions and merges collapsed reject markers (same-reason
  bursts within 5min -> one marker) into the single setMarkers call

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 07:36:31 -05:00
momentum dev 709e7a4344 plan 013 fixup: stop leaking a markers primitive per SSE refresh
createSeriesMarkers(series, markers) attaches a NEW primitive to the series
on every call — refreshChart() called it on every 5s SSE-driven refresh, so
a modal left open accumulated one primitive per refresh (identical markers
overdraw so it looked fine, but it's an unbounded render leak on long-open
modals). v5 pattern: create the markers primitive once in openChart, mutate
it via markersApi.setMarkers(markers) on refresh. chart.remove() in
closeChart already tears the primitive down with the chart.
2026-07-17 06:50:41 -05:00
momentum dev b4ad3d3a66 operator-plane: symbol drill-in chart — 24h candles + live trade markers (plan 013)
Click any symbol cell to open a 24h candlestick chart (vendored TradingView
Lightweight Charts v5.2, Apache-2.0) with buy/sell arrows at the bot's fills,
muted arrows for submitted-unfilled orders, and entry/stop/target price
lines. New read-only /api/chart endpoint (DashboardState.chart) converts
naive-ET minute_bars/event timestamps to UTC epoch seconds; query_fills_feed
gained an optional symbol filter. Chart refreshes off the existing SSE
snapshot signal (5s throttle, full replace) and tears down on close. Also
backfills order_filled markers' missing `side` (OrderFilledEvent's schema
has no side field) via an order_id join to the sibling order_submitted row.

Merged plan 012 (advisor/012-dashboard-order-limits) first — this plan
depends on its stopWorkingBy/targetBy/entryLimitBy maps.
2026-07-17 06:46:52 -05:00
momentum devandClaude Fable 5 1f3ddcabe4 plan 012 fixup: Positions Stop column must use working-filtered stop, not legacy stopBy
sync.py fetches status="all" so the legacy stopBy map (kept for the
R-bar/spark stop line, unchanged) can pick up a cancelled stop leg.
Add stopWorkingBy, filtered to the same WORKING statuses as
targetBy/entryLimitBy, and point the new Positions Stop column at it
so a cancelled leg never renders as live protection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 06:11:40 -05:00
momentum devandClaude Fable 5 b3495187c3 plan 012: show working limit/stop/target prices, drop dead order-id + Mode columns
Positions table gains Stop/Target columns from resting bracket legs
(targetBy/entryLimitBy maps, filtered to working order status). Order
Lifecycle splits the generic Price column into Type/Limit/Stop/Fill via
a client-side join to the broker order snapshot (client_order_id ==
the internal order_id we send Alpaca) since the lifecycle events
themselves never captured limit/stop/type; the truncated order-id
column is dropped in favor of a title attribute on the Symbol cell.
Approval queue drops the always-"auto" Mode column and shows explicit
entry/stop prices in the risk chip instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 06:09:48 -05:00
momentum devandClaude Opus 4.8 092d6072e8 Regime gate (plan 011): deploy-wire the shadow pipeline + on-demand admin trigger + prod backtest
The session-level metalabel P(our ORB profits today) — the one lever that
survives out-of-sample. Backtested on 266 prod sessions (walk-forward):

  trade-always (trained subset): -0.111R/session, 36% hit
  meta_v1 GATED (trades best ~32% of days): -0.004R/session, 48% hit
  delta +0.107R in-sample, +0.128R OOS (recent 80 sessions), improves across folds

It does NOT make the bot profitable — it takes a bleeding strategy to roughly
flat by sitting out the worst ~2/3 of sessions. threshold_v1 rule gates too
little to matter; meta_v1 (walk-forward GBM) is the signal. Leaky full-fit
(+0.61R) >> walk-forward confirms the discipline was load-bearing; the OOS
holding is the evidence the +0.078R is real, not look-ahead.

Changes:
- research/regime.py: batch minute-bar load per session (64k per-fire round-trips
  -> ~266 queries; fixed a >5min build to ~3min) + incremental label pass (skip
  already-labeled sessions; --rebuild forces full). Nightly cost now ~one new day.
- docker-compose.yml + truenas-compose.yml: scheduler runs build-features + score
  (both models) before `experiments run`, nightly. ALL-SHADOW — does NOT gate live
  trading (that stays owner-gated Step 5).
- admin_api.py + admin.html: POST /api/regime/run (detached) + GET /api/regime/status
  + a Run/Status button — the panel had no way to trigger this.
- plans/011 + docs/research/regime_notes.md (6-book distillation) + plans 008/009 docs.

Prod state (shadow, rollback = DROP regime_features/regime_scores + DELETE
experiments WHERE id LIKE 'regime%'): 266 feature rows, 532 scores, 2 experiments
registered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:05:34 -04:00
momentum devandClaude Fable 5 e2b3c1ceae operator-plane: headless smoke + effective-config panel + ledger + unpin ruff
Step 6: tests/smoke_dashboard_headless.py renders the real page in a headless
browser and fails on any JS console/pageerror (standalone script, not
pytest-collected; CI non-blocking first). admin_api gains GET /api/config/effective
(per-key masked value + source: overrides.env vs compose, missing .env.example
keys flagged as drift) + an admin.html table (values XSS-escaped). dashboard.html
gains the Experiment Ledger panel reading the plan-006 verdicts from the snapshot
(empty-state until the tables exist). Removes the last CI ruff per-file pin
(api/main.py:F821) now that os/hmac are imported; scoped ruff is green unpinned.
Marks plan 007 DONE in plans/README.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:36:00 -04:00
momentum devandClaude Fable 5 cb206e45cb operator-plane: truthful LIVE badge + kill-switch UI + action gating
Badge now derives PAPER/LIVE from broker truth (base-URL host) instead of the
schwab_live literal that read PAPER while go-live swapped only ALPACA_BASE_URL;
live flips body.live-mode (top shimmer -> solid red) with a tooltip of host +
account. Adds the header FLATTEN ALL (two-step arm/confirm, 5s auto-disarm) and
PAUSE ENTRIES toggle (+ amber banner), per-position EXIT buttons (two-step), and
an operator-token field sent as X-Dashboard-Token on every POST (also fixes
approve/reject when a token is set). Action controls gate on
has_execution || command_bus; kill-switch controls on command_bus. No keyboard
shortcut for any destructive action. New styling is :root-token-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:19:51 -04:00
momentum devandClaude Opus 4.8 f0fb91a069 Admin panel (:8767): config, profiles, service control, paper/live switch
New admin service — LAN-only, token-authed (ADMIN_TOKEN), card-based UI
matching the owner's preferred dashboard style. Capabilities:
- health: docker containers via /var/run/docker.sock (stdlib unix-socket
  HTTP client), DB heartbeat, Alpaca account/clock, Ollama, feed
- config: whitelisted strategy knobs (TRADING_/TRIGGER_/WATCHER_/...)
  edited via data/overrides.env, which every entrypoint now loads with
  override=True at boot (runtime/env_overrides.py) — so changes apply on
  container restart without touching the deployment YAML; secrets are
  presence-only and rejected server-side if edited
- profiles: named knob presets (seeded: validated-core, conservative,
  squeeze-hunter) applied in one click
- restart buttons per container; paper/live switch guarded behind
  ALPACA_LIVE_* keys existing
Verified live in docker: auth 401s, status/config/profiles/write paths,
secret-edit rejection, page serves. Added to both compose files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:37:18 -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 6a6b38326b Streamline the watch CLI board + dashboard for glance-ability
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>
2026-06-29 19:01:11 -04:00
momentum devandClaude Opus 4.8 fdb2841ae4 Resilient DB host + collapsible dashboard (ops: keep the loop + website up)
DB self-heal: the LAN Postgres is reached from WSL via EITHER the 127.0.0.1 netsh
portproxy OR a direct route to 192.168.1.5, and which one works FLIPS across WSL/host
reboots and network-mode changes — the recurring crash-loop landmine. storage/db_pg.py
_connect now tries the configured LAN host and then the alternate, so the loop boots
regardless of the current tunnel state ("connection refused" is instant, so it's cheap).

Dashboard UI: api/static/dashboard.html panels are now collapsible <details> (native,
no JS for the toggle; inner ids preserved so the live rendering JS is untouched). The
live/actionable panels (Armed Triggers, Open Positions, Approval Queue, Ready Signals,
Account) start OPEN and lead the layout; the big/historical ones (Catalyst Engine,
Daily Performance, Trades, Watch States, Activity Feed, Order Lifecycle, All-Time Score)
start COLLAPSED so the page is compact and scannable. Open/closed state persists per
panel in localStorage; an EXPAND/COLLAPSE-ALL header button toggles everything.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:41:48 -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 895bbb0a82 Emit position_closed events (trade journal) + make dashboard view-only
Trade journal was blind: exits emit no event, so closed_trades / win-rate /
avg-R / the trade list all read 0 even on days with booked round-trips.

Now AlpacaPaperSync.sync_all reconciles closes by diffing the positions
snapshot: a symbol that was open and is now gone/flat has closed. This one
signal catches every exit path — bracket STOP/TARGET legs fill broker-side, EOD
flatten closes via market orders — and reads the exit fill (price + reason) and
the bracket stop level from the orders snapshot, emitting a PositionClosedEvent
with entry/exit/stop/side/realized so query_session_pnl can compute the
R-multiple. Headline realized $ stays broker-authoritative (the broker-delta
branch is unchanged); these events feed the wins/losses/avg-R/trade-list journal.
Robustness: the close baseline seeds from the last PERSISTED snapshot so a close
during downtime is still journaled once; reconcile is wrapped so it can never
break sync; PositionClosedEvent gains optional entry_price/stop_loss_price/side/
quantity fields (older emitters/tests unaffected).

Dashboard is view-only: removed the manual Exit button + exitPos handler from
Open Positions (kept the approval-queue approve/reject — that's the intended
human-in-the-loop control).

Tests: stop->loss, target->win, still-open->no-emit, no-fill->last-mark
fallback, restart-seed-from-persisted. 49 unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:30:35 -04:00
momentum devandClaude Opus 4.8 59dc00fa25 Dashboard UI: catalyst on signals, feed filter, P&L bars, mobile
Four operator-requested enhancements (all dashboard.html, headless-verified, no
JS errors):
- Ready Signals: new Catalyst column — the LLM read (dot + type) per observed
  symbol, not just armed triggers; "—" when there's no recent read.
- Catalyst Engine chip is now clickable to filter the feed to dilutive/veto
  reads only (count shows "N / total"); click again to clear. Verified 40 -> 15
  with every filtered row dilutive.
- Open Positions P/L cell now shows $ + % + a colored magnitude bar (maxes at
  ±10%) so underwater/up names read at a glance — pairs with the P&L fixes.
- Mobile: catalyst-feed rows wrap (headline + rationale) instead of truncating,
  the conviction column drops, the engine chip goes full-width, and buttons get
  38px tap targets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 21:04:01 -04:00
momentum devandClaude Opus 4.8 4153f027f0 Dashboard: first-paint loading overlay
The snapshot does real Postgres work, so the first load left a blank board that
read as broken. Added a full-screen overlay (on-brand spinner + "connecting to
the control room…" + what it's fetching) that:
- shows on first paint, dismissed the instant real data renders (SSE, poll, or
  historical fetch);
- after 5s of waiting, softens the sub-text to "taking a moment — still
  fetching…" so a slow snapshot reads as working, not frozen;
- on a failed first fetch, flips to a red "couldn't reach the control room —
  retrying…" state (stops the spinner) instead of hanging silently.

Verified headless: overlay visible at first paint, display:none once pnl/feed
populate (no JS errors); 500 path shows the error state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 20:38:54 -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
momentum devandClaude Opus 4.8 8cee002c09 Merge PR #2 (Ollama news/catalyst enrichment) onto master + fix review blockers
Integrates the local-LLM enrichment ADDITIVELY on top of all current work, with
every Ollama feature OFF by default — with the flags off the system behaves
exactly as before (199 unit tests pass; a full --once pass runs clean with no
Ollama). Fixes the three issues the adversarial review flagged:

- BLOCKER: keep the Alpaca News ingestion. The PR predated it; the run_live_paper
  conflict is resolved to keep BOTH step_news (fetch -> raw_news_items) AND the
  enrichment. They're complementary: ingestion feeds enrichment authoritative-
  ticker headlines instead of thin RSS.
- HIGH-1: enrichment runs on its OWN daemon thread with its OWN DB connection,
  not the scheduler thread — a slow/hung Ollama can no longer starve exits /
  guard / eod-flatten, and it no longer shares the non-thread-safe research_con.
- HIGH-2: per-headline writes are all-or-nothing — on any ticker store failure
  the partial rows are deleted so the headline fully retries (no stranded
  tickers) and enriched is counted only on full success (no false count).

Phase-2 automation (catalyst score blend, dilution veto, arming-rank multiplier)
remains gated OFF pending the medium fixes (bearish-catalyst boost, prompt-
injection hardening, veto cross-thread read) noted in the review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:54:07 -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 ea799f975c Dashboard: responsive layout for phones
Panels already stacked at <=1040px, but phones still broke: dense tables
overflowed the page, the risk/P&L flex strips didn't wrap, and the armed-trigger
rows had fixed pixel widths. Added a <=640px breakpoint:
- header wraps; desktop-only Sound/Keys buttons hidden; connection chip right-aligned
- risk strip wraps; P&L becomes a 2-up grid with the hero Total P&L, day-curve full row
- tighter main padding; panel hint text hidden to save width
- wide tables get overflow-x:auto + nowrap cells -> swipe horizontally inside the
  panel instead of stretching the page
- armed-trigger radar drops the meta/catalyst columns, keeps symbol + bar + dist

Verified headless at 390px: zero horizontal page overflow (scrollWidth==clientWidth),
tables scroll internally (665px in a 368px panel), all panels render, no JS errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:22:13 -04: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 c14a89585a Dashboard: visual refresh — premium trading-terminal look
Pure CSS (every id/class the JS renders is preserved; verified headless, no JS
errors, all panels intact):
- Depth: radial-gradient background, lifted panels (soft shadow + subtle top
  highlight + hover), glassy blurred sticky header.
- Hierarchy: hero Total P&L (27px) with green/red glow + tabular numerals;
  accent gradient rule before every panel title; thin live shimmer line at the
  top edge; status dots get a soft glow.
- Polish: refined palette, softer/ bordered pills, gradient action buttons with
  press feedback, custom scrollbars, panel load-in animation, hover affordances.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 08:47:24 -04:00
momentum devandClaude Opus 4.8 eb380ee804 Dashboard: fix fatal 'rs' redeclaration that blanked the whole page
The risk-strip code declared `const rs = $("risk-status")` in render(), colliding
with the existing `const rs = d.ready_signals` later in the same scope. A
duplicate lexical declaration is a fatal parse error — the ENTIRE <script> never
executed, so render/populateDates/setView were undefined, SSE never connected,
and every field showed '—' (this, not only the API timeout, was why it "didn't
look like it's working"). Renamed the risk element to `rsEl`.

Verified in a headless browser: no JS errors; the date <select> populates with
LIVE + the last sessions, switches to a historical day and back, both views
render (armed radar, watch list, positions, fills, order lifecycle).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 22:23:33 -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 fbe2eff3d4 Dashboard: fix freshness-chip timezone bug + sync-aware thresholds
Live verification caught it: the data-freshness chip read ~14,477s (RED) on data
that was actually 1s fresh — exactly the EDT offset. Event timestamps are stored
NAIVE LOCAL (datetime.now()), but query_risk_state compared them against UTC now.
Compare naive-local to naive-local instead (tz-aware timestamps still use UTC).
Verified live: chip now reads ~78s. Also align the amber/red thresholds to the
~60s broker-sync cadence (amber >90s, red >180s) so a healthy feed reads green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 20:21:54 -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