Skip to content

Hedging with Jumps (Grzelak)

Background

Delta hedging with jumps using the Merton model.

Demonstrates delta hedging when underlying asset exhibits jump behavior, analyzing profit and loss from hedging errors in presence of discontinuous moves.

Reference: Oosterlee & Grzelak (2019). Mathematical Modeling and Computation in Finance. World Scientific.


Code

```python

-- coding: utf-8 --

""" Delta hedging with jumps using the Merton model.

Demonstrates delta hedging when underlying asset exhibits jump behavior, analyzing profit and loss from hedging errors in presence of discontinuous moves.

Reference: Oosterlee & Grzelak (2019). Mathematical Modeling and Computation in Finance. World Scientific. """

import numpy as np import matplotlib.pyplot as plt import scipy.stats as st import enum

=============================================================================

1. Enum Definition

=============================================================================

class OptionType(enum.Enum): """Enumeration for option type.""" CALL = 1.0 PUT = -1.0

=============================================================================

2. Path Generation

=============================================================================

def generate_paths_merton(num_paths, num_steps, s0, maturity, jump_intensity, jump_mean, jump_std, r, sigma): """ Generate Merton jump-diffusion paths.

Parameters
----------
num_paths : int
    Number of sample paths.
num_steps : int
    Number of time steps.
s0 : float
    Initial stock price.
maturity : float
    Time to maturity (T).
jump_intensity : float
    Poisson jump intensity (xi).
jump_mean : float
    Mean of log-jump size.
jump_std : float
    Standard deviation of log-jump size.
r : float
    Risk-free rate.
sigma : float
    Volatility.

Returns
-------
paths : dict
    Dictionary with keys 'time', 'X' (log-price), 'S' (price).
"""
# Create empty matrices for paths
x = np.zeros((num_paths, num_steps + 1))
s = np.zeros((num_paths, num_steps + 1))
time = np.zeros(num_steps + 1)

dt = maturity / float(num_steps)
x[:, 0] = np.log(s0)
s[:, 0] = s0

# Expectation E(e^J) for J ~ N(jump_mean, jump_std^2)
exp_jump = np.exp(jump_mean + 0.5 * jump_std * jump_std)
z_poisson = np.random.poisson(jump_intensity * dt, (num_paths, num_steps))
z = np.random.normal(0.0, 1.0, (num_paths, num_steps))
j = np.random.normal(jump_mean, jump_std, (num_paths, num_steps))

for i in range(num_steps):
    # Ensure samples from normal have mean 0 and variance 1
    if num_paths > 1:
        z[:, i] = (z[:, i] - np.mean(z[:, i])) / np.std(z[:, i])
    # Merton model: drift-adjusted for jump contribution
    x[:, i + 1] = (x[:, i] + (r - jump_intensity * (exp_jump - 1) - 0.5 * sigma * sigma) * dt
                   + sigma * np.sqrt(dt) * z[:, i] + j[:, i] * z_poisson[:, i])
    time[i + 1] = time[i] + dt

s = np.exp(x)
paths = {"time": time, "X": x, "S": s}
return paths

def generate_paths_gbm(num_paths, num_steps, maturity, r, sigma, s0): """ Generate GBM paths using Euler discretization.

Parameters
----------
num_paths : int
    Number of sample paths.
num_steps : int
    Number of time steps.
maturity : float
    Time to maturity (T).
r : float
    Risk-free rate.
sigma : float
    Volatility.
s0 : float
    Initial stock price.

Returns
-------
paths : dict
    Dictionary with keys 'time' and 'S'.
"""
z = np.random.normal(0.0, 1.0, (num_paths, num_steps))
x = np.zeros((num_paths, num_steps + 1))
w = np.zeros((num_paths, num_steps + 1))
time = np.zeros(num_steps + 1)

x[:, 0] = np.log(s0)

dt = maturity / float(num_steps)
for i in range(num_steps):
    # Ensure samples from normal have mean 0 and variance 1
    if num_paths > 1:
        z[:, i] = (z[:, i] - np.mean(z[:, i])) / np.std(z[:, i])
    w[:, i + 1] = w[:, i] + np.sqrt(dt) * z[:, i]
    x[:, i + 1] = x[:, i] + (r - 0.5 * sigma * sigma) * dt + sigma * (w[:, i + 1] - w[:, i])
    time[i + 1] = time[i] + dt

# Compute stock prices from log-prices
s = np.exp(x)
paths = {"time": time, "S": s}
return paths

=============================================================================

3. Black-Scholes Pricing Functions

=============================================================================

def bs_call_put_option_price(option_type, s0, strikes, sigma, t, maturity, r): """ Black-Scholes Call/Put option price.

Parameters
----------
option_type : OptionType
    CALL or PUT.
s0 : float
    Current stock price.
strikes : array_like
    Strike prices.
sigma : float
    Volatility.
t : float
    Current time.
maturity : float
    Maturity (T).
r : float
    Risk-free rate.

Returns
-------
value : ndarray
    Option price.
"""
strikes = np.array(strikes).reshape([len(strikes), 1])
d1 = (np.log(s0 / strikes) + (r + 0.5 * np.power(sigma, 2.0)) * (maturity - t)) / (sigma * np.sqrt(maturity - t))
d2 = d1 - sigma * np.sqrt(maturity - t)
if option_type == OptionType.CALL:
    value = st.norm.cdf(d1) * s0 - st.norm.cdf(d2) * strikes * np.exp(-r * (maturity - t))
elif option_type == OptionType.PUT:
    value = st.norm.cdf(-d2) * strikes * np.exp(-r * (maturity - t)) - st.norm.cdf(-d1) * s0
return value

def bs_delta(option_type, s0, strikes, sigma, t, maturity, r): """ Black-Scholes delta (first derivative w.r.t. spot).

Parameters
----------
option_type : OptionType
    CALL or PUT.
s0 : float
    Current stock price.
strikes : array_like
    Strike prices.
sigma : float
    Volatility.
t : float
    Current time.
maturity : float
    Maturity (T).
r : float
    Risk-free rate.

Returns
-------
delta : ndarray
    Option delta.
"""
# Handle numerical issues near maturity
if t - maturity > 10e-20 and maturity - t < 10e-7:
    t = maturity

strikes = np.array(strikes).reshape([len(strikes), 1])
d1 = (np.log(s0 / strikes) + (r + 0.5 * np.power(sigma, 2.0)) * (maturity - t)) / (sigma * np.sqrt(maturity - t))
if option_type == OptionType.CALL:
    value = st.norm.cdf(d1)
elif option_type == OptionType.PUT:
    value = st.norm.cdf(d1) - 1.0
return value

=============================================================================

4. Visualization

=============================================================================

def plot_single_path_results(time, stock_prices, call_prices, deltas, pnl): """ Plot results for a single sample path.

Parameters
----------
time : ndarray
    Time grid.
stock_prices : ndarray
    Stock prices along path.
call_prices : ndarray
    Option prices along path.
deltas : ndarray
    Delta values along path.
pnl : ndarray
    Profit and loss along path.
"""
plt.figure(1, figsize=(10, 6))
plt.plot(time, stock_prices, label='Stock')
plt.plot(time, call_prices, label='Call Price')
plt.plot(time, deltas, label='Delta')
plt.plot(time, pnl, label='P&L')
plt.legend()
plt.grid()
plt.xlabel('Time')
plt.ylabel('Value')
plt.title('Single Path Hedging Results (Merton Model with Jumps)')
plt.tight_layout()

def plot_pnl_histogram(pnl_final): """ Plot histogram of final P&L across all paths.

Parameters
----------
pnl_final : ndarray
    Final P&L values for all paths.
"""
plt.figure(2, figsize=(10, 6))
plt.hist(pnl_final, 100)
plt.grid()
plt.xlim([-0.1, 0.1])
plt.xlabel('Final P&L')
plt.ylabel('Frequency')
plt.title('Distribution of Final Hedging P&L (with Jumps)')
plt.tight_layout()

=============================================================================

5. Main

=============================================================================

def main(): """Run delta hedging simulation with Merton jump-diffusion model.""" # ===== Parameters ===== num_paths = 1000 num_steps = 5000 maturity = 1.0 r = 0.1 # Risk-free rate sigma = 0.2 # Volatility (diffusion) jump_intensity = 1.0 # Poisson jump intensity jump_mean = 0.0 # Mean of log-jump size jump_std = 0.25 # Std dev of log-jump size s0 = 1.0 # Initial stock price strikes = [0.95] option_type = OptionType.CALL

np.random.seed(7)
paths = generate_paths_merton(num_paths, num_steps, s0, maturity, jump_intensity,
                              jump_mean, jump_std, r, sigma)
time = paths["time"]
stock = paths["S"]

# ===== Hedging Setup =====
# Define callable functions for option pricing and delta
def option_price_func(t, strike, spot):
    return bs_call_put_option_price(option_type, spot, strike, sigma, t, maturity, r)

def delta_func(t, strike, spot):
    return bs_delta(option_type, spot, strike, sigma, t, maturity, r)

# Initialize portfolio
pnl = np.zeros((num_paths, num_steps + 1))
delta_init = delta_func(0.0, strikes, s0)
pnl[:, 0] = option_price_func(0.0, strikes, s0) - delta_init * s0

# Track option prices and deltas
call_matrix = np.zeros((num_paths, num_steps + 1))
call_matrix[:, 0] = option_price_func(0.0, strikes, s0)
delta_matrix = np.zeros((num_paths, num_steps + 1))
delta_matrix[:, 0] = delta_func(0.0, strikes, s0)

# ===== Dynamic Hedging Loop =====
for i in range(1, num_steps + 1):
    dt = time[i] - time[i - 1]
    delta_old = delta_func(time[i - 1], strikes, stock[:, i - 1])
    delta_curr = delta_func(time[i], strikes, stock[:, i])

    # Update P&L: accrue interest and rehedge
    pnl[:, i] = pnl[:, i - 1] * np.exp(r * dt) - (delta_curr - delta_old) * stock[:, i]
    call_matrix[:, i] = option_price_func(time[i], strikes, stock[:, i])
    delta_matrix[:, i] = delta_curr

# Final settlement: pay option payoff and liquidate hedge
pnl[:, -1] = pnl[:, -1] - np.maximum(stock[:, -1] - np.array(strikes), 0) + delta_matrix[:, -1] * stock[:, -1]

# ===== Results Analysis =====
path_id = 10
plot_single_path_results(time, stock[path_id, :], call_matrix[path_id, :],
                        delta_matrix[path_id, :], pnl[path_id, :])
plot_pnl_histogram(pnl[:, -1])
plt.show()

# Print sample results
print("Path Analysis (Merton Model with Jumps):")
print("path_id={0}, S0={1}, PnL(T-1)={2}, S(T)={3}, max(S(T)-K,0)={4}, PnL(T)={5}".format(
    path_id, s0, pnl[path_id, -2], stock[path_id, -1],
    np.maximum(stock[path_id, -1] - np.array(strikes), 0.0)[0], pnl[path_id, -1]))

if name == "main": main() ```

