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
Metadata
Release files for volsurface 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| volsurface-0.2.0.tar.gz | 45.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| volsurface-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 79.4 kB
Release files / volsurface-0.2.0.tar.gz
| Download URL | volsurface-0.2.0.tar.gz |
|---|---|
| Size | 45.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4cf374597dd727b76ecf3737a047f396cdb4986b937a685ae0220eb542d199a9
|
|
BLAKE2b-256 checksum How to use checksums |
42ed46319d6fbab518b5f705054c3952b4dbcd781a0818b6f080609687dcce61
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|
Release files / volsurface-0.2.0-py3-none-any.whl
| Download URL | volsurface-0.2.0-py3-none-any.whl |
|---|---|
| Size | 34.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
018846dfc24b36ec76c5180e828f12dbe39232322f7cc16797e31e9ea47ae099
|
|
BLAKE2b-256 checksum How to use checksums |
8a8a155f19c21fd6a55d07da08cea9c7f16c658b2ac981af730b0f6d75453f85
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|