From c10ebe02576ff63fdc87abc81dc70af7ab1041f8 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sun, 7 Jun 2026 01:16:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(crypto):=20delta-neutral=20funding=20harve?= =?UTF-8?q?st=20=E2=80=94=20the=20one=20edge=20that=20breaks=200.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigated crypto funding carry (researchers' flagged retail path to >1 Sharpe). DELTA-NEUTRAL cash-and-carry (short perp+long spot, collect funding, price cancels) -- different from the directional carry trap. Funding positive 75pct of time. Backtest net of cost: APR 9.9pct, raw Sharpe +5.68, maxDD -7.4pct, survives to 20bp cost, worst month -1.5pct (delta-neutral held). Discipline finds: (1) real; (2) 5.68 inflated (funding-only model, 1.8pct vol; real basis vol -> honest ~2-3 Sharpe = literature's 1.5-2.5); (3) regime-dependent + DECAYING (negative 2022, 2025, 2026). Caveats: counterparty/exchange tail (FTX -100pct, not in backtest = real killer), decay, operationally real. The ONE genuine path past the 0.7 ceiling -- but it's crypto + counterparty tail is the price of admission. The journey converges: higher Sharpe exists, reachable, but lives where the user didn't want to go with a tail the Sharpe doesn't show. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/surfer/crypto_funding_harvest.py | 91 ++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 scripts/surfer/crypto_funding_harvest.py diff --git a/scripts/surfer/crypto_funding_harvest.py b/scripts/surfer/crypto_funding_harvest.py new file mode 100644 index 000000000..e0dbed0dc --- /dev/null +++ b/scripts/surfer/crypto_funding_harvest.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Delta-neutral crypto funding HARVEST (cash-and-carry) — the one retail path to higher Sharpe. + +When perp funding > 0 (longs pay shorts), go short-perp + long-spot (DELTA-NEUTRAL: price cancels) +and collect the funding rate as carry. No price prediction. Causal: position[t] from funding[t-1] +(funding is persistent), collect funding[t]. Net of realistic round-trip cost. Stress: worst months +(crashes flip funding negative). Variants: broad vs majors, threshold, cost sensitivity. +Treats `fund` as per-day funding (conservative; if per-8h the real APR/Sharpe is higher). +""" +import math +import os +import sys + +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import pit_sweep # noqa: E402 + + +def metrics(r): + r = np.asarray(r); r = r[np.isfinite(r)] + if len(r) < 50 or r.std() == 0: + return (float("nan"),) * 4 + 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, dd + + +def harvest(fund, liq, thr, c_round, year, label, topk=None): + T, N = fund.shape + elig = liq & np.isfinite(fund) + sig = np.zeros((T, N)) + sig[1:] = np.where(elig[1:] & (fund[:-1] > thr), 1.0, 0.0) # causal: position from yesterday's funding + if topk: # restrict to top-K best-funded eligible + for t in range(1, T): + idx = np.where(sig[t] > 0)[0] + if len(idx) > topk: + keep = idx[np.argsort(-fund[t - 1, idx])[:topk]] + m = np.zeros(N); m[keep] = 1.0; sig[t] = m + w = sig / np.maximum(sig.sum(1, keepdims=True), 1) # equal-weight positioned + f = np.nan_to_num(fund) + gross = np.sum(w[:-1] * f[1:], axis=1) + turn = np.sum(np.abs(w[1:] - w[:-1]), axis=1) # fraction of book traded + net = gross - turn * (c_round / 2) # one-way cost each side + ann, vol, sr, dd = metrics(net) + npos = sig.sum(1); avgpos = npos[npos > 0].mean() + print(f"{label:>26} {100*ann:>6.1f} {100*vol:>6.1f} {sr:>+7.2f} {100*dd:>+7.1f} {avgpos:>6.0f} {100*turn.mean():>6.1f}") + return net + + +def main(): + syms, days, close, qv, fund = pit_sweep.load() + T, N = fund.shape + year = (1970 + days / 365.25).astype(int) + qv30 = np.full_like(qv, np.nan) + for t in range(30, T): + qv30[t] = np.nanmean(qv[t - 30:t], axis=0) + liq = np.isfinite(qv30) & (qv30 > 5e6) # >$5M/day quote volume + majors = np.array([s in ("BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT", "XRPUSDT", "ADAUSDT", + "DOGEUSDT", "AVAXUSDT", "LINKUSDT", "MATICUSDT") for s in syms]) + liq_maj = liq & majors[None, :] + + print(f"\n===== CRYPTO FUNDING HARVEST (delta-neutral, {N} coins, {T} days) =====") + print(f"{'variant':>26} {'APR%':>6} {'vol%':>6} {'Sharpe':>7} {'maxDD%':>7} {'#pos':>6} {'turn%':>6}") + base = harvest(fund, liq, 0.0, 0.0010, year, "broad, thr=0, 10bp") + harvest(fund, liq, 0.0003, 0.0010, year, "broad, thr=3bp, 10bp") + harvest(fund, liq, 0.0, 0.0010, year, "broad top-20, 10bp", topk=20) + harvest(fund, liq_maj, 0.0, 0.0010, year, "majors only, 10bp") + print(" -- cost sensitivity (broad, thr=0) --") + harvest(fund, liq, 0.0, 0.0005, year, "broad, 5bp") + harvest(fund, liq, 0.0, 0.0020, year, "broad, 20bp") + harvest(fund, liq, 0.0, 0.0040, year, "broad, 40bp (harsh)") + + # per-year + worst-month stress on the base case + print("\n per-year Sharpe (broad, thr=0, 10bp):") + print(" " + " ".join(f"{y}:{metrics(base[year[1:]==y])[2]:+.1f}" for y in range(2019, 2027) if (year[1:] == y).sum() > 60)) + # monthly buckets + mo = (days[1:] / 30.4).astype(int) + worst = sorted(set(mo), key=lambda m: base[mo == m].sum())[:5] + print(" worst 5 ~monthly buckets (crash stress — does delta-neutral hold?):") + for m in worst: + seg = base[mo == m] + d0 = int(days[1:][mo == m][0]) + import datetime + print(f" ~{datetime.date.fromordinal(d0+719163)}: {100*seg.sum():+.1f}% over {len(seg)}d") + print("\nVERDICT: if net Sharpe >> 1 AND survives crash months (delta-neutral so price-neutral) = the genuine") + print("retail higher-Sharpe edge. Watch: turnover cost (funding flips), and whether tail months go deeply negative.") + + +if __name__ == "__main__": + main()