Hello again.

If you usually read QuantLib’s release notes, you might have noticed that a refactoring of our inflation classes has been going on for a few years. Here is the latest step. It was implemented in version 1.34, released last April, and touched inflation curves.

Today’s notebook shows the basic functionality of inflation indexes and curves, and in doing so it also shows what changed in the latest release and mentions a few older changes in context.

The refactoring is still going on. When a feature is changed, the old interface is deprecated and kept in the library so that client code can still compile if it uses it. After five releases, the old interface is finally removed. This should give people ample time to migrate their method calls to the new signature.

A word of advice: if you’re writing client code in C++, enabling warnings during compilation will alert you of deprecated methods and classes so you aren’t taken by surprise when they are finally removed and the warnings become errors. If you use the bindings to Python, C# or Java, we don’t have a way to insert the warnings in the code; so, please, do read the release notes when a new version of QuantLib comes out. Deprecated features are listed, as well as the suggested alternatives.

Subscribe to my Substack to receive my posts in your inbox, or follow me on Twitter or LinkedIn if you want to be notified of new posts, or subscribe via RSS if you’re the tech type: the buttons for all that are in the footer. Also, I’m available for training, both online and (when possible) on-site: visit my Training page for more information.

Inflation indexes and curves

import QuantLib as ql
import pandas as pd
today = ql.Date(11, ql.May, 2024)
ql.Settings.instance().evaluationDate = today

Inflation indexes

The library provides classes to model some predefined inflation indexes, such as EUHICP or UKRPI; any missing one can be defined using the base class ZeroInflationIndex.

index = ql.EUHICP()

Historical fixings

Historical fixings can be saved (for all instances of the same index) by calling the addFixing method.

The method is inherited from the base Index class, so it requires a specific date even if an inflation fixing corresponds to a whole month. By convention, the date we’ll pass together with a fixing must be the first day of the corresponding month (not the day of publishing, which would be in the following month).

inflation_fixings = [
    ((2022, ql.January), 110.70),
    ((2022, ql.February), 111.74),
    ((2022, ql.March), 114.46),
    ((2022, ql.April), 115.11),
    ((2022, ql.May), 116.07),
    ((2022, ql.June), 117.01),
    ((2022, ql.July), 117.14),
    ((2022, ql.August), 117.85),
    ((2022, ql.September), 119.26),
    ((2022, ql.October), 121.03),
    ((2022, ql.November), 120.95),
    ((2022, ql.December), 120.52),
    ((2023, ql.January), 120.27),
    ((2023, ql.February), 121.24),
    ((2023, ql.March), 122.34),
    ((2023, ql.April), 123.12),
    ((2023, ql.May), 123.15),
    ((2023, ql.June), 123.47),
    ((2023, ql.July), 123.36),
    ((2023, ql.August), 124.03),
    ((2023, ql.September), 124.43),
    ((2023, ql.October), 124.54),
    ((2023, ql.November), 123.85),
    ((2023, ql.December), 124.05),
    ((2024, ql.January), 123.60),
    ((2024, ql.February), 124.37),
    ((2024, ql.March), 125.31),
    ((2024, ql.April), 126.05),
]

for (year, month), fixing in inflation_fixings:
    index.addFixing(ql.Date(1, month, year), fixing)

Asking for a fixing for any past date will return the fixing for the month:

index.fixing(ql.Date(15, ql.March, 2024))
125.31

Of course, some past dates still don’t have an inflation fixing available: at the time of this writing, in the middle of May 2024, the index for this month is not published yet. Fixings for these dates, as well as for future dates, will need to be forecast: and for that, we require an inflation term structure.

Another note: in the past, the index could also return interpolated fixings; however, the result was not always right, since the index didn’t have the correct information on the number of days to use while interpolating. In version 1.29, the interpolation logic was moved into inflation coupons, where the information is available; the corresponding logic in the index was deprecated in the same release and removed in version 1.34.

The logic behind the interpolation is also available as a static method in the CPI class, used by inflation coupons:

observation_lag = ql.Period(3, ql.Months)

ql.CPI.laggedFixing(
    index, ql.Date(15, ql.May, 2024), observation_lag, ql.CPI.Linear
)
124.79451612903226

For instance, the call above interpolates linearly a fixing for May 15th, 2024 with an observation lag of three months. This means that the fixings to be interpolated will be those for February and March 2024, but their weights in the interpolation will be based on the number of days between May 1st and May 15th and between May 15th and June 1st.

Inflation curves

As for other kinds of term structures, it’s possible to create inflation curves by bootstrapping over a number of quoted instruments; at this time, the library provides helpers for zero-coupon inflation swaps. The information needed to build them includes, besides more common data such as the calendar and day count convention, an observation lag for the fixing of the underlying inflation index and the interpolation to be used between fixings.

The helpers will also need an external nominal curve of interest rates. This used to be stored into the inflation curve itself, but for consistency with other helpers and term structures it was moved into the helpers in version 1.15.

