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!
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.
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), face_amount, coupon_rate, day_counter, schedule, 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)): 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
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
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)
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())
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
method also scales the price with respect to the current notional of
current_notional = bond.notional(bond.settlementDate()) print(current_notional)
print(final_payment * 100 / current_notional)
To avoid rescaling, we can use another method:
NPV method would also work; the difference is that
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.
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.