Warrior_EA/research/test_sweep_detail.py

121 lines
5.2 KiB
Python
Raw Permalink Normal View History

research: the stop-run edge fails its own mechanism test - disconfirmed The statistics had cleared a family-wise bar (z to +6.56) and a split-half. Both necessary, neither sufficient: thorough enough mining passes both. What mining cannot do is obey a mechanism it was never fitted to - so the decisive test is whether the effect appears WHERE THE THEORY SAYS IT MUST. Osler's stop-clustering predicts the edge concentrates where the stop reservoir is deepest and fresh forced orders arrive: London open and the London/NY overlap. Measured, by session in real UTC (broker is UTC+2): EURUSD Sydney/Tokyo +0.197* London open -0.063 London -0.156 USDJPY Rollover +0.233 London open -0.223 XAUUSD Sydney/Tokyo +0.070 London open -0.345 LDN/NY -0.185 SP500 Rollover +0.206 NY afternoon -0.098 Exactly inverted. The liquid sessions where stops actually cluster are the worst on every instrument; what remains lives in Sydney/Tokyo and rollover - the THINNEST hours, where fewest stops sit. That is not the mechanism, and thin hours are also where spreads are widest and fills worst, so even the surviving fragment points away from tradeability rather than toward it. Walk-forward by quarter agrees, and shows what the two-way split was hiding: EURUSD 2/4 positive (Q1 -0.029, Q2 +0.062, Q3 -0.010, Q4 +0.089) USDJPY 1/4 XAUUSD 0/4 SP500 2/4 No instrument reaches 3/4. The split-half HOLDS was Q2+Q4 carrying Q1+Q3. Verdict: not an edge. Recording it as disconfirmed rather than leaving an encouraging half-result in the log, because the next person to read this - me - would otherwise build on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:11:33 -04:00
"""Does the stop-run edge behave the way its MECHANISM says it should?
The statistics already cleared a family-wise bar and a split-half. That is necessary and
not sufficient - a mined artifact can do both if the mining was thorough enough. What a
mined artifact CANNOT do is obey a mechanism it was never fitted to.
If the edge is really stop-cluster liquidity, then it must be concentrated where the stop
reservoir is deepest and where fresh forced orders arrive:
- around SESSION OPENS, and the London/NY handover
- NOT uniform around the clock
A uniform effect would be evidence AGAINST the mechanism even with the same p-value, and
would make the whole thing much likelier to be curve-fit. This is the test that can
disconfirm.
Also here: walk-forward by quarters rather than halves (a 2-way split can hide a single
lucky regime), and the other two instruments.
Broker time is UTC+2 (measured, see dukas.py notes) - every hour below is converted to UTC
so the session labels mean what they say.
"""
import numpy as np, sys, datetime as dt
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
from test_liquidity import load, atr_of, rolling_extreme, outcomes
BROKER_OFFSET_H = 2 # SQX conforms to the5ers: broker = UTC + 2
def build(sym, tf='H1', N=50, ov=0.5, R=3.0, H=48):
a, I = load(sym, tf)
g = lambda c: a[:, I[c]]
o, h, l, c = g('open'), g('high'), g('low'), g('close')
spb = g('spread_mean'); spb = np.concatenate([[spb[0]], spb[:-1]])
atr = atr_of(h, l, c, 14); atr = np.concatenate([[atr[0]], atr[:-1]])
n = len(c)
ph = rolling_extreme(h, N, 'max'); pl = rolling_extreme(l, N, 'min')
sh = (h > ph) & ((h - ph) <= ov * atr) & (c < ph) & np.isfinite(ph)
sl = (l < pl) & ((pl - l) <= ov * atr) & (c > pl) & np.isfinite(pl)
fire = np.nonzero(sh | sl)[0]
fire = fire[(fire > N + 2) & (fire < n - H - 2)]
d = np.where(sh[fire], -1, 1)
e = fire + 1
buf = 0.05 * atr[fire]
stop = np.where(d < 0, h[fire] + buf, l[fire] - buf)
ent = o[e]
risk = np.abs(ent - stop) + spb[e]
keep, busy = [], -1
for k in range(len(e)):
if risk[k] <= 0 or e[k] <= busy:
continue
keep.append(k); busy = e[k] + H
keep = np.array(keep, int)
targ = np.where(d < 0, ent - R * risk, ent + R * risk)
w, to = outcomes(o, h, l, e[keep], d[keep],
np.where(d[keep] < 0, stop[keep] + spb[e[keep]],
stop[keep] - spb[e[keep]]),
targ[keep], H)
t = a[:, I['time']][e[keep]]
utc_h = np.array([(dt.datetime.fromtimestamp(x / 1000, dt.UTC).hour - BROKER_OFFSET_H) % 24
for x in t])
dow = np.array([dt.datetime.fromtimestamp(x / 1000, dt.UTC).weekday() for x in t])
return w, d[keep], utc_h, dow, t, R
def expR(w, R):
return w.mean() * R - (1 - w.mean()) if len(w) else 0.0
def boot_ci(w, R, n=4000, seed=3):
"""Bootstrap CI on expected R. Trades are non-overlapping so a plain resample is fine."""
if len(w) < 20:
return (np.nan, np.nan)
rng = np.random.default_rng(seed)
idx = rng.integers(0, len(w), (n, len(w)))
m = w[idx].mean(axis=1)
e = m * R - (1 - m)
return float(np.quantile(e, 0.05)), float(np.quantile(e, 0.95))
if __name__ == '__main__':
SESSIONS = [('Sydney/Tokyo', lambda h: (h >= 0) & (h < 7)),
('London open', lambda h: (h >= 7) & (h < 10)),
('London', lambda h: (h >= 10) & (h < 12)),
('LDN/NY overlap', lambda h: (h >= 12) & (h < 16)),
('NY afternoon', lambda h: (h >= 16) & (h < 21)),
('Rollover/thin', lambda h: (h >= 21) | (h < 0))]
for sym in ('EURUSD', 'USDJPY', 'XAUUSD', 'SP500'):
try:
w, d, uh, dow, t, R = build(sym)
except Exception as ex:
print(f"{sym}: {ex}")
continue
print(f"\n{'='*76}\n{sym} H1 N=50 ov=0.5 R=3 {len(w)} trades "
f"overall expR {expR(w,R):+.3f}\n{'='*76}")
print(" --- by SESSION (UTC) - the mechanism test -------------------------")
print(f" {'session':<16}{'n':>6}{'win%':>8}{'expR':>9}{'90% CI':>20}")
for name, f in SESSIONS:
m = f(uh)
if m.sum() < 30:
continue
lo, hi = boot_ci(w[m], R)
print(f" {name:<16}{int(m.sum()):>6}{100*w[m].mean():>8.2f}{expR(w[m],R):>+9.3f}"
f" [{lo:+.3f}, {hi:+.3f}]{' *' if lo > 0 else ''}")
print(" --- WALK-FORWARD by quarter of the sample -------------------------")
q = np.array_split(np.arange(len(w)), 4)
line = " "
for i, ix in enumerate(q):
line += f"Q{i+1} {expR(w[ix],R):+.3f} (n={len(ix)}) "
print(line)
pos = sum(1 for ix in q if expR(w[ix], R) > 0)
print(f" quarters positive: {pos}/4")
print(" --- by DIRECTION --------------------------------------------------")
for lbl, m in (('fade high sweep (short)', d < 0), ('fade low sweep (long)', d > 0)):
if m.sum() < 30:
continue
lo, hi = boot_ci(w[m], R)
print(f" {lbl:<24}{int(m.sum()):>6}{100*w[m].mean():>8.2f}"
f"{expR(w[m],R):>+9.3f} [{lo:+.3f}, {hi:+.3f}]")