The global evaluation date
Welcome back.
In this post, an initial look at a feature I always used in my examples but never really explained: the global evaluation date. It’s short as notebooks go, but it has some discussion at the end that tries to give some more context. (Pun not intended. What pun? You’ll see.) And of course, it’s also been added to A QuantLib Guide.
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 global evaluation date
If you already used QuantLib, chances are that you know about the global evaluation date. By default, it is set to the current date, which is why you might have ignored it if you only performed calculations as of today; but as soon as you try to calculate figures at some past date, you’ll have to set it properly.
A simple example
import QuantLib as ql
Let’s say you have a bond which has already matured; for instance, this one.
bond = ql.ZeroCouponBond(
settlementDays=3,
calendar=ql.TARGET(),
faceAmount=10_000,
maturityDate=ql.Date(8, ql.February, 2025),
)
As I write this, it’s April 2025; but let’s say you’re checking some end-of-year figures, and want to reprice the bond as of that date:
as_of_date = ql.Date(31, ql.December, 2024)
Next, you set up a discount curve with the correct reference date and use it to price the bond by passing it to the usual engine.
discount_curve = ql.RelinkableYieldTermStructureHandle(
ql.FlatForward(as_of_date, 0.02, ql.Actual360())
)
engine = ql.DiscountingBondEngine(discount_curve)
bond.setPricingEngine(engine)
You already know where this is going: you ask for the price, and you get a null one.
bond.cleanPrice()
0.0
This is because the bond methods don’t rely on the reference date of the curve (which doesn’t always equal the evaluation date) but on the global evaluation date. If we ask the bond, it’s going to tell us that it has matured.
bond.isExpired()
True
The correct way to get its end-of-year price, of course, is to set the evaluation date:
ql.Settings.instance().evaluationDate = as_of_date
At which point the calculations work as expected.
bond.cleanPrice()
99.80574447629698
In C++, which uses ::
for calling class methods and doesn’t have
properties, the syntax to use is
Settings::instance().evaluationDate() = as_of_date;
Other possible points of failure
In a real-world case, you might have found that something was amiss even before asking the bond for its price. If you had tried to create the discount curve by bootstrapping it over the end-of-year quoted rates, you’d have probably failed to do so; the bootstrap algorithm would have considered the 5-years swap rate as starting from today, not from the reference date passed to the curve. Why this inconsistence inside the curve itself? Because, as I mentioned, the reference date of the curve might not equal the evaluation date (I’ve seen desks where the reference date of the swap curve would be the spot date, two business days from the evaluation date) and thus the algorithm can’t rely on it.
Term structures
Term structures can be set up so that their reference date moves with the global evaluation date; I talk about this in more detail in another notebook.
Fixings
Another case where the evaluation date plays a role is determining whether the fixing of an interest-rate index should be calculated. Let’s create an index and give it a flat 1% forecast curve:
forecast_curve = ql.YieldTermStructureHandle(
ql.FlatForward(0, ql.NullCalendar(), 0.01, ql.Actual360())
)
index = ql.Euribor3M(forecast_curve)
The evaluation date is still at the end of 2024. If we try to ask the index for a fixing at a past date, it’s going to gently but firmly rebuke us:
try:
index.fixing(ql.Date(15, ql.February, 2021))
except Exception as e:
print(f"{type(e).__name__}: {e}")
RuntimeError: Missing Euribor3M Actual/360 fixing for February 15th, 2021
That’s because the fixing is in the past with respect to the evaluation date, and therefore it can’t be forecast off the curve: it must have been stored instead (here I’ll use a bogus 2% value). Once we do this, the index returns the stored value.
index.addFixing(ql.Date(15, ql.February, 2021), 0.02)
index.fixing(ql.Date(15, ql.February, 2021))
0.02
If I move the evaluation date before that fixing date, though, the behavior of the index changes. Now the fixing date is in the future with respect to the evaluation date, and therefore the fixing is forecast from the 1% curve we set previously, even though the stored value is still there:
ql.Settings.instance().evaluationDate = ql.Date(1, ql.February, 2021)
index.fixing(ql.Date(15, ql.February, 2021))
0.010012371303880592
Why a global evaluation date anyway?
Yes, sensible question. In this age of multithreading, it feels weird to have a feature that prevents us from perform two parallel calculations as of two different evaluation dates.
The historical reason is that QuantLib was started in 2000, and multi-core computers were not yet the norm at the time. This wouldn’t prevent us to redesign it, per se; but as I wrote in Implementing QuantLib, we don’t really have a workable alternative.
The obvious idea (that is, the idea that quite a few smart people had when we talked about it) was to use some kind of context object that should replace the global settings. But how would one select a context for any given calculation?
It would be appealing to add a setContext
method to the Instrument
class, and to arrange things so that during calculation the instrument
propagates the context to its engine and in turn to any term structures
that need it. However, this can’t be implemented easily.
To begin with, the instrument and its engine are not always directly aware of all the term structures that are involved in the calculation. For instance, a swap contains a number of coupons, any of which might or might not reference a forecast curve. We’re not going to reach them unless we add the relevant machinery to all the classes involved. I’m not sure that we want to set a context to a coupon.
Another problem is that there are some kinds of data sharing that just feel natural in the library. For instance, all instruments based on the Euribor index can share the same index instance and depend on a single forecast curve. Trying to price two such instruments in two different contexts in parallel wouldn’t work. Using contexts would force us to duplicate the index and curve objects, at which point it would be simpler to use a thread-local evaluation date (see below).
Also, I’m skipping over the scenario in which the context is threaded through the various method calls during calculations instead of being set. It would lead to method calls like
termStructure->discount(t, context);
which would completely break caching, would cause discomfort to all parties involved, and if we wanted stuff like this we’d write in Haskell.
Not just globals, unfortunately
Truth be told, sharing the same curve between instruments is ok in a single-threaded calculation, but would be dangerous when multi-threading, even if the evaluation date or the context were the same. A number of calculations in the library are lazy; that is, they’re performed when required but not earlier. For instance, an interest-rate curve would not be bootstrapped when created, but only when actually asked for rates or discount factors. This means that passing the same curve to two instruments and asking them for their values in parallel might trigger the bootstrap of the curve on two different threads. It wouldn’t end well.
Thread-local evaluation date
The library has a setting that can cause the evaluation date (and other
globals) to be thread-local. Unfortunately, it requires recompiling,
which rules out a simple pip install QuantLib
for all you Pythonistas.
One still needs to be careful, though; you better not share of objects between threads, since the bootstrap example above still applies. And it’s somewhat inconvenient, as well; for instance, index fixings would also be saved in a thread-local object and would have to be reloaded in each thread.
Living in the present
What if you’re doing calculations as of today—I mean, the actual current date? Should you still set the evaluation date?
All in all, yes, I’d advise doing that. There’s no harm in it, whereas leaving the evaluation date unset has two (possibly quite minor) disadvantages.
The first: every time the calculations will ask for the evaluation date, it will be retrieved by calling some system or library facility; that’s slower than returning the stored date that you set. Granted, this might be negligible compared to the time taken by the rest of the calculation, but it’s still a waste that can be avoided with no effort.
The second, not very probable but possible: if your calculations ever run beyond midnight, the evaluation date will change and your objects won’t receive any notifications. This would leave them in an inconsistent state.
See you next time!