Skip to content
All library documents

Using Heston Monte Carlo Prices to Plot an Implied Volatility Smile

Article Quant Q&A · Author: GloBag578

Summary

The document presents a Python simulation of the Heston stochastic-volatility model and attempts to build an implied volatility smile. It simulates asset prices and variance using correlated random shocks, estimates European call prices across strikes by averaging discounted terminal payoffs, and inverts those prices to implied volatilities. The accepted response reports that the implied-volatility routine can encounter optimization problems and that rerunning it produced a more plausible curve in the responder’s tests.

The response also changes the parameter set and strike grid in its revised example, so it does not isolate a single cause of the original plot problem. Monte Carlo sampling noise and numerical inversion can both affect the result. The variance process uses a simple discretization with negative values clipped to zero, which can influence simulated prices, and the exchange provides no quantitative accuracy assessment or reproducibility guarantee. Treat the plotted smile as an illustrative simulation rather than a validated calibration.

Key ideas

  • Simulate terminal asset prices under Heston dynamics using correlated shocks to returns and variance.
  • Estimate call prices at each strike from discounted Monte Carlo payoffs.
  • Invert the simulated call prices to obtain implied volatilities for the smile plot.
  • Numerical optimization in the implied-volatility routine may yield unstable or odd results.
  • Changing parameters and the strike grid complicates diagnosis of the original plotting issue.
  • Clipping simulated variance at zero is a modeling choice that can affect the results.

Tags

Full text
# Heston Model python MC simulation


# Heston Model python MC simulation












I have this exercise. $\\\\$ Look for realistic values ​​of the parameters and calculate the price of a European Call with maturity $T = 0.5$ and $S_0 = 1$ for the strike values $​​K = 0.5,0.6, ......, 1.5$. Display the volatility smile.

I poste my code in python, but I don't understand why the volatility smiley is not displayed.

```
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from py_vollib_vectorized import vectorized_implied_volatility as implied_vol

# Parameters
# simulation dependent
S0 = 1           # asset price
T = 0.5              # time in years
r = 0.00003            # risk-free rate
N = 125             # number of time steps in simulation
M = 10000         # number of simulations

# Heston dependent parameters
kappa = 1.1              # rate of mean reversion of variance under risk-neutral dynamics
theta = 0.02        # long-term mean of variance under risk-neutral dynamics
v0 = 0.025          # initial variance under risk-neutral dynamics
rho = -0.7              # correlation between returns and variances under risk-neutral dynamics
sigma = 0.1          # volatility of volatility

def heston_model_sim(S0, v0, rho, kappa, theta, sigma,T, N, M):
    """
    Inputs:
     - S0, v0: initial parameters for asset and variance
     - rho   : correlation between asset returns and variance
     - kappa : rate of mean reversion in variance process
     - theta : long-term mean of variance process
     - sigma : vol of vol / volatility of variance process
     - T     : time of simulation
     - N     : number of time steps
     - M     : number of scenarios / simulations
    
    Outputs:
    - asset prices over time (numpy array)
    - variance over time (numpy array)
    """
    # initialise other parameters
    dt = T/N
    mu = np.array([0,0])
    cov = np.array([[1,rho],
                    [rho,1]])

    # arrays for storing prices and variances
    S = np.full(shape=(N+1,M), fill_value=S0)
    v = np.full(shape=(N+1,M), fill_value=v0)

    # sampling correlated brownian motions under risk-neutral measure
    Z = np.random.multivariate_normal(mu, cov, (N,M))

    for i in range(1,N+1):
        S[i] = S[i-1] * np.exp( (r - 0.5*v[i-1])*dt + np.sqrt(v[i-1] * dt) * Z[i-1,:,0] )
        v[i] = np.maximum(v[i-1] + kappa*(theta-v[i-1])*dt + sigma*np.sqrt(v[i-1]*dt)*Z[i-1,:,1],0)
    
    return S, v

rho_p = 0.98
rho_n = -0.98

S_p,v_p = heston_model_sim(S0, v0, rho_p, kappa, theta, sigma,T, N, M)
S_n,v_n = heston_model_sim(S0, v0, rho_n, kappa, theta, sigma,T, N, M)

fig, (ax1, ax2)  = plt.subplots(1, 2, figsize=(12,5))
time = np.linspace(0,T,N+1)
ax1.plot(time,S_p)
ax1.set_title('Heston Model Asset Prices')
ax1.set_xlabel('Time')
ax1.set_ylabel('Asset Prices')

ax2.plot(time,v_p)
ax2.set_title('Volatility Monte Carlo Simulation')
ax2.set_xlabel('Time')
ax2.set_ylabel('Variance')

plt.show()
# simulate gbm process at time T
gbm = S0*np.exp( (r - theta**2/2)*T + np.sqrt(theta)*np.sqrt(T)*np.random.normal(0,1,M) )

fig, ax = plt.subplots()

ax = sns.kdeplot(S_p[-1], label=r"$\rho= 0.98$", ax=ax)
ax = sns.kdeplot(S_n[-1], label=r"$\rho= -0.98$", ax=ax)
ax = sns.kdeplot(gbm, label="GBM", ax=ax)

plt.title(r'Asset Price Density under Heston Model')
plt.xlim([0.5, 1.5])
plt.xlabel('$S_T$')
plt.ylabel('Density')
plt.legend()
plt.show()

rho = -0.7
S,v = heston_model_sim(S0, v0, rho, kappa, theta, sigma,T, N, M)

# Set strikes and complete MC option price for different strikes
K = np.arange(0.5,1.5)

calls = np.array([np.exp(-r*T)*np.mean(np.maximum(S-k,0)) for k in K])

call_ivs = implied_vol(calls, S0, K, T, r, flag='c', q=0, return_as='numpy', on_error='ignore')

plt.plot(K, call_ivs, label=r'Call')

plt.ylabel('Implied Volatility')
plt.xlabel('Strike')

plt.title('Implied Volatility Smile from Heston Model')
plt.legend()
plt.show()
```

