Using Curves with an Index and Inflation Instruments#

This page exemplifies the ways of constructing Curves dealing with inflation and inflation linked products. E.g. IndexFixedRateBond, ZCIS and IIRS.

Key Points

  • A Series of index values uses real data, with a zero month lag and the month is indexed to the 1st of the month.

  • A Curve can have any index_lag but best practice is to set it to zero to be consistent with index_fixings.

  • A Curve can be calibrated by forecast RPI/CPI index values in a Solver using the Value Instrument type.

Begin with a simple case without a Curve or any index_fixings#

This case uses an IndexFixedRateBond which has two coupon periods. The bond that is created below is fictional. It has the normal 3 month index_lag, ‘daily’ index_method for interpolation and the index_base for the Instrument is set to 381.0.

Its cashflows can be generated but are not fully formed becuase we are lacking information about the index: UK RPI.

[1]:
from rateslib import *
from pandas import Series, DataFrame

today = dt(2025, 5, 12)

ukti = IndexFixedRateBond(
    effective=dt(2024, 5, 27),
    termination=dt(2025, 5, 27),
    fixed_rate=2.0,
    notional=-10e6,
    index_base=381.0,
    index_method="daily",
    index_lag=3,
    spec="uk_gb"
)
[2]:
ukti.cashflows()
[2]:
Type Period Ccy Acc Start Acc End Payment Convention DCF Notional DF ... Rate Spread Real Cashflow Index Base Index Val Index Ratio Cashflow NPV FX Rate NPV Ccy
0 IndexFixedPeriod Regular GBP 2024-05-27 2024-11-27 2024-11-27 ActActICMA 0.5 -10000000.0 None ... 2.0 NaN 100000.0 381.0 None None None None 1.0 None
1 IndexFixedPeriod Regular GBP 2024-11-27 2025-05-27 2025-05-27 ActActICMA 0.5 -10000000.0 None ... 2.0 NaN 100000.0 381.0 None None None None 1.0 None
2 IndexCashflow Exchange GBP NaT 2025-05-27 2025-05-27 NaN NaN -10000000.0 None ... NaN NaN 10000000.0 381.0 None None None None 1.0 None

3 rows × 21 columns

Adding index_fixings as a Series#

Becuase this bond has a 3 month index_lag the most recent print required to determine all the cashflows is the RPI index for March 2025. In rateslib the RPI value for March must be indexed to 1st March, i.e. index_fixings as a Series must have a zero lag. The below are real published RPI prints for the UK. (Note that Bloomberg will index these to the end of the month instead of the start of the month)

[3]:
from pandas import DataFrame
RPI_series = DataFrame([
    [dt(2024, 2, 1), 381.0],
    [dt(2024, 3, 1), 383.0],
    [dt(2024, 4, 1), 385.0],
    [dt(2024, 5, 1), 386.4],
    [dt(2024, 6, 1), 387.3],
    [dt(2024, 7, 1), 387.5],
    [dt(2024, 8, 1), 389.9],
    [dt(2024, 9, 1), 388.6],
    [dt(2024, 10, 1), 390.7],
    [dt(2024, 11, 1), 390.9],
    [dt(2024, 12, 1), 392.1],
    [dt(2025, 1, 1), 391.7],
    [dt(2025, 2, 1), 394.0],
    [dt(2025, 3, 1), 395.3]
], columns=["month", "rate"]).set_index("month")["rate"]
RPI_series
[3]:
month
2024-02-01    381.0
2024-03-01    383.0
2024-04-01    385.0
2024-05-01    386.4
2024-06-01    387.3
2024-07-01    387.5
2024-08-01    389.9
2024-09-01    388.6
2024-10-01    390.7
2024-11-01    390.9
2024-12-01    392.1
2025-01-01    391.7
2025-02-01    394.0
2025-03-01    395.3
Name: rate, dtype: float64

