API reference

Models

Core parametrizations for stochastic volatility inspired IV surfaces. NumPy implementations with optional numba acceleration (see use_numba). Extensible via Parametrization ABC.

pysvi.models.numba_available()[source]

True if the optional numba dependency is installed.

Install it with pip install "svi-py[numba]".

Return type:

bool

pysvi.models.use_numba(enabled=True)[source]

Toggle the numba-accelerated kernels at runtime.

The numba backend is enabled automatically when numba is installed (disable at import time with the environment variable PYSVI_NUMBA=0). All kernels have a pure-NumPy twin, so toggling changes speed, not results — outputs agree to within floating-point rounding (jitted kernels use fastmath).

Parameters:

enabled (bool, default True) – True activates jitted kernels; False falls back to pure NumPy.

Raises:

ImportError – If enabling is requested but numba is not installed.

Return type:

None

Notes

This mutates a process-global flag, which is unsafe under concurrent mixed workloads; prefer backend() for per-context control.

pysvi.models.backend(name)[source]

Context manager pinning the kernel backend for the enclosed block.

with pysvi.backend("numpy"):
    surface = VolSurface.fit(df)   # pure-NumPy kernels here

Unlike use_numba(), the choice is context-local (contextvars): concurrent threads and async tasks each see their own backend, so a web service can serve numba and NumPy requests side by side without races. Contexts nest; the innermost wins.

Parameters:

name (str)

pysvi.models.warm_up()[source]

Compile every jitted kernel and warm the optimizer path; returns the elapsed seconds.

Kernels compile lazily on first use, which costs a few seconds per process per model (disk caching is deliberately off: numba’s cache keys on the importing module name, and the same source imported under two names poisons the cache). Call pysvi.warm_up() once at service start – before taking traffic – to move the entire cost to startup. Includes two micro-calibrations so scipy’s optimizer path is warm too; afterwards a first real calibration runs at steady-state latency. Returns 0.0 immediately when numba is not installed – there is nothing to compile, and the pure-NumPy path has no cold start worth paying for at boot.

Return type:

float

class pysvi.models.ArbitrageFreedom(*values)[source]

Bases: Flag

Configurable arbitrage-freeness constraints for IV parametrizations.

Combine flags with | to enforce multiple conditions simultaneously.

QUASI

Soft parameter-bound constraints only (b > 0, abs(rho) < 1, sigma > 0).

Type:

default

NO_BUTTERFLY

Enforce non-negative density g(k) >= 0 across strikes (no static arb).

Type:

flag

NO_CALENDAR

Enforce non-decreasing total variance in maturity (no calendar spread arb).

Type:

flag

pysvi.models.svi_total_variance(k, a, b, rho, m, sigma)[source]

Raw SVI total variance w(k).

Parameters:
  • k (ndarray)

  • a (float)

  • b (float)

  • rho (float)

  • m (float)

  • sigma (float)

Return type:

ndarray

pysvi.models.ssvi_total_variance(k, theta, rho, phi_theta)[source]

SSVI total variance w(k).

Parameters:
  • k (ndarray)

  • theta (float)

  • rho (float)

  • phi_theta (float)

Return type:

ndarray

pysvi.models.essvi_total_variance(k, theta, rho_theta, phi_theta)[source]

eSSVI total variance w(k).

Parameters:
  • k (ndarray)

  • theta (float)

  • rho_theta (float)

  • phi_theta (float)

Return type:

ndarray

pysvi.models.jw_total_variance(k, v_t, psi_t, p_t, c_t, v_tilde_t, T)[source]

Jump-wings SVI total variance w(k; T).

The jump-wings parametrization [Gatheral 2004] converts to raw SVI via:

b = (p_t + c_t) / 2
rho = 1 - p_t / b   (equivalently (c_t - p_t) / (c_t + p_t))
beta = rho - 2 * psi_t * sqrt(T) / b
alpha = sign(beta) * sqrt(1 / (beta^2) - 1)   when abs(beta) < 1
m = (v_t - v_tilde_t) * T / (b * (-rho + sign(alpha) * sqrt(1 + alpha^2) - alpha * sqrt(1 - rho^2)))
sigma = alpha * m
a = v_tilde_t * T - b * sigma * sqrt(1 - rho^2)
Parameters:
  • k (array) – Log-moneyness log(K/F).

  • v_t (float) – ATM variance (annualised), v_t = sigma_ATM^2.

  • psi_t (float) – ATM skew dw/dk|_{k=0} / (2 T).

  • p_t (float) – Left-wing slope (put wing), p_t >= 0.

  • c_t (float) – Right-wing slope (call wing), c_t >= 0.

  • v_tilde_t (float) – Minimum implied variance, v_tilde_t > 0.

  • T (float) – Time to expiry in years.

Returns:

Total variance w(k) = sigma^2(k) * T.

Return type:

array

pysvi.models.natural_total_variance(k, delta, mu, rho, omega, zeta)[source]

Natural SVI total variance w(k) [Gatheral & Jacquier 2014].

w(k) = Δ + ω/2 {1 + ζρ(k − μ) + sqrt[(ζ(k − μ) + ρ)² + (1 − ρ²)]}

Bijective with raw SVI (see natural_to_raw() / raw_to_natural()); the natural parameters map more directly to ATM level, skew, and curvature.

Parameters:
  • k (array) – Log-moneyness log(K/F).

  • delta (float) – Vertical variance shift (minimum-variance level).

  • mu (float) – Log-moneyness translation of the smile.

  • rho (float) – Skew (correlation), abs(rho) < 1.

  • omega (float) – Overall variance scale, omega >= 0.

  • zeta (float) – Curvature / smile-width scale, zeta > 0.

Returns:

Total variance w(k) = sigma^2(k) * T.

Return type:

array

pysvi.models.natural_to_raw(delta, mu, rho, omega, zeta)[source]

Natural SVI parameters -> raw SVI parameters.

a = Δ + ω(1 − ρ²)/2,   b = ωζ/2,   ρ = ρ,
m = μ − ρ/ζ,           σ = sqrt(1 − ρ²)/ζ

Requires zeta > 0 and abs(rho) < 1.

Returns:

{‘a’, ‘b’, ‘rho’, ‘m’, ‘sigma’}.

Return type:

dict

Parameters:
  • delta (float)

  • mu (float)

  • rho (float)

  • omega (float)

  • zeta (float)

pysvi.models.raw_to_natural(a, b, rho, m, sigma)[source]

Raw SVI parameters -> natural SVI parameters (inverse bijection).

ζ = sqrt(1 − ρ²)/σ,   ω = 2bσ/sqrt(1 − ρ²),   ρ = ρ,
μ = m + ρσ/sqrt(1 − ρ²),   Δ = a − bσ sqrt(1 − ρ²)

Requires sigma > 0 and abs(rho) < 1.

Returns:

{‘delta’, ‘mu’, ‘rho’, ‘omega’, ‘zeta’}.

Return type:

dict

Parameters:
  • a (float)

  • b (float)

  • rho (float)

  • m (float)

  • sigma (float)

pysvi.models.sabr_implied_vol(k, alpha, beta, rho, nu, F, T)[source]

SABR lognormal (Black) implied volatility via Hagan et al. (2002).

The SABR model [Hagan, Kumar, Lesniewski, Woodward 2002] assumes forward dynamics:

dF = alpha * F^beta dW1,   d(alpha) = nu * alpha dW2,
d<W1, W2> = rho dt

and admits the asymptotic implied-vol expansion (HKLW formula):

sigma_B(K, F) = alpha / [(FK)^((1-beta)/2) * D(L)] * (z / x(z))
                * {1 + [ (1-beta)^2 alpha^2 / (24 (FK)^(1-beta))
                       + rho beta nu alpha / (4 (FK)^((1-beta)/2))
                       + (2 - 3 rho^2) nu^2 / 24 ] T}

where:

L    = log(F/K)
D(L) = 1 + (1-beta)^2/24 L^2 + (1-beta)^4/1920 L^4
z    = (nu/alpha) (FK)^((1-beta)/2) L
x(z) = log[(sqrt(1 - 2 rho z + z^2) + z - rho) / (1 - rho)]

At the money z -> 0 and z/x(z) -> 1; the singularity is handled via the Taylor expansion z/x(z) = 1 - rho z / 2 + O(z^2).

Rendered formulas: https://pysvi.readthedocs.io/en/latest/models/sabr.html

Parameters:
  • k (array) – Log-moneyness log(K/F).

  • alpha (float) – Initial (ATM-like) volatility level, alpha > 0.

  • beta (float) – CEV exponent in [0, 1]. Fixed by market convention, not fitted: beta = 1 (lognormal) for FX/equity, beta ~ 0.5 for interest rates, beta = 0 (normal) for spread-like underlyings.

  • rho (float) – Spot/vol correlation, abs(rho) < 1.

  • nu (float) – Vol-of-vol, nu >= 0.

  • F (float) – Forward price of the underlying, F > 0.

  • T (float) – Time to expiry in years, T > 0.

Returns:

Lognormal implied volatilities sigma_B(k).

Return type:

array

pysvi.models.sabr_total_variance(k, alpha, beta, rho, nu, F, T)[source]

SABR total variance w(k) = sigma_B(k)^2 * T via the Hagan expansion.

See sabr_implied_vol() for the model and parameter definitions.

Parameters:
  • k (ndarray)

  • alpha (float)

  • beta (float)

  • rho (float)

  • nu (float)

  • F (float)

  • T (float)

Return type:

ndarray

class pysvi.models.Parametrization(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]

Bases: ABC

Base class for IV surface parametrizations.

Parameters:

arbitrage_condition (ArbitrageFreedom)

abstractmethod calibrate(k, w_target, **kwargs)[source]

Calibrate parameters from log-moneyness k and total variance w_target.

Parameters:
  • k (np.ndarray) – Log-moneyness values log(K/F).

  • w_target (np.ndarray) – Observed total variance values sigma_mkt^2 * T.

  • **kwargs –

    Extra model-specific arguments (e.g. theta for SSVI/eSSVI), plus the common calibration controls accepted by every iterative model:

    • objective : str, default ‘total_variance’ — residual space: ‘total_variance’, ‘implied_vol’, ‘price’ (Black call), ‘vega_weighted’, or ‘bid_ask’ (requires ‘w_bid’/’w_ask’ arrays, the total variance of the bid and ask quotes).

    • loss : str, default ‘l2’ — ‘l2’, ‘huber’, ‘soft_l1’, or ‘cauchy’ (scipy.least_squares convention).

    • f_scale : float, optional — robust-loss scale; defaults to 1.4826 * MAD of the residuals at a pilot l2 fit.

    • initialization : str, default ‘default’ — ‘default’, ‘jump_wings’ (SVI/NaturalSVI only: data-driven wing readoff), or ‘multi_start’ (deterministic start grid; the lowest-objective result wins, and the default start also runs under the default path’s settings, so multi_start is never worse than default).

