Handling dependencies in QuantLib
Welcome back.
Today’s post on handles was originally published as an article in the March 2023 issue of Wilmott Magazine. A word of advice: if you ever submit articles for publication, keep the copyright of your content and grant a non-exclusive license to the publisher. You’re welcome.
In this post, the C++ code is mixed with the text. The full source file is available on my Tutorial page, together with code from other articles and, when available, either the articles themselves or corresponding blog posts like this one.
Subscribe to my Substack to receive my posts in your inbox, or follow me on Twitter or LinkedIn if you want to be notified of new posts, or subscribe via RSS if you’re the tech type: the buttons for all that are in the footer. Also, I’m available for training, both online and (when possible) on-site: visit my Training page for more information.
Handling dependencies in QuantLib
In this column, I’ll describe a class which is used everywhere in the
code, is a critical part of the design of the library, and might just
be a confusing complication if I don’t stop now and explain what
problem it actually solves for you. It’s the Handle
class. (Yes,
the title above is a pun. Sue me.) This will also give me the
opportunity to write about a few techniques and principles that you
can use regardless of QuantLib.
First, let’s get the includes out of the way.
#include <ql/instruments/bonds/zerocouponbond.hpp>
#include <ql/pricingengines/bond/discountingbondengine.hpp>
#include <ql/quotes/simplequote.hpp>
#include <ql/settings.hpp>
#include <ql/termstructures/yield/zerocurve.hpp>
#include <ql/termstructures/yield/zerospreadedtermstructure.hpp>
#include <ql/time/calendars/target.hpp>
#include <ql/time/calendars/unitedkingdom.hpp>
#include <ql/time/daycounters/actual360.hpp>
#include <iostream>
A use case for handles: setting up
Let’s say we hold a number of different bonds. In the first part of
the code, we build four of them and collect them in a vector. In the
interest of brevity, they’re all zero-coupon bonds with different
maturity dates; note, though, that the vector is declared to contain
pointers to the base Bond
class, so we could have easily mixed
different kinds of bonds there.
int main() {
using namespace QuantLib;
auto valueDate = Date(7, March, 2023);
Settings::instance().evaluationDate() = valueDate;
// Create a few bonds
std::vector<ext::shared_ptr<Bond>> bonds;
auto settlementDays = 3;
auto faceAmount = 100.0;
auto calendar = TARGET();
bonds.push_back(ext::make_shared<ZeroCouponBond>(
settlementDays, calendar, faceAmount,
Date(10, February, 2025)));
bonds.push_back(ext::make_shared<ZeroCouponBond>(
settlementDays, calendar, faceAmount,
Date(26, August, 2030)));
bonds.push_back(ext::make_shared<ZeroCouponBond>(
settlementDays, calendar, faceAmount,
Date(13, May, 2024)));
bonds.push_back(ext::make_shared<ZeroCouponBond>(
settlementDays, calendar, faceAmount,
Date(15, August, 2040)));
Then, let’s assume that the bonds were issued by two different
entities, and that we have discount curves available for both. They
might come from an external system, or you might have bootstrapped or
fitted them using some other facility provided by QuantLib. In this
example, we build them using the ZeroCurve
class, which interpolates
a set of given zero rates at different dates; a real system would
provide us with a lot more nodes but, again, I tried to keep the code
short. As for bonds, the specific class doesn’t matter; any class
inherited from the base YieldTermStructure
class would do.
std::vector<Date> curveNodes = {valueDate,
valueDate + 10 * Years,
valueDate + 20 * Years};
std::vector<Rate> curveRates1 = {0.015, 0.025, 0.028};
std::vector<Rate> curveRates2 = {0.005, 0.009, 0.012};
auto issuerCurve1 = ext::make_shared<ZeroCurve>(
curveNodes, curveRates1, Actual360());
auto issuerCurve2 = ext::make_shared<ZeroCurve>(
curveNodes, curveRates2, Actual360());
And here comes the step which might seem unnecessary: instead of using
the curves directly, we wrap them in handles, namely, we pass each
curve to an instance of RelinkableHandle<YieldTermStructure>
. Yes,
it’s a mouthful.
auto issuerHandle1 =
RelinkableHandle<YieldTermStructure>(issuerCurve1);
auto issuerHandle2 =
RelinkableHandle<YieldTermStructure>(issuerCurve2);
To complete our setup, we pass each of the handles to an instance of
DiscountingBondEngine
, which can calculate bond prices by
discounting their coupons according to the given curve. Assuming that
the first two bonds come from the first issuer and the second two from
the other, we set the relevant engine to each bond; and finally, we
can output their prices.
auto engine1 = ext::make_shared<DiscountingBondEngine>(
issuerHandle1);
auto engine2 = ext::make_shared<DiscountingBondEngine>(
issuerHandle2);
bonds[0]->setPricingEngine(engine1);
bonds[1]->setPricingEngine(engine1);
bonds[2]->setPricingEngine(engine2);
bonds[3]->setPricingEngine(engine2);
for (auto b : bonds) // prints: 96.7459
std::cout << b->cleanPrice() // 84.3484
<< "\n"; // 99.3479
// 81.9757
The benefits of using handles
Now, let’s suppose you want to create a modified curve in order to simulate what happens if the credit rating of the first issuer worsens; or maybe you want to change the set of instruments on which you’re fitting one of your curves, or again your system tells you that one of the curves has changed.
Whatever the reason, we can create a new curve and pass it to the
linkTo
method of the corresponding handle: in this example, to
simulate a worsening of the credit, we use a
ZeroSpreadedTermStructure
instance which adds a spread on top of the
existing curve. After doing this, we ask the bonds for their prices,
and the first two bonds will return updated figures. No need to tell
the bonds or the engines that their discount curve has changed; the
library takes care of that.
auto shiftedCurve1 =
ext::make_shared<ZeroSpreadedTermStructure>(
Handle<YieldTermStructure>(issuerCurve1),
Handle<Quote>(
ext::make_shared<SimpleQuote>(0.001)));
issuerHandle1.linkTo(shiftedCurve1);
for (auto b : bonds) // prints: 96.5572
std::cout << b->cleanPrice() // 83.7121
<< "\n"; // 99.3479
// 81.9757
}
This, quite simply, is the reason for the existence of handles and the central role they play in the design of QuantLib: the ability to manage different term structures, quotes, and any kind of market dependencies without having to update each instrument explicitly (or without recreating the engine, as I did in the code from last issue in the interest of simplicity.)
And now, we can dig deeper in the way handles work. Or you could switch from the browser to your IDE for a while, and excitedly—nay, feverishly update your QuantLib-based code to take advantage of these new possibilities, and come back later for the rest of the article. I’ll wait.
Not just pointers
Oh, you’re back. Or you simply didn’t fall for my cheap columnist tricks. Either way, we can proceed.
You probably guessed that the Handle
class works as some kind of
pointer, keeping a reference to an external object and thus providing
the means to observe the updated state of the object if it changes.
You might also be thinking, “Wasn’t std::shared_ptr
enough for that?
And by the way, what’s this ext
namespace I see you’re using for
make_shared
instead of std
?”
Let me sidestep the question about ext
for a bit; you can read it as
std
in the meantime. As for shared_ptr
: yes, it would be
enough for some cases, but not for all.
As a general rule which might deserve to be stated explicitly (and please bear with me a minute if this is old news for you), when we say that we’re “passing an object by pointer”, as in the constructor of the following class:
class C {
A* p2;
public:
C(A* p1) : p2(p1) {}
};
what really happens is that we’re passing a pointer by copy; i.e., the
stored p2
is a copy of the passed p1
. Since they happen to be
pointers, the end result is that they contain the same address and
thus refer to the same instance of the A
class.
For instance, in the context of QuantLib: C
might be an instrument,
and might be given a pointer p1
to a curve (A
) bootstrapped over a
set of quoted OIS rates. If the curve updated its nodes because the
quotes changed, the copied pointer p2
would allow you to access the
updated curve—which updated its nodes but is still the same
instance.
What happens if we do the following, though?
A a1;
A* p1 = &a1;
C c(p1);
A a2;
p1 = &a2;
We have an instance a1
; we take a pointer p1
, let it point to a1
and pass it to c
, which stores a copy of the pointer. Later, we
create another instance a2
and update p1
so that it points to
a2
.
Flash quiz: what instance of A
does c.p2
access, now? Exactly: the
original instance a1
. We modified p1
, but this didn’t modify
c.p2
which is a distinct copy.
Adding another level of indirection
If you wanted c
to access the new object a2
, the semantics you
need are those of pointers to pointers. The class C
needs to be
written as:
class C {
A** pp2;
public:
C(A** pp1) : pp2(pp1) {}
};
What does this give us? We can now write:
A a1;
A* p1 = &a1;
A** pp1 = &p1;
C c(pp1);
A a2;
p1 = &a2;
The class C
now takes a copy of the pointer-to-pointer pp1
, which
points to p1
. Flash quiz: when we update p1
, what instance of A
can c
access? As before, the pointer-to-pointer c.pp2
was not
modified; but this time, this means that it still points to p1
and
it can see its updated value, which now stores the address of (drum
roll) a2
.
This is exactly the semantics of handles in QuantLib; they allow you to change the object they point to, and make the change accessible to any other object that stores a copy of the same handle. I’ll gloss over the implementation here; you can find it described in my book Implementing QuantLib or on this blog.
It sounds complicated. Why not setters?
Well, yes, you could have a setDiscountCurve
method declared in the
engine. But you’d have to keep track of which engines are using each
curve. And of which indexes need forecast curves. And any number of
other objects.
Also, if you, say, model your discount curves as a risk-free curve
plus a credit spread, you’d want to set those components, not the
whole discount curve. And as I mentioned, if you have other
instruments in the portfolio, you’ll soon find yourself in a mess of
setDiscountCurve
, setForecastCurve
, setDefaultProbability
and so
on. All in all, handles sound less complicated to me.
Why not just pointers to pointers then?
That wouldn’t be wrong, either. However, using a specific Handle
class is more convenient, for a number of reasons. The simplest is
that in 2000, when we started (or in 2010, for that matter,) C++
didn’t have auto
to infer types or using
to declare template
aliases, and writing
ext::shared_ptr<ext::shared_ptr<YieldTermStructure> >
was a lot less
convenient than Handle<YieldTermStructure>
.
However—and here we go into some general principles—there are far stronger reasons for creating a wrapper class. If we used the pointer classes provided by the standard (or, in 2000, by Boost,) we would provide to the user the interface of that class; nothing more and nothing less. Instead, a wrapper class allows us to define a custom interface more suited to our needs.1
The advantage of doing this is twofold. On the one hand, it makes it
possible to add behavior; for instance, relinking a handle also sends
notifications to all dependent objects (again, a lot more details are
in Implementing QuantLib.) But on the other hand, and just as
importantly, it makes it possible to remove behavior and thus hide all
the methods of shared_ptr
that might cause problems.
Case in point: in QuantLib we have RelinkableHandle
, which declares
a linkTo
method, as well as a Handle
class which doesn’t. A
RelinkableHandle
can be converted to a Handle
, but not the other
way around; much like a non-const
object can be converted to a
const
. The idea is that the main function, or some higher-level
procedure that manages the calculations, will create relinkable
handles and manage them. When the handles are passed and stored into
instruments or other objects, they are passed as Handle
so they
can’t be relinked.
If we didn’t restrict the interface (or if we passed a pointer to pointer) it would be possible for an instrument to link, say, its volatility handle to something else in order to perform some internal calculation; but in doing so, it would affect other instruments which might also share the same handle (like the bonds from the same issuer, in the code example).
How do we know this? Years ago, we had to pull a release because of a
problem like the one I just described. We only had Handle
at the
time, and it always allowed relinking—and options would merrily
relink their handles while calculating their implied volatility and
leave them modified. Hence, RelinkableHandle
and Handle
. Like
for const
, conventions are great, but having the compiler check them
is even better.
So, what’s the ext namespace?
Oh, right—I did promise. In
short: we have a compilation flag that allows us to define
ext::shared_ptr
as either boost::shared_ptr
(the current default) or
std::shared_ptr
. We did the same for other classes such as
std::function
or std::tuple
, each with its own compilation flag.
This gives us a migration path from using the Boost classes (the only alternative for the first decade of QuantLib) to using the ones in the C++ standard, without breaking other people’s code in the switch.
Boost is still the default for some of these classes; recently,
we changed the default for function
and tuple
, with the
long-term plan to deprecate the switch and just use the standard
classes.
For shared_ptr
, the decision will be a bit more complicated.
There’s a significant difference in behavior between
boost::shared_ptr
and std::shared_ptr
; the first can be configured
so that trying to access a null shared_ptr
raises an exception,
while the second doesn’t check for null pointers and lets you access
them and crash the program.
Therefore, it’s possible that we’ll keep boost::shared_ptr
around as
an alternative for quite a while, for instance when generating the
Python wrappers for QuantLib. Unlike the C++ community, Python
programmers tend to frown upon segmentation faults.
See you next time!
Notes
-
This also holds for containers such as
vector
orlist
, and often also for basic types such asint
,double
orstring
. Search for “primitive obsession” if you want more info. Among others, GeePaw Hill and Ron Jeffries have some good content about it. ↩