Metadata-Version: 2.4
Name: svi-py
Version: 0.4.0
Summary: Stochastic volatility inspired (SVI) parametrizations of the implied volatility surface, plus the SABR model, in Python!
Author: Marwin Steiner
License: MIT
License-File: LICENSE
Requires-Python: >=3.13
Requires-Dist: loguru>=0.7.3
Requires-Dist: numpy>=2.4.2
Requires-Dist: pandas>=3.0.0
Requires-Dist: py-lets-be-rational>=1.0.1
Requires-Dist: py-vollib>=1.0.1
Requires-Dist: scipy>=1.17.0
Requires-Dist: twine>=6.2.0
Description-Content-Type: text/markdown

# svi-py

Stochastic volatility inspired (SVI) parametrizations of the implied volatility surface in Python — plus the SABR stochastic volatility model.

Given a panel of contemporaneous European call and put option prices across strikes and maturities, `svi-py` calibrates smooth, arbitrage-aware total variance surfaces using the SVI family of models and SABR. It handles the full pipeline: implied vol extraction, forward estimation via put-call parity, OTM leg selection, and per-slice calibration with configurable no-arbitrage constraints.

## Installation

```bash
pip install svi-py
```

Requires Python >= 3.13.

## Quick start

You need a DataFrame with columns for strike prices, implied volatilities (or raw option prices from which to compute them), time to maturity, and an implied forward price. A typical workflow:

```python
import numpy as np
import pandas as pd
from pysvi import SVI, get_model, calibrate_slice, apply_slice, ArbitrageFreedom

# Suppose df_slice is a single-maturity cross-section with columns:
#   strike, iv, maturity, implied_forward
model = get_model("svi")
params = calibrate_slice(df_slice, model)

# Generate fitted IVs and residuals
fitted = apply_slice(df_slice, params, model)
print(fitted[["strike", "iv", "fitted_iv", "residual_iv"]])
```

For SSVI/eSSVI, pass the ATM total variance as an extra argument:

```python
model = get_model("ssvi")
theta = float(np.nanmin(df_slice["iv"] ** 2 * df_slice["maturity"]))
params = calibrate_slice(df_slice, model, theta=theta)
```

For jump-wings, pass the time to expiry:

```python
model = get_model("jw")
T = float(df_slice["maturity"].iloc[0])
params = calibrate_slice(df_slice, model, T=T)
```

DirectSVI requires no extra arguments — it fits directly from log-moneyness and total variance:

```python
model = get_model("dsvi")
params = calibrate_slice(df_slice, model)
```

For SABR, pass the time to expiry and the forward, and optionally fix the CEV exponent β (default 0.5):

```python
model = get_model("sabr")
T = float(df_slice["maturity"].iloc[0])
F = float(df_slice["implied_forward"].iloc[0])
params = calibrate_slice(df_slice, model, T=T, F=F, beta=1.0)  # beta=1 for FX/equity
```

### Where do the inputs come from?

`svi-py` expects you to already have implied volatilities and forward prices. If you're starting from raw option prices, the library provides helpers:

- `compute_ivs_vectorized` computes Black-Scholes-Merton implied vols from option mid-prices via `py_vollib`.
- `calculate_implied_forward` estimates the forward price from put-call parity: $F = K + e^{rT}(C - P)$.
- `choose_leg` selects the OTM leg (calls for $K \geq F$, puts for $K < F$) for cleaner vol quotes.

You need a panel of **contemporaneous call and put option prices** across multiple strikes for at least one maturity. The richer the strike grid, the better the calibration.

## Parametrizations

All parametrizations work in total variance space: $w(k) = \sigma^2(k) \cdot T$, where $k = \log(K/F)$ is log-moneyness.

### Raw SVI

The original Gatheral (2004) parametrization with 5 free parameters:

$$w(k) = a + b\left[\rho(k - m) + \sqrt{(k - m)^2 + \sigma^2}\right]$$

| Parameter | Meaning | Constraint |
|-----------|---------|------------|
| $a$ | overall variance level | $a \geq 0$ |
| $b$ | slope / curvature scale | $b > 0$ |
| $\rho$ | skew (correlation) | $\|\rho\| < 1$ |
| $m$ | log-moneyness shift | unconstrained |
| $\sigma$ | vol-of-vol (smile width) | $\sigma > 0$ |

Maximum flexibility (5 degrees of freedom per slice). No automatic arbitrage guarantees beyond soft parameter bounds.

### SSVI (Surface SVI)

Gatheral & Jacquier (2014). Reduces to 2 free parameters per slice by fixing the ATM total variance $\theta = \sigma_{\text{ATM}}^2 T$:

$$w(k;\theta) = \frac{\theta}{2}\left[1 + \rho\,\varphi(\theta)\,k + \sqrt{\left(\varphi(\theta)\,k + \rho\right)^2 + 1 - \rho^2}\right]$$

where $\varphi(\theta) = \eta / \sqrt{\theta}$ is the curvature function.

| Parameter | Meaning | Constraint |
|-----------|---------|------------|
| $\theta$ | ATM total variance (fixed input) | $\theta > 0$ |
| $\rho$ | skew | $\|\rho\| < 1$ |
| $\eta$ | curvature scale | $\eta > 0$ |

