3
$\begingroup$

I am currently working on pricing a Zero Coupon Inflation Swap using Quantlib in Python. During my analysis, I have observed that when the start date and end date of the swap coincide exactly with X years from the valuation date (e.g., a maturity date of March 31, 2041), and I'm pricing the swap on the same date (e.g., March 31, 2023), the price obtained from Quantlib in Python closely matches the price provided by Bloomberg.

However, a notable price discrepancy arises when the time to maturity is not precisely X years from the current date. I suspect that this difference is influenced by seasonality. It is worth noting that Bloomberg's prices (when not using seasonality) are close to the Quantlib prices.

While Quantlib does offer the option to implement seasonality, the results obtained using this approach are not meeting my expectations.

I have obtained seasonality adjustment parameters from Bloomberg (SWIL in BB) and using them now in Quantlib, although I'm not certain if my approach is correct.

Here are the adjustment values for each month taken from Bloomberg:

Month Adjustment
Jan: -0.004238
Feb: 0.004928
Mar: 0.011557
Apr: 0.00558
May: 0.003235
Jun: 0.003415
Jul: -0.002354
Aug: 0.001692
Sep: 0.004916
Oct: 0.005826
Nov: -0.001049
Dec: 0.001429

In the code snippet below, I have included the necessary parameter setup and the implementation of the seasonality adjustment.

Does anyone know how to correctly use the seasonality adjustment in Quantlib? I'm having trouble achieving results that match Bloomberg's prices. Any insights would be greatly appreciated. Code should work as I included all parameters used to create all the curves.

import QuantLib as ql
from QuantLib import *

import pandas as pd

start_date = ql.Date(17,2,2021)
# start_date = ql.Date(31,3,2021)
calc_date = ql.Date(31,3,2023)
calc_date_db = pd.datetime(2023,3,31)
end_date = ql.Date(17,2,2031)
# end_date = ql.Date(31,3,2031)
swap_type = ql.ZeroCouponInflationSwap.Receiver
calendar = ql.TARGET()
day_count_convention = ql.ActualActual()
contract_observation_lag = ql.Period(3, ql.Months)
business_day_convention = ql.ModifiedFollowing
nominal = 10e6
fixed_rate = 2.44 / 100

ql.Settings.instance().evaluationDate = calc_date

# create inflation yield term structure and make relinkable object
inflation_yield_term_structure = ql.RelinkableZeroInflationTermStructureHandle()
inflation_index = ql.EUHICP(False, inflation_yield_term_structure)

# ois rates and create curve
tenor = [0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 15.0, 20.0, 25.0, 30.0, 40.0, 50.0]
rates = [3.227, 3.3535, 3.168, 2.9945, 2.8645, 2.786, 2.7465, 2.7255, 2.7195, 2.722, 2.7325, 2.7615, 2.7845, 2.6993, 2.5608, 2.4373, 2.2565, 2.1525]

ois_curve = pd.Series(rates, index=tenor, name='Tenor')

short_curve = ois_curve[ois_curve.index < 1]
short_curve.index = short_curve.index * 12
long_curve = ois_curve[ois_curve.index >= 1]


helper = [ql.OISRateHelper(0,
                           ql.Period(int(tenor), Months),
                           ql.QuoteHandle(ql.SimpleQuote(rate / 100)),
                           ql.Eonia(),
                           ql.YieldTermStructureHandle(),
                           True)
          for tenor, rate in short_curve.items()]

helper += [ql.OISRateHelper(0,
                           ql.Period(int(tenor), Years),
                           ql.QuoteHandle(SimpleQuote(rate / 100)),
                           ql.Eonia(),
                           ql.YieldTermStructureHandle(),
                           True)
          for tenor, rate in long_curve.items()]


discount_curve = ql.PiecewiseLogCubicDiscount(0, ql.TARGET(), helper, ql.Actual365Fixed())
discount_curve.enableExtrapolation()

