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>
This commit is contained in:
103
scripts/surfer/equity_lowvol_detail.py
Normal file
103
scripts/surfer/equity_lowvol_detail.py
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user