Warrior_EA/research/test_combined.py

156 lines
6.4 KiB
Python
Raw Permalink Normal View History

"""Past structure + current structure, combined - the books' actual operational scheme.
Two results make this the test worth running:
the CONTEXT score is real +0.046 R per agreeing trace, positive slope in 8/8 cells
the SHAKEOUT base is hopeless -0.33 at zero agreement, so context cannot rescue it
A modifier worth +0.05 R per trace only matters bolted to a trigger whose base expectancy
is already near zero. The LPS retest at the PRICE edge is the only trigger measured here
that qualifies: EURUSD H1 came in at -0.041, USDJPY H1 at -0.060. Three or four agreeing
traces would be worth +0.14 to +0.18 on top.
So: same LPS trade, same market order at the next bar's open, same 2R target - scored by
the same five Wyckoff traces, read off the range that produced the breakout. And the same
pre-specified test: expR must RISE MONOTONICALLY with agreement. The slope is the result;
the top bucket is not, because picking it is what mining looks like.
"""
import numpy as np, sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
from test_retail import load_bars, race_px
from test_cause_effect import atr_of, find_ranges
from test_context import trend_t
SYMS = ('EURUSD', 'USDJPY', 'XAUUSD', 'SP500')
def traces(h, l, c, vol, s, i, top, bot, dd, htf_sig):
"""The five traces of book 2 2.3 / 7.1, each +1 if it agrees with direction dd."""
mid = 0.5 * (top + bot)
Lq = i - s
th = max(Lq // 3, 2)
segA, segB = slice(s, s + th), slice(s + th, s + 2 * th)
segC = slice(s + 2 * th, i)
t1 = 1 if (h[segA].max() - mid) > (mid - l[segA].min()) else -1
t2 = 1 if (h[segB].max() - mid) > (mid - l[segB].min()) else -1
t3 = 0
if len(c[segC]):
if t2 > 0:
t3 = 1 if l[segC].min() > bot + 0.25 * (top - bot) else -1
else:
t3 = -1 if h[segC].max() < top - 0.25 * (top - bot) else 1
rr = max(h[i] - l[i], 1e-12)
clspos = (c[i] - l[i]) / rr if dd > 0 else (h[i] - c[i]) / rr
vavg = vol[s:i].mean() if i > s else vol[i]
t4 = 1 if (clspos > 0.6 and vol[i] > 1.2 * max(vavg, 1e-12)) else -1
t5 = 1 if htf_sig == dd else -1
return sum(1 for x in (t1 * dd, t2 * dd, t3 * dd, t4, t5) if x > 0)
def run(sym, tf='H1', theta=0.60, tol=0.35, wait=60, H=200, path_tf='M5', kR=2.0,
htf=200):
a1, I1 = load_bars(sym, tf)
g = lambda k: a1[:, I1[k]]
o, h, l, c = g('open'), g('high'), g('low'), g('close')
vol = g('ticks'); spm = g('spread_mean')
t1a = a1[:, I1['time']].astype(np.int64)
n = len(c)
atr = atr_of(h, l, c, 14); atr = np.concatenate([[atr[0]], atr[:-1]])
L, hi_, lo_ = find_ranges(h, l, c, atr, theta=theta)
a2, I2 = load_bars(sym, path_tf)
ph, pl, pc = a2[:, I2['high']], a2[:, I2['low']], a2[:, I2['close']]
pmap = np.searchsorted(a2[:, I2['time']], t1a)
step = 12 if tf == 'H1' else (3 if tf == 'M15' else 1)
HH = H * step
up = (L > 0) & (c > hi_); dn = (L > 0) & (c < lo_)
fire = np.nonzero(up | dn)[0]
fire = fire[(fire > max(300, htf + 5)) & (fire < n - wait - 5)]
if not len(fire):
return None
dirs = np.where(up[fire], 1, -1)
rows, busy = [], -1
for q in range(len(fire)):
i = int(fire[q])
if i <= busy:
continue
dd = int(dirs[q]); Lq = int(L[i])
s = i - Lq
if s < 1:
continue
top, bot = hi_[i], lo_[i]
lvl = top if dd > 0 else bot
#--- the retest: back to within tol*ATR of the broken edge, closing beyond it
j = -1
for k in range(1, wait + 1):
b_ = i + k
if b_ >= n - 2:
break
near = (l[b_] <= lvl + tol * atr[i]) if dd > 0 else (h[b_] >= lvl - tol * atr[i])
if near and ((c[b_] > lvl) if dd > 0 else (c[b_] < lvl)):
j = b_; break
if (c[b_] < lvl - tol * atr[i]) if dd > 0 else (c[b_] > lvl + tol * atr[i]):
break
if j < 0:
continue
e = j + 1
if e >= n - 1:
continue
pi = int(pmap[e])
if pi + HH >= len(ph):
continue
ent = o[e]
ext = l[j] if dd > 0 else h[j]
stop = ext - dd * 0.10 * atr[i]
risk = abs(ent - stop)
if risk <= 2 * spm[e]:
continue
ag = traces(h, l, c, vol, s, i, top, bot, dd, int(np.sign(c[i] - c[i - htf])))
rows.append((e, pi, dd, ent, stop, risk, spm[e], ag))
busy = e + Lq
if len(rows) < 100:
return None
A = lambda k: np.array([r[k] for r in rows])
E, P, D, EN, ST, RK, SP, AG = (A(k) for k in range(8))
r = race_px(ph, pl, P, D, ST, EN + D * kR * RK, HH)
R = np.where(r > 0, kR, np.where(r < 0, -1.0, 0.0))
un = r == 0
if un.any():
qq = np.minimum(P[un] + HH, len(pc) - 1)
R[un] = (pc[qq] - EN[un]) * D[un] / RK[un]
return R - SP / RK, AG, t1a[E]
if __name__ == '__main__':
syms = [s for s in sys.argv[1:] if s in SYMS] or list(SYMS)
print("=== LPS RETEST x WYCKOFF CONTEXT ===")
print(" the one trigger with a base near zero, scored by the five traces.\n")
print(f" {'symbol':>7}{'tf':>5}{'n':>6} " +
"".join(f"{k:>10}" for k in range(6)) + f"{'slope':>8}{'t':>7}")
PR, PA, PT = [], [], []
for tf in ('M15', 'H1'):
for s in syms:
out = run(s, tf)
if out is None:
print(f" {s:>7}{tf:>5} - too few"); continue
R, AG, T = out
cells = [f"{R[AG==k].mean():+6.2f}({int((AG==k).sum()):>3})"
if (AG == k).sum() >= 25 else f"{'-':>10}" for k in range(6)]
sl, tt = trend_t(R, AG)
print(f" {s:>7}{tf:>5}{len(R):>6} " + "".join(f"{x:>10}" for x in cells)
+ f"{sl:>+8.3f}{tt:>+7.2f}")
PR.append(R); PA.append(AG); PT.append(T)
if PR:
R = np.concatenate(PR); AG = np.concatenate(PA); T = np.concatenate(PT)
sl, tt = trend_t(R, AG)
print(f"\n POOLED n={len(R):,} slope {sl:+.4f} R/trace t {tt:+.2f}")
for k in range(6):
m = AG == k
if m.sum() >= 25:
se = R[m].std(ddof=1) / np.sqrt(m.sum())
o = np.argsort(T[m])
f = [float(x.mean()) for x in np.array_split(R[m][o], 4)]
print(f" {k} agree: n={int(m.sum()):>5} expR {R[m].mean():+7.3f}"
f" +/- {se:.3f} folds " + "".join(f"{x:+7.2f}" for x in f)
+ f" {sum(1 for x in f if x>0)}/4")