forked from animatedread/Warrior_EA
76 lines
4.5 KiB
Python
76 lines
4.5 KiB
Python
"""
| |||
PRE-REGISTERED relative-value (ratio-divergence) scan, 2026-10-08. Trial count: 15 pairs x 1 config = N 15
| |||
(plus the 12 earlier macro trials are a separate family). NOTHING is fitted; no re-run with other numbers
| |||
unless reported as new trials.
| |||
| |||
Spread s = ln(A) - ln(B), beta fixed at 1 (a plain price ratio). z = (s - mean_60)/std_60 (past 60 daily bars incl. today).
| |||
Signal at the close of day t: z >= +2 -> short A / long B ; z <= -2 -> long A / short B. Both legs filled at
| |||
the NEXT open. Exit: z crosses back through 0 -> exit at next open; stop |z| >= 4 -> next open; max hold 30 bars.
| |||
Costs: each leg pays its exported spread once at the entry fill and once at the exit fill (round trip = 2x half?
| |||
conservatively the FULL spread per leg per round trip, as in backtest.simulate). Swap/carry NOT modelled (flagged).
| |||
R multiple = pnl / ((4 - |z_entry|) * std_60) in log-spread units; equity = 1 + 0.5% * R.
| |||
Survival per pair: pooled t >= 3.0 (Bonferroni-ish for 15 trials) AND IS(<=2016) bp > 0 AND OOS(2017+) bp > 0
| |||
AND >= 60% of calendar years with >= 3 trades positive.
| |||
"""
| |||
import sys, numpy as np, pandas as pd
| |||
sys.path.insert(0, ".")
| |||
import backtest as bt
| |||
from scipy import stats
| |||
PAIRS = [("SP500","NAS100"),("SP500","US30"),("NAS100","US30"),("DAX40","UK100"),("DAX40","F40EUR"),("DAX40","ESXEUR"),
| |||
("SP500","DAX40"),("UK100","F40EUR"),("XAUUSD","XAGUSD"),("XTIUSD","XBRUSD"),("EURUSD","GBPUSD"),
| |||
("AUDUSD","NZDUSD"),("EURJPY","GBPJPY"),("BTCUSD","ETHUSD"),("JPN225","HSIHKD")]
| |||
L, ZE, ZS, MAXH, RISK = 60, 2.0, 4.0, 30, 0.005
| |||
| |||
def frame(sym):
| |||
d = bt.load(sym, "PERIOD_D1")
| |||
return pd.DataFrame({"o": d["o"], "c": d["c"], "cost": d["cost"]}, index=pd.DatetimeIndex(d["ts"]).normalize()).groupby(level=0).last()
| |||
| |||
def run(a, b):
| |||
A, B = frame(a), frame(b)
| |||
df = A.join(B, how="inner", lsuffix="a", rsuffix="b").dropna()
| |||
s = np.log(df.ca) - np.log(df.cb)
| |||
m, sd = s.rolling(L).mean(), s.rolling(L).std()
| |||
z = ((s - m) / sd).to_numpy(); sdv = sd.to_numpy(); n = len(df)
| |||
oa, ob, costa, costb = df.oa.to_numpy(), df.ob.to_numpy(), df.costa.to_numpy(), df.costb.to_numpy()
| |||
idx = df.index; out = []; i = L
| |||
while i < n - 2:
| |||
if not np.isfinite(z[i]) or abs(z[i]) < ZE: i += 1; continue
| |||
side = -1 if z[i] > 0 else 1 # +1 = long A / short B
| |||
f = i + 1; ze = z[i]; xj = None
| |||
for j in range(f, min(f + MAXH, n - 1)):
| |||
if (side == -1 and z[j] <= 0) or (side == 1 and z[j] >= 0) or abs(z[j]) >= ZS:
| |||
xj = j + 1; break
| |||
if xj is None: xj = min(f + MAXH, n - 1)
| |||
pnl = side * ((np.log(oa[xj]) - np.log(oa[f])) - (np.log(ob[xj]) - np.log(ob[f])))
| |||
c = costa[f] / oa[f] + costb[f] / ob[f]
| |||
risk = (ZS - abs(ze)) * sdv[i]
| |||
out.append(dict(t=idx[f], x=idx[xj], net=pnl - c, gross=pnl, R=(pnl - c) / risk if risk > 0 else 0, bars=xj - f))
| |||
i = xj + 1
| |||
return pd.DataFrame(out)
| |||
| |||
def stats_of(t):
| |||
if len(t) < 5: return None
| |||
bp = t.net * 1e4; yrs = (t.x.max() - t.t.min()).days / 365.25
| |||
eq = np.cumprod(1 + RISK * t.sort_values("x").R.to_numpy()); pk = np.maximum.accumulate(np.r_[1, eq])[1:]
| |||
dd = ((pk - eq) / pk).max(); tot = eq[-1] - 1
| |||
return dict(n=len(t), pm=len(t) / max(yrs * 12, 1e-9), bp=bp.mean(), t=stats.ttest_1samp(bp, 0).statistic,
| |||
hit=(t.net > 0).mean(), tot=tot, dd=dd, rdd=tot / dd if dd > 1e-9 else np.nan, gross=t.gross.mean() * 1e4)
| |||
| |||
if __name__ == "__main__":
| |||
surv = []
| |||
print(f"{'pair':<16}{'n':>5}{'/mo':>5}{'gross':>7}{'net bp':>8}{'t':>6}{'IS bp':>8}{'OOS bp':>8}{'yrs+':>7}{'DD':>6}{'rdd':>6} verdict")
| |||
allt = {}
| |||
for a, b in PAIRS:
| |||
try: t = run(a, b)
| |||
except Exception as e: print(a, b, "ERR", e); continue
| |||
allt[(a, b)] = t; s = stats_of(t)
| |||
if not s: print(f"{a+'/'+b:<16} too few"); continue
| |||
is_ = t[t.t < "2017-01-01"].net.mean() * 1e4; oos = t[t.t >= "2017-01-01"].net.mean() * 1e4
| |||
yr = t.groupby(t.t.dt.year).net.agg(["mean", "count"]); yr = yr[yr["count"] >= 3]
| |||
frac = (yr["mean"] > 0).mean() if len(yr) else 0
| |||
ok = s["t"] >= 3 and is_ > 0 and oos > 0 and frac >= .6
| |||
if ok: surv.append((a, b))
| |||
print(f"{a+'/'+b:<16}{s['n']:>5}{s['pm']:>5.1f}{s['gross']:>7.1f}{s['bp']:>8.1f}{s['t']:>6.1f}{is_:>8.1f}{oos:>8.1f}{frac:>7.0%}{s['dd']:>6.1%}{s['rdd']:>6.1f} {'SURVIVES' if ok else 'rejected'}")
| |||
print("\nsurvivors:", surv)
| |||
if surv:
| |||
t = pd.concat([allt[p] for p in surv]); s = stats_of(t); print("pooled survivors:", {k: round(v, 3) for k, v in s.items()})
|