1
$\begingroup$

I am dealing with fixed rate bonds. There is one particular bond, 34659UAC0, that caught my eye. This bond has a first coupon period of a whopping 5 years, followed by regular periods of 6 months. My current setup seems unable to deal with this properly. Here is minimal code to replicate this case, on QuantLib 1.32:

import QuantLib as ql
import pdb
import pandas as pd
import numpy as np
import os


# 34659UAC0

daycount = ql.Thirty360(ql.Thirty360.ISMA)

schedule = ql.Schedule(
    ql.Date(1,7,2014),  # effectiveDate
    ql.Date(1,1,2043),  # terminationDate
    ql.Period(ql.Semiannual),  # tenor
    ql.UnitedStates(ql.UnitedStates.GovernmentBond),  # calendar
    ql.Unadjusted,  # convention
    ql.Unadjusted,  # terminationDateConvention
    ql.DateGeneration.Forward,  # rule
    False,  # endOfMonth
    ql.Date(1,7,2019)  # firstDate
)
bond = ql.FixedRateBond(
    0,  # settlementDays
    100.0,  # faceAmount,
    schedule,  # schedule
    [0.12],  # coupons
    daycount,  # accrualDayCounter
    ql.Unadjusted  # paymentConvention
)
yield_ = bond.bondYield(
    100.0,  # clean_price
    daycount,  # dayCounter
    ql.Compounded,  # compounding
    ql.Semiannual,  # frequency
    ql.Date(1,7,2014)  # settlementDate
)
print(f"yield is {yield_:.3f}")

import matplotlib.pyplot as plt

# plot daily clean price from start to end
prices = pd.Series(dtype=float)
t = ql.Date(1,7,2014)
while True:
    cp = bond.cleanPrice(
        yield_,  # yield
        daycount,  # dayCount
        ql.Compounded,  # compounding
        ql.Semiannual,  # frequency
        t  # settlementDate
    )
    prices[t] = cp
    t += 1
    if t == ql.Date(1,1,2043):
        break

fig, axs = plt.subplots(1, 1, figsize=(8, 6))
x_ticks = np.arange(len(prices))
TICK_FREQ = 365 * 2

axs.plot(x_ticks, prices.to_numpy(), label="clean price")
axs.legend()
axs.set_xticks(x_ticks[::TICK_FREQ])
axs.set_xticklabels(prices.index[::TICK_FREQ], rotation=45, fontsize=6)
axs.set_title(f"clean prices")

fig.tight_layout()
fig.savefig(os.path.join(r"C:\data\my_data\tax", f"34659UAC0_cleanprice.png"), dpi=200)
plt.clf()

pdb.set_trace()

This code outputs the yield to be 10.8%, then proceeds to plot clean price on each day from start (the interest accrual date) to end (maturity date). However, one can notice that the bond is issued at par, so the YTM from start date should just be the coupon rate, 12%. Indeed, Bloomberg YAS page gives the correct YTM of 12%. How can I set up QuantLib to give the correct YTM and clean prices (they should just be always 100)?

Bond information:

Interest accrual date 2014-07-01

Maturity date: 2043-01-01

First coupon date: 2019-07-01

coupon rate: 12%, paid semiannualy

enter image description here

Update: The problem is with the frequency parameter in bondYield. It forces a constant frequency, and we need to find a way to specify it to be consistent with coupon payment days.

Update2: To clarify the problem, the behavior I want is different from conventional YTM. An example DimitriVulis has raised in the comments below is that for a bond with annual coupon rate of $c$ but only pays until maturity, what do we expect the YTM to be. If we are using annual compounding, then we formula is $1 = \frac{1 + cn}{(1 + ytm)^n}$. However, I need it to be $1 = \frac{1 + cn}{1 + ytm * n}$. The motivation for this can be demonstrated by the clean price graph for my original problem. Since I am dealing with bond taxation, I need a pull-to-par as the tax basis for the bond, and this curve must be monotonically increasing / decreasing. For a method called "constant yield method", the tax basis is equal to clean price on coupon dates, and equal to a linear interpolation in between coupon dates. Therefore, I need my clean price to be monotonic, and thus my yield to be consistent with coupon payment dates.

Mathematically, the YTM I want should be implied from the following formula: $\frac{issue price}{par price} = \frac{c_1}{1 + ytm * c_1 / c} + \frac{c_2}{(1 + ytm * c_1 / c)(1 + ytm * c_2 / c)} + ... + \frac{1 + c_n}{(1 + ytm * c_1 / c)...(1 + ytm * c_n / c)}$, where $c_1...c_n$ are actual coupon payments (taking into account the time after the previous payment) per unit value, and $c$ is annual coupon rate. Note that if the coupon payments are all $c/2$, then this is equivalent to a standard semiannually-compounded YTM.

$\endgroup$
8
  • $\begingroup$ Since you know the cash flows, have you tried manually calculating prices from these yields? $\endgroup$ Commented Apr 18 at 19:05
  • $\begingroup$ @DimitriVulis I would like to avoid doing that manually since I need to first binary search for the yield, which is slow for my application. On a personal note, I am not totally sure what the formula is in the case of irregular coupon periods... Can you perhaps point me to a source? $\endgroup$
    – Jerry
    Commented Apr 18 at 19:08
  • $\begingroup$ Update: I think the formula in this case is just something like 100 = 60 / (1+ytm*5) + 6 / (1+ytm*5)(1+ytm/2) + 6 / (1+ytm*5)(1+ytm/2)^2 + .... However, I would need to scale the ytm correctly according to the specified day count, which introduces more work. $\endgroup$
    – Jerry
    Commented Apr 18 at 19:20
  • $\begingroup$ You shouldn't need any solver for yield-to-price, rather it should be a closed-form formula. $\endgroup$ Commented Apr 18 at 21:17
  • $\begingroup$ Consider a simpler bond that matures in $n>1$ years. The annual coupon rate is $c$. But it pays nothing until maturity, and at maturity it pays one cash flow: just a $n\times c$ coupon with no compounding / reinvestment / interest on interest plus the par face value. Do you get par price back if you discount the single cash flow using $c$ as yield $1\stackrel{?}{=}\frac{1+cn}{(1+c)^n}$? What does this yield calculation assume about reinvestment? If the price is par now, do you expect the yield to be $c$? $\endgroup$ Commented Apr 19 at 1:03

0