Applies the same five-trace context score from c998d65 to a completely
different entry - the LPS retest instead of the shakeout - as a check that the
dose-response was not specific to one setup.
shakeout trigger slope +0.0464 R/trace t +2.16 8/8 cells positive
LPS trigger slope +0.0425 R/trace t +3.18 7/8 cells positive
Two unrelated triggers, 31,000 non-overlapping trades, same magnitude to within
10%, same sign in 15 of 16 cells. This is the strongest confirmed result in the
whole programme: reading past structure to qualify current structure has real,
measurable predictive content. Villahermosa is describing something that exists.
AND IT IS STILL NOT ENOUGH. The buckets:
1 agree -0.287 3 agree -0.151
2 agree -0.206 4 agree -0.157
Context is a MODIFIER worth ~+0.045 R per agreeing trace. Every trigger it can
modify starts between -0.15 and -0.33, and only five traces exist, so full
confluence still lands short of break-even. Reaching zero would need ~7.
One cell goes positive - EURUSD H1 at 4 agreeing traces, +0.14 on n=133 - and it
is 1 of 40, on the smallest bucket, in the instrument with the lowest costs. That
is the exact shape of the three artifacts already retracted today, so it is
recorded as noise unless a fill-correct simulator says otherwise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
6.4 KiB
Python
156 lines
6.4 KiB
Python
"""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")
|