131 lines
5.8 KiB
Python
131 lines
5.8 KiB
Python
|
|
"""Did charging one average spread INVENT the context slope?
|
||
|
|
|
||
|
|
An honest fill model should move the LEVEL of every bucket by the cost and leave the SHAPE
|
||
|
|
alone - cost is not supposed to know what the Wyckoff traces said. Yet switching engines cut
|
||
|
|
the context slope from +0.0425 (t +3.18) to +0.0239 (t +1.37). Something in the cost is
|
||
|
|
correlated with the score.
|
||
|
|
|
||
|
|
There is an obvious candidate, and it is the thing the bid/ask bars were built to expose:
|
||
|
|
EURUSD's p95 spread is 4x its median. Every earlier test charged a single average and
|
||
|
|
assumed those hours away. If high-agreement setups happen disproportionately in the wide-
|
||
|
|
spread hours - news, thin liquidity, the open - then the old model undercharged exactly the
|
||
|
|
bucket the hypothesis cared about, and manufactured part of the slope out of a billing error.
|
||
|
|
|
||
|
|
THE TEST
|
||
|
|
--------
|
||
|
|
For the same LPS events, regress the REALISED cost (spread at the fill minute / risk) on the
|
||
|
|
agreement count. If the slope of cost-on-agreement is materially non-zero, an average-spread
|
||
|
|
model biases the dose-response by that amount per trace, and the direction of the bias says
|
||
|
|
which way.
|
||
|
|
|
||
|
|
Also reported: what the OLD model would have charged (one median spread for the symbol) so
|
||
|
|
the two can be compared as money rather than as an argument.
|
||
|
|
"""
|
||
|
|
import numpy as np, sys
|
||
|
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||
|
|
import fills, book, wyckoff, test_lps2
|
||
|
|
|
||
|
|
SYMS = ('EURUSD', 'USDJPY', 'XAUUSD', 'SP500')
|
||
|
|
|
||
|
|
|
||
|
|
def costs(sym, tf, **kw):
|
||
|
|
"""Same events as the attribution run, but returning the cost actually paid."""
|
||
|
|
bk = fills.Book(sym)
|
||
|
|
f = book.frame(sym, tf, bk)
|
||
|
|
a = test_lps2.events(sym, tf, bk=bk, f=f, **kw)
|
||
|
|
if a is None:
|
||
|
|
return None
|
||
|
|
#--- re-derive the fill minutes so the spread can be read where it was really paid
|
||
|
|
return a, bk, f
|
||
|
|
|
||
|
|
|
||
|
|
def run(sym, tf):
|
||
|
|
bk = fills.Book(sym)
|
||
|
|
f = book.frame(sym, tf, bk)
|
||
|
|
step = book.TF_SEC[tf] // 60
|
||
|
|
h, l, c = f.h, f.l, f.c
|
||
|
|
atr = f.atr(14)
|
||
|
|
L, hi_, lo_ = book.find_ranges(h, l, atr, theta=0.60)
|
||
|
|
up = (L > 0) & (c > hi_); dn = (L > 0) & (c < lo_)
|
||
|
|
fire = np.nonzero(up | dn)[0]
|
||
|
|
fire = fire[(fire > 305) & (fire < f.n - 65)]
|
||
|
|
dirs = np.where(up[fire], 1, -1)
|
||
|
|
rows, busy = [], -1
|
||
|
|
for q in range(len(fire)):
|
||
|
|
i = int(fire[q])
|
||
|
|
if i <= busy:
|
||
|
|
continue
|
||
|
|
dd = int(dirs[q]); Lq = int(L[i]); s = i - Lq
|
||
|
|
if s < 1:
|
||
|
|
continue
|
||
|
|
top, bot = hi_[i], lo_[i]
|
||
|
|
lvl = top if dd > 0 else bot
|
||
|
|
j = -1
|
||
|
|
for k in range(1, 61):
|
||
|
|
b_ = i + k
|
||
|
|
if b_ >= f.n - 2:
|
||
|
|
break
|
||
|
|
near = (l[b_] <= lvl + 0.35 * atr[i]) if dd > 0 else (h[b_] >= lvl - 0.35 * atr[i])
|
||
|
|
if near and ((c[b_] > lvl) if dd > 0 else (c[b_] < lvl)):
|
||
|
|
j = b_; break
|
||
|
|
if (c[b_] < lvl - 0.35 * atr[i]) if dd > 0 else (c[b_] > lvl + 0.35 * atr[i]):
|
||
|
|
break
|
||
|
|
if j < 0:
|
||
|
|
continue
|
||
|
|
e = j + 1
|
||
|
|
if e >= f.n - 1:
|
||
|
|
continue
|
||
|
|
ext = l[j] if dd > 0 else h[j]
|
||
|
|
stop = ext - dd * 0.10 * atr[i]
|
||
|
|
ag = wyckoff.traces(f, s, i, top, bot, dd, int(np.sign(c[i] - c[i - 200])))
|
||
|
|
rows.append((e, dd, stop, ag))
|
||
|
|
busy = e + Lq
|
||
|
|
if len(rows) < 100:
|
||
|
|
return None
|
||
|
|
E = np.array([r[0] for r in rows]); D = np.array([r[1] for r in rows])
|
||
|
|
ST = np.array([r[2] for r in rows], float); AG = np.array([r[3] for r in rows])
|
||
|
|
start = f.i0[E]
|
||
|
|
ent = np.where(D > 0, bk.ao[start], bk.bo[start])
|
||
|
|
risk = np.abs(ent - ST)
|
||
|
|
ok = risk > 4 * f.spread[E]
|
||
|
|
E, D, ST, AG, start, risk = (v[ok] for v in (E, D, ST, AG, start, risk))
|
||
|
|
sp_real = (bk.ac - bk.bc)[start] # what was actually paid, that minute
|
||
|
|
sp_med = np.median(bk.ac - bk.bc) # what the old model charged, always
|
||
|
|
return dict(ag=AG, cost_real=sp_real / risk, cost_old=sp_med / risk,
|
||
|
|
sp_real=sp_real, sp_med=sp_med, n=len(AG))
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
syms = [s for s in sys.argv[1:] if s in SYMS] or list(SYMS)
|
||
|
|
print("=== IS THE SPREAD CORRELATED WITH THE CONTEXT SCORE? ===")
|
||
|
|
print(" if cost rises with agreement, an average-spread model undercharges the")
|
||
|
|
print(" bucket the hypothesis cares about and inflates the dose-response.\n")
|
||
|
|
print(f" {'sym':>7}{'tf':>5}{'n':>6}{'spread p50':>12}{'p95':>9}{'p95/p50':>9}"
|
||
|
|
f"{'cost real':>11}{'cost old':>10}{'d(cost)/trace':>15}{'t':>7}")
|
||
|
|
PA, PC, PO = [], [], []
|
||
|
|
for tf in ('M15', 'H1'):
|
||
|
|
for s in syms:
|
||
|
|
r = run(s, tf)
|
||
|
|
if r is None:
|
||
|
|
print(f" {s:>7}{tf:>5} - too few"); continue
|
||
|
|
sl, tt = book.slope_t(r['cost_real'], r['ag'])
|
||
|
|
print(f" {s:>7}{tf:>5}{r['n']:>6}{r['sp_med']:>12.5f}"
|
||
|
|
f"{np.quantile(r['sp_real'],0.95):>9.5f}"
|
||
|
|
f"{np.quantile(r['sp_real'],0.95)/r['sp_med']:>9.2f}"
|
||
|
|
f"{r['cost_real'].mean():>11.4f}{r['cost_old'].mean():>10.4f}"
|
||
|
|
f"{sl:>+15.5f}{tt:>+7.2f}")
|
||
|
|
PA.append(r['ag']); PC.append(r['cost_real']); PO.append(r['cost_old'])
|
||
|
|
if PA:
|
||
|
|
ag = np.concatenate(PA); cr = np.concatenate(PC); co = np.concatenate(PO)
|
||
|
|
sl, tt = book.slope_t(cr, ag)
|
||
|
|
so, to = book.slope_t(co, ag)
|
||
|
|
print(f"\n POOLED n={len(ag):,}")
|
||
|
|
print(f" realised cost slope {sl:+.5f} R/trace t {tt:+.2f} mean {cr.mean():.4f}")
|
||
|
|
print(f" old flat cost slope {so:+.5f} R/trace t {to:+.2f} mean {co.mean():.4f}")
|
||
|
|
print(f" BIAS an average-spread model puts into the dose-response:"
|
||
|
|
f" {so - sl:+.5f} R per trace")
|
||
|
|
for k in range(6):
|
||
|
|
m = ag == k
|
||
|
|
if m.sum() >= 25:
|
||
|
|
print(f" {k} agree n={int(m.sum()):>5} realised cost {cr[m].mean():.4f}"
|
||
|
|
f" flat {co[m].mean():.4f} undercharged by {cr[m].mean()-co[m].mean():+.4f}")
|