docs: momentum-ignition setup scope (shadow-only)

Scope for a SECOND, shadow-only lane to catch PLSM-class vertical squeezes the
pullback-ORB scorer is structurally blind to (PLSM was evaluated 1895x and
blocked ~30% every time — a vertical gives no pullback). Produced by a read-only
4-agent mapping workflow; no code changed.

Key findings: detect on blue-sky fresh-HOD + price velocity + ABSOLUTE volume
burst (NOT RVOL, which zeros out on thin IPOs; NOT float, which is NULL). Emit a
new ShadowSignalEvent that the execution snapshot (query_ready_signals_snapshot,
filters event_type=signal_ready) can never read -> structurally cannot trade.
Score nightly by reusing the labeler + eod_replay forward-outcome machinery.
Pre-register the promotion bar (runner-vs-trap separation, distinct-runner n)
before shadowing. Phased P0 detector -> P1 dark emission -> P2 nightly scoring ->
P3 validation -> P4 tiny live lever only after separation is proven. Strictly
additive; never touches the ORB book or anti-chase guards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-06-24 21:26:45 -04:00
1 parent 813c5dce88
commit 7ebb503790
1 file changed
+81
+81
View File
@@ -0,0 +1,81 @@
# Momentum-ignition setup — shadow scope
_Scope produced 2026-06-24 by a read-only 4-agent mapping workflow (wf_55582b6d). No code changed.
A SECOND, shadow-only lane to catch PLSM-class vertical squeezes the pullback-ORB scorer is blind to._
## Why
The live setup is `impulse → pullback → breakout` (`strategy/evaluation/structure.py` `classify_setup`).
A true vertical squeeze gives **no pullback and no formed opening range**, so `classify_setup` returns
NONE and the scorer blocks it — verified: PLSM was evaluated **1,895× on 2026-06-24 and blocked ~30%
every time**. This lane is the missing discipline: ride the vertical, not the base.
## What it detects
An intraday vertical ignition with no consolidation: a fresh **high-of-day above the prior all-time
high (blue sky)**, **accelerating price velocity** (consecutive green bars + an X% move over K minutes),
and an **absolute volume burst**. It deliberately catches the pattern that makes `classify_setup`
return NONE.
## Detection rules — all computable TODAY (binary, no fitting at this n)
| signal | rule | source |
|---|---|---|
| **Blue-sky fresh-HOD** | `current.high > prior_ATH` (max `daily_bars.high` before today) AND this bar is the running session-high HOD. *Don't* reuse `detect_hod_break` (needs consolidation a vertical never gives). | `daily_bars.high` + `minute_bars.high` |
| **Price velocity** | `pct_move(close[t]/close[t-N]-1) ≥ 0.08` over N=5–10 RTH min, AND ≥4 of last 5 bars green, AND higher-highs. (PLSM 12:02–12:16: 10/14 green, +13.9% in 14 min.) | `minute_bars` OHLC, pure arithmetic |
| **Absolute volume burst** | `last_bar_vol / mean(prior K=5 bars) ≥ 3.0` AND cumulative session volume ≥ 100k shares (or a $-volume floor). **Absolute floors, NOT RVOL** — `scan_gappers` RVOL zeros out on thin IPOs (adv=0), and `rolling_avg_volume_20d` is NULL. | `minute_bars.volume` |
| catalyst *(tag only)* | attach max-conviction bullish row (`sentiment>0`, `is_dilutive=False`); never a gate (Ollama gated off / can be down). PLSM "Soaring" → 0.9 conviction. | `news_catalyst_cache` |
| VWAP *(tag only)* | log `above_vwap` + `dist_from_vwap` as grading context (the one validated ~1.5× signal). | `compute_key_levels.vwap` |
| float *(placeholder)* | **DO NOT GATE** — `symbols` table is empty, `float_shares` NULL. Log `None` so a later float backfill can re-grade historical shadows. | `symbols.float_shares` (empty) |
## Hook point
A new zero-arg closure **`step_shadow_ignition()`** in `run_live_paper.py` beside `step_watch` (~L528),
registered via `scheduler.add('shadow_ignition', …, enabled=_flag('SHADOW_IGNITION_ENABLED','0'))` —
**ships dark**. Pure-read against `research_con` (single psycopg2 conn — not thread-safe), emit-only,
never references `rt['execution']`. Reuses the watcher provider's `get_candidates`/`get_bars` (same
symbols/bars) but with its OWN wider caps (`SHADOW_PRICE_MAX≈100`, no gap ceiling) so it validates the
very names live config excludes. Detection is a new pure `detect_momentum_ignition(bars)` in
`strategy/evaluation/ignition.py` — **NOT** wired into `classify_setup`/`evaluate_setup` (the pullback
criteria would reject it and `STRATEGY_SETUPS=opening_range_break` would filter it).
## Shadow mechanism — structurally cannot trade
New `EventType.SHADOW_SIGNAL` + `ShadowSignalEvent` (symbol, trigger, would-be stop, signal_values
dict, session_date). It is **invisible to execution**: `query_ready_signals_snapshot` filters strictly
on `event_type='signal_ready'`, the only thing `TradingExecutionService.tick` reads — a `shadow_signal`
is never seen, and the lane never calls `submit_breakout_now`/`request_approvals`. (Precedent: the VWAP
gate already shadow-logs via `RiskRuleTriggeredEvent action_taken=shadow_logged`.) Safety: wrap the
step in try/except (a shadow failure can't kill the loop), once-per-session per-symbol debounce (so one
squeeze logs once, not 1,895×), never emit `signal_ready`, never set a symbol "ready".
## Measurement — reuse existing forward-outcome machinery (zero new metric code)
1. **Labeler**: add `build_ignition` to `research/labeler.py` (`setup_name=momentum_ignition`,
`setup_version=ign_v1`) reusing the identical label block (`reached_1r/2r_before_minus_1r`,
`max_upside_5/15/60m`, `time_to_max`, `failed_breakout_flag`=the fizzle trap, `held_vwap`). `report`
/`lift` filtered by version give runner-vs-trap separation per signal.
2. **eod_replay**: add `score_shadow_signals` — after the close, read the day's actual `shadow_signal`
events, walk `minute_bars` strictly after each, classify **runner (+1R) / fizzle / halt-down-trap**
(the FCUV mode: a forward bar gapping far below the prior low, or R ≤ −1 within minutes). Run nightly.
## Promotion bar — PRE-REGISTER before shadowing (so the bar can't move to fit noise)
- `reached_1r` rate **materially above** the `failed_breakout` trap rate, AND
- halt-down-trap rate **below a ceiling**, AND
- **distinct runner instances** above the thin-n floor (≥30, or rate×count ≥8) — counted in distinct
runners, NOT calendar days (the ~17–19 days are autocorrelated, one regime).
- Decision hangs on the **eod_replay as-fired** numbers (what a live lever would actually capture).
## Build phases
| phase | blast | what |
|---|---|---|
| **P0** | none | `detect_momentum_ignition(bars)` pure + unit-tested (synthetic bars + the PLSM 12:02–12:16 window). No wiring. |
| **P1** | low | `SHADOW_SIGNAL`/`ShadowSignalEvent` + `step_shadow_ignition` (default OFF, own caps, debounce, try/except). Test asserts it NEVER emits `signal_ready` / never calls execution. |
| **P2** | low | `build_ignition` (labeler) + `score_shadow_signals` (eod_replay), nightly. DB-unreachable tolerant; tests use `:memory:`/`MOMENTUM_PG_SCHEMA`. |
| **P3** | none | Pre-register the promotion thresholds; shadow N weeks; accumulate. Zero capital, zero trading-path edits. |
| **P4** | high | *Only after P3 separation is proven* — a tiny live lever behind `require_ignition_confirmed` (default OFF, mirrors `require_above_vwap`), minimally sized, bounded by ALL shipped catastrophe controls (enforced stops, anti-chase, BE@+5%, halt guard). The ORB book + anti-chase guards untouched — strictly additive. |
## Open questions for you
- **Stop definition** for the R denominator: last-higher-low vs bar-low vs −8%? Changes every reached_1r/trap rate — pick ONE and pre-register. (Maps lean last-higher-low.)
- **Shadow gap ceiling**: drop entirely or cap ~80%? Dropping surfaces the most extreme/trap-prone names (fine for shadow).
- **Catalyst tag** needs Ollama running that day — force-enable it for the shadow grading pass so news-backed vs not can be measured?
- **Distinct-runner-n ≥30** may be unrealistic given runner sparsity — a Bayesian interval at low n instead of a point rate?
## Hard constraints (non-negotiable)
Shadow-only until forward data proves it · never touches the ORB book or anti-chase guards · ships dark
(`SHADOW_IGNITION_ENABLED=0`) · binary rules, no fitting · do NOT gate on RVOL or float (both broken for
exactly these names) · tests use `:memory:`/`MOMENTUM_PG_SCHEMA` (never the live DB).