{ "cells": [ { "cell_type": "markdown", "id": "ade30c8f-90a7-4cdd-8a26-615695309b69", "metadata": {}, "source": [ "# Using Curves with an Index and Inflation Instruments\n", "\n", "This page exemplifies the ways of constructing *Curves* dealing with inflation and inflation linked products.\n", "E.g. ``IndexFixedRateBond``, ``ZCIS`` and ``IIRS``.\n", "\n", "> **Key Points**\n", "> \n", "> - A `Series` of index values uses real data, with a zero month lag and the month is indexed to the 1st of the month.\n", "> - A `Curve` can have any ``index_lag`` but best practice is to set it to zero to be consistent with ``index_fixings``.\n", "> - A `Curve` can be calibrated by forecast RPI/CPI index values in a `Solver` using the `Value` *Instrument* type." ] }, { "cell_type": "markdown", "id": "9763cc48-3a0d-4fde-b82c-ef971d61c6dd", "metadata": {}, "source": [ "### Begin with a simple case **without** a ``Curve`` or any ``index_fixings`` " ] }, { "cell_type": "markdown", "id": "f7b8b1bb-1fc1-459a-acfc-40b105bd03bb", "metadata": {}, "source": [ "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.\n", "\n", "Its **cashflows** can be generated but are **not** fully formed becuase we are lacking information about the index: UK RPI." ] }, { "cell_type": "code", "execution_count": null, "id": "e3d3c3cd-0e91-4e3a-92ab-f571b1bf0ec8", "metadata": {}, "outputs": [], "source": [ "from rateslib import *\n", "from pandas import Series, DataFrame\n", "\n", "today = dt(2025, 5, 12)\n", "\n", "ukti = IndexFixedRateBond(\n", " effective=dt(2024, 5, 27),\n", " termination=dt(2025, 5, 27),\n", " fixed_rate=2.0,\n", " notional=-10e6,\n", " index_base=381.0,\n", " index_method=\"daily\",\n", " index_lag=3,\n", " spec=\"uk_gb\"\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "2b92da11-10d3-49aa-a051-cf81472a6575", "metadata": {}, "outputs": [], "source": [ "ukti.cashflows()" ] }, { "cell_type": "markdown", "id": "9d6430cd-31cb-4e90-a873-f4a4b7ea9afe", "metadata": {}, "source": [ "### Adding ``index_fixings`` as a ``Series``\n", "\n", "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)" ] }, { "cell_type": "code", "execution_count": null, "id": "adbd6d4c-1b66-4115-813a-9709735b029f", "metadata": {}, "outputs": [], "source": [ "from pandas import DataFrame\n", "RPI_series = DataFrame([\n", " [dt(2024, 2, 1), 381.0],\n", " [dt(2024, 3, 1), 383.0],\n", " [dt(2024, 4, 1), 385.0],\n", " [dt(2024, 5, 1), 386.4],\n", " [dt(2024, 6, 1), 387.3],\n", " [dt(2024, 7, 1), 387.5],\n", " [dt(2024, 8, 1), 389.9],\n", " [dt(2024, 9, 1), 388.6],\n", " [dt(2024, 10, 1), 390.7],\n", " [dt(2024, 11, 1), 390.9],\n", " [dt(2024, 12, 1), 392.1],\n", " [dt(2025, 1, 1), 391.7],\n", " [dt(2025, 2, 1), 394.0],\n", " [dt(2025, 3, 1), 395.3]\n", "], columns=[\"month\", \"rate\"]).set_index(\"month\")[\"rate\"]\n", "RPI_series" ] }, { "cell_type": "markdown", "id": "310894e7-9958-4ec2-afdd-f5bc3cbf6417", "metadata": {}, "source": [ "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.\n", "\n", "For good order the ``index_base`` is expected to be (and will be visible in one of the columns in cashflows):\n", "\n", "$$ RPI_{Feb} + (RPI_{Mar} - RPI_{Feb}) * (27-1) / 31 = 382.677.. $$" ] }, { "cell_type": "code", "execution_count": null, "id": "d37fffd8-7b00-41ec-9797-236bea51c50c", "metadata": {}, "outputs": [], "source": [ "ukti = IndexFixedRateBond(\n", " effective=dt(2024, 5, 27),\n", " termination=dt(2025, 5, 27),\n", " fixed_rate=2.0,\n", " notional=-10e6,\n", " index_base=RPI_series,\n", " index_method=\"daily\",\n", " index_lag=3,\n", " index_fixings=RPI_series,\n", " spec=\"uk_gb\"\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "f3b3655b-ac2c-4fb2-a2d1-a8a6d5fbb025", "metadata": {}, "outputs": [], "source": [ "ukti.cashflows()" ] }, { "cell_type": "markdown", "id": "7347cd37-f19c-4cbf-b2a5-cfb2b64e839f", "metadata": {}, "source": [ "### Adding a discount *Curve*\n", "\n", "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**." ] }, { "cell_type": "code", "execution_count": null, "id": "f1f92db7-7d30-4382-b87e-ec79c45718e6", "metadata": {}, "outputs": [], "source": [ "disc_curve = Curve({today: 1.0, dt(2029, 1, 1): 0.95})" ] }, { "cell_type": "markdown", "id": "2825e662-5389-4da7-bc93-d7720ffbd06e", "metadata": {}, "source": [ "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.\n", "\n", "The prices shown below will be for the standard T+1 settlement under the ``uk_gb`` default ``spec``." ] }, { "cell_type": "code", "execution_count": null, "id": "9b06711b-22a1-4b16-86d4-515931c550d8", "metadata": {}, "outputs": [], "source": [ "ukti.cashflows(curves=[None, disc_curve])" ] }, { "cell_type": "code", "execution_count": null, "id": "531c130f-33b0-4e69-9edc-1b97ee8611ad", "metadata": {}, "outputs": [], "source": [ "ukti.rate(curves=[None, disc_curve], metric=\"clean_price\")" ] }, { "cell_type": "code", "execution_count": null, "id": "264e0a54-de5d-4160-94ac-e6d77a12d9c5", "metadata": {}, "outputs": [], "source": [ "ukti.rate(curves=[None, disc_curve], metric=\"index_clean_price\")" ] }, { "cell_type": "markdown", "id": "501d969a-ce75-44f3-857e-e90e6c569155", "metadata": {}, "source": [ "### Adding a forecast *Index Curve*\n", "\n", "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.\n", "\n", "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.\n", "\n", "We calibrate the Curve, for this example, not with market instruments but instead directly with *Index* ``Values`` we wish to use." ] }, { "cell_type": "code", "execution_count": null, "id": "a9efd445-f394-40a6-9acb-d54ad40510c4", "metadata": {}, "outputs": [], "source": [ "index_curve = Curve(\n", " nodes={\n", " dt(2025, 3, 1): 1.0,\n", " dt(2025, 4, 1): 1.0, \n", " dt(2025, 5, 1): 1.0,\n", " dt(2025, 6, 1): 1.0,\n", " dt(2025, 7, 1): 1.0,\n", " },\n", " index_lag=0,\n", " index_base=395.3,\n", " id=\"ic\",\n", ")\n", "solver = Solver(\n", " curves=[index_curve],\n", " instruments=[\n", " Value(effective=dt(2025, 4, 1), metric=\"index_value\", curves=\"ic\"),\n", " Value(effective=dt(2025, 5, 1), metric=\"index_value\", curves=\"ic\"),\n", " Value(effective=dt(2025, 6, 1), metric=\"index_value\", curves=\"ic\"),\n", " Value(effective=dt(2025, 7, 1), metric=\"index_value\", curves=\"ic\"),\n", " ],\n", " s=[396, 397.1, 398, 398.8],\n", " instrument_labels=[\"Apr\", \"May\", \"Jun\", \"Jul\"],\n", ")" ] }, { "cell_type": "markdown", "id": "5fcae14b-7564-4327-a1c2-7e4dfb0f2837", "metadata": {}, "source": [ "### An Instrument with mixed ``index_fixings`` and forecast fixings\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "3cfd3c2b-1153-45e8-9369-6db2ce3157b9", "metadata": {}, "outputs": [], "source": [ "ukti = IndexFixedRateBond(\n", " effective=dt(2024, 9, 16),\n", " termination=dt(2025, 9, 16),\n", " fixed_rate=3.0,\n", " notional=-15e6,\n", " index_base=RPI_series,\n", " index_method=\"daily\",\n", " index_lag=3,\n", " index_fixings=RPI_series,\n", " spec=\"uk_gb\",\n", " curves=[index_curve, disc_curve]\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "98130004-ee62-451b-989a-3610849456dc", "metadata": {}, "outputs": [], "source": [ "ukti.cashflows()" ] }, { "cell_type": "markdown", "id": "4bc9f60c-160e-4115-bd5e-911fa7d8a2fd", "metadata": {}, "source": [ "### Bonus: Risk to RPI prints.\n", "\n", "Actually the way we have constructed this *Index Curve* using the *Solver* means we can directly extract monetary sensitivities to the RPI index values" ] }, { "cell_type": "code", "execution_count": null, "id": "594498ce-8f61-4be7-9243-eca7a478c163", "metadata": {}, "outputs": [], "source": [ "ukti.delta(solver=solver)" ] }, { "cell_type": "markdown", "id": "d9546c6d-468b-473b-8b24-655dbdb350cb", "metadata": {}, "source": [ "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.\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "97c24f94-f93b-46b6-b21c-2982d98c67d0", "metadata": {}, "outputs": [], "source": [ "pv_0 = ukti.npv()\n", "pv_0" ] }, { "cell_type": "code", "execution_count": null, "id": "6431878c-0576-408e-bc2b-9cf5d946cb90", "metadata": {}, "outputs": [], "source": [ "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.\n", "solver.iterate()" ] }, { "cell_type": "code", "execution_count": null, "id": "0c3ad5a0-afd9-4c93-ac02-cb0b5fc822e7", "metadata": {}, "outputs": [], "source": [ "pv_1 = ukti.npv()\n", "pv_1 - pv_0" ] }, { "cell_type": "markdown", "id": "55bba2e5-dc73-4d86-a0de-b08f2130018d", "metadata": {}, "source": [ "### Other Instruments and Other Lags\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "9d4b2efe-0d64-4b5c-b370-ae293c2262ee", "metadata": {}, "outputs": [], "source": [ "ifl = IndexFixedLeg(\n", " effective=dt(2024, 12, 1),\n", " termination=\"8m\",\n", " frequency=\"M\",\n", " fixed_rate=1.0,\n", " notional=-15e6,\n", " convention=\"30360\",\n", " index_base=RPI_series,\n", " index_fixings=RPI_series,\n", " index_lag=2,\n", " index_method=\"monthly\",\n", " currency=\"gbp\"\n", ")" ] }, { "cell_type": "markdown", "id": "e4ce9fba-188f-4b18-8551-3bffbe731f54", "metadata": {}, "source": [ "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*." ] }, { "cell_type": "code", "execution_count": null, "id": "33c1767b-21c3-4f03-849d-d47752101e2a", "metadata": {}, "outputs": [], "source": [ "ifl.cashflows(curve=index_curve, disc_curve=disc_curve)" ] }, { "cell_type": "code", "execution_count": null, "id": "44718e80-470e-47a6-a8a5-6957ddb9dd00", "metadata": {}, "outputs": [], "source": [ "solver.delta(ifl.npv(curve=index_curve, disc_curve=disc_curve, local=True))" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.0" } }, "nbformat": 4, "nbformat_minor": 5 }