"""Range structure, the five-trace context score, and the retest as a CONTINUUM of depths. Two findings set this up: the CONTEXT score is real +0.045 R per agreeing trace, replicated on two triggers the retest level matters, and price edge > value-area edge > VPOC, monotone in 8/8 it matters in the book's - the deeper the level you wait at, the more your fills opposite direction are breakouts that have already failed (adverse selection) That second result was measured at three discrete locations. Since the ordering was monotone at all three, the interesting question is not "which of the three" but "what does the curve do if you keep going" - and in particular whether it crosses zero on the SHALLOW side, which is where a base near zero would have to live for the context modifier to be worth bolting on. THE DESIGN ---------- After a breakout, place a BUY LIMIT (long case) at level = broken_edge + u * ATR u > 0 shallow, never reaching the old edge u = 0 the price edge itself u < 0 deep, back inside the old range and sweep u. The depth is a level YOU CHOOSE when the order is placed, not a property of what price went on to do, so there is no selection bias in the x-axis itself. Everything - level, stop, target - is fixed at placement time from bars already closed. Orders that would fill instantly are DROPPED, not filled at market: a buy limit already above the ask is not a retest, and letting `fills.py` cap it at the open would quietly mix market entries into a test about waiting. THE CONTROL ----------- The same orders at the same DISTANCE from the current price, but with that distance permuted across events - geometry preserved exactly, the identity of the level destroyed. Without it, "buy pullbacks" and "buy pullbacks TO THIS LEVEL" are indistinguishable, and the first is just drift. """ import numpy as np, sys import fills, book from fills import LIMIT, MARKET SYMS = ('EURUSD', 'USDJPY', 'XAUUSD', 'SP500') def traces(f, s, i, top, bot, dd, htf): """The five traces of book 2 2.3 / 7.1. +1 each if it agrees with direction dd. Reads only bars in [s, i], all closed before the order is placed. """ h, l, c, vol = f.h, f.l, f.c, f.v mid = 0.5 * (top + bot) Lq = i - s th = max(Lq // 3, 2) segA, segB, segC = slice(s, s + th), slice(s + th, s + 2 * th), slice(s + 2 * th, i) t1 = 1 if (h[segA].max() - mid) > (mid - l[segA].min()) else -1 t2 = 1 if (h[segB].max() - mid) > (mid - l[segB].min()) else -1 t3 = 0 if segC.stop > segC.start: if t2 > 0: t3 = 1 if l[segC].min() > bot + 0.25 * (top - bot) else -1 else: t3 = -1 if h[segC].max() < top - 0.25 * (top - bot) else 1 rr = max(h[i] - l[i], 1e-12) clspos = (c[i] - l[i]) / rr if dd > 0 else (h[i] - c[i]) / rr vavg = vol[s:i].mean() if i > s else vol[i] t4 = 1 if (clspos > 0.6 and vol[i] > 1.2 * max(vavg, 1e-12)) else -1 t5 = 1 if htf == dd else -1 return sum(1 for x in (t1 * dd, t2 * dd, t3 * dd, t4, t5) if x > 0) def breakouts(f, theta=0.60, htf=200, score=True): """Range breakouts with their structure, one row per event. -> dict of arrays: i (breakout bar), s (range start), d, top, bot, atr, ag (trace count) """ atr = f.atr(14) L, hi_, lo_ = book.find_ranges(f.h, f.l, atr, theta=theta) up = (L > 0) & (f.c > hi_) dn = (L > 0) & (f.c < lo_) fire = np.nonzero(up | dn)[0] fire = fire[(fire > max(300, htf + 5)) & (fire < f.n - 400)] if not len(fire): return None d = np.where(up[fire], 1, -1) s = fire - L[fire] ok = s >= 1 fire, d, s = fire[ok], d[ok], s[ok] htf_sig = np.sign(f.c[fire] - f.c[fire - htf]).astype(int) ag = np.zeros(len(fire), np.int8) if score: for q in range(len(fire)): ag[q] = traces(f, int(s[q]), int(fire[q]), hi_[fire[q]], lo_[fire[q]], int(d[q]), int(htf_sig[q])) return dict(i=fire, s=s, d=d, top=hi_[fire], bot=lo_[fire], atr=atr[fire], L=L[fire], ag=ag) def retest(sym, tf, phi, mrisk=1.0, kR=2.0, wait=40, H=200, theta=0.60, bk=None, f=None, ev=None, placebo=0): """One depth arm, parameterised by RETRACE FRACTION rather than distance in ATR. level = price_now - phi * (price_now - broken_edge) phi -> 0 at market, no pullback demanded phi = 1 the price edge itself - the classic Last Point of Support phi > 1 through the edge, into the old range: value-area and VPOC territory Why not "edge + u*ATR": that version only lets an order exist when the breakout has already extended past u*ATR, so the shallow arms were quietly a strong-breakout filter and the curve mixed depth with extension. As a fraction of the CURRENT distance to the edge, every event qualifies at every phi and the arms are the same sample throughout. stop = entry - d * mrisk * ATR fixed multiple, known at placement target= entry + d * kR * risk """ bk = bk or fills.Book(sym) f = f or book.frame(sym, tf, bk) ev = ev or breakouts(f, theta=theta) if ev is None: return None step = book.TF_SEC[tf] // 60 i, d, atr = ev['i'], ev['d'], ev['atr'] edge = np.where(d > 0, ev['top'], ev['bot']) #--- the order is placed after bar i closes and is live from bar i+1 e = i + 1 start = f.i0[e] ask0, bid0 = bk.ao[start], bk.bo[start] here = np.where(d > 0, ask0, bid0) #--- extension of the breakout beyond the edge, at the moment the order is placed ext = (here - edge) * d lvl = here - d * phi * ext dist = phi * ext if placebo: #--- same distance from the same starting price, level identity destroyed rng = np.random.default_rng(placebo) dist = dist[rng.permutation(len(dist))] lvl = here - d * dist #--- a buy limit must sit strictly BELOW the ask (a sell limit above the bid), else it #--- would fill instantly at the open and a market entry would be mixed into a test #--- about waiting live = (dist > 0) & np.where(d > 0, lvl < ask0, lvl > bid0) keep = live & np.isfinite(lvl) & (atr > 0) if keep.sum() < 100: return None idx = np.nonzero(keep)[0] risk = mrisk * atr[idx] stop = lvl[idx] - d[idx] * risk targ = lvl[idx] + d[idx] * kR * risk out = fills.simulate(bk, start[idx], d[idx], stop, targ, H * step, entry=LIMIT, entry_px=lvl[idx], entry_window=wait * step) if out is None: return None sel = idx[np.nonzero(out['filled'])[0][out['kept']]] out['event'] = sel out['ag'] = ev['ag'][sel] out['start'] = start[sel] #--- overlapping trades share price path; the honest n is the independent one out['indep'] = book.nonoverlap(out['idx'], out['exit_idx'] - out['idx']) out['placed'] = int(keep.sum()) return out def line(tag, out, extra=''): if out is None or out['n'] < 60: return f" {tag:<26} - too few" R = out['R']; ind = out['indep'] Ri = R[ind] return (f" {tag:<26} n={out['n']:>6} ({int(ind.sum()):>5} ind)" f" expR {R.mean():+7.4f} t {book.tstat(R):+6.2f}" f" ind {Ri.mean():+7.4f} t {book.tstat(Ri):+6.2f}" f" fill {100*out['n']/max(out['placed'],1):5.1f}%" f" amb {100*out['ambiguous']:4.1f}% unres {100*out['unresolved']:4.1f}%{extra}")