Files
momentum-system/tests/unit/experiments/test_orderflow_interventions.py
T
momentum devandClaude Fable 5 07ddbd30b9 experiments: register orderflow H3 (spread-expansion gate) + H4 (volume-spike gate)
H4 (3-book): keep-or-zero on trigger-bar volume concentration vs prior 5-bar
sum (>1.0 = completed one-shot burst). H3 (5-book, direction disputed):
fragility-camp primary — zero fires whose fill-time relative spread exceeds
2x the name's own 10-min median from quote_snapshots (which carry bid/ask
SIZES since 07-02 — the note's data-upgrade #1 was already satisfied).
Coverage design learns the dilution_veto_v1 lesson: no-quote fires are
SKIPPED (variant None, excluded from n), so thin coverage cannot false-FAIL.
H5 deliberately deferred until H1's verdict (its incremental value is defined
net of H1's catches). 6 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:32:10 -05:00

197 lines
9.2 KiB
Python

"""Order-flow shelf interventions (docs/research/orderflow_notes.md H1/H2).
H1 trigger_erasure: exit on the first post-fill 1-min close back below the
trigger within a window — the 5-book transitory-impact/recross signature —
else the standing exit stack (baseline). H2 flow_persistence_gate: keep-or-
zero on pre-trigger volume-weighted bar-sign persistence (ongoing-metaorder
signature); fail-open on thin history. Both mirror the regime/dilution
keep-or-zero discipline: no-data can only KEEP, never zero."""
from datetime import date, datetime
import pandas as pd
from research.experiments.replay import iv_flow_persistence, iv_trigger_erasure, Fire
D = date(2026, 6, 10)
def _fire(entry=10.0, stop=9.0, trigger=10.0):
return Fire(fire_id="f1", symbol="AAA", session_date=D,
fill_ts=datetime(2026, 6, 10, 13, 36), entry_price=entry,
stop_price=stop, exit_price=None, baseline_r=None,
trigger_price=trigger, sim_baseline=True)
def _bars(rows):
return pd.DataFrame(rows, columns=["open", "high", "low", "close", "volume"])
FID = {"atr_frac": 0.25, "spread_mult": 3, "adverse_ticks": 1}
# ---------------------------------------------------------------- H1: erasure
def test_erasure_exits_at_first_close_below_trigger():
bars_after = _bars([(10.1, 10.2, 9.85, 9.90, 1000), # close 9.90 < trigger 10 -> exit here
(9.9, 10.5, 9.8, 10.4, 1000)])
v, d = iv_trigger_erasure(_fire(), bars_after, baseline=0.5,
params={"window_bars": 5}, fidelity=FID)
# exit fill 9.90 - 0.01 adverse; entry fill 10.00 + 0.01; risk 1.0
assert abs(v - ((9.89 - 10.01) / 1.0)) < 1e-9
assert d["kind"] == "trigger_erasure" and d["erased"] is True and d["erasure_bar"] == 0
def test_no_erasure_keeps_baseline():
bars_after = _bars([(10.1, 10.3, 10.05, 10.2, 1000)] * 6)
v, d = iv_trigger_erasure(_fire(), bars_after, baseline=0.5,
params={"window_bars": 5}, fidelity=FID)
assert v == 0.5 and d["erased"] is False
def test_erasure_outside_window_keeps_baseline():
rows = [(10.1, 10.3, 10.05, 10.2, 1000)] * 5 + [(10.2, 10.2, 9.5, 9.6, 1000)]
v, d = iv_trigger_erasure(_fire(), _bars(rows), baseline=0.5,
params={"window_bars": 5}, fidelity=FID)
assert v == 0.5 and d["erased"] is False
# ------------------------------------------------------- H2: flow persistence
def _pre_bars(signs, vol=1000):
rows = []
px = 10.0
for s in signs:
o = px
c = px + 0.05 * s
rows.append((o, max(o, c) + 0.01, min(o, c) - 0.01, c, vol))
px = c
return _bars(rows)
def test_persistent_one_sided_flow_keeps_baseline():
bars = _pre_bars([1] * 15) # all up bars -> persistence 1.0
v, d = iv_flow_persistence(bars, entry_idx=15, baseline=0.4,
params={"lookback_bars": 15, "min_persistence": 0.30})
assert v == 0.4 and d["flagged"] is False and d["persistence"] > 0.9
def test_choppy_flow_is_zeroed():
bars = _pre_bars([1, -1] * 8) # alternating -> persistence ~0
v, d = iv_flow_persistence(bars, entry_idx=16, baseline=0.4,
params={"lookback_bars": 15, "min_persistence": 0.30})
assert v == 0.0 and d["flagged"] is True
def test_thin_history_fails_open():
bars = _pre_bars([1, -1, 1]) # only 3 pre-trigger bars -> no-data KEEP
v, d = iv_flow_persistence(bars, entry_idx=3, baseline=0.4,
params={"lookback_bars": 15, "min_persistence": 0.30})
assert v == 0.4 and d["flagged"] is False and d.get("no_data") is True
def test_lookback_excludes_entry_bar():
# 15 up bars then a huge DOWN entry bar at idx 15: excluded -> still persistent
bars = pd.concat([_pre_bars([1] * 15), _bars([(10.0, 10.0, 8.0, 8.0, 99999)])],
ignore_index=True)
v, d = iv_flow_persistence(bars, entry_idx=15, baseline=0.4,
params={"lookback_bars": 15, "min_persistence": 0.30})
assert d["flagged"] is False and d["persistence"] > 0.9
# --------------------------------------------------------- registration JSONs
def test_orderflow_experiments_declared():
import json, pathlib
root = pathlib.Path(__file__).resolve().parents[3] / "experiments"
h1 = json.loads((root / "orderflow_h1_erasure_v1.json").read_text())
h2 = json.loads((root / "orderflow_h2_persistence_v1.json").read_text())
assert h1["intervention"] == {"kind": "trigger_erasure", "window_bars": 5,
"condition": "close_below"}
assert h2["intervention"] == {"kind": "flow_persistence_gate",
"lookback_bars": 15, "min_persistence": 0.30}
for j in (h1, h2):
assert j["bars"] == {"expR_delta_min": 0.05, "min_n": 300}
assert j["fidelity"] == {"atr_frac": 0.25, "spread_mult": 3, "adverse_ticks": 1}
assert j["population"] == {"min_entry_gap_pct": 0.10, "min_day_volume": 300000,
"price_min": 2.0, "price_max": 20.0}
# ------------------------------------------------- H4: volume concentration
def test_isolated_spike_trigger_is_zeroed():
from research.experiments.replay import iv_volume_spike_gate
rows = [(10, 10.1, 9.9, 10, 100)] * 5 + [(10, 10.5, 10, 10.4, 5000)] # spike bar
v, d = iv_volume_spike_gate(_bars(rows), entry_idx=5, baseline=0.4,
params={"lookback_bars": 5, "max_concentration": 1.0})
assert v == 0.0 and d["flagged"] is True and d["concentration"] == 10.0
def test_volume_ramp_trigger_is_kept():
from research.experiments.replay import iv_volume_spike_gate
rows = [(10, 10.1, 9.9, 10, v) for v in (500, 800, 1200, 1800, 2500)] + [(10, 10.5, 10, 10.4, 2000)]
v, d = iv_volume_spike_gate(_bars(rows), entry_idx=5, baseline=0.4,
params={"lookback_bars": 5, "max_concentration": 1.0})
assert v == 0.4 and d["flagged"] is False
def test_spike_gate_thin_history_fails_open():
from research.experiments.replay import iv_volume_spike_gate
rows = [(10, 10.1, 9.9, 10, 100), (10, 10.5, 10, 10.4, 5000)]
v, d = iv_volume_spike_gate(_bars(rows), entry_idx=1, baseline=0.4,
params={"lookback_bars": 5, "max_concentration": 1.0})
assert v == 0.4 and d.get("no_data") is True
# ------------------------------------------------- H3: spread expansion
def _seed_quotes(store, symbol, rows):
store.con.execute(
"CREATE TABLE IF NOT EXISTS quote_snapshots (symbol VARCHAR, ts TIMESTAMPTZ DEFAULT now(), "
"bid_price DOUBLE PRECISION, bid_size BIGINT, ask_price DOUBLE PRECISION, ask_size BIGINT)")
for ts, bid, ask in rows:
store.con.execute(
"INSERT INTO quote_snapshots (symbol, ts, bid_price, bid_size, ask_price, ask_size) "
"VALUES (?,?,?,?,?,?)", [symbol, ts, bid, 100, ask, 100])
def test_spread_blowout_at_trigger_is_zeroed():
from datetime import timezone, timedelta
from storage.event_store import EventStore
from research.experiments.replay import iv_spread_expansion_gate
store = EventStore(":memory:")
fill_naive_et = datetime(2026, 6, 10, 9, 40) # naive ET fill
fill_utc = datetime(2026, 6, 10, 13, 40, tzinfo=timezone.utc)
quotes = [(fill_utc - timedelta(seconds=15 * (i + 2)), 10.00, 10.02) for i in range(8)]
quotes.append((fill_utc - timedelta(seconds=5), 9.95, 10.15)) # last: spread 10x baseline
_seed_quotes(store, "AAA", quotes)
f = _fire()
f = type(f)(**{**f.__dict__, "fill_ts": fill_naive_et})
v, d = iv_spread_expansion_gate(store.con, f, baseline=0.4,
params={"baseline_minutes": 10, "max_expansion_ratio": 2.0})
assert v == 0.0 and d["flagged"] is True and d["expansion_ratio"] > 2.0
def test_spread_normal_at_trigger_is_kept():
from datetime import timezone, timedelta
from storage.event_store import EventStore
from research.experiments.replay import iv_spread_expansion_gate
store = EventStore(":memory:")
fill_utc = datetime(2026, 6, 10, 13, 40, tzinfo=timezone.utc)
quotes = [(fill_utc - timedelta(seconds=15 * (i + 1)), 10.00, 10.02) for i in range(9)]
_seed_quotes(store, "AAA", quotes)
f = _fire()
f = type(f)(**{**f.__dict__, "fill_ts": datetime(2026, 6, 10, 9, 40)})
v, d = iv_spread_expansion_gate(store.con, f, baseline=0.4,
params={"baseline_minutes": 10, "max_expansion_ratio": 2.0})
assert v == 0.4 and d["flagged"] is False
def test_no_quote_coverage_is_skipped_not_kept():
"""The dilution-v1 lesson: fires WITHOUT feature coverage must be EXCLUDED
from n (variant None -> evaluate() skips), never silently counted."""
from storage.event_store import EventStore
from research.experiments.replay import iv_spread_expansion_gate
store = EventStore(":memory:")
_seed_quotes(store, "OTHER", []) # table exists, no rows for AAA
f = _fire()
f = type(f)(**{**f.__dict__, "fill_ts": datetime(2026, 6, 10, 9, 40)})
v, d = iv_spread_expansion_gate(store.con, f, baseline=0.4,
params={"baseline_minutes": 10, "max_expansion_ratio": 2.0})
assert v is None and d.get("skipped") == "no_quote_coverage"