Returns:

Mapping of parameter names to floats, or None on failure.

Return type:

dict or None

abstractmethod total_variance(k, params)[source]

Compute model total variance w(k) given parameters.

Parameters:
  • k (np.ndarray) – Log-moneyness values log(K/F).

  • params (dict) – Calibrated parameter dictionary for this parametrization.

Returns:

Total variance values w(k).

Return type:

np.ndarray

fd_step: float = 1e-05

Step for the default finite-difference derivatives (central, second-order: truncation O(h^2), roundoff on w’’ ~ eps/h^2). Set it on an instance to trade truncation against roundoff; note the density noise this induces for finite-difference models (SABR, DirectSVI) can reach ~1e-2, far above the default diagnostics tolerance.

diagnostics_tol: float = 1e-08

Default tolerance for the arbitrage diagnostics on this model. Models whose derivatives are finite-differenced (SABR, DirectSVI) carry density noise far above the analytic models’ 1e-8; their override keeps diagnose() from reporting false violations on clean fits.

free_params: tuple = ()

Names of the parameters the optimizer actually fits, in order. Per-slice givens (theta, T, F, a fixed beta) and the stored ‘forward’ are not free and carry no uncertainty of their own.

param_jacobian(k, params)[source]

Sensitivity of total variance to the free parameters.

Returns the n x p matrix J with J[i, j] = dw(k_i)/dtheta_j for theta_j in free_params – the ingredient of every identifiability and uncertainty statement (see pysvi.identifiability). The base implementation uses central finite differences with a relative step; models with tractable derivatives override it analytically (SVI, NaturalSVI, SSVI).

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

NDArray[float64]

derivatives(k, params)[source]

Total variance and its first two strike derivatives, (w, w’, w’’).

The base implementation uses central finite differences with step fd_step on total_variance(); parametrizations with tractable analytic derivatives override it (SVI, SSVI, eSSVI, jump-wings). SABR and DirectSVI use this finite-difference default.

Parameters:
  • k (np.ndarray) – Log-moneyness values log(K/F).

  • params (dict) – Calibrated parameter dictionary for this parametrization.

Returns:

(w(k), w’(k), w’’(k)).

Return type:

tuple of np.ndarray

dw_dk(k, params)[source]

First derivative of total variance, w’(k). See derivatives().

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

NDArray[float64]

d2w_dk2(k, params)[source]

Second derivative of total variance, w’’(k). See derivatives().

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

NDArray[float64]

wing_slopes(params)[source]

Asymptotic total-variance wing slopes (left, right).

Returns lim dw/d(abs(k)) on each wing where a closed form exists (the SVI family), else None — callers should fall back to measuring dw/dk at finite k, which underestimates the asymptote for convex w. Used by the Lee-bound check in the diagnostics.

Parameters:

params (Dict[str, float])

Return type:

tuple[float, float] | None

density(k, params)[source]

Risk-neutral density factor g(k).

g(k) = (1 - k w'/(2w))^2 - (w')^2/4 (1/w + 1/4) + w''/2

The slice is free of butterfly arbitrage iff g(k) >= 0 for all k [Gatheral & Jacquier 2014]. Uses derivatives(), so models with analytic derivatives get an analytic density. Only valid where w(k) > 0; non-positive total variance produces NaN/inf or meaningless values rather than raising — validate w separately (the diagnostics module does).

Parameters:
  • k (np.ndarray) – Log-moneyness values log(K/F).

  • params (dict) – Calibrated parameter dictionary for this parametrization.

Returns:

g(k) at each input point.

Return type:

np.ndarray

class pysvi.models.SVI(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]

Bases: Parametrization

Raw SVI total variance parametrization [Gatheral 2004].

w(k) = a + b {ρ(k-m) + sqrt[(k-m)² + σ²]}

No-arbitrage constraints softly enforced via bounds/penalties:

  • b > 0 (positive slope)

  • abs(ρ) < 1 (correlation)

  • σ > 0 (vol of vol)

Calibrates via L-BFGS-B (bounded) → Nelder-Mead fallback. Initial guess: ATM a, median m, wing-informed b/σ.

Rendered formulas: https://pysvi.readthedocs.io/en/latest/models/svi.html

Parameters:

arbitrage_condition (ArbitrageFreedom)

free_params: tuple = ('a', 'b', 'rho', 'm', 'sigma')

Names of the parameters the optimizer actually fits, in order. Per-slice givens (theta, T, F, a fixed beta) and the stored ‘forward’ are not free and carry no uncertainty of their own.

calibrate(k, w_target, **kwargs)[source]

Minimize MSE(w_model(k), w_target) subject to constraints.

Parameters:
  • k (NDArray[np.float64]) – Log-moneyness array.

  • w_target (NDArray[np.float64]) – Market total variances σ_mkt²T.

Returns:

{‘a’, ‘b’, ‘rho’, ‘m’, ‘sigma’} or None (opt failed).

Return type:

Dict[str, float] or None

total_variance(k, params)[source]

Evaluate w(k) = a + b{ρ(k-m) + sqrt[(k-m)² + σ²]}.

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

NDArray[float64]

derivatives(k, params)[source]

Analytic (w, w’, w’’) for raw SVI.

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

tuple[NDArray[float64], NDArray[float64], NDArray[float64]]

wing_slopes(params)[source]

Asymptotic wing slopes: left b(1 - ρ), right b(1 + ρ).

Parameters:

params (Dict[str, float])

Return type:

tuple[float, float] | None

param_jacobian(k, params)[source]

Analytic dw/d(a, b, rho, m, sigma).

With z = k - m and R = sqrt(z^2 + sigma^2):

dw/da = 1            dw/db     = rho z + R
dw/drho = b z        dw/dm     = -b (rho + z / R)
dw/dsigma = b sigma / R
Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

NDArray[float64]

class pysvi.models.NaturalSVI(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]

Bases: Parametrization

Natural SVI parametrization [Gatheral & Jacquier 2014].

w(k) = Δ + ω/2 {1 + ζρ(k − μ) + sqrt[(ζ(k − μ) + ρ)² + (1 − ρ²)]}

Δ : vertical variance shift        (unconstrained)
μ : log-moneyness translation      (unconstrained)
ρ : skew (correlation)             abs(ρ) < 1
ω : overall variance scale         ω > 0
ζ : curvature / smile-width scale  ζ > 0

Same 5 degrees of freedom as raw SVI, connected by an explicit bijection (natural_to_raw() / raw_to_natural()), but the parameters map more directly to ATM level, skew, and curvature — often better behaved in calibration and useful as an initialisation coordinate system for raw SVI.

Calibrates via L-BFGS-B (bounded) → Nelder-Mead fallback; evaluation and derivatives go through the raw-SVI equivalents.

Rendered formulas: https://pysvi.readthedocs.io/en/latest/models/natural.html

Parameters:

arbitrage_condition (ArbitrageFreedom)

free_params: tuple = ('delta', 'mu', 'rho', 'omega', 'zeta')

Names of the parameters the optimizer actually fits, in order. Per-slice givens (theta, T, F, a fixed beta) and the stored ‘forward’ are not free and carry no uncertainty of their own.

calibrate(k, w_target, **kwargs)[source]

Minimize MSE(w_model(k), w_target) subject to constraints.

Parameters:
  • k (NDArray[np.float64]) – Log-moneyness array.

  • w_target (NDArray[np.float64]) – Market total variances σ_mkt²T.

  • **kwargs – Optional ‘w_prev’: prior slice total variance for calendar arb.

Returns:

{‘delta’, ‘mu’, ‘rho’, ‘omega’, ‘zeta’} or None (opt failed).

Return type:

Dict[str, float] or None

total_variance(k, params)[source]

Evaluate natural SVI w(k) via the raw-SVI equivalents.

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

NDArray[float64]

derivatives(k, params)[source]

Analytic (w, w’, w’’) via the raw-SVI equivalent parameters.

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

tuple[NDArray[float64], NDArray[float64], NDArray[float64]]

wing_slopes(params)[source]

Asymptotic wing slopes via the raw-SVI equivalents: b(1 ∓ ρ).

Parameters:

params (Dict[str, float])

Return type:

tuple[float, float] | None

param_jacobian(k, params)[source]

Analytic dw/d(delta, mu, rho, omega, zeta).

With z = k - mu, u = zeta z + rho and S = sqrt(u^2 + 1 - rho^2):

dw/ddelta = 1
dw/dmu    = -(omega zeta / 2) (rho + u / S)
dw/drho   = (omega zeta z / 2) (1 + 1 / S)
dw/domega = (w - delta) / omega
dw/dzeta  = (omega z / 2) (rho + u / S)
Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

NDArray[float64]

class pysvi.models.SSVI(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]

Bases: Parametrization

Surface-consistent SSVI [Gatheral & Jacquier 2014].

w(k;θ) = θ/2 [1 + ρ φ(θ) k + sqrt{(φ(θ) k + ρ)² + (1-ρ²)}]

θ    = ATM total variance (fixed per slice, typically σ_ATM² T)
φ(θ) = η / sqrt(θ) - curvature scale independent of ATM level

Guarantees no butterfly arbitrage across strikes for fixed θ. Calibrates only ρ, η (2 params) given θ.

Rendered formulas: https://pysvi.readthedocs.io/en/latest/models/ssvi.html

Parameters:

arbitrage_condition (ArbitrageFreedom)

free_params: tuple = ('rho', 'eta')

Names of the parameters the optimizer actually fits, in order. Per-slice givens (theta, T, F, a fixed beta) and the stored ‘forward’ are not free and carry no uncertainty of their own.

calibrate(k, w_target, **kwargs)[source]

Fit ρ, η minimizing MSE(w_model, w_target) for fixed θ.

Parameters:
  • k (NDArray[np.float64]) – Log-moneyness.

  • w_target (NDArray[np.float64]) – Market total variances.

  • **kwargs – Must contain ‘theta’: ATM w_ATM.

