Warrior_EA/research/test_controlbias.py

89 lines
3.8 KiB
Python
Raw Permalink Normal View History

"""Is "breakouts underperform nearby random entries" a finding, or a property of the control?
The drift run reported real minus local control at -0.045 R, negative for longs AND shorts,
and it did not go away when the barriers were re-sized from a slow ATR. The tempting reading
is that breakout continuation is anti-predictive and the fade is worth +0.045.
Before believing that, look at what the control actually is. A breakout bar is BY
CONSTRUCTION a local price extreme - price has just traded beyond the top of a range that
contained it for many bars. A control bar drawn uniformly from +/-250 bars around it is
therefore drawn from a window in which the real entry sits near the high (for an upward
breakout) or near the low (for a downward one).
Within any window, buying at the top and selling at the bottom is the worst possible entry
for a mean-reverting series. So real-minus-control would be negative for both directions even
if the breakout carried no information at all. The two arms differ in ENTRY PRICE, not only
in timing.
THE CHECK
---------
For each event, the percentile of the entry price within the +/-250 bar window, for the real
arm and the control arm, oriented so that HIGH means "a worse place to enter for this trade's
direction". A fair control sits near 50 for both. If the real arm sits near 90 while the
control sits near 50, the -0.045 is the control's geometry and there is nothing to fade.
"""
import numpy as np, sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
import fills, book, wyckoff
SYMS = ('EURUSD', 'USDJPY', 'XAUUSD', 'SP500')
def pct_in_window(c, bars, span):
"""Percentile of c[bar] within c[bar-span : bar+span], per event."""
out = np.empty(len(bars))
n = len(c)
for k, b in enumerate(bars):
lo, hi = max(0, b - span), min(n, b + span + 1)
w = c[lo:hi]
out[k] = 100.0 * (w < c[b]).mean()
return out
def run(sym, tf='H1', span=250, seed=3):
bk = fills.Book(sym)
f = book.frame(sym, tf, bk)
ev = wyckoff.breakouts(f, score=False)
if ev is None:
return None
rng = np.random.default_rng(seed)
rows = []
for d0 in (+1, -1):
m = ev['d'] == d0
if m.sum() < 150:
continue
bars = ev['i'][m] + 1
bars = bars[(bars > 300) & (bars < f.n - 5)]
ctl = np.clip(bars + rng.integers(-span, span + 1, len(bars)), 301, f.n - 6)
pr = pct_in_window(f.c, bars, span)
pc = pct_in_window(f.c, ctl, span)
#--- orient so HIGH = a worse place to enter for this direction
if d0 < 0:
pr, pc = 100 - pr, 100 - pc
rows.append((d0, len(bars), pr.mean(), pc.mean()))
return rows
if __name__ == '__main__':
syms = [s for s in sys.argv[1:] if s in SYMS] or list(SYMS)
print("=== IS THE LOCAL CONTROL FAIR? ===")
print(" entry-price percentile within the +/-250 bar window, oriented so HIGH = a")
print(" worse entry for that direction. A fair control sits near 50 on both arms.\n")
print(f" {'sym':>7}{'tf':>4}{'side':>6}{'n':>6}"
f"{'real pct':>10}{'ctrl pct':>10}{'difference':>12}")
d = []
for sym in syms:
for tf in ('H1', 'H4'):
r = run(sym, tf)
if not r:
continue
for d0, n, a, b in r:
d.append(a - b)
print(f" {sym:>7}{tf:>4}{'long' if d0>0 else 'short':>6}{n:>6}"
f"{a:>10.1f}{b:>10.1f}{a-b:>+12.1f}")
d = np.array(d)
print(f"\n mean real-minus-control percentile: {d.mean():+.1f} points "
f"({int((d>0).sum())}/{len(d)} cells worse)")
print("\n A large positive number means the two arms are not comparable: the breakout")
print(" arm is entering at a systematically worse price within the same window, so")
print(" real-minus-control measures the control's geometry, not the signal.")