3
$\begingroup$

I am pricing a non-callable, fixed-rate, Canadian corporate bond with the following parameters:

Name Value
CUSIP 12657ZAT0
Evaluation Date 2/14/2024
Settlement Date 2/16/2024
Bond Issue Date 3/6/2009
Maturity Date 3/6/2024
Coupon Rate 6.215%
Coupon Frequency 2
Day Count ACT/ACT
Face Value 1000
Yield 5.437768%

I can match Bloomberg's accrued days & accrued amount but I have to use a different day count when constructing my FixedRateBond object (see code below). This is because Bloomberg uses ACT/365 when calculating accrued values and ACT/ACT for the rest.

However, I am unable to match the clean price: BBG Price: 100.041 QuantLib Price: 100.021973

I suspect that because I am using ACT/ACT as a parameter in the call to bond.cleanPrice(...), the NPV isn't correctly calculated since it doesn't use ACT/365 for the first period. I could entirely be wrong about this theory.

Is my line of thinking correct? Perhaps I have a flaw in my logic below? Am I missing a parameter somewhere or not using QuantLib correctly?

I'm very new to the world of fixed income and QuantLib so please bear with me. Thanks in advance!

Here is my code for reference:

import QuantLib as ql

SETTLEMENT_DAYS = 2
FACE_VALUE = 1000
DAY_COUNT = ql.ActualActual(ql.ActualActual.ISMA)
ACC_DAY_COUNT = ql.Actual365Fixed(ql.Actual365Fixed.NoLeap) # Note - Accrued values only match when using this day count

evaluationDate = ql.Date(14, 2, 2024)
issueDate = ql.Date(6, 3, 2009)
maturityDate = ql.Date(6, 3, 2024)
yld = 0.05437768
coupon = 0.06215
freq = ql.Period("6M")

ql.Settings.instance().evaluationDate = evaluationDate

bond = ql.FixedRateBond(
    SETTLEMENT_DAYS,
    ql.TARGET(),
    FACE_VALUE,
    issueDate,
    maturityDate,
    freq,
    [coupon],
    ACC_DAY_COUNT,
    ql.Unadjusted,
    ql.Unadjusted,
)

px = bond.cleanPrice(
    yld,
    DAY_COUNT,
    ql.CompoundedThenSimple,
    ql.Semiannual,
    evaluationDate,
)

# Print prices to compare
print(f"BBG Price: {100.041}")
print(f"QuantLib Price: {px:.6f}\n")

# Print details about accrued interest
print(f"Accrual Period: {ql.BondFunctions.accrualPeriod(bond)}")
print(f"Accrual Start: {ql.BondFunctions.accrualStartDate(bond)}")
print(f"Accrual End: {ql.BondFunctions.accrualEndDate(bond)}")
print(f"Accrual Days: {ql.BondFunctions.accrualDays(bond)}")

print(f"Accrued Amount: {ql.BondFunctions.accruedAmount(bond)}") # Matches BBG
print(f"Accrued Period: {ql.BondFunctions.accruedPeriod(bond)}")
print(f"Accrued Days: {ql.BondFunctions.accruedDays(bond)}") # Matches BBG
$\endgroup$

2 Answers 2

4
$\begingroup$

The tricky thing about Canadian bonds is that they use ACT/365F convenction for accrued interest for settlement, but ACT/ACT(ISMA) for accrued interest in yield calculations. Which means in QuantLib you need to construct two different bond objects if you want to perform these two calculations.

The details are available in government's reference doc

[EDIT]

see comments below, section 10.10.1 mentions that in the last coupon period the formula switches to simple yield with ACT/365F convention. That calculation does produce 100.041. Thanks to @Attack68 for spotting.

This is how you can confirm:

import QuantLib as ql
prevCouponDate =ql.Date(6, 9, 2023)
settlementDate = ql.Date(16, 2, 2024)
maturityDate = ql.Date(6, 3, 2024)
yld = 0.05437768
coupon = 0.06215
dc = ql.Actual365Fixed()
frac_ai = dc.yearFraction(prevCouponDate, settlementDate, prevCouponDate, maturityDate)
frac_y = dc.yearFraction(settlementDate, maturityDate, prevCouponDate, maturityDate)
dp_simple = 100.0 * (1.0 + coupon / 2) / (1 + yld * frac_y)
ai = 100*  coupon * frac_ai
cp_s = dp_simple - ai
print(f"clean price simple {cp_s}")

