Warrior_EA/research/wf.py

52 lines
2.8 KiB
Python
Raw Permalink Normal View History

research: offline validation kit, and the answer it produced Keeps the Python that turned a per-hypothesis cost of minutes into seconds, so the next person (or the next me) can re-run any of this without MetaTrader in the loop. kit.py vectorised triple-barrier labeller + scale-free feature set. The labeller is a faithful port - stop tested before target within a bar, so a bar spanning both scores as the loss - and reproduces the EA's own distribution to 0.05pp (24.93/22.01/53.06 vs 24.9/22.0/53.1) at 12x the speed. wf.py purged, embargoed walk-forward gradient boosting. sim.py sequential NON-OVERLAPPING trade simulation. sweep.py the cell x geometry sweep. detail.py full threshold profile for one configuration. WHAT IT FOUND, and the order matters because the first answer was wrong: Naive walk-forward looked like an edge - precision rising monotonically with model confidence, 24.4/24.6/25.3/26.2/27.1%, topping out at +2.66pp and 2.9 sigma. All of it pseudo-replication: a 128-bar barrier means adjacent bars share almost their whole outcome window, so one trade was being counted up to 128 times. Counting each trade ONCE (sim.py) the ordering collapses to 25.9/31.1/26.8/28.1/25.8 and nothing is significant. Same error family as the MI null that assumed independence. That exposed a structural problem bigger than the result: at a 128-bar horizon, 18 years of SP500 H1 yields at most ~300 independent trades, which can only resolve a +6pp edge at 2 sigma. Real edges are 1-3pp. The shipped configuration is not merely unproven - it is statistically UNFALSIFIABLE on the available history. The cost/power screen then showed SP500 is among the worst cells available: 54k bars and spread/ATR 0.34, against EURUSD/USDJPY at 178k bars and 0.05. Weeks of training went into the hardest instrument on the list, 3x less data and 7x the relative cost. Final sweep - 4 instruments x 3 geometries, purged walk-forward, independent trades: nothing clears +2 sigma. The one survivor (EURUSD 2:3 h48, +2.76pp at +1.57 sigma) dissolves under its full threshold profile: non-monotone across thresholds, and its per-fold win rate decays monotonically through time (52.9 -> 45.9 -> 41.7 -> 35.2 -> 26.2). Conclusion: with price/volume-derived technical features there is no tradeable entry-direction edge on these instruments - now tested with a model class that finds interactions, on 3x the data, with honest statistics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:21:44 -04:00
import numpy as np, sys, datetime as dt
sys.stdout.reconfigure(encoding='utf-8',errors='replace')
from sklearn.ensemble import HistGradientBoostingClassifier
rng=np.random.default_rng(0)
def walk_forward(X, y, valid, horizon, n_folds=6, name="", thresholds=(0.0,0.40,0.45,0.50,0.55)):
"""Purged + embargoed walk-forward. Test blocks are contiguous and separated from the
training data by `horizon` bars on BOTH sides, because a triple-barrier label at bar j
depends on bars up to j+horizon - without that gap the training set contains the test
set's outcomes and every score is fiction."""
n=len(y); idx=np.arange(n)
bounds=np.linspace(int(n*0.3), n, n_folds+1).astype(int)
out={t:[[],[]] for t in thresholds} # thr -> [hits, calls]
covs=[]
for f in range(n_folds):
te0,te1=bounds[f],bounds[f+1]
te=(idx>=te0)&(idx<te1)&valid
tr=(idx < te0-horizon)&valid # strictly past, purged by one horizon
if tr.sum()<5000 or te.sum()<500: continue
m=HistGradientBoostingClassifier(max_iter=300,learning_rate=0.06,max_depth=6,
l2_regularization=1.0,random_state=0)
m.fit(X[tr],y[tr])
p=m.predict_proba(X[te]); yt=y[te]
for thr in thresholds:
pred=np.argmax(p,axis=1); conf=np.max(p,axis=1)
sel=(pred!=2)&(conf>=thr) # directional calls only
out[thr][1].append(int(sel.sum()))
out[thr][0].append(int((pred[sel]==yt[sel]).sum()))
covs.append(te.sum())
print(f"\n=== {name} === folds={len(covs)} test bars={sum(covs)}")
base=100*np.mean(y[valid]!=2)
print(f" base rates: Buy {100*np.mean(y[valid]==0):.1f}% Sell {100*np.mean(y[valid]==1):.1f}% directional {base:.1f}%")
for thr in thresholds:
hits=sum(out[thr][0]); calls=sum(out[thr][1])
if calls==0: print(f" conf>={thr:.2f}: no calls"); continue
prec=100*hits/calls
se=100*np.sqrt(0.25*0.75/calls)
print(f" conf>={thr:.2f}: {calls:6d} calls ({100*calls/sum(covs):5.1f}% of bars) precision {prec:5.2f}% "
f"vs 25.00% break-even edge {prec-25:+5.2f}pp ({(prec-25)/se:+.1f} sigma)")
return
F=np.load("F.npy"); meta=np.load("meta.npy"); y=np.load("lab.npy"); valid=np.load("valid.npy")
t=meta[:,0]
print("shapes",F.shape,y.shape,"valid",int(valid.sum()))
walk_forward(F,y,valid,128,name="26 EA features, 2:6 barrier, h=128")
# time-of-day / day-of-week: free, and the only inputs here that are NOT a transform of OHLCV
hrs=np.array([dt.datetime.fromtimestamp(x,dt.UTC).hour for x in t],dtype=float)
dow=np.array([dt.datetime.fromtimestamp(x,dt.UTC).weekday() for x in t],dtype=float)
Xt=np.column_stack([F,np.sin(2*np.pi*hrs/24),np.cos(2*np.pi*hrs/24),dow])
walk_forward(Xt,y,valid,128,name="26 features + hour/day-of-week")
np.save("hrs.npy",hrs); np.save("dow.npy",dow)