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.
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.
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.
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.
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.
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.
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.
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())
.withConvention(ModifiedFollowing)
.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.
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.
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 the shorter syntax, note that using auto s
above would not trigger the assignment operator; it would assign to s
the MakeSchedule
instance.
And with this, we can bring this article to a close. See you next time!