We will look at how to use previously learned methods like Lewis (2001) on Merton (1976) model in order to perform model calibration. As was the case in the previous module, most of the code in this blog is based on and adapted from Hilpisch’s Derivatives Analytics with Python.

To start with, let’s import the necessary libraries:

import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import quad

1. Lewis (2001) Approach

​ Let’s start by revisiting the Fourier methods for pricing in the Merton (1976) model. Essentially, under the Lewis (2001) approach, the value of the Call option is determined by: ​ \(\begin{equation*} C_0 = S_0 - \frac{\sqrt{S_0 K} e^{-rT}}{\pi} \int_{0}^{\infty} \mathbf{Re}[e^{izk} \varphi(z-i/2)] \frac{dz}{z^2+1/4} \end{equation*}\) which means that we just need the characteristic function of the process in Merton (1976). ​

1.1. Merton (1976) Characteristic Function

​ Merton’s (1976) characteristic function is given by: ​ \(\begin{equation*} \varphi^{M76}_0 (u, T) = e^{\left( \left( i u \omega - \frac{u^2 \sigma^2}{2}+ \lambda ( e^{i u \mu_j - u^2 \delta^2/2}-1) \right) T \right)} \end{equation*}\) ​ where, ​ \(\begin{equation*} \omega = r - \frac{\sigma^2}{2} - \lambda \left( e^{\mu_j + \delta^2/2}-1 \right) \end{equation*}\) ​ Thus, let’s start by coding this characteristic function:

def M76_char_func(u, T, r, sigma, lamb, mu, delta):
    """
    Characteristic function for Merton '76 model
    """

    omega = r - 0.5 * sigma**2 - lamb * (np.exp(mu + 0.5 * delta**2) - 1)
    char_func_value = np.exp(
        (
            1j * u * omega
            - 0.5 * u**2 * sigma**2
            + lamb * (np.exp(1j * u * mu - u**2 * delta**2 * 0.5) - 1)
        )
        * T
    )
    return char_func_value

1.2. Lewis (2001) Integral Value

​ The next step is to define the value of the integral in Lewis (2001) given the characteristic function:

def M76_integration_function(u, S0, K, T, r, sigma, lamb, mu, delta):
    """
    Integral function for Lewis (2001) under Merton'76 characteristic function
    """
    char_func = M76_char_func(u - 0.5 * 1j, T, r, sigma, lamb, mu, delta)

    value = 1 / (u**2 + 0.25) * (np.exp(1j * u * np.log(S0 / K)) * char_func).real

    return value

And the value of the Call option:

def M76_call_value(S0, K, T, r, sigma, lamb, mu, delta):
    """
    Value of the Call option under Lewis (2001) for Merton'76 jump diffusion model
    """

    int_value = quad(
        lambda u: M76_integration_function(u, S0, K, T, r, sigma, lamb, mu, delta),
        0,
        50,
        limit=250,
    )[0]

    call_value = max(0, S0 - np.exp(-r * T) * np.sqrt(S0 * K) / np.pi * int_value)

    return call_value

1.3. Pricing Example with Given Parameters

​ Once we have this set of functions, we can, given some parameters, price a call option under the Merton (1976) jump diffusion model. ​ For example, assume the following parameters: ​

  • $S_0 = 100$
  • $K = 100$
  • $T=1$
  • $r = 0.05$
  • $\sigma = 0.4$
  • $\lambda = 1.0$
  • $\mu = -0.2$
  • $\delta = 0.1$ ​ What would be the price of a Call option under Lewis (2001)?
S0 = 100
K = 100
T = 1
r = 0.05
sigma = 0.4
lamb = 1
mu = -0.2
delta = 0.1
print(
    "Value of the Call option under Merton (1976) is:  $",
    M76_call_value(S0, K, T, r, sigma, lamb, mu, delta),
)
Value of the Call option under Merton (1976) is:  $ 19.947853881446505

2. Merton (1976) Model Calibration

​ The next step is to calibrate the Merton (1976) model to observed option market prices in order to infer the model parameters. In order to do so, we are going to follow a very similar approach to that of the Heston model developed in the past module. In fact, we are going to use the same option market data file from the previous module on EuroStoxx 50 options.

2.1. Obtaining Options Market Data

import pandas as pd
from scipy.optimize import brute, fmin
# Market Data from www.eurexchange.com
# as of September 30, 2014

h5 = pd.HDFStore(
    "option_data_M2.h5", "r"
)  # Place this file in the same directory before running the code
data = h5["data"]  # European call & put option data (3 maturities)
h5.close()
S0 = 3225.93  # EURO STOXX 50 level September 30, 2014

As was the case in the previous post, we will select near ATM options:

# Option Selection

tol = 0.02  # Tolerance level to select ATM options (percent around ITM/OTM options)
options = data[(np.abs(data["Strike"] - S0) / S0) < tol]
options["Date"] = pd.DatetimeIndex(options["Date"])
options["Maturity"] = pd.DatetimeIndex(options["Maturity"])

Then, we add time left until maturity and a constant risk-free rate:

# Adding Time-to-Maturity and constant short-rates

for row, option in options.iterrows():
    T = (option["Maturity"] - option["Date"]).days / 365.0
    options.loc[row, "T"] = T
    options.loc[row, "r"] = 0.005  # ECB base rate
options.head()
Date	Strike	Call	Maturity	Put	T	r
38	2014-09-30	3175.0	126.8	2014-12-19	78.8	0.219178	0.005
39	2014-09-30	3200.0	110.9	2014-12-19	87.9	0.219178	0.005
40	2014-09-30	3225.0	96.1	2014-12-19	98.1	0.219178	0.005
41	2014-09-30	3250.0	82.3	2014-12-19	109.3	0.219178	0.005
42	2014-09-30	3275.0	69.6	2014-12-19	121.6	0.219178	0.005

