Skip to content
All library documents

Speeding Up QuantLib OIS Curve Bootstrapping with Telescopic Dates

Article Quant Q&A · Author: sonderak

Summary

The document describes a QuantLib performance issue when constructing OIS rate helpers for a discount curve and updating swap valuations across evaluation dates. In the author’s timing comparison, creating OIS helpers takes much longer than creating deposit and swap helpers, and changing the evaluation date also accounts for most of the time spent on later-date calculations. The author wonders whether OIS helpers create extra date dependencies that make notifications expensive.

The accepted response identifies the OIS helper’s schedule of value dates as the source of avoidable work for curve bootstrapping. Setting the helper’s telescopic value dates option reduces that overhead, because a full schedule is unnecessary for this purpose; the response says the resulting curve is unchanged. The post supplies code and timings for its particular setup, but no broader benchmarks or QuantLib version details, so the performance gain may vary with configuration. The key practical lesson is to use telescopic dates when constructing OIS helpers for bootstrapping.

Key ideas

  • OIS rate helpers can construct many value dates that are unnecessary for curve bootstrapping.
  • The author reports higher setup time for OIS helpers than for deposit and swap helpers in their example.
  • Changing the evaluation date also triggers substantial recalculation in the reported setup.
  • Enabling telescopic value dates can reduce OIS helper overhead without changing the resulting curve, according to the accepted answer.

Tags

Full text
# QuantLib in Python : Execution time with OISRateHelper compared to Swap/Deposit RateHelper


# QuantLib in Python : Execution time with OISRateHelper compared to Swap/Deposit RateHelper












I have been working on valuation of interest rate swaps using dual curve bootstrapping. And for this I use OISRateHelper to create a discount term structure using OIS rates. The entire code below :

