Transcribes all 26 classic vote models (MA 4, RSI 4, MACD 6, Ichimoku 12) from
Signals/*.mqh into vectorised Python, with their shipped constructor weights, then tests
them as entry triggers on 178k-bar FX histories.
Pre-registered by construction: the rules were written long before this test and nothing
about them is fitted here, so there is no in-sample/out-of-sample split to draw and the
whole history is usable. Break-even == chance by the gambler's-ruin identity, so "beats a
coin" and "makes money" are one question. Sequential non-overlapping trades only; a
sign-flip null over the whole pattern family gives the family-wise bar.
The result that matters is a negative one, and it took two lookahead fixes to see:
- _price_extremum reproduced the standard library's CENTRED MinValue(pos-2,5) window,
which reads up to 2 bars newer than the extremum it describes.
- turning_points marked a turn AT bar i, which is only knowable once bar i+1 closes.
Together those two bars of leakage WERE the entire apparent edge. MACD_p4 on EURUSD 1:2
read +5.05pp at +4.05 sigma before, -0.02pp at -0.02 sigma after; USDJPY 1:2 went +5.24pp
-> +0.02pp. RSI_p2's large NEGATIVE went the same way (-10.33pp -> -1.34pp), which is the
tell: a leak inflates whatever sign it lands on.
With both closed, across 4 instruments x 3 geometries: no pattern, no vote threshold, no
quorum and no event+confirmation rule separates from chance. One cell in ~180 tests stars
(SP500 2:6 vote>=30) and it is non-monotone in the threshold either side of the hit.
test_exits.py answers the trade-management half with the control that makes it mean
something: hold entries fixed, vary only the exit, and run every rule again on RANDOM
entries at the same bars. Breakeven-at-1R, chandelier trails, partials and time stops all
move E[R] - and move it by the same amount on random entries. No rule beats its own
control (max +0.99 sigma over 32 comparisons). Management reshapes the win-rate/payoff
split; it does not manufacture expectancy from a directionless entry.
Residual E[R] across every cell is -0.01 to -0.08 R, which is approximately the spread.
Incidental, both worth fixing in the EA:
- CSignalMA pattern 1 is unsatisfiable at the shipped EMA default. For an EMA,
MA[i]-MA[i-1] and c[i]-MA[i] are both positive multiples of (c[i]-MA[i-1]), so
"close below the MA while the MA rises" cannot occur. Dead code (weight 10).
- Ichimoku pattern 11 (Sanyaku, weight 100, the method's top signal) fires on 27% of
bars because it is a conjunction of three standing STATES with no event term, so it
dominates the averaged vote while carrying no trigger information.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
4.4 KiB
Python
109 lines
4.4 KiB
Python
"""Confluence, not lone patterns.
|
|
|
|
The user's framing, and the EA's own design: weight-10 models are CONFIRMING states that
|
|
must never be acted on alone; a trade needs an event model plus agreement. So this tests
|
|
the aggregate, three ways:
|
|
|
|
vote>=T CExpertSignalCustom::Direction() - each module casts its highest-numbered
|
|
matching pattern's weight, signed; the non-zero module votes are AVERAGED;
|
|
trade when |average| >= T. This is literally what the shipped EA does.
|
|
quorum>=K at least K of the 4 modules agree on a direction (any strength).
|
|
event+cfm an ACTIONABLE model (weight > 10) fires, AND at least K confirming models
|
|
(weight == 10) agree with it - the doctrine the weights encode.
|
|
|
|
Same discipline as test_classic.py: sequential non-overlapping trades, break-even ==
|
|
chance by the gambler's-ruin identity, family-wise 5% bar from a sign-flip null.
|
|
"""
|
|
import numpy as np, sys, time
|
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
from classic import module_votes, combined_direction, MODULE_SLICES
|
|
from test_classic import prepare, barrier_outcomes, sequential, TF
|
|
|
|
|
|
def evaluate(label, fire_bars, dirs, winL, winS, H, n, be, rng, nperm, acc):
|
|
ti, td, tw = sequential(fire_bars, dirs, winL, winS, H, n)
|
|
nT = len(ti)
|
|
if nT < 30:
|
|
return None
|
|
wr = tw.mean()
|
|
se = np.sqrt(be * (1 - be) / nT)
|
|
z = (wr - be) / se
|
|
wl_at, ws_at = winL[ti], winS[ti]
|
|
fl = rng.random((nperm, nT)) < 0.5
|
|
pz = (np.where(fl, wl_at[None, :], ws_at[None, :]).mean(axis=1) - be) / se
|
|
acc.append(np.abs(pz))
|
|
return (label, len(fire_bars), nT, 100 * wr, 100 * (wr - be), z)
|
|
|
|
|
|
def run(sym, tf, sl_m, tp_m, H, nperm=2000, seed=1):
|
|
o, h, l, c, a, sp, fireL, fireS, names, weights = prepare(sym, tf)
|
|
n = len(c)
|
|
a_sig = np.concatenate([[a[0]], a[:-1]])
|
|
winL, resL, winS, resS = barrier_outcomes(o, h, l, a_sig, sl_m, tp_m, H, sp)
|
|
be = sl_m / (sl_m + tp_m)
|
|
rng = np.random.default_rng(seed)
|
|
rows, acc = [], []
|
|
|
|
votes = module_votes(fireL, fireS, weights)
|
|
direction = combined_direction(votes)
|
|
V = np.column_stack([votes[m] for m in MODULE_SLICES])
|
|
|
|
# --- 1. the EA's own averaged weighted vote
|
|
for T in (10, 20, 30, 40, 50, 60, 70):
|
|
fb = np.nonzero(np.abs(direction) >= T)[0]
|
|
if len(fb) == 0:
|
|
continue
|
|
r = evaluate(f"vote>={T}", fb, np.sign(direction[fb]).astype(int),
|
|
winL, winS, H, n, be, rng, nperm, acc)
|
|
if r:
|
|
rows.append(r)
|
|
|
|
# --- 2. quorum: K of 4 modules agree, no strength requirement
|
|
agree_long = (V > 0).sum(axis=1)
|
|
agree_short = (V < 0).sum(axis=1)
|
|
for K in (2, 3, 4):
|
|
m = ((agree_long >= K) & (agree_short == 0)) | ((agree_short >= K) & (agree_long == 0))
|
|
fb = np.nonzero(m)[0]
|
|
if len(fb) == 0:
|
|
continue
|
|
d = np.where(agree_long[fb] >= K, 1, -1)
|
|
r = evaluate(f"quorum>={K}", fb, d, winL, winS, H, n, be, rng, nperm, acc)
|
|
if r:
|
|
rows.append(r)
|
|
|
|
# --- 3. an actionable event, corroborated by K confirming states
|
|
act = np.array([w > 10 for w in weights])
|
|
cfm = ~act
|
|
evL, evS = fireL[:, act].any(axis=1), fireS[:, act].any(axis=1)
|
|
ncL, ncS = fireL[:, cfm].sum(axis=1), fireS[:, cfm].sum(axis=1)
|
|
for K in (1, 2, 3, 4, 5):
|
|
mL = evL & (ncL >= K) & ~evS
|
|
mS = evS & (ncS >= K) & ~evL
|
|
fb = np.nonzero(mL | mS)[0]
|
|
if len(fb) == 0:
|
|
continue
|
|
d = np.where(mL[fb], 1, -1)
|
|
r = evaluate(f"event+{K}cfm", fb, d, winL, winS, H, n, be, rng, nperm, acc)
|
|
if r:
|
|
rows.append(r)
|
|
|
|
if not rows:
|
|
return
|
|
crit = np.quantile(np.maximum.reduce(acc), 0.95)
|
|
print(f"\n=== {sym} {TF.get(tf,tf)} SL{sl_m}:TP{tp_m} H={H} "
|
|
f"break-even={100*be:.2f}% (family-wise 5% bar |z|>{crit:.2f}) ===")
|
|
print(f"{'rule':<14}{'fires':>9}{'trades':>8}{'win%':>8}{'edge pp':>9}{'z':>7}")
|
|
for r in sorted(rows, key=lambda x: -x[5]):
|
|
star = ' *' if abs(r[5]) > crit else ''
|
|
print(f"{r[0]:<14}{r[1]:>9}{r[2]:>8}{r[3]:>8.2f}{r[4]:>+9.2f}{r[5]:>+7.2f}{star}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
t0 = time.time()
|
|
for sym in ('EURUSD', 'USDJPY', 'XAUUSD', 'SP500'):
|
|
for (s, p, H) in [(2, 3, 96), (2, 6, 192), (1, 2, 64)]:
|
|
try:
|
|
run(sym, 16385, s, p, H)
|
|
except Exception as ex:
|
|
print(f"{sym} {s}:{p} FAILED {ex}")
|
|
print(f"\ntotal {time.time()-t0:.0f}s")
|