Vol-risk-premium (short-vol BTC+ETH, Deribit DVOL implied minus realized, short-variance daily P&L proxy). VRP Sharpe +1.03, CORRELATION to momentum -0.03 (genuinely uncorrelated), combined +1.22 (~doubles momentum-alone +0.66). FIRST genuine diversifier found -- has BOTH edge AND low-corr (futures trend had corr but no edge -> diluted). CAVEATS: (1) catastrophic negative skew -8.32, worst-day -21sigma (short-vol blowup risk; Sharpe flatters; must tail-manage + size small) (2) recent decay -- negative 2025/2026 (+1.22 is a 2021-24 artifact). Real and promising, categorically better than futures, but pursue carefully not naive deploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
5.0 KiB
Python
112 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
||
"""Crypto volatility-risk-premium (VRP) as a diversifier to the momentum book.
|
||
|
||
VRP = implied vol (Deribit DVOL, BTC+ETH) systematically exceeds realized vol; a vol-seller
|
||
harvests it. Short-variance daily P&L proxy: vrp_t = (DVOL_{t-1}/100)^2/365 - r_t^2
|
||
(collect implied daily variance, pay realized squared return). Approximate (no Greeks) but
|
||
captures the premium's edge + correlation — enough for a diversifier CHECK.
|
||
|
||
Then the prize: Sharpe of VRP, per-year (incl. crash years), worst day, CORRELATION to the
|
||
crypto momentum book, and the combined two-premium book. A real diversifier needs Sharpe>=~0.4
|
||
AND low correlation. Honest: short-vol has negative skew -> Sharpe FLATTERS it; watch the tails.
|
||
"""
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
import urllib.request
|
||
|
||
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
|
||
import torch # noqa: E402
|
||
|
||
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
||
DAY_MS = 86_400_000
|
||
DVOL_DIR = "data/surfer/dvol"
|
||
|
||
|
||
def _get(u):
|
||
return json.load(urllib.request.urlopen(urllib.request.Request(u, headers={"User-Agent": "curl/8"}), timeout=30))
|
||
|
||
|
||
def dvol(ccy):
|
||
os.makedirs(DVOL_DIR, exist_ok=True)
|
||
cache = f"{DVOL_DIR}/{ccy}.json"
|
||
if os.path.exists(cache):
|
||
return {int(k): v for k, v in json.load(open(cache)).items()}
|
||
out, end, start = {}, 1780704000000, 1577836800000
|
||
for _ in range(8):
|
||
u = f"https://www.deribit.com/api/v2/public/get_volatility_index_data?currency={ccy}&start_timestamp={start}&end_timestamp={end}&resolution=86400"
|
||
d = _get(u).get("result", {}).get("data", [])
|
||
if not d:
|
||
break
|
||
for row in d:
|
||
out[int(row[0]) // DAY_MS] = float(row[4])
|
||
end = d[0][0] - 1
|
||
if len(d) < 1000:
|
||
break
|
||
json.dump({str(k): v for k, v in out.items()}, open(cache, "w"))
|
||
return out
|
||
|
||
|
||
def main():
|
||
syms, cdays, cclose, cqv, cfund = pit_sweep.load()
|
||
idx = {s: j for j, s in enumerate(syms)}
|
||
# realized daily returns for BTC/ETH
|
||
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])}
|
||
btc, eth = closes("BTCUSDT"), closes("ETHUSDT")
|
||
dv_btc, dv_eth = dvol("BTC"), dvol("ETH")
|
||
print(f"DVOL coverage: BTC {len(dv_btc)}d ({min(dv_btc)}..{max(dv_btc)}), ETH {len(dv_eth)}d")
|
||
|
||
def vrp_series(px, dv):
|
||
days = sorted(set(px) & set(dv))
|
||
pnl = {}
|
||
for i in range(1, len(days)):
|
||
d0, d1 = days[i - 1], days[i]
|
||
if px[d0] > 0 and px[d1] > 0:
|
||
r = math.log(px[d1] / px[d0])
|
||
iv = dv[d0] / 100.0
|
||
pnl[d1] = (iv * iv) / 365.0 - r * r # short-variance daily P&L
|
||
return pnl
|
||
vb, ve = vrp_series(btc, dv_btc), vrp_series(eth, dv_eth)
|
||
alld = sorted(set(vb) | set(ve))
|
||
vrp = np.array([np.nanmean([vb.get(d, np.nan), ve.get(d, np.nan)]) for d in alld])
|
||
alld = np.array(alld)
|
||
year = (1970 + alld / 365.25).astype(int)
|
||
sr = sharpe_t(torch.tensor(vrp, device=DEV, dtype=torch.float64))
|
||
print(f"\n===== CRYPTO VRP (short-vol BTC+ETH) =====")
|
||
print(f"days={len(vrp)} Sharpe {sr:+.2f} worst-day {np.nanmin(vrp)/np.nanstd(vrp):+.1f}σ skew {float(((vrp-np.nanmean(vrp))**3).mean()/np.nanstd(vrp)**3):+.2f}")
|
||
print("per-year: " + " ".join(f"{y}:{sharpe_t(torch.tensor(vrp[year==y],device=DEV,dtype=torch.float64)):+.2f}" for y in range(2021, 2027) if (year == y).sum() > 30))
|
||
|
||
# crypto momentum book
|
||
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)
|
||
mom_by_day = {int(cdays[1:][i]): mom[i] for i in range(len(mom))}
|
||
common = sorted(set(alld.tolist()) & set(mom_by_day))
|
||
va = np.array([vrp[np.where(alld == d)[0][0]] for d in common])
|
||
ma = np.array([mom_by_day[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)
|
||
svr = sharpe_t(torch.tensor(va[m], device=DEV, dtype=torch.float64))
|
||
smr = sharpe_t(torch.tensor(ma[m], device=DEV, dtype=torch.float64))
|
||
scr = sharpe_t(torch.tensor(comb, device=DEV, dtype=torch.float64))
|
||
print(f"\n===== DIVERSIFIER CHECK — VRP vs crypto-momentum (overlap {m.sum()} days) =====")
|
||
print(f"VRP Sharpe {svr:+.2f} momentum Sharpe {smr:+.2f} CORRELATION {corr:+.2f} combined {scr:+.2f}")
|
||
opt = math.sqrt(max(svr, 0)**2 + max(smr, 0)**2)
|
||
print(f"optimal-weight combined (uncorrelated approx) ~ {opt:.2f} (vs momentum alone {smr:+.2f})")
|
||
print("\nVERDICT: VRP Sharpe>=~0.4 + low |corr| + combined>momentum-alone = genuine diversifier (mind the short-vol tail).")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|