Skip to content
All library documents

How Implied Volatilities Supply Prices for Heston Calibration

Article Quant Q&A · Author: Daniel Berkowitz

Summary

The document explains why a Heston calibration example can report market option prices even though its input data lists implied volatilities rather than prices. QuantLib’s calibration helper converts each supplied implied volatility into a Black–Scholes price using the option’s strike, expiry, spot, and the supplied interest and dividend curves. It then compares those Black prices with prices from the Heston model and adjusts the Heston parameters to reduce the differences.

The example uses one selected expiry and its strike-by-strike volatility observations, then prints helper market values, model values, and relative errors. This makes clear that the calibration target is inferred from the volatility quotes through a pricing convention; it is not an independently supplied set of observed prices. Results therefore depend on the input volatility surface, market conventions, and the Black pricing assumptions, and a single expiry’s small set of strikes does not establish how well the fitted model represents the full surface.

Key ideas

  • QuantLib’s Heston helper turns each implied volatility quote into a Black–Scholes market price.
  • The calibration minimizes differences between helper market values and Heston model values.
  • Spot, strike, expiry, rates, and dividends contribute to the implied-volatility-to-price conversion.
  • A fit to one expiry and a limited strike set may not represent the full volatility surface.

Tags

Full text
# Option Prices And Calibrating The Heston Model Code Question


# Option Prices And Calibrating The Heston Model Code Question












I'm trying to understand this Python code that uses Quantlib to calibrate the parameters of the Heston model. The data that is provided in the code is the spot price, the risk free interest rate, the dividends of the underlining, the day of calibration, expiration dates of options, their strike prices, and their implied volatilities.

What isn't given is the the market prices, yet the code generates a comparison between the market prices and the price computed by the Heston model. How are those prices generated? How can the parameters be fit if the market prices of the options aren't explicitly given? Isn't the point to choose the parameters so that difference between predicted market prices via the Heston model and the actual market prices are minimized?

This is the original source of the code.

Any help clarifying this will be much appreciated.

https://github.com/AIMLModeling/Heston-Model-Calibration/blob/main/HestonVolVisuals.py

