Swap Curve Bootstrapping: Compounding Conventions and Tenor Errors
Summary
The document describes troubleshooting a Chilean swap curve built in QuantLib from deposit and swap quotes. The accepted explanation attributes much of the difference from Bloomberg zero rates to the output-rate convention: the code requests simple compounding, while Bloomberg’s quoted zeros use semiannual compounding. Reading zero rates with compounded semiannual conventions produces closer comparisons. This illustrates that a zero rate is not fully specified by its numerical level alone; the compounding basis matters.
A separate response identifies a tenor-unit error in the supplied setup: an 18-month quote is inadvertently treated as an 18-year swap because the code’s branch assigns the wrong tenor unit. The answers do not establish every market convention needed to reproduce Bloomberg’s curve, and the original question also raises index and calendar configuration. The material therefore offers targeted debugging insights, while leaving a complete Chilean curve specification and full validation unresolved.
Key ideas
- Zero rates depend on their compounding convention, so comparisons require matching the basis used by the reference curve.
- Semiannual compounded rates are more comparable to the Bloomberg values described than simple-compounded rates.
- A branch condition can mistakenly treat an 18-month swap quote as an 18-year tenor.
- Matching conventions addresses only part of curve replication; index, calendar, and other market details may also matter.
Tags
Full text
# Swap Bootstrapping with quantlib
# 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
```
## Answer by Luigi Ballabio (score 3, accepted)
https://quant.stackexchange.com/a/36767
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.
## Answer by Jon Manese (score 2)
https://quant.stackexchange.com/a/36739
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.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.