# Using QuantLib Python to value FX options using stochastic volatility

I would like to use QuantLib (and in particular the python wrapper) to value FX option using the Heston model. Thanks to http://gouthamanbalaraman.com and all of the articles therein : in particular http://gouthamanbalaraman.com/blog/valuing-european-option-heston-model-quantLib.html for valuing equity options via heston and by looking into the C++ code, I have been able to value FX options using the GKM model. I would be super appreciate even for a pointer to the correct area of the C++ code as even with this, I should be able to deduce the correct python calls.

It could very well be there are no closed form solution and one needs to resolve to Monte Carlo, although the paper : https://arxiv.org/pdf/1010.1617.pdf seems to suggest otherwise (in particular page 5).

Cheers!

• David How you set this numbers in the flat forward? flat_ts = ql.YieldTermStructureHandle( ql.FlatForward(2, ql.NullCalendar(), 0.015, ql.Actual365NoLeap()) ) dividend_ts = ql.YieldTermStructureHandle( ql.FlatForward(2, ql.NullCalendar(), -0.0065, ql.Actual365NoLeap()) I know is the quote but how you choose the correct quote for each pair Currency? Thanks Jan 2, 2023 at 22:22

The HestonModelHelper in QuantLib expects a spot value, strike and BlackVol.

In theory, you could convert the strike of your FX Options (which are normally quoted in Delta terms) into an absolute strike (Check this post for details), and then calibrate the model as if the instruments were options on an equity where the foreign rate would be the dividend.

I put together a quick example using 6M options on the EURUSD (Should obviously be improved because the fit is not particularly good...).

import QuantLib as ql
import pandas as pd

flat_ts = ql.YieldTermStructureHandle(
ql.FlatForward(2, ql.NullCalendar(), 0.015, ql.Actual365NoLeap())
)
dividend_ts = ql.YieldTermStructureHandle(
ql.FlatForward(2, ql.NullCalendar(), -0.0065, ql.Actual365NoLeap())
)
spot = 1.08417

# 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 = []

data = [
[1.0953, 4.89],
[1.111, 4.97],
[1.1233, 5.12],
[1.1404, 5.39],
[1.1533, 5.595],
[1.1745, 5.923]
]

tenor = ql.Period('6M')
for strike, vol in data:
helper = ql.HestonModelHelper(tenor, ql.TARGET(), spot,
strike, ql.QuoteHandle(ql.SimpleQuote(vol / 100)), 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(f"theta = {theta:.4f}, kappa = {kappa:.4f}, sigma = {sigma:.4f}, rho = {rho:.4f}, v0 = {v0:.4f}")

avg = 0.0

summary = []
for i, opt in enumerate(heston_helpers):
err = (opt.modelValue()/opt.marketValue() - 1.0)
summary.append((
data[i][0], opt.marketValue(),
opt.modelValue(),
100.0*(opt.modelValue()/opt.marketValue() - 1.0)))
avg += abs(err)
avg = avg*100.0/len(heston_helpers)

print("Average Abs Error (%%) : %5.3f" % (avg))
df = pd.DataFrame(
summary,
columns=["Strikes", "Market value", "Model value", "Relative error (%)"],
index=['']*len(summary))


df.set_index('Strikes')[['Market value', 'Model value']].plot(marker='o')