Files
momentum-system/tests/unit/test_liquid_breakout.py
T
momentum devandClaude Opus 4.8 a25e918240 Liquid intraday-RVOL breakout detector (prototype, shadow-only)
The 2026-07-01 forensic + the source trader's day-14 notes point at a thesis-scope gap:
the day's only clean, held, TRADEABLE +2R moves (MSTR, SSPC) were LIQUID names that ignited
intraday with ~zero premarket gap -- invisible to the premarket-gapper universe -- while the
cheap "held trenders" were untradeable dust and the liquid gappers the bot chased whipsawed.

strategy/evaluation/liquid_breakout.py::detect_liquid_intraday_breakout -- a pure detector that
makes LIQUIDITY (cumulative + per-bar dollar-volume) a first-class gate (the thing runner.py
lacked, which let it fire on dust), plus a price floor + fresh-session-high breakout + VWAP hold.
NO gap, NO blue-sky, NO float. Validated on the real tape: fires on MSTR (@bar15) and SSPC (@bar90),
correctly silent on the sub-$2 dust (DXF) and the <$1M single-print names.

Design finding baked in: RVOL is RECORDED, not a hard gate. MSTR/SSPC were NOT within-session
volume surges -- MSTR was liquid from the open (recent/early vol ratio ~0.5-1.0 the whole grind),
so a hard rvol>=3x "surge" gate structurally kills the steadily-liquid grinders this lane exists to
catch. rvol is stored as a stratification feature (the shadow track will measure whether high-rvol
fires convert better, like the runner GRADE); it only gates if rvol_min>0 is passed.

Selectivity is broad (~50% of the liquid universe fires at least once/day) -- this is a candidate
POPULATION definer, not a precise signal. The decisive question (does it convert to +1R better than
the ORB, and does any rvol/liquidity stratum beat it) is for the shadow track + a pre-registered
forward bar. SHADOW-ONLY: never routes to approve_order; 6th selection signal, must prove conversion
after cost before any live gating. See memory thesis-scope-liquid-intraday-momentum.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:34:56 -04:00

63 lines
2.5 KiB
Python

"""Liquid intraday-RVOL breakout detector — fires on a fillable intraday igniter,
stays SILENT on illiquid dust (the failure mode the naive runner detector had)."""
import pandas as pd
from strategy.evaluation.liquid_breakout import detect_liquid_intraday_breakout
def _rising(n=20, start=10.0, step=0.1, vol_early=1_000, vol_burst=200_000, burst=5):
rows = []
px = start
for i in range(n):
o = px
px = px + step
vol = vol_burst if i >= n - burst else vol_early
rows.append((o, px + 0.01, o - 0.01, px, vol))
return pd.DataFrame(rows, columns=["open", "high", "low", "close", "volume"])
def test_fires_on_liquid_intraday_breakout():
sig = detect_liquid_intraday_breakout(_rising())
assert sig.is_valid, sig.reason
assert sig.reason == "liquid_intraday_breakout"
assert sig.entry_level and sig.stop_level
def test_illiquid_dust_does_not_fire():
# AMCI/CCTG-class: same shape, but nobody's trading it -> tiny dollar-volume
dust = _rising(vol_early=40, vol_burst=60)
sig = detect_liquid_intraday_breakout(dust)
assert not sig.is_valid
assert sig.reason == "illiquid (dollar-volume floor)"
def test_below_price_floor_does_not_fire():
cheap = _rising(start=0.8, step=0.02) # penny dust, even if liquid
sig = detect_liquid_intraday_breakout(cheap, min_dollar_vol=0, min_bar_dollar_vol=0)
assert not sig.is_valid and sig.reason == "below price floor"
def test_rvol_recorded_not_gated_by_default():
# steady-liquid grinder (flat volume, no within-session surge) STILL fires by default:
# rvol is a recorded feature, not a hard gate (the MSTR-class fix). Passing rvol_min>0
# re-enables the gate for callers who want it.
flat = _rising(vol_early=200_000, vol_burst=200_000)
sig = detect_liquid_intraday_breakout(flat)
assert sig.is_valid # fires by default
assert sig.signal_values["rvol"] > 0 # rvol still recorded
gated = detect_liquid_intraday_breakout(flat, rvol_min=3.0)
assert not gated.is_valid and gated.reason == "no rvol surge"
def test_not_at_session_high_does_not_fire():
b = _rising()
b.loc[b.index[-1], ["close", "high"]] = [10.5, 10.55] # pulled back off the high
sig = detect_liquid_intraday_breakout(b)
assert not sig.is_valid and sig.reason in ("not at session high", "below session VWAP")
def test_insufficient_bars_is_quiet():
sig = detect_liquid_intraday_breakout(_rising(n=6))
assert not sig.is_valid and "need >=" in (sig.reason or "")