Files
foxhunt/scripts/surfer/momentum_robustness.py
jgrusewski 0d6d58428d feat(surfer): momentum robustness battery — crypto XS momentum is a robust real edge
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>
2026-06-06 15:18:59 +02:00

72 lines
3.1 KiB
Python
Raw Permalink 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
"""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()