This is going to be a guided tour through some example code I wrote to illustrate the usage of the Markov Functional and Gsr (a.k.a. Hull White) model implementations.
Hull White / Gsr is without any doubt the bread and butter model for rates. It calibrates to a given series of vanilla instruments, it has a parameter (the mean reversion) to control intertemporal correlations (which is important both for bermudan pricing and time travelling), but you can not alter its “factory settings” regarding the smile. At least it is not flat but a skew. Not unrealistic from a qualitative standpoint, but you would have to be lucky to match the market skew decently of course. Markov Functional on the other hand mimics any given smile termstructure exactly as long as it is arbitrage free.
Recently I added Hagan’s internal adjusters to the Gsr implementation, trying to make up for the comparative disadvantage. I coming back to them at the end of this article. Internal adjusters here is in distinction to external adjusters of course, which I am working on as well. More on them later.
Let’s delve into the example. I do not reproduce the whole code here, just the main points of interest. You can find the full example in one of the recent releases of QuantLib under Examples / Gaussian1dModels.
First we set the global evaluation date.
Date refDate(30, April, 2014);
Settings::instance().evaluationDate() = refDate;
The rate curves will be flat, but we assume basis spreads to demonstrate that the models can handle them in a decent way.
Real forward6mLevel = 0.025;
Real oisLevel = 0.02;
I will omit the code to set up the quotes and termstructures here. Swaption volatilities are chosen flat as well
Real volLevel = 0.20;
Now we set up a deal that we can price later on. The underlying is a standard vanilla spot starting swap with
fixed rate against Euribor 6M.
Real strike = 0.04;
boost::shared_ptr<NonstandardSwap> underlying =
boost::make_shared<NonstandardSwap>(VanillaSwap(
VanillaSwap::Payer, 1.0, fixedSchedule, strike,
Thirty360(),
floatingSchedule, euribor6m, 0.00, Actual360()));
Of course there is a reason that we use a NonstandardSwap
instead of a VanillaSwap
. You will see later.
We define a bermudan swaption on that underlying with yearly exercise dates, where the notification of a call should be given two TARGET days before the next accrual start period.
boost::shared_ptr<Exercise> exercise =
boost::make_shared<BermudanExercise>(
exerciseDates, false);
boost::shared_ptr<NonstandardSwaption> swaption =
boost::make_shared<NonstandardSwaption>(
underlying, exercise);
To set up the Gsr model we need to define the grid on which the model volatility is piecewise constant. Since we want to match the market quotes for european calls later on we chose the grid points identical to the exercise dates, except that we do not need a step at the last exercise date obviously. The initial model volatility is set to 
std::vector<Date> stepDates(exerciseDates.begin(),
exerciseDates.end() - 1);
std::vector<Real> sigmas(stepDates.size() + 1, 0.01);
The reversion speed is
as well.
Real reversion = 0.01;
And we are ready to define the model!
boost::shared_ptr<Gsr> gsr = boost::make_shared<Gsr>(
yts6m, stepDates, sigmas, reversion);
We will need a swaption engine for calibration
boost::shared_ptr<PricingEngine> swaptionEngine =
boost::make_shared<Gaussian1dSwaptionEngine>(
gsr, 64, 7.0, true,
false, ytsOis);
Normally it is enough to pass the model gsr
. The
and
are the default parameters for the numerical integration scheme that is used by the engine as well as true
and false
indicating that the payoff should be extrapolated outside the integration domain in a non-flat manner (this is not really important here). The last parameter denotes the discounting curve that should be used for the swaption valuation. Note that this is different from the model’s “main” yield curve, which is the Eurior 6M forward curve (see above).
We set up a second engine for our instrument we want to price.
boost::shared_ptr<PricingEngine>
nonstandardSwaptionEngine =
boost::make_shared<Gaussian1dNonstandardSwaptionEngine>(
gsr, 64, 7.0, true, false, Handle<Quote>(), ytsOis);
On top of the parameters from above, we have an empty quote here. This can be used to introduce a flat credit termstructure into the pricing. We will see later how to use this in exotic bond valuations. For the moment it is just empty, so ignored.
Now we assign the engine to our bermudan swaption
swaption->setPricingEngine(nonstandardSwaptionEngine);
How do we calibrate our Gsr model to price this swaption ? Actually there are some handy methods thanks to the fact that we chose an engine which implements the BasketGeneratingEngine
interface, so we can just say
std::vector<boost::shared_ptr<CalibrationHelper> > basket =
swaption->calibrationBasket(swapBase, *swaptionVol,
BasketGeneratingEngine::Naive);
to get a coterminal basket of at the money swaptions fitting the date schedules of our deal. The swapBase
here encodes the conventions for standard market instruments. The last parameter Naive
tells the engine just to take the exercise dates of the deal and the maturity date of the underlying and create at the money swaptions from it using the standard market conventions.
We can do more involved things and we will below: as soon as the deal specifics are not matching the standard market swaption conventions, we can choose an adjusted basket of calibration instruments! This can be a little thing like five instead of two notification dates for a call, different day count conventions on the legs, a non-yearly fixed leg payment frequency, or bigger things like a different Euribor index, an amortizing notional schedule and so on. I wrote a short note some time ago on this which you can get here if you are interested.
In any case the naive basket looks like this:
Expiry Maturity Nominal Rate Pay/Rec Market ivol
==================================================================================================
April 30th, 2015 May 6th, 2024 1.000000 0.025307 Receiver 0.200000
May 3rd, 2016 May 6th, 2024 1.000000 0.025300 Receiver 0.200000
May 3rd, 2017 May 6th, 2024 1.000000 0.025303 Receiver 0.200000
May 3rd, 2018 May 6th, 2024 1.000000 0.025306 Receiver 0.200000
May 2nd, 2019 May 6th, 2024 1.000000 0.025311 Receiver 0.200000
April 30th, 2020 May 6th, 2024 1.000000 0.025300 Receiver 0.200000
May 3rd, 2021 May 6th, 2024 1.000000 0.025306 Receiver 0.200000
May 3rd, 2022 May 6th, 2024 1.000000 0.025318 Receiver 0.200000
May 3rd, 2023 May 6th, 2024 1.000000 0.025353 Receiver 0.200000
The calibration of the model to this basket is done via
gsr->calibrateVolatilitiesIterative(basket, method, ec);
where method
and ec
are an optimization method (Levenberg-Marquardt in our case) and end criteria for the optimization. I should note that the calibration method is not the default one defined in CalibratedModel
, which does a global optimization on all instruments, but a serialized version calibrating one step of the sigma function to one instrument at a time, which is much faster.
Here is the result of the calibration.
Expiry Model sigma Model price market price Model ivol Market ivol
====================================================================================================
April 30th, 2015 0.005178 0.016111 0.016111 0.199999 0.200000
May 3rd, 2016 0.005156 0.020062 0.020062 0.200000 0.200000
May 3rd, 2017 0.005149 0.021229 0.021229 0.200000 0.200000
May 3rd, 2018 0.005129 0.020738 0.020738 0.200000 0.200000
May 2nd, 2019 0.005132 0.019096 0.019096 0.200000 0.200000
April 30th, 2020 0.005074 0.016537 0.016537 0.200000 0.200000
May 3rd, 2021 0.005091 0.013253 0.013253 0.200000 0.200000
May 3rd, 2022 0.005097 0.009342 0.009342 0.200000 0.200000
May 3rd, 2023 0.005001 0.004910 0.004910 0.200000 0.200000
and the price of our swaption, retrieved in the QuantLib standard way,
Real npv = swaption->NPV();
is around
basispoints.
Bermudan swaption NPV (ATM calibrated GSR) = 0.003808
Now let’s come back to what I mentioned above. Actually the european call rights are not exactly matching the atm swaptions we used for calibration. Namely our underlying swap is not atm, but has a fixed rate of
. So we should use an apdapted basket. Of course in this case you can guess what one should take, but I will use the general machinery to make it trustworthy. The adapted basket can be retrieved by
basket = swaption->calibrationBasket(
swapBase, *swaptionVol,
BasketGeneratingEngine::MaturityStrikeByDeltaGamma);
with the parameter MaturityStrikeByDeltaGamma
indicating that the market swaptions for calibration are chosen from the set of all possible market swaptions (defined by the swapBase
, remember ?) by an optimization of the nominal, strike and maturity as the remaining free parameters such that the zeroth, first and second order derivatives of the exotic’s underlying by the model’s state variable (evaluated at some suitable central point) are matched.
To put it differently, per expiry we seek a market underlying that in all states of the world (here for all values of the state variable of our model) has the same value as the exotic underlying we wish to price. To get this we match the Taylor expansions up to order two of our exotic and market underlying.
Let’s see what this gets us in our four percent strike swaption case. The calibration basket becomes:
Expiry Maturity Nominal Rate Pay/Rec Market ivol
==================================================================================================
April 30th, 2015 May 6th, 2024 0.999995 0.040000 Payer 0.200000
May 3rd, 2016 May 6th, 2024 1.000009 0.040000 Payer 0.200000
May 3rd, 2017 May 6th, 2024 1.000000 0.040000 Payer 0.200000
May 3rd, 2018 May 7th, 2024 0.999953 0.040000 Payer 0.200000
May 2nd, 2019 May 6th, 2024 0.999927 0.040000 Payer 0.200000
April 30th, 2020 May 6th, 2024 0.999996 0.040000 Payer 0.200000
May 3rd, 2021 May 6th, 2024 1.000003 0.040000 Payer 0.200000
May 3rd, 2022 May 6th, 2024 0.999997 0.040000 Payer 0.200000
May 3rd, 2023 May 6th, 2024 1.000002 0.040000 Payer 0.200000
As you can see the calibrated rate for the market swaption is
as expected. What you can also see is that payer swaptions were generated. This is because always out of the money options are chosen to be calibration instruments for the usual reason. The nominal is slightly different from
, but practically did not change. This is more to prove that some numerical procedure worked for you here.
Recalibrating the model to the new basket gives
Expiry Model sigma Model price market price Model ivol Market ivol
====================================================================================================
April 30th, 2015 0.006508 0.000191 0.000191 0.200000 0.200000
May 3rd, 2016 0.006502 0.001412 0.001412 0.200000 0.200000
May 3rd, 2017 0.006480 0.002905 0.002905 0.200000 0.200000
May 3rd, 2018 0.006464 0.004091 0.004091 0.200000 0.200000
May 2nd, 2019 0.006422 0.004766 0.004766 0.200000 0.200000
April 30th, 2020 0.006445 0.004869 0.004869 0.200000 0.200000
May 3rd, 2021 0.006433 0.004433 0.004433 0.200000 0.200000
May 3rd, 2022 0.006332 0.003454 0.003454 0.200000 0.200000
May 3rd, 2023 0.006295 0.001973 0.001973 0.200000 0.200000
indeed different, and the option price
Bermudan swaption NPV (deal strike calibrated GSR) = 0.007627
almost doubled from
to
basispoints. Well actually it more than doubled. Whatever. Puzzle: We did not define a smile for our market swaption surface. So it shouldn’t matter which strike we choose for the calibration instrument, should it ?
There are other applications of the delta-gamma-method. For example we can use an amortizing nominal going linear from
to
. The calibration basket then becomes
Expiry Maturity Nominal Rate Pay/Rec Market ivol
==================================================================================================
April 30th, 2015 August 5th, 2021 0.719236 0.039997 Payer 0.200000
May 3rd, 2016 December 6th, 2021 0.641966 0.040003 Payer 0.200000
May 3rd, 2017 May 5th, 2022 0.564404 0.040005 Payer 0.200000
May 3rd, 2018 September 7th, 2022 0.486534 0.040004 Payer 0.200000
May 2nd, 2019 January 6th, 2023 0.409763 0.040008 Payer 0.200000
April 30th, 2020 May 5th, 2023 0.334098 0.039994 Payer 0.200000
May 3rd, 2021 September 5th, 2023 0.255759 0.039995 Payer 0.200000
May 3rd, 2022 January 5th, 2024 0.177041 0.040031 Payer 0.200000
May 3rd, 2023 May 6th, 2024 0.100000 0.040000 Payer 0.200000
First of all, the nominal of the swaptions is adjusted to the amortizing schedule, being some average over the coming periods respectively. Furthermore, the effective maturity is reduced.
As a side note. The nominal is of course not relevant at all for the calibration step. It does not matter, if you calibrate to a swaption with nominal
or
or
. But it is a nice piece of information as in the last example anyhow, to see if it is plausible what happens.
Now consider a callable bond. You can set this up as a swap, too, with one zero leg and final notional exchange. The NonStandardSwap
allows for all this. The exercise has to be extended to carry a rebate payment reflecting the notional reimbursement in case of exercise. This is handled by the RebatedExercise
extension. The delta-gamma calibration basket now looks as follows.
Expiry Maturity Nominal Rate Pay/Rec Market ivol
==================================================================================================
April 30th, 2015 April 5th, 2024 0.984093 0.039952 Payer 0.200000
May 3rd, 2016 April 5th, 2024 0.985539 0.039952 Payer 0.200000
May 3rd, 2017 May 6th, 2024 0.987068 0.039952 Payer 0.200000
May 3rd, 2018 May 7th, 2024 0.988455 0.039952 Payer 0.200000
May 2nd, 2019 May 6th, 2024 0.990023 0.039952 Payer 0.200000
April 30th, 2020 May 6th, 2024 0.991622 0.039951 Payer 0.200000
May 3rd, 2021 May 6th, 2024 0.993111 0.039951 Payer 0.200000
May 3rd, 2022 May 6th, 2024 0.994190 0.039952 Payer 0.200000
May 3rd, 2023 May 6th, 2024 0.996715 0.039949 Payer 0.200000
The notionals are slightly below
(as well as the maturities and strikes not exactly matching the bermudan swaption case). This is expected however, since the market swaptions are discounted on OIS level, while for the bond we chose to use the 6m curve as a benchmark discounting curve. The effect is small however. Put 6m as discounting to cross check this.
What is more interesting is to assume a positive credit spread. Let’s set this to
basispoints for example. The spread is interpreted as an option adjusted spread, continuously compounded with Actual365Fixed day count convention. The calibration basket gets
Expiry Maturity Nominal Rate Pay/Rec Market ivol
==================================================================================================
April 30th, 2015 February 5th, 2024 0.961289 0.029608 Payer 0.200000
May 3rd, 2016 March 5th, 2024 0.965356 0.029605 Payer 0.200000
May 3rd, 2017 April 5th, 2024 0.969520 0.029608 Payer 0.200000
May 3rd, 2018 April 8th, 2024 0.973629 0.029610 Payer 0.200000
May 2nd, 2019 April 8th, 2024 0.978124 0.029608 Payer 0.200000
April 30th, 2020 May 6th, 2024 0.982682 0.029612 Payer 0.200000
May 3rd, 2021 May 6th, 2024 0.987316 0.029609 Payer 0.200000
May 3rd, 2022 May 6th, 2024 0.991365 0.029603 Payer 0.200000
May 3rd, 2023 May 6th, 2024 0.996646 0.029586 Payer 0.200000
Look what the rate is doing. It is adjusted by roughly the credit spread. Again this is natural, since the hedge swaption for the bond’s call right would have roughly
basispoints margin on the float side. Here it is coming automatically out of our optimization procedure.
Let’s come to our final example. The underlying is a swap exchanging a CMS10y rate against Euribor 6M. To make the numbers a bit nicer I changed the original example code to include a
basispoint margin on the Euribor leg.
We start with the underlying price retrieved from a replication approach. I am using the LinearTsrPricer
here, with the same mean reversion as for the Gsr model above. The pricing is
Underlying CMS Swap NPV = 0.004447
CMS Leg NPV = -0.231736
Euribor Leg NPV = 0.236183
so
basispoints. Now we consider a bermudan swaption (as above, with yearly exercises) on this underlying. A naively calibrated Gsr model yields
Float swaption NPV (GSR) = 0.004291
Float swap NPV (GSR) = 0.005250
The npv of the option is
basispoints. The underlying price, which can be retrieved as an additional result from the engine as follows
swaption4->result<Real>("underlyingValue")
is
basispoints. Please note that the option has its first exercise in one year time, so the first year’s coupons are not included in the exercised into deal. This is why the underlying price is higher than the option value.
What do we see here: The Gsr model is not able to price the underlying swap correctly, the price is around
basispoints higher than in the analytical pricer. This is because of the missing smile fit (in our example the fit to a flat smile, which the Gsr can not do). The Markov Functional model on the other hand can exactly do this. We can calibrate the numeraire of the model such that the market swaption surface is reproduced on the fixing dates of the CMS coupons for swaptions with 10y maturity. The goal is to get a better match with the replication price. Let’s go: The model is set up like this
boost::shared_ptr<MarkovFunctional> markov =
boost::make_shared<MarkovFunctional>(
yts6m, reversion, markovStepDates,
markovSigmas, swaptionVol,
cmsFixingDates, tenors, swapBase,
MarkovFunctional::ModelSettings()
.withYGridPoints(16));
It is not that different from the Gsr model construction. We just have to provide the CMS coupons’ fixing dates and tenors (and the conventions of the swaptions behind), so that we can calibrate to the corresponding smiles. The last parameter is optional and overwrites some numerical parameter with a more relaxed value, so that the whole thing works a bit faster in our example. Ok, what does the Markov model spit out:
Float swaption NPV (Markov) = 0.003549
Float swap NPV (Markov) = 0.004301
The underlying is now matched much better than in the Gsr model, it is up to
basispoints accurate. A perfect match is not expected from theory, because the dynamics of the linear TSR model is not the same as in the Markov model, of course.
The option price, accordingly, is around
basispoints lower compared to the Gsr model. This is around the same magnitude of the underlying mismatch in the Gsr model.
To complete the picture, the Markov model also has a volatility function that can be calibrated to a second instrument set like coterminal swaptions to approximate call rights. It is rather questionable if a call right of a CMS versus Euribor swap is well approximated by a coterminal swaption. Actually I tried to use the delta-gamma-method to search for any representation of such a call right in the Markov model. The following picture is from a different case (it is taken from the paper I mentioned above), but showing what is going on in principle

