forked from animatedread/Warrior_EA
User claim: swings dwarf the spread, so cost cannot be what blocks swing trading at 1:2/1:3 RR. Measured on the validated M1 bid/ask book, SP500 H1, ATR-scaled causal ZigZag at 4 reversal thresholds: - The premise is CONFIRMED: median swing 36-79 spreads, mean up to 110. Perfect-foresight expectancy +28 to +59 pts/leg. - The conclusion does not follow: trading every confirmed leg (enter on the ZigZag confirmation close, real ask/bid fills, exit on the next confirmation) grosses -0.2 to -0.5 pts/leg AT ZERO COST, on 3,681 to 13,871 legs. The confirmation retracement - the event that DEFINES a pivot - consumes the entire swing before the spread is even charged. - Long/short split is symmetric around the index drift (LONG +1.18, SHORT -2.17 gross at 3xATR), i.e. no swing structure beyond drift. - 3.0xATR reversal reproduces the EA ZigZag cadence exactly (median leg 17 bars, 49 legs/1000 bars vs the EA measured 17 and 44). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
115 lines
5.5 KiB
Python
115 lines
5.5 KiB
Python
"""Pivot-to-pivot swing distances vs spread, and the expectancy of actually trading them.
|
|
|
|
The user's claim (2026-08-14, with a chart): "the spread is nothing compared to these moves -
|
|
target 1:2 or 1:3 RR of swings". Both halves get measured here, on the validated M1 bid/ask
|
|
book (see book.py / fills.py), SP500 H1 mid bars for signals, real ask/bid for every fill.
|
|
|
|
1. SWING SIZE - ATR-scaled causal ZigZag (reversal threshold k*ATR14): pivot-to-pivot
|
|
amplitude in points, in ATR, in spreads. This quantifies the user's picture, and the
|
|
user is RIGHT: swings are enormous relative to the spread.
|
|
|
|
2. PERFECT-FORESIGHT EXPECTANCY - buy every low pivot at the ask, sell every high pivot at
|
|
the bid. This is the expectancy the picture suggests. It is not tradeable, because a
|
|
pivot is only DEFINED by what price does afterwards.
|
|
|
|
3. CAUSAL EXPECTANCY - the honest version of the same trade. A pivot exists, causally, the
|
|
moment price has retraced k*ATR from the extreme (the ZigZag's own confirmation rule -
|
|
the same event that draws the leg on a chart). Strategy: when a new leg is confirmed on
|
|
a CLOSED H1 bar, enter WITH the new leg at the next M1 minute's real ask/bid; exit when
|
|
the opposite pivot confirms, same fill discipline. Every leg is traded, long and short
|
|
reported separately (a swing edge must be a directional asymmetry - mirror-test rule).
|
|
GROSS expectancy (mid-to-mid, zero cost) is reported next to NET, so the question
|
|
"is it the spread that kills it?" is answered by subtraction.
|
|
|
|
What the confirmation rule costs is not a defect of ZigZag - it is the definition of a
|
|
swing. Any swing-trading rule pays it in its own coin: you learn the pivot happened only
|
|
after the market has already left it.
|
|
"""
|
|
import numpy as np
|
|
import sys
|
|
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
import book
|
|
import fills
|
|
|
|
SYM = "SP500"
|
|
WARMUP = 50
|
|
|
|
|
|
def zigzag(f, k):
|
|
"""Causal ATR ZigZag on H1 mid bars. Extremes from highs/lows (true swing size);
|
|
confirmation when the CLOSE has retraced k*ATR14 from the extreme.
|
|
Returns list of (pivot_i, confirm_i, dir_of_ended_leg_extreme) where dir=+1 means the
|
|
pivot is a swing HIGH (the leg into it went up; the new leg goes down)."""
|
|
a = f.atr(14) # ATR ending at the previous bar - usable at bar i
|
|
piv = []
|
|
up = True
|
|
ext = f.h[WARMUP]
|
|
ext_i = WARMUP
|
|
for i in range(WARMUP + 1, f.n):
|
|
th = k * a[i]
|
|
if not np.isfinite(th) or th <= 0:
|
|
continue
|
|
if up:
|
|
if f.h[i] > ext:
|
|
ext, ext_i = f.h[i], i
|
|
elif ext - f.c[i] >= th:
|
|
piv.append((ext_i, i, +1))
|
|
up, ext, ext_i = False, f.l[i], i
|
|
else:
|
|
if f.l[i] < ext:
|
|
ext, ext_i = f.l[i], i
|
|
elif f.c[i] - ext >= th:
|
|
piv.append((ext_i, i, -1))
|
|
up, ext, ext_i = True, f.h[i], i
|
|
return piv, a
|
|
|
|
|
|
def main():
|
|
bk = fills.Book(SYM)
|
|
f = book.frame(SYM, "H1", bk)
|
|
sp = float(np.nanmean(f.spread))
|
|
print(f"{SYM} H1: {f.n} bars {np.datetime64(int(f.t[0]), 's')}..{np.datetime64(int(f.t[-1]), 's')}"
|
|
f" | mean spread {sp:.2f} pts")
|
|
for k in (1.0, 1.5, 2.0, 3.0):
|
|
piv, a = zigzag(f, k)
|
|
if len(piv) < 30:
|
|
continue
|
|
pi = np.array([p[0] for p in piv]) # pivot bar
|
|
ci = np.array([p[1] for p in piv]) # confirmation bar
|
|
dr = np.array([p[2] for p in piv]) # +1 swing high
|
|
px = np.where(dr > 0, f.h[pi], f.l[pi]) # pivot price
|
|
# --- 1. swing size: consecutive pivot-to-pivot distance
|
|
amp = np.abs(np.diff(px))
|
|
amp_atr = amp / a[pi[1:]]
|
|
dur = np.diff(pi)
|
|
lag = ci - pi # bars from pivot to knowing about it
|
|
print(f"\n== reversal {k:.1f} x ATR: {len(amp)} legs "
|
|
f"({1000.0 * len(amp) / f.n:.1f}/1000 bars), median duration {np.median(dur):.0f} bars, "
|
|
f"median confirm lag {np.median(lag):.0f} bars ==")
|
|
print(f" swing size: median {np.median(amp):.1f} pts = {np.median(amp_atr):.2f} ATR = "
|
|
f"{np.median(amp) / sp:.0f} spreads | mean {amp.mean():.1f} pts = {amp.mean() / sp:.0f} spreads")
|
|
# --- 2. perfect foresight: every leg, pivot price to pivot price, one spread each
|
|
print(f" perfect foresight (untradeable): {amp.mean() - sp:+.1f} pts/leg net "
|
|
f"({(amp.mean() - sp) / sp:.0f} spreads)")
|
|
# --- 3. causal: enter at first M1 after the confirming bar closes, with the new leg;
|
|
# exit at first M1 after the NEXT confirmation. Real ask/bid opens.
|
|
side = -dr[:-1] # after a swing high confirms, new leg is DOWN
|
|
e = f.i0[np.minimum(ci[:-1] + 1, f.n - 1)]
|
|
x = f.i0[np.minimum(ci[1:] + 1, f.n - 1)]
|
|
ok = x > e
|
|
entry_mid = 0.5 * (bk.ao[e] + bk.bo[e])
|
|
exit_mid = 0.5 * (bk.ao[x] + bk.bo[x])
|
|
gross = side * (exit_mid - entry_mid)
|
|
net = np.where(side > 0, bk.bo[x] - bk.ao[e], bk.bo[e] - bk.ao[x])
|
|
for name, m in (("ALL", ok), ("LONG", ok & (side > 0)), ("SHORT", ok & (side < 0))):
|
|
g, nn = gross[m], net[m]
|
|
n = len(g)
|
|
se = g.std() / np.sqrt(n)
|
|
wr = 100.0 * (nn > 0).mean()
|
|
print(f" causal {name:<5} n={n:5d}: GROSS {g.mean():+7.2f} pts/leg (t={g.mean() / se:+.1f}) | "
|
|
f"NET {nn.mean():+7.2f} | win {wr:.0f}% | vs swing size {amp[m].mean():.0f} pts")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|