```
import QuantLib as ql
ql.IndexManager.instance().clearHistories()

import math
import numpy as np
import pandas as pd
import datetime as dt

#### INPUTS
date0 = dt.datetime.strptime("2012-07-05", "%Y-%m-%d")
date1 = dt.datetime.strptime("2012-07-06", "%Y-%m-%d")

calculation_dates = [ql.Date(date0.day,date0.month,date0.year), ql.Date(date1.day,date1.month,date1.year)]
fwd_start = 0

ois_mat = [ql.Period(x) for x in ['1D', '2W', '1M', '2M', '3M', '4M', '5M', '6M',
                                  '7M', '8M', '9M', '10M', '11M', '12M', '18M', '2Y',
                                  '30M', '3Y', '4Y', '5Y', '6Y', '7Y', '8Y', '9Y', '10Y',
                                  '11Y', '12Y', '15Y', '20Y', '25Y', '30Y', '35Y', '40Y',
                                  '50Y']]

ois_rates0 = [0.331, 0.162, 0.1525, 0.138, 0.136, 0.134, 0.133, 0.132, 0.135, 0.133, 0.134, 0.133, 0.134, 0.135, 0.146, 0.168, 0.197, 0.263, 0.419, 0.622, 0.8364, 1.006, 1.1625, 1.302, 1.429, 1.544, 1.64, 1.839, 1.93, 1.964, 1.999, 2.0465, 2.097, 2.1675] 
ois_rates1 = [0.329, 0.145, 0.134, 0.129, 0.13, 0.1235, 0.125, 0.145, 0.126, 0.12, 0.127, 0.122, 0.123, 0.125, 0.141, 0.165, 0.192, 0.253, 0.402, 0.595, 0.795, 0.976, 1.131, 1.27, 1.4049, 1.517, 1.611, 1.811, 1.901, 1.94, 1.963, 2.0265, 2.091, 2.173]

ois_dict0 = dict(zip(ois_mat, [x/100 for x in ois_rates0]))
ois_dict1 = dict(zip(ois_mat, [x/100 for x in ois_rates1]))

list_ois_rates_dict = [ois_dict0, ois_dict1]

deposit_mat = [ql.Period(1, ql.Months), ql.Period(3, ql.Months), ql.Period(6, ql.Months)]#, ql.Period(12, ql.Months)]

deposit_rates0 = [0.00362, 0.00641, 0.0092]
deposit_rates1 = [0.00255, 0.00549, 0.00831]

deposit_dict0 = dict(zip(deposit_mat, deposit_rates0))
deposit_dict1 = dict(zip(deposit_mat, deposit_rates1))

list_deposit_rates_dict = [deposit_dict0, deposit_dict1]

swap_mats = [ql.Period(x, ql.Years) for x in [1,2,3,4,5,6,7,8,9,10,12,15,20,25,30]]
swap_rates0 = [0.00515, 0.00806, 0.00883, 0.01029, 0.01213, 0.0139, 0.01544, 0.01677, 0.01793, 0.01897, 0.02073, 0.02232, 0.02279, 0.02293, 0.02307]
swap_rates1 = [0.00465, 0.00744, 0.00802, 0.00931, 0.01104, 0.01288, 0.0145, 0.01591, 0.01713, 0.01824, 0.02006, 0.0217, 0.02229, 0.02246, 0.02263]

swap_dict0 = dict(zip(swap_mats, swap_rates0))
swap_dict1 = dict(zip(swap_mats, swap_rates1))

list_swap_curve_rates_dict =  [swap_dict0, swap_dict1]

libor_rates_dict = deposit_dict0.copy()
swap_rates_dict = swap_dict0.copy()
swap_libor_tenors = [ql.Period(x, ql.Months) for x in [6,6,6,6,6,6,6,6,6,6,6,6,6,6,6]]
settlement_days = 0
face_value = 100
fixed_day_count = ql.Thirty360()
float_day_count = ql.Actual360()
ois_day_count = ql.Actual360()

calendar = ql.TARGET()
list_fixed_coupon_frequency = [ql.Annual for x in range(len(swap_libor_tenors))]
currency = ql.EURCurrency()
spread = 0
business_convention = ql.ModifiedFollowing
date_generation = ql.DateGeneration.Forward
end_of_month = False

### CALCULATIONS

fixed_npv = []
float_npv = []
swap_npv = []
swap_fair_rate = []

ql.IndexManager.instance().clearHistories()
date_t0 = calculation_dates[0]
ql.Settings.instance().evaluationDate = date_t0

effective_start_date = calendar.advance(date_t0, settlement_days + fwd_start, ql.Days)

discount_term_structure = ql.RelinkableYieldTermStructureHandle()
forecast_term_structure = ql.RelinkableYieldTermStructureHandle()

oindex = ql.OvernightIndex("", settlement_days, currency, calendar, ois_day_count, discount_term_structure)

######################
t5 = dt.datetime.now()
######################

ois_quote_map = {}
ois_helpers_t0 = []
for r,m in zip(list_ois_rates_dict[0].values(), list_ois_rates_dict[0].keys()):
    quote = ql.SimpleQuote(r)
    helper= ql.OISRateHelper(settlement_days, m, ql.QuoteHandle(quote), oindex)
    ois_helpers_t0.append(helper)
    ois_quote_map[m]=quote

######################
t6 = dt.datetime.now()
######################

deposit_quote_map = {}
deposit_helpers_t0 = []
for  r,m in zip(list_deposit_rates_dict[0].values(), list_deposit_rates_dict[0].keys()):
    quote = ql.SimpleQuote(r)
    helper = ql.DepositRateHelper(ql.QuoteHandle(quote),m,settlement_days,calendar,
                                  business_convention,end_of_month,float_day_count)
    deposit_helpers_t0.append(helper)
    deposit_quote_map[m] = quote

swap_ibor_indices = [ ql.IborIndex("", x, settlement_days, currency, calendar, business_convention, False, float_day_count,
                                   forecast_term_structure) for x in swap_libor_tenors]

for sib in swap_ibor_indices:
    sib.addFixing(date_t0, libor_rates_dict[sib.tenor()])

swap_quote_map = {}
swap_helpers_t0 = []
for r,m, ibor_index,f  in zip(list_swap_curve_rates_dict[0].values(), list_swap_curve_rates_dict[0].keys(),
                                           swap_ibor_indices, list_fixed_coupon_frequency):
    quote = ql.SimpleQuote(r)
    helper = ql.SwapRateHelper(ql.QuoteHandle(quote),m,calendar,f,business_convention,fixed_day_count, ibor_index, 
                               ql.QuoteHandle(ql.SimpleQuote(0)),ql.Period(0, ql.Days), discount_term_structure,
                               settlement_days)

    swap_helpers_t0.append(helper)
    swap_quote_map[m] = quote

######################
t10 = dt.datetime.now()
######################

helpers_t0 = deposit_helpers_t0 + swap_helpers_t0

swap_curve_t0 = ql.PiecewiseLogCubicDiscount(settlement_days, calendar, helpers_t0, fixed_day_count)
discount_curve_t0 = ql.PiecewiseLogCubicDiscount(settlement_days, calendar, ois_helpers_t0, fixed_day_count)

swap_curve_t0.enableExtrapolation()
discount_curve_t0.enableExtrapolation()

discount_term_structure.linkTo(discount_curve_t0)
forecast_term_structure.linkTo(swap_curve_t0)

swap_engine = ql.DiscountingSwapEngine(discount_term_structure)

list_irs = []
for i in range(len(swap_mats)):
    swap_rate = list(swap_rates_dict.values())[i]
    fixed_tenor = ql.Period(list_fixed_coupon_frequency[i])
    float_tenor = swap_ibor_indices[i].tenor()
    fixed_schedule = ql.Schedule(effective_start_date, calendar.advance(effective_start_date, list(swap_rates_dict.keys())[i]),
                                 fixed_tenor, calendar,
                                 business_convention, business_convention,
                                 date_generation, end_of_month)

    float_schedule = ql.Schedule(effective_start_date, calendar.advance(effective_start_date, list(swap_rates_dict.keys())[i]),
                                 float_tenor, calendar,
                                 business_convention, business_convention,
                                 date_generation, end_of_month)

    irs_temp = ql.VanillaSwap(ql.VanillaSwap.Receiver, face_value, fixed_schedule, swap_rate, fixed_day_count,
                              float_schedule, swap_ibor_indices[i], spread, float_day_count)

    irs_temp.setPricingEngine(swap_engine)

    if fwd_start > 0:
        fair_swap_rate = irs_temp.fairRate()
        irs = ql.VanillaSwap(ql.VanillaSwap.Receiver, face_value, fixed_schedule, fair_swap_rate, fixed_day_count,
                             float_schedule, swap_ibor_indices[i], spread, float_day_count)
        irs.setPricingEngine(swap_engine)
        list_irs.append(irs)

    else:
        list_irs.append(irs_temp)

fixed_npv.append([x.fixedLegNPV() for x in list_irs])
float_npv.append([x.floatingLegNPV() for x in list_irs])
swap_npv.append([x.NPV() for x in list_irs])
swap_fair_rate.append([x.fairRate() for x in list_irs])

print("ois helpers on date0: " + str((t6 - t5)))
print("other helpers on date0: " + str((t10 - t6)))

######################
t19 = dt.datetime.now()
######################

tdelta = dt.timedelta()

if len(calculation_dates) > 1:
    for i in range(1, len(calculation_dates)):
        ######################
        t19_1 = dt.datetime.now()
        ######################  
        date_ti = calculation_dates[i]
        ql.Settings.instance().evaluationDate = date_ti
        ######################
        t19_2 = dt.datetime.now()
        ######################
        for k in list_ois_rates_dict[i].keys():
            ois_quote_map[k].setValue(list_ois_rates_dict[i][k])

        for k in list_deposit_rates_dict[i].keys():
            deposit_quote_map[k].setValue(list_deposit_rates_dict[i][k])

        for k in list_swap_curve_rates_dict[i].keys():
            swap_quote_map[k].setValue(list_swap_curve_rates_dict[i][k])

        fixed_npv.append([x.fixedLegNPV() for x in list_irs])
        float_npv.append([x.floatingLegNPV() for x in list_irs])
        swap_npv.append([x.NPV() for x in list_irs])
        swap_fair_rate.append([x.fairRate() for x in list_irs])

        ######################
        tdelta = tdelta + (t19_2 - t19_1)
        ######################        

######################
t20 = dt.datetime.now()
######################

print("Calculations for other dates: " + str((t20 - t19)))
print("Setting evaluation dates for other dates: " + str(tdelta))
```

