Files
foxhunt/scripts/surfer/crypto_sweep.py
jgrusewski cb402ab81f feat(surfer): crypto carry+momentum sweep — FIRST real edge (funding carry)
Batch 2 of the deflated signal sweep: cross-sectional funding carry + momentum on
28 major Binance USDT perps (2019-26, funding baked into return, 10bp cost, deflated
by cumulative 25 trials). XS_carry_7 = Sharpe +0.82, IS+0.77/OOS+0.93 (consistent),
CPCV-median +0.78 (robust across 45 paths), DSR 0.71. First signal all session that is
positive + IS/OOS-consistent + CPCV-median-positive. Develop-grade met, not yet deploy
(DSR<0.95, 5th-pct<0). Caveats: survivorship (current majors), confirm point-in-time.
Forward-paginated funding fetch (fundcov ~1.0).

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

97 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Deflated signal sweep — Batch 2: crypto cross-sectional carry + momentum (Binance perps).
The less-efficient-market bet. Universe = curated major USDT perps (survivorship caveat: v1
uses currently-liquid majors over history → excludes dead coins; documented, not point-in-time).
Funding is baked into the return: a long PAYS positive funding, a short EARNS it, so
Reff = log-return daily_funding. Signals are cross-sectional, market-neutral, unit-gross:
carry (long low/neg funding), momentum (multi-horizon), short reversal, and combos. Each →
daily long-short PnL net of ~10bp taker cost → full/IS/OOS Sharpe, CPCV(45), Deflated Sharpe
deflated by the CUMULATIVE search (Batch1 17 + this batch). Survivor = DSR>0.95 & OOS>0 & CPCVmed>0.
"""
import glob
import math
import sys
import os
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import xs_weights, pnl_w, validate # noqa: E402
PRIOR_TRIALS = 17 # Batch 1 (futures factor zoo) — deflate cumulatively
def load_crypto():
syms, data = [], {}
for p in sorted(glob.glob("data/surfer/crypto/*.npz")):
d = np.load(p)
sym = p.split("/")[-1][:-4]
data[sym] = (d["day"].astype(np.int64), d["close"].astype(float), d["funding"].astype(float))
syms.append(sym)
syms = sorted(syms)
days = np.array(sorted(set().union(*[set(data[s][0].tolist()) for s in syms])))
di = {d: i for i, d in enumerate(days.tolist())}
T, N = len(days), len(syms)
close = np.full((T, N), np.nan); fund = np.full((T, N), np.nan)
for j, s in enumerate(syms):
dd, cc, ff = data[s]
for k in range(len(dd)):
r = di[int(dd[k])]; close[r, j] = cc[k]; fund[r, j] = ff[k]
return syms, days, close, fund
def trailing(lc, L):
out = np.full_like(lc, np.nan); out[L:] = lc[L:] - lc[:-L]; return out
def main():
syms, days, close, fund = load_crypto()
T, N = close.shape
lc = np.log(close)
R = np.zeros((T, N)); R[1:] = lc[1:] - lc[:-1]
R = np.where(np.isfinite(R), R, 0.0)
fund = np.where(np.isfinite(fund), fund, 0.0)
Reff = R - fund # funding baked into the long return
cov = float(np.mean(fund != 0))
print(f"\n===== DEFLATED SIGNAL SWEEP — Batch 2 (crypto perps, carry+momentum) =====")
print(f"symbols={N} days={T} ({days.min()}..{days.max()}) funding-coverage={cov:.2f}")
if cov < 0.5:
print("WARNING: funding coverage low — carry signal unreliable.")
# mean funding over last K days (the carry state)
def trail_mean_fund(K):
out = np.full_like(fund, np.nan)
for t in range(K, T):
out[t] = np.nanmean(fund[t - K:t], axis=0)
return out
sig = {}
sig["XS_carry_3"] = pnl_w(xs_weights(-trail_mean_fund(3)), Reff, cost_bp=10) # long low funding
sig["XS_carry_7"] = pnl_w(xs_weights(-trail_mean_fund(7)), Reff, cost_bp=10)
for L in [7, 30, 90]:
sig[f"XS_mom_{L}"] = pnl_w(xs_weights(trailing(lc, L)), Reff, cost_bp=10)
sig["XS_rev_3"] = pnl_w(xs_weights(-trailing(lc, 3)), Reff, cost_bp=10)
# combos (avg of cross-sectional z-scores)
def z(x):
mu = np.nanmean(x, axis=1, keepdims=True); sd = np.nanstd(x, axis=1, keepdims=True)
return np.nan_to_num((x - mu) / np.where(sd > 0, sd, 1))
sig["XS_carry+mom30"] = pnl_w(xs_weights(z(-trail_mean_fund(7)) + z(trailing(lc, 30))), Reff, cost_bp=10)
sig["XS_carry+rev3"] = pnl_w(xs_weights(z(-trail_mean_fund(7)) + z(-trailing(lc, 3))), Reff, cost_bp=10)
NT = PRIOR_TRIALS + len(sig)
rows = [(nm, validate(p, days, NT)) for nm, p in sig.items()]
rows.sort(key=lambda r: -(r[1]["oos"] if not math.isnan(r[1]["oos"]) else -9))
print(f"N_trials(cumulative deflation) = {NT}")
print(f"{'signal':>16} {'full':>7} {'IS':>7} {'OOS':>7} {'CPCVmed':>8} {'CPCV5%':>8} {'DSR':>6}")
for nm, v in rows:
print(f"{nm:>16} {v['full']:>+7.2f} {v['is_']:>+7.2f} {v['oos']:>+7.2f} "
f"{v['med']:>+8.2f} {v['p5']:>+8.2f} {v['dsr']:>6.2f}")
surv = [nm for nm, v in rows if v["dsr"] > 0.95 and v["oos"] > 0 and v["med"] > 0]
print(f"\nSURVIVORS (DSR>0.95 & OOS>0 & CPCVmed>0): {surv if surv else 'NONE'}")
print("Funding baked into Reff; ~10bp taker cost on turnover. Survivorship: current majors (v1 caveat).")
if __name__ == "__main__":
main()