Schedules

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

In this notebook, I’ll assume you already read about calendars and holidays. I’ll start from there and show how to use QuantLib to generate schedules, i.e., regular sequences of dates, choosing from a number of market conventions. In turn, those can be used to create sequences of coupons; but that’s something for another time.

import QuantLib as ql
import pandas as pd

A simple example

We can build a schedule with as little information as a start date, an end date, and a frequency. Here is the corresponding call:

s = ql.MakeSchedule(
    effectiveDate=ql.Date(11, ql.May, 2021),
    terminationDate=ql.Date(11, ql.May, 2025),
    frequency=ql.Semiannual,
)

pd.DataFrame(list(s), columns=["dates"])
dates
0 May 11th, 2021
1 November 11th, 2021
2 May 11th, 2022
3 November 11th, 2022
4 May 11th, 2023
5 November 11th, 2023
6 May 11th, 2024
7 November 11th, 2024
8 May 11th, 2025

Unsurprisingly, it is a sequence of alternating May 11th and November 11th from the start date to the end date; they are both included.

When building coupons from this schedule, the understanding is that the first date in the schedule is the start of the first coupon; the second date is both the end of the first coupon and the start of the second; the third date is the end of the second coupon and the start of the third; until we get to the last date, which is the end of the last coupon.

Adjusting for holidays

The above schedule didn’t make a distinction between holidays and business days. If we want holidays to be adjusted, we need to choose a calendar:

s = ql.MakeSchedule(
    effectiveDate=ql.Date(11, ql.May, 2021),
    terminationDate=ql.Date(11, ql.May, 2025),
    frequency=ql.Semiannual,
    calendar=ql.TARGET(),
)

pd.DataFrame(list(s), columns=["dates"])
dates
0 May 11th, 2021
1 November 11th, 2021
2 May 11th, 2022
3 November 11th, 2022
4 May 11th, 2023
5 November 13th, 2023
6 May 13th, 2024
7 November 11th, 2024
8 May 12th, 2025

As you can see, a few dates are no longer the 11th of the month and were replaced with the next business day. In this case, those dates fell on Saturdays or Sundays, but of course the adjustment would also be performed if they were mid-week holidays.

For convenience, it’s possible to pass a calendar and at the same time specify that dates should be unadjusted; here is the corresponding call:

s = ql.MakeSchedule(
    effectiveDate=ql.Date(11, ql.May, 2021),
    terminationDate=ql.Date(11, ql.May, 2025),
    frequency=ql.Semiannual,
    calendar=ql.TARGET(),
    convention=ql.Unadjusted,
)

pd.DataFrame(list(s), columns=["dates"])
dates
0 May 11th, 2021
1 November 11th, 2021
2 May 11th, 2022
3 November 11th, 2022
4 May 11th, 2023
5 November 11th, 2023
6 May 11th, 2024
7 November 11th, 2024
8 May 11th, 2025

As I said, this is a convenience; when reading data from a file or a DB, it makes it unnecessary to write logic that chooses whether or not to pass a calendar.

Short and long coupons

If the start and end dates don’t bracket a whole number of periods, it becomes important to specify whether the dates should be generated forwards from the start date or backwards from the end date; the default is to generate them backwards, but it’s probably better to be explicit. The corresponding calls are as follows:

s1 = ql.MakeSchedule(
    effectiveDate=ql.Date(11, ql.May, 2021),
    terminationDate=ql.Date(11, ql.February, 2025),
    frequency=ql.Semiannual,
    calendar=ql.TARGET(),
    forwards=True,
)

s2 = ql.MakeSchedule(
    effectiveDate=ql.Date(11, ql.May, 2021),
    terminationDate=ql.Date(11, ql.February, 2025),
    frequency=ql.Semiannual,
    calendar=ql.TARGET(),
    backwards=True,
)

pd.DataFrame(zip(s1, s2), columns=["forwards", "backwards"])
forwards backwards
0 May 11th, 2021 May 11th, 2021
1 November 11th, 2021 August 11th, 2021
2 May 11th, 2022 February 11th, 2022
3 November 11th, 2022 August 11th, 2022
4 May 11th, 2023 February 13th, 2023
5 November 13th, 2023 August 11th, 2023
6 May 13th, 2024 February 12th, 2024
7 November 11th, 2024 August 12th, 2024
8 February 11th, 2025 February 11th, 2025

In the first case, we ended up with a short last coupon; in the second, with a short first coupon. It’s also possible to specify a long coupon by passing an explicit stub:

s = ql.MakeSchedule(
    effectiveDate=ql.Date(11, ql.February, 2021),
    terminationDate=ql.Date(11, ql.May, 2025),
    frequency=ql.Semiannual,
    firstDate=ql.Date(11, ql.November, 2021),
    calendar=ql.TARGET(),
    forwards=True,
)

pd.DataFrame(list(s), columns=["dates"])
dates
0 February 11th, 2021
1 November 11th, 2021
2 May 11th, 2022
3 November 11th, 2022
4 May 11th, 2023
5 November 13th, 2023
6 May 13th, 2024
7 November 11th, 2024
8 May 12th, 2025

In case of a long last coupon, you can use the nextToLastDate keyword instead of firstDate; the two can also be used together.

End of month

When the dates are close to the end of their month, other conventions can come into play. The default behavior is to generate dates as usual; in the call

s = ql.MakeSchedule(
    effectiveDate=ql.Date(28, ql.February, 2019),
    terminationDate=ql.Date(28, ql.February, 2023),
    frequency=ql.Semiannual,
    calendar=ql.TARGET(),
    forwards=True,
)

pd.DataFrame(list(s), columns=["dates"])
dates
0 February 28th, 2019
1 August 28th, 2019
2 February 28th, 2020
3 August 28th, 2020
4 March 1st, 2021
5 August 30th, 2021
6 February 28th, 2022
7 August 29th, 2022
8 February 28th, 2023

you can see that, for instance, February 28th 2021, a Sunday, is adjusted to March 1st according to the “following” convention. However, if another convention such as “modified following” needs to be used, it can be passed to the call:

s = ql.MakeSchedule(
    effectiveDate=ql.Date(28, ql.February, 2019),
    terminationDate=ql.Date(28, ql.February, 2023),
    frequency=ql.Semiannual,
    calendar=ql.TARGET(),
    convention=ql.ModifiedFollowing,
    forwards=True,
)

pd.DataFrame(list(s), columns=["dates"])
dates
0 February 28th, 2019
1 August 28th, 2019
2 February 28th, 2020
3 August 28th, 2020
4 February 26th, 2021
5 August 30th, 2021
6 February 28th, 2022
7 August 29th, 2022
8 February 28th, 2023

The result shows that February 28th 2021 is adjusted back to February 26th so that it doesn’t change month.

Also, in some cases, the terms of an instrument might stipulate that coupons reset at on the last business day of the month; in that case, the schedule can be generated with:

s = ql.MakeSchedule(
    effectiveDate=ql.Date(28, ql.February, 2019),
    terminationDate=ql.Date(28, ql.February, 2023),
    frequency=ql.Semiannual,
    calendar=ql.TARGET(),
    endOfMonth=True,
    forwards=True,
)

pd.DataFrame(list(s), columns=["dates"])
dates
0 February 28th, 2019
1 August 30th, 2019
2 February 28th, 2020
3 August 31st, 2020
4 February 26th, 2021
5 August 31st, 2021
6 February 28th, 2022
7 August 31st, 2022
8 February 28th, 2023

By comparing this result with the ones above, you can see the difference of behavior for the dates at the end of August.

Specialized rules

The forwards and backwards methods shown above are shorthand for calls to a more general method withRule that allows to specify a generation rule (“forwards” and “backwards” being two such rules.) Other, more specialized rules are available; for instance, if you needed to generate the schedule for the payments of a standard 5-years credit default swap, you would do it as follows:

tradeDate = ql.Date(11, ql.March, 2021)
tenor = ql.Period(5, ql.Years)
maturityDate = ql.cdsMaturity(tradeDate, tenor, ql.DateGeneration.CDS2015)
s = ql.MakeSchedule(
    effectiveDate=tradeDate,
    terminationDate=maturityDate,
    frequency=ql.Quarterly,
    calendar=ql.TARGET(),
    rule=ql.DateGeneration.CDS2015,
)

pd.DataFrame(list(s), columns=["dates"])
dates
0 December 21st, 2020
1 March 22nd, 2021
2 June 21st, 2021
3 September 20th, 2021
4 December 20th, 2021
5 March 21st, 2022
6 June 20th, 2022
7 September 20th, 2022
8 December 20th, 2022
9 March 20th, 2023
10 June 20th, 2023
11 September 20th, 2023
12 December 20th, 2023
13 March 20th, 2024
14 June 20th, 2024
15 September 20th, 2024
16 December 20th, 2024
17 March 20th, 2025
18 June 20th, 2025
19 September 22nd, 2025
20 December 20th, 2025