If the bond is recreated supplying the index_fixings the cashflows will be fully formed. Additionally we can use the same RPI_series to set the index_base value.

For good order the index_base is expected to be (and will be visible in one of the columns in cashflows):

\[RPI_{Feb} + (RPI_{Mar} - RPI_{Feb}) * (27-1) / 31 = 382.677..\]
[4]:
ukti = IndexFixedRateBond(
    effective=dt(2024, 5, 27),
    termination=dt(2025, 5, 27),
    fixed_rate=2.0,
    notional=-10e6,
    index_base=RPI_series,
    index_method="daily",
    index_lag=3,
    index_fixings=RPI_series,
    spec="uk_gb"
)
[5]:
ukti.cashflows()
[5]:
Type Period Ccy Acc Start Acc End Payment Convention DCF Notional DF ... Rate Spread Real Cashflow Index Base Index Val Index Ratio Cashflow NPV FX Rate NPV Ccy
0 IndexFixedPeriod Regular GBP 2024-05-27 2024-11-27 2024-11-27 ActActICMA 0.5 -10000000.0 None ... 2.0 NaN 100000.0 382.677419 388.773333 1.015930 1.015930e+05 None 1.0 None
1 IndexFixedPeriod Regular GBP 2024-11-27 2025-05-27 2025-05-27 ActActICMA 0.5 -10000000.0 None ... 2.0 NaN 100000.0 382.677419 395.090323 1.032437 1.032437e+05 None 1.0 None
2 IndexCashflow Exchange GBP NaT 2025-05-27 2025-05-27 NaN NaN -10000000.0 None ... NaN NaN 10000000.0 382.677419 395.090323 1.032437 1.032437e+07 None 1.0 None

3 rows × 21 columns

Adding a discount Curve#

The npv of the cashflows, and of the bond are still not available becuase there is no discount curve. Let’s add one. Note that its initial date is, as usual, set to today.

[6]:
disc_curve = Curve({today: 1.0, dt(2029, 1, 1): 0.95})

There is now sufficient information to price any aspect of this bond becuase the index_fixings are determined and the discount Curve can value the future cashflows.

The prices shown below will be for the standard T+1 settlement under the uk_gb default spec.

[7]:
ukti.cashflows(curves=[None, disc_curve])
[7]:
Type Period Ccy Acc Start Acc End Payment Convention DCF Notional DF ... Rate Spread Real Cashflow Index Base Index Val Index Ratio Cashflow NPV FX Rate NPV Ccy
0 IndexFixedPeriod Regular GBP 2024-05-27 2024-11-27 2024-11-27 ActActICMA 0.5 -10000000.0 0.000000 ... 2.0 NaN 100000.0 382.677419 388.773333 1.015930 1.015930e+05 0.000000e+00 1.0 0.000000e+00
1 IndexFixedPeriod Regular GBP 2024-11-27 2025-05-27 2025-05-27 ActActICMA 0.5 -10000000.0 0.999422 ... 2.0 NaN 100000.0 382.677419 395.090323 1.032437 1.032437e+05 1.031840e+05 1.0 1.031840e+05
2 IndexCashflow Exchange GBP NaT 2025-05-27 2025-05-27 NaN NaN -10000000.0 0.999422 ... NaN NaN 10000000.0 382.677419 395.090323 1.032437 1.032437e+07 1.031840e+07 1.0 1.031840e+07

3 rows × 21 columns

[8]:
ukti.rate(curves=[None, disc_curve], metric="clean_price")
[8]:
np.float64(100.17305623199086)
[9]:
ukti.rate(curves=[None, disc_curve], metric="index_clean_price")
[9]:
np.float64(103.2686848600485)

Adding a forecast Index Curve#

Now we will add a forecast Index Curve. Rateslib allows Curves to be parametrised according to their own index_lag, but the most natural definition is to define a Curve with a zero index lag, consistent with the Series. This is more transparent.

Our Curve will start as of the last available RPI value date, indexed to that level. I.e. starting at 1st March with a base value of 395.3.

