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

  1. Gamma, E., Helm, R., Johnson, R. and Vlissides, J. 1994. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional.