How Implied Volatilities Supply Prices for Heston Calibration
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.