3
$\begingroup$

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.

$\endgroup$
2
$\begingroup$

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

$\endgroup$
  • $\begingroup$ Also, I have a nagging feeling that the calculation inside get_spot_rates could be simpler, but I'm not quite sure what rates you're calculating exactly... $\endgroup$ – Luigi Ballabio Nov 19 '16 at 17:55
  • $\begingroup$ Actually, the get_spot_rates part of the code was taken directly from your book with Goutham on page 76. I tried modifying it to return the maturities I care about instead of monthly. I'll go back and play with it some more. To be completely honest, as I'm still learning a lot of the methodologies behind the code much of what I work on is based on things in the book. Like on my previous question that I referenced, you've been a fantastic help, Luigi. $\endgroup$ – JVP3122 Nov 21 '16 at 3:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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