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>
36 lines
1.4 KiB
Python
36 lines
1.4 KiB
Python
import numpy as np
|
|
def barrier_labels(high, low, close, atr, sl_mult, tp_mult, horizon, spread):
|
|
"""Faithful port of CExpertSignalAIBase::TripleBarrierLabel.
|
|
Arrays are CHRONOLOGICAL (index 0 = oldest). Stop is tested before target on each
|
|
side, so a bar spanning both barriers scores as the loss. Returns 0=Buy 1=Sell 2=Neutral,
|
|
plus a timed-out mask (neither side resolved at all)."""
|
|
n = len(close)
|
|
lab = np.full(n, 2, dtype=np.int8)
|
|
timed = np.zeros(n, dtype=bool)
|
|
valid = np.zeros(n, dtype=bool)
|
|
for j in range(n):
|
|
a = atr[j]
|
|
if not np.isfinite(a) or a <= 0: continue
|
|
last = min(j + horizon, n - 1)
|
|
if last <= j: continue
|
|
e = close[j]; risk = sl_mult*a; rew = tp_mult*a
|
|
lTp = e + spread + rew; lSl = e + spread - risk
|
|
sTp = e - rew - spread; sSl = e + risk - spread
|
|
lW=lL=sW=sL=False
|
|
hs = high[j+1:last+1]; ls = low[j+1:last+1]
|
|
for k in range(len(hs)):
|
|
hi=hs[k]; lo=ls[k]
|
|
if not (lW or lL):
|
|
if lo <= lSl: lL=True
|
|
elif hi >= lTp: lW=True
|
|
if not (sW or sL):
|
|
if hi >= sSl: sL=True
|
|
elif lo <= sTp: sW=True
|
|
if (lW or lL) and (sW or sL): break
|
|
valid[j]=True
|
|
if lW and not sW: lab[j]=0
|
|
elif sW and not lW: lab[j]=1
|
|
else:
|
|
lab[j]=2
|
|
timed[j] = not (lW or lL or sW or sL)
|
|
return lab, timed, valid
|