#!/usr/bin/env python3 """Tail-managed crypto VRP — does 'don't sell vol into a storm' fix the −21σ skew AND the 2025-26 decay? Pre-registered tail rules (point-in-time, gates use DVOL/return info up to t-1 only): baseline — full short-vol always (the +1.03 / -21σ version) dvol_rising — flat when implied vol is rising over 5d (don't sell into a spike) dvol_level — full size when DVOL < trailing-60d median (calm), half when elevated combo — dvol_level AND not rising loss_cap — idealized: truncate each day's loss at -3σ (stop/protection reference) vol_target — scale exposure to constant risk (inverse trailing P&L vol) Judge: Sharpe, per-year (esp 2025-26), skew, worst-day. Best -> diversifier check vs momentum. """ import json 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 surfer_poc import compute_weights, CFG # noqa: E402 from signal_sweep import sharpe_t # noqa: E402 from vrp_diversifier import dvol # noqa: E402 (cached DVOL fetch) import torch # noqa: E402 DEV = "cuda" if torch.cuda.is_available() else "cpu" def per_ccy(px, dv): days = np.array(sorted(set(px) & set(dv))) iv = np.array([dv[int(d)] / 100.0 for d in days]) r = np.full(len(days), np.nan) for i in range(1, len(days)): if px[int(days[i])] > 0 and px[int(days[i - 1])] > 0: r[i] = math.log(px[int(days[i])] / px[int(days[i - 1])]) raw = np.full(len(days), np.nan) raw[1:] = (iv[:-1] ** 2) / 365.0 - r[1:] ** 2 # short-var daily P&L (iv lagged) return days, iv, r, raw def gates(iv, raw): n = len(iv) rising = np.zeros(n) # 1 if NOT rising (ok to sell) for i in range(1, n): rising[i] = 0.0 if (i >= 6 and iv[i - 1] > iv[i - 6]) else 1.0 level = np.zeros(n) # 1 calm, 0.5 elevated (vs trailing median) for i in range(1, n): win = iv[max(0, i - 61):i - 1] level[i] = 1.0 if (len(win) > 10 and iv[i - 1] < np.median(win)) else 0.5 vt = np.ones(n) # vol-target on raw P&L for i in range(31, n): s = np.nanstd(raw[i - 30:i]) vt[i] = min(1.0, (np.nanstd(raw[~np.isnan(raw)]) / s)) if s > 0 else 1.0 return {"baseline": np.ones(n), "dvol_rising": rising, "dvol_level": level, "combo": rising * level, "vol_target": vt} def main(): syms, cdays, cclose, cqv, cfund = pit_sweep.load() idx = {s: j for j, s in enumerate(syms)} def closes(sym): j = idx[sym]; c = cclose[:, j] return {int(cdays[t]): c[t] for t in range(len(cdays)) if np.isfinite(c[t])} series = {c: per_ccy(closes(c + "USDT"), dvol(c)) for c in ["BTC", "ETH"]} # build per-rule pooled daily P&L across BTC+ETH (aligned by day) def rule_pnl(rule, cap=False): bag = {} for c, (days, iv, r, raw) in series.items(): g = gates(iv, raw)[rule] p = g * raw if cap: sd = np.nanstd(raw) p = np.maximum(p, -3 * sd) for i in range(len(days)): bag.setdefault(int(days[i]), []).append(p[i]) dd = np.array(sorted(bag)) return dd, np.array([np.nanmean(bag[int(d)]) for d in dd]) rules = ["baseline", "dvol_rising", "dvol_level", "combo", "vol_target"] print("\n===== TAIL-MANAGED CRYPTO VRP =====") print(f"{'rule':>14} {'Sharpe':>7} {'skew':>6} {'worstσ':>7} | per-year 2021..26") results = {} for rule in rules + ["loss_cap"]: dd, p = rule_pnl("baseline" if rule == "loss_cap" else rule, cap=(rule == "loss_cap")) pf = p[np.isfinite(p)] sr = sharpe_t(torch.tensor(p, device=DEV, dtype=torch.float64)) sk = float(((pf - pf.mean()) ** 3).mean() / (pf.std() ** 3 + 1e-12)) worst = float(np.nanmin(p) / (np.nanstd(p) + 1e-12)) yr = (1970 + dd / 365.25).astype(int) py = " ".join(f"{y}:{sharpe_t(torch.tensor(p[yr==y],device=DEV,dtype=torch.float64)):+.2f}" for y in range(2021, 2027) if (yr == y).sum() > 30) print(f"{rule:>14} {sr:>+7.2f} {sk:>+6.2f} {worst:>+7.1f} | {py}") results[rule] = (dd, p, sr, sk) # diversifier check for combo (the principled don't-sell-into-storm rule) dd, p, _, _ = results["combo"] cR = np.zeros_like(cclose); cR[1:] = np.log(cclose)[1:] - np.log(cclose)[:-1]; cR = np.where(np.isfinite(cR), cR, 0.0) cf = np.where(np.isfinite(cfund), cfund, 0.0) cw, _ = compute_weights(cclose, cqv, cdays, CFG) mom = np.sum(cw[:-1] * (cR - cf)[1:], axis=1) mbd = {int(cdays[1:][i]): mom[i] for i in range(len(mom))} common = sorted(set(dd.tolist()) & set(mbd)) va = np.array([p[np.where(dd == d)[0][0]] for d in common]); ma = np.array([mbd[d] for d in common]) m = np.isfinite(va) & np.isfinite(ma) corr = float(np.corrcoef(va[m], ma[m])[0, 1]) sv, sm = np.std(va[m]), np.std(ma[m]) comb = ((1 / sv) * va[m] + (1 / sm) * ma[m]) / (1 / sv + 1 / sm) cyr = (1970 + np.array(common) / 365.25).astype(int)[m] print(f"\n===== DIVERSIFIER CHECK (combo tail-managed VRP vs momentum, {m.sum()}d) =====") print(f"VRP {sharpe_t(torch.tensor(va[m],device=DEV,dtype=torch.float64)):+.2f} momentum {sharpe_t(torch.tensor(ma[m],device=DEV,dtype=torch.float64)):+.2f} CORR {corr:+.2f} combined {sharpe_t(torch.tensor(comb,device=DEV,dtype=torch.float64)):+.2f}") print("combined per-year: " + " ".join(f"{y}:{sharpe_t(torch.tensor(comb[cyr==y],device=DEV,dtype=torch.float64)):+.2f}" for y in range(2021, 2027) if (cyr == y).sum() > 30)) print("\nVERDICT: tail rule restores 2025-26 to positive + skew less extreme + combined>momentum = deployable diversifier.") if __name__ == "__main__": main()