Beyond Black-Scholes: Heston Model for Stochastic Volatility

No of Post Views:

76 hits

1. The Quest for a Better Model: Why Black-Scholes Falls Short

For decades, the Black-Scholes-Merton model has been the cornerstone of option pricing. Its elegance and simplicity are undeniable. It provides a beautiful, closed-form solution that revolutionized finance. However, its foundation rests on a set of assumptions that often crumble when faced with the chaotic reality of financial markets. The most significant of these is the assumption of constant volatility.

Anyone who has watched the markets for more than a day knows that volatility is anything but constant. It’s a living, breathing entity; spiking during times of panic, subsiding in periods of calm, and generally exhibiting its own unpredictable behavior. This dynamic nature gives rise to well-documented phenomena that the Black-Scholes model simply cannot explain, such as the volatility smile and skew. When we plot the implied volatilities of options against their strike prices, we don’t get the flat line that Black-Scholes predicts. Instead, we see a “smile” or a “smirk” indicating that out-of-the-money and in-the-money options command a higher implied volatility than at-the-money options.

This discrepancy isn’t just an academic curiosity; it has profound implications for pricing and hedging. If our model is built on a flawed premise, the prices and risk metrics it produces will be unreliable. This realization spurred a generation of quants to search for a successor, a model that could embrace the wild, random nature of volatility itself.

Enter the stochastic volatility models. The core idea is brilliantly intuitive: if volatility is not constant, let’s model it as a random process, just like we model the asset price. Among the most celebrated of these is the Heston model, developed by Steven Heston in 1993. It provides a mathematical framework that not only accounts for changing volatility but also captures the crucial correlation between an asset’s price and its volatility.

In this article, we’ll take a deep dive into the Heston model. We will dissect its mechanics, learn how to simulate it in Python, and use it to price options, finally generating the volatility smile that eludes the classic Black-Scholes world.

2. The Mechanics of the Heston Model

The Heston model describes the dynamics of both the asset price and its variance using a system of two correlated stochastic differential equations (SDEs). It might look intimidating at first, but each component has a clear, intuitive purpose.

Equation 1: The Asset Price Process ()

Mathematical equation representing the asset price process in the Heston model, displayed in black font.
  • \(S_t\) : The price of the asset at time t.
  • μ: The expected return of the asset (the drift).
  • \(v_t\) : The variance of the asset’s returns at time t. This is the crucial difference! Instead of a constant σ2, we have a process ​ that changes over time.
  • \(dw_t^1\) : A Wiener process, or Brownian motion, driving the asset price.

This equation looks very similar to the one in the Black-Scholes model, but with the constant variance σ2 replaced by the stochastic variance process .

Equation 2: The Variance Process ()

A graphical representation of the asset price process in the Heston model, featuring stochastic differential equations.

This is where the magic happens. This equation describes how the variance evolves. Let’s break down its parameters:

  • κ (kappa): The speed of mean reversion. This parameter controls how quickly the variance  tends to revert to its long-term average. A high κ means the variance snaps back to the average quickly, while a low κ means it wanders for longer.
  • θ (theta): The long-term mean variance. This is the level that the variance process is constantly being pulled towards. Over the long run, we expect the average variance to be θ.
  • ξ (xi, often also denoted as σv​): The volatility of volatility (or “vol-of-vol”). This parameter determines how volatile the variance process itself is. A high ξ leads to more dramatic swings in the asset’s variance.
  • \(dw_t^2\): Another Wiener process that drives the variance.

The Correlation (ρ)

The two Wiener processes, \(dw_t^1\)​​​ and \(dw_t^2\)​​​, are not independent. They are correlated by a parameter ρ (rho), such that \(dw_t^1\)\(dw_t^2\)= ρdt. This correlation is one of the most powerful features of the Heston model.

  • In equity markets, ρ is typically negative. This captures the leverage effect: as the stock price ​ falls, its volatility ​​ tends to rise, and vice-versa. This negative correlation is a key factor in producing the volatility skew we observe in real-world option markets.

Finally, for the variance process to remain positive and well-behaved, the model must satisfy the Feller condition: 2κθ>ξ2. This ensures that the variance never hits zero.

3. Bringing the Model to Life: Simulation in Python

To truly understand the Heston model, we need to see it in action. Since the SDEs don’t have a simple analytical solution for the asset path (unlike Geometric Brownian Motion), we turn to numerical simulation. We’ll use the Euler-Maruyama method to discretize the equations and simulate paths for both the asset price and its variance.

Let’s set up our environment and define the model parameters.

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

# Set plotting style
sns.set_theme(style="whitegrid")

# Model Parameters
S0 = 100.0      # Initial asset price
v0 = 0.04       # Initial variance (equals 20% volatility)
mu = 0.05       # Expected return
kappa = 2.0     # Speed of mean reversion
theta = 0.04    # Long-term mean variance
xi = 0.3        # Volatility of volatility
rho = -0.7      # Correlation between asset and variance processes

