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) <noreply@anthropic.com>
92 lines
4.3 KiB
Python
92 lines
4.3 KiB
Python
#!/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()
|