Files
foxhunt/scripts/surfer/vrp_tail.py
jgrusewski ad7feb61ff feat(surfer): tail-managed VRP — real second sleeve (with honest haircut)
'Don't sell vol when implied vol rising' (point-in-time, no lookahead verified) takes
crypto short-vol baseline +1.03 (skew -8.3, 2025 -0.26/2026 -0.77) to combo +2.89,
ALL years positive (2026 +2.26), worst -21sigma->-17sigma. Effect robust across gate
variants (rising/level/combo/loss_cap all fix 2025-26) = real economic effect (implied
leads realized), not one lucky rule. Confirms the -21sigma tail + recent decay were the
same mis-timing. Diversifier: corr -0.04, combined +2.57. HONEST HAIRCUT: +2.9 optimistic
(gate-selection best-of-5 + no-Greeks proxy idealization + residual -15sigma tail + 5y
sample) -> realistic ~1.0-1.5; but corr+edge robust -> combined ~1.37 > momentum 0.66.
Real second sleeve, size SMALL (tail reduced not gone). Best diversifier found.

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

119 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()