Extends the local-LLM layer from news to TRADES, reusing the proven enrichment
pattern (structured JSON, Postgres cache, off-hot-path scheduler, graceful
degradation). Advisory only — never gates a trade.
strategy/evaluation/trade_analysis.py: four analyses keyed by analysis_type —
* armed — does a loaded setup still make sense to pursue? (pursue/avoid/
monitor + confidence + concerns), weighing numbers + catalyst
* weak — why a too-soft setup failed and whether a thesis remains
* postmortem — closed-trade lesson (uses r_multiple / exit_reason)
* eod — once-a-day session narrative
Schema-constrained decoding (ANALYSIS_SCHEMA), tolerant _parse, and a
context_hash so an unchanged board isn't re-sent to the GPU. run_* helpers
upsert into ai_trade_analysis_cache; trade_analysis_map / query_trade_analysis
read it back. NEVER raise.
Wiring:
- ai_trade_analysis_cache table (storage/pg_schema.sql)
- OllamaConfig.trade_analysis_{enabled,interval_seconds,batch_limit} + env
- run_live_paper step_analyze on the Scheduler, gated by ollama.enabled AND
TRADE_ANALYSIS_ENABLED (ships OFF); reads the in-memory trigger book + day P&L
- query_trade_analysis projection; api snapshots() attaches the verdict to armed
triggers + approval rows and exposes trade_analysis
- dashboard: per-name AI verdict chip (pursue=green/avoid=red/monitor=amber) +
an "AI session note" panel
- .env.example documents the toggles
Tests: parse/enum/clamp, prompt builders, persistence + context_hash dedupe,
closed-trade + EOD paths, config from_env, and the dashboard projection. 230 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M5HTMkaadGDnAPtXe2hWwg
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
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>
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>
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>
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>
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>
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>
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>