Returns:

{‘rho’, ‘eta’, ‘theta’} or None.

Return type:

Dict[str, float] or None

total_variance(k, params)[source]

Compute model total variance w(k) given parameters.

Parameters:
  • k (np.ndarray) – Log-moneyness values log(K/F).

  • params (dict) – Calibrated parameter dictionary for this parametrization.

Returns:

Total variance values w(k).

Return type:

np.ndarray

derivatives(k, params)[source]

Analytic (w, w’, w’’) for SSVI.

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

tuple[NDArray[float64], NDArray[float64], NDArray[float64]]

wing_slopes(params)[source]

Asymptotic wing slopes: θφ(1 ∓ ρ)/2.

Parameters:

params (Dict[str, float])

Return type:

tuple[float, float] | None

param_jacobian(k, params)[source]

Analytic dw/d(rho, eta), with theta a per-slice given.

With phi = eta / sqrt(theta), u = phi k + rho and S = sqrt(u^2 + 1 - rho^2):

dw/drho = (theta phi k / 2) (1 + 1 / S)
dw/deta = dw/dphi / sqrt(theta),
dw/dphi = (theta k / 2) (rho + u / S)
Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

NDArray[float64]

class pysvi.models.ESSVI(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]

Bases: Parametrization

Extended SSVI with ρ(θ) parametrization.

w(k;θ) = θ/2 [1 + ρ(θ) φ(θ) k + sqrt{(φ(θ) k + ρ(θ))² + (1-ρ(θ)²)}]

ρ(θ) = clip(ρ₀ + ρ₁ (θ/θ_ref)^α, -0.999, 0.999)  ← term structure skew
φ(θ) = η / sqrt(θ)                                ← curvature

θ_ref smooths ρ across maturities (often median ATM θ). 4 params total. Enables realistic calendar skew evolution.

Rendered formulas: https://pysvi.readthedocs.io/en/latest/models/essvi.html

Parameters:

arbitrage_condition (ArbitrageFreedom)

free_params: tuple = ('rho0', 'rho1', 'alpha', 'eta')

Names of the parameters the optimizer actually fits, in order. Per-slice givens (theta, T, F, a fixed beta) and the stored ‘forward’ are not free and carry no uncertainty of their own.

calibrate(k, w_target, **kwargs)[source]

Fit ρ₀, ρ₁, α, η given θ, θ_ref via penalized MSE.

Heavy penalty on η≤0, mild on abs(ρ(θ))>0.95 for stability.

Parameters:
  • k (NDArray[np.float64]) – Log-moneyness.

  • w_target (NDArray[np.float64]) – Total variances.

  • **kwargs – ‘theta’: slice ATM w ‘theta_ref’: reference θ (defaults to theta)

Returns:

All params + computed ‘rho_theta’.

Return type:

Dict[str, float] or None

total_variance(k, params)[source]

Compute model total variance w(k) given parameters.

Parameters:
  • k (np.ndarray) – Log-moneyness values log(K/F).

  • params (dict) – Calibrated parameter dictionary for this parametrization.

Returns:

Total variance values w(k).

Return type:

np.ndarray

derivatives(k, params)[source]

Analytic (w, w’, w’’) for eSSVI (SSVI form with ρ(θ)).

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

tuple[NDArray[float64], NDArray[float64], NDArray[float64]]

wing_slopes(params)[source]

Asymptotic wing slopes: θφ(1 ∓ ρ(θ))/2.

Parameters:

params (Dict[str, float])

Return type:

tuple[float, float] | None

class pysvi.models.JumpWings(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]

Bases: Parametrization

SVI jump-wings parametrization [Gatheral 2004].

Parameters are (v_t, psi_t, p_t, c_t, v_tilde_t) per slice at maturity T:

v_t       : ATM variance sigma_ATM^2
psi_t     : ATM skew (dw/dk at k=0) / (2T)
p_t       : left (put) wing slope,  p_t >= 0
c_t       : right (call) wing slope, c_t >= 0
v_tilde_t : minimum implied variance, v_tilde_t > 0

Internally converts to raw SVI (a, b, rho, m, sigma) for evaluation. Calibrates 5 jump-wings params via L-BFGS-B with Nelder-Mead fallback.

Rendered formulas: https://pysvi.readthedocs.io/en/latest/models/jumpwings.html

Parameters:

arbitrage_condition (ArbitrageFreedom)

free_params: tuple = ('v_t', 'psi_t', 'p_t', 'c_t', 'v_tilde_t')

Names of the parameters the optimizer actually fits, in order. Per-slice givens (theta, T, F, a fixed beta) and the stored ‘forward’ are not free and carry no uncertainty of their own.

calibrate(k, w_target, **kwargs)[source]

Fit jump-wings params minimizing MSE(w_model, w_target).

Parameters:
  • k (NDArray[np.float64]) – Log-moneyness array.

  • w_target (NDArray[np.float64]) – Market total variances.

  • **kwargs – Must contain ‘T’: time to expiry in years. Optional ‘w_prev’: prior slice total variance for calendar arb.

Returns:

{‘v_t’, ‘psi_t’, ‘p_t’, ‘c_t’, ‘v_tilde_t’, ‘T’} or None.

Return type:

Dict[str, float] or None

total_variance(k, params)[source]

Evaluate w(k) via jump-wings → raw SVI conversion.

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

NDArray[float64]

derivatives(k, params)[source]

Analytic (w, w’, w’’) via the raw-SVI equivalent parameters.

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

tuple[NDArray[float64], NDArray[float64], NDArray[float64]]

wing_slopes(params)[source]

Asymptotic wing slopes via the raw-SVI equivalents: b(1 ∓ ρ).

Parameters:

params (Dict[str, float])

Return type:

tuple[float, float] | None

pysvi.models.directsvi_fit(k, w)[source]

Direct algebraic SVI fit via conic section eigenvalue problem.

Linearises the SVI equation as a hyperbola in (x, y) = (k, w) space:

z₀x² + z₁y² + z₂xy + z₃x + z₄y + z₅ = 0

and solves for the 6 conic coefficients via a constrained eigenvalue problem (hyperbola constraint: z₂² − 4z₀z₁ > 0).

Parameters:
  • k (array) – Log-moneyness values.

  • w (array) – Total variance values (market observed).

Returns:

Conic coefficients [z0, z1, z2, z3, z4, z5] normalised so z1 = 1.

Return type:

ndarray, shape (6,)

References

Schadner, W. “Direct Fit for SVI Implied Volatilities”, Journal of Derivatives (forthcoming). Implementation based on wol-fi/directSVI.

pysvi.models.directsvi_total_variance(k, z0, z1, z2, z3, z4, z5)[source]

Evaluate the DirectSVI conic for given log-moneyness.

Solves the conic z₀x² + z₁y² + z₂xy + z₃x + z₄y + z₅ = 0 for y via the quadratic formula:

y = (−(z₂x + z₄) + √((z₂x + z₄)² − 4z₁(z₀x² + z₃x + z₅))) / (2z₁)

Selects the positive root (total variance must be non-negative).

Parameters:
  • k (array) – Log-moneyness.

  • z0 (float) – Conic coefficients.

  • z1 (float) – Conic coefficients.

  • z2 (float) – Conic coefficients.

  • z3 (float) – Conic coefficients.

  • z4 (float) – Conic coefficients.

  • z5 (float) – Conic coefficients.

Returns:

Total variance w(k).

Return type:

array

class pysvi.models.DirectSVI(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]

Bases: Parametrization

Direct algebraic SVI fit via conic section [Schadner].

Linearises the SVI total variance curve as a hyperbola:

z₀k² + z₁w² + z₂kw + z₃k + z₄w + z₅ = 0

and solves a constrained eigenvalue problem for the 6 conic coefficients — no iterative optimisation needed.

The direct fit is fast and robust but does not support penalty-based arbitrage enforcement (NO_BUTTERFLY / NO_CALENDAR). Only ArbitrageFreedom.QUASI is meaningful.

Rendered formulas: https://pysvi.readthedocs.io/en/latest/models/directsvi.html

Parameters:

arbitrage_condition (ArbitrageFreedom)

diagnostics_tol: float = 0.0001

Default tolerance for the arbitrage diagnostics on this model. Models whose derivatives are finite-differenced (SABR, DirectSVI) carry density noise far above the analytic models’ 1e-8; their override keeps diagnose() from reporting false violations on clean fits.

free_params: tuple = ('z0', 'z2', 'z3', 'z4', 'z5')

Names of the parameters the optimizer actually fits, in order. Per-slice givens (theta, T, F, a fixed beta) and the stored ‘forward’ are not free and carry no uncertainty of their own.

calibrate(k, w_target, **kwargs)[source]

Fit conic coefficients via direct eigenvalue solve.

Parameters:
  • k (array) – Log-moneyness.

  • w_target (array) – Market total variances.

Returns:

{‘z0’, ‘z1’, ‘z2’, ‘z3’, ‘z4’, ‘z5’}.

Return type:

dict or None

total_variance(k, params)[source]

Evaluate w(k) from conic coefficients.

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

NDArray[float64]

class pysvi.models.SABR(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]

Bases: Parametrization

SABR stochastic volatility model [Hagan et al. 2002].

dF = α F^β dW₁,  dα = ν α dW₂,  d<W₁,W₂> = ρ dt

Implied vols come from the Hagan (HKLW) lognormal asymptotic expansion; total variance is w(k) = σ_B(k)² T. The market standard for interest-rate (swaption/cap) and FX volatility smiles.

β is fixed by market convention (not fitted): 1 for FX/equity, ~0.5 for rates, 0 for normal dynamics. Calibrates (α, ρ, ν) given β, F, T via L-BFGS-B with Nelder-Mead fallback.

Butterfly-density derivatives are evaluated by finite differences (the Hagan expansion has no tractable closed-form w’’, and the expansion itself can violate no-arbitrage for extreme strikes or long maturities — NO_BUTTERFLY is a numerical check, not a structural guarantee).

Rendered formulas: https://pysvi.readthedocs.io/en/latest/models/sabr.html

Parameters:

arbitrage_condition (ArbitrageFreedom)

diagnostics_tol: float = 0.0001

Default tolerance for the arbitrage diagnostics on this model. Models whose derivatives are finite-differenced (SABR, DirectSVI) carry density noise far above the analytic models’ 1e-8; their override keeps diagnose() from reporting false violations on clean fits.

