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>
7.1 KiB
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)
- Labeler: add
build_ignitiontoresearch/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/liftfiltered by version give runner-vs-trap separation per signal. - eod_replay: add
score_shadow_signals— after the close, read the day's actualshadow_signalevents, walkminute_barsstrictly 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_1rrate materially above thefailed_breakouttrap 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).