Sometimes during the last year I published one
post on simulating
Hull-White interest rate paths using Quantlib. My conclusion was, that with all the tools provided by this wonderful library, this task should be (relatively) easy thing to do. However, as we know the devil is always in the details - in this case, in the actual process parameters (mean reversion, sigma). Before processing any simulation, we have to get those parameters from somewhere. In this post, we use Quantlib tools for calibrating our model parameters to swaption prices existing in the market.
There are so many variations of this model out there (depending on time-dependencies of different parameters), but the following is the model we are going to use in this example.
Parameters alpha and sigma are
constants and theta is time-dependent variable (usually calibrated to current yield curve). Swaption surface is presented in the picture below. Within the table below, time to swaption maturity has been defined in vertical axis, while tenor for underlying swap contract has been defined in horizontal axis. Co-terminal swaptions (used in calibration process) have been specifically marked with yellow colour.
Calibration results
Test cases for three different calibration schemes are included in the example program. More specifically, we :
- calibrate the both parameters
- calibrate sigma parameter and freeze reversion parameter to 0.05
- calibrate reversion parameter and freeze sigma parameter to 0.01
With the given data, we get the following results for these calibration schemes.
The program
As preparatory task, builder class for constructing yield curve should be implemented into a new project from
here. After this task, the following header file (ModelCalibrator.h) and tester file (Tester.cpp) should be added to this new project.
First, piecewise yield curve (USD swap curve) and swaption volatilities (co-terminal swaptions) are created by using two free functions in tester. All the data has been hard-coded inside these functions. Needless to say, in any production-level program, this data feed should come from somewhere else. After required market data and ModelCalibrator object has been created, calibration helpers for diagonal swaptions are going to be created and added to ModelCalibrator. Finally, test cases for different calibration schemes are processed.
// ModelCalibrator.h
#pragma once
#include <ql\quantlib.hpp>
#include <algorithm>
//
namespace MJModelCalibratorNamespace
{
using namespace QuantLib;
//
template <typename MODEL, typename OPTIMIZER = LevenbergMarquardt>
class ModelCalibrator
{
public:
// default implementation (given values) for end criteria class
ModelCalibrator(const EndCriteria& endCriteria = EndCriteria(1000, 500, 0.0000001, 0.0000001, 0.0000001))
: endCriteria(endCriteria)
{ }
void AddCalibrationHelper(boost::shared_ptr<CalibrationHelper>& helper)
{
// add any type of calibration helper
helpers.push_back(helper);
}
void Calibrate(boost::shared_ptr<MODEL>& model,
const boost::shared_ptr<PricingEngine>& pricingEngine,
const Handle<YieldTermStructure>& curve,
const std::vector<bool> fixedParameters = std::vector<bool>())
{
// assign pricing engine to all calibration helpers
std::for_each(helpers.begin(), helpers.end(),
[&pricingEngine](boost::shared_ptr<CalibrationHelper>& helper)
{ helper->setPricingEngine(pricingEngine); });
//
// create optimization model for calibrating requested model
OPTIMIZER solver;
//
if (fixedParameters.empty())
{
// calibrate all involved parameters
model->calibrate(helpers, solver, this->endCriteria);
}
else
{
// calibrate all involved non-fixed parameters
// hard-coded : vector for weights and constraint type
model->calibrate(helpers, solver, this->endCriteria, NoConstraint(), std::vector<Real>(), fixedParameters);
}
}
private:
EndCriteria endCriteria;
std::vector<boost::shared_ptr<CalibrationHelper>> helpers;
};
}
//
//
//
//
// Tester.cpp
#include "PiecewiseCurveBuilder.cpp"
#include "ModelCalibrator.h"
#include <iostream>
//
using namespace MJPiecewiseCurveBuilderNamespace;
namespace MJ_Calibrator = MJModelCalibratorNamespace;
//
// declarations for two free functions used to construct required market data
void CreateCoTerminalSwaptions(std::vector<Volatility>& diagonal);
void CreateYieldCurve(RelinkableHandle<YieldTermStructure>& curve,
const Date& settlementDate, const Calendar& calendar);
//
//
//
int main()
{
try
{
// dates
Date tradeDate(4, September, 2017);
Settings::instance().evaluationDate() = tradeDate;
Calendar calendar = TARGET();
Date settlementDate = calendar.advance(tradeDate, Period(2, Days), ModifiedFollowing);
//
// market data : create piecewise yield curve
RelinkableHandle<YieldTermStructure> curve;
CreateYieldCurve(curve, settlementDate, calendar);
//
// market data : create co-terminal swaption volatilities
std::vector<Volatility> diagonal;
CreateCoTerminalSwaptions(diagonal);
//
// create model calibrator
MJ_Calibrator::ModelCalibrator<HullWhite> modelCalibrator;
//
// create and add calibration helpers to model calibrator
boost::shared_ptr<IborIndex> floatingIndex(new USDLibor(Period(3, Months), curve));
for (unsigned int i = 0; i != diagonal.size(); ++i)
{
int timeToMaturity = i + 1;
int underlyingTenor = diagonal.size() - i;
//
// using 1st constructor for swaption helper class
modelCalibrator.AddCalibrationHelper(boost::shared_ptr<CalibrationHelper>(new SwaptionHelper(
Period(timeToMaturity, Years), // time to swaption maturity
Period(underlyingTenor, Years), // tenor of the underlying swap
Handle<Quote>(boost::shared_ptr<Quote>(new SimpleQuote(diagonal[i]))), // swaption volatility
floatingIndex, // underlying floating index
Period(1, Years), // tenor for underlying fixed leg
Actual360(), // day counter for underlying fixed leg
floatingIndex->dayCounter(), // day counter for underlying floating leg
curve))); // term structure
}
//
// create model and pricing engine, calibrate model and print calibrated parameters
// case 1 : calibrate all involved parameters (HW1F : reversion, sigma)
boost::shared_ptr<HullWhite> model(new HullWhite(curve));
boost::shared_ptr<PricingEngine> jamshidian(new JamshidianSwaptionEngine(model));
modelCalibrator.Calibrate(model, jamshidian, curve);
std::cout << "calibrated reversion = " << model->params()[0] << std::endl;
std::cout << "calibrated sigma = " << model->params()[1] << std::endl;
std::cout << std::endl;
//
// case 2 : calibrate sigma and fix reversion to famous 0.05
model = boost::shared_ptr<HullWhite>(new HullWhite(curve, 0.05, 0.0001));
jamshidian = boost::shared_ptr<PricingEngine>(new JamshidianSwaptionEngine(model));
std::vector<bool> fixedReversion = { true, false };
modelCalibrator.Calibrate(model, jamshidian, curve, fixedReversion);
std::cout << "fixed reversion = " << model->params()[0] << std::endl;
std::cout << "calibrated sigma = " << model->params()[1] << std::endl;
std::cout << std::endl;
//
// case 3 : calibrate reversion and fix sigma to 0.01
model = boost::shared_ptr<HullWhite>(new HullWhite(curve, 0.05, 0.01));
jamshidian = boost::shared_ptr<PricingEngine>(new JamshidianSwaptionEngine(model));
std::vector<bool> fixedSigma = { false, true };
modelCalibrator.Calibrate(model, jamshidian, curve, fixedSigma);
std::cout << "calibrated reversion = " << model->params()[0] << std::endl;
std::cout << "fixed sigma = " << model->params()[1] << std::endl;
}
catch (std::exception& e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
//
void CreateCoTerminalSwaptions(std::vector<Volatility>& diagonal)
{
// hard-coded data
// create co-terminal swaptions
diagonal.push_back(0.3133); // 1x10
diagonal.push_back(0.3209); // 2x9
diagonal.push_back(0.3326); // 3x8
diagonal.push_back(0.331); // 4x7
diagonal.push_back(0.3281); // 5x6
diagonal.push_back(0.318); // 6x5
diagonal.push_back(0.3168); // 7x4
diagonal.push_back(0.3053); // 8x3
diagonal.push_back(0.2992); // 9x2
diagonal.push_back(0.3073); // 10x1
}
//
void CreateYieldCurve(RelinkableHandle<YieldTermStructure>& curve,
const Date& settlementDate, const Calendar& calendar)
{
// hard-coded data
// create piecewise yield curve by using builder class
DayCounter curveDaycounter = Actual360();
PiecewiseCurveBuilder<ZeroYield, Linear> builder;
//
// cash rates
pQuote q_1W(new SimpleQuote(0.012832));
pIndex i_1W(new USDLibor(Period(1, Weeks)));
builder.AddDeposit(q_1W, i_1W);
//
pQuote q_1M(new SimpleQuote(0.012907));
pIndex i_1M(new USDLibor(Period(1, Months)));
builder.AddDeposit(q_1M, i_1M);
//
pQuote q_3M(new SimpleQuote(0.0131611));
pIndex i_3M(new USDLibor(Period(3, Months)));
builder.AddDeposit(q_3M, i_3M);
//
// futures
Date IMMDate;
pQuote q_DEC17(new SimpleQuote(98.5825));
IMMDate = IMM::nextDate(settlementDate + Period(3, Months));
builder.AddFuture(q_DEC17, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
//
pQuote q_MAR18(new SimpleQuote(98.5425));
IMMDate = IMM::nextDate(settlementDate + Period(6, Months));
builder.AddFuture(q_MAR18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
//
pQuote q_JUN18(new SimpleQuote(98.4975));
IMMDate = IMM::nextDate(settlementDate + Period(9, Months));
builder.AddFuture(q_JUN18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
//
pQuote q_SEP18(new SimpleQuote(98.4475));
IMMDate = IMM::nextDate(settlementDate + Period(12, Months));
builder.AddFuture(q_SEP18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
//
pQuote q_DEC18(new SimpleQuote(98.375));
IMMDate = IMM::nextDate(settlementDate + Period(15, Months));
builder.AddFuture(q_DEC18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
//
pQuote q_MAR19(new SimpleQuote(98.3425));
IMMDate = IMM::nextDate(settlementDate + Period(18, Months));
builder.AddFuture(q_MAR19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
//
pQuote q_JUN19(new SimpleQuote(98.3025));
IMMDate = IMM::nextDate(settlementDate + Period(21, Months));
builder.AddFuture(q_JUN19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
//
pQuote q_SEP19(new SimpleQuote(98.2675));
IMMDate = IMM::nextDate(settlementDate + Period(24, Months));
builder.AddFuture(q_SEP19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
//
pQuote q_DEC19(new SimpleQuote(98.2125));
IMMDate = IMM::nextDate(settlementDate + Period(27, Months));
builder.AddFuture(q_DEC19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
//
pQuote q_MAR20(new SimpleQuote(98.1775));
IMMDate = IMM::nextDate(settlementDate + Period(30, Months));
builder.AddFuture(q_MAR20, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
//
pQuote q_JUN20(new SimpleQuote(98.1425));
IMMDate = IMM::nextDate(settlementDate + Period(33, Months));
builder.AddFuture(q_JUN20, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
//
// swaps
pIndex swapFloatIndex(new USDLibor(Period(3, Months)));
pQuote q_4Y(new SimpleQuote(0.01706));
builder.AddSwap(q_4Y, Period(4, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_5Y(new SimpleQuote(0.0176325));
builder.AddSwap(q_5Y, Period(5, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_6Y(new SimpleQuote(0.01874));
builder.AddSwap(q_6Y, Period(6, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_7Y(new SimpleQuote(0.0190935));
builder.AddSwap(q_7Y, Period(7, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_8Y(new SimpleQuote(0.02011));
builder.AddSwap(q_8Y, Period(8, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_9Y(new SimpleQuote(0.02066));
builder.AddSwap(q_9Y, Period(9, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_10Y(new SimpleQuote(0.020831));
builder.AddSwap(q_10Y, Period(10, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_11Y(new SimpleQuote(0.02162));
builder.AddSwap(q_11Y, Period(11, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_12Y(new SimpleQuote(0.0217435));
builder.AddSwap(q_12Y, Period(12, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_15Y(new SimpleQuote(0.022659));
builder.AddSwap(q_15Y, Period(15, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_20Y(new SimpleQuote(0.0238125));
builder.AddSwap(q_20Y, Period(20, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_25Y(new SimpleQuote(0.0239385));
builder.AddSwap(q_25Y, Period(25, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
pQuote q_30Y(new SimpleQuote(0.02435));
builder.AddSwap(q_30Y, Period(30, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
//
curve = builder.GetCurveHandle(settlementDate, curveDaycounter);
}
I have noticed, that there are some rule-of-thumbs, but the actual calibration for any of these models is not so straightforward as one may think. There are some subtle issues on market data quality and products under pricing, which are leaving us with relatively high degree of freedom. Further step towards the calibration
red pill can be taken by checking out
this excellent research paper, written by the guys from Mizuho Securities.
Finally, as always, thanks a lot again for reading this blog.
-Mike