We calibrate the Curve, for this example, not with market instruments but instead directly with Index Values we wish to use.

[10]:
index_curve = Curve(
    nodes={
        dt(2025, 3, 1): 1.0,
        dt(2025, 4, 1): 1.0,
        dt(2025, 5, 1): 1.0,
        dt(2025, 6, 1): 1.0,
        dt(2025, 7, 1): 1.0,
    },
    index_lag=0,
    index_base=395.3,
    id="ic",
)
solver = Solver(
    curves=[index_curve],
    instruments=[
        Value(effective=dt(2025, 4, 1), metric="index_value", curves="ic"),
        Value(effective=dt(2025, 5, 1), metric="index_value", curves="ic"),
        Value(effective=dt(2025, 6, 1), metric="index_value", curves="ic"),
        Value(effective=dt(2025, 7, 1), metric="index_value", curves="ic"),
    ],
    s=[396, 397.1, 398, 398.8],
    instrument_labels=["Apr", "May", "Jun", "Jul"],
)
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 1.6235874018262206e-18, `time`: 0.0043s

An Instrument with mixed index_fixings and forecast fixings#

Now we can create an Instrument which requires both historical fixings and forecast values. Changing the dates of the fictional bond to end in, say, September 2025, requires the fixings forecast on the curve for June and July. Note we choose to add the curves directly at Instrument initialisation.

[11]:
ukti = IndexFixedRateBond(
    effective=dt(2024, 9, 16),
    termination=dt(2025, 9, 16),
    fixed_rate=3.0,
    notional=-15e6,
    index_base=RPI_series,
    index_method="daily",
    index_lag=3,
    index_fixings=RPI_series,
    spec="uk_gb",
    curves=[index_curve, disc_curve]
)
[12]:
ukti.cashflows()
[12]:
Type Period Ccy Acc Start Acc End Payment Convention DCF Notional DF ... Rate Spread Real Cashflow Index Base Index Val Index Ratio Cashflow NPV FX Rate NPV Ccy
0 IndexFixedPeriod Regular GBP 2024-09-16 2025-03-16 2025-03-17 ActActICMA 0.5 -15000000.0 0.000000 ... 3.0 NaN 225000.0 387.4 391.906452 1.011633 2.276173e+05 0.000000e+00 1.0 0.000000e+00
1 IndexFixedPeriod Regular GBP 2025-03-16 2025-09-16 2025-09-16 ActActICMA 0.5 -15000000.0 0.995114 ... 3.0 NaN 225000.0 387.4 398.400000 1.028394 2.313887e+05 2.302582e+05 1.0 2.302582e+05
2 IndexCashflow Exchange GBP NaT 2025-09-16 2025-09-16 NaN NaN -15000000.0 0.995114 ... NaN NaN 15000000.0 387.4 398.400000 1.028394 1.542592e+07 1.535055e+07 1.0 1.535055e+07

3 rows × 21 columns

Bonus: Risk to RPI prints.#

Actually the way we have constructed this Index Curve using the Solver means we can directly extract monetary sensitivities to the RPI index values

[13]:
ukti.delta(solver=solver)
[13]:
local_ccy gbp
display_ccy gbp
type solver label
instruments d8438_ Apr 0.000000
May 0.000000
Jun 19554.222151
Jul 19554.222151

For the 15mm GBP bond owned here, for each unit of the RPI print that comes above the supposed values of 398.0 and 398.8 the PnL will increase by £19.5k. Thus a +0.1% MoM surpise in June shifts up the values in June and July both by about 0.4. This would be expected to affect the NPV by £15.6k.

