Typed, extensible toolkit for implied volatility surface calibration
Project description
volsurface
A typed, extensible Python toolkit for implied volatility surface calibration. Fits Raw SVI per expiry or SSVI across the full surface, checks for arbitrage, estimates forwards from put-call parity, and renders publication-quality plots — all with mypy --strict compliance throughout.
Features
- Raw SVI parameterisation — per-expiry L-BFGS-B calibration with data-scaled multi-start initial guesses
- SSVI surface model — globally consistent surface with shared (ρ, η, γ) parameters; free of calendar arbitrage by construction
- Put-call parity forward estimation — estimates the implied forward at each expiry from mid-prices, with robust median aggregation and sanity bounds
- Arbitrage checks — butterfly (convexity), calendar spread, and SVI parameter-level no-arbitrage conditions
- Fit diagnostics — per-slice and aggregate RMSE, max/mean absolute error, and side-by-side surface comparison tables
- Yahoo Finance integration — fetch, filter by liquidity, and clean live option chains in one call
- Surface interpolation — linear interpolation in total-variance space between fitted expiries
- Visualisation — 3D surface plots, heatmaps, and per-expiry smile overlays
- Fully typed —
mypy --strictcompliant withpy.typedmarker - Extensible — implement
VolModelto add any new parameterisation
Installation
pip install volsurface # core only
pip install volsurface[yahoo] # + Yahoo Finance data
pip install volsurface[plot] # + matplotlib plotting
pip install volsurface[all] # everything
Quick Start
Fetch, fit, query
from volsurface.market_data import fetch_chain
from volsurface.calibration import calibrate_surface
from volsurface.models import RawSVI
from volsurface.plotting import plot_surface
# Fetch SPY option chains and estimate forwards from put-call parity
slices = fetch_chain("SPY", min_volume=30, moneyness_range=(0.8, 1.2))
# Fit Raw SVI independently to each expiry
result = calibrate_surface(slices, RawSVI, ticker="SPY")
print(f"Fitted {result.surface.n_expiries} expiries")
print(f"Arbitrage clean: {result.arbitrage_report.is_clean}")
# Query any (strike, expiry) point — interpolates between fitted expiries
point = result.surface.iv(strike=570.0, expiry_years=0.5)
print(f"IV at K=570, T=0.5y: {point.iv:.4f}")
plot_surface(result.surface, kind="3d")
Fit SSVI and compare
from volsurface.models import SSVI
from volsurface.diagnostics import compare_surfaces
from volsurface.core import VolSurface
# Fit SSVI: global (rho, eta, gamma) calibrated across all expiries at once
ssvi = SSVI()
ssvi_slices, fit_result = ssvi.fit_surface(slices)
print(f"rho={ssvi.global_params.rho:.4f} eta={ssvi.global_params.eta:.4f}")
# Assemble an SSVI VolSurface for querying and plotting
ssvi_surface = VolSurface(ticker="SPY (SSVI)")
for ms in slices:
ssvi_surface.slices[ms.expiry_years] = ssvi_slices[ms.expiry_years]
ssvi_surface.market_data[ms.expiry_years] = ms
comparison = compare_surfaces(result.surface, ssvi_surface, "Raw SVI", "SSVI")
print(comparison.summary_table())
Working with Custom Data
import numpy as np
from volsurface.market_data import clean_chain
from volsurface.models import RawSVI
strikes = np.array([90, 95, 100, 105, 110], dtype=float)
ivs = np.array([0.25, 0.22, 0.20, 0.21, 0.24])
slice_ = clean_chain(strikes, ivs, expiry_years=0.25, forward=100.0, spot=100.0)
model = RawSVI()
result = model.fit(slice_)
print(f"RMSE: {result.rmse:.6f}")
print(f"Params: {result.params}")
Adding a New Model
Implement the VolModel abstract base class — three methods required:
from volsurface.models.base import VolModel
from volsurface.core import FitResult, MarketSlice
import numpy as np
class MyModel(VolModel):
@property
def n_params(self) -> int:
return 4
def fit(self, market_slice: MarketSlice) -> FitResult:
# calibrate to market_slice.log_moneyness, market_slice.total_variance
...
def total_variance(self, log_moneyness):
# return model total variance for given log-moneyness values
...
The fitted model plugs directly into calibrate_surface, plot_smile, and diagnose_surface.
Documentation
Full documentation including the Theory & Models reference: https://cjpvanderwouden.github.io/volsurface/
Development
git clone https://github.com/cjpvanderwouden/volsurface.git
cd volsurface
pip install -e ".[dev]"
pytest # tests + coverage
ruff check src/ tests/ # linting
mypy --strict src/ # type checking
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file volsurface-0.2.0.tar.gz.
File metadata
- Download URL: volsurface-0.2.0.tar.gz
- Upload date:
- Size: 45.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4cf374597dd727b76ecf3737a047f396cdb4986b937a685ae0220eb542d199a9
|
|
| MD5 |
07ad8b981356e7243d6b661eb631b14c
|
|
| BLAKE2b-256 |
42ed46319d6fbab518b5f705054c3952b4dbcd781a0818b6f080609687dcce61
|
File details
Details for the file volsurface-0.2.0-py3-none-any.whl.
File metadata
- Download URL: volsurface-0.2.0-py3-none-any.whl
- Upload date:
- Size: 34.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
018846dfc24b36ec76c5180e828f12dbe39232322f7cc16797e31e9ea47ae099
|
|
| MD5 |
c76d3fa606f7682f714805fbd4aaf7ab
|
|
| BLAKE2b-256 |
8a8a155f19c21fd6a55d07da08cea9c7f16c658b2ac981af730b0f6d75453f85
|