Skip to content

Volatility surface

This guide covers the library's volatility-surface containers and the most common ways to build them.

The two central ideas are:

  • a smile at one expiry, represented in total variance
  • a surface across expiries, interpolated in total variance

For a continuous analytic parameter surface, the library also provides dedicated eSSVI objects. Those are covered in eSSVI.

Core objects

from option_pricing.vol import Smile, VolSurface

A Smile stores:

  • T: expiry in years
  • y = ln(K / F(T)): log-moneyness grid
  • w = T * iv^2: total variance on that grid

A VolSurface stores multiple smile slices plus a forward(T) callable.

VolSurface is the generic container for grid-based or per-expiry smile stacks. If you need analytic time derivatives such as w_T, use ESSVIImpliedSurface or ESSVISmoothedSurface instead of wrapping eSSVI data into VolSurface.

Build a simple grid-based surface from (T, K, iv) points

import numpy as np
from option_pricing.vol import VolSurface

S, r, q = 100.0, 0.02, 0.00

def forward(T: float) -> float:
    return float(S * np.exp((r - q) * float(T)))

rows = [
    (0.5, 90.0, 0.24),
    (0.5, 100.0, 0.20),
    (0.5, 110.0, 0.22),
    (1.0, 90.0, 0.25),
    (1.0, 100.0, 0.21),
    (1.0, 110.0, 0.23),
]

surface = VolSurface.from_grid(rows, forward=forward)

Query implied vol at any strike and expiry

iv_scalar = surface.iv(K=100.0, T=0.75)
iv_vec = surface.iv(K=np.array([90.0, 100.0, 110.0]), T=0.75)

Within a slice, the object interpolates in total variance. Across expiries, the default public query path uses no-arbitrage-aware interpolation at constant log-moneyness.

Work directly with slices

slice_noarb = surface.slice(0.75, method="no_arb")

If your endpoint slices are differentiable analytic smiles such as SVI, you can also request linear-in-total-variance interpolation:

slice_linear = surface.slice(0.75, method="linear_w")

That path is especially useful for local-vol construction because it preserves dw/dy and d2w/dy2 when the endpoint slices provide them.

Build an SVI-based surface directly

If your inputs are market quotes (T, K, iv), you can calibrate one SVI smile per expiry and wrap the result in a VolSurface in one step:

surface_svi = VolSurface.from_svi(
    rows,
    forward=forward,
    calibrate_kwargs={
        "repair_butterfly": True,
        "repair_method": "line_search",
        "butterfly_min_g_tol": None,
        "butterfly_min_g_tol_scale": 1.0,
    },
)

That gives you analytic per-expiry slices instead of grid-interpolated smiles.

Use eSSVI for a continuous analytic surface

VolSurface is not the only surface path in the library.

If you want a continuous parameter surface with explicit eSSVI constraints and analytic derivatives in both log-moneyness and time, use the dedicated eSSVI types:

  • ESSVIImpliedSurface for direct term-structure parameterizations
  • ESSVINodalSurface for exact calibrated-node interpolation
  • ESSVISmoothedSurface for Dupire-oriented smooth projection

Those workflows live in eSSVI because they are not just another VolSurface.from_* constructor; they expose their own calibration, projection, and validation helpers.

Surface no-arbitrage sanity checks

from option_pricing.vol import check_surface_noarb

def df(T: float) -> float:
    return float(np.exp(-r * float(T)))

rep = check_surface_noarb(surface, df=df)
print(rep.ok)
print(rep.message)

These checks are lightweight sanity checks for:

  • strike monotonicity / convexity proxies within each expiry
  • calendar monotonicity in total variance across expiries

Notes

  • VolSurface.from_grid(...) is lightweight and dependency-light.
  • VolSurface.from_svi(...) is the right starting point if you plan to build a Local volatility surface later.
  • For a time-differentiable implied surface that is better aligned with Dupire workflows, prefer the eSSVI path in eSSVI.
  • For full SVI calibration details, see SVI.