Warrior_EA/research/test_lps2.py
AnimateDread f1b7dcf7f3 fix: correct MI sample alignment and improve BN weight diagnostic report
The MI sample builder used `MathAbs(labelBarOffset)` as a padding, causing rows from offset and non-offset builds to be paired with a double shift. This broke the positive control, failed the 5× gate, and voided all reported mutual‑information figures. Replace with the fixed `MiShiftPad` constant to ensure builds enumerate the same set of bars and row-k alignment is preserved.

Add `BatchOptionsTotal()` to `CNeuronBatchNormOCL` and split the packed BN weight array in the learning report into separate norms for the outgoing dense matrix, gamma, beta, running statistics, and Adam moment buffers. This turns an ambiguous single‑norm reading into precise diagnostics that distinguish weight divergence from scaling issues.
2026-08-02 08:12:47 -04:00

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