```
import QuantLib as ql
import math
import numpy as np from mpl_toolkits.mplot3d 
import Axes3D
import matplotlib.pyplot as plt
from matplotlib import cm
day_count = ql.Actual365Fixed()
calendar = ql.UnitedStates()

calculation_date = ql.Date(9, 11, 2021)
spot = 659.37
ql.Settings.instance().evaluationDate = calculation_date

dividend_yield = ql.QuoteHandle(ql.SimpleQuote(0.0))
risk_free_rate = 0.01
dividend_rate = 0.00
flat_ts = ql.YieldTermStructureHandle(
ql.FlatForward(calculation_date, risk_free_rate, day_count))
dividend_ts = ql.YieldTermStructureHandle(
ql.FlatForward(calculation_date, dividend_rate, day_count))

expiration_dates = [ql.Date(9,12,2021), ql.Date(9,1,2022), ql.Date(9,2,2022),
                ql.Date(9,3,2022), ql.Date(9,4,2022), ql.Date(9,5,2022), 
                ql.Date(9,6,2022), ql.Date(9,7,2022), ql.Date(9,8,2022),
                ql.Date(9,9,2022), ql.Date(9,10,2022), ql.Date(9,11,2022), 
                ql.Date(9,12,2022), ql.Date(9,1,2023), ql.Date(9,2,2023),
                ql.Date(9,3,2023), ql.Date(9,4,2023), ql.Date(9,5,2023), 
                ql.Date(9,6,2023), ql.Date(9,7,2023), ql.Date(9,8,2023),
                ql.Date(9,9,2023), ql.Date(9,10,2023), ql.Date(9,11,2023)]
strikes = [527.50, 560.46, 593.43, 626.40, 659.37, 692.34, 725.31, 758.28]
data = [
[0.37819, 0.34177, 0.30394, 0.27832, 0.26453, 0.25916, 0.25941, 0.26127],
[0.3445, 0.31769, 0.2933, 0.27614, 0.26575, 0.25729, 0.25228, 0.25202],
[0.37419, 0.35372, 0.33729, 0.32492, 0.31601, 0.30883, 0.30036, 0.29568],
[0.37498, 0.35847, 0.34475, 0.33399, 0.32715, 0.31943, 0.31098, 0.30506],
[0.35941, 0.34516, 0.33296, 0.32275, 0.31867, 0.30969, 0.30239, 0.29631],
[0.35521, 0.34242, 0.33154, 0.3219, 0.31948, 0.31096, 0.30424, 0.2984],
[0.35442, 0.34267, 0.33288, 0.32374, 0.32245, 0.31474, 0.30838, 0.30283],
[0.35384, 0.34286, 0.33386, 0.32507, 0.3246, 0.31745, 0.31135, 0.306],
[0.35338, 0.343, 0.33464, 0.32614, 0.3263, 0.31961, 0.31371, 0.30852],
[0.35301, 0.34312, 0.33526, 0.32698, 0.32766, 0.32132, 0.31558, 0.31052],
[0.35272, 0.34322, 0.33574, 0.32765, 0.32873, 0.32267, 0.31705, 0.31209],
[0.35246, 0.3433, 0.33617, 0.32822, 0.32965, 0.32383, 0.31831, 0.31344],
[0.35226, 0.34336, 0.33651, 0.32869, 0.3304, 0.32477, 0.31934, 0.31453],
[0.35207, 0.34342, 0.33681, 0.32911, 0.33106, 0.32561, 0.32025, 0.3155],
[0.35171, 0.34327, 0.33679, 0.32931, 0.3319, 0.32665, 0.32139, 0.31675],
[0.35128, 0.343, 0.33658, 0.32937, 0.33276, 0.32769, 0.32255, 0.31802],
[0.35086, 0.34274, 0.33637, 0.32943, 0.3336, 0.32872, 0.32368, 0.31927],
[0.35049, 0.34252, 0.33618, 0.32948, 0.33432, 0.32959, 0.32465, 0.32034],
[0.35016, 0.34231, 0.33602, 0.32953, 0.33498, 0.3304, 0.32554, 0.32132],
[0.34986, 0.34213, 0.33587, 0.32957, 0.33556, 0.3311, 0.32631, 0.32217],
[0.34959, 0.34196, 0.33573, 0.32961, 0.3361, 0.33176, 0.32704, 0.32296],
[0.34934, 0.34181, 0.33561, 0.32964, 0.33658, 0.33235, 0.32769, 0.32368],
[0.34912, 0.34167, 0.3355, 0.32967, 0.33701, 0.33288, 0.32827, 0.32432],
[0.34891, 0.34154, 0.33539, 0.3297, 0.33742, 0.33337, 0.32881, 0.32492]]
implied_vols = ql.Matrix(len(strikes), len(expiration_dates))
for i in range(implied_vols.rows()):
    for j in range(implied_vols.columns()):
    implied_vols[i][j] = data[j][i]
black_var_surface = ql.BlackVarianceSurface(
calculation_date, calendar, 
expiration_dates, strikes, 
implied_vols, day_count)

strikes_grid = np.arange(strikes[0], strikes[-1],10)
expiry = 1.0 # years
implied_vols = [black_var_surface.blackVol(expiry, s) 
            for s in strikes_grid] # can interpolate here
actual_data = data[11] # cherry picked the data for given expiry (1 year)

fig, ax = plt.subplots()
ax.plot(strikes_grid, implied_vols, label="Black Surface")
ax.plot(strikes, actual_data, "o", label="Actual")
ax.set_xlabel("Strikes", size=12)
ax.set_ylabel("Vols", size=12)
legend = ax.legend(loc="upper right")
plot_years = np.arange(0, 2, 0.1)
plot_strikes = np.arange(535.0, 750.0, 1.0)
fig = plt.figure()
ax = fig.subplots(subplot_kw={'projection': '3d'})
X, Y = np.meshgrid(plot_strikes, plot_years)
X, Y = np.meshgrid(plot_strikes, plot_years)

Z = np.array([black_var_surface.blackVol(y, x) 
          for xr, yr in zip(X, Y) 
              for x, y in zip(xr,yr) ]
         ).reshape(len(X), len(X[0]))

surf = ax.plot_surface(X,Y,Z, rstride=1, cstride=1, cmap=cm.coolwarm, 
            linewidth=0.1)
fig.colorbar(surf, shrink=0.5, aspect=5)

plt.show()
```

