FIFO queue priority via price-level episodes: join back of queue, only trades advance, fill when same-side aggressor volume clears the queue. Re-measures markout + OFI-conditioning on ACTUALLY-WON (adversely-biased) fills. Verdict (Q1+Q2 2024 ES): ~40% fill rate; won-fill markout -0.50..-0.55t (winner's curse vs optimistic -0.40t); naive AND OFI-conditioned passive MM both NEGATIVE (-$1..-$2/fill). The optimistic Q2 OFI tilt was a win-every-queue artifact. Combined with the crossing result: no capturable seconds-horizon ES edge for a non-colocated participant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
204 lines
9.4 KiB
Python
204 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Queue-aware passive market-making fill model on ES MBP-10 — the decisive
|
|
viability test (does the passive edge survive realistic, adversely-biased fills?).
|
|
|
|
The optimistic harness (measure_passive_mm.py) assumed we win every queue and got
|
|
break-even / a Q2 OFI-conditioned tilt. The whole result rides on that assumption.
|
|
This model adds FIFO queue priority:
|
|
|
|
* Segment each side into PRICE-LEVEL EPISODES (maximal runs of constant best
|
|
bid/ask price).
|
|
* At each episode start we "join the back of the queue": queue_ahead = touch size.
|
|
* Only TRADES advance us (cancels assumed behind us — the conservative FIFO
|
|
backtest standard). We FILL when cumulative same-side aggressor volume during
|
|
the episode >= queue_ahead.
|
|
* Episode end:
|
|
- price moves AWAY favorably without filling → MISS (we missed that move)
|
|
- price moves through our side via trades → FILL (adverse: we bought as
|
|
it fell / sold as it rose)
|
|
* The fills we WIN are concentrated in heavy-same-side-flow episodes = adverse.
|
|
|
|
Then re-measure markout + OFI-conditioned selective quoting on the WON fills, and
|
|
report the realistic fill rate. If the OFI-conditioned edge survives here across
|
|
quarters, passive MM is viable; if it evaporates, it was a win-every-queue artifact.
|
|
|
|
Usage:
|
|
python3 scripts/measure_passive_mm_queue.py <file.dbn.zst> [--n-cap N] [--fee-ticks F] [--ofi-window-sec W]
|
|
python3 scripts/measure_passive_mm_queue.py --self-test
|
|
"""
|
|
import argparse
|
|
import sys
|
|
import time
|
|
|
|
import numpy as np
|
|
|
|
TICK = 0.25
|
|
TICK_USD = 12.50
|
|
PX_SCALE = 1e9
|
|
|
|
|
|
def compute_ofi(bid_px, bid_sz, ask_px, ask_sz):
|
|
pb_n, pb_p = bid_px[1:], bid_px[:-1]
|
|
qb_n, qb_p = bid_sz[1:].astype(np.float64), bid_sz[:-1].astype(np.float64)
|
|
pa_n, pa_p = ask_px[1:], ask_px[:-1]
|
|
qa_n, qa_p = ask_sz[1:].astype(np.float64), ask_sz[:-1].astype(np.float64)
|
|
e_bid = qb_n * (pb_n >= pb_p) - qb_p * (pb_n <= pb_p)
|
|
e_ask = qa_p * (pa_n >= pa_p) - qa_n * (pa_n <= pa_p)
|
|
return e_bid + e_ask
|
|
|
|
|
|
def episode_fills(level_px, touch_sz, consume_vol):
|
|
"""FIFO queue fills per price-level episode.
|
|
|
|
level_px[i] : best price on our side at event i (int fixed-point)
|
|
touch_sz[i] : size resting at the touch at event i (queue we join behind)
|
|
consume_vol[i]: same-side aggressor trade volume at event i (advances queue)
|
|
|
|
Returns boolean fill mask (True at the event where our episode-start order fills).
|
|
One hypothetical order placed at each episode start (join back of queue).
|
|
"""
|
|
n = len(level_px)
|
|
chg = np.empty(n, bool); chg[0] = True
|
|
chg[1:] = level_px[1:] != level_px[:-1]
|
|
ep = np.cumsum(chg) - 1 # 0-based episode id per event
|
|
ep_start = np.flatnonzero(chg) # first event index of each episode
|
|
queue0_ep = touch_sz[ep_start].astype(np.float64) # queue we join behind
|
|
cs = np.cumsum(consume_vol.astype(np.float64))
|
|
base = cs[ep_start] - consume_vol[ep_start] # cumsum just BEFORE episode start
|
|
within = cs - base[ep] # same-side volume consumed since episode start (incl. current)
|
|
q0_evt = queue0_ep[ep]
|
|
crossed = (q0_evt > 0) & (within >= q0_evt)
|
|
prev_crossed = np.r_[False, crossed[:-1]]
|
|
same_ep = np.r_[False, ep[1:] == ep[:-1]]
|
|
fill = crossed & ~(prev_crossed & same_ep) # first crossing within each episode
|
|
return fill, ep, ep_start, chg
|
|
|
|
|
|
def self_test():
|
|
# one bid-level episode at 100 with queue0=5; sells of 3 then 3 → fill at idx2.
|
|
bid = np.array([100, 100, 100, 100, 99], dtype=np.int64)
|
|
bsz = np.array([5, 5, 5, 5, 4], dtype=np.int64)
|
|
sell = np.array([0, 3, 3, 0, 0], dtype=np.float64) # sell-aggressor vol at bid
|
|
fill, ep, ep_start, chg = episode_fills(bid, bsz, sell)
|
|
assert fill.tolist() == [False, False, True, False, False], fill.tolist()
|
|
# second episode (99) has no sells → no fill
|
|
assert ep.tolist() == [0, 0, 0, 0, 1], ep.tolist()
|
|
print("queue-fill self-test PASS: fill at idx2 (cum sell 6 >= queue0 5)")
|
|
return 0
|
|
|
|
|
|
def decode(path, n_cap):
|
|
import zstandard
|
|
import databento_dbn as d
|
|
ts = np.zeros(n_cap, np.int64)
|
|
bpx = np.zeros(n_cap, np.int64); apx = np.zeros(n_cap, np.int64)
|
|
bsz = np.zeros(n_cap, np.int64); asz = np.zeros(n_cap, np.int64)
|
|
inst = np.zeros(n_cap, np.int32)
|
|
agg = np.zeros(n_cap, np.int8) # +1 buy-agg (lifts ask), -1 sell-agg (hits bid)
|
|
tsz = np.zeros(n_cap, np.float64) # trade size (0 for non-trades)
|
|
dec = d.DBNDecoder(); dctx = zstandard.ZstdDecompressor()
|
|
i = 0; t0 = time.time(); done = False
|
|
with open(path, "rb") as fh:
|
|
reader = dctx.stream_reader(fh)
|
|
while not done:
|
|
chunk = reader.read(1 << 22)
|
|
if not chunk:
|
|
break
|
|
dec.write(chunk)
|
|
for rec in dec.decode():
|
|
if "MBP10" not in type(rec).__name__:
|
|
continue
|
|
l0 = rec.levels[0]
|
|
ts[i] = rec.ts_event
|
|
bpx[i] = l0.bid_px; apx[i] = l0.ask_px
|
|
bsz[i] = l0.bid_sz; asz[i] = l0.ask_sz
|
|
inst[i] = rec.instrument_id
|
|
a = str(rec.action); s = str(rec.side)
|
|
if a.endswith("TRADE") or a == "T":
|
|
if s.endswith("BID") or s == "B":
|
|
agg[i] = 1; tsz[i] = rec.size
|
|
elif s.endswith("ASK") or s == "A":
|
|
agg[i] = -1; tsz[i] = rec.size
|
|
i += 1
|
|
if i >= n_cap:
|
|
done = True
|
|
break
|
|
return ts[:i], bpx[:i], apx[:i], bsz[:i], asz[:i], inst[:i], agg[:i], tsz[:i], time.time() - t0
|
|
|
|
|
|
def markout_table(side_name, fi, pos_sign, fill_px, mid, ts, ofi_w, fee_ticks, n_episodes):
|
|
mid_fill = mid[fi]
|
|
half = pos_sign * (mid_fill - fill_px) / TICK
|
|
aligned = np.sign(ofi_w[fi]) == pos_sign
|
|
print(f"\n[{side_name}] won fills={len(fi)} fill_rate={100*len(fi)/max(n_episodes,1):.1f}% of episodes "
|
|
f"half-spread={half.mean():.3f}t OFI-aligned={100*aligned.mean():.1f}%")
|
|
print(f"{'Δ(s)':>6} {'markout':>9} {'net_all':>9} {'net_$':>8} {'net_aligned':>12} {'net_adverse':>12} {'sel_$/fill':>11}")
|
|
for d_s in [0.1, 1.0, 5.0, 10.0, 60.0]:
|
|
idx2 = np.clip(np.searchsorted(ts, ts[fi] + int(d_s * 1e9), side="left"), 0, len(mid) - 1)
|
|
markout = pos_sign * (mid[idx2] - mid_fill) / TICK
|
|
net = half + markout - fee_ticks
|
|
na = net[aligned].mean() if aligned.any() else float("nan")
|
|
nadv = net[~aligned].mean() if (~aligned).any() else float("nan")
|
|
sel = (na * TICK_USD) if aligned.any() else float("nan")
|
|
print(f"{d_s:>6} {markout.mean():>9.4f} {net.mean():>9.4f} {net.mean()*TICK_USD:>7.2f}$ "
|
|
f"{na:>12.4f} {nadv:>12.4f} {sel:>10.2f}$")
|
|
|
|
|
|
def analyze(path, n_cap, fee_ticks, ofi_window_sec):
|
|
ts, bpx, apx, bsz, asz, inst, agg, tsz, dt = decode(path, n_cap)
|
|
n = len(ts)
|
|
ids, cnts = np.unique(inst, return_counts=True)
|
|
dom = ids[cnts.argmax()]
|
|
m = inst == dom
|
|
ts, bpx, apx, bsz, asz, agg, tsz = (a[m] for a in (ts, bpx, apx, bsz, asz, agg, tsz))
|
|
valid = (bpx > 0) & (apx > 0) & (bpx < apx)
|
|
ts, bpx, apx, bsz, asz, agg, tsz = (a[valid] for a in (ts, bpx, apx, bsz, asz, agg, tsz))
|
|
mid = (bpx + apx) / 2.0 / PX_SCALE
|
|
print(f"\n=== {path.split('/')[-1]} (QUEUE-AWARE) ===")
|
|
print(f"decoded {n} events in {dt:.1f}s | dominant={dom} → {valid.sum()} clean front-month events")
|
|
|
|
ofi = np.empty(len(ts)); ofi[0] = 0.0
|
|
ofi[1:] = compute_ofi(bpx, bsz, apx, asz)
|
|
cum_ofi = np.cumsum(ofi)
|
|
w_ns = int(ofi_window_sec * 1e9)
|
|
j0 = np.searchsorted(ts, ts - w_ns, side="left")
|
|
ofi_w = cum_ofi - cum_ofi[j0] # trailing-window OFI per event
|
|
|
|
# BUY side: episodes of constant bid_px; sell-aggressor (agg==-1) volume consumes bid queue
|
|
sell_vol = np.where(agg == -1, tsz, 0.0)
|
|
buy_fill, ep_b, ep_start_b, _ = episode_fills(bpx, bsz, sell_vol)
|
|
fib = np.flatnonzero(buy_fill)
|
|
markout_table("BUY (rest at bid)", fib, +1.0, bpx[fib] / PX_SCALE, mid, ts, ofi_w,
|
|
fee_ticks, len(ep_start_b))
|
|
|
|
# SELL side: episodes of constant ask_px; buy-aggressor (agg==+1) volume consumes ask queue
|
|
buy_vol = np.where(agg == 1, tsz, 0.0)
|
|
sell_fill, ep_a, ep_start_a, _ = episode_fills(apx, asz, buy_vol)
|
|
fia = np.flatnonzero(sell_fill)
|
|
markout_table("SELL (rest at ask)", fia, -1.0, apx[fia] / PX_SCALE, mid, ts, ofi_w,
|
|
fee_ticks, len(ep_start_a))
|
|
|
|
print(f"\nfee={fee_ticks}t/side (${fee_ticks*TICK_USD:.2f}); markout marked at mid (no flatten cost).")
|
|
print("Compare to optimistic (measure_passive_mm.py): if markout here is MORE adverse and")
|
|
print("net_aligned no longer > 0, the Q2 OFI edge was a win-every-queue artifact.")
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("file", nargs="?")
|
|
ap.add_argument("--n-cap", type=int, default=8_000_000)
|
|
ap.add_argument("--fee-ticks", type=float, default=0.1)
|
|
ap.add_argument("--ofi-window-sec", type=float, default=1.0)
|
|
ap.add_argument("--self-test", action="store_true")
|
|
args = ap.parse_args()
|
|
if args.self_test:
|
|
return self_test()
|
|
if not args.file:
|
|
ap.error("file required (or --self-test)")
|
|
analyze(args.file, args.n_cap, args.fee_ticks, args.ofi_window_sec)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|