Correct Futures Quotes in QuantLib Yield Curve Bootstrapping
Summary
This document explains a common input error when bootstrapping a yield curve with QuantLib futures helpers. The key point is that FuturesRateHelper expects the market futures price, quoted as 100 minus the implied rate, rather than a rate converted into decimal form. Passing a converted rate can distort the curve around futures maturities, even when deposit and swap inputs are scaled appropriately for their helpers.
The accepted answer recommends supplying the futures quotes directly and notes that the helper’s name can make its expected input unclear. The example concerns a curve built from deposits, Eurodollar-style futures, and swaps, with convexity adjustments. It does not compare interpolation methods or quantify the corrected curve’s accuracy, so the takeaway is limited to quote conventions for this helper; users should still check the conventions and units expected by each instrument helper.
Key ideas
- FuturesRateHelper expects futures prices quoted as 100 minus the implied rate.
- Do not convert those futures prices into decimal rates before passing them to the helper.
- Other rate helpers may require percentage rates to be converted to decimals, so input conventions differ.
- A futures quote convention mismatch can produce implausible curve values near futures maturities.
Tags
Full text
# QL-Python Bootstrapping Yield Curve FuturesRateHelper throwing off results
# QL-Python Bootstrapping Yield Curve FuturesRateHelper throwing off results
I'm trying to perform a bootstrap of a yield curve from deposit rates, futures, and swaps, and the interpolation is "blowing up" for the futures maturities being off by two orders of magnitude (100x). I'm not sure what I'm doing wrong, if anything, or if there's an incorrect assumption I'm making somewhere. I know some of the code is superfluous, such as having start and maturity dates for the three types if I'm also including explicit periods, but I didn't have much chance to clean it up. I should also note that I tried changing future_rate_val to be simply the price instead of 100-price, but I got a similarly erroneous result, which is somewhat troubling in and of itself. Any help would be appreciated.
``
```
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 month in range(0, months):
yrs = month/12.0
d = calendar.advance(ref_date, ql.Period(month, ql.Months))
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))
""" Deposit Rates """
depo_starts = [ql.Date(1,9,2016),ql.Date(2,9,2016),ql.Date(6,9,2016),ql.Date(6,9,2016),ql.Date(6,9,2016)]
depo_maturities = [ql.Date(2,9,2016),ql.Date(6,9,2016),ql.Date(6,10,2016),ql.Date(7,11,2016),ql.Date(6,12,2016)]
depo_periods = [ql.Period(1,ql.Days),ql.Period(2,ql.Days),ql.Period(1,ql.Months),ql.Period(2,ql.Months),ql.Period(3,ql.Months)]
depo_rates = [0.45, 0.48, 0.51765, 0.6328, 0.8176]
""" Futures """
future_starts = [ql.Date(21,9,2016),ql.Date(21,12,2016),ql.Date(15,3,2017),ql.Date(21,6,2017),ql.Date(20,9,2017),ql.Date(20,12,2017),ql.Date(21,3,2018),ql.Date(20,6,2018),ql.Date(19,9,2018),ql.Date(19,12,2018),ql.Date(20,3,2019),ql.Date(19,6,2019)]
future_maturities = [ql.Date(21,12,2016),ql.Date(21,3,2017),ql.Date(15,6,2017),ql.Date(21,9,2017),ql.Date(20,12,2017),ql.Date(20,3,2018),ql.Date(21,6,2018),ql.Date(20,9,2018),ql.Date(19,12,2018),ql.Date(19,3,2019),ql.Date(22,6,2019),ql.Date(19,9,2019)]
future_rates = [99.13,99.095,99.075,99.045,99.015,98.975,98.955,98.93,98.9,98.855,98.83,98.8]
""" Swaps """
swap_maturities = [ql.Date(8,9,2020),ql.Date(7,9,2021),ql.Date(6,9,2022),ql.Date(6,9,2023),ql.Date(6,9,2024),ql.Date(8,9,2025),ql.Date(8,9,2026),ql.Date(6,9,2028),ql.Date(8,9,1931),ql.Date(8,9,1936),ql.Date(6,9,1941),ql.Date(6,9,1946),ql.Date(6,9,1956),ql.Date(6,9,1966)]
swap_periods = [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),ql.Period(50,ql.Years)]
swap_rates = [1.06329999999999,1.12319999999999,1.1825,1.24150000000001,1.2975,1.349,1.3965,1.484,1.579,1.68250000000001,1.73049999999999,1.75749999999999,1.77579999999999,1.76000000000001]
""" 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
""" DepositRateHelper """
depo_helpers = []
for i in range(len(depo_rates)):
depo_helpers.append(ql.DepositRateHelper(ql.QuoteHandle(ql.SimpleQuote(depo_rates[i]/100)),depo_periods[i],3,ql.TARGET(),ql.ModifiedFollowing,False,ql.Actual360()))
""" FuturesRateHelper """
# Convexity Adjustments
conv_adjust = [0,0.0001,0.0003,0.0007,0.0013,0.0027,0.005,0.0078,0.0112,0.0158,0.0211,0.0277]
convexity_quote = []
for i in range(len(conv_adjust)):
convexity_quote.append(ql.SimpleQuote(conv_adjust[i]/100.0))
imm = ql.IMM.nextDate(calc_date)
futures_helpers = []
for i in range(len(future_rates)):
future_rate_val = (100.0-future_rates[i])/100.0
futures_helpers.append(ql.FuturesRateHelper(ql.QuoteHandle(ql.SimpleQuote(future_rate_val)),imm,ql.Euribor3M(),ql.QuoteHandle(convexity_quote[i])))
imm = ql.IMM.nextDate(imm)
""" 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 = depo_helpers + futures_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 = 50*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(splcd["Maturities"],splcd["Curve"], '.',label="LogCubicDiscount")
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('LIBORBootstrap.csv','w',newline='') as f:
writer = csv.writer(f)
for row in rows:
writer.writerow(row)
```
## Answer by Luigi Ballabio (score 3, accepted)
https://quant.stackexchange.com/a/30973
The FuturesRateHelper class knows that futures are quoted as 100-rate, so there's no need to convert the prices. You can just create them as
```
futures_helpers.append(ql.FuturesRateHelper(
ql.QuoteHandle(ql.SimpleQuote(future_rates[i])),
imm,ql.Euribor3M(),ql.QuoteHandle(convexity_quote[i])))
```
I admit that the name of the class can lead one astray by mentioning rates instead of prices. Also, the other helpers might suggest that the division by 100 is needed, but that is only because the rates are given as, say, 0.43% when they are really 0.0043.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.