Guarantees no butterfly arbitrage by construction for each fixed $\theta$.

### eSSVI (Extended SSVI)

Extends SSVI with maturity-dependent skew via a $\rho(\theta)$ term structure:

$$\rho(\theta) = \text{clip}\!\left(\rho_0 + \rho_1 \left(\frac{\theta}{\theta_{\text{ref}}}\right)^\alpha,\; -1,\; 1\right)$$

The total variance formula is the same as SSVI but with $\rho \to \rho(\theta)$. This adds 4 parameters globally ($\rho_0, \rho_1, \alpha, \eta$) and enables realistic calendar skew evolution across maturities. $\theta_{\text{ref}}$ is a reference ATM total variance (typically the median across slices) that normalises the power law.

### DirectSVI

A closed-form SVI calibration method (Schadner, forthcoming) that linearises the SVI equation by rewriting it as a conic section (hyperbola) in $(k, w)$ space:

$$z_0 k^2 + z_1 w^2 + z_2 kw + z_3 k + z_4 w + z_5 = 0$$

The 6 conic coefficients $z$ are found by solving a quadratically constrained eigenvalue problem (hyperbola constraint $z_2^2 - 4z_0 z_1 > 0$):

1. Build design matrices $D_2 = [k^2,\; w^2]$ and $D_1 = [kw,\; k,\; w,\; 1]$
2. Compute scatter matrices $S_{22}, S_{21}, S_{11}$
3. Solve $M\mathbf{a}_2 = \lambda\, C_1\mathbf{a}_2$ where $M = S_{22} - S_{21}S_{11}^{-1}S_{21}^\top$ and $C_1 = \begin{pmatrix}0 & -2\\-2 & 0\end{pmatrix}$
4. Select eigenvector for smallest positive eigenvalue; recover remaining coefficients via $\mathbf{a}_1 = -S_{11}^{-1}S_{21}^\top\mathbf{a}_2$

Evaluation solves the conic for $w$ given $k$ via the quadratic formula. No iterative optimisation is needed, making this the fastest calibration method in the library.

| Parameter | Meaning |
|-----------|---------|
| $z_0$ – $z_5$ | Conic section coefficients (normalised so $z_1 = 1$) |

> **Note:** DirectSVI does not support penalty-based arbitrage enforcement (`NO_BUTTERFLY` / `NO_CALENDAR`). Only `ArbitrageFreedom.QUASI` is meaningful.

