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>
97 lines
4.3 KiB
Python
97 lines
4.3 KiB
Python
#!/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()
|