GO_LIVE flags `spread_pct` as a not-yet-real field. The reason it was null is
structural: OHLCV bars carry no NBBO, so `minute_bars.spread_pct` can never be
populated from the bars endpoint. The execution-relevant figure is the spread at
the moment a setup is evaluated — a wide book quietly eats R on the thin sub-$20
names this system trades.
This makes that metric real, shadow-first (measure before gating), mirroring how
the catalyst score was introduced:
- `strategy/evaluation/liquidity.py`: pure `compute_spread_pct(bid, ask)` →
(ask-bid)/mid, or None for missing/crossed/non-positive quotes (None never
collapses to a real 0.0). Unit-tested.
- `AlpacaPaperClient.get_latest_quotes`: batched latest-NBBO fetch, mirrors
`get_latest_trades`.
- `Watcher`: optional injected `spread_provider` (same shape as
`catalyst_score_provider`); default None == prior behavior byte-for-byte.
Computed spread is logged onto the criteria_evaluated and signal_ready events.
Observe-only — it does NOT gate ready/blocked.
- `run_live_paper`: TTL-cached spread provider off the client; degrades to None
on any fault, SPREAD_PCT_ENABLED=0 to silence.
The dead `scanner.spread_pct_max` knob is intentionally left for a later
promotion, once the logged data shows spread separates outcomes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019psgRHdtTXz6TCnnqPiqGw