Full gauntlet on the lottery-aversion lead (liquid high-vol cohort). BASE L/S +0.74/OOS +0.72 but: name-bootstrap frac>0 only 0.77 (fragile vs momentum's 1.00); edge lives at $5-20M floor (smaller names, real spreads > the 10bp assumed), dies at $50M; the L/S edge is mostly shorting hard-to-borrow hype names (30%/yr borrow -> +0.04 gone); long-only realistic = +0.33, DSR 0.07 (not significant). corr to crypto book -0.05 (uncorrelated -> would diversify IF real). DSR 0.22/0.07 -- neither clears 0.5. Closest non-crypto market, theoretically sound, uncorrelated -- but marginal after realistic borrow+small-name cost, NOT deploy-grade. A lead on the shelf, not a strategy. Crypto momentum+VRP remains the only deploy-grade edge. Re-confirms the boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
130 lines
5.6 KiB
Python
130 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Full gauntlet on the equity low-vol/lottery-aversion lead (liquid high-vol cohort).
|
|
|
|
The make-or-break for the first non-crypto edge: (1) name-bootstrap (robust to WHICH names? —
|
|
the test that killed carry), (2) cohort-param robustness (did $10M/60pct manufacture it?),
|
|
(3) realistic short-borrow + long-only (is the edge trapped in the un-cheap short?),
|
|
(4) honest deflation + per-year, (5) correlation to the crypto momentum book (diversifier?).
|
|
Free — DBEQ on disk.
|
|
"""
|
|
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 xs_weights, validate, sharpe_t # noqa: E402
|
|
import pit_sweep # noqa: E402
|
|
from surfer_poc import compute_weights, CFG # noqa: E402
|
|
import torch # noqa: E402
|
|
|
|
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
|
|
|
|
|
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(x[np.isfinite(x)], device=DEV, dtype=torch.float64)
|
|
|
|
def cohort(dv_floor, vol_pct):
|
|
u = np.zeros((T, N), bool)
|
|
for t in range(T):
|
|
liq = (dv30[t] > dv_floor) & np.isfinite(close[t]) & np.isfinite(vol63[t])
|
|
e = np.where(liq)[0]
|
|
if len(e) > 50:
|
|
u[t, e[vol63[t, e] >= np.quantile(vol63[t, e], vol_pct)]] = True
|
|
return u
|
|
|
|
def smooth_weekly(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 lowvol_pnl(univ, cols=None, long_only=False, borrow_ann=0.0):
|
|
sig = (-vol63).copy(); sig[~univ] = np.nan
|
|
if cols is not None:
|
|
mask = np.zeros(N, bool); mask[cols] = True; sig[:, ~mask] = np.nan
|
|
if long_only:
|
|
w = np.zeros((T, N))
|
|
for t in range(T):
|
|
e = np.where(np.isfinite(sig[t]))[0]
|
|
if len(e) > 20:
|
|
k = max(int(0.10 * len(e)), 5); w[t, e[np.argsort(-sig[t, e])[:k]]] = 1.0 / k
|
|
else:
|
|
w = xs_weights(sig)
|
|
w = smooth_weekly(w)
|
|
g = np.sum(w[:-1] * R[1:], axis=1)
|
|
g -= np.sum(np.abs(w[1:] - w[:-1]), axis=1) * 10.0 / 1e4 # 10bp trade cost
|
|
if borrow_ann > 0: # borrow on short notional
|
|
short_notional = np.sum(np.clip(-w[:-1], 0, None), axis=1)
|
|
g -= short_notional * borrow_ann / 252.0
|
|
return g
|
|
|
|
base = cohort(1e7, 0.60)
|
|
base_pnl = lowvol_pnl(base)
|
|
v = validate(base_pnl, days, 30)
|
|
print(f"\n===== EQUITY LOW-VOL GAUNTLET (liquid high-vol cohort, deflate N=30) =====")
|
|
print(f"BASE L/S: full {v['full']:+.2f} OOS {v['oos']:+.2f} CPCVmed {v['med']:+.2f} DSR {v['dsr']:.2f}")
|
|
|
|
# (1) name-bootstrap
|
|
cohort_cols = np.where(base.any(0))[0]
|
|
rng = np.random.default_rng(5); sh = []
|
|
for _ in range(200):
|
|
c = rng.choice(cohort_cols, size=max(len(cohort_cols) // 2, 20), replace=False)
|
|
s = sharpe_t(T_(lowvol_pnl(base, cols=c)))
|
|
if not math.isnan(s):
|
|
sh.append(s)
|
|
sh = np.array(sh)
|
|
print(f"(1) name-bootstrap(200): frac>0 {np.mean(sh>0):.2f} median {np.median(sh):+.2f} 5th {np.percentile(sh,5):+.2f}")
|
|
|
|
# (2) cohort-param robustness
|
|
print("(2) cohort-param grid (L/S full Sharpe):")
|
|
for fl in [5e6, 1e7, 2e7, 5e7]:
|
|
row = []
|
|
for vp in [0.50, 0.60, 0.70]:
|
|
row.append(sharpe_t(T_(lowvol_pnl(cohort(fl, vp)))))
|
|
print(f" ${fl/1e6:>3.0f}M floor: " + " ".join(f"vp{int(vp*100)}:{r:+.2f}" for vp, r in zip([0.50,0.60,0.70], row)))
|
|
|
|
# (3) short-borrow + long-only
|
|
print("(3) short-borrow & long-only (net):")
|
|
for ba in [0.0, 0.10, 0.30]:
|
|
print(f" L/S borrow {int(ba*100)}%/yr: {sharpe_t(T_(lowvol_pnl(base, borrow_ann=ba))):+.2f}")
|
|
lo = lowvol_pnl(base, long_only=True)
|
|
vlo = validate(lo, days, 30)
|
|
print(f" LONG-ONLY (no borrow): full {vlo['full']:+.2f} OOS {vlo['oos']:+.2f} CPCVmed {vlo['med']:+.2f} DSR {vlo['dsr']:.2f}")
|
|
|
|
# (4) per-year (base L/S)
|
|
print("(4) per-year (base L/S): " + " ".join(f"{y}:{sharpe_t(T_(base_pnl[year[1:]==y])):+.2f}" for y in range(2023,2027) if (year[1:]==y).sum()>40))
|
|
|
|
# (5) correlation to crypto momentum book
|
|
syms, cdays, cc, cqv, cf = pit_sweep.load()
|
|
cw, _ = compute_weights(cc, cqv, cdays, CFG)
|
|
cR = np.zeros_like(cc); cR[1:] = np.log(cc)[1:] - np.log(cc)[:-1]; cR = np.where(np.isfinite(cR), cR, 0.0)
|
|
cff = np.where(np.isfinite(cf), cf, 0.0)
|
|
cmom = np.sum(cw[:-1] * (cR - cff)[1:], axis=1)
|
|
cmd = {int(cdays[1:][i]): cmom[i] for i in range(len(cmom))}
|
|
emd = {int(days[1:][i]): base_pnl[i] for i in range(len(base_pnl))}
|
|
common = sorted(set(cmd) & set(emd))
|
|
a = np.array([cmd[d] for d in common]); b = np.array([emd[d] for d in common])
|
|
m = np.isfinite(a) & np.isfinite(b)
|
|
corr = float(np.corrcoef(a[m], b[m])[0, 1]) if m.sum() > 50 else float("nan")
|
|
print(f"(5) corr to crypto-momentum book: {corr:+.2f} (low => diversifier in a preferred market)")
|
|
print("\nVERDICT: bootstrap frac>0~1 + grid broadly + long-only positive + DSR>0.5 + low crypto-corr = real 2nd sleeve.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|