Lookback sweep smooth+positive 5-120d (peak mom_20 DSR 0.95); coin-bootstrap 200 random half-universes frac>0=1.00, 5th-pct +0.34 (survivorship-proxy passed); vol-scaled approx raw; survives 2x cost; IS+0.89/OOS+0.88. Crypto cross-sectional momentum (>=45-coin breadth) passes every test that killed carry. One weakness: negative 2019/2022 (momentum-crash) -> regime overlay. Develop-grade strongly met. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
3.1 KiB
Python
72 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
||
"""Stress-test the one survivor: crypto cross-sectional momentum.
|
||
|
||
Three robustness probes on the broad-but-clean universe (funding-cov>0.90, >900d):
|
||
1. LOOKBACK SWEEP — is the edge smooth across 5..120d (real factor) or a single spike (overfit)?
|
||
2. COIN-BOOTSTRAP — resample WHICH coins are in the cross-section 200× (survivorship proxy):
|
||
does momentum stay positive regardless of which coins survived?
|
||
3. VOL-SCALED — does risk-adjusting the momentum signal (ret/vol) help?
|
||
Validation reused from the sweep (full/IS/OOS Sharpe, CPCV, Deflated Sharpe).
|
||
"""
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
import numpy as np
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
import crypto_sweep as cs # noqa: E402
|
||
from signal_sweep import xs_weights, pnl_w, validate, sharpe_t # noqa: E402
|
||
import torch # noqa: E402
|
||
|
||
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
||
cs.MIN_FUNDCOV, cs.MIN_DAYS = 0.90, 900
|
||
|
||
|
||
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 = cs.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)
|
||
Reff = R - np.where(np.isfinite(fund), fund, 0.0)
|
||
vol30 = np.full_like(lc, np.nan)
|
||
for t in range(30, T):
|
||
vol30[t] = np.nanstd(R[t - 30:t], axis=0)
|
||
|
||
print(f"\n===== MOMENTUM ROBUSTNESS — {N} clean coins, {T}d =====")
|
||
print("\n[1] LOOKBACK SWEEP (XS momentum, ~10bp cost)")
|
||
print(f"{'lookback':>9} {'full':>6} {'IS':>6} {'OOS':>6} {'CPCVmed':>8} {'DSR':>5}")
|
||
for L in [5, 10, 20, 30, 45, 60, 90, 120]:
|
||
pnl = pnl_w(xs_weights(trailing(lc, L)), Reff, cost_bp=10)
|
||
v = validate(pnl, days, 8)
|
||
print(f"{L:>9} {v['full']:>+6.2f} {v['is_']:>+6.2f} {v['oos']:>+6.2f} {v['med']:>+8.2f} {v['dsr']:>5.2f}")
|
||
|
||
print("\n[2] VOL-SCALED vs RAW (30d)")
|
||
for nm, sg in [("raw_mom30", trailing(lc, 30)), ("volscaled_mom30", trailing(lc, 30) / np.where(vol30 > 0, vol30, np.nan))]:
|
||
v = validate(pnl_w(xs_weights(sg), Reff, cost_bp=10), days, 8)
|
||
print(f" {nm:>16}: full {v['full']:>+.2f} OOS {v['oos']:>+.2f} CPCVmed {v['med']:>+.2f}")
|
||
|
||
print("\n[3] COIN-BOOTSTRAP — mom_30 on 200 random half-universes (survivorship proxy)")
|
||
rng = np.random.default_rng(42)
|
||
sig30 = trailing(lc, 30)
|
||
sh = []
|
||
for _ in range(200):
|
||
cols = rng.choice(N, size=max(N // 2, 10), replace=False)
|
||
sub = np.full((T, N), np.nan); sub[:, cols] = sig30[:, cols]
|
||
pnl = pnl_w(xs_weights(sub), Reff[:, :], cost_bp=10) # weights already zero outside cols
|
||
s = sharpe_t(torch.tensor(pnl, device=DEV, dtype=torch.float64))
|
||
if not math.isnan(s):
|
||
sh.append(s)
|
||
sh = np.array(sh)
|
||
print(f" draws={len(sh)} mean Sharpe {sh.mean():+.2f} median {np.median(sh):+.2f} "
|
||
f"5th-pct {np.percentile(sh,5):+.2f} frac>0 {np.mean(sh>0):.2f}")
|
||
print("\nVERDICT: smooth lookback curve + bootstrap frac>0 near 1.0 + 5th-pct>0 => robust real factor.")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|