okWarrior_EA/research/volume_gate.py

31 lines
1.9 KiB
Python

"""
PRE-REGISTERED (2026-10-08), one config, N=13: relative-volume gate on the D1 4-index dip-z book.
rv = volume_t / mean(volume over the previous 20 bars), computed per symbol on the broker's own D1 bars
(tick-count regimes jump over time, so only the RATIO inside a rolling window is used, never the level).
G6a: rv >= 1.0 (above-average volume on the signal bar) G6b: rv < 1.0. Judged as before, OOS windows only shown.
"""
import sys, numpy as np, pandas as pd
sys.path.insert(0, ".")
import backtest as bt, macro_gates as mg
from run_screen import zscore_entries
def collect(mode):
out = []
for s in mg.IDX:
d = bt.load(s, "PERIOD_D1"); e, xma = zscore_entries(d, 20, -1.5, 0)
v = pd.Series(d["v"].astype(float)); rv = (v / v.shift(1).rolling(20).mean()).to_numpy()
if mode == "a": e = e & np.nan_to_num(rv >= 1.0, nan=False)
if mode == "b": e = e & np.nan_to_num(rv < 1.0, nan=False)
tr = bt.add_r(bt.simulate(d, e, 1, xma, 10, 3.0), d, 3.0)
for t in tr: t["symbol"] = s; t["exit_t"] = d["ts"][t["exit_i"]]
out += tr
return out
runs = {m: collect(m) for m in ("base", "a", "b")}
print(f"{'run':<6}{'window':<11}{'n':>5}{'bp':>7}{'meanR':>7}{'tot':>8}{'DD':>7}{'ret/DD':>8}")
for m, tr in runs.items():
for w, (lo, hi) in {"IS<=2016": ("2008-01-01", "2017-01-01"), "OOS2017-20": ("2017-01-01", "2021-01-01"), "OOS2021+": ("2021-01-01", "2030-01-01"), "2022": ("2022-01-01", "2023-01-01"), "FULL": ("2008-01-01", "2030-01-01")}.items():
s = mg.window_stats(tr, lo, hi)
print(f"{m:<6}{w:<11}" + (f"{s['n']:>5}{s['bp']:>7.1f}{s['R']:>7.3f}{s['tot']:>8.1%}{s['dd']:>7.1%}{s['rdd']:>8.2f}" if s else " <3"))
print()
for s in mg.IDX:
x = [t["ret"]*1e4 for t in runs["a"] if t["symbol"] == s]; y = [t["ret"]*1e4 for t in runs["b"] if t["symbol"] == s]
print(f"{s:<7} high-vol n={len(x):>3} {np.mean(x):6.1f}bp | low-vol n={len(y):>3} {np.mean(y):6.1f}bp")