## dummy parameters

```
v0 = 0.01; kappa = 0.2; theta = 0.02; rho = -0.75; sigma = 0.5;

process = ql.HestonProcess(flat_ts, dividend_ts, 
                       ql.QuoteHandle(ql.SimpleQuote(spot)), 
                       v0, kappa, theta, sigma, rho)
  model = ql.HestonModel(process)
 engine = ql.AnalyticHestonEngine(model) 
 heston_helpers = []
 black_var_surface.setInterpolation("bicubic")
 one_year_idx = 11 # 12th row in data is for 1 year expiry
 date = expiration_dates[one_year_idx]
 for j, s in enumerate(strikes):
t = (date - calculation_date )
p = ql.Period(t, ql.Days)
sigma = data[one_year_idx][j]
#sigma = black_var_surface.blackVol(t/365.25, s)
helper = ql.HestonModelHelper(p, calendar, spot, s, 
                              ql.QuoteHandle(ql.SimpleQuote(sigma)),
                              flat_ts, 
                              dividend_ts)
helper.setPricingEngine(engine)
heston_helpers.append(helper)
 lm = ql.LevenbergMarquardt(1e-8, 1e-8, 1e-8)
  model.calibrate(heston_helpers, lm, 
             ql.EndCriteria(500, 50, 1.0e-8,1.0e-8, 1.0e-8))
 theta, kappa, sigma, rho, v0 = model.params()

  print ("\ntheta = %f, kappa = %f, sigma = %f, rho = %f, v0 = %f" % (theta, kappa, sigma, rho, v0))
  avg = 0.0

 print ("%15s %15s %15s %20s" % (
"Strikes", "Market Value", 
 "Model Value", "Relative Error (%)"))
 print ("="*70)
for i, opt in enumerate(heston_helpers):
err = (opt.modelValue()/opt.marketValue() - 1.0)
print ("%15.2f %14.5f %15.5f %20.7f " % (
    strikes[i], opt.marketValue(), 
    opt.modelValue(), 
    100.0*(opt.modelValue()/opt.marketValue() - 1.0)))
 avg += abs(err)
 avg = avg*100.0/len(heston_helpers)
 print ("-"*70)
 print ("Average Abs Error (%%) : %5.3f" % (avg))

 theta = 0.131545, kappa = 10.957884, sigma = 3.991680, rho = -0.351518, v0 = 
 0.076321
    Strikes    Market Value     Model Value   Relative Error (%)
     ======================================================================
     527.50       44.53343        44.32072           -0.4776460 
     560.46       54.89815        55.07795            0.3275233 
     593.43       67.20964        67.50343            0.4371238 
     626.40       80.76865        81.66110            1.1049427 
     659.37       98.71891        97.58279           -1.1508670 
     692.34       93.09155        92.34694           -0.7998741 
     725.31       79.44659        79.24588           -0.2526358 
     758.28       67.42434        67.77431            0.5190524
```

Average Abs Error (%) : 0.634

## Answer by jherek (score 2, accepted)

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

In your code sample, the market prices are given, in terms of vols (in the data array)...

## Answer by user78712 (score 0)

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

Quantlib model has an implicit pricer (given the implied vols) to calculate the "market values", it uses the Black-Scholes Model to calculate the market_prices, and then calibrates the Heston model parameters to match that...

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.