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:
FlagConfigurable 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.essvi_total_variance(k, theta, rho_theta, phi_theta)[source]¶
eSSVI total variance w(k).
- 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.
- class pysvi.models.Parametrization(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]¶
Bases:
ABCBase 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 (seepysvi.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_stepontotal_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:
ParametrizationRaw 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]]
- class pysvi.models.NaturalSVI(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]¶
Bases:
ParametrizationNatural 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 ζ > 0Same 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:
ParametrizationSurface-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 levelGuarantees 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:
ParametrizationExtended 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
- class pysvi.models.JumpWings(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]¶
Bases:
ParametrizationSVI 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]
- 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:
ParametrizationDirect 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.
- class pysvi.models.SABR(arbitrage_condition=<ArbitrageFreedom.QUASI: 0>)[source]¶
Bases:
ParametrizationSABR 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
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:
- 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:
objectA 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:
objectA 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 bycalibrate_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
regularityanddw_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()andcalibrate_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_sliceschema: columnsstrike,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_refdefaulting to the median across slices –Tfor jump-wings, andT/Ffor SABR (betadefaults to 0.5 — override viamodel_kwargs).Calibration controls (
objective,loss,f_scale,initialization) pass through to every slice viamodel_kwargs. Slices that fail to calibrate are skipped with a warning; fitting fails only if no slice succeeds. For cross-slice calendar enforcement usecalibrate_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
modelis 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:
- 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.
- iv(strike, maturity, return_status=False)[source]¶
Implied volatility at absolute strike(s), any maturity in range.
With
return_status=Truealso 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)
- 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:
- diagnose(**kwargs)[source]¶
Fit report and arbitrage diagnostics in one result block.
Combines
fit_report(calibration status, quote accounting, residuals, settings, provenance) withcheck_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:
- save(path)[source]¶
Write the surface to
pathas 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:
- 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)
- 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. Withenforce_calendarthe 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_sliceschema).model (str or Parametrization, default "ssvi") – Factory name or model instance. DirectSVI is rejected when
enforce_calendaris 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
modelis a factory name (NO_BUTTERFLY may be OR-ed in; NO_CALENDAR is added byenforce_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 (
betafor SABR, etc.). The bid_ask objective is not supported in the joint eSSVI path.mode (str)
prior (VolSurface)
anchor (float)
- Return type:
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:
objectA cleaned option chain ready for surface calibration.
Build with
from_dataframe(); access the calibration panel viapanel(the calibrate_slice schema:strike,iv,maturity,implied_forward, plusiv_bid/iv_ask), and fit a surface withfit().- 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
- 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 andspotis 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.
expiryis a year fraction – or, withcontext=, real expiry dates resolved through the context’s day count;cpaccepts ‘c’/’call’/’p’/’put’ (case-insensitive).expiry (str) – Column names.
expiryis a year fraction – or, withcontext=, real expiry dates resolved through the context’s day count;cpaccepts ‘c’/’call’/’p’/’put’ (case-insensitive).cp (str) – Column names.
expiryis a year fraction – or, withcontext=, real expiry dates resolved through the context’s day count;cpaccepts ‘c’/’call’/’p’/’put’ (case-insensitive).bid (str) – Column names.
expiryis a year fraction – or, withcontext=, real expiry dates resolved through the context’s day count;cpaccepts ‘c’/’call’/’p’/’put’ (case-insensitive).ask (str) – Column names.
expiryis a year fraction – or, withcontext=, real expiry dates resolved through the context’s day count;cpaccepts ‘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 frominterest_rate_models(its implied zero curve from today is used), or any callable T -> r(T) – e.g. ascipy.interpolate.CubicSplineover 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 onrejections, andfit()carries them onto the surface’s fit report – nothing disappears without a count. Unrecognizedcpvalues raise in every mode (schema error, not bad data).
- Return type:
- 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()whenenforce_calendaris set, elseVolSurface.fit().rsets 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).modesets the failure handling for the fit (seefrom_dataframe()); the chain’s ingestion accounting is carried onto the returned surface’s fit report.- Parameters:
model (str | Parametrization)
enforce_calendar (bool)
arbitrage_condition (ArbitrageFreedom)
r (float | None)
mode (str)
- Return type:
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:
objectFit 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:
- 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:
objectCalibration 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.
- 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:
- class pysvi.report.SurfaceDiagnostics(fit, arbitrage)[source]¶
Bases:
objectFit report and arbitrage diagnostics in one result block.
Returned by
VolSurface.diagnose(). Fields are individually accessible;str()renders the combined formatted block.- Parameters:
fit (SurfaceFitReport | None)
arbitrage (ArbitrageReport)
- 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
- day_count¶
One of
ACT/365F(default),ACT/360,ACT/365.25,BUS/252(business days over 252; supplyholidays).- 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.
- discount(T)[source]¶
Discount factor D(T) = exp(-r(T) T).
Tmay also be a date, resolved throughyear_fraction()first.- 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)^+withsigma^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). Withn <= pthe 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:
- 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_thresholdof its magnitude, and a pair as near-degenerate when their correlation exceedscorr_thresholdin 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:
- 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.okis 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 atk_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 Tper quote column and1 / (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:
objectNumerical 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:
objectArbitrage 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_dataso verification covers the same domain calibration penalized.k_max (float, default -2.0, 2.0) – Evaluation grid bounds in log-moneyness. Prefer
k_dataso 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:
- 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:
model (Parametrization) – Model instance matching all params dicts.
slices (mapping or iterable of (maturity, params)) – Calibrated slices, as a dict {maturity: params} or an iterable of pairs. Sorted internally by maturity; duplicate maturities are rejected (two fits of one expiry are not a calendar pair).
k_min (float) – As in
check_slice_arbitrage().k_max (float) – As in
check_slice_arbitrage().n_grid (int) – As in
check_slice_arbitrage().tol (float | None) – As in
check_slice_arbitrage().k_data – As in
check_slice_arbitrage().
- Return type:
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:
objectEconomic 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
executablewhen a static butterfly built from the quoted crossed prices (buy at ask, sell at bid) has negative cost,quote_consistentwhen the violation disappears inside the bid/ask uncertainty, andmathematicalwhen 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
diagnosedoes) 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 nopanelwith 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.