From a9d1af9ec8a663fc3a05ccee6aebc957691a5b18 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sat, 6 Jun 2026 19:09:04 +0200 Subject: [PATCH] =?UTF-8?q?feat(surfer):=20minute-horizon=20gate=20?= =?UTF-8?q?=E2=80=94=20deep=20ML=20(Mamba2/CfC/TLOB)=20has=20no=20fuel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- scripts/surfer/micro_gate.py | 105 +++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 scripts/surfer/micro_gate.py diff --git a/scripts/surfer/micro_gate.py b/scripts/surfer/micro_gate.py new file mode 100644 index 000000000..394323214 --- /dev/null +++ b/scripts/surfer/micro_gate.py @@ -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()