Files
foxhunt/scripts/surfer/equity_lowvol_detail.py
jgrusewski 0c5af2d9a0 feat(surfer): +0.74 autopsy — real anomaly, un-harvestable for our constraints
Decomposed the equity low-vol +0.74. Deployable half DEAD: long leg (low-vol, borrow-free)
alpha +0.04 / DSR 0.02 = just cohort beta. ALL edge is shorting high-vol decile (needs
expensive borrow). Episodic: top-10 days = 133pct of P&L (a few crash events). Just broke:
2026 = -2.72 (anomaly reversed). Beta-neutral residual +0.98 = genuine betting-against-beta
anomaly but lumpy/short-only/recently-inverted. Turnover low (cost was fair). Verdict: real
anomaly, un-harvestable by a small retail borrow-constrained book. Detail killed the lead
honestly. Crypto momentum+VRP remains the only deploy-grade edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:21:39 +02:00

104 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Decompose the equity low-vol +0.74 in detail: alpha vs beta, which leg, turnover, concentration.
Questions: (1) Is it cross-sectional ALPHA or a structural short-beta directional bet?
(2) Which leg carries it — long low-vol (deployable, no borrow) or short high-vol (expensive)?
(3) Turnover (is the cost charge fair?). (4) P&L concentration over time. (5) Beta-neutral residual.
"""
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from equity_factor_gate import load, roll # noqa: E402
from signal_sweep import validate, sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DV_FLOOR, VOL_PCT, COST = 1e7, 0.60, 10.0
def main():
insts, days, close, dvol = 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)
dv30 = roll(np.mean, np.nan_to_num(dvol), 30)
vol63 = roll(np.std, R, 63)
year = (1970 + days / 365.25).astype(int)
T_ = lambda x: torch.tensor(np.asarray(x)[np.isfinite(np.asarray(x))], device=DEV, dtype=torch.float64)
univ = np.zeros((T, N), bool)
for t in range(T):
e = np.where((dv30[t] > DV_FLOOR) & np.isfinite(close[t]) & np.isfinite(vol63[t]))[0]
if len(e) > 50:
univ[t, e[vol63[t, e] >= np.quantile(vol63[t, e], VOL_PCT)]] = True
def held(w, K=5):
a = 2.0 / 6
for t in range(1, T):
w[t] = a * w[t] + (1 - a) * w[t - 1]
wh = w.copy(); last = 0
for t in range(T):
if t % K == 0:
last = t
wh[t] = w[last]
return wh
def leg(which, q=0.10):
"""EW long basket of a decile: 'low'=lowest-vol, 'high'=highest-vol, 'all'=whole cohort."""
w = np.zeros((T, N))
for t in range(T):
e = np.where(univ[t])[0]
if len(e) < 20:
continue
if which == "all":
w[t, e] = 1.0 / len(e)
else:
order = e[np.argsort(vol63[t, e])] # ascending vol
k = max(int(q * len(e)), 5)
sel = order[:k] if which == "low" else order[-k:]
w[t, sel] = 1.0 / k
w = held(w)
pnl = np.sum(w[:-1] * R[1:], axis=1) - np.sum(np.abs(w[1:] - w[:-1]), axis=1) * COST / 1e4
turn = float(np.mean(np.sum(np.abs(w[1:] - w[:-1]), axis=1)))
return pnl, turn
lo, lo_turn = leg("low")
hi, hi_turn = leg("high")
mkt, _ = leg("all")
ls = lo - hi # dollar-neutral L/S (gross of the extra cost already in legs)
sr = sharpe_t
print(f"\n===== EQUITY LOW-VOL +0.74 DECOMPOSITION (cohort liquid>${DV_FLOOR/1e6:.0f}M & vol>{int(VOL_PCT*100)}pct) =====")
print(f"cohort EW (beta/market) Sharpe {sr(T_(mkt)):+.2f}")
print(f"LONG leg (low-vol decile) Sharpe {sr(T_(lo)):+.2f} alpha-vs-cohort {sr(T_(lo-mkt)):+.2f} turn/period {lo_turn:.2f}")
print(f"HIGH-vol decile Sharpe {sr(T_(hi)):+.2f} (short it -> +{sr(T_(mkt-hi)):+.2f} short-alpha-vs-cohort)")
print(f"L/S (low - high) Sharpe {sr(T_(ls)):+.2f}")
# beta decomposition of L/S vs cohort market
a, b = np.asarray(mkt), np.asarray(ls)
m = np.isfinite(a) & np.isfinite(b); x, yv = a[m], b[m]
beta = float(np.cov(x, yv)[0, 1] / (np.var(x) + 1e-12))
resid = yv - beta * x
print(f"\nL/S beta to cohort-market = {beta:+.2f} (negative = structural short-beta tilt)")
print(f"L/S beta-NEUTRAL residual alpha Sharpe = {sr(T_(resid)):+.2f} "
f"(this is the TRUE cross-sectional alpha; if ~0 it was just short-beta)")
# long-leg deployable check (no borrow)
vlo = validate(lo - mkt, days, 30) # long-leg market-neutralized (long decile vs cohort)
print(f"\nLONG-leg alpha (deployable, no borrow): full {vlo['full']:+.2f} OOS {vlo['oos']:+.2f} CPCVmed {vlo['med']:+.2f} DSR {vlo['dsr']:.2f}")
# per-year + concentration
print("per-year L/S: " + " ".join(f"{y}:{sr(T_(ls[year[1:]==y])):+.2f}" for y in range(2023,2027) if (year[1:]==y).sum()>40))
p = np.nan_to_num(ls); tot = p.sum(); top = np.sort(p)[-10:].sum()
print(f"P&L concentration: top-10 days = {100*top/ (tot+1e-12):.0f}% of total (high => episodic/fragile)")
print(f"turnover: low-leg {lo_turn:.2f}/period, high-leg {hi_turn:.2f}/period (low => slow signal, cost charge fair)")
print("\nREAD: if beta-neutral residual ~0 => short-beta bet (fragile); if long-leg alpha DSR>0.5 => deployable long-only edge.")
if __name__ == "__main__":
main()