Schedules in QuantLib
Hello again.
This post was originally published in the March 2024 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.
Schedules in QuantLib
For this article, I thought I’d try something new: I won’t completely change the subject from what I covered in the last issue, that is, calendars and holidays. This month, 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. What I will also show, instead, is a C++ technique that can come useful at times. Off we go.
Some examples
We can build a schedule with as little information as a start date, an end date, and a frequency. Here is the corresponding call:
Schedule s = MakeSchedule()
.from(Date(11, May, 2021))
.to(Date(11, May, 2025))
.withFrequency(Semiannual);
If you then write something like
for (auto date : s)
std::cout << date << std::endl;
you’ll see the dates listed in the table below. Unsurprisingly, it is a sequence of alternating May 11th and November 11th from the start date to the end date; they are both included.
date | |
---|---|
0 | May 11, 2021 |
1 | November 11, 2021 |
2 | May 11, 2022 |
3 | November 11, 2022 |
4 | May 11, 2023 |
5 | November 11, 2023 |
6 | May 11, 2024 |
7 | November 11, 2024 |
8 | May 11, 2025 |
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:
Schedule s = MakeSchedule()
.from(Date(11, May, 2021))
.to(Date(11, May, 2025))
.withFrequency(Semiannual)
.withCalendar(TARGET());
The above generates the schedule in the next table. 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.
date | |
---|---|
0 | May 11, 2021 |
1 | November 11, 2021 |
2 | May 11, 2022 |
3 | November 11, 2022 |
4 | May 11, 2023 |
5 | November 13, 2023 |
6 | May 13, 2024 |
7 | November 11, 2024 |
8 | May 12, 2025 |
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:
Schedule s = MakeSchedule()
.from(Date(11, May, 2021))
.to(Date(11, May, 2025))
.withFrequency(Semiannual)
.withCalendar(TARGET())
.withConvention(Unadjusted);
The result is the same as the one we got when we didn’t pass a calendar. 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:
Schedule s1 = MakeSchedule()
.from(Date(11, May, 2021))
.to(Date(11, February, 2025))
.withFrequency(Semiannual)
.withCalendar(TARGET())
.forwards();
Schedule s2 = MakeSchedule()
.from(Date(11, May, 2021))
.to(Date(11, February, 2025))
.withFrequency(Semiannual)
.withCalendar(TARGET())
.backwards();
The results are shown in the table below. In the first case, we ended up with a short last coupon; in the second, with a short first coupon.
forwards | backwards | |
---|---|---|
0 | May 11, 2021 | May 11, 2021 |
1 | November 11, 2021 | August 11, 2021 |
2 | May 11, 2022 | February 11, 2022 |
3 | November 11, 2022 | August 11, 2022 |
4 | May 11, 2023 | February 13, 2023 |
5 | November 13, 2023 | August 11, 2023 |
6 | May 13, 2024 | February 12, 2024 |
7 | November 11, 2024 | August 12, 2024 |
8 | February 11, 2025 | February 11, 2025 |
It’s also possible to specify a long coupon by passing an explicit stub:
Schedule s = MakeSchedule()
.from(Date(11, February, 2021))
.to(Date(11, May, 2025))
.withFirstDate(Date(11, November, 2021))
.withFrequency(Semiannual)
.withCalendar(TARGET())
.forwards();
The result is shown below. In case of a long last coupon, you
can use the withNextToLastDate
method instead of withFirstDate
;
the two can also be used together.
date | |
---|---|
0 | February 11, 2021 |
1 | November 11, 2021 |
2 | May 11, 2022 |
3 | November 11, 2022 |
4 | May 11, 2023 |
5 | November 13, 2023 |
6 | May 13, 2024 |
7 | November 11, 2024 |
8 | May 12, 2025 |
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; the call
Schedule s = MakeSchedule()
.from(Date(28, February, 2019))
.to(Date(28, February, 2023))
.withFrequency(Semiannual)
.withCalendar(TARGET())
.forwards();
results in the dates shown in the next table, where for instance February 28th 2021, a Sunday, is adjusted to March 1st according to the “following” convention.
date | |
---|---|
0 | February 28, 2019 |
1 | August 28, 2019 |
2 | February 28, 2020 |
3 | August 28, 2020 |
4 | March 1, 2021 |
5 | August 30, 2021 |
6 | February 28, 2022 |
7 | August 29, 2022 |
8 | February 28, 2023 |
However, if another convention such as “modified following” needs to be used, it can be passed to the call:
Schedule s = MakeSchedule()
.from(Date(28, February, 2019))
.to(Date(28, February, 2023))
.withFrequency(Semiannual)
.withCalendar(TARGET())
.withConvention(ModifiedFollowing)
.forwards();
The result is displayed below and shows that February 28th 2021 is adjusted back to February 26th so that it doesn’t change month.
date | |
---|---|
0 | February 28, 2019 |
1 | August 28, 2019 |
2 | February 28, 2020 |
3 | August 28, 2020 |
4 | February 26, 2021 |
5 | August 30, 2021 |
6 | February 28, 2022 |
7 | August 29, 2022 |
8 | February 28, 2023 |
Also, in some cases, the terms of an instrument might stipulate that coupons reset on the last business day of the month; in that case, the schedule can be generated with:
Schedule s = MakeSchedule()
.from(Date(28, February, 2019))
.to(Date(28, February, 2023))
.withFrequency(Semiannual)
.withCalendar(TARGET())
.forwards()
.endOfMonth();
The result is shown next; by comparing it with the tables above, you can see the difference of behavior for the dates at the end of August.
date | |
---|---|
0 | February 28, 2019 |
1 | August 30, 2019 |
2 | February 28, 2020 |
3 | August 31, 2020 |
4 | February 26, 2021 |
5 | August 31, 2021 |
6 | February 28, 2022 |
7 | August 31, 2022 |
8 | February 28, 2023 |
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:
auto tradeDate = Date(11, March, 2021);
auto tenor = Period(5, Years);
auto maturityDate =
cdsMaturity(tradeDate, tenor,
DateGeneration::CDS2015);
Schedule s = MakeSchedule()
.from(tradeDate)
.to(maturityDate)
.withFrequency(Quarterly)
.withCalendar(TARGET())
.withRule(DateGeneration::CDS2015);
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 or the next business day.
The result is shown below.
date | |
---|---|
0 | December 21, 2020 |
1 | March 22, 2021 |
2 | June 21, 2021 |
3 | September 20, 2021 |
4 | December 20, 2021 |
5 | March 21, 2022 |
6 | June 20, 2022 |
7 | September 20, 2022 |
8 | December 20, 2022 |
9 | March 20, 2023 |
10 | June 20, 2023 |
11 | September 20, 2023 |
12 | December 20, 2023 |
13 | March 20, 2024 |
14 | June 20, 2024 |
15 | September 20, 2024 |
16 | December 20, 2024 |
17 | March 20, 2025 |
18 | June 20, 2025 |
19 | September 22, 2025 |
20 | December 20, 2025 |
This covers most of the functionality of schedules in QuantLib.
However, you might still be curious about one thing. 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());
used in the examples. 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.
And with this, we can bring this article to a close. See you next time!