2.2. Defining Error Functions

​ Once we have options market data that we have to “match” with our model, our next step is to define an error function for the specific case of the Merton model. ​ In this case, we will use the Root Mean Squared Error (RMSE) as a measure. This is basically defined as: ​ \begin{equation} RMSE = \sqrt{\frac{1}{N}\sum_{n=1}^N \left( C^_n - C^{M76}_n\right)^2} \end{equation} ​ where $C^$ is observed market prices and $C^{M76}$ are the values produced by Merton’s (1976) model.

i = 0
min_RMSE = 100
def M76_error_function(p0):
    """
    Error function for parameter calibration in Merton'76 model
    ---------------
    Parameters to calibrate:
    sigma: float
          volatility factor in diffusion term
    lambda: float
          jump intensity
    mu: float
          expected jump size
    delta: float
          standard deviation of jump
    ----------------
    RMSE: Root Mean Squared Error
    """
    global i, min_RMSE
    sigma, lamb, mu, delta = p0

    if sigma < 0.0 or delta < 0.0 or lamb < 0.0:
        return 500.0
    se = []

    for row, option in options.iterrows():
        model_value = M76_call_value(
            S0, option["Strike"], option["T"], option["r"], sigma, lamb, mu, delta
        )
        se.append((model_value - option["Call"]) ** 2)

    RMSE = np.sqrt(sum(se) / len(se))
    min_RMSE = min(min_RMSE, RMSE)
    if i % 50 == 0:
        print("%4d |" % i, np.array(p0), "| %7.3f | %7.3f" % (RMSE, min_RMSE))
    i += 1
    return RMSE

2.3. Calibration Function

​ Next, we need to create our calibration function. This follows the same logic as in the case of Heston from the previous module. For illustrative purposes, let’s set a somewhat high tolerance level so that the optimization does not convey time. By the end of this blog, we will dig deeper into the different issues that arise when accurately calibrating a model.

def M76_calibration_full():
    """
    Calibrates Merton (1976) stochastic volatility model to market quotes
    """
    # First run with brute force
    # (scan sensible regions, for faster convergence)
    p0 = brute(
        M76_error_function,
        (
            (0.075, 0.201, 0.025),  # sigma
            (0.10, 0.401, 0.1),  # lambda
            (-0.5, 0.01, 0.1),  # mu
            (0.10, 0.301, 0.1),
        ),  # delta
        finish=None,
    )

    # Second run with local, convex minimization
    # (we dig deeper where promising results)
    opt = fmin(
        M76_error_function, p0, xtol=0.0001, ftol=0.0001, maxiter=550, maxfun=1050
    )
    return opt
opt = M76_calibration_full()
   0 | [ 0.075  0.1   -0.5    0.1  ] |  31.540 |  31.540
  50 | [ 0.075  0.3   -0.1    0.3  ] |  22.852 |  11.298
 100 | [ 0.1  0.2 -0.2  0.2] |  19.922 |   8.654
 150 | [ 0.125  0.1   -0.3    0.1  ] |  10.704 |   5.571
 200 | [ 0.125  0.4   -0.5    0.3  ] |  55.500 |   4.662
 250 | [0.15 0.2  0.   0.2 ] |   6.619 |   3.586
 300 | [ 0.175  0.1   -0.1    0.1  ] |  14.171 |   3.586
 350 | [ 0.175  0.4   -0.3    0.3  ] |  54.376 |   3.586
 400 | [ 0.2  0.3 -0.4  0.2] |  63.380 |   3.586
 450 | [ 0.14702168  0.19533978 -0.10189428  0.10218084] |   3.495 |   3.428
 500 | [ 0.14987758  0.11503181 -0.14398098  0.09850597] |   3.401 |   3.401
 550 | [ 0.15597729  0.01124105 -0.20255149  0.07785796] |   3.359 |   3.359
 600 | [ 0.15617567  0.00947711 -0.20364524  0.07721602] |   3.358 |   3.358
Optimization terminated successfully.
         Current function value: 3.358419
         Iterations: 107
         Function evaluations: 183

2.4. Plotting Results of Calibration

​ Finally, let’s see how our model calibration performs graphically. For this, we will create a function that plots the different estimates together with the observed market prices. Given that our dataset has option market prices for 3 different maturities, we will also check the performance of the calibration for the different maturities:

def generate_plot(opt, options):
    # First, we calculate model prices
    sigma, lamb, mu, delta = opt
    options["Model"] = 0.0
    for row, option in options.iterrows():
        options.loc[row, "Model"] = M76_call_value(
            S0, option["Strike"], option["T"], option["r"], sigma, lamb, mu, delta
        )

    # Second, we plot
    mats = sorted(set(options["Maturity"]))
    options = options.set_index("Strike")
    for i, mat in enumerate(mats):
        options[options["Maturity"] == mat][["Call", "Model"]].plot(
            style=["b-", "ro"], title="%s" % str(mat)[:10]
        )
        plt.ylabel("Option Value")
generate_plot(opt, options)

merton_model_calibration

3. Conclusion

​ This is all regarding Merton (1976) model calibration. We have basically repeated the process of the last module with Heston (1993) and adapted things to the Merton jump diffusion model. ​ Now that you know how to properly calibrate somewhat simple models, we are going to make a step forward and look at a more complex one. Ideally, a model would incorporate stochastic volatility features present in Heston (1993), with the jump diffusion of Merton (1976). The question is, can we build a model that incorporates both of these desirable characteristics? ​ The answer is yes, and that’s what we will begin to explore in the next post. ​ References

  • Hilpisch, Yves. Derivatives Analytics with Python: Data Analysis, Models, Simulation, Calibration and Hedging. John Wiley & Sons, 2015.