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