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.