Files
foxhunt/scripts/surfer/fetch_xvenue_hist.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

91 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Fetch historical funding (Binance + Bybit) for liquid coins on both, to BACKTEST the cross-venue
funding arb instead of waiting for live paper-forward. Aggregates 8h settlements to daily per venue.
Cached. Saves data/surfer/xvenue/panel.json = {coin: {date: [binance_daily, bybit_daily]}}.
"""
import datetime
import json
import os
import time
import urllib.request
OUT = "data/surfer/xvenue"
NCOINS = 60
DAYS = 330
def get(u, post=None):
data = json.dumps(post).encode() if post else None
h = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
for a in range(4):
try:
return json.loads(urllib.request.urlopen(urllib.request.Request(u, data=data, headers=h), timeout=25).read())
except Exception:
if a == 3:
return None
time.sleep(3 * (a + 1))
return None
def base(s):
return s[:-4] if s.endswith("USDT") else s
def day_of(ms):
return datetime.datetime.utcfromtimestamp(int(ms) / 1000).strftime("%Y-%m-%d")
def binance_hist(coin):
r = get(f"https://fapi.binance.com/fapi/v1/fundingRate?symbol={coin}USDT&limit=1000")
d = {}
for x in (r or []):
d.setdefault(day_of(x["fundingTime"]), 0.0)
d[day_of(x["fundingTime"])] += float(x["fundingRate"])
return d
def bybit_hist(coin):
out, end = {}, None
for _ in range(6):
u = f"https://api.bybit.com/v5/market/funding/history?category=linear&symbol={coin}USDT&limit=200"
if end:
u += f"&endTime={end}"
r = get(u)
lst = ((r or {}).get("result") or {}).get("list") or []
if not lst:
break
for x in lst:
t = x["fundingRateTimestamp"]
out.setdefault(day_of(t), 0.0)
out[day_of(t)] += float(x["fundingRate"])
end = int(lst[-1]["fundingRateTimestamp"]) - 1
time.sleep(0.3)
return out
def main():
os.makedirs(OUT, exist_ok=True)
binv = {base(x["symbol"]): float(x["quoteVolume"]) for x in get("https://fapi.binance.com/fapi/v1/ticker/24hr") if x["symbol"].endswith("USDT")}
byr = get("https://api.bybit.com/v5/market/tickers?category=linear")["result"]["list"]
bybv = {base(x["symbol"]): float(x.get("turnover24h", 0)) for x in byr if x["symbol"].endswith("USDT")}
coins = sorted([c for c in binv if c in bybv and binv[c] > 10e6 and bybv[c] > 10e6], key=lambda c: -binv[c])[:NCOINS]
print(f"universe: {len(coins)} coins on both venues >$10M/day")
panel = {}
for i, c in enumerate(coins):
cf = f"{OUT}/{c}.json"
if os.path.exists(cf):
panel[c] = json.load(open(cf)); continue
bn = binance_hist(c); by = bybit_hist(c)
days = sorted(set(bn) & set(by))
panel[c] = {d: [bn[d], by[d]] for d in days}
json.dump(panel[c], open(cf, "w"))
if i % 10 == 0:
print(f" {i+1}/{len(coins)} {c}: {len(panel[c])} common days")
time.sleep(0.4)
json.dump(panel, open(f"{OUT}/panel.json", "w"))
print(f"DONE: {len(panel)} coins -> {OUT}/panel.json")
if __name__ == "__main__":
main()