{ "cells": [ { "cell_type": "markdown", "id": "087fc973-33e9-4d99-a264-1b570dd119a2", "metadata": {}, "source": [ "# FX Volatility Surface Temporal Interpolation" ] }, { "cell_type": "code", "execution_count": null, "id": "8681bb4b-c22f-451d-a2dd-35b50dde60be", "metadata": {}, "outputs": [], "source": [ "from rateslib import *\n", "from pandas import Series\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "markdown", "id": "110beaf4-55fe-43f7-9aa0-8c4443e2a7a6", "metadata": {}, "source": [ "This article will demonstrate how *rateslib* performs temporal interpolation when FX volatility *Surfaces* are constructued with cross-sectional *Smiles* at given expiries." ] }, { "cell_type": "markdown", "id": "c9ff9d40-a4f6-4cc0-9886-3a3e5ec75002", "metadata": {}, "source": [ "## FXDeltaVolSurfaces" ] }, { "cell_type": "markdown", "id": "9509d3c8-8fc0-4191-89c2-14983c590d4c", "metadata": {}, "source": [ "The default *FXDeltaVolSurface* is constructed with parametrised cross-sectional\n", "*FXDeltaVolSmiles*. The **temporal interpolation** method determines a *delta-node* between the two surrounding *Smiles* using linear total variance, which has been shown (see Clark: FX Option Pricing) to be equivalent to flat forward volatility within the interval.\n", "\n", "Consider Table 4.7 of that same publication, *Clark: FX Option Pricing*. To replicate the data there we will create a *Surface* here which has flat line *Smiles* (i.e. there is just one volatility datapoint at each expiry) in the following way:" ] }, { "cell_type": "code", "execution_count": null, "id": "e93ecafc-954a-40b0-a60d-4795f5bdde1c", "metadata": {}, "outputs": [], "source": [ "fxvs = FXDeltaVolSurface(\n", " expiries=[\n", " dt(2024, 2, 12), # Spot\n", " dt(2024, 2, 16), # 1W\n", " dt(2024, 2, 23), # 2W\n", " dt(2024, 3, 1), # 3W\n", " dt(2024, 3, 8), # 4W\n", " ],\n", " delta_indexes=[0.5],\n", " node_values=[[8.15], [11.95], [11.97], [11.75], [11.80]],\n", " eval_date=dt(2024, 2, 9),\n", " delta_type=\"forward\",\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "6c36686c-a22e-47bf-9dbb-49df07964bdc", "metadata": {}, "outputs": [], "source": [ "fxvs.plot()" ] }, { "cell_type": "markdown", "id": "d4d99760-ec13-461a-9b0d-939693e13347", "metadata": {}, "source": [ "In the time/expiry dimension we will plot the volatility as measured for every calendar day expiry for the four weeks, using the 50% delta midpoint on each *Smile*." ] }, { "cell_type": "code", "execution_count": null, "id": "23279170-3494-4923-b8ad-bc26d818ff03", "metadata": {}, "outputs": [], "source": [ "cal = get_calendar(\"all\")\n", "x, y = [], []\n", "for date in cal.cal_date_range(dt(2024, 2, 10), dt(2024, 3, 8)):\n", " x.append(date)\n", " y.append(fxvs.get_smile(date)[0.5])\n", "\n", "fig, ax = plt.subplots(1,1)\n", "plt.xticks(rotation=90)\n", "ax.plot(x,y)" ] }, { "cell_type": "markdown", "id": "c7c99238-acaf-413f-988a-8d182e84f66f", "metadata": {}, "source": [ "### Using Weights\n", "\n", "The comment in the publication is that markets do not assign volatility to calendar days when the market is closed.\n", "In this section we will provide weights that manipulate the forward volatility and align with table 4.7.\n", "\n", "| Date | Weight | Volatility to Expiry |\n", "| ---- | ------ | ------------ |\n", "| 10 Feb '24 | 0.0 | 0.0 |\n", "| 11 Feb '24 | 0.0 | 0.0 |\n", "| **12 Feb '24** | 1.0 | **8.15** |\n", "| 13 Feb '24 | 1.0 | 9.99 |\n", "| 14 Feb '24 | 1.0 | 10.95 |\n", "| 15 Feb '24 | 1.0 | 11.54 |\n", "| **16 Feb '24** | 1.0 | **11.95** |\n", "| 17 Feb '24 | 0.0 | 11.18 |\n", "| 18 Feb '24 | 0.0 | 10.54 |\n", "| 19 Feb '24 | 1.0 | 10.96 |\n", "| 20 Feb '24 | 1.0 | 11.29 |\n", "| 21 Feb '24 | 1.0 | 11.56 |\n", "| 22 Feb '24 | 1.0 | 11.78 |\n", "| **23 Feb '24** | 1.0 | **11.97** |\n", "| 24 Feb '24 | 0.0 | 11.56 |\n", "| 25 Feb '24 | 0.0 | 11.20 |\n", "| 26 Feb '24 | 1.0 | 11.34 |\n", "| 27 Feb '24 | 1.0 | 11.46 |\n", "| 28 Feb '24 | 1.0 | 11.57 |\n", "| 29 Feb '24 | 1.0 | 11.66 |\n", "| **1 Mar '24** | 1.0 | **11.75** |\n", "| 2 Mar '24 | 0.0 | 11.48 |\n", "| 3 Mar '24 | 0.0 | 11.23 |\n", "| 4 Mar '24 | 1.0 | 11.36 |\n", "| 5 Mar '24 | 1.0 | 11.49 |\n", "| 6 Mar '24 | 1.0 | 11.60 |\n", "| 7 Mar '24 | 1.0 | 11.70 |\n", "| **8 Mar '24** | 1.0 | **11.80** |\n", "| 9 Mar '24 | 0.0 | 11.59 |\n", "\n", "\n", "\n", "\n" ] }, { "cell_type": "markdown", "id": "a330db8d-ae61-42f1-9be3-03aef7e3c32f", "metadata": {}, "source": [ "We can use the calendar methods in *rateslib* to create an indexed *Series* with zero weights where we want to have them." ] }, { "cell_type": "code", "execution_count": null, "id": "9937e9d7-37f9-4d8d-9c09-5734fbfdd192", "metadata": {}, "outputs": [], "source": [ "# Use a generic business day calendar to find the weekends\n", "cal = get_calendar(\"bus\")\n", "weekends = [\n", " _ for _ in cal.cal_date_range(dt(2024, 2, 9), dt(2024, 3, 11))\n", " if _ not in cal.bus_date_range(dt(2024, 2, 9), dt(2024, 3, 11))\n", "]\n", "weights = Series(0.0, index=weekends)\n", "weights" ] }, { "cell_type": "markdown", "id": "7fec8a77-0728-4805-a61a-fb2e914cf655", "metadata": {}, "source": [ "Now we will rebuild an *FXDeltaVolSurface* and plot the difference to before." ] }, { "cell_type": "code", "execution_count": null, "id": "3a809360-e35e-4c0c-8f84-1279ed2fc23e", "metadata": {}, "outputs": [], "source": [ "fxvs_2 = FXDeltaVolSurface(\n", " expiries=[\n", " dt(2024, 2, 12), # Spot\n", " dt(2024, 2, 16), # 1W\n", " dt(2024, 2, 23), # 2W\n", " dt(2024, 3, 1), # 3W\n", " dt(2024, 3, 8), # 4W\n", " ],\n", " delta_indexes=[0.5],\n", " node_values=[[8.15], [11.95], [11.97], [11.75], [11.80]],\n", " eval_date=dt(2024, 2, 9),\n", " delta_type=\"forward\",\n", " weights=weights,\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "79a81db2-b221-4fec-8bab-91ac02c06f5a", "metadata": {}, "outputs": [], "source": [ "cal = get_calendar(\"all\")\n", "x, y, y2 = [], [], []\n", "for date in cal.cal_date_range(dt(2024, 2, 10), dt(2024, 3, 8)):\n", " x.append(date)\n", " y.append(fxvs.get_smile(date)[0.5])\n", " y2.append(fxvs_2.get_smile(date)[0.5])\n", "\n", "fig, ax = plt.subplots(1,1)\n", "plt.xticks(rotation=90)\n", "ax.plot(x,y, label=\"excl. weights\")\n", "ax.plot(x,y2, label=\"incl. weights\")\n", "ax.plot([dt(2024, 2, 12), dt(2024, 2, 16), dt(2024, 2, 23), dt(2024, 3, 1), dt(2024, 3, 8)],\n", " [8.15, 11.95, 11.97, 11.75, 11.80],\n", " \"o\", label=\"benchmarks\"\n", " )\n", "ax.legend()" ] }, { "cell_type": "markdown", "id": "30f05648-75b6-41d9-818c-db397cf946e0", "metadata": {}, "source": [ "We observe the familiar sawtooth pattern that is frequently observed in short dated FX market vol." ] }, { "cell_type": "markdown", "id": "e53d84f1-5b58-47e9-b793-3b2aebb94b43", "metadata": {}, "source": [ "## FXSabrSurface\n", "\n", "The *FXSabrSurface* is constructed with cross-sectional *FXSabrSmiles*. For a given *strike* a volatility is obtained on the neighbouring *Smiles* and temporal interpolation is applied exactly as above.\n", "\n", "An *FXForwards* market is required by *FXSabrSurfaces* in order to determine ATM-forward FX rates used within the SABR formula." ] }, { "cell_type": "code", "execution_count": null, "id": "d289131e-f6d5-4a4a-985b-79f846427566", "metadata": {}, "outputs": [], "source": [ "eur = Curve({dt(2024, 2, 9): 1.0, dt(2026, 2, 15): 1.0})\n", "usd = Curve({dt(2024, 2, 9): 1.0, dt(2026, 2, 15): 1.0})\n", "fxf = FXForwards(\n", " fx_rates=FXRates({\"eurusd\": 1.34664}, settlement=dt(2024, 2, 13)),\n", " fx_curves={\"eureur\": eur, \"usdusd\": usd, \"eurusd\": eur},\n", ")\n", "solver = Solver(\n", " curves=[eur, usd],\n", " instruments=[\n", " Value(dt(2024, 2, 10), curves=eur, metric=\"cc_zero_rate\"),\n", " Value(dt(2024, 2, 10), curves=usd, metric=\"cc_zero_rate\")\n", " ],\n", " s=[1.00, 0.4759550366220911],\n", " fx=fxf,\n", ")" ] }, { "cell_type": "markdown", "id": "8c5d9b0f-6e40-4635-8d34-c6ed61475942", "metadata": {}, "source": [ "Use the same ``weights`` as defined above for the **temporal interpolation**." ] }, { "cell_type": "code", "execution_count": null, "id": "82b86f46-de81-4e79-81d4-1f6d6368f803", "metadata": {}, "outputs": [], "source": [ "fxss = FXSabrSurface(\n", " expiries=[\n", " dt(2024, 2, 12), # Spot\n", " dt(2024, 2, 16), # 1W\n", " dt(2024, 2, 23), # 2W\n", " dt(2024, 3, 1), # 3W\n", " dt(2024, 3, 8), # 4W\n", " ],\n", " node_values=[\n", " [0.0815, 1.0, 0.0, 0.0], \n", " [0.1195, 1.0, 0.0, 0.0], \n", " [0.1197, 1.0, 0.0, 0.0], \n", " [0.1175, 1.0, 0.0, 0.0], \n", " [0.1180, 1.0, 0.0, 0.0],\n", " ],\n", " eval_date=dt(2024, 2, 9),\n", " pair=\"eurusd\",\n", " delivery_lag=2,\n", " calendar=\"tgt|fed\",\n", ")\n", "fxss_2 = FXSabrSurface(\n", " expiries=[\n", " dt(2024, 2, 12), # Spot\n", " dt(2024, 2, 16), # 1W\n", " dt(2024, 2, 23), # 2W\n", " dt(2024, 3, 1), # 3W\n", " dt(2024, 3, 8), # 4W\n", " ],\n", " node_values=[\n", " [0.0815, 1.0, 0.0, 0.0], \n", " [0.1195, 1.0, 0.0, 0.0], \n", " [0.1197, 1.0, 0.0, 0.0], \n", " [0.1175, 1.0, 0.0, 0.0], \n", " [0.1180, 1.0, 0.0, 0.0],\n", " ],\n", " eval_date=dt(2024, 2, 9),\n", " pair=\"eurusd\",\n", " delivery_lag=2,\n", " calendar=\"tgt|fed\",\n", " weights=weights,\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "af04af94-57bb-4ed7-9e5f-86eed3c7aea3", "metadata": {}, "outputs": [], "source": [ "x, y, y2 = [], [], []\n", "for date in cal.cal_date_range(dt(2024, 2, 10), dt(2024, 3, 8)):\n", " x.append(date)\n", " y.append(fxss.get_from_strike(1.36, fxf, date)[1])\n", " y2.append(fxss_2.get_from_strike(1.36, fxf, date)[1])\n", "\n", "fig, ax = plt.subplots(1,1)\n", "plt.xticks(rotation=90)\n", "ax.plot(x,y, label=\"excl. weights\")\n", "ax.plot(x,y2, label=\"incl. weights\")\n", "ax.plot([dt(2024, 2, 12), dt(2024, 2, 16), dt(2024, 2, 23), dt(2024, 3, 1), dt(2024, 3, 8)],\n", " [8.15, 11.95, 11.97, 11.75, 11.80],\n", " \"o\", label=\"benchmarks\"\n", " )\n", "ax.legend()" ] }, { "cell_type": "markdown", "id": "3a78770e-20d5-457d-b4e1-abe8e1683ad9", "metadata": {}, "source": [ "The same recognisable chart is obtained." ] } ], "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.0" } }, "nbformat": 4, "nbformat_minor": 5 }