free_params: tuple = ('alpha', 'rho', 'nu')

Names of the parameters the optimizer actually fits, in order. Per-slice givens (theta, T, F, a fixed beta) and the stored ‘forward’ are not free and carry no uncertainty of their own.

calibrate(k, w_target, **kwargs)[source]

Fit (α, ρ, ν) minimizing MSE(w_model, w_target) for fixed β.

Parameters:
  • k (NDArray[np.float64]) – Log-moneyness log(K/F).

  • w_target (NDArray[np.float64]) – Market total variances σ_mkt² T.

  • **kwargs – Must contain ‘T’ (years) and ‘F’ (forward price). Optional ‘beta’: CEV exponent in [0, 1], default 0.5. Optional ‘w_prev’: prior slice total variance for calendar arb.

Returns:

{‘alpha’, ‘beta’, ‘rho’, ‘nu’, ‘F’, ‘T’} or None.

Return type:

Dict[str, float] or None

total_variance(k, params)[source]

Evaluate w(k) = σ_B(k)² T via the Hagan expansion.

Parameters:
  • k (NDArray[float64])

  • params (Dict[str, float])

Return type:

NDArray[float64]

Calibration

High-level calibration pipeline for IV surfaces from option panels. Supports SVI, SSVI, eSSVI via models.Parametrization classes.

pysvi.calibration.parse_ticker_info(path)[source]

Parse SPY option ticker information from OCC-format filename.

Recognizes patterns like ‘SPY250119C00450000’ where: - SPY = underlying ticker - 250119 = YYMMDD expiry (e.g., Jan 19, 2025) - C/P = call/put flag - 00450000 = strike × 1000 (e.g., 4500 → $450 strike)

Returns None for non-matching filenames. Designed for SPY options but pattern works for any OCC-format equity option files.

Examples

>>> parse_ticker_info(Path("SPY250119C00450000.csv"))
{'expiry': '250119', 'expiry_dt': Timestamp('2025-01-19'),
 'cp': 'C', 'strike': 450.0}
Parameters:

path (Path)

pysvi.calibration.compute_ivs_vectorized(prices, spots, strikes, ttes, r, q=0.0, flags=None)[source]

Compute Black-Scholes-Merton implied vols from option prices.

Parameters:
  • prices (NDArray[np.float64]) – Option mid-prices (or bid/ask).

  • spots (NDArray[np.float64]) – Underlying spot prices (same length as prices).

  • strikes (NDArray[np.float64]) – Strike prices.

  • ttes (NDArray[np.float64]) – Time-to-expiry in years.

  • r (float) – Risk-free rate (continuous).

  • q (float, default 0.0) – Dividend yield (continuous).

  • flags (array of str, optional) – ‘c’/’C’ for calls, ‘p’/’P’ for puts.

Returns:

Implied vols (NaN for failures).

Return type:

NDArray[np.float64]

Notes

Uses py_vollib BSM solver.

pysvi.calibration.calculate_implied_forward(spot, tte, r, strike, call_mid, put_mid)[source]

Compute forward price F from put-call parity on same strike slice.

PCP: C - P = e^(-rT) * (F - K)
→    F = K + e^(rT) * (C - P)
Parameters:
  • spot (pd.Series) – Underlying spot price time series.

  • tte (pd.Series) – Time-to-expiry (years) for this expiry.

  • r (float, curve, model, or callable) – Continuously compounded risk-free rate in any form _rate_at accepts: flat float, interest_rate_models.DiscountCurve, an interest-rate model, or a callable T -> r(T).

  • strike (pd.Series) – Fixed strike (same value across series).

  • call_mid (pd.Series) – Call option mid-prices (same strike/expiry).

  • put_mid (pd.Series) – Put option mid-prices (matching call).

Returns:

Implied forwards (NaN where data invalid/missing).

Return type:

pd.Series

Notes

Assumes same index/length across series. Handles NaNs gracefully.

Examples

>>> import pandas as pd
>>> spot = pd.Series([100.0, 101.0])
>>> call_mid = pd.Series([3.2, 3.5])
>>> put_mid = pd.Series([2.8, 3.0])
>>> strike = pd.Series([100.0, 100.0])
>>> tte = pd.Series([0.25, 0.25])
>>> calculate_implied_forward(spot, tte, 0.05, strike, call_mid, put_mid)
0    100.50
1    101.26
dtype: float64
pysvi.calibration.choose_leg(strike, forward, call_mid, put_mid)[source]

Select most liquid OTM option leg for robust IV surface construction.

Intuition: OTM options have higher liquidity/depth vs ITM counterparts due to hedging demand asymmetry and lower capital requirements. ITM options embed intrinsic value → noisier extrinsic/vol pricing.

Logic: * K ≥ F → call OTM (put ITM) → prefer call * K < F → put OTM (call ITM) → prefer put * Fallback to other leg if NaN/missing

Improves SVI calibration stability by prioritizing cleaner quotes.

Parameters:
  • strike (float) – Option strike K.

  • forward (float) – Implied forward F_{t,T}.

  • call_mid (float) – Call option mid-price.

  • put_mid (float) – Put option mid-price.

Returns:

Preferred leg price (mid), or other if unavailable.

Return type:

float

Examples

>>> choose_leg(105, 100, 2.5, np.nan)  # Call OTM → use call
2.5
>>> choose_leg(95, 100, np.nan, 1.8)   # Put OTM → use put
1.8
pysvi.calibration.prepare_slice(df_slice, maturity_col='maturity', strike_col='strike', iv_col='iv', forward_col='implied_forward', min_points=5, return_index=False)[source]

Transform single maturity slice to SVI-ready inputs: k, w_target, F.

Pipeline: 1. Extract T, F (uniform across slice) 2. Filter valid (finite, positive) K, σ_mkt 3. Compute k = log(K/F), w = σ_mkt² × T 4. Final finite check + extreme k clipping (±10 ~ 0.00005% moneyness)

Rejects illiquid/noisy slices (< min_points after cleaning).

Parameters:
  • df_slice (pd.DataFrame) – Single maturity cross-section (strikes × time).

  • maturity_col (str, default "maturity") – Column with T (years fraction).

  • strike_col (str, default "strike") – Strike prices K.

  • iv_col (str, default "iv") – BSM implied vols σ_mkt.

  • forward_col (str, default "implied_forward") – F_{t,T} (constant per slice).

  • min_points (int, default 5) – Minimum valid strikes required.

  • return_index (bool, default False) – Also return the positional indices of the surviving rows (into df_slice), so companion columns (e.g. bid/ask implied vols) can be filtered identically to the quotes.

Returns:

(k, w_target, F), or (k, w_target, F, index) with return_index=True; the entries are None if invalid.

Return type:

tuple

Notes

k clipping prevents optimizer divergence from wing noise.

Examples

>>> df = pd.DataFrame({"strike": [90,100,110], "iv": [0.22,0.20,0.23],
...                    "maturity": 0.25, "implied_forward": 100})
>>> prepare_slice(df)
(array([-0.105,  0.   ,  0.095]), array([0.012, 0.010, 0.013]), 100.0)
pysvi.calibration.calibrate_slice(df_slice, model, maturity_col='maturity', **model_kwargs)[source]

Calibrate parametrization to single maturity cross-section.

Orchestrates prepare_slice() → model.calibrate() → store F.

Parameters:
  • df_slice (pd.DataFrame) – Strike slice for one T (from groupby ‘maturity’).

  • model (Parametrization) – SVI/SSVI/eSSVI instance.

  • maturity_col (str, default "maturity") – Passed to prepare_slice().

  • **model_kwargs – Forwarded to model.calibrate().

Returns:

Calibrated params + ‘forward’, or None if prep/calibration fails.

Return type:

Dict[str, float] or None

Examples

>>> svi = SVI()
>>> params = calibrate_slice(df_slice, svi)
>>> params["a"], params["forward"]
(0.01, 100.0)
pysvi.calibration.apply_slice(df_slice, params, model, maturity_col='maturity', strike_col='strike', iv_col='iv', fitted_col='fitted_iv', residual_col='residual_iv')[source]

Generate fitted IVs + residuals for calibrated slice.

Forward pass: w(k; params) → σ_fit = sqrt(w/T)

Adds columns in-place to copy. Clamps w/T ≥ 0 for numerical stability. Residuals optional (requires original iv_col).

Parameters:
  • df_slice (pd.DataFrame) – Original slice (strikes for this T).

  • params (Dict[str, float]) – From calibrate_slice(), must contain ‘forward’.

  • model (Parametrization) – Matching model instance.

  • maturity_col (str, default "maturity") – Uniform T value.

  • strike_col (str, default "strike") – K values.

  • iv_col (str, default "iv") – For residuals (ignored if missing).

  • fitted_col (str, default "fitted_iv") – Output fitted σ column name.

  • residual_col (str, default "residual_iv") – Output residual column name.

Returns:

Enriched slice with fitted_iv, residual_iv.

Return type:

pd.DataFrame

Examples

>>> import pandas as pd
>>> import numpy as np
>>> df_slice = pd.DataFrame({
...     "strike": np.array([90, 100, 110]),
...     "iv": np.array([0.22, 0.20, 0.23]),
...     "maturity": 0.25
... })
>>> params = {"forward": 100.0, "a": 0.01, "b": 0.1, "rho": -0.5, "m": 0, "sigma": 0.3}
>>> svi = SVI()
>>> fitted_df = apply_slice(df_slice, params, svi)
>>> fitted_df[["strike", "iv", "fitted_iv", "residual_iv"]].round(3)
   strike    iv  fitted_iv  residual_iv
0     90.0  0.220     0.219       0.001
1    100.0  0.200     0.200       0.000
2    110.0  0.230     0.229       0.001
pysvi.calibration.get_model(model_name, arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]

Factory for parametrization by lowercase name.

Supported: * ‘svi’: Raw SVI (Gatheral 2004) - 5 params * ‘natural’ / ‘nsvi’: Natural SVI (Gatheral & Jacquier 2014) - 5 params * ‘ssvi’: Surface SSVI - arbitrage-free across T * ‘essvi’: eSSVI - extended rho(T) parametrization * ‘directsvi’ / ‘dsvi’: Direct algebraic SVI (Schadner) - 6 conic coefficients * ‘sabr’: SABR (Hagan et al. 2002) - stochastic vol model for rates/FX smiles

