Welcome back.

This post was inspired by a question by Dagur Gunnarsson (thanks!) on the QuantLib mailing list. How do you price a bond that adds the coupon payment to its notional after each period? I gave a tentative answer on the list, but afterwards I wrote a Jupyter notebook to check my answer. Here is the result. Enjoy!

Payment-in-kind bonds

import QuantLib as ql
import pandas as pd

today = ql.Date(28, ql.April, 2023)
ql.Settings.instance().evaluationDate = today

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:

start_date = ql.Date(8, ql.February, 2021)
maturity_date = ql.Date(8, ql.February, 2026)
frequency = ql.Semiannual
calendar = ql.TARGET()
convention = ql.Following
settlement_days = 3
coupon_rate = 0.03
day_counter = ql.Thirty360(ql.Thirty360.BondBasis)
face_amount = 10000

We first build the schedule, as we would do for a vanilla fixed-rate bond.

schedule = ql.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(calendar.adjust(schedule[1]),
                                  face_amount, coupon_rate,
                                  day_counter, schedule[0], schedule[1]))

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)):
    previous = coupons[-1]
    coupons.append(ql.FixedRateCoupon(calendar.adjust(schedule[i]),
                                      previous.nominal() + previous.amount(),
                                      coupon_rate, day_counter,
                                      schedule[i-1], schedule[i]))

Finally, we can build a Bond instance by passing the list of coupons:

bond = ql.Bond(settlement_days, calendar, start_date, coupons)

We can check the resulting cash flows:

def coupon_info(cf):
    c = ql.as_coupon(cf)
    if not c:
        return (cf.date(), None, None, cf.amount())
    else:
        return (c.date(), c.nominal(), c.rate(), c.amount())

df = pd.DataFrame([coupon_info(c) for c in bond.cashflows()],
                  columns=('date','nominal','rate','amount'),
                  index=range(1,len(bond.cashflows())+1))
print(df)
                   date       nominal  rate        amount
1      August 9th, 2021  10000.000000  0.03    150.833333
2      August 9th, 2021           NaN   NaN   -150.833333
3    February 8th, 2022  10150.833333  0.03    151.416597
4    February 8th, 2022           NaN   NaN   -151.416597
5      August 8th, 2022  10302.249931  0.03    154.533749
6      August 8th, 2022           NaN   NaN   -154.533749
7    February 8th, 2023  10456.783680  0.03    156.851755
8    February 8th, 2023           NaN   NaN   -156.851755
9      August 8th, 2023  10613.635435  0.03    159.204532
10     August 8th, 2023           NaN   NaN   -159.204532
11   February 8th, 2024  10772.839966  0.03    161.592599
12   February 8th, 2024           NaN   NaN   -161.592599
13     August 8th, 2024  10934.432566  0.03    164.016488
14     August 8th, 2024           NaN   NaN   -164.016488
15  February 10th, 2025  11098.449054  0.03    168.326477
16  February 10th, 2025           NaN   NaN   -168.326477
17     August 8th, 2025  11266.775532  0.03    167.123837
18     August 8th, 2025           NaN   NaN   -167.123837
19   February 9th, 2026  11433.899369  0.03    172.461315
20   February 9th, 2026           NaN   NaN  11433.899369

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 = bond.cashflows()[-2].amount() + bond.cashflows()[-1].amount()
print(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:

discount_curve = ql.FlatForward(today, 0.0, ql.Actual365Fixed())

bond.setPricingEngine(ql.DiscountingBondEngine(
    ql.YieldTermStructureHandle(discount_curve)))
print(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:

current_notional = bond.notional(bond.settlementDate())
print(current_notional)
10613.635434706595
print(final_payment * 100 / current_notional)
109.35330081248891

To avoid rescaling, we can use another method:

print(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.

Follow me on Twitter or LinkedIn if you want to be notified of new posts, or subscribe via RSS: the buttons for 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.