inflation_quotes = [
    (ql.Period(1, ql.Years), 2.93),
    (ql.Period(2, ql.Years), 2.95),
    (ql.Period(3, ql.Years), 2.965),
    (ql.Period(4, ql.Years), 2.98),
    (ql.Period(5, ql.Years), 3.0),
    (ql.Period(7, ql.Years), 3.06),
    (ql.Period(10, ql.Years), 3.175),
    (ql.Period(12, ql.Years), 3.243),
    (ql.Period(15, ql.Years), 3.293),
    (ql.Period(20, ql.Years), 3.338),
    (ql.Period(25, ql.Years), 3.348),
    (ql.Period(30, ql.Years), 3.348),
    (ql.Period(40, ql.Years), 3.308),
    (ql.Period(50, ql.Years), 3.228),
]

calendar = ql.TARGET()
observation_lag = ql.Period(3, ql.Months)
day_counter = ql.Thirty360(ql.Thirty360.BondBasis)
interpolation = ql.CPI.Linear

nominal_curve = ql.YieldTermStructureHandle(
    ql.FlatForward(today, 0.03, ql.Actual365Fixed())
)

helpers = []

for tenor, quote in inflation_quotes:
    maturity = calendar.advance(today, tenor)
    helpers.append(
        ql.ZeroCouponInflationSwapHelper(
            ql.makeQuoteHandle(quote / 100),
            observation_lag,
            maturity,
            calendar,
            ql.Following,
            day_counter,
            index,
            interpolation,
            nominal_curve,
        )
    )

Now, inflation curves are kind of odd compared to other curves. Usually, term structures in QuantLib (e.g. for interest rates or default probabilities) forecast their underlying quantities starting from today; yesterday’s rates are assumed to be known, and so is whether an issuer defaulted. As I mentioned, though, inflation curves can also be used to forecast still unpublished fixings corresponding to past dates. Therefore, they have a so-called base date in the past which separates known and unknown fixings.

Up to version 1.33, though, the base date was not specified explicitly. Instead, the constructors of most inflation curves required an observation lag, probably because of some past confusion with the observation lag of the instruments used for bootstrapping; the base date was calculated by starting from today’s date, subtracting the lag and taking the first date of the resulting month.

However, the only base date that makes sense is the date of the last available inflation fixing: the fixings are known up to that point and need to be forecast after it. Therefore, the observation lag could not be a constant attribute of a given curve, but had to be calculated based on the available data: at the beginning of May, when the April fixing was not published yet, we needed a lag of two months to get to March, while mid-May we had to switch to a lag of one month to get an April base date as soon as the corresponding fixing was published. The curve also needed a base rate to be used for $t=0$; in the current implementation, the base rate is still required when using the old constructor but is ignored by the bootstrapping process.

availability_lag = ql.Period(1, ql.Months)
fixing_frequency = ql.Monthly
base_rate = 0.029

inflation_curve = ql.PiecewiseZeroInflation(
    today,
    calendar,
    ql.Actual365Fixed(),
    availability_lag,
    fixing_frequency,
    base_rate,
    helpers,
)
pd.DataFrame(inflation_curve.nodes(), columns=["node", "rate"]).style.format(
    {"rate": "{:.4%}"}
)
  node rate
0 April 1st, 2024 2.0985%
1 March 1st, 2025 2.0985%
2 March 1st, 2026 2.5889%
3 March 1st, 2027 2.7210%
4 March 1st, 2028 2.7980%
5 March 1st, 2029 2.8545%
6 March 1st, 2031 2.9586%
7 March 1st, 2034 3.1057%
8 March 1st, 2036 3.1858%
9 March 1st, 2039 3.2467%
10 March 1st, 2044 3.3029%
11 March 1st, 2049 3.3190%
12 March 1st, 2054 3.3235%
13 March 1st, 2064 3.2888%
14 March 1st, 2074 3.2117%

Using the curve, an inflation index can forecast future fixings:

hicp = ql.EUHICP(ql.ZeroInflationTermStructureHandle(inflation_curve))
hicp.fixing(ql.Date(1, ql.February, 2027))
135.99222083993126

Starting with version 1.34, though, it’s finally possible to specify the base date explicitly; in order to determine it, we also added a lastFixingDate method to inflation indexes for convenience.

inflation_curve = ql.PiecewiseZeroInflation(
    today, hicp.lastFixingDate(), fixing_frequency, ql.Actual365Fixed(), helpers
)
pd.DataFrame(inflation_curve.nodes(), columns=["node", "rate"]).style.format(
    {"rate": "{:.4%}"}
)
  node rate
0 April 1st, 2024 2.0985%
1 March 1st, 2025 2.0985%
2 March 1st, 2026 2.5889%
3 March 1st, 2027 2.7210%
4 March 1st, 2028 2.7980%
5 March 1st, 2029 2.8545%
6 March 1st, 2031 2.9586%
7 March 1st, 2034 3.1057%
8 March 1st, 2036 3.1858%
9 March 1st, 2039 3.2467%
10 March 1st, 2044 3.3029%
11 March 1st, 2049 3.3190%
12 March 1st, 2054 3.3235%
13 March 1st, 2064 3.2888%
14 March 1st, 2074 3.2117%

As you can see above, the result of the bootstrapping process is the same, and so are the fixings forecast by the index:

hicp = ql.EUHICP(ql.ZeroInflationTermStructureHandle(inflation_curve))
hicp.fixing(ql.Date(1, ql.February, 2027))
135.99222083993126

The old constructors for the various inflation curves available in the library are now deprecated and will be removed in QuantLib 1.39.