Warrior_EA/research/test_swing_fade.py

115 行
5 KiB
Python

research: RETRACTION - the retail-fade edge was a fill-timing artifact The +0.097 R EURUSD result in c9b489e is wrong. So is the +0.108 R pooled edge in c2dd9eb and the selection model in 821f16d, which used the same labels. THE BUG. The mirror test entered at the retail trigger price e2 - a STOP order level - but started the outcome race at the OPEN of the fill bar. Price at that open is on the far side of e2 by construction; that is why the order is a stop order. So the race began before price had reached the entry, which handed the fade a free run toward its target and pushed its stop further away than it really was. Retail's side carries the same bias with the sign reversed, so the DIFFERENCE - which is exactly how 'edge' was computed - was inflated twice over. Found by generalising the trigger to swing-extreme breakouts, per the user's suggestion. That version returned +0.7 R at t +110, which is not a result, and it has the identical structure: enter at a level, measure from the bar open. WITH AN HONEST INTRABAR FILL (first M5 bar that actually trades at the entry), every EURUSD cell inverts: bar open honest fill H1 pin +0.0953 -0.0096 H1 pin +0.0559 -0.0573 H1 inside +0.0612 -0.0395 M15 pin +0.0407 -0.0135 The 4/4 walk-forward held because the bias was present in every fold. A walk-forward validates against regime change, not against a broken fill model. The tell was there and I walked past it: sweep_entry() was the ONE test that modelled the fill properly, and it was the ONE test that came out negative. When one arm of a suite disagrees with the rest, check what it does differently before believing the majority. So the standing conclusion returns to what it was: retail setups are close to a coin flip that pays the spread, and there is nothing in them to harvest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 23:17:55 -04:00
"""Generalise the trigger: fade breakouts of SWING extremes, not named candlestick patterns.
The user's point, and it is the right one: we do not need to encode every retail pattern.
Retail buys the break of a swing high and puts stops under the swing low, and Wyckoff
labels the same locations. If what the pin/inside fade is really capturing is "a stop order
filled at a local extreme reverts", then the extreme is the ingredient and the candlestick
name is decoration.
This tests exactly that, and it is the honest way to find out whether the earlier result was
a pattern or a location:
trigger price trades through the most recent CONFIRMED swing high/low
fade take the other side at that level (a limit order into their stop/breakout buying)
stop m x ATR - deliberately decoupled from any pattern's geometry
target 1 x risk
Confirmation lag is the thing to get right. A swing high at bar i is only known at bar i+N,
so the level may only be USED from i+N onward. Using it earlier is the classic fractal
lookahead and it would make any of this look wonderful.
"""
import numpy as np, sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
from test_retail import load_bars, race_px, PIP
from test_cause_effect import atr_of
SYMS = ('EURUSD', 'USDJPY', 'XAUUSD', 'SP500')
def swings(h, l, N):
"""Confirmed swing highs/lows. Returned as arrays holding, for each bar, the most recent
swing level that is ALREADY CONFIRMED at that bar (NaN until one exists)."""
n = len(h)
hi = np.full(n, np.nan); lo = np.full(n, np.nan)
W = np.lib.stride_tricks.sliding_window_view
if n < 2 * N + 2:
return hi, lo
wh = W(h, 2 * N + 1); wl = W(l, 2 * N + 1)
is_hi = wh.argmax(axis=1) == N # centre bar is the max of its window
is_lo = wl.argmin(axis=1) == N
cur_h = np.nan; cur_l = np.nan
for i in range(n):
#--- a pivot centred at c is confirmed at c+N; index into the window arrays
c = i - N
k = c - N
if 0 <= k < len(is_hi):
if is_hi[k]:
cur_h = h[c]
if is_lo[k]:
cur_l = l[c]
hi[i] = cur_h; lo[i] = cur_l
return hi, lo
def run(sym, tf='H1', Ns=(3, 5, 8), ms=(0.25, 0.5, 1.0), H=200, path_tf='M5'):
a1, I1 = load_bars(sym, tf)
g = lambda k: a1[:, I1[k]]
o, h, l, c = g('open'), g('high'), g('low'), g('close')
spm = g('spread_mean')
t1 = a1[:, I1['time']].astype(np.int64)
atr = atr_of(h, l, c, 14); atr = np.concatenate([[atr[0]], atr[:-1]])
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']], t1)
HH = H * (12 if tf == 'H1' else 3)
out = []
for N in Ns:
shi, slo = swings(h, l, N)
for d, lvl in ((-1, shi), (+1, slo)): # -1 = fade an upside break
#--- break happens on bar i if the level was confirmed BEFORE i and price
#--- trades through it during i
prev = np.concatenate([[np.nan], lvl[:-1]])
brk = (np.isfinite(prev) &
(h > prev if d < 0 else l < prev) &
(np.concatenate([[np.nan], h[:-1]]) <= prev if d < 0
else np.concatenate([[np.nan], l[:-1]]) >= prev))
idx = np.nonzero(brk)[0]
idx = idx[(idx > 2 * N + 20) & (idx < len(c) - 5)]
if len(idx) < 300:
continue
entry = prev[idx]
pi = np.clip(pmap[idx], 0, len(ph) - 1)
keep = pi + HH < len(ph)
idx, entry, pi = idx[keep], entry[keep], pi[keep]
if len(idx) < 300:
continue
j = np.maximum(idx - 1, 0)
sp = spm[j]
dd = np.full(len(idx), d)
for m in ms:
R0 = np.maximum(m * atr[j], 2 * sp)
r = race_px(ph, pl, pi, dd, entry - dd * R0, entry + dd * R0, HH)
R = np.where(r > 0, 1.0, np.where(r < 0, -1.0, 0.0))
un = r == 0
if un.any():
q = np.minimum(pi[un] + HH, len(pc) - 1)
R[un] = (pc[q] - entry[un]) * dd[un] / R0[un]
R = R - sp / R0
se = R.std(ddof=1) / np.sqrt(len(R))
out.append((N, 'break^' if d < 0 else 'breakv', m, len(R),
(sp / R0).mean(), R.mean(), R.mean() / max(se, 1e-12)))
return out
if __name__ == '__main__':
syms = [s for s in sys.argv[1:] if s in SYMS] or list(SYMS)
print("=== FADE THE BREAK OF A CONFIRMED SWING EXTREME ===")
print(" the location, stripped of any candlestick name. stop = m x ATR.\n")
print(f" {'symbol':>7}{'tf':>5}{'N':>4}{'side':>8}{'m':>6}{'trades':>8}"
f"{'cost(R)':>9}{'expR':>9}{'t':>8}")
for tf in ('M15', 'H1'):
for s in syms:
for r in run(s, tf):
print(f" {s:>7}{tf:>5}{r[0]:>4}{r[1]:>8}{r[2]:>6.2f}{r[3]:>8}"
f"{r[4]:>9.3f}{r[5]:>+9.3f}{r[6]:>+8.2f}"
f"{' <--' if r[5] > 0 and r[6] > 3 else ''}")