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>