forked from animatedread/Warrior_EA
61 lines
4.1 KiB
Python
61 lines
4.1 KiB
Python
"""
| |||
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())
|