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")
|