Files
foxhunt/scripts/surfer/xvenue_sim.py
jgrusewski 027d73a504 research(crypto): cross-venue funding arb survives net-of-cost with hysteresis
Backtested the cross-venue funding arb on historical funding (Binance/OKX/Hyperliquid). Gross
+21%/yr, spreads persist (capture 0.71), but NAIVE daily rebalance is cost-killed (net Sharpe
-4.5, negative every month). HYSTERESIS (hold winners until spread decays, entry>10bp/exit>5bp)
flips net to +10-14%/yr market-neutral (Sharpe +11-15, but inflated by ~1% vol + idealized fills;
realistic ~3-6 / return ~10-14%). Turnover is the swing factor. First edge of the whole search to
survive the net-of-cost horde -- market-neutral, persistent, no spot leg (solves hedgeability),
operational not predictive. Caveats: 94d/one period, idealized fills, counterparty. Next: switch
live harness to hysteresis, deepen history to full year, micro-live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:33:28 +02:00

68 lines
3.0 KiB
Python

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