Skip to content

SVI Volatility Smile

quantflow.options.svi.SVI pydantic-model

Bases: BaseModel

Gatheral's Stochastic Volatility Inspired (SVI) parametrisation of the implied volatility smile, introduced in Gatheral (2004) and further analysed in Gatheral and Jacquier (2014).

The raw SVI parametrisation expresses the total implied variance \(w(k) = \sigma^2(k) \cdot \tau\) as a function of log-strike \(k = \log(K/F)\):

\[\begin{equation} w(k) = a + b \left[\rho (k - m) + \sqrt{(k - m)^2 + \theta^2}\right] \end{equation}\]

Fields:

  • a (Decimal)
  • b (Decimal)
  • rho (Decimal)
  • m (Decimal)
  • theta (Decimal)

a pydantic-field

a

Vertical shift of the smile: overall level of total implied variance. Must satisfy \(a + b \theta \sqrt{1 - \rho^2} \geq 0\) to avoid negative variance.

b pydantic-field

b

Angle between the left and right asymptotes of the smile. Controls the overall steepness of the wings. Must be non-negative.

rho pydantic-field

rho

Correlation parameter controlling the skew of the smile. Negative values produce a left-skewed smile (typical for equities), positive values produce a right skew. Must satisfy \(|\rho| < 1\).

m pydantic-field

m

Location parameter: the log-moneyness at the vertex of the smile. Shifts the smile horizontally. A value of zero centres the smile at the forward.

theta pydantic-field

theta

Smoothness parameter controlling the curvature of the smile around the vertex. Larger values produce a flatter region near \(m\). Must be strictly positive.

total_variance

total_variance(k)

Total implied variance \(w(k)\).

Returns an array of the same shape as \(k\).

PARAMETER DESCRIPTION
k

Log-moneyness log(K/F), scalar or array

TYPE: ArrayLike

Source code in quantflow/options/svi.py
def total_variance(
    self,
    k: Annotated[ArrayLike, Doc("Log-moneyness log(K/F), scalar or array")],
) -> np.ndarray:
    r"""Total implied variance $w(k)$.

    Returns an array of the same shape as $k$.
    """
    k_arr = np.asarray(k, dtype=float)
    a = float(self.a)
    b = float(self.b)
    rho = float(self.rho)
    m = float(self.m)
    theta = float(self.theta)
    km = k_arr - m
    return a + b * (rho * km + np.sqrt(km**2 + theta**2))

iv

iv(k, ttm)

Implied volatility \(\sigma(k) = \sqrt{w(k) / \tau}\).

Returns an array of the same shape as \(k\). Values are set to zero where total variance is non-positive.

PARAMETER DESCRIPTION
k

Log-moneyness log(K/F), scalar or array

TYPE: ArrayLike

ttm

Time to maturity in years

TYPE: float

Source code in quantflow/options/svi.py
def iv(
    self,
    k: Annotated[ArrayLike, Doc("Log-moneyness log(K/F), scalar or array")],
    ttm: Annotated[float, Doc("Time to maturity in years")],
) -> np.ndarray:
    r"""Implied volatility $\sigma(k) = \sqrt{w(k) / \tau}$.

    Returns an array of the same shape as $k$. Values are set to zero where
    total variance is non-positive.
    """
    w = self.total_variance(k)
    return np.where(w > 0, np.sqrt(np.maximum(w, 0.0) / ttm), 0.0)

fit classmethod

fit(k, iv, ttm)

Fit SVI smile to observed implied volatilities via non-linear least squares.

Minimises the sum of squared differences between observed and model total variances.

PARAMETER DESCRIPTION
k

Log-moneyness log(K/F) for each option

TYPE: ArrayLike

iv

Observed implied volatilities

TYPE: ArrayLike

ttm

Time to maturity in years

TYPE: float

Source code in quantflow/options/svi.py
@classmethod
def fit(
    cls,
    k: Annotated[ArrayLike, Doc("Log-moneyness log(K/F) for each option")],
    iv: Annotated[ArrayLike, Doc("Observed implied volatilities")],
    ttm: Annotated[float, Doc("Time to maturity in years")],
) -> SVI:
    """Fit SVI smile to observed implied volatilities via non-linear least squares.

    Minimises the sum of squared differences between observed and model
    total variances.
    """
    k_arr = np.asarray(k, dtype=float)
    iv_arr = np.asarray(iv, dtype=float)
    w_obs = iv_arr**2 * ttm

    atm_var = float(np.interp(0.0, k_arr, w_obs)) if k_arr.size else w_obs.mean()
    x0 = [atm_var * 0.9, 0.1, 0.0, 0.0, 0.1]

    def residuals(x: list[float]) -> np.ndarray:
        a, b, rho, m, theta = x
        km = k_arr - m
        w_fit = a + b * (rho * km + np.sqrt(km**2 + theta**2))
        return w_fit - w_obs

    result = least_squares(
        residuals,
        x0,
        bounds=(
            [-np.inf, 0.0, -1.0 + 1e-6, -np.inf, 1e-6],
            [np.inf, np.inf, 1.0 - 1e-6, np.inf, np.inf],
        ),
    )
    a, b, rho, m, theta = result.x
    return cls(
        a=to_decimal(round(a, 10)),
        b=to_decimal(round(b, 10)),
        rho=to_decimal(round(rho, 10)),
        m=to_decimal(round(m, 10)),
        theta=to_decimal(round(theta, 10)),
    )