Thanks for the support.

## Answer by Pleb (score 0, accepted)

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

#### Spotted issues:





- While testing the code, I have spotted that `implied_vol` function might run into some optimization issues, that generates a weird smile. I got a decent smile (shown below) after a couple of re-runs of the function.

There might be some other small issues, but fixing the above problems gives me a working solution.

#### Verification:

I took the liberty of fixing the above issues and ran the code. However, I used the original Heston parameters which are found in the Youtube Tutorial that you clearly followed to produce the simulations, density and smile. This results in the following graphs:

And the following smile:

I will leave interpretation of the graphs up to you. I hope this helps.

#### Appendix: Python Code

```
# Parameters
# simulation dependent
S0 = 1.0          # asset price
T = 0.5              # time in years
r = 0.00003            # risk-free rate
#r=0.02
N = 252             # number of time steps in simulation
M = 10000         # number of simulations

# Heston dependent parameters
#kappa = 1.1              # rate of mean reversion of variance under risk-neutral dynamics
#theta = 0.02        # long-term mean of variance under risk-neutral dynamics
#v0 = 0.025          # initial variance under risk-neutral dynamics
#rho = -0.7              # correlation between returns and variances under risk-neutral dynamics
#sigma = 0.1          # volatility of volatility

kappa = 3
theta = 0.20**2
v0=0.25**2
#rho = 0.7
sigma = 0.6 

def heston_model_sim(S0, v0, rho, kappa, theta, sigma,T, N, M):
    """
    Inputs:
     - S0, v0: initial parameters for asset and variance
     - rho   : correlation between asset returns and variance
     - kappa : rate of mean reversion in variance process
     - theta : long-term mean of variance process
     - sigma : vol of vol / volatility of variance process
     - T     : time of simulation
     - N     : number of time steps
     - M     : number of scenarios / simulations
    
    Outputs:
    - asset prices over time (numpy array)
    - variance over time (numpy array)
    """
    # initialise other parameters
    dt = T/N
    mu = np.array([0,0])
    cov = np.array([[1,rho],
                    [rho,1]])

    # arrays for storing prices and variances
    S = np.full(shape=(N+1,M), fill_value=S0)
    v = np.full(shape=(N+1,M), fill_value=v0)

    # sampling correlated brownian motions under risk-neutral measure
    Z = np.random.multivariate_normal(mu, cov, (N,M))

    for i in range(1,N+1):
        S[i] = S[i-1] *  np.exp((r - 0.5*v[i-1])*dt + np.sqrt(v[i-1] * dt) * Z[i-1,:,0])
        v[i] = np.maximum(v[i-1] + kappa*(theta-v[i-1])*dt + sigma*np.sqrt(v[i-1]*dt)*Z[i-1,:,1],0)
    
    return S, v

rho_p = 0.98
rho_n = -0.98

S_p,v_p = heston_model_sim(S0, v0, rho_p, kappa, theta, sigma,T, N, M)
S_n,v_n = heston_model_sim(S0, v0, rho_n, kappa, theta, sigma,T, N, M)

fig, (ax1, ax2)  = plt.subplots(1, 2, figsize=(12,5))
time = np.linspace(0,T,N+1)
ax1.plot(time,S_p)
ax1.set_title('Heston Model Asset Prices')
ax1.set_xlabel('Time')
ax1.set_ylabel('Asset Prices')

ax2.plot(time,v_p)
ax2.set_title('Volatility Monte Carlo Simulation')
ax2.set_xlabel('Time')
ax2.set_ylabel('Variance')

plt.show()
# simulate gbm process at time T
gbm = S0*np.exp( (r - theta**2/2)*T + np.sqrt(theta)*np.sqrt(T)*np.random.normal(0,1,M) )

fig, ax = plt.subplots()

ax = sns.kdeplot(S_p[-1], label=r"$\rho= 0.98$", ax=ax)
ax = sns.kdeplot(S_n[-1], label=r"$\rho= -0.98$", ax=ax)
ax = sns.kdeplot(gbm, label="GBM", ax=ax)

plt.title(r'Asset Price Density under Heston Model')
plt.xlim([0.5, 1.5])
plt.xlabel('$S_T$')
plt.ylabel('Density')
plt.legend()
plt.show()

rho = -0.7
S,v = heston_model_sim(S0, v0, rho, kappa, theta, sigma,T, N, M)

# Set strikes and complete MC option price for different strikes

K = np.arange(0.1,1.6,0.1)

calls = np.array([np.exp(-r*T)*np.mean(np.maximum(S-k,0)) for k in K])

call_ivs = implied_vol(calls, S0, K, T, r, flag='c', q=0, return_as='numpy', on_error='ignore')

plt.plot(K, call_ivs, label=r'Call')

plt.ylabel('Implied Volatility')
plt.xlabel('Strike')

plt.title('Implied Volatility Smile from Heston Model')
plt.legend()
plt.show()
```

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.