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!

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.

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())


(
    pd.DataFrame(
        [coupon_info(c) for c in bond.cashflows()],
        columns=("date", "nominal", "rate", "amount"),
        index=range(1, len(bond.cashflows()) + 1),
    ).style.format({"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 = bond.cashflows()[-2].amount() + bond.cashflows()[-1].amount()
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))
)
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())
current_notional
10613.635434706595
final_payment * 100 / current_notional
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.