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