#!/usr/bin/env python3 """Backtest the cross-venue funding arb on historical funding (Binance vs Bybit). The persistence test, on history: each day pick the top-K coins by |binance-bybit funding spread|, position to collect it (short higher-funding venue, long lower), and book the REALIZED next-day funding difference (not the snapshot). If spreads persist -> positive; if they mean-revert before you collect -> ~0 net. Net of round-trip cost on turnover. Reports gross/net, Sharpe, by top-K. """ import json import math import os import sys import numpy as np PANEL = "data/surfer/xvenue/panel.json" COST_RT = 0.0010 HURDLE = 0.0005 def main(): panel = json.load(open(PANEL)) dates = sorted(set().union(*[set(v) for v in panel.values()])) di = {d: i for i, d in enumerate(dates)} coins = list(panel) T, N = len(dates), len(coins) bn = np.full((T, N), np.nan); by = np.full((T, N), np.nan) for j, c in enumerate(coins): for d, (b, y) in panel[c].items(): bn[di[d], j] = b; by[di[d], j] = y spread = bn - by # signed: + means binance funding higher print(f"cross-venue backtest: {N} coins, {T} days ({dates[0]}..{dates[-1]})") def sim(K, cost): rets = [] prev = set() for t in range(T - 1): s_t = spread[t]; s_n = spread[t + 1] ok = np.isfinite(s_t) & np.isfinite(s_n) & (np.abs(s_t) > HURDLE) idx = np.where(ok)[0] if len(idx) == 0: rets.append(0.0); continue top = idx[np.argsort(-np.abs(s_t[idx]))[:K]] p = np.sign(s_t[top]) realized = float(np.mean(p * s_n[top])) # collect next-day actual difference cur = set(coins[j] for j in top) turn = len(cur ^ prev) / max(len(cur), 1) rets.append(realized - turn * (cost / 2)); prev = cur r = np.array(rets) ann = r.mean() * 365; vol = r.std() * math.sqrt(365) eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min()) return ann, vol, (ann / vol if vol > 0 else float("nan")), dd, eq[-1] - 1 print(f"\n{'topK':>5} {'annNET%':>8} {'vol%':>6} {'Sharpe':>7} {'maxDD%':>7} {'totalNET%':>9}") for K in [5, 10, 20]: a, v, sh, dd, tot = sim(K, COST_RT) g = sim(K, 0.0)[0] print(f"{K:>5} {100*a:>+8.1f} {100*v:>6.1f} {sh:>+7.2f} {100*dd:>+7.1f} {100*tot:>+9.1f} (gross ann {100*g:+.0f}%)") # persistence diagnostic: sign(spread_t) == sign(spread_t+1) fraction fin = np.isfinite(spread[:-1]) & np.isfinite(spread[1:]) & (np.abs(spread[:-1]) > HURDLE) persist = np.mean(np.sign(spread[:-1][fin]) == np.sign(spread[1:][fin])) print(f"\n spread-sign persistence (1 day): {100*persist:.0f}% (>>50% = spreads persist = real; ~50% = noise/revert)") print(" VERDICT: net Sharpe>1 + persistence>>50% = real capturable edge; net~0/persist~50% = mean-reverts before you collect.") if __name__ == "__main__": main()