TP: Optimizing Heston Model Monte Carlo Simulation
TP: Optimizing Heston Model Monte Carlo Simulation
In this practical exercise, we’ll optimize a Monte Carlo simulation for pricing options under the Heston model. We’ll start with a basic implementation and then apply various optimization techniques, including Numba and caching. This TP is not to run on website directly as numba is not available on it.
Part 1: Baseline Implementation
Here’s our starting point - a basic implementation of the Heston model Monte Carlo simulation:
import numpy as np
import time
from scipy.stats import norm
def generate_random_values(num_simulations, num_steps):
return np.random.normal(0, 1, (num_simulations, num_steps, 2))
def heston_monte_carlo(S0, K, T, r, kappa, theta, sigma, rho, v0, num_simulations, num_steps):
= generate_random_values(num_simulations, num_steps)
random_values = T / num_steps
dt = np.zeros((num_simulations, num_steps + 1))
prices = np.zeros((num_simulations, num_steps + 1))
variances
0] = S0
prices[:, 0] = v0
variances[:, for sim in range(num_simulations):
for i in range(1, num_steps + 1):
= random_values[sim, i-1, 0]
dW1 = rho * dW1 + np.sqrt(1 - rho**2) * random_values[sim, i-1, 1]
dW2
= np.maximum(variances[sim, i-1] + kappa * (theta - variances[sim, i-1]) * dt +
variances[sim, i] * np.sqrt(variances[sim, i-1] * dt) * dW2, 0)
sigma
= prices[sim, i-1] * np.exp((r - 0.5 * variances[sim, i-1]) * dt +
prices[sim, i] -1] * dt) * dW1)
np.sqrt(variances[sim, i
= np.mean(np.maximum(prices[:, -1] - K, 0))
option_prices return np.exp(-r * T) * option_prices
# Test the implementation
= 10
num_options = 2, 0.04, 0.3, -0.7, 0.04
kappa, theta, sigma, rho, v0 = np.random.randint(90, 110, num_options)
S0s = np.random.randint(90, 110, num_options)
Ks = 1
T = 0.05
r = 1000, 252
num_simulations, num_steps
= time.time()
start_time for option_row in range(num_options):
= S0s[option_row]
S0 = Ks[option_row]
K = heston_monte_carlo(S0, K, T, r, kappa, theta, sigma, rho, v0, num_simulations, num_steps)
price
print(f"Basic implementation took {(time.time() - start_time)/num_options:.4f} seconds per option")
Part 2: Optimization with Numba
Your first task is to optimize the Monte Carlo simulation using Numba. Here’s the structure you should follow:
from numba import jit, prange
@jit(nopython=True, cache=True, parallel=True)
def heston_monte_carlo_numba(S0, K, T, r, kappa, theta, sigma, rho, v0, num_simulations, num_steps):
# Your implementation here
pass
# Test the Numba-optimized function
= 1000
num_options # ... (use the same parameters as in the baseline implementation)
= time.time()
start_time for option_row in range(num_options):
= S0s[option_row]
S0 = Ks[option_row]
K = heston_monte_carlo_numba(S0, K, T, r, kappa, theta, sigma, rho, v0, num_simulations, num_steps)
price_numba
print(f"Numba implementation took {(time.time() - start_time)/num_options:.4f} seconds per option")
Part 3: Optimization with Caching
Next, implement a cached version of the Monte Carlo simulation. Here’s the structure to follow:
from functools import cache
@cache
def generate_random_values(num_simulations, num_steps):
return np.random.normal(0, 1, (num_simulations, num_steps, 2))
@cache
def heston_monte_carlo_cached(S0, K, T, r, kappa, theta, sigma, rho, v0, num_simulations, num_steps):
# Your implementation here
pass
# Test the cached implementation
= time.time()
start_time for option_row in range(num_options):
= S0s[option_row]
S0 = Ks[option_row]
K = heston_monte_carlo_cached(S0, K, T, r, kappa, theta, sigma, rho, v0, num_simulations, num_steps)
price_cached
print(f"First run of cached implementation took {time.time() - start_time:.2f} seconds")
# Run it again to see the effect of caching
= time.time()
start_time for option_row in range(num_options):
= S0s[option_row]
S0 = Ks[option_row]
K = heston_monte_carlo_cached(S0, K, T, r, kappa, theta, sigma, rho, v0, num_simulations, num_steps)
price_cached
print(f"Second run of cached implementation took {time.time() - start_time:.2f} seconds")
Part 4: Analysis and Comparison
Compare the performance of the three implementations (basic, Numba, and cached). What speedup do you achieve with each optimization technique?
Experiment with different numbers of simulations and time steps. How does the performance of each implementation scale?
Discuss the trade-offs between the different implementations in terms of speed, memory usage, and code complexity.
Part 5: Vectorization (Bonus)
As a final task, implement a vectorized version of the initial implementation without using Numba:
def heston_monte_carlo_vectorized(S0, K, T, r, kappa, theta, sigma, rho, v0, num_simulations, num_steps):
# Your vectorized implementation here
pass
# Test the vectorized implementation
= time.time()
start_time for option_row in range(num_options):
= S0s[option_row]
S0 = Ks[option_row]
K = heston_monte_carlo_vectorized(S0, K, T, r, kappa, theta, sigma, rho, v0, num_simulations, num_steps)
price_vectorized
print(f"Vectorized implementation took {(time.time() - start_time)/num_options:.4f} seconds per option")
Compare the performance of this vectorized implementation with the previous versions. Discuss the advantages and potential limitations of vectorization in this context.