# Simulation Parameters
T = 1.0         # Time to maturity (1 year)
N = 252         # Number of time steps in a year
M = 1000        # Number of simulations (paths)
dt = T / N      # Size of each time step

def heston_simulation(S0, v0, mu, kappa, theta, xi, rho, T, N, M):
    """
    Simulates asset price and variance paths using the Heston model.

    Returns:
        S_paths (np.ndarray): Simulated asset price paths.
        v_paths (np.ndarray): Simulated variance paths.
    """
    # Initialize arrays to store the paths
    S = np.full((N + 1, M), S0)
    v = np.full((N + 1, M), v0)

    # Generate correlated random numbers
    # Z1 is for the asset price, Z2 is for the variance
    Z1 = np.random.normal(size=(N, M))
    Z2 = rho * Z1 + np.sqrt(1 - rho**2) * np.random.normal(size=(N, M))

    for i in range(1, N + 1):
        # Discretize the variance process
        # We use a 'Full Truncation' scheme to ensure variance stays positive
        v[i] = np.maximum(v[i-1] + kappa * (theta - v[i-1]) * dt +
                          xi * np.sqrt(np.maximum(v[i-1], 0)) * np.sqrt(dt) * Z2[i-1, :], 0)

        # Discretize the asset price process
        S[i] = S[i-1] * np.exp((mu - 0.5 * v[i-1]) * dt +
                               np.sqrt(np.maximum(v[i-1], 0)) * np.sqrt(dt) * Z1[i-1, :])

    return S, v

# Run the simulation
S_paths, v_paths = heston_simulation(S0, v0, mu, kappa, theta, xi, rho, T, N, M)

# --- Plotting the results ---
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))

# Plot asset paths
ax1.plot(S_paths[:, :10]) # Plot first 10 paths
ax1.set_title('Heston Model: Asset Price Paths', fontsize=16)
ax1.set_xlabel('Time Steps')
ax1.set_ylabel('Asset Price')
ax1.grid(True)

# Plot variance paths
ax2.plot(v_paths[:, :10]) # Plot first 10 paths
ax2.axhline(theta, color='r', linestyle='--', label=f'Long-Term Mean (theta={theta})')
ax2.set_title('Heston Model: Variance Paths', fontsize=16)
ax2.set_xlabel('Time Steps')
ax2.set_ylabel('Variance')
ax2.legend()
ax2.grid(True)

plt.suptitle('Heston Model Simulation', fontsize=20)
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()
Line graph showing multiple simulated asset price paths over time using the Heston model, with varying colors representing different simulations.

The plots generated by the code beautifully illustrate the model’s behavior. We can see the asset price evolving randomly, while the variance path on the right shows its own random walk, constantly being pulled back towards the long-term mean, θ.

4. Uncovering Deeper Truths: Return Distributions

A major failing of Black-Scholes is its assumption that log returns are normally distributed. Real-world returns exhibit skewness (asymmetry) and kurtosis (fat tails), meaning extreme events are more common than a normal distribution would suggest.

The Heston model, thanks to its stochastic volatility and the correlation parameter ρ, can generate these features organically. Let’s analyze the distribution of our simulated returns.

# --- This code continues from the previous block ---
# --- You should have S_paths available in your environment ---

# Calculate log returns from the simulated asset paths
log_returns = np.log(S_paths[-1] / S0)

# Calculate statistics
mean_return = np.mean(log_returns)
std_return = np.std(log_returns)
skewness = np.mean(((log_returns - mean_return) / std_return)**3)
kurtosis = np.mean(((log_returns - mean_return) / std_return)**4) # Excess kurtosis is this minus 3

print(f"--- Heston Model Return Statistics ---")
print(f"Skewness: {skewness:.4f}")
print(f"Kurtosis: {kurtosis:.4f}")

# Plot the distribution
plt.figure(figsize=(12, 7))
sns.histplot(log_returns, kde=True, bins=50, stat='density', label='Heston Returns')

# Overlay a normal distribution with the same mean and std dev for comparison
xmin, xmax = plt.xlim()
x = np.linspace(xmin, xmax, 100)
p = norm.pdf(x, mean_return, std_return)
plt.plot(x, p, 'k', linewidth=2, label='Normal Distribution')

plt.title('Distribution of Heston Log Returns vs. Normal Distribution', fontsize=16)
plt.xlabel('Log Return')
plt.ylabel('Density')
plt.legend()
plt.show()
Histogram comparing the distribution of Heston model log returns with a normal distribution overlay, showing density on the y-axis and log return values on the x-axis.

When you run this code, you’ll notice a few things. With our chosen parameter ρ=−0.7, the skewness will be negative. This is consistent with the leverage effect in real markets: large downward price moves are associated with spikes in volatility, creating a long left tail in the distribution of returns.

Furthermore, the kurtosis will likely be greater than 3 (the kurtosis of a normal distribution). This indicates “fat tails,” meaning our Heston model predicts that extreme positive and negative returns are more probable than under the Black-Scholes framework. This is a much more realistic representation of market behavior.