[14]:
pv_0 = ukti.npv()
pv_0
[14]:
<Dual: 15580804.210107, (ic0, ic1, ic2, ...), [0.0, 0.0, 0.0, ...]>
[15]:
solver.s = s=[396, 397.1, 398.4, 399.2]  # <-- Shift the Jun and Jul prints both up by 0.4, i.e. 0.1% MOM suprise in Jun.
solver.iterate()
SUCCESS: `func_tol` reached after 2 iterations (levenberg_marquardt), `f_val`: 1.419343797096256e-14, `time`: 0.0037s
[16]:
pv_1 = ukti.npv()
pv_1 - pv_0
[16]:
<Dual: 15643.374393, (ic0, ic1, ic2, ...), [0.0, 0.0, 0.0, ...]>

Other Instruments and Other Lags#

We can use the objects already created to price other Instruments. We directly construct an IndexFixedLeg below as an example with an index_lag of 2.

[17]:
ifl = IndexFixedLeg(
    schedule=Schedule(dt(2024, 12, 1), "8m", "M"),
    fixed_rate=1.0,
    notional=-15e6,
    convention="30360",
    index_base=RPI_series,
    index_fixings=RPI_series,
    index_lag=2,
    index_method="monthly",
    currency="gbp"
)

The cashflows below show the index values beginning with the November 2024 RPI value progressing through to the known March 2025 value and then adopting the values forecast by the Curve.

[18]:
ifl.cashflows(curve=index_curve, disc_curve=disc_curve)
[18]:
Type Period Ccy Acc Start Acc End Payment Convention DCF Notional DF ... Rate Spread Real Cashflow Index Base Index Val Index Ratio Cashflow NPV FX Rate NPV Ccy
0 IndexFixedPeriod Regular GBP 2024-12-01 2025-01-01 2025-01-03 30360 0.083333 -15000000.0 0.000000 ... 1.0 None 12500.0 390.7 390.9 1.000512 12506.398771 0.000000 1.0 0.000000
1 IndexFixedPeriod Regular GBP 2025-01-01 2025-02-01 2025-02-03 30360 0.083333 -15000000.0 0.000000 ... 1.0 None 12500.0 390.7 392.1 1.003583 12544.791400 0.000000 1.0 0.000000
2 IndexFixedPeriod Regular GBP 2025-02-01 2025-03-01 2025-03-03 30360 0.083333 -15000000.0 0.000000 ... 1.0 None 12500.0 390.7 391.7 1.002560 12531.993857 0.000000 1.0 0.000000
3 IndexFixedPeriod Regular GBP 2025-03-01 2025-04-01 2025-04-03 30360 0.083333 -15000000.0 0.000000 ... 1.0 None 12500.0 390.7 394.0 1.008446 12605.579729 0.000000 1.0 0.000000
4 IndexFixedPeriod Regular GBP 2025-04-01 2025-05-01 2025-05-03 30360 0.083333 -15000000.0 0.000000 ... 1.0 None 12500.0 390.7 395.3 1.011774 12647.171743 0.000000 1.0 0.000000
5 IndexFixedPeriod Regular GBP 2025-05-01 2025-06-01 2025-06-03 30360 0.083333 -15000000.0 0.999152 ... 1.0 None 12500.0 390.7 396.0 1.013565 12669.567443 12658.822374 1.0 12658.822374
6 IndexFixedPeriod Regular GBP 2025-06-01 2025-07-01 2025-07-03 30360 0.083333 -15000000.0 0.997997 ... 1.0 None 12500.0 390.7 397.1 1.016381 12704.760686 12679.307428 1.0 12679.307428
7 IndexFixedPeriod Regular GBP 2025-07-01 2025-08-01 2025-08-03 30360 0.083333 -15000000.0 0.996804 ... 1.0 None 12500.0 390.7 398.4 1.019708 12746.352698 12705.616727 1.0 12705.616727

8 rows × 21 columns

[19]:
solver.delta(ifl.npv(curve=index_curve, disc_curve=disc_curve, local=True))
[19]:
local_ccy gbp
display_ccy gbp
type solver label
instruments d8438_ Apr 31.966723
May 31.929759
Jun 31.891608
Jul 0.000000