Hi,
I am a big fan of Quantlib and I have been using quantlib python to price thousands of callable bonds every day (from OAS to clean price and from price to OAS). I am still learning the library as I am not so familiar with C++ and online resources on python examples are somewhat limited, so I have to explore a lot of things myself.
Now, I have an issue with callable bond pricing. I am trying to compute the clean price of a real callable bond in the credit market, namely AES 6 05/15/26. I set the coupon rate to 10% instead of original 6% just to let you see the problem clearly on a larger scale.
This bond is near the call date (next call is on 5/15/21, in roughly 0.5 years, and the call price is 103) so if the spread is near 0 and the valuation date is today (11/3/2020), I would expect the bond to be priced around 108 as it is "priced to next call". This is also confirmed by Bloomberg. However as I shocked the OAS of the bond to 1bp, the price I got was actually 113.27, well above that. What happens I guess is that the quantlib is mistakenly pricing in one more coupon payment (there is only half a year left so only 1 semi-annual coupon before the call).
To replicate the bug in an even more straightforward way, I change the next call date to 11/10/2020, which is just 7 days from now, and the "clean price" I got based on 1bp of OAS is 108.19, still well above 103, which is the call price I had expected (again, it looks like one more coupon is priced in).
Magically, If I set the next call date to 11/9/2020, the price is finally consistent with my intuition, at 103.17, meaning by just changing the call date from 11/10/2020 to 11/9/2020, the clean price dropped 5pts! This strange behavior made me wonder if the cleanPriceOAS function is in fact computing the dirty price or something else.
I have posted my code below (you can run it directly). Could you please take a look and let me know if I am using the pricer in a correct way or there is actually a bug somewhere?
Any hint or suggestion are highly appreciated here!
import numpy as np
import pandas as pd
import QuantLib as ql
from datetime import datetime, timedelta
today = datetime.today()
dayCount = ql.Thirty360()
calendar = ql.UnitedStates()
interpolation = ql.Linear()
compounding = ql.Compounded
compoundingFrequency = ql.Semiannual
tenor = ql.Period(ql.Semiannual)
bussinessConvention = ql.Unadjusted
dateGeneration = ql.DateGeneration.Backward
monthEnd = False
class CallableBond(object):
#a wrapper I define to hold ql objects.
def __init__(self, issue_dt = None, maturity_dt = None, coupon = None, calldates = [], callprices = []):
self.issue_dt = issue_dt
self.maturity_dt = maturity_dt
self.callprices = callprices
self.calldates = calldates
self.coupon = coupon
self.today_dt = today
self.callability_schedule = ql.CallabilitySchedule()
for i, call_dt in enumerate(self.calldates):
callpx = self.callprices[i]
day, month, year = call_dt.day, call_dt.month, call_dt.year
call_date = ql.Date(day, month, year)
callability_price = ql.CallabilityPrice(callpx, ql.CallabilityPrice.Clean)
self.callability_schedule.append(ql.Callability(
callability_price,
ql.Callability.Call,
call_date))
def value_bond(self, a, s, grid_points):
model = ql.HullWhite(self.spotCurveHandle, a, s)
engine = ql.TreeCallableFixedRateBondEngine(model, grid_points, self.spotCurveHandle)
self.model = model
self.bond.setPricingEngine(engine)
return self.bond
def makebond(self, asofdate = today):
self.maturityDate = ql.Date(self.maturity_dt.day, self.maturity_dt.month, self.maturity_dt.year)
self.dayCount = dayCount
self.calendar = calendar
self.interpolation = interpolation
self.compounding = compounding
self.compoundingFrequency = compoundingFrequency
AsofDate = ql.Date(asofdate.day, asofdate.month, asofdate.year)
ql.Settings.instance().evaluationDate = AsofDate
self.asofdate = asofdate
self.spotRates = list(np.array([0.0811, 0.0811, 0.0864, 0.0938, 0.1167, 0.1545, 0.1941, 0.3749, 0.6235, 0.8434, 1.3858, 1.6163, 1.6163])/100)
self.spotDates = [ql.Date(3,11,2020), ql.Date(3,12,2020), ql.Date(1,2,2021), ql.Date(30,4,2021), ql.Date(3,11,2021), ql.Date(4,11,2022), ql.Date(3,11,2023), ql.Date(3,11,2025), ql.Date(4,11,2027), ql.Date(4,11,2030), ql.Date(2,11,2040), ql.Date(4,11,2050), ql.Date(3,11,2090)]
spotCurve_asofdate = ql.ZeroCurve(self.spotDates, self.spotRates, self.dayCount, self.calendar, self.interpolation, self.compounding, self.compoundingFrequency)
spotCurveHandle1 = ql.YieldTermStructureHandle(spotCurve_asofdate)
self.spotCurve = spotCurve_asofdate
self.spotCurveHandle = spotCurveHandle1
self.issueDate = ql.Date(self.issue_dt.day, self.issue_dt.month, self.issue_dt.year)
self.tenor = tenor
self.bussinessConvention = bussinessConvention
self.schedule = ql.Schedule(self.issueDate, self.maturityDate, self.tenor, self.calendar, self.bussinessConvention, self.bussinessConvention , dateGeneration, monthEnd)
self.coupons = [self.coupon]
self.settlementDays = 0
self.faceValue = 100
self.bond = ql.CallableFixedRateBond(
self.settlementDays, self.faceValue,
self.schedule, self.coupons, self.dayCount,
ql.Following, self.faceValue, self.issueDate,
self.callability_schedule)
self.value_bond(0.03, 0.012, 80)
def cleanPriceOAS(self, oas = None):
if np.isnan(oas):
return np.nan
px = self.bond.cleanPriceOAS(oas, self.spotCurveHandle, self.dayCount, self.compounding, self.compoundingFrequency, self.spotCurveHandle.referenceDate())
return px
if __name__ == '__main__':
issue_dt = datetime(2016, 5, 25)
maturity_dt = datetime(2026, 5, 15)
coupon = 10/100
calldates, callprices = [datetime(2020, 11, 9), datetime(2022, 5, 15), datetime(2023, 5, 15), datetime(2024, 5, 15)], [103, 102, 101, 100]
bond = CallableBond(issue_dt, maturity_dt, coupon, calldates, callprices)
bond.makebond(datetime.today())
print(bond.cleanPriceOAS(1/10000)) #computing the clean price for OAS=1bp, with call date 11/9/2020 would give 103.17