Case-insensitive. Extensible: add to dict.

Parameters:
  • model_name (str) – ‘svi’, ‘natural’, ‘nsvi’, ‘ssvi’, ‘essvi’, ‘jumpwings’, ‘jw’, ‘directsvi’, ‘dsvi’, ‘sabr’.

  • arbitrage_condition (ArbitrageFreedom, default QUASI) – Arbitrage constraints to enforce during calibration.

Returns:

Instantiated model.

Return type:

Parametrization

Raises:

KeyError – Unknown model_name.

Examples

>>> svi = get_model("SVI")
>>> ssvi = get_model("ssvi", ArbitrageFreedom.NO_BUTTERFLY)

Surface

Fitted volatility surface: evaluation, interpolation, and pricing.

VolSurface turns per-slice calibration results into the object quant work actually consumes: model -> calibration -> fitted surface. It owns calibrated slices across maturities and exposes vectorized evaluation (IVs, total variance, ATM level/skew/curvature), maturity interpolation between slices, arbitrage verification, and a Black-76 pricing and Greeks layer on the slice forwards.

calibrate_surface is the calendar-aware fitter: it orders expiries, derives per-slice inputs, chains the prior slice’s total variance into each NO_CALENDAR penalty automatically, and — for eSSVI — fits the global term structure jointly across all slices.

Conventions

  • Pricing is Black-76 on the slice forward with a flat continuously compounded rate r (default 0): C = e^{-rT}[F N(d1) - K N(d2)].

  • Greeks hold the implied volatility fixed (sticky-strike): delta and gamma are with respect to the forward, vega is per unit volatility, theta is per year of calendar time.

  • Between fitted maturities the surface interpolates (linearly in total variance at fixed log-moneyness by default; see interp_method); forwards interpolate log-linearly. Extrapolation beyond the fitted maturity range raises.

class pysvi.surface.VarianceEvent(time, variance, label='')[source]

Bases: object

A discrete variance event (earnings, FOMC, CPI, election).

Real term structures contain known jumps: generic maturity interpolation smooths across them, silently asserting all term-structure curvature is continuous variance. With events, total variance decomposes as:

w(k, T) = w_cont(k, T) + sum of event variances with t_e <= T

Fitting subtracts each expiry’s cumulative event variance before calibration (slices store the CONTINUOUS component), interpolation acts on the continuous component, and evaluation adds the events back on the correct side of each event time – so the surface reproduces the jump exactly instead of smearing it, and the calendar diagnostics (which see the continuous slices) raise no false violation across an event.

Parameters:
  • time (float)

  • variance (float)

  • label (str)

time

Event time as a year fraction (same clock as maturities).

Type:

float

variance

Total variance added by the event (ATM units, e.g. sigma_event^2 * dt), non-negative.

Type:

float

label

Optional tag (“AAPL earnings”, “FOMC”).

Type:

str

pysvi.surface.implied_event_variances(df, events)[source]

What the quoted term structure says about each event’s variance.

For every event, finds the quoted expiries straddling it and reports the jump in ATM total variance across the event (atm_w(T_after) - atm_w(T_before)) next to the variance the event specifies. The quoted jump also contains the continuous variance accrued between the two expiries, so it is an UPPER bound on the event variance; a specified variance far above it is inconsistent with the market.

Returns a list of dicts: {event, T_before, T_after, quoted_jump, specified}. Events without straddling quotes report None bounds.

Return type:

list

class pysvi.surface.VolSurface(model, slices, r=0.0, interp_method='total_variance', fit_report=None, events=())[source]

Bases: object

A fitted implied-volatility surface across maturities.

Construct via fit() (independent per-slice calibration), calibrate_surface() (calendar-aware), or directly from calibrated slices:

surface = VolSurface.fit(df, model="svi")
surface = VolSurface(model, {0.25: params_1, 0.5: params_2})

Direct construction requires each params dict to carry 'forward' (as returned by calibrate_slice).

Parameters:
  • model (Parametrization) – The model instance all slices were calibrated with.

  • slices (mapping or iterable of (maturity, params)) – Calibrated slices; sorted by maturity internally.

  • r (float, default 0.0) – Flat continuously compounded discount rate used by the pricing layer.

  • interp_method (str, default "total_variance") – Maturity interpolation between fitted slices. “total_variance” blends w(k) linearly in T at fixed log-moneyness (model-agnostic; calendar-free whenever the bracketing slices are). “theta” (SSVI/eSSVI only) interpolates the ATM total variance and shape parameters, yielding a genuine parametric slice at any maturity. “monotone_cubic” is a shape-preserving cubic (PCHIP, Fritsch-Carlson) in maturity at fixed log-moneyness across ALL fitted slices: continuously differentiable in T (C1, see regularity and dw_dT()), exact at fitted maturities, and monotone in T wherever the fitted slices are – so calendar-free slices stay calendar-free between expiries. Requires at least two fitted slices.

  • fit_report (SurfaceFitReport, optional) – Calibration provenance and per-slice evidence; populated by fit() and calibrate_surface(), None on direct construction.

classmethod fit(df, model='svi', arbitrage_condition=<ArbitrageFreedom.QUASI: 0>, r=0.0, interp_method='total_variance', mode='warn', prior=None, anchor=0.0, events=(), **model_kwargs)[source]

Calibrate every maturity slice of an option panel independently.

Expects the calibrate_slice schema: columns strike, iv, maturity, implied_forward, with multiple maturities in one DataFrame. Model-specific per-slice arguments are derived automatically: theta (ATM total variance) for SSVI and eSSVI – plus, for eSSVI only, theta_ref defaulting to the median across slices – T for jump-wings, and T/F for SABR (beta defaults to 0.5 — override via model_kwargs).

Calibration controls (objective, loss, f_scale, initialization) pass through to every slice via model_kwargs. Slices that fail to calibrate are skipped with a warning; fitting fails only if no slice succeeds. For cross-slice calendar enforcement use calibrate_surface().

Parameters:
  • df (pd.DataFrame) – Multi-expiry option panel.

  • model (str or Parametrization, default "svi") – Factory name or a model instance.

  • arbitrage_condition (ArbitrageFreedom, default QUASI) – Used when model is a factory name.

  • r (float, default 0.0) – Flat discount rate for the pricing layer.

  • interp_method (str, default "total_variance") – Maturity interpolation method (see the class docstring).

  • mode (str, default "warn") – Failure handling for slices. "strict": the first slice that is too thin to calibrate or fails to converge raises with its maturity. "warn": skipped with a logged warning. "lenient": skipped silently. Every mode records the failure on the fit report.

  • **model_kwargs – Forwarded to every per-slice calibration.

  • prior (VolSurface)

  • anchor (float)

Return type:

VolSurface

property maturities: ndarray

Fitted maturities, ascending.

slice_at(maturity)[source]

Parameter dict of the (possibly synthetic) slice at maturity.

Exact for fitted maturities. Between slices this requires a parametric interpolation method (interp_method="theta"); the default total-variance blend has no parameter representation and raises here (evaluation methods still work at any maturity).

Return type:

Dict[str, float]

params(maturity)[source]

Calibrated parameter dict of the fitted slice at maturity (a copy).

Return type:

Dict[str, float]

forward(maturity)[source]

Forward price at maturity (log-linear between fitted slices).

Return type:

float

property regularity: str

“C0” or “C1”.

“C0” (the default total-variance blend and the theta method): w(k, T) is continuous in T but its maturity derivative jumps at every fitted slice. Ready for implied vols, prices, and sticky-strike Greeks; NOT ready for quantities that consume dw/dT – Dupire local volatility, forward variance, PDE coefficients – whose inputs would be discontinuous.

“C1” (interp_method=”monotone_cubic”): dw/dT exists and is continuous everywhere in the fitted maturity range (exposed via dw_dT()), making the surface Dupire-ready in maturity. Smoothness in strike comes from the model itself and is analytic (C-infinity) for the SVI family either way.

Type:

Smoothness guarantee of the surface in maturity

dw_dT(k, maturity)[source]

Maturity derivative of total variance, dw/dT at fixed k.

The Dupire numerator. Only available when the interpolation method is C1 in maturity (interp_method=”monotone_cubic”); the C0 methods have jump discontinuities at the fitted slices, and a one-sided number there would be silently wrong.

total_variance(k, maturity)[source]

Total variance w(k) at any maturity in the fitted range.

iv(strike, maturity, return_status=False)[source]

Implied volatility at absolute strike(s), any maturity in range.

With return_status=True also returns a domain-of-validity label per point – "observed" (a fitted maturity, inside that slice’s quoted strike range), "interpolated" (between fitted maturities, inside the bracketing slices’ joint quoted range), or "extrapolated" (outside the quoted domain: the model’s wings, not market information). Requires a fit report (surfaces constructed directly from parameter dicts carry no quoted ranges and raise).

Parameters:

return_status (bool)

atm_vol(maturity)[source]

At-the-money (k = 0) implied volatility.

Return type:

float

skew(maturity)[source]

ATM total-variance skew dw/dk at k = 0.

Return type:

float

curvature(maturity)[source]

ATM total-variance curvature d2w/dk2 at k = 0.

Return type:

float

check_arbitrage(**kwargs)[source]

Run the arbitrage diagnostics over all fitted slices.

Forwards to pysvi.diagnostics.check_arbitrage() (butterfly, Lee wing bounds, calendar); keyword arguments (k_min, k_max, n_grid, tol, k_data) pass through.

Return type:

ArbitrageReport

diagnose(**kwargs)[source]

Fit report and arbitrage diagnostics in one result block.

Combines fit_report (calibration status, quote accounting, residuals, settings, provenance) with check_arbitrage(). When no explicit grid is given and a fit report is available, the arbitrage checks run on the quoted log-moneyness range (the surface’s domain of validity) rather than the wide default grid. print(surface.diagnose()) renders the formatted block; all fields are individually accessible.

Return type:

SurfaceDiagnostics

save(path)[source]

Write the surface to path as versioned JSON.

The schema captures everything evaluation needs — model name and arbitrage condition, per-slice maturities and parameters (forwards included), the flat rate, the interpolation method — plus the fit report and provenance when present. Calibrating is expensive and evaluating is cheap: save once, distribute, and load() reproduces evaluation exactly.

Return type:

None

classmethod load(path)[source]

Reconstruct a surface saved by save().

Validates the schema version and model name; raises ValueError on an unknown schema or model.

Return type:

VolSurface

price(strike, maturity, cp='call')[source]

Black-76 option price at absolute strike(s).

call = e^{-rT} [F N(d1) - K N(d2)]
put  = e^{-rT} [K N(-d2) - F N(-d1)]

with d1 = (log(F/K) + w/2)/sqrt(w), d2 = d1 - sqrt(w), and w the surface total variance at the strike.

Parameters:

cp (str)

delta(strike, maturity, cp='call')[source]

Black-76 forward delta, e^{-rT} N(d1) (call) or -e^{-rT} N(-d1) (put).

Holds the implied volatility fixed (sticky-strike).

Parameters:

cp (str)

gamma(strike, maturity)[source]

Black-76 gamma, e^{-rT} phi(d1) / (F sqrt(w)). Same for calls and puts.

vega(strike, maturity)[source]

Black-76 vega per unit volatility, e^{-rT} F phi(d1) sqrt(T).

theta(strike, maturity, cp='call')[source]

Black-76 theta per year of calendar time, holding sigma fixed.

theta = r V - e^{-rT} F phi(d1) sigma / (2 sqrt(T))
Parameters:

cp (str)

pysvi.surface.calibrate_surface(df, model='ssvi', enforce_calendar=True, arbitrage_condition=<ArbitrageFreedom.QUASI: 0>, r=0.0, interp_method='total_variance', mode='warn', prior=None, anchor=0.0, events=(), **model_kwargs)[source]

Calendar-aware multi-expiry calibration returning a VolSurface.

Orders the expiries and calibrates them oldest-first, automatically evaluating each fitted slice on the next slice’s penalty grid and passing it as w_prev — the manual chaining the per-slice API requires. With enforce_calendar the NO_CALENDAR flag is added to the model’s arbitrage condition, and for SSVI/eSSVI the per-slice ATM total variances are made non-decreasing before fitting.

For eSSVI the global term structure (rho0, rho1, alpha, eta) is fitted jointly across all slices against the shared theta_ref (median ATM total variance), rather than independently per slice — every returned slice carries identical shape parameters.

After fitting, SSVI-form slices are checked against the Gatheral-Jacquier sufficient no-butterfly bounds and a warning is logged for slices outside the proven-safe region.

Parameters:
  • df (pd.DataFrame) – Multi-expiry option panel (calibrate_slice schema).

  • model (str or Parametrization, default "ssvi") – Factory name or model instance. DirectSVI is rejected when enforce_calendar is set (no penalty support).

  • enforce_calendar (bool, default True) – Add NO_CALENDAR to the arbitrage condition and chain w_prev.

  • arbitrage_condition (ArbitrageFreedom, default QUASI) – Base condition when model is a factory name (NO_BUTTERFLY may be OR-ed in; NO_CALENDAR is added by enforce_calendar).

  • r (float, default 0.0) – Flat discount rate for the pricing layer.

  • interp_method (str, default "total_variance") – Maturity interpolation method for the returned surface.

  • **model_kwargs – Calibration controls and model extras, forwarded to every slice (beta for SABR, etc.). The bid_ask objective is not supported in the joint eSSVI path.

  • mode (str)

  • prior (VolSurface)

  • anchor (float)

Return type:

VolSurface

Chain ingestion

Option-chain ingestion: raw call/put quotes to a calibration-ready panel.

OptionChain formalises the preprocessing every user otherwise reassembles by hand: mid computation, implied forwards from put-call parity, OTM leg selection, Black-76 implied-vol inversion, and bid/ask IV bands. The result is the calibrate_slice panel schema plus iv_bid/iv_ask columns, and chain.fit(…) goes straight to a VolSurface.

IV inversion is Black-76 on the implied forward (dividends and repo are embedded in the forward), so neither spot nor a dividend assumption is needed when both legs are quoted; a spot plus rate/dividend inputs serve only as the forward fallback for expiries missing put-call pairs.

pysvi.chain.RateLike

flat float; an interest_rate_models.DiscountCurve or interest-rate model (Vasicek, Hull-White, …); or any callable T -> r(T), e.g. a scipy CubicSpline over curve pillars.

Type:

A rate view in any accepted form

alias of float | Callable[[float], float] | _SupportsZeroRate

class pysvi.chain.OptionChain(panel, rate=0.0, rejections=None)[source]

Bases: object

A cleaned option chain ready for surface calibration.

Build with from_dataframe(); access the calibration panel via panel (the calibrate_slice schema: strike, iv, maturity, implied_forward, plus iv_bid/iv_ask), and fit a surface with fit().

Parameters:
  • panel (DataFrame)

  • rate (float | Callable[[float], float] | _SupportsZeroRate)

  • rejections (dict | None)

rejections

{“rejected_quotes”, “failed_inversions”, “skipped_expiries” (list of T)}. Populated by from_dataframe.

Type:

Ingestion accounting

property panel: DataFrame

Calibration panel (a copy).

classmethod from_dataframe(df, strike='strike', expiry='expiry', cp='cp', bid='bid', ask='ask', spot=None, rate=0.0, dividend_yield=0.0, mode='warn', context=None)[source]

Ingest raw call/put quotes into a calibration-ready chain.

Per expiry: mids from bid/ask (rows with crossed, missing, or non-positive quotes are dropped), the implied forward as the median put-call-parity forward over strikes quoting both legs (falling back to spot * exp((r - q) T) when no pair exists and spot is given), OTM leg selection, and Black-76 implied vols for mid, bid, and ask on the selected leg. Quotes whose mid cannot be inverted become NaN ivs and are counted out by the downstream fit report.

Parameters:
  • df (pd.DataFrame) – Raw quotes; one row per option.

  • strike (str) – Column names. expiry is a year fraction – or, with context=, real expiry dates resolved through the context’s day count; cp accepts ‘c’/’call’/’p’/’put’ (case-insensitive).

  • expiry (str) – Column names. expiry is a year fraction – or, with context=, real expiry dates resolved through the context’s day count; cp accepts ‘c’/’call’/’p’/’put’ (case-insensitive).

  • cp (str) – Column names. expiry is a year fraction – or, with context=, real expiry dates resolved through the context’s day count; cp accepts ‘c’/’call’/’p’/’put’ (case-insensitive).

  • bid (str) – Column names. expiry is a year fraction – or, with context=, real expiry dates resolved through the context’s day count; cp accepts ‘c’/’call’/’p’/’put’ (case-insensitive).

  • ask (str) – Column names. expiry is a year fraction – or, with context=, real expiry dates resolved through the context’s day count; cp accepts ‘c’/’call’/’p’/’put’ (case-insensitive).

  • spot (float, optional) – Underlying spot, used only for the forward fallback.

  • rate (float, curve, model, or callable, default 0.0) – Continuously compounded zero rates: a flat float, an interest_rate_models.DiscountCurve, an interest-rate model from interest_rate_models (its implied zero curve from today is used), or any callable T -> r(T) – e.g. a scipy.interpolate.CubicSpline over curve pillars.

  • dividend_yield (float, curve, model, or callable, default 0.0) – Continuous dividend yield for the forward fallback only (put-call-parity forwards embed dividends already).

  • context (MarketContext, optional) – Single source of numeraire and time conventions (pysvi.context.MarketContext): supplies spot, rate, dividend view and the day count that turns expiry dates into year fractions. Mutually exclusive with the separate spot/rate/dividend_yield arguments.

  • mode (str, default "warn") – Failure handling. "strict": the first bad input raises with its location (invalid quote rows, an expiry that cannot form a forward, a mid quote whose implied vol will not invert). "warn": problems are logged and recorded. "lenient": problems are filtered silently but still recorded. All modes record counts on rejections, and fit() carries them onto the surface’s fit report – nothing disappears without a count. Unrecognized cp values raise in every mode (schema error, not bad data).

Return type:

OptionChain

fit(model='svi', enforce_calendar=False, arbitrage_condition=<ArbitrageFreedom.QUASI: 0>, r=None, mode='warn', **model_kwargs)[source]

Calibrate a surface from the chain in one call.

Routes to calibrate_surface() when enforce_calendar is set, else VolSurface.fit(). r sets the surface’s flat pricing rate; when omitted it defaults to the chain’s rate if that is a flat float, else 0.0 (a callable term structure has no flat representation on the surface yet). mode sets the failure handling for the fit (see from_dataframe()); the chain’s ingestion accounting is carried onto the returned surface’s fit report.

Parameters:
Return type:

VolSurface

Reports

Calibration fit reports and the surface diagnostics wrapper.

A surface is the output of a calibration process, and the process itself carries the evidence needed to decide whether the output is trustworthy. SurfaceFitReport records that evidence per slice (status, quote accounting, residual statistics, quoted domain) together with the settings and provenance of the fit; SurfaceDiagnostics combines it with the arbitrage diagnostics into one formatted, field-accessible result block (in the spirit of scikit-learn reports).

pysvi.report.SLICE_OK = 'ok'

Slice statuses recorded by the fitters.

class pysvi.report.SliceFitReport(maturity, status, n_quotes, n_used, iv_rmse=None, max_abs_iv_residual=None, k_min=None, k_max=None)[source]

Bases: object

Fit evidence for a single maturity slice.

Parameters:
  • maturity (float)

  • status (str)

  • n_quotes (int)

  • n_used (int)

  • iv_rmse (float | None)

  • max_abs_iv_residual (float | None)

  • k_min (float | None)

  • k_max (float | None)

maturity

Slice maturity in years.

Type:

float

status

“ok”, “failed” (calibration did not converge), or “insufficient_data” (slice rejected before calibration).

Type:

str

n_quotes

Rows entering the slice.

Type:

int

n_used

Quotes surviving cleaning (finite, positive) and used in the fit.

Type:

int

iv_rmse

RMSE of fitted vs market implied vol on the used quotes.

Type:

float or None

max_abs_iv_residual

Largest absolute implied-vol residual on the used quotes.

Type:

float or None

k_min, k_max

Quoted log-moneyness range (the slice’s domain of validity).

Type:

float or None

pysvi.report.build_slice_report(maturity, status, n_quotes, k=None, iv_mkt=None, iv_fit=None)[source]

Assemble a SliceFitReport, computing residual statistics when fitted.

Parameters:
  • maturity (float)

  • status (str)

  • n_quotes (int)

Return type:

SliceFitReport

