feat(surfer): Reflexivity Harvester design — killed by marginal-alpha gate

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>
This commit is contained in:
jgrusewski
2026-06-06 19:43:42 +02:00
parent b664673df3
commit a7d6671dea

View File

@@ -0,0 +1,126 @@
#!/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()