Provides examples of short interest rate model calibration to swaption volatilities in QuantLib Python
I have talked about Hull-White model in my earlier blog posts. The focus of those posts was to see how to use the model classes. The model parameters were assumed to be given. However in practice, the model parameters need to calibrated from market data. Typically instruments such as swaptions, caps or floors and their market prices / volatilities are taken as inputs. Then the model parameters are fit in such a way that the model prices these options close enough. The goodness of fit depends, apart from the choice of the numerical methods, on the type of model itself. This is because models such as Hull-White 1 factor cannot fit some of the humped volatility term structures observed in the market. Never the less, Hull-White is usually a good starting point to understand calibration process.
Here we will discuss Hull-White model in detail. Then we will also show how the same procedure can be applied to calibrate other short rate models.
import QuantLib as ql
from collections import namedtuple
import math
Hull-White model was one of the first practical exogenous models that attempted to fit to the market interest rate term structures. The model is described as:
\begin{equation} dr_t = (\theta(t) - a r_t) dt + \sigma dW_t \end{equation}
where $a$ is the mean reversion constant, $\sigma$ is the volatility parameter. The parameter $\theta(t)$ is chosen in order to fit the input term structure of interest rates.
What is the "right" value for parameters $a$ and $\sigma$? This is the question that we address by calibrating to market instruments.
today = ql.Date(15, ql.February, 2002);
settlement= ql.Date(19, ql.February, 2002);
ql.Settings.instance().evaluationDate = today;
term_structure = ql.YieldTermStructureHandle(
ql.FlatForward(settlement,0.04875825,ql.Actual365Fixed())
)
index = ql.Euribor1Y(term_structure)
In this example we are going to calibrate to the swaption volatilities as shown below.
CalibrationData = namedtuple("CalibrationData",
"start, length, volatility")
data = [CalibrationData(1, 5, 0.1148),
CalibrationData(2, 4, 0.1108),
CalibrationData(3, 3, 0.1070),
CalibrationData(4, 2, 0.1021),
CalibrationData(5, 1, 0.1000 )]
Here we use the JamshidianSwaptionEngine
to value the swaptions as part of calibration. The JamshidianSwaptionEngine
requires one-factor affine models as input. For other interest rate models, we need a pricing engine that is more suited to those models.
model = ql.HullWhite(term_structure);
engine = ql.JamshidianSwaptionEngine(model)
swaptions = create_swaption_helpers(data, index, term_structure, engine)
optimization_method = ql.LevenbergMarquardt(1.0e-8,1.0e-8,1.0e-8)
end_criteria = ql.EndCriteria(10000, 100, 1e-6, 1e-8, 1e-8)
model.calibrate(swaptions, optimization_method, end_criteria)
a, sigma = model.params()
print "a = %6.5f, sigma = %6.5f" % (a, sigma)
calibration_report(swaptions, data)
model = ql.HullWhite(term_structure);
engine = ql.JamshidianSwaptionEngine(model)
swaptions = create_swaption_helpers(data, index, term_structure, engine)
optimization_method = ql.LevenbergMarquardt(1.0e-8,1.0e-8,1.0e-8)
end_criteria = ql.EndCriteria(10000, 100, 1e-6, 1e-8, 1e-8)
model.calibrate(swaptions, optimization_method, end_criteria)
a, sigma = model.params()
print "a = %6.5f, sigma = %6.5f" % (a, sigma)
calibration_report(swaptions, data)
Some times we need to calibrate with one parameter held fixed. This can be done in the QuantLib libraries. However, this ability is not exposed in the SWIG wrappers as of version 1.6. I have created a github issue and provided a patch to address this issue. You will need this patch to execute the following cells. Below, the model is calibrated with a fixed reversion value of 5%.
constrained_model = ql.HullWhite(term_structure, 0.05, 0.001);
engine = ql.JamshidianSwaptionEngine(constrained_model)
swaptions = create_swaption_helpers(data, index, term_structure, engine)
optimization_method = ql.LevenbergMarquardt(1.0e-8,1.0e-8,1.0e-8)
end_criteria = ql.EndCriteria(10000, 100, 1e-6, 1e-8, 1e-8)
constrained_model.calibrate(swaptions, optimization_method, end_criteria, ql.NoConstraint(), [], [True, False])
a, sigma = constrained_model.params()
print "a = %6.5f, sigma = %6.5f" % (a, sigma)
calibration_report(swaptions, data)
The Black Karasinski model is described as:
\begin{equation} d\ln(r_t) = (\theta_t - a \ln(r_t)) dt + \sigma dW_t \end{equation}
In order to calibrate, we use the TreeSwaptionEngine
which will work with all short rate models. The calibration is shown below.
model = ql.BlackKarasinski(term_structure);
engine = ql.TreeSwaptionEngine(model, 100)
swaptions = create_swaption_helpers(data, index, term_structure, engine)
optimization_method = ql.LevenbergMarquardt(1.0e-8,1.0e-8,1.0e-8)
end_criteria = ql.EndCriteria(10000, 100, 1e-6, 1e-8, 1e-8)
model.calibrate(swaptions, optimization_method, end_criteria)
a, sigma = model.params()
print "a = %6.5f, sigma = %6.5f" % (a, sigma)
calibration_report(swaptions, data)
As a final example, let us look at a calibration example of the 2-factor G2++ model. \begin{equation} dr_t = \varphi(t) + x_t + y_t \end{equation}
where $ x_t $ and $ y_t $ are defined by
\begin{eqnarray} dx_t &=& -a x_t dt + \sigma dW^1_t\nonumber \\ dy_t &=& -b y_t dt + \eta dW^2_t \nonumber \\ \left<dW^1_t dW^2_t\right> &=& \rho dt \end{eqnarray}
Once again, we use the TreeSwaptionEngine
to value the swaptions in the calibration step.
model = ql.G2(term_structure);
engine = ql.TreeSwaptionEngine(model, 25)
# engine = ql.G2SwaptionEngine(model, 10, 400)
# engine = ql.FdG2SwaptionEngine(model)
swaptions = create_swaption_helpers(data, index, term_structure, engine)
optimization_method = ql.LevenbergMarquardt(1.0e-8,1.0e-8,1.0e-8)
end_criteria = ql.EndCriteria(1000, 100, 1e-6, 1e-8, 1e-8)
model.calibrate(swaptions, optimization_method, end_criteria)
a, sigma, b, eta, rho = model.params()
print "a = %6.5f, sigma = %6.5f, b = %6.5f, eta = %6.5f, rho = %6.5f " % (a, sigma, b, eta, rho)
calibration_report(swaptions, data)
In this post, we saw some simple examples of calibrating the model to the swaption volatilities.
Click here to download the ipython notebook