136 行
6 KiB
Python
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")
|