From 485cec03725ccd5853043401d9fabc69b4e66b0d Mon Sep 17 00:00:00 2001 From: momentum dev Date: Mon, 3 Aug 2026 21:06:37 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20wide=5Fstop=5Fveto=5Fv1=20=E2=80=94=20s?= =?UTF-8?q?kip=20entries=20with=20stops=20>12%=20below=20entry=20(shadow)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Year-replay stop-width buckets: <8% +0.072R, 8-12% +0.120R, 12-16% -0.054R, >16% -0.092R — the wide cohort holds every recent dollar disaster (BIYA 16.7%, FCUV day-2 17.5%), where the exit ladder's blind spot (BE/trail only arm on the way up) rides the full catastrophe distance. New stopwidth_gate intervention (fail-open without levels) + registered spec, same forward bar as the pm-high gate; the two cohorts overlap so the adjudicator attributes which does the work. In-sample counts for nothing. Co-Authored-By: Claude Fable 5 --- experiments/wide_stop_veto_v1.json | 9 ++++++++ research/experiments/replay.py | 26 ++++++++++++++++++++++ tests/unit/experiments/test_pmhigh_gate.py | 20 +++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 experiments/wide_stop_veto_v1.json diff --git a/experiments/wide_stop_veto_v1.json b/experiments/wide_stop_veto_v1.json new file mode 100644 index 0000000..01b5e16 --- /dev/null +++ b/experiments/wide_stop_veto_v1.json @@ -0,0 +1,9 @@ +{ + "id": "wide_stop_veto_v1", + "hypothesis": "Skip ORB entries whose stop sits more than 12% below entry. Year-replay buckets (2026-08-03): <8% +0.072R, 8-12% +0.120R, 12-16% -0.054R, >16% -0.092R; both July dollar-disasters (BIYA 16.7%, FCUV day-2 17.5%) were wide-stop, where the exit ladder's blind spot (nothing arms until price rises) rides the full catastrophe distance. In-sample evidence counts for nothing toward the bar; overlaps orb_pmhigh_gate_v1's cohort — adjudicate both to attribute the work.", + "population": {"min_entry_gap_pct": 0.10, "min_day_volume": 300000, "price_min": 2.0, "price_max": 20.0}, + "intervention": {"kind": "stopwidth_gate", "max_stop_pct": 0.12}, + "bars": {"expR_delta_min": 0.05, "min_n": 200}, + "futility": {"min_n": 400, "expR_delta_max": -0.02}, + "fidelity": {"atr_frac": 0.25, "spread_mult": 3, "adverse_ticks": 1} +} diff --git a/research/experiments/replay.py b/research/experiments/replay.py index 8744e0d..2b40360 100644 --- a/research/experiments/replay.py +++ b/research/experiments/replay.py @@ -476,6 +476,26 @@ def iv_asym_exit(con, fire, bars, bars_after, params, fidelity): # multi-fire coordination needed, so it plugs into the per-fire _compute_fire # path instead of a _run_rank-style batch). # --------------------------------------------------------------------------- # +def iv_stopwidth_gate(fire: "Fire", baseline: float | None, + params: dict) -> tuple[float | None, dict]: + """variant_r = 0.0 when the entry's stop distance exceeds ``max_stop_pct`` + of the entry price, else keep baseline. + + Year-replay buckets (2026-08-03): stop <8% +0.072R (n=647), 8-12% +0.120R + (n=340), 12-16% -0.054R (n=112), >16% -0.092R (n=67) — and every dollar + disaster of late July (BIYA 16.7%, FCUV day-2 17.5%) sat in the wide + cohort, where the exit ladder's blind spot (nothing arms until price + rises) rides the full catastrophe distance. Overlaps the pmhigh_gate + cohort (faded day-2 monsters have both traits); running both lets the + adjudicator attribute the work.""" + if fire.entry_price in (None, 0) or fire.stop_price is None: + return baseline, {"kind": "stopwidth_gate", "stop_pct": None, "reason": "no_levels"} + stop_pct = (fire.entry_price - fire.stop_price) / fire.entry_price + wide = stop_pct > float(params.get("max_stop_pct", 0.12)) + return (0.0 if wide else baseline), {"kind": "stopwidth_gate", + "stop_pct": round(stop_pct, 4), "gated": wide} + + def iv_pmhigh_gate(con, fire: "Fire", bars, entry_idx: int, baseline: float | None) -> tuple[float | None, dict]: """variant_r = baseline if REGULAR-HOURS price reclaimed the PREMARKET high @@ -857,6 +877,12 @@ def _compute_fire(con, exp: dict, fire: Fire, favorable: dict | None = None, if fire.sim_baseline: d["source"] = "setup_sim" return {"baseline_r": baseline, "variant_r": v, "detail": d} + if kind == "stopwidth_gate": + baseline = _sim_baseline_r(fire, bars_after, fidelity) if fire.sim_baseline else fire.baseline_r + v, d = iv_stopwidth_gate(fire, baseline, params) + if fire.sim_baseline: + d["source"] = "setup_sim" + return {"baseline_r": baseline, "variant_r": v, "detail": d} if kind == "regime_gate": # needs the baseline BEFORE computing variant (unlike the other kinds, # which compute variant first and attach fire.sim_baseline's simulated diff --git a/tests/unit/experiments/test_pmhigh_gate.py b/tests/unit/experiments/test_pmhigh_gate.py index 3aa862c..fc43441 100644 --- a/tests/unit/experiments/test_pmhigh_gate.py +++ b/tests/unit/experiments/test_pmhigh_gate.py @@ -54,3 +54,23 @@ def test_bars_after_entry_do_not_count(): bars = _bars([(9.5, False), (10.1, True), (10.9, True)]) v, d = iv_pmhigh_gate(_Con(10.5), _fire(), bars, 1, 1.0) assert v == 0.0 and d["broke"] is False + + +def test_stopwidth_gate_zeroes_wide_and_keeps_tight(): + from research.experiments.replay import iv_stopwidth_gate + P = {"max_stop_pct": 0.12} + wide = Fire(fire_id="o1", symbol="W", session_date=date(2026, 8, 3), fill_ts=None, + entry_price=17.60, stop_price=14.43, exit_price=None, baseline_r=-0.7, + trigger_price=17.35) # 18% stop -> gated + v, d = iv_stopwidth_gate(wide, -0.7, P) + assert v == 0.0 and d["gated"] is True + tight = Fire(fire_id="o2", symbol="T", session_date=date(2026, 8, 3), fill_ts=None, + entry_price=10.0, stop_price=9.2, exit_price=None, baseline_r=0.5, + trigger_price=10.0) # 8% stop -> kept + v, d = iv_stopwidth_gate(tight, 0.5, P) + assert v == 0.5 and d["gated"] is False + nolvl = Fire(fire_id="o3", symbol="N", session_date=date(2026, 8, 3), fill_ts=None, + entry_price=None, stop_price=None, exit_price=None, baseline_r=0.3, + trigger_price=None) + v, d = iv_stopwidth_gate(nolvl, 0.3, P) + assert v == 0.3 and d["reason"] == "no_levels" # fail-open