Warrior_EA/research/test_lps2.py

136 行
6 KiB
Python

"""Attribution: did the honest engine kill the context finding, or did I change the trade?
`test_context2.py` found no dose-response (pooled slope -0.025, t -0.96) where the original
run found +0.0425 at t +3.18. But it changed two things at once - the fill engine AND the
trade - so the failure cannot yet be pinned on either. This isolates them by running the
ORIGINAL LPS configuration, unchanged, on the new engine:
breakout close beyond a qualified range edge at bar i
retest first bar j within tol*ATR of the broken edge that still closes beyond it,
abandoned if price closes back through the edge (wait up to 60 bars)
entry MARKET at the open of bar j+1 <- no level, so no fill artifact ever
stop the retest bar's own extreme, 0.10 ATR beyond
target entry + 2 * risk
That is exactly what produced +0.0425. The only difference is that the outcome now races on
the M1 bid/ask book instead of M5 mid bars with an average spread bolted on.
slope stays near +0.04 -> the engine is fine and my shallow-limit variant broke it
slope collapses -> the original finding depended on the old harness
Both are worth knowing and only this comparison can tell them apart.
"""
import numpy as np, sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
import fills, book, wyckoff
SYMS = ('EURUSD', 'USDJPY', 'XAUUSD', 'SP500')
def events(sym, tf, theta=0.60, tol=0.35, wait=60, H=200, kR=2.0, htf=200,
bk=None, f=None):
"""The original LPS trade, market-entered, on the M1 bid/ask book."""
bk = bk or fills.Book(sym)
f = f or book.frame(sym, tf, bk)
step = book.TF_SEC[tf] // 60
h, l, c = f.h, f.l, f.c
n = f.n
atr = f.atr(14)
L, hi_, lo_ = book.find_ranges(h, l, atr, theta=theta)
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
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
ext = l[j] if dd > 0 else h[j]
stop = ext - dd * 0.10 * atr[i]
ag = wyckoff.traces(f, s, i, top, bot, dd, int(np.sign(c[i] - c[i - htf])))
rows.append((e, dd, stop, ag, Lq))
busy = e + Lq
if len(rows) < 100:
return None
E = np.array([r[0] for r in rows]); D = np.array([r[1] for r in rows])
ST = np.array([r[2] for r in rows], float); AG = np.array([r[3] for r in rows])
start = f.i0[E]
#--- entry price is not known until the fill, so the target must be built from it;
#--- run once to get the fill, then set the target at kR x the realised risk
ent = np.where(D > 0, bk.ao[start], bk.bo[start])
risk = np.abs(ent - ST)
ok = risk > 4 * f.spread[E]
if ok.sum() < 100:
return None
E, D, ST, AG, start, ent, risk = (v[ok] for v in (E, D, ST, AG, start, ent, risk))
out = fills.simulate(bk, start, D, ST, ent + D * kR * risk, H * step, entry=fills.MARKET)
if out is None:
return None
sel = np.nonzero(out['filled'])[0][out['kept']]
ind = book.nonoverlap(out['idx'], out['exit_idx'] - out['idx'])
return dict(R=out['R'][ind], ag=AG[sel][ind], t=bk.t[out['idx'][ind]],
n=int(ind.sum()), amb=out['ambiguous'], unres=out['unresolved'])
if __name__ == '__main__':
syms = [s for s in sys.argv[1:] if s in SYMS] or list(SYMS)
print("=== ORIGINAL LPS CONFIG, NEW ENGINE - attribution run ===")
print(" market entry at the next bar's open, stop at the retest extreme, 2R target.")
print(" the old harness gave slope +0.0425 R/trace at t +3.18.\n")
print(f" {'sym':>7}{'tf':>5}{'n':>6} " + "".join(f"{k:>12}" for k in range(6))
+ f"{'slope':>9}{'t':>7}{'expR':>9}")
PR, PA, PT, cells = [], [], [], []
for tf in ('M15', 'H1'):
for s in syms:
a = events(s, tf)
if a is None:
print(f" {s:>7}{tf:>5} - too few"); continue
R, ag = a['R'], a['ag']
txt = [f"{R[ag==k].mean():+6.3f}({int((ag==k).sum()):>4})"
if (ag == k).sum() >= 25 else f"{'-':>12}" for k in range(6)]
sl, tt = book.slope_t(R, ag)
cells.append(sl)
print(f" {s:>7}{tf:>5}{a['n']:>6} " + "".join(txt)
+ f"{sl:>+9.4f}{tt:>+7.2f}{R.mean():>+9.4f}")
PR.append(R); PA.append(ag); PT.append(a['t'])
if PR:
R = np.concatenate(PR); ag = np.concatenate(PA); T = np.concatenate(PT)
sl, tt = book.slope_t(R, ag)
print(f"\n POOLED n={len(R):,} base expR {R.mean():+.4f}"
f" slope {sl:+.4f} R/trace t {tt:+.2f}"
f" positive-slope cells {sum(1 for x in cells if x>0)}/{len(cells)}")
for k in range(6):
m = ag == k
if m.sum() < 25:
continue
se = R[m].std(ddof=1) / np.sqrt(m.sum())
o = np.argsort(T[m])
q = [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.4f}"
f" +/- {se:.4f} quarters " + "".join(f"{x:>+8.3f}" for x in q)
+ f" {sum(1 for x in q if x>0)}/4")