Skip to content
All library documents

Fixed-Rate Bond Coupon Amounts and Day-Count Conventions in QuantLib

Article Quant Q&A · Author: Gregmf90

Summary

The question concerns a QuantLib fixed-rate bond whose semiannual coupon cash flows vary slightly instead of equaling a constant half-year coupon. The code builds a bond schedule with dates adjusted to business days and uses Actual/365 Fixed for coupon accrual. The resulting accrued interest agrees with an internal reference, but the reported clean and dirty prices differ from expectations.

Responses distinguish coupon calculation conventions from accrued-interest conventions. For many fixed-coupon bonds, the periodic coupon is the annual rate divided by payment frequency, while day count is used to accrue interest between coupon dates. Actual/Actual may therefore change accrual fractions without producing the desired fixed cash flows. Suggested adjustments include using unadjusted schedule dates and a simple day counter, while keeping discounting dates appropriately aligned to business days. The replies present possible QuantLib settings, not a verified resolution; conventions depend on the instrument and market, and changing them can affect accrued interest as well as pricing.

Key ideas

  • Actual/365 Fixed can make scheduled coupon amounts vary with the length of each accrual period.
  • Many fixed-coupon bonds pay a constant coupon equal to the annual rate divided by payment frequency.
  • Coupon cash-flow conventions and accrued-interest day-count conventions may serve different purposes.
  • Unadjusted schedule dates and a simple day counter are suggested for fixed periodic coupons, but require market-convention checks.

Tags

Full text
# Pricing a fixed rate bond in Quantlib Python


# Pricing a fixed rate bond in Quantlib Python












I'm trying to implement a pricing model for fixed rate bonds with the code below.

```
import QuantLib as ql
import pandas as pd

todaysDate = ql.Date(31, 8, 2017)
ql.Settings.instance().evaluationDate = todaysDate

spotDates = [ql.Date(1,9,2017), ql.Date(5,9,2017), ql.Date(7,9,2017), ql.Date(14,9,2017),   ql.Date(21,9,2017), ql.Date(2,10,2017), ql.Date(31,10,2017), ql.Date(30,11,2017), ql.Date(2,1,2018), ql.Date(31,1,2018), ql.Date(28,2,2018), ql.Date(3,4,2018), ql.Date(30,4,2018)]
spotRates = [0.066682, 0.067199, 0.067502, 0.068526, 0.069462, 0.070742, 0.072984, 0.073566, 0.073174, 0.072844, 0.072610, 0.072467, 0.072366]

dayCount = ql.Actual365Fixed()
calendar = ql.SouthAfrica()
interpolation = ql.Linear()
compounding = ql.Compounded
compoundingFrequency = ql.Semiannual

spotCurve = ql.ZeroCurve(spotDates, spotRates, dayCount, calendar, 
interpolation, compounding, compoundingFrequency)
spotCurveHandle = ql.YieldTermStructureHandle(spotCurve)

issueDate = ql.Date(20, 4, 2009)
maturityDate = ql.Date(20, 4, 2018)
tenor = ql.Period(ql.Semiannual)
calendar = ql.SouthAfrica()
bussinessConvention = ql.Following
dateGeneration = ql.DateGeneration.Backward
monthEnd = False

schedule = ql.Schedule (issueDate, maturityDate, tenor, calendar, bussinessConvention, bussinessConvention, dateGeneration, monthEnd)

dayCount = ql.Actual365Fixed()
couponRate = 0.0925
coupons = [couponRate]

settlementDays = 3
faceValue = 100
fixedRateBond = ql.FixedRateBond(settlementDays, faceValue, schedule, coupons, dayCount)

bondEngine = ql.DiscountingBondEngine(spotCurveHandle)
fixedRateBond.setPricingEngine(bondEngine)

fixedRateBond.NPV()
print(fixedRateBond.NPV())
print(fixedRateBond.dirtyPrice())
print(fixedRateBond.cleanPrice())
print(fixedRateBond.accruedAmount())
print(fixedRateBond.dayCounter())
print(fixedRateBond.settlementDate())

for c in fixedRateBond.cashflows():
    print('%20s %12f' % (c.date(), c.amount()))
```

My cash flow schedule looks a bit strange, I would have expected values of 4.625.

```
October 20th, 2009     4.637671
April 20th, 2010       4.612329
October 20th, 2010     4.637671
April 20th, 2011       4.612329
October 20th, 2011     4.637671
April 20th, 2012       4.637671
October 22nd, 2012     4.688356
April 22nd, 2013       4.612329
October 21st, 2013     4.612329
April 22nd, 2014       4.637671
October 20th, 2014     4.586986
April 20th, 2015       4.612329
October 20th, 2015     4.637671
April 20th, 2016       4.637671
October 20th, 2016     4.637671
April 20th, 2017       4.612329
October 20th, 2017     4.637671
April 20th, 2018       4.612329
April 20th, 2018     100.000000
```

Model values produced are:

```
104.60163528858176
104.6774279539175
101.18016767994489
3.497260273972613
Actual/365 (Fixed) day counter
September 5th, 2017
```

The value I get for accrued interest is spot on with the values provided by our internal system, but the prices are a bit off. Clean price expectation is 100.81517 and Dirty price expectation is 104.31243

## Answer by bn_ln (score 6)

https://quant.stackexchange.com/a/46108

try:

`dayCount = ql.ActualActual(ql.ActualActual.ISMA,schedule)`

## Answer by Dimitri Vulis (score 1)

https://quant.stackexchange.com/a/46119

Your bond pays fixed 9.25% a year, twice a year. For most fixed-coupon bonds, the coupon is not "daycounted" - it should be exactly annual coupon / frequency = 4.625% (there are very few exceptions, like Mexican mbonos). The daycount is used if you need to calculate the accrued in the middle of the coupon period, e.g. to get a dirty price. For most fixed-coupon loans, lpns, fixed-copon legs of IR swaps, etc the market convention is to daycount the coupons, which is what your code does.

Just focusing on projecting the expected cash flows, I don't think changing the daycount convention to Actual / Actual would help. It would result in period fraction being 182/365 or 183/365 or (in leap year) /366, while you want exactly 1/2, irrespecive of holidays and number of days in various months. It would also produce different accrued during the coupon period, and you're satisfied with the accrued you have now.

I think (haven't tried) a way to get the desired cash flows from QL may be to change the bussinessConvention that you pass to the schedule from

bussinessConvention = ql.Following

to bussinessConvention = ql.Unadjusted

But then you do want the discount factors that you apply to your cash flows to be on business days (adjusted for weekends and holidays).

## Answer by Gustavo (score 0)

https://quant.stackexchange.com/a/54167

I've needed to do the exact same simple thing and found the solution to be to set both `daycount = SimpleDayCounter()` and `bussinessConvention = Unadjusted`

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.