5. Pricing Options and Finding the Smile

Now for pricing a European option and observing the volatility smile. We’ll use a Monte Carlo approach. The logic is straightforward:

  1. Simulate thousands of asset price paths under the Heston model.
  2. For each path, calculate the option’s payoff at expiration. For a call option, this is max(ST​−K,0).
  3. Calculate the average of all these payoffs.
  4. Discount this average payoff back to the present value using the risk-free rate.

Let’s implement this. We’ll need to run the simulation under the risk-neutral measure, which simply means we replace the drift μ with the risk-free rate r.

# --- This code builds upon the previous blocks ---

# Option and Market Parameters
K = 100.0       # Strike price
r = 0.05        # Risk-free rate
# We will reuse the other Heston & simulation parameters (S0, v0, etc.)

# --- Monte Carlo Pricing Function ---
def heston_mc_pricer(S0, K, r, T, v0, kappa, theta, xi, rho, N, M):
    """
    Prices a European call option using the Heston model via Monte Carlo.
    Note: We run the simulation under the risk-neutral measure (mu=r).
    """
    # Simulate paths under the risk-neutral measure
    S_paths_risk_neutral, _ = heston_simulation(S0, v0, r, kappa, theta, xi, rho, T, N, M)
    
    # Calculate payoff for each path
    payoffs = np.maximum(S_paths_risk_neutral[-1] - K, 0)
    
    # Discount the average payoff
    option_price = np.mean(payoffs) * np.exp(-r * T)
    
    return option_price

# Price an at-the-money call option
M_pricing = 50000 # Use more simulations for better accuracy
price = heston_mc_pricer(S0, K, r, T, v0, kappa, theta, xi, rho, N, M_pricing)
print(f"\nHeston Monte Carlo Price for ATM Call Option: {price:.4f}")


# --- Generating the Volatility Smile ---

# Black-Scholes analytical pricer for implied volatility calculation
def black_scholes_call(S, K, T, r, sigma):
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    call_price = (S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2))
    return call_price

# Bisection method to find implied volatility
def implied_volatility(market_price, S, K, T, r):
    low = 0.001
    high = 2.0
    for i in range(100): # 100 iterations is plenty
        mid = (low + high) / 2
        price_at_mid = black_scholes_call(S, K, T, r, mid)
        if price_at_mid < market_price:
            low = mid
        else:
            high = mid
    return mid

strikes = np.arange(80, 141, 5)
implied_vols = []

print("\nCalculating implied volatilities for different strikes...")
for k_strike in strikes:
    # Get the Heston price for this strike (this is our "market price")
    heston_price = heston_mc_pricer(S0, k_strike, r, T, v0, kappa, theta, xi, rho, N, M_pricing)
    
    # Find the BS volatility that matches this price
    iv = implied_volatility(heston_price, S0, k_strike, T, r)
    implied_vols.append(iv)
    print(f"Strike: {k_strike}, Heston Price: {heston_price:.4f}, Implied Vol: {iv*100:.2f}%")

# Plot the volatility smile/skew
plt.figure(figsize=(10, 6))
plt.plot(strikes, np.array(implied_vols) * 100, 'o-')
plt.title('Volatility Smile/Skew from Heston Model', fontsize=16)
plt.xlabel('Strike Price (K)')
plt.ylabel('Implied Volatility (%)')
plt.grid(True)
plt.show()
A graph illustrating the volatility smile, showing implied volatility percentages on the Y-axis against strike prices on the X-axis, demonstrating a downward trend.

The final plot is the grand prize. Instead of a flat line, we see a downward-sloping curve, a clear volatility skew. For lower strike prices (out-of-the-money puts), the implied volatility is higher, while for higher strike prices, it’s lower. This shape is a direct result of the negative correlation (ρ) we built into our model, perfectly mimicking the patterns seen in real equity option markets.

6. Conclusion: A More Realistic World

The Heston model represents a monumental leap forward from the world of constant volatility. It provides a robust framework for capturing the essential dynamics of financial markets:

  • Volatility is not constant: It reverts to a long-term mean.
  • Volatility is itself volatile: It has its own source of randomness.
  • Asset prices and volatility are correlated: This crucial link allows the model to produce realistic return distributions and generate the volatility skews we see.

Of course, the Heston model is not the final word. The market has other tricks up its sleeve, like sudden, discontinuous jumps in prices caused by major news events. In the next article, we will explore this phenomenon and introduce another powerful tool for the modern quant: the Merton Jump-Diffusion model. But for now, you have a solid understanding of how to model, simulate, and price with one of the most important and influential models in quantitative finance.


Leave a Reply

Discover more from SimplifiedZone

Subscribe now to keep reading and get access to the full archive.

Continue reading

Discover more from SimplifiedZone

Subscribe now to keep reading and get access to the full archive.

Continue reading