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 Ccy Payment Notional Period Convention DCF Acc Start Acc End DF ... Base Ccy NPV Ccy Collateral Rate Spread Index Base Index Val Index Ratio Index Fix Date Unindexed Cashflow
leg1 0 FixedPeriod GBP 2024-11-27 -10000000.0 Regular ActActICMA 0.5 2024-05-27 2024-11-27 None ... GBP None None 2.0 NaN 381.0 None None 2024-11-27 100000.0
1 FixedPeriod GBP 2025-05-27 -10000000.0 Regular ActActICMA 0.5 2024-11-27 2025-05-27 None ... GBP None None 2.0 NaN 381.0 None None 2025-05-27 100000.0
2 Cashflow GBP 2025-05-27 -10000000.0 NaN NaN NaN NaT NaT None ... GBP None None NaN NaN 381.0 None None 2025-05-27 10000000.0

3 rows × 23 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
fixings.add("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"])
fixings["RPI_series"]
[3]:
(-2466891604736727079,
 reference_date
 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,
 (Timestamp('2024-02-01 00:00:00'), Timestamp('2025-03-01 00:00:00')))

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_method="daily",
    index_lag=3,
    index_fixings="RPI_series",
    spec="uk_gb"
)
[5]:
ukti.cashflows()
[5]:
Type Ccy Payment Notional Period Convention DCF Acc Start Acc End DF ... Base Ccy NPV Ccy Collateral Rate Spread Index Base Index Val Index Ratio Index Fix Date Unindexed Cashflow
leg1 0 FixedPeriod GBP 2024-11-27 -10000000.0 Regular ActActICMA 0.5 2024-05-27 2024-11-27 None ... GBP None None 2.0 NaN 382.677419 388.773333 1.015930 2024-11-27 100000.0
1 FixedPeriod GBP 2025-05-27 -10000000.0 Regular ActActICMA 0.5 2024-11-27 2025-05-27 None ... GBP None None 2.0 NaN 382.677419 395.090323 1.032437 2025-05-27 100000.0
2 Cashflow GBP 2025-05-27 -10000000.0 NaN NaN NaN NaT NaT None ... GBP None None NaN NaN 382.677419 395.090323 1.032437 2025-05-27 10000000.0

3 rows × 23 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 Ccy Payment Notional Period Convention DCF Acc Start Acc End DF ... Base Ccy NPV Ccy Collateral Rate Spread Index Base Index Val Index Ratio Index Fix Date Unindexed Cashflow
leg1 0 FixedPeriod GBP 2024-11-27 -10000000.0 Regular ActActICMA 0.5 2024-05-27 2024-11-27 0.000000 ... GBP 0.000000e+00 None 2.0 NaN 382.677419 388.773333 1.015930 2024-11-27 100000.0
1 FixedPeriod GBP 2025-05-27 -10000000.0 Regular ActActICMA 0.5 2024-11-27 2025-05-27 0.999422 ... GBP 1.031840e+05 None 2.0 NaN 382.677419 395.090323 1.032437 2025-05-27 100000.0
2 Cashflow GBP 2025-05-27 -10000000.0 NaN NaN NaN NaT NaT 0.999422 ... GBP 1.031840e+07 None NaN NaN 382.677419 395.090323 1.032437 2025-05-27 10000000.0

3 rows × 23 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.0017s

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_method="daily",
    index_lag=3,
    index_fixings="RPI_series",
    spec="uk_gb",
    curves=[index_curve, disc_curve]
)
[12]:
ukti.cashflows()
[12]:
Type Ccy Payment Notional Period Convention DCF Acc Start Acc End DF ... Base Ccy NPV Ccy Collateral Rate Spread Index Base Index Val Index Ratio Index Fix Date Unindexed Cashflow
leg1 0 FixedPeriod GBP 2025-03-17 -15000000.0 Regular ActActICMA 0.5 2024-09-16 2025-03-16 0.000000 ... GBP 0.000000e+00 None 3.0 NaN 387.4 391.906452 1.011633 2025-03-16 225000.0
1 FixedPeriod GBP 2025-09-16 -15000000.0 Regular ActActICMA 0.5 2025-03-16 2025-09-16 0.995114 ... GBP 2.302582e+05 None 3.0 NaN 387.4 398.400000 1.028394 2025-09-16 225000.0
2 Cashflow GBP 2025-09-16 -15000000.0 NaN NaN NaN NaT NaT 0.995114 ... GBP 1.535055e+07 None NaN NaN 387.4 398.400000 1.028394 2025-09-16 15000000.0

3 rows × 23 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 72757_ 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.0012s
[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 = FixedLeg(
    schedule=Schedule(dt(2024, 12, 1), "8m", "M"),
    fixed_rate=1.0,
    notional=-15e6,
    convention="30360",
    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(index_curve=index_curve, disc_curve=disc_curve)
[18]:
Type Ccy Payment Notional Period Convention DCF Acc Start Acc End DF ... Base Ccy NPV Ccy Collateral Rate Spread Index Base Index Val Index Ratio Index Fix Date Unindexed Cashflow
0 FixedPeriod GBP 2025-01-03 -15000000.0 Regular 30360 0.083333 2024-12-01 2025-01-01 0.000000 ... GBP 0.000000 None 1.0 None 390.7 390.9 1.000512 2025-01-01 12500.0
1 FixedPeriod GBP 2025-02-03 -15000000.0 Regular 30360 0.083333 2025-01-01 2025-02-01 0.000000 ... GBP 0.000000 None 1.0 None 390.7 392.1 1.003583 2025-02-01 12500.0
2 FixedPeriod GBP 2025-03-03 -15000000.0 Regular 30360 0.083333 2025-02-01 2025-03-01 0.000000 ... GBP 0.000000 None 1.0 None 390.7 391.7 1.002560 2025-03-01 12500.0
3 FixedPeriod GBP 2025-04-03 -15000000.0 Regular 30360 0.083333 2025-03-01 2025-04-01 0.000000 ... GBP 0.000000 None 1.0 None 390.7 394.0 1.008446 2025-04-01 12500.0
4 FixedPeriod GBP 2025-05-03 -15000000.0 Regular 30360 0.083333 2025-04-01 2025-05-01 0.000000 ... GBP 0.000000 None 1.0 None 390.7 395.3 1.011774 2025-05-01 12500.0
5 FixedPeriod GBP 2025-06-03 -15000000.0 Regular 30360 0.083333 2025-05-01 2025-06-01 0.999152 ... GBP 12658.822374 None 1.0 None 390.7 396.0 1.013565 2025-06-01 12500.0
6 FixedPeriod GBP 2025-07-03 -15000000.0 Regular 30360 0.083333 2025-06-01 2025-07-01 0.997997 ... GBP 12679.307428 None 1.0 None 390.7 397.1 1.016381 2025-07-01 12500.0
7 FixedPeriod GBP 2025-08-03 -15000000.0 Regular 30360 0.083333 2025-07-01 2025-08-01 0.996804 ... GBP 12705.616727 None 1.0 None 390.7 398.4 1.019708 2025-08-01 12500.0

8 rows × 23 columns

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