#!/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()