Files
foxhunt/scripts/surfer/equity_vrp.py
jgrusewski d43fb5e61f feat(surfer): equity-index VRP frontier — real+affordable, measurement needs engineering
Bought XSP (mini-SPX) options ohlcv-1d 2013-2026 = $66.86 (hard-capped $70, get_cost-gated,
year-chunked vs 504, 2025 backfilled) + SPY $0.01. CORRECTION: index VRP is NOT $900 (that was
SPY root); clean instrument XSP $66.86/13y or SPX $143/13y. THREE measurement attempts all gave
spurious NEGATIVE VRP (RV 28-33% vs implied ~16%) = MEASUREMENT ERROR not finding (contradicts
decades of SPX VRP evidence). Bugs: noisy parity underlying; ~469/2800 days survive -> multi-day
gaps inflate RV; SPY!=XSP divergence corrupts ATM. Root: XSP EOD ohlcv too sparse to reconstruct
clean underlying+ATM-IV. Implied (16.7%) reads right. Frontier real+affordable+in-hand but proper
extraction is a real options-quant build (denser SPX +$76 or IV-surface w/ quotes), not a gate.
Did NOT record -2.7 as a result (artifact). Crypto momentum+VRP remains only deploy-grade edge.

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

116 lines
5.5 KiB
Python
Raw Permalink 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
"""Equity-index VRP on XSP (mini-SPX) options, 2013-2026 — the one real-prior frontier.
For each day: find the ~30d ATM straddle (strike where call~=put = the forward), back out
implied vol from the straddle price, compare to forward realized vol. VRP = IV - RV; a
short-vol seller harvests it. 13y spans 2018/2020/2022 tail events. Tests raw VRP Sharpe,
per-year (incl. crashes), tail/skew, AND the tail-managed version (don't sell when IV rising
— the gate that fixed crypto VRP). XSP = cash-settled European -> no early-exercise distortion.
"""
import glob
import math
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from signal_sweep import sharpe_t # noqa: E402
import torch # noqa: E402
DEV = "cuda" if torch.cuda.is_available() else "cpu"
DAY_NS = 86_400 * 10**9
def load_xsp():
import databento as db
rows = []
for p in sorted(glob.glob("data/surfer/xsp/*.dbn")):
try:
df = db.DBNStore.from_file(p).to_df().reset_index()
except Exception:
continue
if df.empty or "symbol" not in df.columns:
continue
df = df[df["close"] > 0]
rows.append(df[["ts_event", "symbol", "close"]])
import pandas as pd
d = pd.concat(rows, ignore_index=True)
s = d["symbol"].astype(str).str.replace(" ", "", regex=False) # "XSP240119C00450000"
d["right"] = s.str[-9]
d["strike"] = s.str[-8:].astype(float) / 1000.0
d["expiry"] = s.str[-15:-9] # YYMMDD
d["day"] = (d["ts_event"].astype("int64") // DAY_NS)
return d
def to_epoch_day(yymmdd):
import datetime
y = 2000 + int(yymmdd[:2]); mo = int(yymmdd[2:4]); da = int(yymmdd[4:6])
return (datetime.date(y, mo, da) - datetime.date(1970, 1, 1)).days
def main():
d = load_xsp()
print(f"loaded {len(d)} XSP option-days, {d['day'].nunique()} trading days")
d["exp_day"] = d["expiry"].map(to_epoch_day)
d["ttm"] = d["exp_day"] - d["day"]
d = d[(d["ttm"] >= 20) & (d["ttm"] <= 45)] # ~30d window
iv_by_day = {}
S_by_day = {}
for day, g in d.groupby("day"):
# pick the expiry closest to 30d
exp = g.iloc[(g["ttm"] - 30).abs().argsort()].iloc[0]["exp_day"]
ge = g[g["exp_day"] == exp]
calls = ge[ge["right"] == "C"].groupby("strike")["close"].mean() # dedup AM/PM series
puts = ge[ge["right"] == "P"].groupby("strike")["close"].mean()
common = calls.index.intersection(puts.index)
if len(common) < 3:
continue
diff = (calls[common] - puts[common]).abs()
katm = diff.idxmin() # ATM: where C~=P
S = float(katm + calls[katm] - puts[katm]) # parity forward
straddle = float(calls[katm] + puts[katm])
T = float(ge["ttm"].iloc[0]) / 365.0
if S <= 0 or T <= 0:
continue
iv = straddle / (0.8 * S * math.sqrt(T)) # ATM straddle -> implied vol
iv_by_day[int(day)] = iv; S_by_day[int(day)] = S
days = np.array(sorted(S_by_day))
S = np.array([S_by_day[x] for x in days]); IV = np.array([iv_by_day[x] for x in days])
r = np.zeros(len(days)); r[1:] = np.log(S[1:] / S[:-1])
# forward 21-day realized vol (annualized) — what the seller faces
H = 21
rv_fwd = np.full(len(days), np.nan)
for t in range(len(days) - H):
rv_fwd[t] = np.std(r[t + 1:t + 1 + H]) * math.sqrt(252)
vrp = IV - rv_fwd # premium (positive = seller wins)
# short-vol daily P&L proxy: collect implied variance, pay realized squared return
iv_lag = np.concatenate([[np.nan], IV[:-1]])
svol = (iv_lag ** 2) / 252.0 - r ** 2
year = np.array([1970 + x / 365.25 for x in days]).astype(int)
T_ = lambda x: torch.tensor(np.asarray(x)[np.isfinite(np.asarray(x))], device=DEV, dtype=torch.float64)
print(f"\n===== EQUITY-INDEX VRP (XSP, {days.min()}..{days.max()}, {len(days)} days) =====")
print(f"mean implied vol {np.nanmean(IV):.1%} mean fwd-realized {np.nanmean(rv_fwd):.1%} "
f"mean VRP {np.nanmean(vrp):.1%} (positive => premium exists)")
print(f"VRP frac>0 (IV>RV): {np.nanmean(vrp[np.isfinite(vrp)]>0):.2f}")
print(f"\nshort-vol daily P&L: Sharpe {sharpe_t(T_(svol)):+.2f} skew {float(((svol[np.isfinite(svol)]-np.nanmean(svol))**3).mean()/np.nanstd(svol)**3):+.2f} worst-day {np.nanmin(svol)/np.nanstd(svol):+.1f}σ")
print("per-year short-vol: " + " ".join(f"{y}:{sharpe_t(T_(svol[year==y])):+.1f}" for y in range(2013, 2027) if (year == y).sum() > 60))
# tail-managed: don't sell when implied vol rising over 5d (the crypto-VRP gate)
rising = np.zeros(len(days))
for t in range(6, len(days)):
rising[t] = 0.0 if IV[t - 1] > IV[t - 6] else 1.0
svol_tm = rising * svol
print(f"\ntail-managed (don't sell into rising IV): Sharpe {sharpe_t(T_(svol_tm)):+.2f} "
f"skew {float(((svol_tm[np.isfinite(svol_tm)]-np.nanmean(svol_tm))**3).mean()/np.nanstd(svol_tm)**3):+.2f} worst {np.nanmin(svol_tm)/np.nanstd(svol_tm):+.1f}σ")
print("per-year tail-managed: " + " ".join(f"{y}:{sharpe_t(T_(svol_tm[year==y])):+.1f}" for y in range(2013, 2027) if (year == y).sum() > 60))
print("\nVERDICT: VRP frac>0 high + short-vol Sharpe>0 + tail-managed improves skew/recent = real, harvestable equity VRP.")
if __name__ == "__main__":
main()