Files
momentum-system/tests/unit/experiments/test_orderflow_interventions.py
T
momentum devandClaude Fable 5 46d3460d5c experiments: depth_overhang_v1 — L2 order-book overhang gate (first signal to clear the bar+null)
Plan 017 Phase 0->3. Databento depth backfilled for 602 fires across 21
sessions -> depth_features table. Reweighted to true 23% population win rate,
gating on ask-overhang-above-break (keep>=0.034) lifts win rate 23->28% and
clears the random-gate null (+0.057 vs +0.05 on the conservative +1/-1
bracket). iv_depth_overhang_gate = keep-or-zero, skip-on-no-coverage (the
dilution-v1 lesson). This commit registers the REAL-exit harness run
(simulate_exit asymmetric trail) for the true live delta. Counter-intuitive
mechanism: more resting supply above the break = MORE likely to win
(absorption/fuel), invisible in bar data — the owner's 'size stacked above'
instinct, proven.

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

265 lines
13 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"
# ------------------------------------------------- H6: quote-size imbalance
def _seed_sized_quotes(store, symbol, fill_utc, sizes):
"""sizes: list of (bid_size, ask_size) at 15s cadence ending just before fill."""
from datetime import timedelta
_seed_quotes(store, "_ddl_only_", []) # ensure table
for i, (bs, asz) in enumerate(sizes):
store.con.execute(
"INSERT INTO quote_snapshots (symbol, ts, bid_price, bid_size, ask_price, ask_size) "
"VALUES (?,?,?,?,?,?)",
[symbol, fill_utc - timedelta(seconds=15 * (len(sizes) - i)), 10.0, bs, 10.02, asz])
def test_bid_heavy_book_is_kept():
from datetime import timezone
from storage.event_store import EventStore
from research.experiments.replay import iv_quote_imbalance_gate
store = EventStore(":memory:")
fill_utc = datetime(2026, 6, 10, 13, 40, tzinfo=timezone.utc)
_seed_sized_quotes(store, "AAA", fill_utc, [(900, 100)] * 8) # imbalance +0.8
f = _fire()
f = type(f)(**{**f.__dict__, "fill_ts": datetime(2026, 6, 10, 9, 40)})
v, d = iv_quote_imbalance_gate(store.con, f, baseline=0.4,
params={"baseline_minutes": 10, "min_book_imbalance": 0.0})
assert v == 0.4 and d["flagged"] is False and d["book_imbalance"] > 0.5
def test_ask_heavy_book_is_zeroed():
from datetime import timezone
from storage.event_store import EventStore
from research.experiments.replay import iv_quote_imbalance_gate
store = EventStore(":memory:")
fill_utc = datetime(2026, 6, 10, 13, 40, tzinfo=timezone.utc)
_seed_sized_quotes(store, "AAA", fill_utc, [(100, 900)] * 8) # imbalance -0.8
f = _fire()
f = type(f)(**{**f.__dict__, "fill_ts": datetime(2026, 6, 10, 9, 40)})
v, d = iv_quote_imbalance_gate(store.con, f, baseline=0.4,
params={"baseline_minutes": 10, "min_book_imbalance": 0.0})
assert v == 0.0 and d["flagged"] is True and d["book_imbalance"] < -0.5
def test_ofi_no_coverage_is_skipped():
from storage.event_store import EventStore
from research.experiments.replay import iv_quote_imbalance_gate
store = EventStore(":memory:")
_seed_quotes(store, "OTHER", [])
f = _fire()
f = type(f)(**{**f.__dict__, "fill_ts": datetime(2026, 6, 10, 9, 40)})
v, d = iv_quote_imbalance_gate(store.con, f, baseline=0.4,
params={"baseline_minutes": 10, "min_book_imbalance": 0.0})
assert v is None and d.get("skipped") == "no_quote_coverage"
# --------------------------------------------------- depth overhang (plan 017)
def test_depth_overhang_gate_zeros_low_keeps_high_skips_missing():
from research.experiments.replay import iv_depth_overhang_gate
dh = {"setup:A": 0.30, "setup:B": 0.01} # A above thresh, B below
p = {"min_overhang": 0.05}
fa = _fire(); fa = type(fa)(**{**fa.__dict__, "fire_id": "setup:A"})
fb = _fire(); fb = type(fb)(**{**fb.__dict__, "fire_id": "setup:B"})
fc = _fire(); fc = type(fc)(**{**fc.__dict__, "fire_id": "setup:C"}) # not in table
va,da = iv_depth_overhang_gate(fa, 0.4, dh, p)
vb,db = iv_depth_overhang_gate(fb, 0.4, dh, p)
vc,dc = iv_depth_overhang_gate(fc, 0.4, dh, p)
assert va == 0.4 and da["flagged"] is False # high overhang -> keep
assert vb == 0.0 and db["flagged"] is True # low overhang -> zero
assert vc is None and dc.get("skipped") == "no_depth_coverage" # missing -> skip n