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>
76 lines
3.4 KiB
Python
76 lines
3.4 KiB
Python
import numpy as np, datetime as dt
|
|
R=r"C:/Users/admin/AppData/Roaming/MetaQuotes/Terminal/Common/Files/Warrior_EA/Research/"
|
|
|
|
def load_rates(sym,tf):
|
|
d=np.genfromtxt(R+f"{sym}_{tf}_rates.csv",delimiter=',',names=True)
|
|
return d['time'],d['open'],d['high'],d['low'],d['close'],d['tickvol'],d['spread']
|
|
|
|
def atr(h,l,c,n=14):
|
|
pc=np.roll(c,1); pc[0]=c[0]
|
|
tr=np.maximum(h-l,np.maximum(np.abs(h-pc),np.abs(l-pc)))
|
|
out=np.convolve(tr,np.ones(n)/n,mode='full')[:len(tr)]
|
|
out[:n]=tr[:n].mean() if n<len(tr) else tr.mean()
|
|
return out
|
|
|
|
def sma(x,n):
|
|
out=np.convolve(x,np.ones(n)/n,mode='full')[:len(x)]
|
|
out[:n]=x[:n].mean()
|
|
return out
|
|
|
|
def features(t,o,h,l,c,v):
|
|
"""Scale-free, causal features - everything divided by ATR or expressed as a ratio, so the
|
|
same model is comparable across instruments and across 18 years of price levels."""
|
|
a=atr(h,l,c,14); a=np.where(a>0,a,np.nan)
|
|
F=[]; names=[]
|
|
for k in (1,2,3,5,10,20,50):
|
|
r=(c-np.roll(c,k))/a; r[:k]=0; F.append(r); names.append(f"ret{k}")
|
|
for k in (10,20,50):
|
|
hh=np.array([h[max(0,i-k+1):i+1].max() for i in range(len(h))])
|
|
ll=np.array([l[max(0,i-k+1):i+1].min() for i in range(len(l))])
|
|
F.append(np.where(hh>ll,(c-ll)/(hh-ll),0.5)); names.append(f"donch{k}")
|
|
for k in (10,20,50):
|
|
F.append((c-sma(c,k))/a); names.append(f"sma{k}dist")
|
|
F.append(a/sma(a,50)); names.append("atrratio")
|
|
rng=np.where(h>l,h-l,np.nan)
|
|
F.append((c-o)/rng); names.append("body")
|
|
F.append((h-np.maximum(o,c))/rng); names.append("upwick")
|
|
F.append((np.minimum(o,c)-l)/rng); names.append("dnwick")
|
|
F.append(rng/a); names.append("rangeatr")
|
|
vm=sma(v,50); F.append(np.where(vm>0,v/vm,1.0)); names.append("volratio")
|
|
d=np.roll(c,1)-np.roll(c,2)
|
|
up=np.where(d>0,d,0); dn=np.where(d<0,-d,0)
|
|
rs=sma(up,14)/np.where(sma(dn,14)>0,sma(dn,14),np.nan)
|
|
F.append(100-100/(1+rs)); names.append("rsi14")
|
|
hh=np.array([dt.datetime.fromtimestamp(x,dt.UTC).hour for x in t],dtype=float)
|
|
dw=np.array([dt.datetime.fromtimestamp(x,dt.UTC).weekday() for x in t],dtype=float)
|
|
F.append(np.sin(2*np.pi*hh/24)); names.append("hsin")
|
|
F.append(np.cos(2*np.pi*hh/24)); names.append("hcos")
|
|
F.append(dw); names.append("dow")
|
|
X=np.column_stack(F)
|
|
return np.nan_to_num(X,nan=0.0,posinf=0.0,neginf=0.0),names,a
|
|
|
|
def barrier_vec(h,l,c,a,sl,tp,H,spread):
|
|
"""Vectorised triple-barrier. Stop is tested before target WITHIN a bar, so a bar spanning
|
|
both scores as the loss - implemented as strict tp_idx < sl_idx."""
|
|
n=len(c); INF=np.iinfo(np.int32).max
|
|
lab=np.full(n,2,dtype=np.int8); valid=np.zeros(n,bool)
|
|
risk=sl*a; rew=tp*a
|
|
lTp=c+spread+rew; lSl=c+spread-risk; sTp=c-rew-spread; sSl=c+risk-spread
|
|
CH=200000//max(H,1)+1
|
|
for s in range(0,n,CH):
|
|
e=min(s+CH,n); m=e-s
|
|
if e+H>n: e2=n-H
|
|
else: e2=e
|
|
if e2<=s: break
|
|
mm=e2-s
|
|
wi=np.arange(1,H+1)[None,:]+np.arange(s,e2)[:,None]
|
|
wh=h[wi]; wl=l[wi]
|
|
def first(mask):
|
|
any_=mask.any(axis=1); return np.where(any_,mask.argmax(axis=1),INF)
|
|
lsl=first(wl<=lSl[s:e2,None]); ltp=first(wh>=lTp[s:e2,None])
|
|
ssl=first(wh>=sSl[s:e2,None]); stp=first(wl<=sTp[s:e2,None])
|
|
lw=ltp<lsl; sw=stp<ssl
|
|
seg=np.full(mm,2,dtype=np.int8)
|
|
seg[lw&~sw]=0; seg[sw&~lw]=1
|
|
lab[s:e2]=seg; valid[s:e2]=np.isfinite(a[s:e2])&(a[s:e2]>0)
|
|
return lab,valid
|