ADR-025: Price Action Sentinel – Vertical Line Alerts First, Committee Second
Date: 2026-07-03 Status: Accepted (default-on)
Context
On 2026-07-02, gold experienced a vertical 10-minute rally (ADP miss) with no alerts. The window fix (see ADR-006 2026-07-03 revision note) plugged the gap for "news-driven major events," but exposed a structural deficiency: event_watch only alerts on news narratives. Pure price events (flash crashes, short squeezes, liquidity-driven moves without headlines) are naturally invisible to the 30-minute news cycle; a spike can rise and fall completely between two scans.
Data reality constraint: yfinance futures/stock minute bars have a ~10-15 minute delay (measured GC=F 5m delay at 14 minutes). The alert will inevitably be 15-25 minutes behind the market move – this sentinel is not, and cannot be, a front-running tool (aligning with ADR-023's honest positioning). Its value is:
- The system speaks before the user asks: Before/while the user sees the vertical line, an email already contains "what happened + the most recent verdict anchor" – FOMO interception shifts from "waiting for the user to ask" to "proactively showing up."
- Downside linkage to real money decisions: DCA batch trigger prices and crash defense sentinels all rely on "someone waking me up when the drop is significant enough." The upside only manages sentiment; the downside manages the wallet.
Decision
Add jobs/price_sentinel.py (cron default */5 0-2,8-23 * * *, same window as event_watch, every 5 minutes), reusing the existing event pipeline without creating a new alert channel:
- Perception (Zero LLM): yfinance 5m close price → 10-minute percentage change; daily ATR% (
utils.market_metrics.compute_metrics) for normalization. Trigger condition:|move_10m| ≥ sentinel_atr_mult × daily ATR%(default 0.8 – an 80% daily move in 10 minutes constitutes a "vertical line"; the 2026-07-02 event was 1.5×). Falls back to an absolute floor of 1.0% if ATR is missing. - Sequencing Contract (User's original requirement: "Alert me first, then run the committee"): Create an
event_type=price_actionevent in EventStore → Send alert email first (send_event_alert, with the claim embedding the most recent verdict anchor + current price + ×ATR) → ThenPOST /api/committee/run. A committee trigger failure only logs a warning, it must never affect the already-sent alert; an email failure also does not block the committee trigger. - Fatigue Prevention: Same symbol, same direction is silenced for
sentinel_cooldown_min(default 120 minutes); a sharp rise followed by a sharp fall are different directions and can each be reported. Cooldown state is persisted inmemory/.state/price_sentinel_cooldowns.json. - Market Close Protection: If the last 5m bar is >25 minutes old, it is considered market close or data stall, and the check is skipped – do not mistake yesterday's tail for today's anomaly.
Relationship with event_watch
| event_watch (News) | price_sentinel (Price) | |
|---|---|---|
| Sensor | RSS/DDGS fetch + flash LLM normalization | yfinance 5m + pure arithmetic (Zero LLM) |
| Trigger Condition | severity≥mid ∧ hits holdings ∧ stance≠neutral | ATR normalized threshold + cooldown |
| Alert Sequence | Get committee task_id first, then send email (email includes link, does not wait for verdict) | Email first, then trigger (alert unconditional priority) |
| Event Persistence | Same EventStore | Same EventStore (price_action type, no embedding) |
| Downstream | Email + committee rerun | Same (reuses the same two paths) |
Consequences
- Zero cost for empty scans (no email, no LLM call, no committee trigger when there is no new event)
- Committee reruns consume DeepSeek tokens – frequency is controlled by cooldown + ATR threshold, expected 0-2 times per symbol per day (0 times on most days)
price_actionevents enter the RAG corpus (no embedding, only participate in dimension filtering, not vector recall)- Thresholds/cooldowns/windows/switches are all configurable via the config whitelist (ADR-017), and the GUI will automatically reflect them