#!/usr/bin/env python3 """Frontier (a) — cross-exchange funding dispersion: edge distinct from momentum? Fetch funding from Bybit + OKX (free) to pair with Binance funding + prices (crypto_pit). Build cross-sectional signals among multi-venue coins: multi-venue-avg funding (robust carry), cross-venue dispersion (positioning stress), Binance-premium. Validate each + correlation to the full crypto momentum book + marginal-alpha regression. Realistic: Reff = return - Binance funding (traded venue), death-excl, 10bp. Honest prior: low (cross-venue spreads arbitraged). """ 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 pit_sweep # noqa: E402 from surfer_poc import compute_weights, CFG # noqa: E402 from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402 import torch # noqa: E402 DEV = "cuda" if torch.cuda.is_available() else "cpu" DAY_MS = 86_400_000 XF = "data/surfer/xfund" COINS = ["BTC", "ETH", "SOL", "XRP", "DOGE", "ADA", "AVAX", "LINK", "LTC", "DOT", "NEAR", "ATOM", "FIL", "ETC", "XLM", "UNI", "AAVE", "BNB"] def _get(u): try: return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30)) except Exception: return None def bybit(coin): cache = f"{XF}/bybit_{coin}.json" if os.path.exists(cache): return {int(k): v for k, v in json.load(open(cache)).items()} out, end = {}, int(time.time() * 1000) for _ in range(40): r = _get(f"https://api.bybit.com/v5/market/funding/history?category=linear&symbol={coin}USDT&endTime={end}&limit=200") lst = (r or {}).get("result", {}).get("list", []) if not lst: break for x in lst: t = int(x["fundingRateTimestamp"]); out.setdefault(t // DAY_MS, 0.0) out[t // DAY_MS] += float(x["fundingRate"]) end = min(int(x["fundingRateTimestamp"]) for x in lst) - 1 if len(lst) < 200: break time.sleep(0.06) os.makedirs(XF, exist_ok=True); json.dump({str(k): v for k, v in out.items()}, open(cache, "w")) return out def okx(coin): cache = f"{XF}/okx_{coin}.json" if os.path.exists(cache): return {int(k): v for k, v in json.load(open(cache)).items()} out, before = {}, "" for _ in range(60): u = f"https://www.okx.com/api/v5/public/funding-rate-history?instId={coin}-USDT-SWAP&limit=100" if before: u += f"&after={before}" r = _get(u); data = (r or {}).get("data", []) if not data: break for x in data: t = int(x["fundingTime"]); out.setdefault(t // DAY_MS, 0.0) out[t // DAY_MS] += float(x["fundingRate"]) before = min(int(x["fundingTime"]) for x in data) if len(data) < 100: break time.sleep(0.06) os.makedirs(XF, exist_ok=True); json.dump({str(k): v for k, v in out.items()}, open(cache, "w")) return out def main(): syms, days, close, qv, fund = pit_sweep.load() idx = {s: j for j, s in enumerate(syms)} coins = [c for c in COINS if c + "USDT" in idx] print(f"fetching Bybit+OKX funding for {len(coins)} coins...") by = {c: bybit(c) for c in coins} ok = {c: okx(c) for c in coins} nby = sum(1 for c in coins if len(by[c]) > 100); nok = sum(1 for c in coins if len(ok[c]) > 100) print(f" Bybit covered {nby}/{len(coins)}, OKX covered {nok}/{len(coins)}") N = len(coins); T = len(days) di = {int(days[t]): t for t in range(T)} bn_f = np.full((T, N), np.nan); by_f = np.full((T, N), np.nan); ok_f = np.full((T, N), np.nan) cl = np.full((T, N), np.nan) for j, c in enumerate(coins): col = idx[c + "USDT"] cl[:, j] = close[:, col]; bn_f[:, j] = fund[:, col] for d, v in by[c].items(): if d in di: by_f[di[d], j] = v for d, v in ok[c].items(): if d in di: ok_f[di[d], j] = v R = np.zeros((T, N)); R[1:] = np.log(cl)[1:] - np.log(cl)[:-1]; R = np.where(np.isfinite(R), R, 0.0) bnf = np.where(np.isfinite(bn_f), bn_f, 0.0) Reff = R - bnf # trade on Binance -> pay Binance funding # cross-venue stack stack = np.stack([bn_f, by_f, ok_f]) # [3,T,N] avg_f = np.nanmean(stack, axis=0) # multi-venue avg funding disp_f = np.nanstd(stack, axis=0) # cross-venue dispersion prem = bn_f - np.nanmean(np.stack([by_f, ok_f]), axis=0) # Binance premium vs others def roll_mean(A, L): out = np.full_like(A, np.nan) for t in range(L, len(A)): out[t] = np.nanmean(A[t - L:t], axis=0) return out sigs = { "carry_BINANCE_only": -roll_mean(np.where(np.isfinite(bn_f), bn_f, np.nan), 7), # control: is it just major-coin carry? "carry_multivenue": -roll_mean(avg_f, 7), # long low avg funding (robust carry) "venue_dispersion": -roll_mean(disp_f, 7), # low disagreement? test "venue_dispersion+": roll_mean(disp_f, 7), # high disagreement "binance_premium": -roll_mean(prem, 7), # fade Binance-crowded } valid = np.isfinite(cl) & np.isfinite(avg_f) year = (1970 + days / 365.25).astype(int) def book(sig): s = sig.copy(); s[~valid] = np.nan return pnl_w(xs_weights(s), Reff, cost_bp=10) # full-universe momentum book for correlation cw, _ = compute_weights(close, qv, days, CFG) cR = np.zeros_like(close); cR[1:] = np.log(close)[1:] - np.log(close)[:-1]; cR = np.where(np.isfinite(cR), cR, 0.0) cf = np.where(np.isfinite(fund), fund, 0.0) mom = np.sum(cw[:-1] * (cR - cf)[1:], axis=1) mom_by = {int(days[1:][i]): mom[i] for i in range(len(mom))} print(f"\n===== CROSS-EXCHANGE FUNDING — {len(coins)} multi-venue coins, death-excl, 10bp, deflate N=55 =====") print(f"{'signal':>18} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'DSR':>5} {'corr_mom':>8} {'marg_t':>7}") pdays = days[1:] for nm, sg in sigs.items(): p = book(sg); v = validate(p, days, 55) pb = {int(pdays[i]): p[i] for i in range(len(p))} common = sorted(set(pb) & set(mom_by)) a = np.array([mom_by[d] for d in common]); b = np.array([pb[d] for d in common]) m = np.isfinite(a) & np.isfinite(b); a, b = a[m], b[m] corr = float(np.corrcoef(a, b)[0, 1]) if len(a) > 100 else float("nan") beta1 = np.cov(a, b)[0, 1] / (np.var(a) + 1e-12); res = b - beta1 * a mt = float(res.mean() / (res.std() / math.sqrt(len(res)) + 1e-12)) print(f"{nm:>18} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['dsr']:>5.2f} {corr:>+8.2f} {mt:>+7.2f}") print("\nVERDICT: any signal with full+OOS+CPCVmed>0, DSR>0.5, low |corr_mom|, AND marg_t>2 = real distinct edge.") print("(marg_t = t-stat of marginal alpha vs momentum book). Honest prior: cross-venue spreads arbitraged -> expect fail.") if __name__ == "__main__": main()