From ab2810896cadb8a36d892aa579a906374c198d4d Mon Sep 17 00:00:00 2001 From: momentum dev Date: Tue, 14 Jul 2026 22:24:18 -0400 Subject: [PATCH] experiment-os: generic exit-config intervention + v1/pm1 exit experiments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iv_exit_config replays ANY target/breakeven/trail combo through the shared exit engine vs the live-exit baseline — reusable way to adjudicate exit numbers. Counterfactual over setup fires: v1 (slow, 1.37R median winners) gains most from prior_low trailing (win 17->24%, +0.036R); pm1 (fast, 2.64R median, 10R tail) gains most from NO target + step0.5 (+0.273R, win 24->34%) — the fixed target throws away pm1's fat tail. Registered both as tracked experiments. +test. Co-Authored-By: Claude Fable 5 --- experiments/exit_pm1_fattail.json | 9 ++++++ experiments/exit_prior_low_v1.json | 9 ++++++ research/experiments/replay.py | 30 +++++++++++++++++++ .../unit/experiments/test_replay_fidelity.py | 15 ++++++++++ 4 files changed, 63 insertions(+) create mode 100644 experiments/exit_pm1_fattail.json create mode 100644 experiments/exit_prior_low_v1.json diff --git a/experiments/exit_pm1_fattail.json b/experiments/exit_pm1_fattail.json new file mode 100644 index 0000000..2e4f555 --- /dev/null +++ b/experiments/exit_pm1_fattail.json @@ -0,0 +1,9 @@ +{ + "id": "exit_pm1_fattail", + "hypothesis": "Removing the fixed target and step-locking profit (trail_r_step=0.5) captures the premarket fat tail (pm1 winners median 2.64R, peak 22min, tail to 10R) that a target-2R cap throws away — the biggest exit delta measured (+0.27R over the live exit).", + "population": {"min_entry_gap_pct": 0.10, "min_day_volume": 300000, "price_min": 2.0, "price_max": 20.0}, + "intervention": {"kind": "exit_config", "exit_cfg": {"target_r": 0.0, "breakeven_at_pct": 0.05, "trail_r_step": 0.5}}, + "bars": {"expR_delta_min": 0.05, "min_n": 300}, + "futility": {"min_n": 500, "expR_delta_max": -0.03}, + "fidelity": {"atr_frac": 0.25, "spread_mult": 3, "adverse_ticks": 1} +} diff --git a/experiments/exit_prior_low_v1.json b/experiments/exit_prior_low_v1.json new file mode 100644 index 0000000..cea38c3 --- /dev/null +++ b/experiments/exit_prior_low_v1.json @@ -0,0 +1,9 @@ +{ + "id": "exit_prior_low_v1", + "hypothesis": "Adding a prior-low trailing stop (trail_after_r=1) to the live exit lifts the win rate and captures give-back that the no-trail breakeven ratchet scratches — the fix for slow small v1 winners (median 1.37R, peak 134min).", + "population": {"min_entry_gap_pct": 0.10, "min_day_volume": 300000, "price_min": 2.0, "price_max": 20.0}, + "intervention": {"kind": "exit_config", "exit_cfg": {"target_r": 2.0, "breakeven_at_pct": 0.05, "trail_mode": "prior_low", "trail_after_r": 1.0}}, + "bars": {"expR_delta_min": 0.03, "min_n": 300}, + "futility": {"min_n": 500, "expR_delta_max": -0.03}, + "fidelity": {"atr_frac": 0.25, "spread_mult": 3, "adverse_ticks": 1} +} diff --git a/research/experiments/replay.py b/research/experiments/replay.py index b39957a..adc33a3 100644 --- a/research/experiments/replay.py +++ b/research/experiments/replay.py @@ -315,6 +315,34 @@ def _sim_baseline_r(fire, bars_after, fidelity) -> float | None: return sum(fl.frac * ((fl.price - adv) - entry_fill) / risk for fl in res.fills) +def iv_exit_config(fire, bars_after, params, fidelity): + """Generic exit-config experiment: run the shared exit engine (simulate_exit) + with an arbitrary ExitConfig from params['exit_cfg'] and measure R vs the LIVE + exit baseline, same adverse-fill accounting. Lets ANY target/breakeven/trail + combination be adjudicated over logged fires without new code — the reusable + way to answer 'does this exit number retain more profit?'.""" + from strategy.exits import TRAIL_NONE + risk = fire.entry_price - fire.stop_price + if risk <= 0 or not len(bars_after): + return None, {"kind": "exit_config", "reason": "no_bars_or_risk"} + ec = params.get("exit_cfg", {}) + cfg = ExitConfig( + target_r=float(ec.get("target_r", 2.0)), + breakeven_at_r=float(ec.get("breakeven_at_r", 0.0)), + breakeven_at_pct=float(ec.get("breakeven_at_pct", 0.05)), + trail_mode=ec.get("trail_mode", TRAIL_NONE), + trail_pct=float(ec.get("trail_pct", 0.0)), + trail_after_r=float(ec.get("trail_after_r", 1.0)), + trail_r_step=float(ec.get("trail_r_step", 0.0)), + profit_lock_tiers=[], scale_out_r=0.0, first_red_exit=False, + catastrophe_pct=0.0, enforce_stop_grace_passes=0) + adv = _adverse(fidelity) + entry_fill = fire.entry_price + adv + res = simulate_exit(entry_fill, fire.stop_price, bars_after, cfg) + v = sum(fl.frac * ((fl.price - adv) - entry_fill) / risk for fl in res.fills) + return v, {"kind": "exit_config", "cfg": ec, "reason": res.reason} + + def iv_volume_fade(fire, bars_after, params, fidelity): """VOLUME-FADE exit: exit at market to LOCK a gain when the momentum volume dies, instead of scratching at breakeven. @@ -447,6 +475,8 @@ def _compute_fire(con, exp: dict, fire: Fire) -> dict | None: v, d = iv_asym_exit(con, fire, bars, bars_after, params, fidelity) elif kind == "volume_fade": v, d = iv_volume_fade(fire, bars_after, params, fidelity) + elif kind == "exit_config": + v, d = iv_exit_config(fire, bars_after, params, fidelity) else: return None d["fidelity"] = fidelity diff --git a/tests/unit/experiments/test_replay_fidelity.py b/tests/unit/experiments/test_replay_fidelity.py index da19a76..48e5863 100644 --- a/tests/unit/experiments/test_replay_fidelity.py +++ b/tests/unit/experiments/test_replay_fidelity.py @@ -154,3 +154,18 @@ def test_volume_fade_no_fade_rides_to_eod(): for i in range(6)]) v, d = replay.iv_volume_fade(fire, bars, {"baseline_bars": 5, "min_hold_bars": 3}, FID) assert d["exit"] == "eod" and v > 0 + + +def test_exit_config_no_target_beats_capped_on_a_runner(): + """Generic exit-config intervention: on a bar path that keeps running, a + no-target trailing config captures more than a target-2R cap (the pm1 fat-tail + insight), and both apply the adverse-fill accounting.""" + fire = _fire(entry=10.0, stop=9.0) # risk 1.0 + # steady runner well past +2R, making higher lows (prior_low trail rides it) + bars = _vbars([(10.0 + i*0.5, 10.4 + i*0.5, 10.0 + i*0.5, 10.3 + i*0.5, 1000) for i in range(8)]) + capped, dc = replay.iv_exit_config(fire, bars, {"exit_cfg": {"target_r": 2.0}}, FID) + runner, dr = replay.iv_exit_config( + fire, bars, {"exit_cfg": {"target_r": 0.0, "trail_mode": "prior_low", "trail_after_r": 1.0}}, FID) + assert dc["kind"] == "exit_config" and dr["kind"] == "exit_config" + assert capped is not None and runner is not None + assert runner > capped, f"no-target trail should beat the 2R cap on a runner ({runner} vs {capped})"