""" Neural-network scan, OOS only (2026-10-08). MLP ensemble (5 seeds, 64-32, L2) on pooled SP500/NAS100/US30/DAX40 D1. Inputs at the close of day t: last 10 daily returns (scaled by trailing 60d vol), 20d z-score, VRP, VIX/VIX3M, VIX 1d change, RV10/RV60, BAA10Y 5d change, weekday+month cyclic, symbol one-hot. Target: next-open -> close 3 bars later > 0. Walk-forward: retrain each year Y=2013..2026 on data before Y minus a 7-day embargo; evaluate only on Y. Report OOS AUC (overall and per year) and the gross bp of simple fixed rules: long if p>0.60, short if p<0.40. Compared with buy-every-day. """ import sys, warnings, numpy as np, pandas as pd sys.path.insert(0, ".") warnings.filterwarnings("ignore") import backtest as bt, macro_gates as mg from sklearn.neural_network import MLPClassifier from sklearn.preprocessing import StandardScaler from sklearn.metrics import roc_auc_score H = 3 def build(): spx = bt.load("SP500", "PERIOD_D1") gt = mg.gate_table(pd.Series(spx["c"], index=pd.DatetimeIndex(spx["ts"]).normalize())) gt["dvix"] = gt.vix.pct_change() out = [] for k, s in enumerate(mg.IDX): d = bt.load(s, "PERIOD_D1"); ts = pd.DatetimeIndex(d["ts"]).normalize(); c, o = pd.Series(d["c"]), d["o"]; n = len(c) r = np.log(c).diff(); sig60 = r.rolling(60).std() f = pd.DataFrame({"sym": s, "date": ts}) for L in range(10): f[f"r{L}"] = (r.shift(L) / sig60).to_numpy() f["z20"] = ((c - c.rolling(20).mean()) / c.rolling(20).std()).to_numpy() f["rv_ratio"] = (r.rolling(10).std() / sig60).to_numpy() for col in ("vrp", "ratio", "dvix", "cr5"): f[col] = gt[col].reindex(ts).to_numpy() f["dow_s"] = np.sin(2 * np.pi * ts.dayofweek / 5); f["dow_c"] = np.cos(2 * np.pi * ts.dayofweek / 5) f["m_s"] = np.sin(2 * np.pi * ts.month / 12); f["m_c"] = np.cos(2 * np.pi * ts.month / 12) for j, t in enumerate(mg.IDX): f[f"is_{t}"] = float(j == k) fw = np.full(n, np.nan) for i in range(n - H - 1): fw[i] = (c.iloc[i + H + 1] - o[i + 1]) / o[i + 1] * 1e4 f["fwd"] = fw; out.append(f) X = pd.concat(out, ignore_index=True) X = X[X.date >= "2009-10-01"].replace([np.inf, -np.inf], np.nan).dropna() # VIX3M starts 2009-09 X["year"] = X.date.dt.year; return X if __name__ == "__main__": X = build(); feats = [c for c in X.columns if c not in ("sym", "date", "fwd", "year")] print(f"rows {len(X)} features {len(feats)} base rate up {(X.fwd>0).mean():.1%}") P = pd.Series(np.nan, index=X.index); aucs = {} for Y in range(2013, 2027): tr = X[X.date < pd.Timestamp(f"{Y}-01-01") - pd.Timedelta(days=7)]; te = X[X.year == Y] if len(te) < 50: continue sc = StandardScaler().fit(tr[feats]); a, b = sc.transform(tr[feats]), sc.transform(te[feats]) p = np.mean([MLPClassifier((64, 32), alpha=1e-2, max_iter=60, random_state=s, learning_rate_init=1e-3) .fit(a, tr.fwd > 0).predict_proba(b)[:, 1] for s in range(5)], axis=0) P[te.index] = p aucs[Y] = roc_auc_score(te.fwd > 0, p) m = P.notna(); Xo = X[m].assign(p=P[m]) print("OOS AUC by year:", {k: round(v, 3) for k, v in aucs.items()}) print(f"OOS AUC overall {roc_auc_score(Xo.fwd>0, Xo.p):.3f} years > 0.5: {sum(v>0.5 for v in aucs.values())}/{len(aucs)} mean {np.mean(list(aucs.values())):.3f}") print(f"p distribution: mean {Xo.p.mean():.3f} sd {Xo.p.std():.3f} min {Xo.p.min():.2f} max {Xo.p.max():.2f}") bh = Xo.fwd.mean(); print(f"\nbuy every day: {bh:.1f} bp (n {len(Xo)})") for name, sel, sgn in (("long p>0.60", Xo.p > .60, 1), ("short p<0.40", Xo.p < .40, -1), ("long p>0.55", Xo.p > .55, 1), ("short p<0.45", Xo.p < .45, -1)): s = Xo[sel]; if len(s): print(f"{name:<14} n={len(s):>5} mean {sgn*s.fwd.mean():7.1f} bp hit {((sgn*s.fwd)>0).mean():.0%} years+ {sum((sgn*g.fwd.mean())>0 for _, g in s.groupby('year'))}/{s.year.nunique()}") # decile view: does higher p mean higher forward return? Xo["dec"] = pd.qcut(Xo.p, 5, labels=False, duplicates="drop"); print("\nquintile of p -> mean fwd bp:", Xo.groupby("dec").fwd.mean().round(1).to_dict())