Calibrate stop-loss exit slippage per VIX regime from your own options trade logs — validated against your observed fills — with automatic daily CBOE VIX data refresh.
Built for options backtesters who keep finding that their backtest's stop-loss model and their broker's actual fills disagree. The novel part of this toolkit is the reconciliation methodology: it detects when two calibrations are measuring different things (e.g. a rule-driven reprice vs. realized market slippage) and bridges them via an empirical realization ratio k, producing per-VIX-tercile gap constants you can actually trust.
This happens constantly in options backtesting. You calibrate a stop-loss gap constant one way from your fill log and get, say, 1.10. You calibrate it another way against realized P/L and get 0.20. Both computations "work." Neither is obviously wrong. Which do you book?
In our own production calibration we hit exactly this:
| Calibration | Result | What it was actually measuring |
|---|---|---|
| Per-tercile LUT from the trade log's exit prices | 1.10 / 1.11 / 1.11 | The traders' deterministic reprice rule ("buy back at 2× credit"), not the market |
| Ground truth from observed fills | implied ≈ 0.20 | Realized loss as a fraction of credit |
The tell-tale symptom: all three terciles land within ~0.01 of each other. A genuine regime signal doesn't look like that — a constant restated three times does.
- Define one canonical quantity. Here: the gap fraction of credit at trigger, where modeled stopped loss =
credit × (1 + g). - Detect the artifact. If logged stop-exit multiples cluster tightly around one value (e.g. median 2.0× credit with tiny IQR), the log likely records a rule-driven reprice, not market realizations.
diagnose_reprice_artifact()flags this. - Bridge with k. On the window where ground-truth fills exist:
k = mean(m_true | observed stops) / mean(m_logged | same window) g_tercile = k × mean(m_logged | tercile) − 1 - Validate out-of-model. Simulate
credit × (1 + g_tercile)per stop and compare to actual P/L.
In our case study this took conflicting numbers (a legacy scalar 0.0469, a raw LUT of ~1.10–1.12) to reconciled tercile constants of 0.198 / 0.203 / 0.204, which validated to −0.69% error on stopped fills and −1.89% on the full book against ground truth. The legacy scalar measured slippage beyond an already-booked stop level (different baseline); the raw LUT restated the reprice rule. Only the k-bridged form matched what the engine actually books.
The synthetic example in examples/ reproduces this entire story end-to-end: logged exits cluster at 2.00× (the planted rule), truth realizes ~60% of that, and reconciliation recovers k ≈ 0.60 and g ≈ 0.20 per tercile.
pip install -r requirements.txt
# Run the whole pipeline on the committed SYNTHETIC sample data:
python -m vix_tercile_calibrator.cli calibrate \
--fills examples/fills.csv --vix examples/vix.csv
python -m vix_tercile_calibrator.cli reconcile \
--fills examples/fills.csv --truth examples/truth_stopped_fills.csv \
--vix examples/vix.csv --out reconciled.json
python -m vix_tercile_calibrator.cli costs examples/fills.csvOr as a library:
from vix_tercile_calibrator import (
load_vix, compute_gap_lut, reconcile_calibrations, tercile_bounds,
)
vix = load_vix("data/vix.csv")
lut = compute_gap_lut(stops_df, vix, stop_multiple=1.0)./scripts/fetch_vix_daily.sh data/vix.csv # full CBOE history merged in
./scripts/fetch_vix_daily.sh data/vix.csv --min-date 2025-01-01Idempotent (silent exit 0 when already current), so it is safe to run from cron daily.
| column | required | description |
|---|---|---|
date |
✓ | session date (YYYY-MM-DD or YYYYMMDD) |
ticker |
✓ | underlying symbol |
credit |
✓ | net credit received per contract (option price units) |
exit_reason |
✓ | e.g. stop, target, close, expiry |
live_exit_px |
✓ (for stops) | recorded exit price (price units) |
slippage |
optional | recorded entry slippage, price units |
pnl_model |
optional | model P/L in $ |
pnl_obs |
optional | observed P/L in $ |
Any broker export can be mapped to these columns; nothing else is read.
| column | description |
|---|---|
date |
session date |
ticker |
underlying symbol |
credit |
net credit received |
reason |
fill reason (stop marks stopped fills) |
pnl_obs |
actual P/L in $ |
loss_multiple |
actual stopped-loss multiple of credit (optional; derivable) |
Two columns: date,close. Refresh it from CBOE with scripts/fetch_vix_daily.sh.
Each fill gets the most recent VIX close on or before its date (prior session — no lookahead). Tercile bounds are the 33.3/66.7 percentiles of the closes covering your fills: low ≤ b1 < mid ≤ b2 < high. Rows predating the VIX series are dropped rather than forward-filled.
The committed examples/ files are generated by examples/generate_sample_data.py with a fixed seed — no real trades, accounts, or people. The generator plants:
- 260 VIX sessions with regime-switching dynamics (real terciles),
- 4,000 synthetic short-premium fills, 12% stopped,
- logged stop exits clustered at exactly 2.0× credit (the planted reprice rule),
- ground truth realizing k = 0.60 of the logged multiple with noise.
Running reconcile on it recovers k ≈ 0.60, reconciled g ≈ 0.202 in every tercile, and validation error near zero — while calibrate alone reports the misleading raw g ≈ 1.0 and flags the reprice artifact.
vix_tercile_calibrator/
vix.py CBOE download/normalize/merge, idempotent refresh
calibration.py tercile assignment (no lookahead), gap LUT computation
reconciliation.py artifact detection, realization ratio k, bridged LUT, validation
costs.py cost extraction from the generic fills schema
cli.py calibrate / reconcile / costs subcommands
scripts/fetch_vix_daily.sh daily cron-friendly VIX refresh
examples/ synthetic sample data + generator
tests/ pytest suite (all synthetic)
kis calibrated on whatever overlap window your ground truth covers; validation on that window is partially in-sample for the level. Tercile shape comes from the independent full set.- If your ground truth only spans one volatility regime (e.g. all fills in the high tercile), low/mid levels remain untested.
- The artifact detector is a heuristic; always eyeball your logged-multiple distribution.
- This toolkit calibrates models; it is not investment advice.
MIT — see LICENSE.