Exercises

Exercise 1. In the Merton jump-diffusion model, the stock can gap suddenly. Explain why delta hedging with the BS formula fails to fully replicate the option when jumps occur.

Solution to Exercise 1

BS delta hedging assumes continuous paths. When a jump occurs, the stock price changes discontinuously by \(J\) (a random amount). The hedge position \(\Delta \cdot S\) changes by \(\Delta \cdot J \cdot S\), but the option value changes by a nonlinear amount depending on the jump size. The mismatch \(V(Se^J) - V(S) - \Delta \cdot S(e^J - 1) \neq 0\) is the unhedgeable jump risk. This risk cannot be diversified away with continuous trading.


Exercise 2. Compare the P&L histogram from jump-diffusion hedging versus pure GBM hedging. Why are the tails heavier with jumps?

Solution to Exercise 2

With jumps, large stock movements occur instantaneously, creating large hedging errors that the delta cannot anticipate. The P&L distribution develops heavier tails (more extreme gains and losses) because each jump creates a discrete, unhedgeable gain/loss proportional to the jump size. The kurtosis of the P&L distribution increases significantly compared to the near-normal GBM case.


Exercise 3. If jump intensity is \(\lambda = 1\) (one jump per year on average) with mean \(\mu_J = 0\) and \(\sigma_J = 0.25\), estimate the average absolute hedging loss per jump.

Solution to Exercise 3

Per jump, the loss is approximately \(\frac{1}{2}\Gamma S^2(e^J - 1)^2\) where \(J \sim N(0, 0.0625)\). For small jumps: \((e^J - 1) \approx J\), so loss \(\approx \frac{1}{2}\Gamma S^2 J^2\). Expected loss per jump: \(\frac{1}{2}\Gamma S^2 \mathbb{E}[J^2] = \frac{1}{2}\Gamma S^2 \sigma_J^2 = \frac{1}{2}(0.02)(1)(0.0625) = 0.000625\). With 1 jump per year, annual expected loss from jumps \(\approx \$0.000625\) per unit notional.


Exercise 4. To reduce jump hedging error, one could use options with different strikes. Explain how a "crash-protection" strategy using OTM puts can help.

Solution to Exercise 4

OTM puts provide insurance against large downward jumps. By buying puts at a lower strike, the hedger creates a portfolio that gains value when large negative jumps occur, partially offsetting the loss on the short call position. This is effectively buying tail risk protection. The cost is the put premium, which must be weighed against the expected jump losses. In markets with frequent jumps, the cost of puts may be justified by the reduction in extreme P&L outcomes.