feat: research.slippage — paper-vs-real fill variance from recorded NBBO

Prices each closed round-trip against the nearest quote_snapshots NBBO:
per-trade spread cost, book-walk flag (order size > displayed ask), and a
cost-adjusted P&L total. Measured this week: 0.3-0.5% median spreads on
gate-passing names, ~12% profit drag — far below the static 2-4% microcap
proxy. Coverage gaps (quote recorder only follows tracked names) reported
per-trade, not silently dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-07-22 10:04:47 -05:00
1 parent 21b38109aa
commit e9a1f577a9
1 file changed
+81
+81
View File
@@ -0,0 +1,81 @@
"""Paper-vs-real fill variance: spread cost per closed trade from recorded NBBO.
Paper fills are optimistic (limit fills at last price, zero spread). This report
prices each closed round-trip against the NEAREST quote_snapshots NBBO at entry
and exit: round-trip cost = entry-time spread + exit-time spread, on the traded
size, plus a book-walk flag when the order size exceeded the displayed ask size.
Run any time: python -m research.slippage [--since YYYY-MM-DD]
"""
from __future__ import annotations
import argparse
import json
from research.multi_schema import open_research_db
# ponytail: NBBO snapshots are 8-15s apart — nearest-quote is an approximation,
# not the actual tape. Upgrade path: record the quote INSIDE the fill event.
_NEAR = "interval '3 minutes'"
def _nbbo(con, symbol: str, ts) -> tuple | None:
return con.execute(
f"SELECT bid_price, ask_price, ask_size FROM quote_snapshots "
f"WHERE symbol=? AND ts BETWEEN ?::timestamptz - {_NEAR} AND ?::timestamptz + {_NEAR} "
"AND bid_price > 0 AND ask_price > bid_price "
"ORDER BY abs(extract(epoch FROM ts - ?::timestamptz)) LIMIT 1",
[symbol, ts, ts, ts]).fetchone()
def report(con, since: str | None = None) -> dict:
rows = con.execute(
"SELECT timestamp, payload_json FROM events WHERE event_type='position_closed'"
+ (" AND timestamp >= ?" if since else "") + " ORDER BY timestamp",
[since] if since else []).fetchall()
out, tot_pnl, tot_cost = [], 0.0, 0.0
for ts, payload in rows:
p = json.loads(payload or "{}")
sym = p.get("symbol")
qty = float(p.get("quantity") or 0)
pnl = float(p.get("realized_pnl") or 0)
entry = float(p.get("entry_price") or 0)
if not sym or qty <= 0 or entry <= 0:
continue
tot_pnl += pnl
q = _nbbo(con, sym, ts)
if not q:
out.append({"symbol": sym, "ts": str(ts), "pnl": pnl, "cost": None,
"note": "no quote coverage"})
continue
bid, ask, ask_size = float(q[0]), float(q[1]), float(q[2] or 0)
spread = (ask - bid) / ask
cost = spread * entry * qty * 2 # entry + exit each cross half... full spread x2 is conservative
walk = qty > ask_size > 0
tot_cost += cost
out.append({"symbol": sym, "ts": str(ts), "pnl": round(pnl, 2),
"spread_pct": round(spread * 100, 3), "cost": round(cost, 2),
"book_walk": walk})
return {"trades": out, "paper_pnl": round(tot_pnl, 2),
"est_spread_cost": round(tot_cost, 2),
"cost_adjusted_pnl": round(tot_pnl - tot_cost, 2)}
def main() -> None:
ap = argparse.ArgumentParser(description="paper-vs-real fill variance report")
ap.add_argument("--since")
args = ap.parse_args()
con = open_research_db("market")
r = report(con, since=args.since)
for t in r["trades"]:
if t.get("cost") is None:
print(f" {t['symbol']:6s} {t['ts'][:16]} pnl {t['pnl']:>8.2f} (no quote coverage)")
else:
print(f" {t['symbol']:6s} {t['ts'][:16]} pnl {t['pnl']:>8.2f} spread {t['spread_pct']:.2f}%"
f" cost {t['cost']:>7.2f}" + (" BOOK-WALK" if t["book_walk"] else ""))
print(f"\npaper P&L {r['paper_pnl']} est spread cost {r['est_spread_cost']}"
f" cost-adjusted {r['cost_adjusted_pnl']}")
if __name__ == "__main__":
main()