5
$\begingroup$

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

$\endgroup$
5
  • $\begingroup$ QuantLib can't be possibly wrong here. It's 100% correct. Either your internal system is wrong or your inputs are not consistent. Can you show a manual calculation why you think you should have 4.625? $\endgroup$
    – ABCD
    Oct 20, 2017 at 13:01
  • 2
    $\begingroup$ It's correct, given the inputs. For instance, the calculation of the coupons is correct based on the Act/365 day count convention passed during constructions. If your coupons pay 4.625 in real life, it means they're probably using an Act/Act convention and you should pass the latter to the FixedRateBond constructor. $\endgroup$ Oct 21, 2017 at 12:42
  • $\begingroup$ Okay, the coupon value being exact here is not the major concern, since my accrued interest amounts are exactly in line therefore my day count convention must be correct. My main concern would be why the clean price and all in price are not in line with my expectations. Is my use of the zero spot rate curve on the dates I've specified correct? $\endgroup$
    – Gregmf90
    Oct 23, 2017 at 7:52
  • $\begingroup$ If the final coupon amounts are different from what you expect, the accrued amounts will eventually be wrong as well; the conventions might just happen to give the same result on today's date. You might want to try initializing the bond with a different day counter and see what happens to the price. $\endgroup$ Oct 24, 2017 at 11:00
  • $\begingroup$ As for the zero-rate curve: the initialization and usage is correct, as far as syntax and semantics are concerned. You'll have to verify if your system actually uses linear interpolation and semiannually-compounded rates, as for the inputs. It would help if you could retrieve the discounts corresponding to each coupon. $\endgroup$ Oct 24, 2017 at 11:00

3 Answers 3

5
$\begingroup$

try:

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

$\endgroup$
1
$\begingroup$

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).

$\endgroup$
0
$\begingroup$

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

$\endgroup$

Your Answer

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

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