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 anyindex_lag
but best practice is to set it to zero to be consistent withindex_fixings
.A
Curve
can be calibrated by forecast RPI/CPI index values in aSolver
using theValue
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):
[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 |