This is somewhat related to the question I asked here but simpler. I am trying to bootstrap a yield curve from swaps, and am having a problem with the dates/maturities that are coming out. The code I'm using is below, and the issue that I'm having is that when the results are returned the Maturities field is not matching the maturities I'm inputting.
import QuantLib as ql
from pandas import DataFrame
import matplotlib.pyplot as plt
import csv
def get_spot_rates(yieldcurve, day_count, calendar=ql.UnitedStates(), months=121):
spots = []
tenors = []
ref_date = yieldcurve.referenceDate()
calc_date = ref_date
for yrs in yieldcurve.times():
d = calendar.advance(ref_date, ql.Period(int(yrs*365.25), ql.Days))
compounding = ql.Compounded
freq = ql.Semiannual
zero_rate = yieldcurve.zeroRate(yrs, compounding, freq)
tenors.append(yrs)
eq_rate = zero_rate.equivalentRate(day_count,compounding,freq,calc_date,d).rate()
spots.append(eq_rate*100)
return DataFrame(list(zip(tenors, spots)),columns=["Maturities","Curve"],index=['']*len(tenors))
swap_maturities = [ql.Date(9,9,2016),
ql.Date(15,9,2016),
ql.Date(22,9,2016),
ql.Date(29,9,2016),
ql.Date(11,10,2016),
ql.Date(9,11,2016),
ql.Date(8,12,2016),
ql.Date(10,1,2017),
ql.Date(8,2,2017),
ql.Date(8,3,2017),
ql.Date(8,6,2017),
ql.Date(8,9,2017),
ql.Date(8,3,2018),
ql.Date(10,9,2018),
ql.Date(10,9,2019),
ql.Date(10,9,2020),
ql.Date(9,9,2021),
ql.Date(8,9,2022),
ql.Date(8,9,2023),
ql.Date(10,9,2024),
ql.Date(10,9,2025),
ql.Date(10,9,2026),
ql.Date(8,9,2028),
ql.Date(10,9,2031),
ql.Date(10,9,2036),
ql.Date(10,9,2041),
ql.Date(10,9,2046),
ql.Date(8,9,2056)
]
swap_periods = [ql.Period(1,ql.Days),
ql.Period(1,ql.Weeks),
ql.Period(2,ql.Weeks),
ql.Period(3,ql.Weeks),
ql.Period(1,ql.Months),
ql.Period(2,ql.Months),
ql.Period(3,ql.Months),
ql.Period(4,ql.Months),
ql.Period(5,ql.Months),
ql.Period(6,ql.Months),
ql.Period(9,ql.Months),
ql.Period(1,ql.Years),
ql.Period(18,ql.Months),
ql.Period(2,ql.Years),
ql.Period(3,ql.Years),
ql.Period(4,ql.Years),
ql.Period(5,ql.Years),
ql.Period(6,ql.Years),
ql.Period(7,ql.Years),
ql.Period(8,ql.Years),
ql.Period(9,ql.Years),
ql.Period(10,ql.Years),
ql.Period(12,ql.Years),
ql.Period(15,ql.Years),
ql.Period(20,ql.Years),
ql.Period(25,ql.Years),
ql.Period(30,ql.Years),
ql.Period(40,ql.Years)
]
swap_rates = [0.37,
0.4025,
0.4026,
0.399,
0.3978,
0.4061,
0.41,
0.4155,
0.4273,
0.4392,
0.461,
0.4805,
0.5118,
0.538,
0.587,
0.638,
0.7,
0.756,
0.818,
0.865,
0.913,
0.962,
1.045,
1.137,
1.2355,
1.281,
1.305,
1.346
]
""" Parameter Setup """
calc_date = ql.Date(1,9,2016)
ql.Settings.instance().evaluationDate = calc_date
calendar = ql.UnitedStates()
bussiness_convention = ql.ModifiedFollowing
day_count = ql.Actual360()
coupon_frequency = ql.Annual
""" SwapRateHelper """
swap_helpers = []
for rate,tenor in list(zip(swap_rates,swap_periods)):
swap_helpers.append(ql.SwapRateHelper(ql.QuoteHandle(ql.SimpleQuote(rate/100.0)),
tenor, calendar,
coupon_frequency, bussiness_convention,
day_count,
ql.Euribor3M()))
rate_helpers = swap_helpers
yc_linearzero = ql.PiecewiseLinearZero(calc_date,rate_helpers,day_count)
yc_cubiczero = ql.PiecewiseCubicZero(calc_date,rate_helpers,day_count)
max_maturity = 40*12
splz = get_spot_rates(yc_linearzero, day_count, months=max_maturity + 1)
spcz = get_spot_rates(yc_cubiczero, day_count, months=max_maturity + 1)
max_rate = swap_rates[-1]
min_rate = min(splz.Curve)
max_rate = max(splz.Curve)
"""Plotting"""
plt.plot(splz["Maturities"],splz["Curve"],'--', label="LinearZero")
plt.plot(spcz["Maturities"],spcz["Curve"],label="CubicZero")
plt.xlabel("Years", size=12)
plt.ylabel("Zero Rate", size=12)
plt.xlim(0,max_maturity/12.0)
plt.ylim([min_rate * 0.9,max_rate * 1.1])
plt.legend()
plt.show()
rows = zip(splz.Maturities,splz.Curve)
with open('OISBootstrap.csv','w',newline='') as f:
writer = csv.writer(f)
for row in rows:
writer.writerow(row)
Anyone with Python and QL can run the whole thing and see the results, but for example I'm getting the following values for the last five Maturities: 15.2361111111, 20.3111111111, 25.3777777778, 30.45, 40.5972222222 instead of 15, 20, 25, 30, and 40.
Thanks in advance for what I assume is a pretty newb question.
Answer
15 years does correspond to t=15.236 according to the day counter you told the curve to use.
First, you can't get exactly 15 anyway. Your calculation date is September 1st, 2016; according to the usual conventions, the swap whose rate you're quoting starts spot, that is, two business days after the calculation date. Given the weekend (your calculation date is a Thursday) and the holiday on Labor Day (September 4th) this lands you on September 6th. Thus, the 15-years swap would have its maturity on September 6th, 2031; but that's a Saturday, so it rolls to September 8th, 2031 (also, your input maturity is September 10th, so you might want to check that, too). That's 15 years and a week after your calculation date, which is also t=0 for your curve (since you're passing it to its constructor as reference date).
Second: you're passing to the curve a day count convention of Actual/360, for which one year doesn't correspond to 1, but to 365/360=1.01389 (or 366/360 on a leap year).
Putting all together, you get that the 5485 days between the calculation date and the maturity of the 15 years swap correspond to 5485/360=5.23611. Similar calculations apply to the other maturities.
Using Actual/365 (Fixed) gives results closer to what you would expect, since it corresponds to t=1 for a "regular" year; but since leap years correspond to 366/365, you still get t=5485/365=15.0274.
Other day counters have better properties for calculating rates (for instance, Act/Act gives you t=1 for 1 year) but I don't advise using them in term structures, since they lack other properties; for instance, there exist pairs of dates $d_1$ and $d_2$ for which the time $T(d_1,d_2)$ between them is 0, or triples $d_1$, $d_2$, $d_3$ for which $T(d_1,d_3) \neq T(d_1,d_2) + T(d_2,d_3)$.
What I suggest instead is that you work with dates directly. In your get_spot_rates
function, instead of trying to recalculate the maturity dates as
d = calendar.advance(ref_date, ql.Period(int(yrs*365.25), ql.Days))
call yieldcurve.nodes()
, which gives you the list of pairs (date,rate) corresponding to each maturity. You can use those dates in the call to equivalentRate
, as you already do, and also in the call to yieldcurve.zeroRate
.
(And by the way, calendar.advance(ref_date, 365, Days)
doesn't do what you think. It advances the date by 365 business days, which is a lot more than one year.)
No comments:
Post a Comment