Skip to content
All library documents

Lognormal Mixture Models for Option Pricing and Volatility Smiles

Article Quant Q&A · Author: solid

Summary

The document concerns reproducing a volatility-smile figure from a paper on lognormal-mixture dynamics. The question presents a shifted mixture model, with component weights and volatilities, and attempts to calculate an at-the-money implied volatility and an approximate smile across strikes. It compares parameter sets at different shift values and asks for help matching the published figure.

The accepted answer supplies a separate option-pricing function for the shifted mixture. It adjusts strikes and the initial asset level for the shift, computes component-level Black–Scholes terms, and combines their prices using mixture weights to obtain call or put values. This gives a pricing route that can support implied-volatility calculations, but the excerpt does not show a reproduced figure, numerical comparison, calibration procedure, or explanation of the original discrepancy. The code is therefore a model implementation aid rather than evidence that the paper’s results have been matched.

Key ideas

  • A lognormal mixture represents an asset-price distribution as a weighted combination of lognormal components.
  • A shift parameter changes the asset and strike inputs used in pricing while preserving the model’s intended drift structure.
  • The accepted answer prices calls or puts by calculating component values and aggregating them with mixture weights.
  • Matching a published volatility smile still requires calibration and comparison against the paper’s setup.

Tags

Full text
# Lognormal-mixture dynamics and calibration to market volatility smiles


# Lognormal-mixture dynamics and calibration to market volatility smiles












Can someone assist me in replicating the code and results from page 11, Figure 3 of the paper 'Lognormal-mixture dynamics and calibration to market volatility smiles' by Damiano Brigo, Fabio Mercurio, and Francesco Rapisarda, available at https://www.ma.imperial.ac.uk/~dbrigo/lognsmile.pdf? I have posted my code attempt, but it does not perfectly match the one in the paper. Any help or guidance on achieving this would be greatly appreciated.

```
import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt

class LognormalMixture(object):
    """
    *
    * Title: Lognormal-mixture dynamics and calibration 
    * to market volatility smiles.
    * From: Damiano Brigo, Fabio Mercurio, Francesco Rapisarda.
    *
    """
    class Shifted(object):
        """
        *
        * From chapter 4: Shifting the overall distribution.
        * Quote: "We now show how to construct an even more general model 
        * by shifting the process (11), while preserving the correct drift. 
        * Precisely, we assume that the new asset-price process A0 is obtained
        * through the following affine transformation of the process S ...".
        *
        """
        def _sigma_0 \
        (
            lambda_: np.ndarray, eta_: np.ndarray, t: float, alpha: float
        ) -> float:
            """
            *
            * \sigma(0) is the ATM-forward implied volatility.
            *
            * @param array $lambda_ - weights of each lognormal dist.
            * @param array $eta_    - volatilities standardized by time
            * @param float $t       - maturity
            * @param float $alpha   - 
            *
            """
            return 2/np.sqrt(t) * norm.ppf((1 - alpha) * np.sum(lambda_ * \
                norm.cdf(0.5 * eta_ * np.sqrt(t))) + 0.5 * alpha)

        def _sigma_m \
        (
            k: np.ndarray, s0: float, mu: float, lambda_: np.ndarray, 
            eta_: np.ndarray, t: float, alpha: float
        ) -> float:
            """
            *
            * Shifting the overall distribution:
            * Black-Scholes volatility derived from array of log-moneyness.
            *
            * @param array $m       - log moneyness
            * @param array $lambda_ - weights of each lognormal dist.
            * @param array $eta_    - volatilities standardized by time
            * @param float $t       - maturity
            * @param float $alpha   -
            *
            """
            # calculte log-moneyness 
            k_a = k - s0 * alpha * np.exp(mu*t)
            a0 = s0 * (1 - alpha)
            m = np.log(a0/k_a) + mu * t

            # atm-forward implied volatilities
            sigma_0: float = LognormalMixture.Shifted._sigma_0(
                lambda_, eta_, t, alpha)

            # black-scholes implied-volatilities
            sigma_m = sigma_0
            sigma_m += alpha * m * (np.sum(lambda_ * \
                norm.cdf(-0.5 * eta_ * np.sqrt(t))) - 0.5) * (np.sqrt(t) / \
                np.sqrt(np.pi*2) * np.exp(-0.125 * sigma_0**2 * t))**-1
            return sigma_m + 0.5 * ((t*(1-alpha))**-1 * np.sum(lambda_/eta_ * \
                np.exp(0.125*t*(sigma_0**2-eta_**2))) - (sigma_0*t)**-1 + \
                0.25 * alpha**2 * sigma_0 * t * ((np.sum(lambda_ * norm.cdf(
                -0.5 * eta_ * np.sqrt(t))) - 0.5)/ (np.sqrt(t)/np.sqrt(
                np.pi*2) * np.exp(-0.125 * sigma_0**2 * t)))**2) * m**2  
        

lnm = LognormalMixture.Shifted
k_ = np.arange(80,121,0.5)
lambda_ = np.array([0.6,0.4])
s0 = 100
t = 2
plt.figure(figsize=(11,7))

# sp1
plt.subplot(121)
plt.grid()
plt.plot(k_, lnm._sigma_m(k=k_, s0=s0, mu=0.05, lambda_=lambda_, 
    eta_=np.array([0.35,0.1]),  t=t, alpha=0.0), label="α=0.0", c="b")
plt.plot(k_, lnm._sigma_m(k=k_, s0=s0, mu=0.05, lambda_=lambda_, 
    eta_=np.array([0.35,0.1]),  t=t, alpha=-0.2), label="α=-0.2", c="g", ls="--")
plt.plot(k_, lnm._sigma_m(k=k_, s0=s0, mu=0.05, lambda_=lambda_, 
    eta_=np.array([0.35,0.1]),  t=t, alpha=-0.4), label="α=-0.4", c="r", ls=":")
plt.legend()

# sp2
plt.subplot(122)
plt.grid()
plt.plot(k_, lnm._sigma_m(k=k_, s0=s0, mu=0.05, lambda_=lambda_, 
    eta_=np.array([0.35,0.1]),  t=t, alpha=0.0), label="α=0.0", c="b")
plt.plot(k_, lnm._sigma_m(k=k_, s0=s0, mu=0.05, lambda_=lambda_, 
    eta_=np.array([0.1099,0.3553]),  t=t, alpha=-0.2), label="α=-0.2", c="g", ls="--")
plt.plot(k_, lnm._sigma_m(k=k_, s0=s0, mu=0.05, lambda_=lambda_, 
    eta_=np.array([0.09809,0.2979]),  t=t, alpha=-0.4), label="α=-0.4", c="r", ls=":")
plt.legend()
```

