diff --git a/scripts/surfer/cross_exchange_funding.py b/scripts/surfer/cross_exchange_funding.py new file mode 100644 index 000000000..1a9b87430 --- /dev/null +++ b/scripts/surfer/cross_exchange_funding.py @@ -0,0 +1,161 @@ +#!/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()