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>
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>
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>
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>
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>
- 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>
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>
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>
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>
/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>
Version: CI now tags images YYYY.MM.DD-<shortsha> alongside :latest/:sha
and passes GIT_SHA/BUILD_TAG build-args; the running app reports them via
dashboard /api/health and admin /api/status (TrueNAS custom apps only show
'custom/1.0.0', so the app self-reports what it actually runs).
Config parity: every scheduler-run script (nightly_tune, eod_replay,
premarket_eval, regime, market_context, experiments cli) now loads
config_overrides at startup like the live services do — a bare env would
replay under DEFAULT exits (the documented 4.5x-baseline landmine).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stored minute-bar history is 273 sessions (~13 months); the 45-session
clamp blocked months-scale ranges now that the nightly labeler keeps
setup_events current. Requests stay serialized behind _backtest_lock; a
full-history run takes minutes by operator choice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prior commit imported query_loop_session but a botched idempotency
guard skipped inserting the snapshot field (the import string itself
matched the 'already present' check) — ruff unused-import failed CI and
the stale-session banner had no data. Field added; banner live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
The self-heal commit (a00f967) inserted the _research method INSIDE __init__ at
method-body indent, truncating it so _change_signal/_backtest_lock/_backtest_
cache never got set -> AttributeError on backtest. Moved _research to a sibling
method; __init__ now sets all attrs. Two chart/sparkline tests updated: a None
research_con now REOPENS lazily, so 'no research db attached' is reached only
when the reopen also fails (they monkeypatch open to fail). 22 api tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The charts weren't broken — the data pipe was empty. dashboard_api opened the
research connection ONCE at boot; if the DB was unreachable then (container
started before Postgres, or a blip) it stayed None forever, so /api/chart and
/api/bars returned 'no research db attached' and every candlestick + sparkline
rendered blank. Live-verified: GET /api/chart returned bars:[] reason:'no
research db attached' on the deployed dashboard. Now DashboardState._research()
opens the connection LAZILY and reopens if None — same self-heal as the
event-store emit fix. 2 tests; chart/backtest endpoint tests still green.
No library change: Lightweight Charts v5.2 is fine, it just had nothing to draw.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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.
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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
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>
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>
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>