class pysvi.report.SurfaceFitReport(model, objective, loss, initialization, calendar_enforced, backend, slices, created_utc, pysvi_version, n_rejected_quotes=0, n_failed_inversions=0, n_skipped_expiries=0)[source]

Bases: object

Calibration provenance and per-slice fit evidence for a surface.

Parameters:
  • model (str)

  • objective (str)

  • loss (str)

  • initialization (str)

  • calendar_enforced (bool)

  • backend (str)

  • slices (Tuple[SliceFitReport, ...])

  • created_utc (str)

  • pysvi_version (str)

  • n_rejected_quotes (int)

  • n_failed_inversions (int)

  • n_skipped_expiries (int)

model

Parametrization class name.

Type:

str

objective, loss, initialization

Calibration controls used for every slice.

Type:

str

calendar_enforced

Whether cross-slice calendar chaining was active.

Type:

bool

backend

“numba” or “numpy” at fit time.

Type:

str

slices

Per-slice evidence, ordered by maturity (failed slices included).

Type:

tuple of SliceFitReport

created_utc

Fit timestamp (ISO 8601, UTC).

Type:

str

pysvi_version

Library version that produced the fit.

Type:

str

n_rejected_quotes: int = 0

raw quote rows rejected before the panel, mid-quote inversions that failed to a NaN implied vol, and expiries dropped whole. Zero when the surface was fitted from a prepared panel directly.

Type:

Ingestion accounting (populated by OptionChain.fit)

property ok: bool

True iff every slice in the input panel calibrated.

quoted_range()[source]

Union of the quoted log-moneyness ranges across slices that actually made it into the surface – a failed slice’s range must not widen the domain the diagnostics certify.

Return type:

Tuple[float, float] | None

summary()[source]

Formatted fit-report block.

Return type:

str

pysvi.report.FAILURE_MODES = ('strict', 'warn', 'lenient')

Failure-handling modes shared by the ingestion and fit entry points. strict: the first bad input raises with its location. warn (default): problems are logged and recorded. lenient: problems are filtered silently but still recorded – nothing ever disappears without a count.

pysvi.report.build_surface_report(slices, model, model_kwargs, calendar_enforced)[source]

Assemble a SurfaceFitReport with settings and provenance.

Parameters:
  • slices (Tuple[SliceFitReport, ...])

  • model (str)

  • model_kwargs (dict)

  • calendar_enforced (bool)

Return type:

SurfaceFitReport

class pysvi.report.SurfaceDiagnostics(fit, arbitrage)[source]

Bases: object

Fit report and arbitrage diagnostics in one result block.

Returned by VolSurface.diagnose(). Fields are individually accessible; str() renders the combined formatted block.

Parameters:
property ok: bool

True iff every slice fitted and no arbitrage was detected.

Market context

MarketContext: one coherent source of numeraire and time conventions.

A surface stores a rate and per-slice forwards, and maturities are bare year-fraction floats. Nothing in those pieces says which day-count produced T = 0.5, or guarantees that discounting, forwards and time all came from the same convention. MarketContext is that single source of truth: a valuation time, a rate view, a dividend view, a spot, and a day-count convention, from which year fractions, discount factors and fallback forwards are all derived coherently. Pass it to OptionChain.from_dataframe(context=...) (with real expiry DATES in the panel) and every downstream number shares one convention by construction; mixing a context with separately supplied rate/spot inputs is loudly rejected.

class pysvi.context.MarketContext(valuation_time, spot=None, rate=0.0, dividend_yield=0.0, day_count='ACT/365F', holidays=None)[source]

Coherent numeraire, day-count and calendar conventions.

Parameters:
  • valuation_time (object)

  • spot (float | None)

  • rate (object)

  • dividend_yield (object)

  • day_count (str)

  • holidays (Sequence | None)

valuation_time

The single “now” every year fraction is measured from.

Type:

timestamp-like

spot

Underlying spot at valuation time (forward fallback only; real forwards come from put-call parity as always).

Type:

float, optional

rate

Continuously compounded zero rates in any form the library accepts (flat float, irm.DiscountCurve, an interest-rate model, or T -> r(T)).

Type:

float, curve, model, or callable

dividend_yield

Continuous dividend view, same forms.

Type:

float, curve, model, or callable

day_count

One of ACT/365F (default), ACT/360, ACT/365.25, BUS/252 (business days over 252; supply holidays).

Type:

str

holidays

Holiday calendar for BUS/252.

Type:

sequence of dates, optional

year_fraction(when)[source]

Year fraction from valuation_time to when (scalar or array of timestamps/date strings), under this day count.

rate_at(T)[source]

Continuously compounded zero rate r(T) under this context.

discount(T)[source]

Discount factor D(T) = exp(-r(T) T). T may also be a date, resolved through year_fraction() first.

Return type:

float

forward(T)[source]

Fallback forward spot * exp((r - q) T); real forwards come from put-call parity in the chain.

Return type:

float

pysvi.context.DAY_COUNTS = ('ACT/365F', 'ACT/360', 'ACT/365.25', 'BUS/252')

Supported day-count conventions.

Identifiability

Parameter identifiability and uncertainty for calibrated slices.

A fitted smile can be excellent while its parameters are barely determined: near-indistinguishable SVI smiles arise from very different (a, b, rho, m, sigma), especially on narrow strike ranges where the wings are unconstrained. Stable fitted implied vol is NOT stable fitted parameters – a signal built on parameter changes can be pure optimizer noise. This module quantifies that:

  • condition_number() – conditioning of the (column-scaled) parameter Jacobian dw/dtheta at the quotes.

  • parameter_uncertainty() – Gauss-Newton covariance at the optimum, residual-scaled, as per-parameter standard errors and a correlation matrix.

  • identifiability_report() – the two combined into a formatted block flagging poorly identified parameters and near-degenerate parameter pairs.

The Jacobian comes from Parametrization.param_jacobian(): analytic for the SVI family (raw, natural, SSVI), central finite differences elsewhere.

pysvi.identifiability.condition_number(J)[source]

Condition number of the column-scaled parameter Jacobian.

Columns are scaled to unit norm first, so the number measures the geometry of the parameter directions (how close to collinear they are at the quotes), not their units. Large values mean some parameter combination moves the fitted smile almost not at all – the optimizer could trade those parameters against each other freely. Returns inf for a rank-deficient Jacobian.

Parameters:

J (NDArray[float64])

Return type:

float

pysvi.identifiability.parameter_uncertainty(model, params, k, w_target)[source]

Standard errors and correlations from the Gauss-Newton approximation at the fitted optimum.

The covariance is sigma^2 (J^T J)^+ with sigma^2 = RSS / (n - p) in total-variance space and + the pseudo-inverse (so a rank-deficient Jacobian yields large-but-finite numbers in the identified directions and the report flags the degeneracy). With n <= p the fit is under-determined and every standard error is inf.

Parameters:
  • model (Parametrization)

  • params (Dict[str, float])

  • k (NDArray[float64])

  • w_target (NDArray[float64])

Return type:

ParameterUncertainty

pysvi.identifiability.identifiability_report(model, params, k, w_target, rel_threshold=0.5, corr_threshold=0.95)[source]

Assess how well the fitted parameters are determined by the data.

Flags a parameter as poorly identified when its standard error exceeds rel_threshold of its magnitude, and a pair as near-degenerate when their correlation exceeds corr_threshold in absolute value (the optimizer can trade one against the other with almost no change to the fitted smile). Typical trigger: a narrow strike range leaving the wing parameters unconstrained while the IV RMSE is tiny.

Parameters:
  • model (Parametrization) – The calibrated model instance.

  • params (dict) – Its calibrated parameters (as returned by calibrate).

  • k (ndarray) – The quotes the fit used: log-moneyness and total variance (from prepare_slice).

  • w_target (ndarray) – The quotes the fit used: log-moneyness and total variance (from prepare_slice).

  • rel_threshold (float, default 0.5) – Relative standard-error threshold for the per-parameter flag.

  • corr_threshold (float, default 0.95) – Absolute-correlation threshold for the pair flag.

Return type:

IdentifiabilityReport

class pysvi.identifiability.ParameterUncertainty(names, values, std_errors, correlation, dof, rss)[source]

Gauss-Newton parameter uncertainty at a calibrated optimum.

Parameters:
  • names (Tuple[str, ...])

  • values (Tuple[float, ...])

  • std_errors (Tuple[float, ...])

  • correlation (NDArray[float64])

  • dof (int)

  • rss (float)

names

Free-parameter names, in Jacobian column order.

Type:

tuple of str

values

Fitted values.

Type:

tuple of float

std_errors

Per-parameter standard errors (sqrt of the covariance diagonal); inf where the fit is under-determined.

Type:

tuple of float

correlation

p x p parameter correlation matrix.

Type:

ndarray

dof

Residual degrees of freedom, n_points - n_params.

Type:

int

rss

Residual sum of squares in total-variance space.

Type:

float

class pysvi.identifiability.IdentifiabilityReport(model, n_points, k_min, k_max, rmse_w, condition_number, uncertainty, poorly_identified, degenerate_pairs, rel_threshold, corr_threshold)[source]

Identifiability assessment of one calibrated slice.

print(report) renders the formatted block; every field is individually accessible. ok is False when any parameter is flagged poorly identified or any pair is nearly degenerate.

Parameters:
  • model (str)

  • n_points (int)

  • k_min (float)

  • k_max (float)

  • rmse_w (float)

  • condition_number (float)

  • uncertainty (ParameterUncertainty)

  • poorly_identified (Tuple[str, ...])

  • degenerate_pairs (Tuple[Tuple[str, str, float], ...])

  • rel_threshold (float)

  • corr_threshold (float)

pysvi.identifiability.quote_sensitivity(model, params, k, w_target)[source]

Sensitivity of the fitted parameters to each quote, dtheta/dw_i.

Implicit-function Jacobian at the least-squares optimum: with J = dw_model/dtheta at the quotes, a perturbation dw of the quote vector moves the optimum by dtheta = (J^T J)^+ J^T dw – the Gauss-Newton system that already powers the uncertainty reports. Returns the p x n matrix whose column i answers “if quote i’s total variance moves by 1, where do the parameters go”.

Valid to first order at a (local) optimum of the unpenalized least-squares objective; arbitrage penalties active at the optimum shift the picture only when they bind.

Parameters:
  • model (Parametrization)

  • params (Dict[str, float])

  • k (NDArray[float64])

  • w_target (NDArray[float64])

