Skip to content
All library documents

Applying Seasonality Adjustments to Zero-Coupon Inflation Swaps

Article Quant Q&A · Author: Oamriotn

Summary

The document describes an attempt to price a euro zero-coupon inflation swap in QuantLib and reconcile the result with Bloomberg. The author reports that prices are close when the swap dates fall on whole-year anniversaries of the valuation date, while non-anniversary maturities produce discrepancies suspected to involve seasonality. Bloomberg monthly adjustment values are provided, along with a Python setup for discount and inflation curves, historical inflation fixings, and a multiplicative seasonality object.

The code applies monthly factors to a calibrated inflation curve, but the author is unsure whether the factors and base date are being used correctly and asks for guidance. The account offers no validated correction or evidence that the implementation reproduces Bloomberg. It is therefore useful as a concrete calibration and implementation problem, but it does not establish the source of the mismatch; curve construction, conventions, observation lag, and seasonality treatment may all matter.

Key ideas

  • The example builds a QuantLib zero-coupon inflation swap using euro inflation and discount curves.
  • The reported Bloomberg and QuantLib prices are closer for whole-year maturities than for other dates.
  • The author suspects monthly seasonality adjustments explain the discrepancy and supplies monthly values.
  • The document does not confirm that the seasonality implementation is correct or give a resolved pricing result.

Tags

Full text
# Seasonality adjustment within Quantlib Zero Coupon Inflation Swap


# Seasonality adjustment within Quantlib Zero Coupon Inflation Swap












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())
```

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.