13 crypto signals, deflated by cumulative 30. Carry family all develop-grade, survive 2x cost; per-year reveals THE failure mode = 2022 deleveraging carry-crash (carry_7 -1.2 in 2022, +0.4..+2.1 other years). Momentum OOS-negative (decayed); chasing rising funding strongly negative. Carry is the robust core; its one tail (carry crash) is exactly what the ML regime overlay is built to manage -> the two threads unite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
5.1 KiB
Python
120 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
||
"""Deflated signal sweep — Batch 2 (crypto perps): broaden + robustness-check.
|
||
|
||
Cross-sectional, market-neutral, unit-gross signals on major Binance USDT perps. Funding
|
||
baked into the return (Reff = log-return − daily funding; long pays positive funding). ~10bp
|
||
taker cost on turnover. Deflated Sharpe deflated by the CUMULATIVE search (Batch1 17 + these).
|
||
For any DEVELOP-grade signal (CPCVmed>0 & IS>0 & OOS>0): also report 2× cost + per-year Sharpe
|
||
(regime robustness). Survivorship caveat: v1 = currently-liquid majors over history.
|
||
"""
|
||
import glob
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
import numpy as np
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
|
||
import torch # noqa: E402
|
||
|
||
PRIOR_TRIALS = 17 # Batch 1 (futures factor zoo)
|
||
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
||
|
||
|
||
def load_crypto():
|
||
syms, data = [], {}
|
||
for p in sorted(glob.glob("data/surfer/crypto/*.npz")):
|
||
d = np.load(p); s = p.split("/")[-1][:-4]
|
||
data[s] = (d["day"].astype(np.int64), d["close"].astype(float), d["funding"].astype(float))
|
||
syms.append(s)
|
||
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 zc(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))
|
||
|
||
|
||
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
|
||
vol30 = np.full_like(lc, np.nan)
|
||
for t in range(30, T):
|
||
vol30[t] = np.nanstd(R[t - 30:t], axis=0)
|
||
|
||
def tmf(K): # trailing mean funding over K days
|
||
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
|
||
|
||
f7, f3, f14 = tmf(7), tmf(3), tmf(14)
|
||
W = {} # name -> weights[T,N]
|
||
for K in [3, 7, 14, 30]:
|
||
W[f"XS_carry_{K}"] = xs_weights(-tmf(K))
|
||
for L in [7, 30, 90]:
|
||
W[f"XS_mom_{L}"] = xs_weights(trailing(lc, L))
|
||
W["XS_rev_3"] = xs_weights(-trailing(lc, 3))
|
||
W["XS_fundmom"] = xs_weights(f3 - f14) # rising funding (positioning building)
|
||
W["XS_lowvol"] = xs_weights(-vol30)
|
||
W["XS_carry+mom30"] = xs_weights(zc(-f7) + zc(trailing(lc, 30)))
|
||
W["XS_carry+rev3"] = xs_weights(zc(-f7) + zc(-trailing(lc, 3)))
|
||
W["XS_carry+lowvol"] = xs_weights(zc(-f7) + zc(-vol30))
|
||
|
||
NT = PRIOR_TRIALS + len(W)
|
||
year = (1970 + days / 365.25).astype(int)
|
||
rows = []
|
||
pnls = {}
|
||
for nm, w in W.items():
|
||
pnl = pnl_w(w, Reff, cost_bp=10)
|
||
pnls[nm] = (w, pnl)
|
||
rows.append((nm, validate(pnl, days, NT)))
|
||
rows.sort(key=lambda r: -(r[1]["oos"] if not math.isnan(r[1]["oos"]) else -9))
|
||
print(f"\n===== CRYPTO SWEEP (broadened) — {N} perps, {T}d ({days.min()}..{days.max()}), fundcov={np.mean(fund!=0):.2f} =====")
|
||
print(f"N_trials(cumulative deflation) = {NT}")
|
||
print(f"{'signal':>16} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'5%':>6} {'DSR':>5}")
|
||
for nm, v in rows:
|
||
print(f"{nm:>16} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['p5']:>+6.2f} {v['dsr']:>5.2f}")
|
||
|
||
print("\n----- ROBUSTNESS for develop-grade signals (CPCVmed>0 & IS>0 & OOS>0) -----")
|
||
yrs = sorted(set(year[1:].tolist()))
|
||
print(f"{'signal':>16} {'full10bp':>9} {'full20bp':>9} | per-year Sharpe: " + " ".join(f"{y}" for y in yrs))
|
||
for nm, v in rows:
|
||
if v["med"] > 0 and v["is_"] > 0 and v["oos"] > 0:
|
||
w, pnl = pnls[nm]
|
||
pnl2 = pnl_w(w, Reff, cost_bp=20)
|
||
s10 = sharpe_t(torch.tensor(pnl, device=DEV, dtype=torch.float64))
|
||
s20 = sharpe_t(torch.tensor(pnl2, device=DEV, dtype=torch.float64))
|
||
py = []
|
||
for y in yrs:
|
||
m = year[1:] == y
|
||
py.append(sharpe_t(torch.tensor(pnl[m], device=DEV, dtype=torch.float64)) if m.sum() > 30 else float("nan"))
|
||
print(f"{nm:>16} {s10:>+9.2f} {s20:>+9.2f} | " + " ".join(f"{p:>+5.2f}" if not math.isnan(p) else " n/a" for p in py))
|
||
|
||
surv = [nm for nm, v in rows if v["dsr"] > 0.95 and v["oos"] > 0 and v["med"] > 0]
|
||
print(f"\nDEPLOY survivors (DSR>0.95 & OOS>0 & CPCVmed>0): {surv if surv else 'NONE'}")
|
||
print("Develop-grade = robust real edge worth building; deploy-grade = DSR>0.95 (harsh, deflated by all trials).")
|
||
print("Caveat: survivorship (current majors); confirm point-in-time next.")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|