Swap Bootstrapping with quantlib

I've been trying to get the zero rates of the Chilean Swap curve with Quantlib in Python, but I haven't been able to set up the parameters correctly. This is my code:

import QuantLib as ql
import pandas as pd

#Custom Calendar with Chilean Holidays
def create_calendar_chile(start_year,n_years):
Chile = ql.WeekendsOnly()
days = [1,14,15,1,21,26,2,16,15,18,19,9,27,1,19,8,17,25,31]
months = [1,4,4,5,5,6,8,9,9,10,10,11,12,12,12,12]
name = ['Año Nuevo','Viernes Santo','Sabado Santo','Dia del Trabajo','Dia de las Glorias Navales','San Pedro y San Pablo','Elecciones Primarias','Dia de la Virgen del Carmen','Asuncion de la Virgen','Independencia Nacional','Glorias del Ejercito','Encuentro de dos mundos','Día de las Iglesias Evangélicas y Protestantes','Día de todos los Santos','Elecciones Presidenciales y Parlamentarias','Inmaculada Concepción','Segunda vuelta Presidenciales','Navidad','Feriado Bancario']
for i in range(n_years+1):
for x,y in zip(days,months):
date = ql.Date(x,y,start_year+i)
Chile.addHoliday(date)

return Chile

today = ql.Date(25, 10, 2017)
ql.Settings.instance().evaluationDate = today

swap_clp = [2.46, 2.40, 2.40, 2.41, 2.54, 2.68, 3.01, 3.3, 3.53, 3.69, 3.87, 4.02, 4.13, 4.23, 4.38, 4.38, 4.56]
terms = [3,6,9,12,18,2,3,4,5,6,7,8,9,10,12,15,20]
## SWAP Parameters ##
calendar = create_calendar_chile(2001,50)
bussiness_convention = ql.Following
day_count = ql.Actual360()

#Overnigth Rate
TPM = 2.5
depo_helper = [ql.DepositRateHelper(ql.QuoteHandle(ql.SimpleQuote(TPM/100)),ql.Period(1,ql.Days),1,calendar,ql.Unadjusted,False,ql.Actual360())]

#Swap Rates
swap_helpers = []
for i in range(len(terms)):
if i < 4:
coupon_frequency = ql.Once
tenor = ql.Period(terms[i],ql.Months)
rate = swap_clp[i]
swap_helpers.append(ql.SwapRateHelper(ql.QuoteHandle(ql.SimpleQuote(rate/100.0)),tenor, calendar,coupon_frequency, bussiness_convention,day_count,ql.Euribor3M()))
else:
coupon_frequency = ql.Semiannual
tenor = ql.Period(terms[i],ql.Years)
rate = swap_clp[i]
swap_helpers.append(ql.SwapRateHelper(ql.QuoteHandle(ql.SimpleQuote(rate/100.0)),tenor, calendar,coupon_frequency, bussiness_convention,day_count,ql.Euribor3M()))

#Yield Curve
rate_helpers = depo_helper + swap_helpers
yieldcurve = ql.PiecewiseLinearZero(today,rate_helpers,day_count)

spots = []
tenors = []
for d in yieldcurve.dates():
yrs = day_count.yearFraction(today, d)
compounding = ql.Simple
freq = ql.Annual
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)


And I'm getting the following results:

>>> df
Dates     Tenors     spots
0   October 25th, 2017   0.000000  0.000000
1   October 27th, 2017   0.005556  2.500087
2   January 29th, 2018   0.266667  2.461170
3     April 27th, 2018   0.511111  2.401418
4      July 27th, 2018   0.763889  2.401059
5   October 29th, 2018   1.025000  2.410821
6   October 28th, 2019   2.036111  2.739493
7   October 27th, 2020   3.050000  3.141727
8   October 27th, 2021   4.063889  3.529495
9   October 27th, 2022   5.077778  3.875600
10  October 27th, 2023   6.091667  4.157661
11  October 28th, 2024   7.111111  4.495323
12  October 27th, 2025   8.122222  4.817217
13  October 27th, 2026   9.136111  5.100542
14  October 27th, 2027  10.150000  5.390948
15  October 29th, 2029  12.186111  5.945168
16  October 27th, 2032  15.225000  6.352031
17  October 29th, 2035  18.272222  2.707640
18  October 27th, 2037  20.297222  8.655828


The zero rates are wrong according to Bloomberg boostrap (leaving aside smalls differences in the rates and dates). I'm using the Euribor3M index in the Swap Helpers but I assume thats wrong. How could I set up a custom index in python? Also, the zero levels seem to be kind of high compared to the real zeros:

Bloomberg:
Date    Days    Term    InstType    Mid Zero
26-10-2017  1   O/N CASH    2,500   2,500
30-01-2018  97  3 MO    CASH    2,460   2,460
30-04-2018  187 6 MO    CASH    2,400   2,400
30-07-2018  278 9 MO    CASH    2,400   2,400
30-10-2018  370 1 YR    CASH    2,410   2,410
30-04-2019  552 18 MO   CASH    2,540   2,540
30-10-2019  735 2 YR    SWAP    2,680   2,684
30-10-2020  1101    3 YR    SWAP    3,010   3,024
29-10-2021  1465    4 YR    SWAP    3,300   3,327
28-10-2022  1829    5 YR    SWAP    3,540   3,582
30-10-2023  2196    6 YR    SWAP    3,700   3,752
30-10-2024  2562    7 YR    SWAP    3,870   3,939
30-10-2025  2927    8 YR    SWAP    4,030   4,119
30-10-2026  3292    9 YR    SWAP    4,140   4,243
29-10-2027  3656    10 YR   SWAP    4,230   4,346
30-10-2032  5484    15 YR   SWAP    4,380   4,502
30-10-2037  7310    20 YR   SWAP    4,560   4,742

• QuantLib is 100% correct in the numbers given the inputs. – HelloWorld Oct 26 '17 at 1:00
• Are you sure "PiecewiseLinearZero" is correct? I'd expect Bloomberg go for more complicated modeling than piecewise linear interpolation? PiecewiseLinearZero is a very simple model that many institutions prefer something else. – HelloWorld Oct 26 '17 at 1:00
• Yes what about using a Cubic Spline interpolation? Can you do it in QuantLib? – JejeBelfort Oct 26 '17 at 9:48
• I don't think the problem is in the interpolation method...I belive i set up the inputs wrong. I don't know why i'm 2.707 as zero rate in the 17th row in quantlib, seems very far from the others. – Jose Pedro Melo Oct 26 '17 at 11:45
• Seems that the bloomberg's rates are coumpounded semiannualy, but i still can't figure out why i'm getting the 2.707 for 18 years. – Jose Pedro Melo Oct 28 '17 at 2:59

2 Answers

You probably figured this out already, since you mention in the comments that Bloomberg rates are compounded semiannually. However, the higher rates have to do with the compounding convention you're choosing. You're asking for rates with simple compounding, and that overrides the annual frequency you're passing and causes the rates to be higher than you expect. Using Compounded and Semiannual returns values much closer to the ones you quoted.

For the 2.707 you get at year 18, there's a bug in your code. The 18 month tenor is loaded as 18 years

#Swap Rates
swap_helpers = []
for i in range(len(terms)):
if i < 4:  # should be i < 5!!!
...


Don't know the conventions used for the Bloomberg rates to comment on the difference though.