forked from mnbvc188199/Warrior_EA
- Implemented `catalog.py` to define instrument metadata and price scaling checks. - Developed `sqx.py` for decoding SQX tick .dat files, handling scaling and volume adjustments. - Created `sqxbars.py` to decode SQX bar files, ensuring price integrity against known ranges. - Introduced `volume_gate.py` for relative volume analysis on D1 bars, facilitating backtesting strategies.
61 lines
3.6 KiB
Python
61 lines
3.6 KiB
Python
"""
| |
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")
|