discount_handle = ql.RelinkableYieldTermStructureHandle()
discount_handle.linkTo(discount_curve)

# create inflation resets series and add to inflation_index ql object
inflation_resets = pd.Series(data=[104.05, 104.23, 104.77, 105.06, 104.94, 105.29, 104.9, 104.45, 104.54, 104.73,
                              104.35, 104.7, 104.88, 105.1, 106.09, 106.69, 106.97, 107.26, 107.16, 107.54,
                              108.06, 108.99, 109.49, 109.97, 110.3, 111.35, 114.12, 114.78, 115.74, 116.7,
                              116.83, 117.55, 118.99, 120.79, 120.7, 120.24, 119.96, 120.94],
                        index=pd.date_range(start='2020-01-01', periods=38, freq='M'))

def add_inflation_reset_rates():
    for date, rate in inflation_resets.items():
        date = ql.Date(date.day, date.month, date.year)
        rate = rate / 100
        inflation_index.addFixing(date, rate)

add_inflation_reset_rates()


# create inflation curve and make ql inflation curve object
data = {'Tenor': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20, 25, 30, 40, 50],
        'Rates': [3.77, 3.025, 2.788, 2.6715, 2.607, 2.5705, 2.5473, 2.444, 2.444, 2.446, 2.463, 2.5515, 2.567, 2.6165, 2.6415, 2.7375, 2.7838]}

inflation_curve = pd.Series(data['Rates'], index=data['Tenor'])

inflation_rate_helpers = []

for tenor, rate in inflation_curve.iteritems():
    maturity = calendar.advance(calc_date, ql.Period(int(tenor), ql.Years))
    quote = ql.QuoteHandle(ql.SimpleQuote(rate / 100.0))
    helper = ql.ZeroCouponInflationSwapHelper(quote,
                                              contract_observation_lag,
                                              maturity,
                                              calendar,
                                              business_day_convention,
                                              day_count_convention,
                                              inflation_index,
                                              discount_handle)
    inflation_rate_helpers.append(helper)

base_zero_rate = 0
inflation_curve = ql.PiecewiseZeroInflation(calc_date,
                                            calendar,
                                            day_count_convention,
                                            contract_observation_lag,
                                            ql.Monthly,
                                            inflation_index.interpolated(),
                                            base_zero_rate,
                                            inflation_rate_helpers,
                                            1.0e-12,
                                            ql.Linear())
inflation_yield_term_structure.linkTo(inflation_curve)


# add seasonality to inflation_curve
seasonality_base_date = ql.Date(1, 1, 2010)
frequency = ql.Monthly
seasonality_factors = [0.991831,
                       1.004143,
                       1.010895,
                       1.003874,
                       1.002072,
                       1.002082,
                       0.995812,
                       1.001432,
                       1.004451,
                       1.003218,
                       0.999,
                       1.001887]

seasonality = ql.MultiplicativePriceSeasonality(seasonality_base_date, frequency, seasonality_factors)
inflation_curve.setSeasonality(seasonality)

swap = ql.ZeroCouponInflationSwap(swap_type,
                                  nominal,
                                  start_date,
                                  end_date,
                                  calendar,
                                  business_day_convention,
                                  day_count_convention,
                                  fixed_rate,
                                  inflation_index,
                                  contract_observation_lag)

# inspect float cashflow
floating_leg = swap.inflationLeg()
for cashflow_float in floating_leg:
    print(f'print inflation leg cashflows')
    print(cashflow_float.date())
    print(cashflow_float.amount())

# inspect fixed cashflow
fixed_leg = swap.fixedLeg()
for cashflow_fixed in fixed_leg:
    print(f'print fixed leg cashflows')
    print(cashflow_fixed.date())
    print(cashflow_fixed.amount())

# price and print inflation swap price
swap_engine = ql.DiscountingSwapEngine(discount_handle)
swap.setPricingEngine(swap_engine)
print(swap.NPV())
$\endgroup$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.