Return type:

NDArray[float64]

pysvi.identifiability.surface_sensitivity(model, params, k, w_target, k_eval)[source]

Quote-to-surface Jacobian: dw(k_eval) / dw(quote_i), m x n.

Chains quote_sensitivity() through the model’s parameter Jacobian at the evaluation points: row j says how the fitted total variance at k_eval[j] responds to a unit move in each quote’s total variance. The core of hedging, P&L explain and scenario analysis: bump one quote, read the whole smile’s response without recalibrating.

Parameters:
  • model (Parametrization)

  • params (Dict[str, float])

  • k (NDArray[float64])

  • w_target (NDArray[float64])

  • k_eval (NDArray[float64])

Return type:

NDArray[float64]

pysvi.identifiability.iv_surface_sensitivity(model, params, k, w_target, k_eval, T)[source]

surface_sensitivity() in implied-vol units on both sides.

Entry (j, i) is div(k_eval_j)/div(quote_i): with w = iv^2 T on both sides, the w-space Jacobian is scaled by 2 iv_i T per quote column and 1 / (2 iv_j T) per evaluation row. The natural view for “this quote moves 1 vol point – what does the smile do”.

Parameters:
  • model (Parametrization)

  • params (Dict[str, float])

  • k (NDArray[float64])

  • w_target (NDArray[float64])

  • k_eval (NDArray[float64])

  • T (float)

Return type:

NDArray[float64]

Diagnostics

First-class arbitrage diagnostics for calibrated parametrizations.

Verifies fitted slices and surfaces rather than trusting that penalty-constrained calibration succeeded:

  • domain validity — w(k) must be finite and strictly positive before any other criterion is meaningful (g(k) is only defined for w > 0)

  • butterfly arbitrage — non-negative risk-neutral density g(k)

  • Lee wing bounds — total-variance wing slopes at most 2 [Lee 2004], using the model’s closed-form asymptotic slopes where available (the SVI family) and a grid-edge measurement otherwise

  • calendar arbitrage — total variance non-decreasing in maturity

check_slice_arbitrage works on a single (model, params) pair; check_arbitrage on a set of slices across maturities. Both return report dataclasses that carry the numerical evidence (minima, locations, invalid-point counts, grid) and render a human-readable summary via str(). A report never claims freedom from arbitrage it could not evaluate: invalid or non-finite grid regions fail the check.

pysvi.diagnostics.LEE_BOUND = 2.0

limsup w(k)/abs(k) <= 2.

Type:

Lee moment bound on total-variance wing slopes

class pysvi.diagnostics.SliceArbitrageReport(maturity, n_invalid, min_total_variance, butterfly_free, min_density, min_density_k, lee_free, left_wing_slope, right_wing_slope, wing_slope_method, max_lee_violation, k_min, k_max, n_grid)[source]

Bases: object

Numerical arbitrage evidence for a single calibrated slice.

Parameters:
  • maturity (float | None)

  • n_invalid (int)

  • min_total_variance (float)

  • butterfly_free (bool)

  • min_density (float)

  • min_density_k (float)

  • lee_free (bool)

  • left_wing_slope (float)

  • right_wing_slope (float)

  • wing_slope_method (str)

  • max_lee_violation (float)

  • k_min (float)

  • k_max (float)

  • n_grid (int)

maturity

Slice maturity, if provided.

Type:

float or None

n_invalid

Grid points where w is non-finite, w <= 0, or g is non-finite. Any invalid point fails the butterfly check: the criterion is undefined there, so freedom from arbitrage cannot be certified.

Type:

int

min_total_variance

Minimum of w(k) over the grid (NaN if w is nowhere finite).

Type:

float

butterfly_free

True iff the whole grid is valid and min g(k) >= -tol.

Type:

bool

min_density

Minimum of g(k) over the valid grid points (NaN if none).

Type:

float

min_density_k

Log-moneyness at which the minimum density occurs (NaN if none).

Type:

float

lee_free

True iff both wing slopes are finite and <= LEE_BOUND + tol.

Type:

bool

left_wing_slope

Left (put) wing slope dw/d(abs(k)).

Type:

float

right_wing_slope

Right (call) wing slope dw/dk.

Type:

float

wing_slope_method

“asymptotic” (closed form, SVI family) or “grid_edge” (measured at the grid boundary; underestimates the asymptote for convex w, so widen the grid for wide smiles).

Type:

str

max_lee_violation

max(wing slope - LEE_BOUND, 0) over both wings (NaN if a slope is non-finite).

Type:

float

k_min, k_max

Evaluation grid bounds.

Type:

float

n_grid

Number of grid points.

Type:

int

property ok: bool

True iff every per-slice condition was evaluable and passed.

class pysvi.diagnostics.ArbitrageReport(slices, calendar_free, min_calendar_margin, min_calendar_k, min_calendar_pair)[source]

Bases: object

Arbitrage evidence for a set of calibrated slices across maturities.

Parameters:
  • slices (List[SliceArbitrageReport])

  • calendar_free (bool)

  • min_calendar_margin (float)

  • min_calendar_k (float | None)

  • min_calendar_pair (Tuple[float, float] | None)

slices

Per-slice reports, ordered by maturity.

Type:

list of SliceArbitrageReport

calendar_free

True iff every adjacent maturity pair was evaluable and total variance is non-decreasing in maturity at every grid point (within tol). Trivially True for a single slice.

Type:

bool

min_calendar_margin

Minimum of w(k, T_next) - w(k, T) over adjacent maturity pairs and grid points; negative values are violations. +inf when fewer than two slices; NaN when no pair was evaluable.

Type:

float

min_calendar_k

Log-moneyness of the worst calendar margin (None if none evaluable).

Type:

float or None

min_calendar_pair

The (T, T_next) pair attaining the worst margin (None if none evaluable).

Type:

(float, float) or None

property ok: bool

True iff every slice passes and there is no calendar arbitrage.

pysvi.diagnostics.check_slice_arbitrage(model, params, maturity=None, k_min=-2.0, k_max=2.0, n_grid=801, tol=None, k_data=None)[source]

Check a single calibrated slice for butterfly arbitrage and Lee bounds.

Domain validity comes first: grid points where w(k) is non-finite, non-positive, or where g(k) is non-finite are counted as invalid, and any invalid point fails the butterfly check (freedom from arbitrage is never certified on an unevaluated region).

Lee bounds use the model’s closed-form asymptotic wing slopes where available (Parametrization.wing_slopes(); the SVI family), and otherwise measure dw/dk at the grid edges — a proxy that underestimates the asymptote for convex w, so widen the grid for very wide smiles when the method reads “grid_edge”.

Parameters:
  • model (Parametrization) – Model instance matching the params dict.

  • params (dict) – Calibrated parameters (as returned by model.calibrate).

  • maturity (float, optional) – Recorded in the report for labelling; not used in the checks.

  • k_min (float, default -2.0, 2.0) – Evaluation grid bounds in log-moneyness. Prefer k_data so verification covers the same domain calibration penalized.

  • k_max (float, default -2.0, 2.0) – Evaluation grid bounds in log-moneyness. Prefer k_data so verification covers the same domain calibration penalized.

  • n_grid (int, default 801) – Grid resolution.

  • tol (float, optional) – Defaults to the model’s diagnostics_tol (1e-8 for the analytic family, 1e-4 for finite-difference SABR/DirectSVI). Numerical tolerance for violations. For models with finite-difference derivatives (SABR, DirectSVI) the density carries FD noise well above this; use a looser tol (e.g. 1e-4) there for marginal fits.

  • k_data (array, optional) – Observed log-moneyness values. When given, the grid is [min(k_data) - 0.5, max(k_data) + 0.5] — the same policy the NO_BUTTERFLY/NO_CALENDAR calibration penalties use, so verification and enforcement cover the same domain.

Return type:

SliceArbitrageReport

pysvi.diagnostics.check_arbitrage(model, slices, k_min=-2.0, k_max=2.0, n_grid=801, tol=None, k_data=None)[source]

Check calibrated slices across maturities for arbitrage.

Runs check_slice_arbitrage() on every slice and additionally verifies the calendar condition w(k, T2) >= w(k, T1) for T2 > T1 on a shared grid, for every adjacent maturity pair. A pair with no finite margins (degenerate slices) fails the calendar check rather than passing silently.

Parameters:
Return type:

ArbitrageReport

Examples

>>> report = check_arbitrage(model, {0.25: p1, 0.5: p2})
>>> report.ok
True
>>> print(report)
pysvi.diagnostics.CLASS_EXTRAPOLATION = 'extrapolation_risk'

Classification labels for arbitrage findings.

class pysvi.diagnostics.ArbitrageClassification(maturity, finding, k_violation, classification, detail)[source]

Bases: object

Economic classification of one slice’s arbitrage findings.

The numerical diagnostics report mathematical evidence; this layer says what that evidence MEANS: a violation outside the quoted strike range is extrapolation risk (the model’s wings, not a trade); inside the range it is executable when a static butterfly built from the quoted crossed prices (buy at ask, sell at bid) has negative cost, quote_consistent when the violation disappears inside the bid/ask uncertainty, and mathematical when no quote band is available to decide.

Parameters:
  • maturity (float | None)

  • finding (str)

  • k_violation (float | None)

  • classification (str | None)

  • detail (str)

pysvi.diagnostics.classify_arbitrage(surface, panel=None)[source]

Classify a surface’s arbitrage findings economically.

Runs the diagnostics on the quoted range (as diagnose does) and classifies each slice’s butterfly/Lee finding:

  • extrapolation_risk – the violation sits outside the slice’s quoted strike range: a property of the model’s wings, not a constructible trade.

  • executable – inside the quoted range AND a static butterfly built from the panel’s bid/ask quotes around the violation has negative worst-case cost (buy wings at ask, sell body at bid).

  • quote_consistent – inside the quoted range but the worst-case butterfly cost is non-negative: the violation lives within bid/ask uncertainty.

  • mathematical – inside the quoted range, but no panel with iv_bid/iv_ask was given to decide executability.

Parameters:
  • surface (VolSurface) – A fitted surface with a fit report (for the quoted ranges).

  • panel (pd.DataFrame, optional) – Quote panel with iv_bid/iv_ask columns (as OptionChain produces) for the executability test.

Return type:

tuple of ArbitrageClassification, one per fitted slice.