Canadian Bond Pricing: Accrued Interest and Yield Conventions
Summary
The document investigates why QuantLib and Bloomberg produce different clean prices for a Canadian fixed-rate corporate bond. It explains that Canadian bond calculations can use ACT/365 Fixed for settlement accrued interest while using ACT/ACT ISMA for yield calculations. In the final coupon period, the cited discussion further identifies a switch to simple yield with an ACT/365 Fixed time fraction, which accounts for the pricing discrepancy in the example.
The answers compare manual calculations and a rateslib result, showing that convention details and the treatment of accrued interest affect the quoted price. The example is specific to a Canadian bond near maturity; it is not a general pricing recipe for all fixed-income instruments. The initial answer’s compounded-yield calculation is explicitly corrected in an edit, so readers should follow the later simple-yield explanation for this case and verify conventions against the applicable bond documentation.
Key ideas
- Canadian bond accrued interest and yield calculations can use different day-count conventions.
- The example uses ACT/365 Fixed for settlement accrual and ACT/ACT ISMA for yield calculations.
- The final coupon period may require simple yield under an ACT/365 Fixed convention.
- Price comparisons depend on matching the instrument’s conventions and accrued-interest treatment.
- The worked example concerns one Canadian bond near maturity.
Tags
Full text
# QuantLib calculations for a Canadian corporate fixed rate bond differ from BBG YAS
# QuantLib calculations for a Canadian corporate fixed rate bond differ from BBG YAS
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
```
## Answer by Denys Usynin (score 4)
https://quant.stackexchange.com/a/78333
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}")
```
## Answer by Attack68 (score 2)
https://quant.stackexchange.com/a/78349
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.Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.