The Observer pattern in QuantLib
Hello!
Today’s post was originally published in the November 2023 issue of Wilmott Magazine. The full source code 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.
The Observer pattern in QuantLib
Welcome back. This month (bi-month?) I’ll take inspiration from a discussion which is taking place as I write, and whose result you will find implemented in the library by the time you read this. It should make for a different kind of article; like Walt Whitman and Bob Dylan, QuantLib contains multitudes.
The subject of the discussion is the implementation of the Observer pattern in QuantLib. I’ll try to use it to show some of the trade-offs and changes that a design pattern might have to undergo, in order to address constraints that come from real-world usage of a library. As will be clear to anyone that looks at the version-control logs of QuantLib, the ideas and code in the library are by no means the result of my work alone; and in this case especially, the ideas I’ll describe come from a number of clever people of which yours truly is not one.
Let’s just take the headers out of the way before we start:
#include <ql/indexes/ibor/estr.hpp>
#include <ql/indexes/ibor/euribor.hpp>
#include <ql/instruments/makevanillaswap.hpp>
#include <ql/pricingengines/swap/discountingswapengine.hpp>
#include <ql/termstructures/yield/oisratehelper.hpp>
#include <ql/termstructures/yield/piecewiseyieldcurve.hpp>
#include <ql/time/calendars/target.hpp>
#include <ql/time/daycounters/actual360.hpp>
#include <ql/time/daycounters/thirty360.hpp>
#include <iostream>
The Observer pattern
The basic structure of the pattern is the one described in the classic Gang of Four book1 and shown in the figure below.
The Observable
class (called Subject in Design Patterns) models
anything that can change in time; for instance a market quote, or a
term structure of some kind. The Observer
class, instead, models
anything that depends on one or more observables and needs to be
notified when they change; for instance, an instrument whose price
depends on some quote or term structure.
Note that a given object can be both an observer and an observable; for instance, an interest-rate curve can be observed by instruments and in turn can observe a number of quoted market rates over which it is bootstrapped.
The implementation (not shown here; I’d need to oversimplify it)
provides the means for observables to broadcast their changes and for
interested observers to react. In short: an observer will ask to
register with one or more observables, each of whom will add it to the
set of its observers. When a concrete observable object (say, a
quote) executes a method that changes its state, that method will also
call notifyObservers
, which will (unsurprisingly) notify all its
observers; this is done by looping over them and calling the update
method of each one. In a concrete observer object, that method will
perform whatever specific action it needs to react to the change.
Oh, and of course, we’re not barbarians. The diagram shows sets of
pointers for the sake of conciseness, but those sets actually contain
instances of shared_ptr
and, to avoid cycles, weak_ptr
.
Some assembly required
The authors of the original Design Patterns book warned that the patterns they examined were (I’m paraphrasing) some kind of blueprint, and the implementations they provided were not prescriptive but just examples that should be adapted depending on the context, the specific requirements of one’s use case, and the language used among other things.
This was certainly true in our case. As we started to use the
pattern, we became aware of questions that our usage caused, and whose
answer can’t be prescribed or even considered in a generic
description. For instance: what if, during the notification loop, one
of the observers’ update
methods raises an exception? Do we let it
escape the loop or do we catch it? Our compromise in this case was to
catch it (so that the loop doesn’t end and all observers are notified)
and to raise an exception at the end of the loop.
If you look at our implementation, you’ll probably see a number of these concerns. In this article, though, I’ll be focusing on one particular problem that’s been with us for a couple of decades and that, as I mentioned in the introduction, we’re still wrestling with.
Large notification chains
Let’s say we have a dual-curve framework for pricing interest-rate swaps: an ESTR curve is bootstrapped over a set of OIS quotes, and then it is used as discount curve in the bootstrap of a forecast curve for the 6-months Euribor index, based on a set of swap quotes. The two curves are then used to price a fixed-vs-floating swap.
Here is the code to set up the above. First we need to create the quotes for the set of OIS, the handles holding them, and the helpers object used for the bootstrap, as well as the ESTR curve itself and a handle that contains it (for a refresher on handles, see my article in the March 2023 issue).
int main() {
using namespace QuantLib;
auto valueDate = Date(8, August, 2023);
Settings::instance().evaluationDate() = valueDate;
struct RateQuote {
Period tenor;
Rate rate;
};
std::vector<RateQuote> oisData = {
{Period(1, Years), 3.85},
{Period(2, Years), 3.52},
{Period(3, Years), 3.29},
{Period(5, Years), 3.11},
{Period(7, Years), 3.04},
{Period(10, Years), 3.02},
{Period(12, Years), 3.01},
{Period(15, Years), 2.97},
{Period(20, Years), 2.90},
{Period(30, Years), 2.85}
};
auto estr = ext::make_shared<Estr>();
std::vector<ext::shared_ptr<SimpleQuote>> oisQuotes;
std::vector<ext::shared_ptr<RateHelper>> oisHelpers;
for (const auto& q : oisData) {
Real rate = q.rate / 100.0;
auto quote = ext::make_shared<SimpleQuote>(rate);
auto helper = ext::make_shared<OISRateHelper>(
2, q.tenor, Handle<Quote>(quote), estr);
oisQuotes.push_back(quote);
oisHelpers.push_back(helper);
}
auto estrCurve = ext::make_shared<
PiecewiseYieldCurve<Discount, LogLinear>>(
0, TARGET(), oisHelpers, Actual360());
auto estrHandle = Handle<YieldTermStructure>(estrCurve);
Then we repeat the process for the Euribor curve, using the ESTR curve as an additional dependency for the second set of helpers.
std::vector<RateQuote> swapData = {
{Period(1, Years), 4.13},
{Period(2, Years), 3.87},
{Period(3, Years), 3.54},
{Period(5, Years), 3.37},
{Period(7, Years), 3.22},
{Period(10, Years), 3.20},
{Period(20, Years), 3.10},
{Period(30, Years), 2.91}
};
auto euribor6m = ext::make_shared<Euribor6M>();
std::vector<ext::shared_ptr<RateHelper>> swapHelpers;
for (const auto& q : swapData) {
Real rate = q.rate / 100.0;
auto quote = ext::make_shared<SimpleQuote>(rate);
auto helper = ext::make_shared<SwapRateHelper>(
Handle<Quote>(quote), q.tenor, TARGET(), Annual,
Unadjusted, Thirty360(Thirty360::European),
euribor6m, Handle<Quote>(), 0 * Days, estrHandle);
swapHelpers.push_back(helper);
}
auto euriborCurve = ext::make_shared<
PiecewiseYieldCurve<Discount, LogLinear>>(
0, TARGET(), swapHelpers, Actual360());
auto euriborHandle =
Handle<YieldTermStructure>(euriborCurve);
Finally, we build an instance of the Euribor6M
class that can turn the forecast curve into predicted fixings, we pass
it to the constructor of the swap we want to price (which also builds
all its coupons) and we set it a pricing engine that uses the ESTR
curve for discounting.
euribor6m = ext::make_shared<Euribor6M>(euriborHandle);
ext::shared_ptr<VanillaSwap> swap =
MakeVanillaSwap(10 * Years, euribor6m, 0.03);
swap->setPricingEngine(
ext::make_shared<DiscountingSwapEngine>(estrHandle));
std::cout << swap->NPV() << std::endl;
With the exception of the quotes (which are only observed), most of these objects observe their dependencies and are in turn observed by other objects built on top of them. In this case, which I would call a run-of-the-mill one, this leads to the large chain of dependencies shown in the next figure.
When the market moves and the quotes change, as in the next few lines of the code (where, just as an example, all rates increase by one basis point), they start a deluge of notifications.
for (auto& q : oisQuotes)
q->setValue(q->value() + 0.0001);
std::cout << swap->NPV() << std::endl;
}
Each quote notifies its handle, which in turn notifies its helper and the ESTR curve. In a naive implementation, the ESTR curve would forward each notification to every swap helper, and each of them would notify the Euribor curve—and so on, with the Euribor index later notifying each coupon and each coupon notifying the swap.
The number of OIS quotes, the number of swap helpers and the number of coupons compound. That’s way too many notifications going around and telling objects to update themselves. We need to reduce the work done.
Lazy objects
Our first order of business, though, is to reduce needless recalculations. I’m referring to objects like the ESTR or Euribor curve; when the market moves, we want them to recalculate only when the values of all the quotes have been changed, not each time they receive a notification because a single quote has moved.
This was done a long time ago by writing the LazyObject
class and
inheriting the piecewise curves from it. You can look up the details
in Implementing QuantLib if
you want, but the gist is that its update
method (which, as you
remember, is the one that gets called when a notification comes)
doesn’t do anything except setting a dirty bit which means that the
lazy object is out of date. Recalculation is only performed (and the
dirty bit is reset) when the object is asked for results; for
instance, when the curves are asked for discount factors or forward
rates. A number of other classes in the library inherit from
LazyObject
; in particular, all instruments and, as of the next
version of the library, all cash flows.
Lazy notifications
The behavior of lazy objects suggested (again, a long time ago)
another optimization. The reasoning went like this: when a
LazyObject
instance is up to date and receives a notification, it
sets its dirty bit and forwards the notification to its own observers.
Now, as long as the dirty bit is not reset, we know that the object
wasn’t yet asked for any results. This, in turn, means (we thought)
that its observers have not yet tried to recalculate. Therefore, when
another notification comes while the dirty bit is still not reset,
it’s pointless to forward it, because the object’s observers have
already received the first one and didn’t yet act upon it; they will
in good time.
This works great for reducing the number of notifications. When each quote sends a notification that eventually reaches the ESTR curve, the latter only forwards the first to each swap helper, and so does the Euribor curve. The number of OIS quotes, the number of swap helpers and the number of coupons no longer compound.
In fact, this worked so great that it took us several years to find out that our assumptions didn’t work in some specific cases. As I wrote, we assumed that if a lazy object was not asked for results, it meant that its observers didn’t yet try to recalculate; however, it was also possible that an instrument started the calculation, but it found out it was already expired so it didn’t bother asking the lazy object for results. From that point onward, the lazy object would happily swallow genuine notifications.
The solution was to add the possibility for a lazy object to forward all notifications, and to enable it on a per-object basis in the specific cases we had found. Recently, though, we’ve been talking again about this, and in the end we also added the possibility to trade some speed for more safety (quite some speed, at times) and enable this behavior for all lazy objects. That’s not the default, and I’m not sure it will ever be. However, it spurred another discussion on whether it’s possible to reduce the number of notifications.
Simplifying the notification chain
Well, it turns out it is possible. One recent pull request simplifies
the chain in a particular case: the OIS and swap helpers don’t
generate notifications by themselves, they only forward them, and they
are not going to change once the curve is built, so we can let the
curves register directly with whatever their helpers are observing
(the Observer
class has a method, registerWithObservables
, for this
purpose).
This yields the smaller notification chain shown in the figure below, and it means, e.g., that the ESTR handle no longer sends a notification to each swap helper, but it only sends a single notification directly to the Euribor curve.
More pruning
A similar simplification could be made for the swap coupons, if we
know they’re not going to change. In general, this can’t be
guaranteed (for instance, coupons can be set different pricers during
their lifetime) and therefore the pruning can’t be done automatically;
but if you’re the one doing the setup and you know you’re not going to
change the coupon pricer, there’s a simplifyNotificationGraph
function available that you can call manually. In our sample case,
this would result in the smaller notification graph in the next
figure, in which the Euribor index sends a single notification to the
swap instead of sending one to each coupon.
Summary
This article was not meant to defend our implementation, but to give an example of the problems and the constraints that come up in real-world usage. The idea was to give you some food for thought, and maybe to cause you to ask some questions—such as, for example, “Dear QuantLib developers, did you by any chance paint yourselves in a corner by choosing the Observer pattern?”
Dear reader, it’s possible. The Observer pattern influences heavily the way the library was implemented and is used. As usual, it’s all about tradeoffs. In a way, yes, we did bind our own hands; but this made it possible to use the library in what we feel is a natural way. I’m not suggesting that you should do the same, if you were to write your library; your context might vary.
That’s all for today. If we ever meet for a beer, I can tell you about a second, thread-safe implementation of the pattern in the library. It’s slower but, when using QuantLib from C# or Java, it prevents the garbage collector from deleting objects while they’re being notified and from causing a segmentation fault. All about tradeoffs, remember?
References
-
Gamma, E., Helm, R., Johnson, R. and Vlissides, J. 1994. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional. ↩