2
$\begingroup$

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
$\endgroup$
5
$\begingroup$

I see several problems that might explain those differences:

  1. The frequency of the fixed leg on a EONIA swap is Annual and not semi
  2. The deposit facility rate is not part of the EONIA curve. Use the Eonia rate.
  3. 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%

| improve this answer | |
$\endgroup$

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.