Skip to content
All library documents

Calibrating a Heston Model to FX Options with QuantLib

Article Quant Q&A · Author: RokoMCRoko

Summary

The document explains how to adapt QuantLib’s Heston option-pricing workflow to foreign exchange options. It treats the foreign interest rate like a dividend yield and the domestic rate as the risk-free curve, allowing FX options to be handled similarly to equity options. Because FX options are commonly quoted by delta, the strikes must first be converted to absolute strike prices before using the model helpers.

An example calibrates Heston parameters to six-month EUR/USD option quotes with QuantLib’s model helper, analytic pricing engine, and Levenberg–Marquardt optimizer. It compares model prices with market prices and reports relative pricing errors. The example’s fit is explicitly described as weak and should be improved; the note also raises, but does not resolve, whether a closed-form approach is available for the Garman–Kohlhagen setting. The quoted dataset is small, so it illustrates implementation rather than establishing model performance.

Key ideas

  • FX option delta quotes need conversion to absolute strikes for this calibration workflow.
  • The foreign rate can be represented as a dividend yield, with the domestic rate used as the risk-free curve.
  • QuantLib’s Heston helper can calibrate model parameters to market option quotes.
  • The example uses an analytic Heston engine and nonlinear least-squares optimization.
  • Its pricing fit is acknowledged to be poor, so the example is illustrative rather than conclusive.

Tags

Full text
# Using QuantLib Python to value FX options using stochastic volatility


# 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!

## Answer by David Duarte (score 3)

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

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')
```

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.