Synthesized all findings into a novel crypto-native design (residual-momentum long + exhaustion/death-weighted short + vol-regime gross-gate), spawned 4 research agents, ran the decisive test the critic+exhaustion-researcher both named. Exhaustion-short is momentum RE-SPELLED: corr +0.82 to residual momentum, marginal alpha t -0.37 (negative), combined OOS +0.50 < momentum-alone +0.77 (adding it HURTS). Momentum ALREADY harvests the crypto death-alpha (it already shorts dying coins -- the survivorship finding); explicit death-features double-count. Regime-gate untestable (n~4 crashes). Ship Comp-1. Product unchanged: residual-momentum (OOS +0.77, CPCVmed +0.82, DSR 0.57 deflated-by-50) + tail-managed VRP. The marginal-alpha gate killed our own creative design = discipline working. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
127 lines
5.5 KiB
Python
127 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""The decisive gate both the critic and the exhaustion-researcher named: does the
|
|
exhaustion-short carry MARGINAL ALPHA over plain residual momentum, or is it collinear?
|
|
|
|
Build, on the PIT crypto panel: (1) residual-momentum L/S book [validated], (2) exhaustion
|
|
L/S book (z-sum of: neg residual-mom, fading dollar-volume rank, extreme funding, drawdown).
|
|
Then: correlation, and regress exhaustion daily returns on momentum daily returns -> the
|
|
intercept (alpha) is what matters. High corr + ~0 alpha => momentum re-spelled => drop it.
|
|
Also A/B the squeeze veto (no short when funding<=0). Realistic: Reff = return - funding
|
|
(shorts pay positive funding), death-excl, 10bp.
|
|
"""
|
|
import math
|
|
import os
|
|
import sys
|
|
|
|
import numpy as np
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import pit_sweep # 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"
|
|
TOPK = 50
|
|
|
|
|
|
def roll(fn, X, L):
|
|
out = np.full_like(X, np.nan)
|
|
for t in range(L, len(X)):
|
|
out[t] = fn(X[t - L:t], axis=0)
|
|
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, qv, fund = pit_sweep.load()
|
|
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)
|
|
qv = np.nan_to_num(qv)
|
|
dv30 = roll(np.mean, qv, 30)
|
|
tradeable = np.ones((T, N), bool)
|
|
for j in range(N):
|
|
idx = np.where(np.isfinite(close[:, j]))[0]
|
|
if len(idx):
|
|
tradeable[max(0, idx[-1] - 4):idx[-1] + 1, j] = False
|
|
Reff = np.where(tradeable, R - fund, 0.0)
|
|
univ = np.zeros((T, N), bool)
|
|
for t in range(T):
|
|
elig = np.where((dv30[t] > 0) & np.isfinite(close[t]))[0]
|
|
if len(elig):
|
|
univ[t, elig[np.argsort(-dv30[t, elig])[:TOPK]]] = True
|
|
year = (1970 + days / 365.25).astype(int)
|
|
|
|
# residual (beta-stripped) momentum
|
|
mkt = np.array([R[t][univ[t]].mean() if univ[t].any() else 0.0 for t in range(T)])
|
|
beta = np.zeros((T, N)); Wb = 60
|
|
for t in range(Wb, T):
|
|
mw = mkt[t - Wb:t]; vb = mw.var() + 1e-12
|
|
beta[t] = ((R[t - Wb:t] * mw[:, None]).mean(0) - R[t - Wb:t].mean(0) * mw.mean()) / vb
|
|
rcum = np.cumsum(R - beta * mkt[:, None], axis=0)
|
|
resid_mom = np.full((T, N), np.nan); resid_mom[20:] = rcum[20:] - rcum[:-20]
|
|
|
|
# exhaustion terms (all oriented so HIGH => short candidate)
|
|
dvr = np.full((T, N), np.nan) # cross-sectional dollar-vol rank (0..1)
|
|
for t in range(T):
|
|
e = np.where(univ[t])[0]
|
|
if len(e) > 1:
|
|
r = dv30[t, e].argsort().argsort().astype(float) / (len(e) - 1)
|
|
dvr[t, e] = r
|
|
dvr_slope = np.full((T, N), np.nan); dvr_slope[30:] = dvr[30:] - dvr[:-30] # falling rank = fading
|
|
fund7 = roll(np.mean, fund, 7) # crowded-long carry
|
|
rmax = roll(np.max, lc, 90); dd = lc - rmax # drawdown-from-ATH (<=0)
|
|
|
|
exh = zc(-resid_mom) + zc(-dvr_slope) + zc(fund7) + zc(-dd) # additive, equal-weight (no fitting)
|
|
|
|
def book(sig, veto_short=None):
|
|
s = sig.copy(); s[~univ] = np.nan
|
|
w = xs_weights(s)
|
|
if veto_short is not None:
|
|
w = np.where((w < 0) & veto_short, 0.0, w) # drop shorts where veto true
|
|
return pnl_w(w, Reff, cost_bp=10)
|
|
|
|
pnl_mom = book(resid_mom)
|
|
pnl_exh = book(-exh) # long low-exhaustion / short high
|
|
squeeze_veto = fund <= 0 # don't short crowded-short (squeeze fuel)
|
|
pnl_exh_veto = book(-exh, veto_short=squeeze_veto)
|
|
|
|
def stats(p):
|
|
v = validate(p, days, 50); return v
|
|
T_ = lambda x: torch.tensor(x[np.isfinite(x)], device=DEV, dtype=torch.float64)
|
|
|
|
print(f"\n===== EXHAUSTION-SHORT MARGINAL-ALPHA GATE (PIT top{TOPK}, death-excl, 10bp, deflate N=50) =====")
|
|
for nm, p in [("residual_momentum", pnl_mom), ("exhaustion", pnl_exh), ("exhaustion+veto", pnl_exh_veto)]:
|
|
v = stats(p)
|
|
print(f" {nm:>18}: full {v['full']:+.2f} IS {v['is_']:+.2f} OOS {v['oos']:+.2f} CPCVmed {v['med']:+.2f} DSR {v['dsr']:.2f}")
|
|
|
|
# THE decisive test: regress exhaustion returns on momentum returns -> marginal alpha
|
|
a, b = pnl_mom, pnl_exh
|
|
m = np.isfinite(a) & np.isfinite(b)
|
|
x, y = a[m], b[m]
|
|
corr = float(np.corrcoef(x, y)[0, 1])
|
|
beta1 = float(np.cov(x, y)[0, 1] / (np.var(x) + 1e-12))
|
|
resid = y - beta1 * x
|
|
alpha_daily = float(resid.mean())
|
|
alpha_ann_sr = alpha_daily / (resid.std() + 1e-12) * math.sqrt(365)
|
|
alpha_t = alpha_daily / (resid.std() / math.sqrt(len(resid)) + 1e-12)
|
|
print(f"\n REGRESS exhaustion ~ momentum: corr {corr:+.2f} beta {beta1:+.2f}")
|
|
print(f" marginal alpha: ann-Sharpe {alpha_ann_sr:+.2f} t-stat {alpha_t:+.2f} (>2 = real distinct edge)")
|
|
|
|
# does combining beat momentum alone (OOS)?
|
|
sm, se = np.nanstd(a), np.nanstd(b)
|
|
comb = (np.nan_to_num(a) / sm + np.nan_to_num(b) / se)
|
|
vc = stats(comb); vm = stats(pnl_mom)
|
|
print(f"\n combined(mom+exh) OOS {vc['oos']:+.2f} vs momentum-alone OOS {vm['oos']:+.2f} "
|
|
f"=> {'ADDS' if vc['oos'] > vm['oos'] + 0.1 else 'no improvement'}")
|
|
print("\nVERDICT: high corr + alpha t<2 + no OOS improvement => exhaustion is momentum re-spelled -> DROP (ship Comp-1).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|