Files
foxhunt/scripts/surfer/crypto_funding_liveness.py
jgrusewski 9c2c38eb5d feat(crypto): funding carry liveness — ALIVE (regime not decay), regime filter rescues 2025-26
Decisive liveness investigation. Decay diagnostic: positive-funding yield intact (mean_pos
3.0-3.4bp in 2025-26 = same as 2019/2023), NOT compressed. What changed is BREADTH: frac_pos
0.88(2024)->0.59(2026), avg funding negative = deleverage regime. Naive harvest whipsawed
(2025-26 Sharpe -5.8); regime filter (trailing funding > hurdle, sit out the rest) RESCUES to
+17-22pct APR, Sharpe +6.7-9.1. Edge is alive = cross-sectional selection, not death. Caveats:
raw Sharpe inflated by missing basis vol (realistic ~2.5-4); hurdle needs OOS confirm;
counterparty tail unchanged. Strongest deployable edge of the search: ~2.5-4 Sharpe
market-neutral, alive today, crypto + counterparty tail as cost of admission.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 01:20:04 +02:00

98 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""Decay/liveness: is the crypto funding carry still ALIVE now, or competed to death?
The harvest was +12 Sharpe 2020-24 but negative 2022, 2025, 2026. Decisive question: is that
(a) STRUCTURAL COMPRESSION (funding yield shrinking year-over-year = arbitraged away = dead) or
(b) REGIME (funding flips negative in deleverage, recovers = dormant)? And does a regime filter
(only harvest reliably-positive funding, else sit in cash) rescue the recent period?
Diagnostics: (1) funding-yield decay table by year; (2) quarterly net-harvest liveness timeline;
(3) regime-filtered vs unfiltered in the recent window.
"""
import datetime
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 sharpe(r):
r = r[np.isfinite(r)]
return float("nan") if len(r) < 20 or r.std() == 0 else r.mean() / r.std() * math.sqrt(365)
def harvest(fund, elig, hurdle, c_round, trail=None):
T, N = fund.shape
cond = fund > hurdle
if trail: # regime filter: trailing-mean funding > hurdle
tf = np.full_like(fund, np.nan)
for t in range(trail, T):
tf[t] = np.nanmean(fund[t - trail:t], axis=0)
cond = tf > hurdle
sig = np.zeros((T, N))
sig[1:] = np.where(elig[1:] & cond[:-1], 1.0, 0.0) # causal
w = sig / np.maximum(sig.sum(1, keepdims=True), 1)
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)
net = gross - turn * (c_round / 2)
inmkt = sig.sum(1)[1:] > 0 # days actually positioned
return net, inmkt
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)
elig = np.isfinite(qv30) & (qv30 > 5e6) & np.isfinite(fund)
print("\n===== (1) FUNDING-YIELD DECAY by year (compression vs regime) =====")
print(f"{'year':>6} {'#coins':>7} {'frac_pos':>9} {'mean_all(bp)':>13} {'mean_pos(bp)':>13}")
for y in range(2019, 2027):
m = year == y
e = elig[m]
fy = fund[m][e]
if len(fy) < 100:
continue
ncoin = int(e.sum(1).mean()) if e.ndim == 2 else int(e.mean() * N)
fpos = float((fy > 0).mean())
print(f"{y:>6} {ncoin:>7} {fpos:>9.2f} {1e4*fy.mean():>13.2f} {1e4*fy[fy > 0].mean():>13.2f}")
print("\n===== (2) QUARTERLY net-harvest liveness (broad, thr=0, 10bp) =====")
net, _ = harvest(fund, elig, 0.0, 0.0010)
q = ((days[1:] - days[1]) / 91.3).astype(int)
dd = days[1:]
print(f"{'quarter':>10} {'netAPR%':>8} {'Sharpe':>7}")
for qq in sorted(set(q)):
seg = net[q == qq]
d0 = datetime.date.fromordinal(int(dd[q == qq][0]) + 719163)
if d0.year >= 2024: # focus recent
print(f"{str(d0):>10} {100 * seg.sum() * (365 / max(len(seg), 1)) / 1:>8.1f} {sharpe(seg):>7.1f}")
print("\n===== (3) REGIME FILTER rescue test (recent: 2025-2026) =====")
recent = year[1:] >= 2025
variants = [("unfiltered thr=0 10bp", harvest(fund, elig, 0.0, 0.0010)[0]),
("filtered tf14>3bp 10bp", harvest(fund, elig, 0.0003, 0.0010, trail=14)[0]),
("filtered tf14>5bp 10bp", harvest(fund, elig, 0.0005, 0.0010, trail=14)[0]),
("filtered tf30>5bp 10bp", harvest(fund, elig, 0.0005, 0.0010, trail=30)[0])]
print(f"{'variant':>24} {'2025-26 APR%':>13} {'2025-26 Sharpe':>15} {'full Sharpe':>12}")
for nm, r in variants:
rr = r[recent]
apr = 100 * np.nansum(rr) * 365 / max(np.isfinite(rr).sum(), 1)
print(f"{nm:>24} {apr:>13.1f} {sharpe(rr):>15.1f} {sharpe(r):>12.1f}")
print("\nVERDICT: if mean_pos(bp) stable across years but frac_pos dipped 2025-26 = REGIME (dormant, recoverable).")
print("If mean_pos shrinks monotonically = COMPRESSION (competed to death). If a filter turns 2025-26 positive")
print("= alive-but-needs-regime-timing. If even filtered 2025-26 is negative/zero = the edge is GONE now.")
if __name__ == "__main__":
main()