import pandas as pd
import QuantLib as ql
= ql.Date(15, ql.January, 2024) ql.Settings.instance().evaluationDate
Cash flows and bonds
In another notebook, I’ve shown how to build sequences of regular dates (schedules, in QuantLib lingo) and I hinted that they could be used in turn to build sequences of coupons. In this notebook, we’ll do just that; and furthermore, we’ll see how to use those coupons to build a few simple bonds.
A simple fixed-rate bond
As an example, we’re going to create a simple 5-years bond paying 3% semiannually. The first thing we do is a call to MakeSchedule
, like the ones I described in the previous issue; it takes the start and end date for the coupons, their frequency, and some other information such as the calendar used to adjust dates when they fall on a holiday.
As a reminder, 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.
= ql.MakeSchedule(
schedule =ql.Date(8, ql.February, 2021),
effectiveDate=ql.Date(8, ql.February, 2026),
terminationDate=ql.Semiannual,
frequency=ql.TARGET(),
calendar=ql.Following,
convention=True,
backwards )
In C++, the same call would be written as
=
Schedule schedule ()
MakeSchedule.from(Date(8, February, 2021))
.to(Date(8, February, 2026))
.withFrequency(Semiannual)
.withCalendar(TARGET())
.withConvention(Following)
.backwards();
The actual task of building the coupons is done by FixedRateLeg
, another function provided by the library, which is the next thing we call (after defining some other bits of information such as the day-count convention for accruing the coupon rate). It takes the schedule, to be used as described above, and the other data we need.
= ql.Thirty360(ql.Thirty360.BondBasis)
day_count
= ql.FixedRateLeg(
fixed_coupons =schedule,
schedule=[0.03],
couponRates=day_count,
dayCount=[10_000],
nominals )
FixedRateLeg
, like MakeSchedule
, has a number of parameters we can pass to specify the coupons—or, in C++, of methods we can chain: in this case, the C++ code would be
=
Leg fixedCoupons (schedule)
FixedRateLeg.withCouponRates(0.03, dayCount)
.withNotionals(10000.0);
Here we’re doing the base minimum; we’re passing the rate to be paid by the coupons, their day-count convention, and the notional used for the calculation of the interest. If needed, we could pass additional specifications: for instance, a payment lag with respect to the end of the coupons, or a number of ex-coupon days.
The name of the C++ return type, Leg
, is probably more suited to swaps than to bonds. Like many things in the library, which grew bit by bit, it was defined in a particular context and it stuck. It’s not a class in its own right, but an alias for std::vector<ext::shared_ptr<CashFlow>>
which would be a mouthful to use; CashFlow
is the base class for cash flows, and derived classes model both coupons (an interest paid based on some kind of rate, fixed or otherwise) and more simple payments like the final redemption of a bond.
The last step, in order to get a working bond instance, is passing the coupons and some additional information to the constructor of the Bond
class.
= 3
settlement_days = ql.Date(5, ql.February, 2021)
issue_date
= ql.Bond(
bond1
settlement_days,
ql.TARGET(),
issue_date,
fixed_coupons, )
The constructor not only stores the coupons, but also figures out that the bond must make a final payment to reimburse its notional. We can see it by asking the bond for its cash flows (via its cashflows
method) and inspecting them: the last row corresponds to the redemption.
Note that the cashflow
method returns a list of instances of the base CashFlow
class; in order to access their more specific methods, we need to downcast them. In C++, we would use dynamic_pointer_cast<Coupon>
; in Python, where template are not available, the latter is exported as the as_coupon
function, which returns None
if the cast doesn’t succeed (as in the case of the redemption).
= []
data for cf in bond1.cashflows():
= ql.as_coupon(cf)
c if c is not None:
data.append((c.date(), c.nominal(), c.rate(), c.amount()))else:
None, None, cf.amount()))
data.append((cf.date(),
pd.DataFrame(=["date", "nominal", "rate", "amount"]
data, columnsformat({"nominal": "{:.0f}", "rate": "{:.3%}", "amount": "{:.3f}"}) ).style.
date | nominal | rate | amount | |
---|---|---|---|---|
0 | August 9th, 2021 | 10000 | 3.000% | 150.833 |
1 | February 8th, 2022 | 10000 | 3.000% | 149.167 |
2 | August 8th, 2022 | 10000 | 3.000% | 150.000 |
3 | February 8th, 2023 | 10000 | 3.000% | 150.000 |
4 | August 8th, 2023 | 10000 | 3.000% | 150.000 |
5 | February 8th, 2024 | 10000 | 3.000% | 150.000 |
6 | August 8th, 2024 | 10000 | 3.000% | 150.000 |
7 | February 10th, 2025 | 10000 | 3.000% | 151.667 |
8 | August 8th, 2025 | 10000 | 3.000% | 148.333 |
9 | February 9th, 2026 | 10000 | 3.000% | 150.833 |
10 | February 9th, 2026 | nan | nan% | 10000.000 |
Finally, we can check that the bond is working by asking, for instance, what would be the yield for a clean price of 98. In C++, the corresponding Bond
method is called yield
; in Python, it needed to be renamed to the redundant bondYield
because yield
is a reserved keyword.
98.0, day_count, ql.Compounded, ql.Semiannual) bond1.bondYield(
0.04021360635757447
A shortcut to the same bond
As you might have guessed, a fixed-rate bond is common enough that we have a more convenient way to instantiate it. Instead of the two separate calls to FixedRateLeg
and the Bond
constructor, we can call the constructor of the derived FixedRateBond
class and pass the same information. Displaying the cash flows of the resulting bond would show that they are the same as the ones above.
= ql.FixedRateBond(
bond1b
settlement_days,10000.0,
schedule,0.03],
[
day_count, )
A note on overloading
If you were to head over to GitHub and look at changes in the FixedRateBond
class across versions, you would see that it used to have multiple constructors, but most of them were deprecated and now only one remains. Is that some kind of design principle that should be followed?
Not really, no. In this case, it was a matter of balancing convenience of use in C++ against other languages, such as Python, to which the library is exported. Overloaded constructors would be more convenient in C++, as they would provide shortcuts to different use cases. However, with our current toolchain (the excellent SWIG) they would make it impossible to use keyword arguments in Python; instead, with a single constructor, we can enable them in our Python wrappers and allow writing calls like
= FixedRateBond(
bond
settlement_days,10000.0,
schedule,0.03],
[
day_count,=another_day_count,
firstPeriodDayCounter )
where we don’t need to pass (like in C++) all the default parameters defined by the constructor between the two day counts.
So, who should win? C++ or Python? I don’t have any figures about the number of people that use QuantLib in either language, but in this case C++ could graciously yield without suffering much harm; as we’ve seen, FixedRateLeg
already gave it a pretty convenient way to instantiate various use cases. As usual, the mileage in your code might vary.
An amortizing fixed-rate bond
I mentioned above that FixedRateLeg
has a number of additional methods I didn’t show; moreover, the ones I did show can take additional information. In the next call, for instance, the code passes a sequence of different notionals for the generated coupons, starting from 10000 in the first year and decreasing by 2000 each year; that is, every second coupon, since the schedule is semiannual.
= ql.FixedRateLeg(
amortizing_coupons
schedule,=[0.03],
couponRates=day_count,
dayCount=[
nominals10000.0,
10000.0,
8000.0,
8000.0,
6000.0,
6000.0,
4000.0,
4000.0,
2000.0,
2000.0,
], )
The C++ call would be
=
Leg amortizingCoupons (schedule)
FixedRateLeg.withCouponRates(0.03, dayCount)
.withNotionals({
10000.0, 10000.0,
8000.0, 8000.0,
6000.0, 6000.0,
4000.0, 4000.0,
2000.0, 2000.0
});
In this case, passing the returned coupons to the Bond
constructor results in an amortizing bond; again, we can check that by displaying its cash flows. As before, the machinery in the constructor looked at the notionals of the coupons and figured out that, when the notional varies between two consecutive coupons, a notional payment needed to be inserted.
= ql.Bond(
bond2
settlement_days,
ql.TARGET(),
issue_date,
amortizing_coupons, )
= []
data for cf in bond2.cashflows():
= ql.as_coupon(cf)
c if c is not None:
data.append((c.date(), c.nominal(), c.rate(), c.amount()))else:
None, None, cf.amount()))
data.append((cf.date(),
pd.DataFrame(=["date", "nominal", "rate", "amount"]
data, columnsformat({"nominal": "{:.0f}", "rate": "{:.3%}", "amount": "{:.3f}"}) ).style.
date | nominal | rate | amount | |
---|---|---|---|---|
0 | August 9th, 2021 | 10000 | 3.000% | 150.833 |
1 | February 8th, 2022 | 10000 | 3.000% | 149.167 |
2 | February 8th, 2022 | nan | nan% | 2000.000 |
3 | August 8th, 2022 | 8000 | 3.000% | 120.000 |
4 | February 8th, 2023 | 8000 | 3.000% | 120.000 |
5 | February 8th, 2023 | nan | nan% | 2000.000 |
6 | August 8th, 2023 | 6000 | 3.000% | 90.000 |
7 | February 8th, 2024 | 6000 | 3.000% | 90.000 |
8 | February 8th, 2024 | nan | nan% | 2000.000 |
9 | August 8th, 2024 | 4000 | 3.000% | 60.000 |
10 | February 10th, 2025 | 4000 | 3.000% | 60.667 |
11 | February 10th, 2025 | nan | nan% | 2000.000 |
12 | August 8th, 2025 | 2000 | 3.000% | 29.667 |
13 | February 9th, 2026 | 2000 | 3.000% | 30.167 |
14 | February 9th, 2026 | nan | nan% | 2000.000 |
More cash flows and bonds
The library, of course, is not limited to fixed-rate coupons. For instance, a C++ call like
=
Leg cashflows (schedule, euribor6m)
IborLeg.withSpreads(0.001)
.withNotionals(10000.0);
would generate coupons paying the fixings of the 6-months Euribor rate plus 10 basis points. You’ll forgive me for only showing this in passing; adding it as an example to the code would require setting up the index and its forecast curve, and that is probably best done in another notebook with more details.