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>
68 lines
3.0 KiB
Python
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()
|