Config‑driven Python project that studies whether the shape and dynamics of the VIX futures curve can be used to time index put overlays more efficiently than a static premium budget.
The code builds VIX term‑structure features, discovers volatility states (K‑means baseline), and turns a SPY put hedge on/off based on state‑conditioned rules. It backtests cost, drawdown reduction, and Expected Shortfall (ES95/ES99), then reports hedge efficiency (loss reduced per unit premium).
- Synthetic or real data: runs immediately with synthetic series; drops in with your CSV histories.
- Config‑first: all parameters in
config/config.yaml(states, policy, costs, dates). - Unsupervised states: K‑means on term‑structure features; HMM stub ready for future work.
- Option pricing: simple Black–Scholes with IV proxied from VIX and tenor.
- Backtest engine: monthly rebalance cadence, premium ledger, simplified MTM.
- Reporting: CSV of timeseries and a NAV plot under
reports/.
This is a compact research scaffold meant for coursework / prototyping. For production, replace the option surface with broker or vendor IVs, add fills/slippage models, and harden walk‑forward state refits and roll logic.
vix_hedge_states/
config/
config.yaml # main experiment configuration
experiments/
exp_policy_sweep.yaml # example grid (not wired to CLI yet)
src/vix_hedge_states/
__init__.py
io.py # data load + synthetic generator
features.py # term-structure features, z-scores
states.py # KMeans state fit/predict + naming
pricer.py # Black–Scholes, IV proxy
hedge.py # rule-based on/off & sizing
backtest.py # engine + simple accounting
eval.py # ES/MaxDD/Vol metrics
plots.py # NAV plot
utils/
__init__.py
dates.py, logging.py, math.py
scripts/
run_backtest.py # main CLI entry point
data/ # drop your CSVs here (optional)
reports/ # outputs (figures, tables, timeseries)
tests/
test_smoke.py
README.md
requirements.txt
pyproject.toml
# (optional) in a fresh virtual environment
pip install -r requirements.txtPython ≥ 3.9 is recommended.
python -m scripts.run_backtest --config config/config.yamlWhat happens:
- If no CSVs under
data/, a synthetic SPY/VIX/VX1‑4/VVIX dataset is generated. - Features are engineered and standardized.
- K‑means discovers
kvolatility states; states are mapped to readable names. - A rule‑based hedge policy (e.g., hedge only in stress/backwardation states) trades monthly.
- Results: metrics printed to console, NAV plot saved to
reports/figures/nav.png, timeseries toreports/backtest_timeseries.csv.
Place CSVs into data/ with these minimal schemas (ISO dates: YYYY-MM-DD):
data/vix.csvdate,vix 2007-01-03,12.55 ... , ...
data/vvix.csv(optional)date,vvix 2012-01-03,90.1 ... , ...
data/vx.csv(annualized levels, percent points)date,vx1,vx2,vx3,vx4 2007-01-03,14.8,15.2,15.7,16.0 ... , ... , ... , ... , ...
data/spy_tr.csv(total return index, any base)date,spy_tr 2007-01-03,100.00 ... , ...
The engine normalizes
spy_trto 100 at backtest start; any base is fine.
Key blocks (abridged):
seed: 42
paths:
data_dir: data
reports_dir: reports
figures_dir: reports/figures
tables_dir: reports/tables
data:
vix_file: vix.csv
vvix_file: vvix.csv
vx_file: vx.csv
spy_tr_file: spy_tr.csv
features:
use_columns: [vix, vvix, vx1, vx2, vx3, basis1, slope12, slope13, curvature, dvix_5, dvix_20]
rolling_zscore_window: 1260
states:
method: kmeans # (hmm stubbed for later)
k: 4
labels: ["carry_contango", "flat", "stress_backwardation", "convex_stress"]
policy:
annual_budget: 0.02 # 2%/yr of NAV
hedge_states: ["stress_backwardation", "convex_stress"]
tenor_days: 126 # ~ 6M
moneyness_pct: -0.10 # 10% OTM put
trigger:
type: simple
rules:
require_negative_slope12: true
require_vix_z_gt: 0.5
require_vvix_z_gt: 0.0
costs:
option_bid_ask_bps: 0.0015
spy_commission_bps: 0.0001
backtest:
start: 2007-01-01
end: 2024-12-31
initial_nav: 100.0
rebalance_cadence_days: 21
evaluation:
es_alpha: 0.95
es_alpha_tail: 0.99Tweak these to run alternate strategies (e.g., tenor=63, moneyness=-0.05, budget=0.03).
- Features: basis (vx1−VIX), slopes (vx2−vx1, vx3−vx1), curvature, VIX momentum, RV proxy.
- States: K‑means clusters z‑scored features; clusters are mapped to intuitive regime names.
- Policy: hedge only in selected states and when simple gates pass (negative slope, elevated VIX/VVIX z).
- Pricing: Black–Scholes put with IV ≈
VIX * sqrt(30/tenor_days)(rough proxy). - Backtest: monthly portfolio actions, budget‑aware sizing, simplified MTM on expiry (illustrative).
Metrics: ES95/ES99, Max Drawdown, volatility, plus premium spend in the ledger.
- All experiments are driven by YAML config.
- A fixed
seedcontrols clustering reproducibility. - Outputs are written to
reports/and can be version‑controlled.
- HMM states: add
hmmlearnand implement sticky Gaussian HMM instates.py. - Option surface: plug vendor IV surfaces (moneyness×tenor skew) into
pricer.py. - Execution realism: daily re‑pricing, b/a slippage scaling in stress, delta limits.
- Policy grid: wire
experiments/exp_policy_sweep.yamlto a small grid‑runner to export heatmaps of{ES reduction vs premium}across tenor/moneyness/budget.
pytest -q
tests/test_smoke.py ensures imports succeed. Add your own tests under tests/.
- IV proxy from VIX is approximate; real surfaces recommended.
- Monthly rebalance & expiry MTM are simplified; see comments in
backtest.py. - State mapping is heuristic for readability; feel free to hard‑code label ordering.
Add your preferred license (MIT/Apache‑2.0).
Q: Can I run without VVIX or SKEW?
A: Yes. Missing fields are forward‑filled/bypassed; remove them from features.use_columns or let synthetic data supply them.
Q: Does the backtest include dynamic delta or rolling rules?
A: The scaffold re‑opens monthly; extending to delta/risk‑based triggers is encouraged.
Q: How do I compare against a static 2% policy?
A: Set policy.hedge_states to ["carry_contango","flat","stress_backwardation","convex_stress"]
to always‑on, or create a second config with annual_budget: 0.02 and no triggers.