""" PRE-REGISTERED conditional-direction scan (2026-10-08): "given vol regime x recent move x weekday, where does price go?" Universe: SP500/NAS100/US30/DAX40 D1, pooled. Features known at the close of day t: VRP bucket {<=0, 0-5, >5} (VIX - RV10 of SP500, same as macro_gates) ret1 bucket {<=-1%, mid, >=+1%} (that index's own prior-day return) ret5 bucket {<=-2%, mid, >=+2%} (5-day return) weekday {Mon..Fri} => 135 cells. Outcome: open(t+1) -> close(t+3), minus real round-trip spread. Walk-forward: for test year Y = 2013..2026 select on data < Y: cells with n>=40 and |t| >= 2.5 on the (excess over train mean) outcome, top 5 by |t|, trade each in the sign of its train excess in year Y. Statistic: mean net bp per selected-cell trade over all OOS years. NULL: the identical procedure with the cell label of each day randomly permuted within each index (500 permutations) -> p = share of null >= observed. Pass: observed stat > 0 and permutation p <= 0.05. One config, no re-runs with other numbers. """ import sys, numpy as np, pandas as pd sys.path.insert(0, ".") import backtest as bt, macro_gates as mg H = 3 def build(): spx = bt.load("SP500", "PERIOD_D1") gt = mg.gate_table(pd.Series(spx["c"], index=pd.DatetimeIndex(spx["ts"]).normalize())) rows = [] for s in mg.IDX: d = bt.load(s, "PERIOD_D1"); ts = pd.DatetimeIndex(d["ts"]).normalize(); c, o = d["c"], d["o"]; n = len(c) df = pd.DataFrame({"sym": s, "date": ts, "c": c}, index=range(n)) df["r1"] = pd.Series(c).pct_change(); df["r5"] = pd.Series(c).pct_change(5) df["vrp"] = gt.vrp.reindex(ts).to_numpy(); df["dow"] = ts.dayofweek fwd = np.full(n, np.nan); for i in range(n - H - 1): fwd[i] = (c[i + H + 1] - o[i + 1]) / o[i + 1] - d["cost"][i + 1] / o[i + 1] # buy next open, sell close of t+4? (H+1 bars later) df["fwd"] = fwd * 1e4; df["cost"] = d["cost"] / np.r_[o[1:], o[-1]] * 1e4 rows.append(df) X = pd.concat(rows, ignore_index=True).dropna(subset=["r1", "r5", "vrp", "fwd"]) X["cell"] = (pd.cut(X.vrp, [-1e9, 0, 5, 1e9], labels=False) * 45 + pd.cut(X.r1, [-1, -.01, .01, 1], labels=False) * 15 + pd.cut(X.r5, [-1, -.02, .02, 1], labels=False) * 5 + X.dow).astype(int) X["year"] = X.date.dt.year return X def procedure(X, cell, years=range(2013, 2027), K=5, MINN=40, TH=2.5): pnl = [] for Y in years: tr, te = X.year < Y, X.year == Y g = X[tr].assign(cell=cell[tr]).groupby("cell").fwd m, sd, nn = g.mean(), g.std(), g.count(); mu = X[tr].fwd.mean() t = (m - mu) / (sd / np.sqrt(nn)); t = t[(nn >= MINN) & (t.abs() >= TH)] pick = t.reindex(t.abs().sort_values(ascending=False).index)[:K] for cl, tv in pick.items(): sel = X[te & (cell == cl)] if len(sel): pnl += list(np.sign(tv) * (sel.fwd + sel.cost) - sel.cost) # direction flips the gross, cost always paid return np.array(pnl) if __name__ == "__main__": X = build(); cell = X.cell.copy() obs = procedure(X, cell) print(f"cells {X.cell.nunique()} rows {len(X)} OOS selected trades {len(obs)} mean net {obs.mean():.1f} bp hit {(obs>0).mean():.1%}") rng = np.random.default_rng(0); null = [] for k in range(500): sh = X.groupby("sym").cell.transform(lambda v: rng.permutation(v.to_numpy())) p = procedure(X, sh); null.append(p.mean() if len(p) else np.nan) null = np.array(null); null = null[~np.isnan(null)] print(f"null mean {null.mean():.1f} sd {null.std():.1f} p(null>=obs) = {(null >= obs.mean()).mean():.3f}") print("VERDICT:", "PASS" if obs.mean() > 0 and (null >= obs.mean()).mean() <= .05 else "REJECTED")