## Answer by solid (score 1, accepted)

https://quant.stackexchange.com/a/80083

```
class LogNormalMixture(object):
"""
Lognormal-mixture dynamics and calibration to market volatility smiles.

This class implements the model and methods described in the paper:
"Lognormal-mixture dynamics and calibration to market volatility smiles" 
by Damiano Brigo, Fabio Mercurio, Francesco Rapisarda.

From chapter 4: Shifting the overall distribution.
Quote: "We now show how to construct an even more general model
by shifting the process (11), while preserving the correct drift.
Precisely, we assume that the new asset-price process A0 is obtained
through the following affine transformation of the process S ...".
"""

@staticmethod
def sigma_0(
    lambdas: np.ndarray, etas: np.ndarray, tau: float, alpha: float
) -> float:
    """
    Calculate the ATM-forward implied volatility (\sigma(0)).

    Parameters:
    lambdas (np.ndarray): Weights of each lognormal distribution.
    etas (np.ndarray): Volatilities standardized by time.
    tau (float): Maturity in years.
    alpha (float): Shift parameter.

    Returns:
    float: The ATM-forward implied volatility.
    """
    return (
        2
        / np.sqrt(tau)
        * norm.ppf(
            (1 - alpha) * np.sum(lambdas * norm.cdf(0.5 * etas * np.sqrt(tau)))
            + 0.5 * alpha
        )
    )

@staticmethod
def npv(
    strikes: np.ndarray,
    s0: float,
    mu: float,
    lambdas: np.ndarray,
    etas: np.ndarray,
    tau: float,
    alpha: float,
    flag: int = 1
) -> float:
    """
    Calculate the net-present-value of options using the LogNormalMixture model.

    Parameters:
    strikes (np.ndarray): Strike prices.
    s0 (float): Initial asset price.
    mu (float): Drift rate.
    lambdas (np.ndarray): Weights of each lognormal distribution.
    etas (np.ndarray): Volatilities standardized by time.
    tau (float): Maturity in years.
    alpha (float): Shift parameter.
    flag (int): Flag type (1 for call, -1 for put).

    Returns:
    float: The net-present-value of the options.
    """
    k = (strikes.reshape(-1, 1) - s0 * alpha * np.exp(mu * tau))
    a0 = s0 * (1 - alpha)

    d1 = (np.log(a0 / k) + (mu + 0.5 * etas**2) * tau) / (np.sqrt(tau) * etas)
    d2 = (np.log(a0 / k) + (mu - 0.5 * etas**2) * tau) / (np.sqrt(tau) * etas)
    return (
        flag
        * np.exp(-mu * tau)
        * np.sum(
            lambdas
            * (
                a0 * np.exp(mu * tau) * norm.cdf(flag * d1)
                - k * norm.cdf(flag * d2)
            ),
            axis=1
        )
    )
```

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.