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

114 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""Deep multi-venue funding history (Binance + OKX + Hyperliquid, ~1yr) for a confident cross-venue
backtest. Binance & HL are deep (~330d); OKX ~3mo. Aggregate to daily per venue. Cache per coin.
Saves data/surfer/xvenue/panel2.json = {coin: {date: {bn, okx, hl}}}.
"""
import datetime
import json
import os
import time
import urllib.request
OUT = "data/surfer/xvenue2"
NCOINS = 35
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"}
if post:
h["Content-Type"] = "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 []):
k = day_of(x["fundingTime"]); d[k] = d.get(k, 0.0) + float(x["fundingRate"])
return d
def okx_hist(coin):
out, after = {}, None
for _ in range(6):
u = f"https://www.okx.com/api/v5/public/funding-rate-history?instId={coin}-USDT-SWAP&limit=100"
if after:
u += f"&after={after}"
r = get(u); data = (r or {}).get("data") or []
if not data:
break
for x in data:
k = day_of(x["fundingTime"]); out[k] = out.get(k, 0.0) + float(x["fundingRate"])
after = data[-1]["fundingTime"]; time.sleep(0.2)
return out
def hl_hist(coin, now_ms):
out, start = {}, now_ms - DAYS * 86400000
for _ in range(30):
r = get("https://api.hyperliquid.xyz/info", post={"type": "fundingHistory", "coin": coin, "startTime": start})
if not r:
break
for x in r:
k = day_of(x["time"]); out[k] = out.get(k, 0.0) + float(x["fundingRate"])
last = int(r[-1]["time"])
if last <= start or len(r) < 2:
break
start = last + 1
return out
def main():
os.makedirs(OUT, exist_ok=True)
now_ms = int(time.time() * 1000)
binv = {base(x["symbol"]): float(x["quoteVolume"]) for x in get("https://fapi.binance.com/fapi/v1/ticker/24hr") if x["symbol"].endswith("USDT")}
hlmeta = get("https://api.hyperliquid.xyz/info", post={"type": "metaAndAssetCtxs"})
hlcoins = {u["name"] for u in hlmeta[0]["universe"]}
coins = sorted([c for c in binv if c in hlcoins and binv[c] > 10e6], key=lambda c: -binv[c])[:NCOINS]
print(f"universe: {len(coins)} coins (Binance>$10M & on Hyperliquid)")
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); okx = okx_hist(c); hl = hl_hist(c, now_ms)
days = sorted(set(bn) | set(okx) | set(hl))
rec = {}
for d in days:
v = {}
if d in bn:
v["bn"] = bn[d]
if d in okx:
v["okx"] = okx[d]
if d in hl:
v["hl"] = hl[d]
if len(v) >= 2:
rec[d] = v
panel[c] = rec
json.dump(rec, open(cf, "w"))
print(f" {i+1}/{len(coins)} {c}: {len(rec)} days w/ >=2 venues (bn{len(bn)} okx{len(okx)} hl{len(hl)})")
time.sleep(0.3)
json.dump(panel, open(f"{OUT}/panel2.json", "w"))
print(f"DONE: {len(panel)} coins -> {OUT}/panel2.json")
if __name__ == "__main__":
main()