**Reference:** Schadner, W. "Direct Fit for SVI Implied Volatilities", *Journal of Derivatives* (forthcoming). See also [`wol-fi/directSVI`](https://github.com/wol-fi/directSVI).

### Jump-Wings

The jump-wings parametrization (Gatheral 2004) re-expresses SVI in terms of financially interpretable quantities:

| Parameter | Meaning |
|-----------|---------|
| $v_t$ | ATM variance $\sigma_{\text{ATM}}^2$ |
| $\psi_t$ | ATM skew |
| $p_t$ | left (put) wing slope, $p_t \geq 0$ |
| $c_t$ | right (call) wing slope, $c_t \geq 0$ |
| $\tilde{v}_t$ | minimum implied variance, $\tilde{v}_t > 0$ |

These map to raw SVI $(a, b, \rho, m, \sigma)$ via a bijection:

$$b = \frac{p_t + c_t}{2}, \quad \rho = 1 - \frac{p_t}{b}, \quad \beta = \rho - \frac{2\psi_t\sqrt{T}}{b}$$

$$\alpha = \text{sgn}(\beta)\sqrt{\frac{1}{\beta^2} - 1}, \quad m = \frac{(v_t - \tilde{v}_t)\,T}{b\left[-\rho + \text{sgn}(\alpha)\sqrt{1 + \alpha^2} - \alpha\sqrt{1 - \rho^2}\right]}$$

$$\sigma = |\alpha \cdot m|, \quad a = \tilde{v}_t \cdot T - b\,\sigma\sqrt{1 - \rho^2}$$

Same 5 degrees of freedom as raw SVI but with parameters that have direct market interpretation (wing slopes, ATM level, minimum variance).

### SABR

The SABR stochastic volatility model (Hagan, Kumar, Lesniewski & Woodward 2002) — the market standard for interest-rate (swaptions, caps/floors) and FX volatility smiles. Unlike the SVI family, SABR is a *dynamic model*: it specifies stochastic differential equations for the forward and its volatility,

$$dF = \alpha F^\beta\, dW_1, \qquad d\alpha = \nu\, \alpha\, dW_2, \qquad d\langle W_1, W_2\rangle = \rho\, dt$$

Implied volatilities come from the Hagan et al. asymptotic expansion (the "HKLW formula"):

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

where $L = \log(F/K)$, $D(L) = 1 + \frac{(1-\beta)^2}{24}L^2 + \frac{(1-\beta)^4}{1920}L^4$, $z = \frac{\nu}{\alpha}(FK)^{(1-\beta)/2} L$, and $x(z) = \log\!\left[\frac{\sqrt{1-2\rho z+z^2}+z-\rho}{1-\rho}\right]$. At the money $z/x(z) \to 1$ (handled via Taylor expansion). Total variance is then $w(k) = \sigma_B^2(k)\, T$.

| Parameter | Meaning | Constraint |
|-----------|---------|------------|
| $\alpha$ | initial volatility level | $\alpha > 0$ |
| $\beta$ | CEV exponent (**fixed**, not fitted) | $0 \leq \beta \leq 1$ |
| $\rho$ | spot/vol correlation | $\|\rho\| < 1$ |
| $\nu$ | vol-of-vol | $\nu \geq 0$ |

Following market practice, $\beta$ is fixed by convention rather than calibrated: $\beta = 1$ (lognormal) for FX and equity, $\beta \approx 0.5$ for interest rates, $\beta = 0$ (normal) for spread-like underlyings. Calibration fits $(\alpha, \rho, \nu)$ per slice given $\beta$, $F$, $T$:

```python
model = get_model("sabr")
params = calibrate_slice(df_slice, model, T=T, F=F, beta=0.5)
```

> **Note:** the HKLW expansion is accurate near the money and for moderate maturities, but is known to lose accuracy (and can even imply negative densities) for deep wings or very long maturities. `NO_BUTTERFLY` performs a numerical density check via finite differences — it is a guard, not a structural guarantee.

**Reference:** Hagan, P., Kumar, D., Lesniewski, A., Woodward, D. (2002). "Managing Smile Risk", *Wilmott Magazine*.

## Arbitrage freeness

Every parametrization accepts an `arbitrage_condition` argument controlling how strictly no-arbitrage is enforced during calibration. The options are flags that can be combined with `|`:

```python
from pysvi import ArbitrageFreedom

# Default: soft parameter bounds only
model = get_model("svi")  # ArbitrageFreedom.QUASI

# Enforce no butterfly arbitrage (non-negative density)
model = get_model("svi", ArbitrageFreedom.NO_BUTTERFLY)

# Enforce no calendar spread arbitrage (non-decreasing total variance in T)
model = get_model("ssvi", ArbitrageFreedom.NO_CALENDAR)

# Enforce both
model = get_model("svi", ArbitrageFreedom.NO_BUTTERFLY | ArbitrageFreedom.NO_CALENDAR)
```

### `QUASI` (default)

Soft parameter-bound constraints only: $b > 0$, $|\rho| < 1$, $\sigma > 0$. Enforced via bounded optimisation and penalty terms. Fast, and usually sufficient for liquid underlyings.

### `NO_BUTTERFLY`

Enforces non-negative call price density $g(k) \geq 0$ across strikes, where:

$$g(k) = \left(1 - \frac{k\,w'(k)}{2\,w(k)}\right)^2 - \frac{w'(k)^2}{4}\left(\frac{1}{w(k)} + \frac{1}{4}\right) + \frac{w''(k)}{2}$$

Butterfly arbitrage exists whenever $g(k) < 0$ for some $k$. The calibrator evaluates $g$ on a fine grid and penalises violations. Note that SSVI and eSSVI already guarantee $g(k) \geq 0$ by their functional form; this flag adds an explicit numerical check.

### `NO_CALENDAR`

Enforces non-decreasing total variance in maturity: $w(k, T_2) \geq w(k, T_1)$ for $T_2 > T_1$ at every $k$. This is a cross-slice condition. Pass the prior (shorter-maturity) slice's total variance via the `w_prev` keyword argument to `calibrate`:

```python
# After calibrating the first slice:
w_prev = model.total_variance(k_grid, params_first_slice)

# Calibrate the next slice with calendar constraint:
params_next = model.calibrate(k, w_target, w_prev=w_prev)
```

## Calibration details

All models calibrate via L-BFGS-B (bounded quasi-Newton) with automatic Nelder-Mead fallback. The pipeline for a single maturity slice is:

1. **`prepare_slice`**: extracts $T$, $F$, computes $k = \log(K/F)$ and $w = \sigma_{\text{mkt}}^2 T$, filters invalid data, clips extreme moneyness.
2. **`model.calibrate`**: minimises MSE$(w_{\text{model}}, w_{\text{target}})$ plus penalty terms.
3. **`apply_slice`**: evaluates the fitted surface, computes $\sigma_{\text{fit}} = \sqrt{w/T}$ and residuals.

The factory function `get_model(name)` accepts `"svi"`, `"ssvi"`, `"essvi"`, `"jumpwings"` (or `"jw"`), `"directsvi"` (or `"dsvi"`), and `"sabr"`.

## Contributing

Contributions, bug reports, and feature requests are welcome. Open an issue or submit a PR on [GitHub](https://github.com/marwinsteiner/pysvi).

**Wanted: the original Gamma-Vanna-Volga paper.** The Gamma-Vanna-Volga parametrization is something of a holy grail in the quant vol surface literature and would be a great addition to this library. If you have a copy of the original paper, please send it to [marwin.steiner@gmail.com](mailto:marwin.steiner@gmail.com).

## License

MIT
