Warrior_EA/research/test_swing_fade.py
AnimateDread 79f108ab84 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

115 lines
5 KiB
Python

"""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 ''}")