First, the cdsMaturity function returns the standardized maturity date for the passed trade date; for March 11th 2021, that would be December 20th 2025 (it would roll to June 2025 only later in March.) Then, we pass the calculated maturity date to MakeSchedule while also specifying a CDS2015 date-generation rule; this recalculates the start date of the CDS and also adjusts all the dates in the schedule to the twentieth of their months of the next business day.

Schedule syntax in C++

In C++, the calls to MakeSchedule above would have the following syntax:

Schedule s = MakeSchedule()
    .from(Date(28, February, 2019))
    .to(Date(28, February, 2023))
    .withFrequency(Semiannual)
    .withCalendar(TARGET())
    .forwards();

And if you’re not familiar with this idiom, you might be asking: what about the syntax of MakeSchedule? Why aren’t we using a constructor like all decent folks, and what happens when we chain all those method calls?

The Named Parameter idiom

The Schedule class does have a constructor, of course, but it’s a bit awkward to use. As the time of this writing, corresponding to QuantLib 1.32, its signature is:

Schedule(Date effectiveDate,
         const Date& terminationDate,
         const Period& tenor,
         Calendar calendar,
         BusinessDayConvention convention,
         BusinessDayConvention terminationDateConvention,
         DateGeneration::Rule rule,
         bool endOfMonth,
         const Date& firstDate,
         const Date& nextToLastDate);

This means it requires a whole lot of parameters, even in the simplest case. Reasonable defaults exist for some of them (a null calendar, following for the conventions, backwards for the generation rule, false for the end of month, and no first or next-to-last date) but if we added them, we’d run into another problem. When we’re good with most of the default parameters but want to change one of the last ones (say, firstDate), there’s no easy syntax we can use for the call. In Python, which supports named parameters, we’d say

s = Schedule(
    Date(11, February, 2021),
    Date(11, May, 2025),
    Period(6, Months),
    firstDate = Date(11, November, 2021),
)

but in C++, we’d have to pass all the parameters before firstDate, even if they all equal the defaults.

The solution? The Named Parameter idiom (a.k.a Fluent Interface). We write a helper class, MakeSchedule in our case, which contains the parameters needed to build a schedule and gives them sensible default values:

class MakeSchedule {
    ...
  private:
    Calendar calendar_;
    Date effectiveDate_;
    Date terminationDate_;
    Period tenor_;
    BusinessDayConvention convention_ = Following;
    DateGeneration::Rule rule_ = DateGeneration::Backward;
    bool endOfMonth_ = false;
    Date firstDate_ = Date();
    Date nextToLastDate_ = Date();
};

Settings the parameters

To set the values of the parameters, we give MakeSchedule a number of setter methods; the twist here is that each of these methods returns the object itself, making it possible to chain them.

class MakeSchedule {
  public:
    MakeSchedule& from(const Date& effectiveDate) {
        effectiveDate_ = effectiveDate;
        return *this;
    }

    MakeSchedule& to(const Date& terminationDate) {
        terminationDate_ = terminationDate;
        return *this;
    }

    MakeSchedule& withTenor(const Period& tenor) {
        tenor_ = tenor;
        return *this;
    }

    MakeSchedule& forwards() {
        rule_ = DateGeneration::Forward;
        return *this;
    }

    ...
};

Getting our schedule

At this point, we’re able to write

MakeSchedule()
    .from(Date(11, May, 2021))
    .to(Date(11, February, 2025))
    .withFrequency(Semiannual)
    .withCalendar(TARGET())
    .forwards()

but the result is still a MakeSchedule instance, not a schedule. In order to build the latter, we could add an explicit to_schedule() method that calls the Schedule constructor and returns the result. However, we went for a fancier solution.

A little-used feature of C++ are user-defined conversion functions. You can google them for details, but the gist is that, if A and B are two unrelated classes, you can give B a method which returns an instance of A, declared as

class B {
  public:
    operator A() const;
    ...
};

and if you then write

B b;
A a = b;

the compiler will look first for an A constructor taking a B instance, and then (after seeing it isn’t there) it will look into B, find the conversion method, invoke it, and assign to a the instance of A returned by it.

In our case, the conversion function will be declared in MakeSchedule as

class MakeSchedule {
  public:
    ...
    
    operator Schedule() const;
};

and its implementation will call the Schedule constructor with the required parameters and return the resulting schedule. Putting everything together, we get the syntax

Schedule s = MakeSchedule()
    .from(Date(11, May, 2021))
    .to(Date(11, May, 2025))
    .withFrequency(Semiannual)
    .withCalendar(TARGET());

And in case you were wondering why I didn’t use a shorter syntax, note that using auto s above would not trigger the assignment operator and the conversion to a schedule; it would declare s as a MakeSchedule instance.