therefore ignore the code below which uses compounded formula

[/EDIT]

In case of this bond given that there is only coupon left it is very easy to manually perform the relevant calculation, using the formula from section 10.1:

prevCouponDate =ql.Date(6, 9, 2023)
settlementDate = ql.Date(16, 2, 2024)
maturityDate = ql.Date(6, 3, 2024)
yld = 0.05437768
coupon = 0.06215
dcc = maturityDate - prevCouponDate
dcs = settlementDate - prevCouponDate
dsc = maturityDate - settlementDate
dp = 100.0 * (1.0 + coupon / 2) * (1+yld/2)**(-dsc/dcc)
ap = dcs / dcc / 2
ai = 100*  coupon * ap
cp = dp - ai
print(f"clean price {cp}")
$\endgroup$
3
  • $\begingroup$ Thanks for the reference doc! I'll try the 2 different bond approaches now and see if that helps. Also, your code above still seems to result in a different clean price than what I would expect to see. I get 100.03606130113818 when I am expecting to see 100.041. Am I missing something here? $\endgroup$
    – Juice
    Commented Feb 15 at 15:21
  • $\begingroup$ Actually section 10.10.1 mentions that in the last coupon period the formula switches to simple yield with ACT/365F convention. That calculation does produce 100.041. Thanks to @Attack68 for spotting. $\endgroup$ Commented Feb 17 at 11:09
  • $\begingroup$ Thanks for the edit, this was indeed the issue! $\endgroup$
    – Juice
    Commented Feb 17 at 21:04
2
$\begingroup$

For what its worth your comment mentions @DenysUsynin anwser returns 100.03606.

rateslib gets the same result. It uses a special convention for Canadian bonds called "ActActICMA_stub365f".

from rateslib import *

frb = FixedRateBond(effective=dt(2009, 3, 6), termination=dt(2024, 3, 6),
                    spec="cadgb", fixed_rate=6.215)

frb.accrued(dt(2024, 2, 16))                                     # 2.77546
frb.price(ytm=5.437768, settlement=dt(2024, 2, 16))              # 100.036061 
frb.price(ytm=5.437768, settlement=dt(2024, 2, 16), dirty=True)  # 102.819151

Unhelpfully here if you take the dirty price and subtract the accrued you end up with 100.04369, which is also caused by the problematic different accrued calculations during an accrued interest calculation or a yield to maturity calculation.

$\endgroup$
5
  • $\begingroup$ "which is also caused by the problematic different accrued calculations during an accrued interest calculation or a yield to maturity calculation" -- can you please expand on this? As far as I know, there's only 1 formula when it comes to pricing a semi-annual, regular coupon rate bond (see page 30 on iiac-accvm.ca/wp-content/uploads/…). I'm not sure how Bloomberg and another similar software I use yield the same result as one another but don't match QuantLib or Excel. $\endgroup$
    – Juice
    Commented Feb 16 at 14:37
  • $\begingroup$ Yes there is only one formula for Canadian government bonds, but it is unconventional and non-standard. When designing software to do this for all the bonds of the world with all their conventions means decisions have to be made how to code them without specifically duplicating code for every single one. The issue have I pointed out is directly related to this design pattern not ideally suiting the CadGB scenario. Although, note that 100.36061 is the clean price obtained in Rateslib and according to the formula in section 10.1 of the document. $\endgroup$
    – Attack68
    Commented Feb 16 at 15:09
  • 2
    $\begingroup$ In Bloomberg it specifically states that for bonds settling in the final period it uses a simple money market yield calculation. In which case the formula that gets applied to this bond is 10.10.1. Rateslib and Quantlib do not appear to be making this change. This is becuase rateslib and quantlib are asserting that a "yield-to-maturity" is required as entry to the pricing function. The value fo 5.437768 is not a yield-to-maturity, but it is a simply money-market yield and therefore requires a different formula or a different construction. $\endgroup$
    – Attack68
    Commented Feb 16 at 15:18
  • 1
    $\begingroup$ Fixed income conventions.... $\endgroup$
    – AKdemy
    Commented Feb 17 at 11:26
  • 2
    $\begingroup$ This is precisely the answer I was looking for. Thank you @Attack68! $\endgroup$
    – Juice
    Commented Feb 17 at 21:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.