The exotic underlying is actually well matched by a market swaption’s underlying around the model’s state
, which is the expansion point for the method, so it does what it is supposed to do. But the global match is poor, so there does not seem to be a good reason to include additional swaptions into the calibration to represent call rights. They do not hurt, but do not specifically represent the call rights, so just add some more market information to the model.
Anyhow, we can do it, so we do it. If we just take atm coterminals and calibrate the Markov model’s volatility function to them, we get as a calibration result
Expiry Model sigma Model price market price Model ivol Market ivol
====================================================================================================
April 30th, 2015 0.010000 0.016111 0.016111 0.199996 0.200000
May 3rd, 2016 0.012276 0.020062 0.020062 0.200002 0.200000
May 3rd, 2017 0.010534 0.021229 0.021229 0.200001 0.200000
May 3rd, 2018 0.010414 0.020738 0.020738 0.200001 0.200000
May 2nd, 2019 0.010361 0.019096 0.019096 0.199998 0.200000
April 30th, 2020 0.010339 0.016537 0.016537 0.200002 0.200000
May 3rd, 2021 0.010365 0.013253 0.013253 0.199998 0.200000
May 3rd, 2022 0.010382 0.009342 0.009342 0.200001 0.200000
May 3rd, 2023 0.010392 0.004910 0.004910 0.200001 0.200000
0.009959
I am not going into details about the volatility function here, but note, that the first step is fixed (at its initial value of
) and the step after the last expiry date matters. In addition a global calibration to all coterminals simultaneously is necessary, the iterative approach will not work for the model.
The pricing results for the underlying does not change that much, the fit is still good as desired:
Float swap NPV (Markov) = 0.004331
There is one last thing I want to mention and which is not yet part of the library or the example code. Hagan introduced a technique called “internal adjusters” to make the Gsr model work in situations like the one we have here, namely that the underlying is not matched well. He mentions this approach in his paper on callable range accrual notes where he uses his LGM (same as Gsr or Hull White) model for pricing and observes that he does not calibrate to underlying Libor caplets or floorlets very well. He suggests to introduce an adjusting factor to be multiplied with the model volatility in case we are evaluating such a caplet or floorlet during the pricing of the exotic. So the missing model fit is compensated by using a modified model volatility “when needed” (and only then, i.e. when evaluating the exotic coupon we want to match).
This sounds like a dirty trick, destroying the model in a way and introducing arbitrage. On the other hand mispricing the market vanillas introduces arbitrage in a much more obvious and undesirable way. So why not. If Pat Hagan says we can do it, it is safe I guess. We should say however that Hagan’s original application was restricted to adjust Libor volatilities. Here we make up for a completely wrong model smile. And we could even go a step further and match e.g. market CMS spread coupon prices in a Gsr model, although the model does not even allow for rate decorrelation. So one should be careful, how far one wants to go with this trick.
I added these adjusters to the Gsr model. If you don’t specify or calibrate them, they are not used though, so nothing changes from an end user perspective. In our example we would set up a calibration basket like this
std::vector<boost::shared_ptr<CalibrationHelperBase> >
adjusterBasket;
for (Size i = 0; i < leg0.size(); ++i) {
boost::shared_ptr<CmsCoupon> coupon =
boost::dynamic_pointer_cast<CmsCoupon>(leg0[i]);
if (coupon->fixingDate() > refDate) {
boost::shared_ptr<AdjusterHelper> tmp =
boost::make_shared<AdjusterHelper>(
swapBase, coupon->fixingDate(),
coupon->date());
tmp->setCouponPricer(cmsPricer);
tmp->setPricingEngine(floatSwaptionEngine);
adjusterBasket.push_back(tmp);
}
}
The adjuster helper created here corresponds to the CMS coupons of our trade. We set the linear TSR pricer (cmsPricer
) to produce the reference results and the Gsr pricing engine in order to be able to calibrate the adjusters to match the reference prices. Now we can say
gsr->calibrateAdjustersIterative(adjusterBasket,
method, ec);
like before for the volatilities. What we get is
Expiry Adjuster Model price Reference price
================================================================================
April 30th, 2015 1.0032 2447560.9183 2447560.9183
May 3rd, 2016 1.0353 2402631.1363 2402631.1363
May 3rd, 2017 1.0640 2378624.8507 2378624.8507
May 3rd, 2018 1.0955 2324333.7739 2324333.7739
May 2nd, 2019 1.1239 2295880.1247 2295880.1247
April 30th, 2020 1.1643 2261229.3425 2261229.3425
May 3rd, 2021 1.1948 2228406.7519 2228406.7519
May 3rd, 2022 1.2214 2196901.0808 2196901.0808
May 3rd, 2023 1.2732 2177227.7967 2177227.7967
The prices here are with reference to a notional of one hundred million. The adjuster values needed to match the reference prices from the replication pricer are not too far from one, which is good, because it means that the model is not bent too much in order to produce the CMS coupon prices.
The pricing in the new model is as follows
GSR (adjusted) option value = 0.003519
GSR (adjusted) underlying value = 0.004452
The underlying match is (by construction) very good thanks to the adjusters. Also the option value is adjusted down as desired, we are now very close to the markov model’s value. Needless to say that this does not work out that well all the time. Also as an important disclaimer, an option on CMS10Y against Euribor6M has features of a spread option, which is highly correlation sensitive. Neither the (adjusted) Gsr nor the Markov model can produce correlations other than one for the spread components, so they will always underprice the option in this sense.
Enough for today, look at the example and play around with the code. Or read the paper.