121 lines
5.4 KiB
Python
121 lines
5.4 KiB
Python
|
|
"""The only base that has ever been positive - and whether anything can be bolted onto it.
|
||
|
|
|
||
|
|
Every trigger tested in this project starts at -0.15 to -0.33 R, so the context modifier has
|
||
|
|
never had anything to lift. But one thing HAS survived every test: drift. SP500 and gold rose
|
||
|
|
for the whole sample, the engine null re-measured it independently (random longs beat random
|
||
|
|
shorts by +0.08 to +0.10 R), and [[project_anomaly_families_tested]] found it was the single
|
||
|
|
family to clear cost.
|
||
|
|
|
||
|
|
So the question step 2 really asks is: **does a long-only breakout on a drifting instrument
|
||
|
|
give a base at or above zero, and does anything add to it?**
|
||
|
|
|
||
|
|
THREE ARMS, AND THE SECOND IS THE ONE THAT MATTERS
|
||
|
|
--------------------------------------------------
|
||
|
|
REAL range breakout in direction d, market entry at the next bar's open
|
||
|
|
LOCAL the same trade at a RANDOM bar within +/-250 bars - same instrument, same regime,
|
||
|
|
same drift rate, same barrier geometry, same n. Only the breakout is gone.
|
||
|
|
GLOBAL a random bar anywhere in the sample
|
||
|
|
|
||
|
|
If REAL - LOCAL is zero, the breakout contributes nothing and whatever expectancy the trade
|
||
|
|
has is exposure, not timing. That is the honest way to report a drift-powered result: it is
|
||
|
|
beta, and beta is available by buying and holding without paying a spread 3,000 times.
|
||
|
|
|
||
|
|
EURUSD and USDJPY are included as NEGATIVE CONTROLS. They barely drifted, so if the "edge"
|
||
|
|
is drift they should show nothing, and if a cell there lights up while the mechanism says it
|
||
|
|
should not, the mechanism is wrong rather than the cell being lucky.
|
||
|
|
"""
|
||
|
|
import numpy as np, sys, time
|
||
|
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||
|
|
import fills, book, wyckoff
|
||
|
|
|
||
|
|
SYMS = ('EURUSD', 'USDJPY', 'XAUUSD', 'SP500')
|
||
|
|
MRISK, KR, H = 3.0, 2.0, 200
|
||
|
|
|
||
|
|
|
||
|
|
def trade(bk, f, bars, d, mrisk, kR, H, step):
|
||
|
|
"""Market entry at the open of `bars`, stop mrisk*ATR away, target kR*risk."""
|
||
|
|
atr = f.atr(14)
|
||
|
|
ok = (bars > 300) & (bars < f.n - 5) & np.isfinite(atr[bars]) & (atr[bars] > 0)
|
||
|
|
bars, d = bars[ok], d[ok]
|
||
|
|
start = f.i0[bars]
|
||
|
|
ent = np.where(d > 0, bk.ao[start], bk.bo[start])
|
||
|
|
risk = mrisk * atr[bars]
|
||
|
|
o = fills.simulate(bk, start, d, ent - d * risk, ent + d * kR * risk, H * step,
|
||
|
|
entry=fills.MARKET)
|
||
|
|
if o is None:
|
||
|
|
return None
|
||
|
|
o['indep'] = book.nonoverlap(o['idx'], o['exit_idx'] - o['idx'])
|
||
|
|
o['bars'] = bars[np.nonzero(o['filled'])[0][o['kept']]]
|
||
|
|
return o
|
||
|
|
|
||
|
|
|
||
|
|
def run(sym, tf, mrisk=MRISK, kR=KR, seed=3, span=250):
|
||
|
|
bk = fills.Book(sym)
|
||
|
|
f = book.frame(sym, tf, bk)
|
||
|
|
step = book.TF_SEC[tf] // 60
|
||
|
|
ev = wyckoff.breakouts(f)
|
||
|
|
if ev is None:
|
||
|
|
return None
|
||
|
|
rng = np.random.default_rng(seed)
|
||
|
|
out = {}
|
||
|
|
for d0, tag in ((+1, 'long'), (-1, 'short')):
|
||
|
|
m = ev['d'] == d0
|
||
|
|
if m.sum() < 150:
|
||
|
|
continue
|
||
|
|
bars = ev['i'][m] + 1
|
||
|
|
ag = ev['ag'][m]
|
||
|
|
real = trade(bk, f, bars, np.full(m.sum(), d0), mrisk, kR, H, step)
|
||
|
|
loc = trade(bk, f,
|
||
|
|
np.clip(bars + rng.integers(-span, span + 1, len(bars)), 301, f.n - 6),
|
||
|
|
np.full(m.sum(), d0), mrisk, kR, H, step)
|
||
|
|
glo = trade(bk, f, rng.integers(301, f.n - 6, len(bars)),
|
||
|
|
np.full(m.sum(), d0), mrisk, kR, H, step)
|
||
|
|
if real is None:
|
||
|
|
continue
|
||
|
|
sel = np.nonzero(real['filled'])[0][real['kept']]
|
||
|
|
out[tag] = dict(real=real, loc=loc, glo=glo, ag=ag[sel])
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def stat(o):
|
||
|
|
if o is None:
|
||
|
|
return np.array([0.0])
|
||
|
|
return o['R'][o['indep']]
|
||
|
|
|
||
|
|
|
||
|
|
def diff_t(a, b):
|
||
|
|
se = np.sqrt(a.var(ddof=1) / len(a) + b.var(ddof=1) / len(b))
|
||
|
|
return (a.mean() - b.mean()) / max(se, 1e-12)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
syms = [s for s in sys.argv[1:] if s in SYMS] or list(SYMS)
|
||
|
|
print("=== DRIFT AS A BASE: does the breakout add anything to the exposure? ===")
|
||
|
|
print(f" stop {MRISK} ATR, target {KR}R, market entry. EURUSD/USDJPY are the")
|
||
|
|
print(" negative controls - little drift, so they should show little.\n")
|
||
|
|
print(f" {'sym':>7}{'tf':>4}{'side':>6}{'n':>6}{'REAL':>9}{'t':>6}"
|
||
|
|
f"{'local':>9}{'global':>9}{'R-local':>9}{'t':>6}{'ctx slope':>11}{'t':>6}")
|
||
|
|
rows = []
|
||
|
|
for sym in syms:
|
||
|
|
for tf in ('H1', 'H4'):
|
||
|
|
r = run(sym, tf)
|
||
|
|
if not r:
|
||
|
|
continue
|
||
|
|
for tag, v in r.items():
|
||
|
|
A = stat(v['real']); Lc = stat(v['loc']); G = stat(v['glo'])
|
||
|
|
sl, tt = book.slope_t(v['real']['R'][v['real']['indep']],
|
||
|
|
v['ag'][v['real']['indep']])
|
||
|
|
ex = A.mean() - Lc.mean()
|
||
|
|
rows.append((sym, tf, tag, ex, A.mean(), sl))
|
||
|
|
print(f" {sym:>7}{tf:>4}{tag:>6}{len(A):>6}{A.mean():>+9.4f}"
|
||
|
|
f"{book.tstat(A):>+6.2f}{Lc.mean():>+9.4f}{G.mean():>+9.4f}"
|
||
|
|
f"{ex:>+9.4f}{diff_t(A, Lc):>+6.2f}{sl:>+11.4f}{tt:>+6.2f}")
|
||
|
|
if rows:
|
||
|
|
ex = np.array([r[3] for r in rows])
|
||
|
|
base = np.array([r[4] for r in rows])
|
||
|
|
print(f"\n breakout excess over local control: mean {ex.mean():+.4f}, "
|
||
|
|
f"positive {int((ex>0).sum())}/{len(ex)}")
|
||
|
|
print(f" base expR: mean {base.mean():+.4f}, positive {int((base>0).sum())}/{len(base)}")
|
||
|
|
for tag in ('long', 'short'):
|
||
|
|
b = np.array([r[4] for r in rows if r[2] == tag])
|
||
|
|
print(f" {tag:>5}: base mean {b.mean():+.4f} positive {int((b>0).sum())}/{len(b)}")
|