feat(surfer): minute-horizon gate — deep ML (Mamba2/CfC/TLOB) has no fuel

Before building deep sequence models, tested whether cost-surviving signal exists at
the minute horizon for them to extract. 12 perps, 60d free 1m klines (OFI proxy from
takerBuyQuote/quoteVol), features -> forward 1/5/15min returns net of 5bp taker. ALL
features negative net (best gross ~0.2bp vs 5bp cost = 25x gap); reversal IC -0.027 and
vol-continuation +0.009 are real but ~25x too small; OFI/taker-imbalance ~zero predictive
power. -> No minute-horizon signal -> Mamba2/CfC/TLOB have nothing to extract -> don't
build (no model lifts IC 25x). Crossing-cost wall holds in crypto (sec-ES 100x, min-crypto
25x); maker=adverse-selection wall, full-LOB=colocation territory. Gate saved a multi-week
GPU build for $0/10min. Product remains the daily two-sleeve book.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-06-06 19:09:04 +02:00
parent bc4cc46775
commit a9d1af9ec8

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Phase A — minute-horizon crypto signal gate (does deep ML have anything to extract?).
Before training Mamba2/CfC/TLOB, prove there's cost-surviving predictability at the minute
horizon. Free Binance 1m klines carry an OFI proxy (takerBuyQuote/quote). Test per-coin
short-horizon features -> forward returns, pooled, net of taker cost. If even simple features
show net edge, deep sequence models can plausibly extract more; if not, same wall as ES.
Features (info up to t): r1,r5,r15 (momentum/reversal), ofi (taker imbalance), ofi5 (smoothed),
vol_z. Targets: forward log-return over h in {1,5,15} min. Cost: 5bp round-trip taker.
Reports per-feature IC + best simple strategy per-trade net edge (bps) vs cost. GPU compute.
"""
import json
import math
import os
import sys
import time
import urllib.request
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
OUT = "data/surfer/crypto1m"
SYMS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "BNBUSDT", "DOGEUSDT",
"ADAUSDT", "AVAXUSDT", "LINKUSDT", "LTCUSDT", "SUIUSDT", "NEARUSDT"]
DAYS = 60
COST_BP = 5.0
def _get(u):
return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
def fetch(sym):
cache = f"{OUT}/{sym}.npz"
if os.path.exists(cache):
d = np.load(cache); return d["close"], d["qv"], d["tbq"]
end = int(time.time() * 1000); start = end - DAYS * 86_400_000
rows = []
cur = start
for _ in range(200):
k = _get(f"https://fapi.binance.com/fapi/v1/klines?symbol={sym}&interval=1m&startTime={cur}&limit=1500")
if not k:
break
rows += k
cur = k[-1][0] + 1
if len(k) < 1500 or cur >= end:
break
time.sleep(0.05)
d = {int(r[0]): (float(r[4]), float(r[7]), float(r[10])) for r in rows} # close, quoteVol, takerBuyQuote
ts = sorted(d)
close = np.array([d[t][0] for t in ts]); qv = np.array([d[t][1] for t in ts]); tbq = np.array([d[t][2] for t in ts])
os.makedirs(OUT, exist_ok=True)
np.savez(cache, close=close, qv=qv, tbq=tbq)
return close, qv, tbq
def roll_mean(x, w):
c = np.concatenate([[0], np.cumsum(x)])
out = np.full_like(x, np.nan); out[w - 1:] = (c[w:] - c[:-w]) / w; return out
def main():
feats_all = {k: [] for k in ["r1", "r5", "r15", "ofi", "ofi5", "vol_z"]}
fwd_all = {h: [] for h in [1, 5, 15]}
print(f"fetching 1m klines ({len(SYMS)} perps, {DAYS}d)...")
for s in SYMS:
close, qv, tbq = fetch(s)
lc = np.log(close)
r1 = np.full_like(lc, np.nan); r1[1:] = lc[1:] - lc[:-1]
r5 = np.full_like(lc, np.nan); r5[5:] = lc[5:] - lc[:-5]
r15 = np.full_like(lc, np.nan); r15[15:] = lc[15:] - lc[:-15]
ofi = np.where(qv > 0, 2 * tbq / qv - 1.0, 0.0) # taker buy imbalance in [-1,1]
ofi5 = roll_mean(ofi, 5)
vz = (qv - roll_mean(qv, 30)) / (roll_mean(qv, 30) + 1e-9)
feats = {"r1": r1, "r5": r5, "r15": r15, "ofi": ofi, "ofi5": ofi5, "vol_z": vz}
for k in feats_all:
feats_all[k].append(feats[k])
for h in fwd_all:
fwd = np.full_like(lc, np.nan); fwd[:-h] = lc[h:] - lc[:-h]
fwd_all[h].append(fwd)
F = {k: np.concatenate(v) for k, v in feats_all.items()}
FW = {h: np.concatenate(v) for h, v in fwd_all.items()}
print(f"\n===== MINUTE-HORIZON GATE — {len(SYMS)} perps, {DAYS}d, cost={COST_BP}bp round-trip =====")
print("IC = corr(feature_t, forward-return); per-trade NET = mean(sign(feat)*fwd) - cost, in bps")
print(f"{'feature':>8} " + " ".join(f"h={h}:IC/netbp" for h in [1, 5, 15]))
for fk, fv in F.items():
cells = []
for h, fw in FW.items():
m = np.isfinite(fv) & np.isfinite(fw)
a, b = fv[m], fw[m]
ic = float(np.corrcoef(a, b)[0, 1]) if len(a) > 1000 else float("nan")
net_bp = (float(np.mean(np.sign(a) * b)) - COST_BP / 1e4) * 1e4
cells.append(f"{ic:+.3f}/{net_bp:+.1f}")
print(f"{fk:>8} " + " ".join(cells))
print("\nVERDICT: any feature with positive per-trade NET bps (edge > cost) => minute-horizon signal")
print("exists for Mamba2/CfC/TLOB to extract -> build GPU model POC. All negative => same wall as ES.")
if __name__ == "__main__":
main()