An open, interactive implied-volatility surface explorer for any US-listed options ticker. Pull a live option chain, fit an arbitrage-free SVI surface, and view it in 3D, by skew, and by term structure.
python src/vol_surface.py --ticker SPYI wanted to look at the implied-vol surface for tickers my investment club was kicking around, and the free tools are thin, paywalled, or wired to one ETF. The good surfaces sit behind an institutional terminal. So I built a small, runnable viewer that takes any ticker on Yahoo Finance. UNG was the first ticker I wired up, and it stays the default.
- Pulls the option chain for any ticker from Yahoo Finance (
yfinance), reads a CSV, or runs on built-in sample data offline. - Computes implied vol from the bid/ask mid by inverting Black-Scholes with a bisection solver, so the IV reflects an executable price instead of a stale last trade.
- Fits SVI per expiry under no-arbitrage constraints:
w(k) = a + b·[ρ(k−m) + √((k−m)² + σ²)], total variance against log-moneyness. - Renders three interactive Plotly views: the 3D surface, the per-expiry skew with the SVI fit over the market quotes, and the ATM term structure.
- Surfaces model-vs-market disagreement: contracts whose quoted IV sits far from the SVI fit (beyond the bid-ask width), so you can see where the market and the model part ways.
The point is the surface itself. The disagreement view is a secondary read you can ignore; it shows where quotes sit off the model, not a set of trades to put on.
SVI fits each smile with five interpretable parameters and, under the constraints above, stays free of static (butterfly) arbitrage. That turns a noisy set of quoted prices into a smooth surface you can read across strikes and expiries.
git clone https://github.com/Keyan-sm/open-vol-surface.git
cd open-vol-surface
pip install -r requirements.txtCommand line (live data, best during US market hours):
python src/vol_surface.py --ticker SPY # any options ticker: AAPL, USO, UNG, ...
python src/vol_surface.py --ticker AAPL --min-volume 10 --min-oi 50| Flag | Meaning | Default |
|---|---|---|
--ticker |
Ticker symbol | UNG |
--min-volume |
Minimum volume to include a quote | 0 |
--min-oi |
Minimum open interest | 0 |
--max-trade-age |
Max age (days) of a quote's last trade | 1 |
Charts (*.html) and CSVs land in output/, prefixed with the ticker.
Python API (offline, deterministic sample data):
import sys; sys.path.insert(0, "src")
from vol_surface import run_analysis
results = run_analysis(ticker="SPY", use_sample=True, output_dir="output")
print(results["metrics"]) # ATM IV, 25d skew, term slope, vol regimeWeb dashboard (explore any ticker in the browser):
python src/app.py # Flask API on localhost, then open web/index.htmlanalyze_trade.py is a worked example: it reads one contract off the surface and prints the
SVI model IV against the market, a small look at where the two disagree.
For --ticker SPY, the run writes to output/:
| File | View |
|---|---|
spy_vol_surface_3d.html |
3D volatility surface mesh + market quotes colored by liquidity |
spy_vol_skew.html |
IV smile per expiry: SVI fit, market points, >2σ outliers |
spy_vol_term_structure.html |
ATM IV term structure |
spy_mispricings_table.html |
Contracts that disagree with the model, ranked |
spy_mispricings.csv / _summary.csv |
The same data plus summary metrics |
open-vol-surface/
├── src/
│ ├── vol_surface.py # core: any-ticker data, Black-Scholes IV, SVI fit, viz
│ └── app.py # Flask API for the web dashboard
├── web/index.html # browser dashboard
├── analyze_trade.py # worked single-contract example (model vs market)
├── tests/test_svi_smoke.py # offline smoke test (sample data -> SVI fit is finite & arb-free)
├── output/ # generated charts (*.html) and CSVs
├── requirements.txt
├── LICENSE
└── README.md
A research and educational tool, not investment advice. The fit is only as good as the input quotes; illiquid strikes and wide wings distort the surface, and IV from thin or stale quotes reads as unreliable. Yahoo Finance data runs delayed or incomplete outside market hours.
MIT