{ "cells": [ { "cell_type": "markdown", "id": "b7fefde3-a591-4ec6-91f4-dab1f4d0e961", "metadata": {}, "source": [ "# Inflation Indexes and Curves 2 (Quantlib comparison)\n", "\n", "This guide replicates and is a comparison to the Quantlib tutorial page at\n", "https://www.quantlibguide.com/Inflation%20indexes%20and%20curves.html\n", "\n", "## Inflation Indexes\n", "\n", "Historical index fixings in *rateslib* should be indexed to the 1st of the appropriate inflation month." ] }, { "cell_type": "code", "execution_count": null, "id": "36ce78fa-5eba-4062-9bee-c8429465a867", "metadata": {}, "outputs": [], "source": [ "from rateslib import *\n", "from pandas import Series, MultiIndex" ] }, { "cell_type": "code", "execution_count": null, "id": "599b18ea-97fb-4d63-8a61-13d4163401c0", "metadata": {}, "outputs": [], "source": [ "inflation_fixings = [\n", " (dt(2022, 1, 1), 110.70),\n", " (dt(2022, 2, 1), 111.74),\n", " (dt(2022, 3, 1), 114.46),\n", " (dt(2022, 4, 1), 115.11),\n", " (dt(2022, 5, 1), 116.07),\n", " (dt(2022, 6, 1), 117.01),\n", " (dt(2022, 7, 1), 117.14),\n", " (dt(2022, 8, 1), 117.85),\n", " (dt(2022, 9, 1), 119.26),\n", " (dt(2022, 10, 1), 121.03),\n", " (dt(2022, 11, 1), 120.95),\n", " (dt(2022, 12, 1), 120.52),\n", " (dt(2023, 1, 1), 120.27),\n", " (dt(2023, 2, 1), 121.24),\n", " (dt(2023, 3, 1), 122.34),\n", " (dt(2023, 4, 1), 123.12),\n", " (dt(2023, 5, 1), 123.15),\n", " (dt(2023, 6, 1), 123.47),\n", " (dt(2023, 7, 1), 123.36),\n", " (dt(2023, 8, 1), 124.03),\n", " (dt(2023, 9, 1), 124.43),\n", " (dt(2023, 10, 1), 124.54),\n", " (dt(2023, 11, 1), 123.85),\n", " (dt(2023, 12, 1), 124.05),\n", " (dt(2024, 1, 1), 123.60),\n", " (dt(2024, 2, 1), 124.37),\n", " (dt(2024, 3, 1), 125.31),\n", " (dt(2024, 4, 1), 126.05),\n", "]\n", "dates, values = zip(*inflation_fixings)\n", "fixings = Series(values, dates)" ] }, { "cell_type": "markdown", "id": "fe8d5863-766e-4e9b-ae67-1261a71e82f5", "metadata": {}, "source": [ "*Rateslib* contains an `index_value` method that will determine such for a given reference value date and other common parameters." ] }, { "cell_type": "code", "execution_count": null, "id": "a0a399a0-04b3-47ed-95a3-ad6b128504f5", "metadata": {}, "outputs": [], "source": [ "index_value(\n", " index_lag=0,\n", " index_method=\"monthly\",\n", " index_fixings=fixings,\n", " index_date=dt(2024, 3, 15)\n", ")" ] }, { "cell_type": "markdown", "id": "eb9cc687-77d3-4d34-b660-f46246da2270", "metadata": {}, "source": [ "For example to replicate the *Quantlib* example of a lagged reference date we can use:" ] }, { "cell_type": "code", "execution_count": null, "id": "952b0c42-4e83-4e5d-a50b-353a921210b0", "metadata": {}, "outputs": [], "source": [ "index_value(\n", " index_lag=3,\n", " index_method=\"daily\",\n", " index_fixings=fixings,\n", " index_date=dt(2024, 5, 15)\n", ")" ] }, { "cell_type": "markdown", "id": "25b970b3-67a3-4f6d-ad07-5425bd023f61", "metadata": {}, "source": [ "## Inflation Curves" ] }, { "cell_type": "markdown", "id": "262f443d-b6fb-48c8-89ff-da5bc52e0dcd", "metadata": {}, "source": [ "Create a nominal discount curve for cashflows. Calibrated to a 3% continuously compounded rate." ] }, { "cell_type": "code", "execution_count": null, "id": "be29773c-7582-4474-a180-a930f8a8a73e", "metadata": {}, "outputs": [], "source": [ "nominal_curve = Curve(\n", " nodes={dt(2024, 5, 11): 1.0, dt(2074, 5, 18): 1.0},\n", " interpolation=\"log_linear\",\n", " convention=\"Act365F\",\n", " id=\"discount\"\n", ")\n", "solver1 = Solver(\n", " curves=[nominal_curve],\n", " instruments=[Value(dt(2074, 5, 11), metric=\"cc_zero_rate\", curves=\"discount\")],\n", " s=[3.0],\n", " id=\"rates\",\n", " instrument_labels=[\"nominal\"],\n", ")" ] }, { "cell_type": "markdown", "id": "5845daa2-9245-4c25-80e2-541bc7a2ff95", "metadata": {}, "source": [ "Now create an inflation curve, based on the last known CPI print, calibrated with zero coupon inflation swaps rates. Notice that an inflation curve starts as of the last known fixing as its `index_base`. This is similar to *Quantlib*, not be design, but by necessity since this is the only information we have that can define the start of the curve." ] }, { "cell_type": "code", "execution_count": null, "id": "5f9577b4-1d49-4b88-a5f4-aba19f9ca198", "metadata": {}, "outputs": [], "source": [ "inflation_curve = Curve(\n", " nodes={\n", " dt(2024, 4, 1): 1.0, # <- last known inflation print.\n", " dt(2025, 5, 11): 1.0, # 1y\n", " dt(2026, 5, 11): 1.0, # 2y\n", " dt(2027, 5, 11): 1.0, # 3y\n", " dt(2028, 5, 11): 1.0, # 4y\n", " dt(2029, 5, 11): 1.0, # 5y\n", " dt(2031, 5, 11): 1.0, # 7y\n", " dt(2034, 5, 11): 1.0, # 10y\n", " dt(2036, 5, 11): 1.0, # 12y\n", " dt(2039, 5, 11): 1.0, # 15y\n", " dt(2044, 5, 11): 1.0, # 20y\n", " dt(2049, 5, 11): 1.0, # 25y\n", " dt(2054, 5, 11): 1.0, # 30y\n", " dt(2064, 5, 11): 1.0, # 40y\n", " dt(2074, 5, 11): 1.0, # 50y\n", " },\n", " interpolation=\"log_linear\",\n", " convention=\"Act365F\",\n", " index_base=126.05,\n", " index_lag=0,\n", " id=\"inflation\"\n", ")\n", "solver = Solver(\n", " pre_solvers=[solver1],\n", " curves=[inflation_curve],\n", " instruments=[\n", " ZCIS(dt(2024, 5, 11), \"1y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"2y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"3y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"4y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"5y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"7y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"10y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"12y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"15y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"20y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"25y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"30y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"40y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"50y\", spec=\"eur_zcis\", curves=[\"inflation\", \"discount\"], leg2_index_fixings=fixings),\n", " ],\n", " s=[2.93, 2.95, 2.965, 2.98, 3.0, 3.06, 3.175, 3.243, 3.293, 3.338, 3.348, 3.348, 3.308, 3.228],\n", " instrument_labels=[\"1y\", \"2y\", \"3y\", \"4y\", \"5y\", \"7y\", \"10y\", \"12y\", \"15\", \"20y\", \"25y\", \"30y\", \"40y\", \"50y\"],\n", " id=\"zcis\",\n", ") " ] }, { "cell_type": "markdown", "id": "8a30887d-1c92-4a3e-9c34-d26e7fd5d3dc", "metadata": {}, "source": [ "The data can be output to a table or plotted as below." ] }, { "cell_type": "code", "execution_count": null, "id": "a1bcfe1f-9830-4245-8624-c09118589dfe", "metadata": {}, "outputs": [], "source": [ "inflation_curve.plot(\"1m\")" ] }, { "cell_type": "code", "execution_count": null, "id": "15b7d3aa-688e-468a-b6ab-257f16ccad71", "metadata": {}, "outputs": [], "source": [ "inflation_curve.plot_index(right=dt(2030, 6, 1))" ] }, { "cell_type": "markdown", "id": "2e150b3e-5924-4776-891a-150b7a233db9", "metadata": {}, "source": [ "Some of the forecast values from the curve can be obtained directly from *Curve* methods." ] }, { "cell_type": "code", "execution_count": null, "id": "f06d993b-6eae-4b9f-a74f-87a9db8e6b43", "metadata": {}, "outputs": [], "source": [ "inflation_curve.index_value(dt(2027, 4, 1), index_lag=3, interpolation=\"monthly\")" ] }, { "cell_type": "code", "execution_count": null, "id": "38426aa2-d030-4ed2-a016-b0f64eaed561", "metadata": {}, "outputs": [], "source": [ "inflation_curve.index_value(dt(2027, 4, 1), index_lag=0, interpolation=\"monthly\")" ] }, { "cell_type": "code", "execution_count": null, "id": "a66eb510-f446-40b4-9746-0ae3f57c466b", "metadata": {}, "outputs": [], "source": [ "inflation_curve.index_value(dt(2027, 5, 15), index_lag=3, interpolation=\"daily\")" ] }, { "cell_type": "markdown", "id": "9c81acc5-e233-4b45-8b89-efe3ba8b0b2a", "metadata": {}, "source": [ "## Seasonality\n", "\n", "The way *rateslib* handles seasonality is to replicate it via its *CompositeCurve* framework. With this approach, adding seasonality can be done in a multitude of ways, but since Quantlib has used *seasonaility factors* we will synthesize that approach within *rateslib's* framework. We create a new curve with **node dates** reflecting the key dates used by Quantlib.\n", "\n", "The theory here for *rateslib* is as follows.\n", "\n", "A discount factor on a *rateslib* *CompositeCurve* is very approximately the product of its contained curves (c1 and c2):\n", "\n", "$$\n", "v_{cc} \\approx v_{c1} v_{c2}\n", "$$\n", "\n", "The *CompositeCurve* index value takes its *index base* from the first (main) curve in its composition, therefore:\n", "\n", "$$\n", "I_{value(cc)} = \\frac{I_{base(c1)}}{v_{cc}} \\approx = \\frac{I_{base(c1)}}{v_{c1}v_{c2}} = I_{value(c1)} \\frac{1}{v_{c2}}\n", "$$\n", "\n", "If we set the *index base* on the seasonality curve (c2) **exactly to 1.0** this equation reduces to:\n", "\n", "$$\n", "I_{value(cc)} \\approx I_{value(c1)} I_{value(c2)}\n", "$$\n", "\n", "So the index value on the *CompositeCurve* equals the product of the two index values.\n", "\n", "Thus, if we set the set the *index values* on the seasonality curve (c2) directly they can act directly as multipliers to the underlying inflation values on the inflation curve.\n", "This seasonality curve can be calibrated using a *Solver*, and in this particular case (to match Quantlib) they are calibrated directly with *index values*.\n", "\n", "Here we add the same seasonality factors in years 2025, 2026 and 2027." ] }, { "cell_type": "code", "execution_count": null, "id": "3d758f00-9c4d-480d-b45a-1293c9adf134", "metadata": {}, "outputs": [], "source": [ "season = Curve(\n", " nodes={\n", " dt(2024, 4, 1): 1.0,\n", " dt(2024, 12, 1): 1.0,\n", " **{dt(2025, _, 1): 1.0 for _ in range(1, 13)},\n", " **{dt(2026, _, 1): 1.0 for _ in range(1, 13)},\n", " **{dt(2027, _, 1): 1.0 for _ in range(1, 13)},\n", " dt(2074, 5, 11): 1.0,\n", " },\n", " interpolation=\"log_linear\",\n", " convention=\"Act365F\",\n", " index_base=1.0, # <- as per theory above\n", " index_lag=0, # <- matches the main inflation curve\n", " id=\"seasonality\"\n", ")" ] }, { "cell_type": "markdown", "id": "3c3cd72c-fb9b-451d-a7e5-1bcf6a9a03ce", "metadata": {}, "source": [ "The seasonality factors will be added by directly inserting index values at specific dates." ] }, { "cell_type": "code", "execution_count": null, "id": "0c106636-d165-4319-93cb-10f7567d4cac", "metadata": {}, "outputs": [], "source": [ "multipliers = [\n", " 1.003245,\n", " 1.001994,\n", " 0.999715,\n", " 1.000495,\n", " 1.000929,\n", " 0.998687,\n", " 0.995949,\n", " 0.994682,\n", " 0.995949,\n", " 1.000519,\n", " 1.003705,\n", " 1.004186,\n", "]\n", "season_solver = Solver(\n", " curves=[season],\n", " instruments=\\\n", " [Value(dt(2024, 12, 1), curves=[season], metric=\"index_value\")] +\\\n", " [Value(dt(2025, _, 1), curves=[season], metric=\"index_value\") for _ in range(1, 13)] +\\\n", " [Value(dt(2026, _, 1), curves=[season], metric=\"index_value\") for _ in range(1, 13)] +\\\n", " [Value(dt(2027, _, 1), curves=[season], metric=\"index_value\") for _ in range(1, 13)],\n", " s=[1.0] + multipliers*3\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "be4890f8-bafc-4bd2-9bda-3c2e3bdc25bd", "metadata": {}, "outputs": [], "source": [ "season.plot(\"1b\", left=dt(2024, 4, 1), right=dt(2028, 4, 1))" ] }, { "cell_type": "markdown", "id": "6705e4a0-6720-4697-af6a-d65c61d5f36e", "metadata": {}, "source": [ "Once the seasonality is designed it must be composited with an underlying inflation curve and the instruments re-solved" ] }, { "cell_type": "code", "execution_count": null, "id": "b25f41c1-03e1-470e-b663-a8004da0cca3", "metadata": {}, "outputs": [], "source": [ "inflation_with_season = CompositeCurve([inflation_curve, season], id=\"inflation_s\")" ] }, { "cell_type": "code", "execution_count": null, "id": "23864f06-446d-4daf-8997-968d1c2d0d19", "metadata": {}, "outputs": [], "source": [ "solver = Solver(\n", " pre_solvers=[solver1], # nominal discount curve\n", " curves=[inflation_with_season, inflation_curve],\n", " instruments=[\n", " ZCIS(dt(2024, 5, 11), \"1y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"2y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"3y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"4y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"5y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"7y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"10y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"12y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"15y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"20y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"25y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"30y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"40y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ZCIS(dt(2024, 5, 11), \"50y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], leg2_index_fixings=fixings),\n", " ],\n", " s=[2.93, 2.95, 2.965, 2.98, 3.0, 3.06, 3.175, 3.243, 3.293, 3.338, 3.348, 3.348, 3.308, 3.228],\n", " instrument_labels=[\"1y\", \"2y\", \"3y\", \"4y\", \"5y\", \"7y\", \"10y\", \"12y\", \"15y\", \"20y\", \"25y\", \"30y\", \"40y\", \"50y\"],\n", " id=\"zcis\",\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "11873f01-c672-4f12-bc59-31bfd23c4fde", "metadata": {}, "outputs": [], "source": [ "inflation_with_season.plot(\"1b\", comparators=[inflation_curve], left=dt(2024, 9, 1), right=dt(2030, 9, 1))" ] }, { "cell_type": "code", "execution_count": null, "id": "c6cdb2f8-cc8a-43a6-8b13-4303c971eafc", "metadata": {}, "outputs": [], "source": [ "inflation_with_season.plot_index(comparators=[inflation_curve], left=dt(2024, 9, 1), right=dt(2030, 9, 1))" ] }, { "cell_type": "markdown", "id": "68c025d7-0e9a-4e63-b9b7-2650e3f10008", "metadata": {}, "source": [ "### New sampled values" ] }, { "cell_type": "code", "execution_count": null, "id": "ca3d660e-396f-4aae-97f7-3d5f86f79c32", "metadata": {}, "outputs": [], "source": [ "inflation_with_season.index_value(dt(2027, 4, 1), index_lag=3, interpolation=\"monthly\")" ] }, { "cell_type": "code", "execution_count": null, "id": "65fb0c62-9409-4d99-aa4a-6b8c7856ec0e", "metadata": {}, "outputs": [], "source": [ "inflation_with_season.index_value(dt(2027, 4, 1), index_lag=0, interpolation=\"monthly\")" ] }, { "cell_type": "code", "execution_count": null, "id": "37f9c148-456f-4054-83a1-7e789aa6d2c7", "metadata": {}, "outputs": [], "source": [ "inflation_with_season.index_value(dt(2027, 5, 15), index_lag=3, interpolation=\"daily\")" ] }, { "cell_type": "markdown", "id": "bf1f3367-21f5-439f-89fd-9df3c078ad62", "metadata": {}, "source": [ "The trick here is obviously to find a representation of a `seasonaility` curve that matches one's expectation of seasonality adjustments. Here, a `Solver` calibration was used to separately solve the `seasonality` curve to inflation rate adjustments." ] }, { "cell_type": "code", "execution_count": null, "id": "9599f39c-39c2-45fc-bdd9-8b8086919c7f", "metadata": {}, "outputs": [], "source": [ "for i, date in enumerate([dt(2025, _, 1) for _ in range(1, 13)]+[dt(2026, _, 1) for _ in range(1, 13)]):\n", " a = inflation_curve.index_value(date, index_lag=0, interpolation=\"monthly\")\n", " b = inflation_with_season.index_value(date, index_lag=0, interpolation=\"monthly\")\n", " print(f\"Date: {date}, Inflation curve: {float(a):.5f}, With seasonality: {float(b):.5f}, Multiplier: {float(b/a):.7f}, Intended Multiplier: {multipliers[i%12]:.7f}\")" ] }, { "cell_type": "markdown", "id": "6da82d38-4183-4adf-83d8-fd57cf7ab40e", "metadata": {}, "source": [ "## Inflation Swap and DV01\n", "\n", "We can easily construct a ZCIS or other type of inflation based *instrument* and use the native `delta` and `gamma` methods associated with a `Solver` to extract risk sensitivities." ] }, { "cell_type": "code", "execution_count": null, "id": "58a79013-bf18-415b-bc85-8e0ad6fe2afa", "metadata": {}, "outputs": [], "source": [ "zcis = ZCIS(dt(2024, 3, 11), \"4y\", spec=\"eur_zcis\", curves=[\"inflation_s\", \"discount\"], fixed_rate=3.0, leg2_index_fixings=fixings)" ] }, { "cell_type": "code", "execution_count": null, "id": "b557f116-c639-4879-8a4b-d7acca8f8407", "metadata": {}, "outputs": [], "source": [ "zcis.rate(solver=solver)" ] }, { "cell_type": "code", "execution_count": null, "id": "393b055d-bd42-4311-b067-99ff3723d597", "metadata": {}, "outputs": [], "source": [ "zcis.npv(solver=solver)" ] }, { "cell_type": "code", "execution_count": null, "id": "0de7b460-df8a-4e5a-aba9-44044dfd74db", "metadata": {}, "outputs": [], "source": [ "df = zcis.delta(solver=solver)\n", "descriptors = df.agg([\"sum\"])\n", "descriptors.index = MultiIndex.from_tuples([(\"sum\", \"sum\", \"sum\")])\n", "df.style.format(precision=0).concat(descriptors.style.format(precision=0))" ] }, { "cell_type": "markdown", "id": "57009ff3-cf84-477b-9134-fd696bc4e6bf", "metadata": {}, "source": [ "This risk display shows no exposure to the **seasonality** configuration becuase the *Solver* that was used to calibrate the seasonality was not added as a ``pre_solver`` to the *Solver* used in the last step. Therefore it treats the seasonality as a **constant** and not as variable to which risk sensitivities can be obtained.\n", "\n", "To observe the change, simply replace ``pre_solvers=[solver1]`` with ``pre_solvers=[solver1, season_solver]``, in the above *Solver* and re run the cells." ] }, { "cell_type": "code", "execution_count": null, "id": "ea42c6e4-7001-4c6c-883b-a1b07bca9aef", "metadata": {}, "outputs": [], "source": [ "zcis.gamma(solver=solver).style.format(precision=1)" ] } ], "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 }