{ "cells": [ { "cell_type": "markdown", "id": "eae7849d-bb6f-4016-879c-46b21353097e", "metadata": {}, "source": [ "# A EURUSD market for IRS, cross-currency and FX volatility\n", "\n", "In this notebook we demonstrate the code for *rateslib* to build:\n", "\n", "- local currency interest rate curves in EUR and USD from RFR swaps,\n", "- collateral curves accounting for the cross-currency basis and FX swap points,\n", "- volatility surface priced from FX volatility products.\n" ] }, { "cell_type": "markdown", "id": "8195cc4e-dcaf-492a-b3b0-218ac28550e3", "metadata": {}, "source": [ "### Input market data\n", "\n", "First things first we need market data for the interest rates curves and forward FX curves.\n", "This data was observed on 28th May 2024." ] }, { "cell_type": "code", "execution_count": null, "id": "e25b47f0-de95-4f47-9de2-9e50c9799a05", "metadata": {}, "outputs": [], "source": [ "from rateslib import *\n", "import numpy as np\n", "from pandas import DataFrame" ] }, { "cell_type": "code", "execution_count": null, "id": "e352b0b9-ff4b-4528-9237-2deba2fc0bcf", "metadata": {}, "outputs": [], "source": [ "fxr = FXRates({\"eurusd\": 1.0867}, settlement=dt(2024, 5, 30))" ] }, { "cell_type": "code", "execution_count": null, "id": "c3713f62-9278-41f6-a956-4a6a103776d8", "metadata": {}, "outputs": [], "source": [ "mkt_data = DataFrame(\n", " data=[['1w', 3.9035,5.3267,3.33,],\n", " ['2w', 3.9046,5.3257,6.37,],\n", " ['3w',3.8271,5.3232,9.83,],\n", " ['1m',3.7817,5.3191,13.78,],\n", " ['2m',3.7204,5.3232,30.04,],\n", " ['3m',3.667,5.3185,45.85,-2.5],\n", " ['4m',3.6252,5.3307,61.95,],\n", " ['5m',3.587,5.3098,78.1,],\n", " ['6m',3.5803,5.3109,94.25,-3.125],\n", " ['7m',3.5626,5.301,110.82,],\n", " ['8m',3.531,5.2768,130.45,],\n", " ['9m',3.5089,5.2614,145.6,-7.25],\n", " ['10m',3.4842,5.2412,162.05,],\n", " ['11m',3.4563,5.2144,178,],\n", " ['1y',3.4336,5.1936,None,-6.75],\n", " ['15m',3.3412,5.0729,None,-6.75],\n", " ['18m',3.2606,4.9694,None,-6.75],\n", " ['21m',3.1897,4.8797,None,-7.75],\n", " ['2y',3.1283,4.8022,None,-7.875],\n", " ['3y',2.9254,4.535,None,-9],\n", " ['4y',2.81,4.364,None,-10.125],\n", " ['5y',2.7252,4.256,None,-11.125],\n", " ['6y',2.6773,4.192,None,-12.125],\n", " ['7y',2.6541,4.151,None,-13],\n", " ['8y',2.6431,4.122,None,-13.625],\n", " ['9y',2.6466,4.103,None,-14.25],\n", " ['10y',2.6562,4.091,None,-14.875],\n", " ['12y',2.6835,4.084,None,-16.125],\n", " ['15y',2.7197,4.08,None,-17],\n", " ['20y',2.6849,4.04,None,-16],\n", " ['25y',2.6032,3.946,None,-12.75],\n", " ['30y',2.5217,3.847,None,-9.5]],\n", " columns=[\"tenor\", \"estr\", \"sofr\", \"fx_swap\", \"xccy\"],\n", ")\n", "mkt_data" ] }, { "cell_type": "markdown", "id": "f91c7283-fb3d-434d-8b33-68a095e8606c", "metadata": {}, "source": [ "### Solving rates curves and FX forwards curve\n", "\n", "We will create all *Curves* and solve them all using the *Solver*. It is possible to solve everything simultaneously in a\n", "single *Solver* but this is less efficient than decoupling the known separable components, and using multiple *Solvers* in a\n", "dependency chain." ] }, { "cell_type": "code", "execution_count": null, "id": "4d3dcc1a-a4e3-46c4-a32c-841a4820da89", "metadata": {}, "outputs": [], "source": [ "eur = Curve(\n", " nodes={\n", " dt(2024, 5, 28): 1.0,\n", " **{add_tenor(dt(2024, 5, 30), _, \"F\", \"tgt\"): 1.0 for _ in mkt_data[\"tenor\"]}\n", " },\n", " calendar=\"tgt\",\n", " interpolation=\"log_linear\",\n", " convention=\"act360\",\n", " id=\"estr\",\n", ")\n", "usd = Curve(\n", " nodes={\n", " dt(2024, 5, 28): 1.0,\n", " **{add_tenor(dt(2024, 5, 30), _, \"F\", \"nyc\"): 1.0 for _ in mkt_data[\"tenor\"]}\n", " },\n", " calendar=\"nyc\",\n", " interpolation=\"log_linear\",\n", " convention=\"act360\",\n", " id=\"sofr\",\n", ")\n", "eurusd = Curve(\n", " nodes={\n", " dt(2024, 5, 28): 1.0,\n", " **{add_tenor(dt(2024, 5, 30), _, \"F\", \"tgt\"): 1.0 for _ in mkt_data[\"tenor\"]}\n", " },\n", " interpolation=\"log_linear\",\n", " convention=\"act360\",\n", " id=\"eurusd\",\n", ")" ] }, { "cell_type": "markdown", "id": "aaf83c99-896c-4974-831c-d29cb127b61a", "metadata": {}, "source": [ "With *Curves* created but not necessarily calibrated we can design the FXForwards market mapping:" ] }, { "cell_type": "code", "execution_count": null, "id": "f4b3cc9c-4962-4a65-b934-35a02b5ed33a", "metadata": {}, "outputs": [], "source": [ "fxf = FXForwards(\n", " fx_rates=fxr,\n", " fx_curves={\"eureur\": eur, \"eurusd\": eurusd, \"usdusd\": usd}\n", ")" ] }, { "cell_type": "markdown", "id": "416dc324-69b0-438a-85be-7a40d1c68969", "metadata": {}, "source": [ "The *Instruments* used to solve the ESTR curve are ESTR swaps and the SOFR curve are SOFR swaps:" ] }, { "cell_type": "code", "execution_count": null, "id": "560268b8-b321-4614-9f0b-b8c0c99964b6", "metadata": {}, "outputs": [], "source": [ "estr_swaps = [IRS(dt(2024, 5, 30), _, spec=\"eur_irs\", curves=\"estr\") for _ in mkt_data[\"tenor\"]]\n", "estr_rates = mkt_data[\"estr\"].tolist()\n", "labels = mkt_data[\"tenor\"].to_list()\n", "sofr_swaps = [IRS(dt(2024, 5, 30), _, spec=\"usd_irs\", curves=\"sofr\") for _ in mkt_data[\"tenor\"]]\n", "sofr_rates = mkt_data[\"sofr\"].tolist()" ] }, { "cell_type": "code", "execution_count": null, "id": "dab3168e-26ef-4676-842e-8f04ac48461b", "metadata": {}, "outputs": [], "source": [ "eur_solver = Solver(\n", " curves=[eur],\n", " instruments=estr_swaps,\n", " s=estr_rates,\n", " fx=fxf,\n", " instrument_labels=labels,\n", " id=\"eur\",\n", ")\n", "usd_solver = Solver(\n", " curves=[usd],\n", " instruments=sofr_swaps,\n", " s=sofr_rates,\n", " fx=fxf,\n", " instrument_labels=labels,\n", " id=\"usd\",\n", ")" ] }, { "cell_type": "markdown", "id": "6343067d-fe16-4887-95bc-f36bc7fe187d", "metadata": {}, "source": [ "The cross currency curve use a combination of *FXSwaps* and *XCS*:" ] }, { "cell_type": "code", "execution_count": null, "id": "f52f54e1-bad4-4a6c-82f6-98432e69442d", "metadata": {}, "outputs": [], "source": [ "fxswaps = [FXSwap(dt(2024, 5, 30), _, pair=\"eurusd\", curves=[None, \"eurusd\", None, \"sofr\"]) for _ in mkt_data[\"tenor\"][0:14]]\n", "fxswap_rates = mkt_data[\"fx_swap\"][0:14].tolist()\n", "xcs = [XCS(dt(2024, 5, 30), _, spec=\"eurusd_xcs\", curves=[\"estr\", \"eurusd\", \"sofr\", \"sofr\"]) for _ in mkt_data[\"tenor\"][14:]]\n", "xcs_rates = mkt_data[\"xccy\"][14:].tolist()" ] }, { "cell_type": "code", "execution_count": null, "id": "706c0dd6-60ad-405b-989c-837bb8e5f2a6", "metadata": {}, "outputs": [], "source": [ "fx_solver = Solver(\n", " pre_solvers=[eur_solver, usd_solver],\n", " curves=[eurusd],\n", " instruments=fxswaps + xcs,\n", " s=fxswap_rates + xcs_rates,\n", " fx=fxf,\n", " instrument_labels=labels,\n", " id=\"eurusd_xccy\",\n", ")" ] }, { "cell_type": "markdown", "id": "c8dcfd5f-c601-4bdf-84de-124bbb083b67", "metadata": {}, "source": [ "## Solving an FX Vol Surface\n", "\n", "Next we will use the market FX volatility quotes to build a surface. These prices are all expressed in log-normal vol terms under normal market conventions and the instruments 1Y or less use spot unadjusted delta and those longer than 1y use forward undajusted delta." ] }, { "cell_type": "code", "execution_count": null, "id": "78a4c4bf-2ed1-4beb-b0f7-8db0e0b263e6", "metadata": {}, "outputs": [], "source": [ "vol_data = DataFrame(\n", " data=[\n", " ['1w',4.535,-0.047,0.07,-0.097,0.252],\n", " ['2w',5.168,-0.082,0.077,-0.165,0.24],\n", " ['3w',5.127,-0.175,0.07,-0.26,0.233],\n", " ['1m',5.195,-0.2,0.07,-0.295,0.235],\n", " ['2m',5.237,-0.28,0.087,-0.535,0.295],\n", " ['3m',5.257,-0.363,0.1,-0.705,0.35],\n", " ['4m',5.598,-0.47,0.123,-0.915,0.422],\n", " ['5m',5.776,-0.528,0.133,-1.032,0.463],\n", " ['6m',5.92,-0.565,0.14,-1.11,0.49],\n", " ['9m',6.01,-0.713,0.182,-1.405,0.645],\n", " ['1y',6.155,-0.808,0.23,-1.585,0.795],\n", " ['18m',6.408,-0.812,0.248,-1.588,0.868],\n", " ['2y',6.525,-0.808,0.257,-1.58,0.9],\n", " ['3y',6.718,-0.733,0.265,-1.45,0.89],\n", " ['4y',7.025,-0.665,0.265,-1.31,0.885],\n", " ['5y',7.26,-0.62,0.26,-1.225,0.89],\n", " ['6y',7.508,-0.516,0.27,-0.989,0.94],\n", " ['7y',7.68,-0.442,0.278,-0.815,0.975],\n", " ['10y',8.115,-0.267,0.288,-0.51,1.035],\n", " ['15y',8.652,-0.325,0.362,-0.4,1.195],\n", " ['20y',8.651,-0.078,0.343,-0.303,1.186],\n", " ['25y',8.65,-0.029,0.342,-0.218,1.178],\n", " ['30y',8.65,0.014,0.341,-0.142,1.171],\n", " ],\n", " columns=[\"tenor\", \"atm\", \"25drr\", \"25dbf\", \"10drr\", \"10dbf\"]\n", ")\n", "vol_data[\"expiry\"] = [add_tenor(dt(2024, 5, 28), _, \"MF\", \"tgt\") for _ in vol_data[\"tenor\"]]\n", "vol_data" ] }, { "cell_type": "markdown", "id": "a7ad062f-8386-49a7-a8cb-333b577b9e1b", "metadata": {}, "source": [ "A *Surface* is defined by given expiries and delta grdipoints. All vol values are initially set to 5.0, and will be calibrated by the \n", "*Instruments*." ] }, { "cell_type": "code", "execution_count": null, "id": "ca180225-c2d4-438a-9d11-f9d829d4ff92", "metadata": {}, "outputs": [], "source": [ "surface = FXDeltaVolSurface(\n", " eval_date=dt(2024, 5, 28),\n", " expiries=vol_data[\"expiry\"],\n", " delta_indexes=[0.1, 0.25, 0.5, 0.75, 0.9],\n", " node_values=np.ones((23, 5))*5.0,\n", " delta_type=\"forward\",\n", " id=\"eurusd_vol\"\n", ")" ] }, { "cell_type": "markdown", "id": "e773b098-7f41-41d9-8e9c-908c6cec81b6", "metadata": {}, "source": [ "Define the instruments and their rates for 1Y or less:" ] }, { "cell_type": "code", "execution_count": null, "id": "459da8b9-413e-480a-9869-62f2189b9977", "metadata": {}, "outputs": [], "source": [ "fx_args = dict(\n", " pair=\"eurusd\", \n", " curves=[None, \"eurusd\", None, \"sofr\"], \n", " calendar=\"tgt\", \n", " delivery_lag=2, \n", " payment_lag=2,\n", " eval_date=dt(2024, 5, 28),\n", " modifier=\"MF\",\n", " premium_ccy=\"usd\",\n", " vol=\"eurusd_vol\",\n", ")\n", "\n", "instruments_le_1y, rates_le_1y, labels_le_1y = [], [], []\n", "for row in range(11):\n", " instruments_le_1y.extend([\n", " FXStraddle(strike=\"atm_delta\", expiry=vol_data[\"expiry\"][row], delta_type=\"spot\", **fx_args),\n", " FXRiskReversal(strike=[\"-25d\", \"25d\"], expiry=vol_data[\"expiry\"][row], delta_type=\"spot\", **fx_args),\n", " FXBrokerFly(strike=[\"-25d\", \"atm_delta\", \"25d\"], expiry=vol_data[\"expiry\"][row], delta_type=\"spot\", **fx_args),\n", " FXRiskReversal(strike=[\"-10d\", \"10d\"], expiry=vol_data[\"expiry\"][row], delta_type=\"spot\", **fx_args),\n", " FXBrokerFly(strike=[\"-10d\", \"atm_delta\", \"10d\"], expiry=vol_data[\"expiry\"][row], delta_type=\"spot\", **fx_args),\n", " ])\n", " rates_le_1y.extend([vol_data[\"atm\"][row], vol_data[\"25drr\"][row], vol_data[\"25dbf\"][row], vol_data[\"10drr\"][row], vol_data[\"10dbf\"][row]])\n", " labels_le_1y.extend([f\"atm_{row}\", f\"25drr_{row}\", f\"25dbf_{row}\", f\"10drr_{row}\", f\"10dbf_{row}\"])" ] }, { "cell_type": "markdown", "id": "57229c3e-1879-4981-8cb6-36fc5cccb06c", "metadata": {}, "source": [ "Also define the instruments and rates for greater than 1Y:" ] }, { "cell_type": "code", "execution_count": null, "id": "a5bb6c03-b28c-48cb-bd4b-b814cb41cfdf", "metadata": {}, "outputs": [], "source": [ "instruments_gt_1y, rates_gt_1y, labels_gt_1y = [], [], []\n", "for row in range(11, 23):\n", " instruments_gt_1y.extend([\n", " FXStraddle(strike=\"atm_delta\", expiry=vol_data[\"expiry\"][row], delta_type=\"forward\", **fx_args),\n", " FXRiskReversal(strike=[\"-25d\", \"25d\"], expiry=vol_data[\"expiry\"][row], delta_type=\"forward\", **fx_args),\n", " FXBrokerFly(strike=[\"-25d\", \"atm_delta\", \"25d\"], expiry=vol_data[\"expiry\"][row], delta_type=\"forward\", **fx_args),\n", " FXRiskReversal(strike=[\"-10d\", \"10d\"], expiry=vol_data[\"expiry\"][row], delta_type=\"forward\", **fx_args),\n", " FXBrokerFly(strike=[\"-10d\", \"atm_delta\", \"10d\"], expiry=vol_data[\"expiry\"][row], delta_type=\"forward\", **fx_args),\n", " ])\n", " rates_gt_1y.extend([vol_data[\"atm\"][row], vol_data[\"25drr\"][row], vol_data[\"25dbf\"][row], vol_data[\"10drr\"][row], vol_data[\"10dbf\"][row]])\n", " labels_gt_1y.extend([f\"atm_{row}\", f\"25drr_{row}\", f\"25dbf_{row}\", f\"10drr_{row}\", f\"10dbf_{row}\"])" ] }, { "cell_type": "markdown", "id": "fc52314d-837d-4a51-a2ee-aa0cda11f28c", "metadata": {}, "source": [ "Now solve for all calibrating instruments and rates. " ] }, { "cell_type": "code", "execution_count": null, "id": "5eaff434-c158-4c21-af2c-4531d1f840ac", "metadata": {}, "outputs": [], "source": [ "surface_solver = Solver(\n", " surfaces=[surface],\n", " instruments=instruments_le_1y+instruments_gt_1y,\n", " s=rates_le_1y+rates_gt_1y,\n", " instrument_labels=labels_le_1y+labels_gt_1y,\n", " fx=fxf,\n", " pre_solvers=[fx_solver],\n", " id=\"eurusd_vol\"\n", ")" ] }, { "cell_type": "markdown", "id": "92e1e04b-121f-4de2-9cb1-00cd716fdacd", "metadata": {}, "source": [ "### 3D Surface Plot and Cross-sectional Smiles" ] }, { "cell_type": "code", "execution_count": null, "id": "98ccdc8c-1738-4144-aaaa-e0bf62817be2", "metadata": {}, "outputs": [], "source": [ "surface.plot()" ] }, { "cell_type": "code", "execution_count": null, "id": "963077a0-249f-4632-a593-16c806dbc09f", "metadata": {}, "outputs": [], "source": [ "surface.smiles[0].plot(comparators=surface.smiles[1:])" ] }, { "cell_type": "markdown", "id": "7d447f20-17cf-48e0-88cf-2cb10421dda8", "metadata": {}, "source": [ "## Calculating a generic option price" ] }, { "cell_type": "code", "execution_count": null, "id": "cd193993-a065-402a-9654-0faf0eb9e676", "metadata": {}, "outputs": [], "source": [] } ], "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.12.4" } }, "nbformat": 4, "nbformat_minor": 5 }