Calendars and holidays

(Originally published as an article in Wilmott Magazine, January 2024.)

import QuantLib as ql

In QuantLib, calendars are usually declared by country, possibly with an additional enumeration specifying different calendars for particular exchanges or purposes; for example, the second calendar declared here is the one used for the U.S. government bond market. The first calendar we’re declaring is an exception to the rule; it’s the super-national TARGET calendar used for Euro. All in all, the library declares a few dozen calendars plus, as we’ll see later, a few classes that make it possible to build more.

cal1 = ql.TARGET()
cal2 = ql.UnitedStates(ql.UnitedStates.GovernmentBond)

Holidays, adjustments, end of month

Now, what can we do with calendars? Their basic functionality is to tell us whether a given day is a holiday or not; for instance, you can see below how the TARGET calendar tells us that May 1st, Labor Day, is a holiday, while the U.S. Government Bond calendar tells us that it’s a business day; the opposite holds if we look at June 19th, which is celebrated as Juneteenth in the United States. There is no particular treatment for weekends (they are simply considered holidays) and of course they, too, can change depending on the calendar.

d1 = ql.Date(1, ql.May, 2023)
d2 = ql.Date(19, ql.June, 2023)
cal1.isHoliday(d1)
True
cal2.isHoliday(d1)
False
cal1.isBusinessDay(d2)
True
cal2.isBusinessDay(d2)
False

When passed a holiday, calendars can also adjust it to the nearest business day, where “nearest” is determined according to a given convention; for instance, the cells below write out that the first business day after May 1st 2023, according to TARGET, was May 2nd, whereas the first business day before Juneteenth 2023 was June 16th (the 17th and 18th being Saturday and Sunday). When passed a business day, the adjust method returns it unmodified.

print(cal1.adjust(d1, ql.Following))
May 2nd, 2023
print(cal2.adjust(d2, ql.Preceding))
June 16th, 2023

Other methods can tell us whether a given date is the last business day of the month; or, given a date, which date is the last business day of that month. For some reason, there are no corresponding methods for the first of the month.

cal1.isEndOfMonth(d1)
False
print(cal1.endOfMonth(d1))
May 31st, 2023

Jumping…

I’m not going to show how to build coupon schedules in this notebook, but we can see how calendars provide the basic building block for that task; namely, how they can advance a date by a number of months or years and get the nearest business day. The basic functionality for jumping (either forwards or backwards) is already in the Date class; and the Calendar class combines, in its advance method, a jump and an adjustment.

print(d2 + ql.Period(2, ql.Months))
August 19th, 2023
print(cal1.adjust(d2 + ql.Period(2, ql.Months)))
August 21st, 2023
print(cal1.advance(d2, ql.Period(2, ql.Months)))
August 21st, 2023

…and walking

There is a case, though, in which calling the advance method gives a different result than increasing the date and then calling adjust; and that’s when the period we’re passing is measured in days. In this specific case, the advance method increases the date by the given number of business days; as we need to do, for instance, when calculating a settlement date, or a lagged payment date.

print(d2 + ql.Period(7, ql.Days))
June 26th, 2023
print(cal1.adjust(d2 + ql.Period(7, ql.Days)))
June 26th, 2023
print(cal1.advance(d2, ql.Period(7, ql.Days)))
June 28th, 2023

Unforeseen holidays

Most holidays are, as you might expect, defined as rules. Some are more or less easy to express in terms of the Western calendar; they are something like “May 1st, every year”, or “January 1st, possibly moved to Monday if it falls on a weekend”, or again “the last Monday of August”. In time, rules can change, too: for instance, Juneteenth was declared as a holiday in 2022 and markets started closing on that day in 2023, so the corresponding rule must include that condition.

Other holidays, mostly from Asian markets, have rules which are based on a lunar calendar and are therefore harder to express. In this case, we usually code them as specific days and update them as they are published on the market site; “we” meaning our esteemed contributors. The one exception is Easter, which is also based on a lunar calendar but can be found tabulated for the next couple of centuries and more.

Finally, some specific days can be declared one-off holidays by the relevant authority; for instance, in correspondence of a general election or, like this past year, for the death of a sovereign. Another, more idiosyncratic case? In the past years, SIFMA decided that Good Friday would only be a half-closure (and therefore a business day for the corresponding calendar) but SOFR would not be fixed on that day—making us realize that, grr, we needed a specialized SOFR calendar besides the existing ones.

So, what are you to do if a new holiday is published or declared but the next QuantLib release is three months away? Fortunately, you don’t need to wait for us to update the code; you don’t even need to update the code yourself and recompile the library, if you’re not so inclined. Calendars provide a method, addHoliday, that can declare a date as a new holiday at run time; you can see it in action below. You can also see that, when you add a holiday to a calendar, all instances of the same calendar (TARGET, in this case) are affected by the change. A corresponding method removeHoliday is available, even though it’s less commonly used.

d3 = ql.Date(7, ql.June, 2023)
cal1.isHoliday(d3)
False
cal1.addHoliday(d3)

cal1b = ql.TARGET()
cal1b.isHoliday(d3)
True

Joint calendars

Another case in which the provided calendars are not quite enough: building the schedules for some deals (or, in general, some sets of dates) requires to consider holidays from two or more calendars. In this case, the JointCalendar class can be used to create the required calendar from a few underlying ones; as the cells below show, it’s possible to specify if we want to join the sets of their holidays (i.e., a day is considered a holiday if it is a holiday for at least one of the underlying calendars) or the sets of their business days (i.e., a day is considered a holiday if it is a holiday for all of the underlying calendars.)

cal3 = ql.JointCalendar(cal1, cal2, ql.JoinHolidays)
cal4 = ql.JointCalendar(cal1, cal2, ql.JoinBusinessDays)
cal3.isHoliday(d1)
True
cal4.isHoliday(d1)
False
d4 = ql.Date(25, ql.December, 2023)
cal4.isHoliday(d4)
True

Bespoke calendars

Finally, in a shocking turn of events, you can also choose to disregard all the calendars we’re providing. I did it myself on a past project; in that case, we already had a data provider who was feeding us updated lists of holidays for a number of calendars and joint calendars. Any new holiday would go into our database before we were aware of it, freeing us from having to maintain lists of holidays to add or remove from the calendars built into the library.

For that use case, the right thing to do was to use the BespokeCalendar class. As you can see, each instance of the class is a blank slate to be filled with a specification of the weekends and a list of holidays. This gave us a generic way to instantiate any calendar by reading the corresponding information from the database. Also, using bespoke calendars gives you a way to create a new calendar if you’re using QuantLib from another language (say, Python or C#) and you don’t want to modify and recompile the underlying C++ code.

cal5 = ql.BespokeCalendar("my-cal")
cal5.addWeekend(ql.Saturday)
cal5.addWeekend(ql.Sunday)

holidays = [
    ql.Date(15, ql.April, 2022),
    ql.Date(18, ql.April, 2022),
    ql.Date(26, ql.December, 2022),
    ql.Date(7, ql.April, 2023),
    ql.Date(10, ql.April, 2023),
    ql.Date(1, ql.May, 2023),
    ql.Date(7, ql.June, 2023),
    ql.Date(25, ql.December, 2023),
    ql.Date(26, ql.December, 2023),
    ql.Date(1, ql.January, 2024),
]

for d in holidays:
    cal5.addHoliday(d)
cal5.isHoliday(d1)
True