Bootstrapping an EONIA OIS Curve in QuantLib
Summary
The document investigates why a QuantLib curve built from EONIA overnight index swaps differs from a Bloomberg curve. The accepted answer points to several setup choices that can affect calibration: using the appropriate fixed-leg payment frequency, selecting the EONIA rate rather than the deposit facility rate as the short-end input, and matching the compounding convention used to report zero rates. It supplies an alternative helper setup and illustrates checking the resulting curve by repricing OIS instruments and comparing their fair rates with the supplied market quotes.
The example is specific to the EONIA market and the quoted conventions and data in the question. It demonstrates how instrument conventions and curve construction choices can create differences, but does not establish that the revised setup will match every Bloomberg curve. A production implementation still needs market-specific calendars, settlement rules, dates, quote conventions, and curve settings to be verified against the relevant source.
Key ideas
- OIS curve calibration depends on using market-consistent swap conventions.
- The answer identifies fixed-leg payment frequency as a potential source of curve differences.
- The short-end input should reflect the overnight index rate rather than an unrelated facility rate.
- Reported zero rates should use a compounding convention consistent with the comparison curve.
- Repricing swaps against the constructed curve provides a check against input quotes.
Tags
Full text
# Bootstrapping OIS curve
# Bootstrapping OIS curve
I am trying to get a zero curve from a series of EONIA-based OIS rates with Quantlib. When comparing my output with Bloomberg, I find some differences (see at the end of the question), and and I don't get to find what I am doing wrong (It is my first time bootstraping OIS rates).
I went through the Quantlib documentation, and I tried to follow their method without luck. Then, if someone spots where the problem could be and lets me know, I would be super grateful!
```
today = ql.Date(22, 9, 2020)
ql.Settings.instance().evaluationDate = today
OIS_rate = [-0.47, -0.472, -0.4755, -0.481, -0.485, -0.489, -0.505, -0.519, -0.54, -0.552, -0.559, -0.5502, -0.5308, -0.5, -0.462, -0.4257, -0.382, -0.337, -0.2435, -0.1385, -0.056, -0.049, -0.078, -0.128, -0.1723]
terms = [1, 2, 3, 4, 5, 6, 9, 12, 18, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20, 25, 30, 40, 50]
calendar = ql.TARGET()
bussiness_convention = ql.Following
day_count = ql.Actual360()
#Overnigth Rate
depo_facility = -0.5
depo_helper = [ql.DepositRateHelper(ql.QuoteHandle(ql.SimpleQuote(depo_facility/100)), ql.Period(1,ql.Days), 1, calendar, ql.Unadjusted, False, day_count)]
settlement_days_EONIA = 1
EONIA = ql.OvernightIndex("EONIA", settlement_days_EONIA, ql.EURCurrency(),ql.TARGET(), day_count)
# Build OIS helpers
OIS_helpers = []
for i in range(len(terms)):
if i < 8:
coupon_frequency = ql.Once
tenor = ql.Period(terms[i],ql.Months)
rate = OIS_rate[i]
#OIS_helpers.append(ql.SwapRateHelper(ql.QuoteHandle(ql.SimpleQuote(rate/100.0)),tenor, calendar,coupon_frequency, bussiness_convention,day_count,ql.Euribor3M()))
OIS_helpers.append(ql.OISRateHelper(settlement_days_EONIA, tenor, ql.QuoteHandle(ql.SimpleQuote(rate/100)), EONIA))
elif i == 8:
coupon_frequency = ql.Semiannual
tenor = ql.Period(terms[i],ql.Months)
rate = OIS_rate[i]
OIS_helpers.append(ql.OISRateHelper(settlement_days_EONIA, tenor, ql.QuoteHandle(ql.SimpleQuote(rate/100)), EONIA))
else:
coupon_frequency = ql.Semiannual
tenor = ql.Period(terms[i],ql.Years)
rate = OIS_rate[i]
OIS_helpers.append(ql.OISRateHelper(settlement_days_EONIA, tenor, ql.QuoteHandle(ql.SimpleQuote(rate/100)), EONIA))
rate_helpers = depo_helper + OIS_helpers
yieldcurve = ql.PiecewiseLogCubicDiscount(today,rate_helpers,day_count)
spots = []
tenors = []
for d in yieldcurve.dates():
yrs = day_count.yearFraction(today, d)
compounding = ql.Simple
freq = ql.Semiannual
zero_rate = yieldcurve.zeroRate(yrs, compounding, freq)
tenors.append(yrs)
eq_rate = zero_rate.equivalentRate(day_count,compounding,freq,today,d).rate()
spots.append(100*eq_rate)
datatable = {'Dates':yieldcurve.dates(),'Tenors':tenors,'spots':spots}
df = pd.DataFrame.from_dict(datatable)
```
Hereunder the spot rates I get, compared to those found in Bloomberg <SWDF 133 8>
```
Tenor Bloomberg Output
1 MO -0.469 -0.47102
2 MO -0.4711 -0.472474
3 MO -0.4747 -0.475779
4 MO -0.4802 -0.48116
5 MO -0.4843 -0.485102
6 MO -0.4884 -0.489064
9 MO -0.5047 -0.504981
12 MO -0.519 -0.518946
18 MO -0.5397 -0.538899
2 YR -0.5519 -0.550289
3 YR -0.5589 -0.555675
4 YR -0.5503 -0.545571
5 YR -0.531 -0.525296
6 YR -0.5006 -0.494237
7 YR -0.463 -0.456508
8 YR -0.4271 -0.420624
9 YR -0.3837 -0.377845
10 YR -0.3391 -0.333913
12 YR -0.2459 -0.242628
15 YR -0.1405 -0.139172
20 YR -0.057 -0.056707
25 YR -0.0497 -0.049446
30 YR -0.0787 -0.077762
40 YR -0.1278 -0.124637
50 YR -0.1705 -0.163544
```
## Answer by David Duarte (score 8, accepted)
https://quant.stackexchange.com/a/58336
I see several problems that might explain those differences:
- The frequency of the fixed leg on a EONIA swap is Annual and not semi
- The deposit facility rate is not part of the EONIA curve. Use the Eonia rate.
- You are calculating rates with simple compounding and not annual compounding
Here is an alternative implementation:
```
tenors = [
'1D', '1W', '2W', '1M', '2M', '3M', '4M', '5M', '6M', '7M', '8M', '9M', '10M', '11M', '1Y',
'18M', '2Y', '30M', '3Y', '4Y', '5Y', '6Y', '7Y', '8Y', '9Y', '10Y', '11Y', '12Y',
'15Y', '20Y', '25Y', '30Y', '35Y', '40Y', '50Y']
rates = [
-0.467, -0.472, -0.47, -0.46, -0.471, -0.47, -0.481, -0.487, -0.5, -0.495, -0.5, -0.506,
-0.51, -0.515, -0.52, -0.541, -0.551, -0.556, -0.56, -0.551, -0.531, -0.5, -0.462, -0.426,
-0.379, -0.337, -0.293, -0.251, -0.147, -0.068, -0.055, -0.09, -0.099, -0.134, -0.172]
eonia = ql.Eonia()
helpers = []
for tenor, rate in zip(tenors,rates):
if tenor == '1D':
helpers.append( ql.DepositRateHelper(rate / 100, eonia ) )
else:
helpers.append( ql.OISRateHelper(2, ql.Period(tenor), ql.QuoteHandle(ql.SimpleQuote(rate/100)), eonia) )
eonia_curve = ql.PiecewiseLogCubicDiscount(0, ql.TARGET(), helpers, ql.Actual365Fixed())
discount_curve = ql.YieldTermStructureHandle(eonia_curve)
swapEngine = ql.DiscountingSwapEngine(discount_curve)
```
You can then create OIS Swap instruments to get their fair rate.
```
overnightIndex = ql.Eonia(discount_curve)
for tenor, rate in zip(tenors, rates):
if tenor == '1D': continue
ois_swap = ql.MakeOIS(ql.Period(tenor), overnightIndex, 0.01, pricingEngine=swapEngine)
print(f"{tenor}\t{ois_swap.fairRate():.4%}\t{rate:.4f}%")
```
1W -0.4720% -0.4720% 2W -0.4700% -0.4700% 1M -0.4600% -0.4600% 2M -0.4710% -0.4710% 3M -0.4700% -0.4700% 4M -0.4810% -0.4810% 5M -0.4870% -0.4870% 6M -0.5000% -0.5000% 7M -0.4950% -0.4950% 8M -0.5000% -0.5000% 9M -0.5060% -0.5060% (...) 10Y -0.3370% -0.3370% 11Y -0.2930% -0.2930% 12Y -0.2510% -0.2510% 15Y -0.1470% -0.1470% 20Y -0.0680% -0.0680% 25Y -0.0550% -0.0550% 30Y -0.0900% -0.0900% 35Y -0.0990% -0.0990% 40Y -0.1340% -0.1340% 50Y -0.1720% -0.1720%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.