import QuantLib as ql
import pandas as pd
= ql.Date(28, ql.April, 2023)
today = today ql.Settings.instance().evaluationDate
Payment-in-kind bonds
In this kind of bonds, the interest matured during a coupon period is not paid off but added as additional notional. This doesn’t fit very well the currently available coupon types, whose notional is supposed to be known upon construction. However, for fixed-rate PIK bonds we can work around the limitation.
A workaround
The idea is to build the coupons one by one, each time feeding into a coupon information from the previous one. Let’s say we have the usual information for a bond:
= ql.Date(8, ql.February, 2021)
start_date = ql.Date(8, ql.February, 2026)
maturity_date = ql.Semiannual
frequency = ql.TARGET()
calendar = ql.Following
convention = 3
settlement_days = 0.03
coupon_rate = ql.Thirty360(ql.Thirty360.BondBasis)
day_counter = 10000 face_amount
We first build the schedule, as we would do for a vanilla fixed-rate bond.
= ql.Schedule(
schedule
start_date,
maturity_date,
ql.Period(frequency),
calendar,
convention,
convention,
ql.DateGeneration.Backward,False,
)
Instead of using the usual classes or functions for building a bond or a whole fixed-rate leg, though, we’ll start by building only the first coupon:
= []
coupons
coupons.append(
ql.FixedRateCoupon(1]),
calendar.adjust(schedule[
face_amount,
coupon_rate,
day_counter,0],
schedule[1],
schedule[
) )
The rest of the coupons can now be built one by one, each time taking the amount from the previous coupon and adding it to the notional:
for i in range(2, len(schedule)):
= coupons[-1]
previous
coupons.append(
ql.FixedRateCoupon(
calendar.adjust(schedule[i]),+ previous.amount(),
previous.nominal()
coupon_rate,
day_counter,- 1],
schedule[i
schedule[i],
) )
Finally, we can build a Bond
instance by passing the list of coupons:
= ql.Bond(settlement_days, calendar, start_date, coupons) bond
We can check the resulting cash flows:
def coupon_info(cf):
= ql.as_coupon(cf)
c if not c:
return (cf.date(), None, None, cf.amount())
else:
return (c.date(), c.nominal(), c.rate(), c.amount())
(
pd.DataFrame(for c in bond.cashflows()],
[coupon_info(c) =("date", "nominal", "rate", "amount"),
columns=range(1, len(bond.cashflows()) + 1),
indexformat(
).style."amount": "{:.2f}", "nominal": "{:.2f}", "rate": "{:.2%}"}
{
) )
date | nominal | rate | amount | |
---|---|---|---|---|
1 | August 9th, 2021 | 10000.00 | 3.00% | 150.83 |
2 | August 9th, 2021 | nan | nan% | -150.83 |
3 | February 8th, 2022 | 10150.83 | 3.00% | 151.42 |
4 | February 8th, 2022 | nan | nan% | -151.42 |
5 | August 8th, 2022 | 10302.25 | 3.00% | 154.53 |
6 | August 8th, 2022 | nan | nan% | -154.53 |
7 | February 8th, 2023 | 10456.78 | 3.00% | 156.85 |
8 | February 8th, 2023 | nan | nan% | -156.85 |
9 | August 8th, 2023 | 10613.64 | 3.00% | 159.20 |
10 | August 8th, 2023 | nan | nan% | -159.20 |
11 | February 8th, 2024 | 10772.84 | 3.00% | 161.59 |
12 | February 8th, 2024 | nan | nan% | -161.59 |
13 | August 8th, 2024 | 10934.43 | 3.00% | 164.02 |
14 | August 8th, 2024 | nan | nan% | -164.02 |
15 | February 10th, 2025 | 11098.45 | 3.00% | 168.33 |
16 | February 10th, 2025 | nan | nan% | -168.33 |
17 | August 8th, 2025 | 11266.78 | 3.00% | 167.12 |
18 | August 8th, 2025 | nan | nan% | -167.12 |
19 | February 9th, 2026 | 11433.90 | 3.00% | 172.46 |
20 | February 9th, 2026 | nan | nan% | 11433.90 |
For each date before maturity, we see two opposite payments: the 3% interest payment (with a positive sign since it’s made to the holder of the bond) as well as a negative payment that models the same amount being immediately put by the holder into the bond.
We didn’t create those payments explicitly; the Bond
constructor created them automatically to account for the change in face amount between consecutive coupons. This feature was coded in order to generate amortizing payments in case of a decreasing face amount, but it works in the opposite direction just as well.
At maturity, we also have two payments; however, this time they are the 3% interest payment and the final reimbursement. Together, they give the final payment:
= (
final_payment -2].amount() + bond.cashflows()[-1].amount()
bond.cashflows()[
) final_payment
11606.360684055619
One thing to note, though, is that asking the bond for its price might not give the expected result. Let’s use a null rate for discounting, so we can spot the issue immediately:
= ql.FlatForward(today, 0.0, ql.Actual365Fixed())
discount_curve
bond.setPricingEngine(
ql.DiscountingBondEngine(ql.YieldTermStructureHandle(discount_curve))
) bond.dirtyPrice()
109.35330081248894
Apart for the scaling to base 100, you might have expected the (undiscounted) bond value to equal the final amount. However, according to the convention for amortizing bonds, the dirtyPrice
method also scales the price with respect to the current notional of the bond:
= bond.notional(bond.settlementDate())
current_notional current_notional
10613.635434706595
* 100 / current_notional final_payment
109.35330081248891
To avoid rescaling, we can use another method:
bond.settlementValue()
11606.36068405562
The NPV
method would also work; the difference is that NPV
discounts to the reference date of the discount curve (today’s date, in this case) while settlementValue
discounts to the settlement date of the bond.
Limitations
Of course, this works without problems if the coupon rate is fixed. For floating-rate bonds, we can use the same workaround; but the resulting cashflows and the bond will need to be thrown away and rebuilt when the forecasting curve changes, because their face amount will also change. This means that we can calculate one-off prices, but we won’t be able to keep the bond and have it react when the curve changes, as vanilla floaters do.