From ee2215f4d3bb8a5a4fde9d0dc913b2632ca636bc Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sun, 7 Jun 2026 18:58:24 +0200 Subject: [PATCH] feat(crypto): cross-venue harness -> hysteresis (deployable) + deep full-year history (1) Live cross_venue_funding.py paper harness now uses HYSTERESIS (enter >10bp/day, hold until spread decays <5bp) instead of naive daily top-K -> tracks the deployable low-turnover version (naive was cost-killed -4.5 Sharpe; hysteresis nets +10-14%/yr). State reset. (2) fetch_xvenue_hist2 now paginates Binance fundingRate -> full ~330d history (was 67d) so Binance-HL overlap = full year (330 days) for the regime test across ~11 months. Monitor armed for the full-year backtest. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/surfer/cross_venue_funding.py | 15 ++++++++++++--- scripts/surfer/fetch_xvenue_hist2.py | 21 ++++++++++++++------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/scripts/surfer/cross_venue_funding.py b/scripts/surfer/cross_venue_funding.py index aeee64ef5..0721c5f7a 100644 --- a/scripts/surfer/cross_venue_funding.py +++ b/scripts/surfer/cross_venue_funding.py @@ -26,7 +26,9 @@ LIQ = 10e6 # >$10M/day on BOTH legs (clean, fungible) MAXF = 0.005 # exclude legs with |daily funding| > 50bp/day = distress/artifact (un-tradeable) TOPK = 10 # book the top-K spreads COST_RT = 0.0010 # ~10bp round-trip (2 perp legs, maker) -HURDLE = 0.0005 # 5bp/day spread to bother +HURDLE = 0.0005 # 5bp/day spread to show in scan +ENTRY = 0.0010 # hysteresis: only ENTER a new pair above 10bp/day +EXIT = 0.0005 # hysteresis: HOLD a pair until its spread decays below 5bp/day (cuts turnover) def get(url, post=None): @@ -122,8 +124,15 @@ def cmd_run(): cur = {r["coin"]: r["spread"] for r in rows} prev = st["positions"] realized = sum(w * cur.get(c, 0.0) for c, w in prev.items()) # carry on yesterday's book at today's spreads - qual = [r for r in rows if r["spread"] > HURDLE][:TOPK] - newpos = {r["coin"]: 1.0 / len(qual) for r in qual} if qual else {} + # HYSTERESIS (the deployable, low-turnover version): hold winners until they decay, only enter strong fresh + held = [c for c in prev if cur.get(c, 0.0) > EXIT] + for r in sorted([r for r in rows if r["spread"] > ENTRY], key=lambda r: -r["spread"]): + if len(held) >= TOPK: + break + if r["coin"] not in held: + held.append(r["coin"]) + qual = [r for r in rows if r["coin"] in held] + newpos = {c: 1.0 / len(held) for c in held} if held else {} turn = sum(abs(newpos.get(c, 0) - prev.get(c, 0)) for c in set(newpos) | set(prev)) net = realized - turn * (COST_RT / 2) st.update(positions=newpos, days=st["days"] + 1, last_run_date=today) diff --git a/scripts/surfer/fetch_xvenue_hist2.py b/scripts/surfer/fetch_xvenue_hist2.py index 2b0585267..7fd05db2e 100644 --- a/scripts/surfer/fetch_xvenue_hist2.py +++ b/scripts/surfer/fetch_xvenue_hist2.py @@ -37,12 +37,19 @@ 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 binance_hist(coin, now_ms): + out, start = {}, now_ms - DAYS * 86400000 + for _ in range(8): # paginate back full DAYS window + r = get(f"https://fapi.binance.com/fapi/v1/fundingRate?symbol={coin}USDT&startTime={start}&limit=1000") + if not r: + break + for x in r: + k = day_of(x["fundingTime"]); out[k] = out.get(k, 0.0) + float(x["fundingRate"]) + last = int(r[-1]["fundingTime"]) + if last <= start or len(r) < 2: + break + start = last + 1 + return out def okx_hist(coin): @@ -88,7 +95,7 @@ def main(): 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) + bn = binance_hist(c, now_ms); okx = okx_hist(c); hl = hl_hist(c, now_ms) days = sorted(set(bn) | set(okx) | set(hl)) rec = {} for d in days: