Swap Curve Maturities Depend on Date and Day-Count Conventions
Summary
This QuantLib example explains why maturities reported from a bootstrapped swap curve can differ from the tenor labels supplied as inputs. The curve's time coordinate is calculated using its reference date and day-count convention, so a quoted fifteen-year swap need not map to exactly fifteen curve years. Spot-start conventions, business-day adjustments, weekends, and holidays can also shift the actual maturity date from the nominal tenor date.
The answer traces the discrepancy to the example's Actual/360 convention and recommends retrieving the curve's date-rate nodes directly when producing spot rates. Actual/365 Fixed may give values closer to calendar years, but leap years still affect the result. The explanation cautions that alternative day counters have tradeoffs for term structures and that advancing by a specified number of business days is not the same as advancing by a calendar year. The lesson concerns date handling and curve representation, not a comparison of calibration accuracy or market performance.
Key ideas
- A curve's maturity time reflects its reference date and chosen day-count convention.
- Spot-start rules and business-day adjustments can move swap maturity dates beyond nominal tenor dates.
- Actual/360 measures calendar days against a 360-day year, so its curve times differ from calendar-year labels.
- Use the curve's own date-rate nodes to preserve the maturity dates associated with its rates.
- Advancing a calendar by a number of business days does not produce the same date as advancing by that many calendar days.
Tags
Full text
# QuantLib Python Swap Yield Curve Bootstrapping Dates and Maturities
# QuantLib Python Swap Yield Curve Bootstrapping Dates and Maturities
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 by Luigi Ballabio (score 4, accepted)
https://quant.stackexchange.com/a/31082
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.)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.