I have calculated execution time for creating an instance of OISRateHelpers and compared them to execution time for creating instances of DepositRateHelpers and SwapRateHelpers.

```
ois helpers on date0: 0:00:00.299389
other helpers on date0: 0:00:00.000999
```

The output shows that there is a major difference between.

Also, when recalculating the swaps, on other dates (other than date0), it's the ql.Settings.instance().evaluationDate which takes up more time.

```
Calculations for other dates: 0:00:09.596271
Setting evaluation dates for other dates: 0:00:09.554356
```

From this thread, I have come to understand that setting new dates changes reference dates for all the instances declared before.

I am trying to understand :

- Why creating instances of OISRateHelpers takes longer than creating instances of SwapRateHelpers/DepositRateHelpers?

- The fact that changing evaluation dates is time consuming, is it possible that OISRateHelper is creating 'redundant' dependencies which require notifications for changes in evaluation dates as well? Thus making it time consuming when resetting evaluation dates in ql.Settings?

Thank you!

## Answer by sonderak (score 1, accepted)

https://quant.stackexchange.com/a/41000

I sent the same question to the quantlib mailing list. And the solution was to set `telescopicValueDates = True` for OISRateHelpers.

> OISRateHelper creates a schedule with lots of dates; this is not really necessary for the purpose of curve bootstrapping and you can avoid it by setting the parameter telescopicValueDates to true which will speed up things (without changing the resulting curve of course).

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.