{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Calibration, volatilité locale et stochastique - TP\n", "(Stefano De Marco)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Calibration of an implied volatility parameterization to a market smile" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We consider end-of-day option quotes for the CAC40 index, date = 2 October 2013, maturity = 20 June 2014.\n", "\n", "$\\blacktriangleright$ We import quotes (strikes, mid call and mid put prices) from csv file." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "quotes = np.loadtxt(open(\"CAC40_last_option_quotes_2_oct_2013_maturity_20_june_2014.csv\", \"r\"), delimiter=\",\", skiprows=1)\n", "\n", "strikes = quotes[:, 0]\n", "call_prices = quotes[:, 1]\n", "put_prices = quotes[:, 2]\n", "\n", "maturity = 258./365\n", "\n", "print(\"Strikes: \", strikes)\n", "print(\"Call prices: \", call_prices)\n", "print(\"Put prices: \", put_prices, \"\\n\")\n", "\n", "print(\"Maturity (years): %1.2f\" %maturity)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$\\blacktriangleright$ Let us have a look at the price quotes" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.plot(strikes, call_prices, color='royalblue', linewidth=0., marker=\"*\", label=\"Calls\")\n", "plt.plot(strikes, put_prices, color='indianred', linewidth=0., marker=\"*\", label=\"Puts\")\n", "\n", "plt.xlabel(\"strike $K$\", fontsize=12)\n", "plt.ylabel(\"option price\", fontsize=12)\n", "plt.title(\"Call and Put option prices\", fontsize=14)\n", "\n", "plt.legend(loc=9, fontsize=13, bbox_to_anchor=(1.4, 1.0), ncol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### $\\blacktriangleright$ We test put-call parity, and determine the discount factor and the forward value from option quotes\n", "\n", "#### $\\blacktriangleright$ TO DO: recall theoretical parity relation for call and put prices without arbitrage,\n", "in terms of forward price $F_0^T$ and discount factor $B(0,T) = e^{-r T}$\n", "\n", "$$\n", "\\mathrm{Call}(K) - \\mathrm{Put}(K) = ????\n", "\\qquad \\forall \\, K.\n", "$$" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "########################################################\n", "## TO DO: stock the vector of values of Call(K) - Put(K) \n", "## in the array call_minus_put\n", "#########################################################\n", "call_minus_put = ????\n", "\n", "plt.plot(strikes, call_minus_put, color='black', linewidth=0., marker=\"*\", label=\"Call(K) - Put(K)\")\n", "\n", "plt.xlabel(r\"strike $K$\", fontsize=12)\n", "plt.ylabel(\"price\", fontsize=12)\n", "plt.axhline(0, color='grey', linestyle=\"--\")\n", "\n", "plt.legend(loc=9, fontsize=13, bbox_to_anchor=(1.4, 1.0), ncol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### $\\blacktriangleright$ TO DO: perform a linear regression so to determine the discount factor and the forward value\n", "\n", "We determine the slope $b$ and the intercept $a$ of the linear function that best interpolates the values of $\\mathrm{Call}(K) - \\mathrm{Put}(K)$:\n", "\n", "$$\n", "f(K) = a + b \\, K.\n", "$$\n", "\n", "This gives a way of setting the discount factor and the forward for the maturity $T$ we are working with:\n", "\n", "$$\n", "B(0,T) = ???\n", "\\qquad\n", "F_0^T = ??? \\qquad \\mbox{(in terms of $a$ and $b$)}\n", "$$" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import scipy.stats as sps\n", "\n", "#########################################################\n", "## TO DO: use the function linregress of scipy.stats \n", "## to regress the values of Call(K) - Put(K) against the \n", "## strikes \n", "#########################################################\n", "\n", "(slope, intercept, rvalue, other_value1, other_value2) = sps.linregress( ???? , ???? )\n", "\n", "print(\"Slope = %1.3f\" %slope)\n", "print(\"Intercept =: %1.2f\" %intercept)\n", "print(\"Correlation coefficient = %1.12f \\n\" %rvalue)\n", "\n", "##############################################\n", "## TO DO: complete with the value of B(0,T) \n", "## and the forward value F(0,T)\n", "##############################################\n", "\n", "zcb_price = ????\n", "\n", "F_T = ????\n", "\n", "print(\"Therefore: \\n\")\n", "print(\"B(0,T) = %1.3f\" %zcb_price)\n", "print(\"Forward value F_T = %1.2f\" %F_T)\n", "\n", "##############################################\n", "# Let us plot the result of linear regression\n", "##############################################\n", "plt.plot(strikes, call_minus_put, color='black', linewidth=0., marker=\"*\", markersize=9, label=\"Call(K) - Put(K)\")\n", "plt.plot(strikes, intercept + slope*strikes, color='blue', linestyle =\"--\", linewidth=0.5, label=\"linear interpolation\")\n", "\n", "plt.xlabel(r\"strike $K$\", fontsize=12)\n", "plt.ylabel(\"price\", fontsize=12)\n", "\n", "plt.legend(loc=9, fontsize=13, bbox_to_anchor=(1.4, 1.0), ncol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### $\\blacktriangleright$ TO DO: evaluate the implied volatility generated by the option quotes.\n", "\n", "We are now in a good position to evaluate the implied volatility associated to the option prices. Recall that the market implied volatility $\\hat \\sigma(T,K)$ for maturity $T$ and strike $K$ is the unique solution to the equation\n", "\n", "$$\n", "Call^{\\mathrm{mkt}}(T,K) = Call_{\\mathrm{BS}}(T, K, F_0^T, B(0,T), \\hat \\sigma(T,K)),\n", "$$\n", "\n", "where $Call_{\\mathrm{BS}}(T, K, F, D, \\sigma)$ is the Black-Scholes call price for maturity $T$, strike $K$, forward value $F$, discount factor $D=e^{-rT}$ and volatility parameter $\\sigma$.\n", "\n", "We import an implied volatility solver --> contained in the python file BlackScholesFunctions.py\n", "\n", "NOTE: the function volImplCore_Newton in BlackScholesFunctions.py can evaluate the implied volatility of a call or a put price, according to the value of the argument CallOrPutFlag" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from BlackScholesFunctions import volImplCore_Newton\n", "\n", "###########################\n", "## Implied vol from puts\n", "###########################\n", "vols_puts = []\n", "\n", "for i in range(strikes.size):\n", " K = strikes[i]\n", " \n", " #########################################################\n", " ## TO DO: complete the arguments of the function \n", " ## volImplCore_Newton\n", " ## so to compute the implied volatility of the put price P(K)\n", " ## for the current strike\n", " #########################################################\n", " \n", " vol, iterations = volImplCore_Newton(tau = maturity, K = K, DF = zcb_price,\n", " \n", " F = ???, ## required forward value\n", " price = ???,\n", " CallOrPutFlag = ???,\n", " \n", " initial_point='automatic', prix_tol = 1.e-4, max_iter=50)\n", " \n", " vols_puts = np.append(vols_puts, vol)\n", "\n", "############################\n", "## Implied vol from calls\n", "############################\n", "vols_calls = []\n", "\n", "for i in range(strikes.size):\n", " K = strikes[i]\n", " \n", " #########################################################\n", " ## TO DO: complete the arguments of the function \n", " ## volImplCore_Newton\n", " ## so to compute the implied volatility of the call price C(K)\n", " ## for the current strike\n", " #########################################################\n", "\n", " vol, iterations = volImplCore_Newton(tau = maturity, K = K, DF = zcb_price,\n", " \n", " F = ???, ## required forward value\n", " price = ???,\n", " CallOrPutFlag = ???,\n", " \n", " initial_point='automatic', prix_tol = 1.e-4, max_iter=50)\n", " \n", " vols_calls = np.append(vols_calls, vol)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$\\blacktriangleright$ Plot the resulting smile:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(9,6))\n", "\n", "plt.plot(strikes, ???? ,\n", " color='indianred', linewidth=0., marker=\".\", markersize=12, label=\"Implied vol puts\")\n", "\n", "plt.plot(strikes, ???? ,\n", " color='royalblue', linewidth=0., marker=\"+\", markersize=14, label=\"Implied vol calls\")\n", "\n", "plt.xlabel(\"strike $K$\", fontsize=12)\n", "plt.ylabel(\"Implied vol\", fontsize=12)\n", "plt.title(\"Implied vol from call and put prices\", fontsize=14)\n", "\n", "plt.legend(loc=9, fontsize=13, bbox_to_anchor=(1.4, 1.0), ncol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$\\blacktriangleright$ In terms of log-forward moneyness $k = \\log(K / F_0^T)$:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(9,6))\n", "\n", "mkt_log_mon = ????\n", "\n", "plt.plot(mkt_log_mon, ???? ,\n", " color='indianred', linewidth=0., marker=\".\", markersize=12, label=\"Implied vol puts\")\n", "\n", "plt.plot(mkt_log_mon, ???? ,\n", " color='royalblue', linewidth=0., marker=\"+\", markersize=14, label=\"Implied vol calls\")\n", "\n", "plt.xlabel(r\"log-forward moneyness $k$\", fontsize=12)\n", "plt.ylabel(\"Implied vol\", fontsize=12)\n", "plt.title(\"Implied vol from call and put prices\", fontsize=14)\n", "\n", "plt.legend(loc=9, fontsize=13, bbox_to_anchor=(1.4, 1.0), ncol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The SVI parameterisation\n", "\n", "#### $\\blacktriangleright$ Implied total variance\n", "\n", "The implied total variance $w(T,k)$ for maturity $T$ and log-forward moneyness $k$ is defined by\n", "\n", "$$\n", "w(T,k) = T \\, \\hat \\sigma^2(T, k)\n", "$$\n", "\n", "where (with a slight abuse of notation with respect to the notation above) $\\hat \\sigma(T, k)$ is the implied volatility for the same maturity and log-forward moneyness.\n", "\n", "\n", "#### $\\blacktriangleright$ The SVI (Stochastic Volatility Inspired) parameterisation\n", "\n", "SVI is a parametric family of functions proposed by [Gatheral 2004] to model the total implied variance smile for a fixed maturity $T$ :\n", "\n", "\\begin{equation}\n", "w_\\mathrm{SVI}(k) = a + b \\left(\\rho (k - \\bar k) + \\sqrt{(k - \\bar k)^2 + \\sigma^2} \\right),\n", "\\qquad k \\in \\mathbb R,\n", "\\end{equation}\n", "\n", "where the parameters $(a, b, \\rho, \\bar k, \\sigma)$ satisfy\n", "\n", "$$\n", "b > 0 \\qquad \\quad \\rho \\in [-1,1] \\qquad \\quad \\sigma > 0, \\qquad \\quad a, \\bar k \\in \\mathbb R.\n", "$$\n", "\n", "Note that, since $\\min_{k \\in \\mathbb R} w_\\mathrm{SVI}(k) = a + b \\, \\sigma \\, \\sqrt{1 - \\rho^2}$, we also have to impose the condition\n", "\n", "$$\n", "a + b \\, \\sigma \\, \\sqrt{1 - \\rho^2} > 0\n", "$$\n", "\n", "in order to grant positivity of the function $w_\\mathrm{SVI}$." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### To Do: fit an SVI parameterisation to the market quotes\n", "\n", "We want to find a parameter set $(a^*, b^*, \\rho^*, \\bar k^*, \\sigma^*)$ that best fits the observed values $w^{\\mathrm{mkt}}$.\n", "\n", "We formulate this calibration problem as a least-square problem:\n", "\n", "$$\n", "\\min_{(a,b,\\rho,\\bar k,\\sigma)}\n", "\\sum_{i} \\left( w^{\\mathrm{mkt}}(k_i) - w_\\mathrm{SVI} \\left(k_i \\, ; a,b,\\rho,\\bar k,\\sigma \\right) \\right)^2\n", "$$\n", "\n", "where the $k_i$ are the observed log-moneyness points and $w^{\\mathrm{mkt}}(k_i) = T \\bigl( \\hat \\sigma^{\\mathrm{mkt}}(k_i) \\bigr)^2$ the observed values of total implied variance." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$\\blacktriangleright$ The problem above is a constrained optimization problem.\n", "Some of the constraints on the parameter set are easy to impose (they are \"box constraints\"):\n", "\n", "$$\n", "b > 0 \\qquad \\quad \\rho \\in [-1,1] \\qquad \\quad \\sigma > 0\n", "$$\n", "\n", "while the positivity constraint\n", "\n", "$$\n", "a + b \\, \\sigma \\, \\sqrt{1 - \\rho^2} > 0\n", "$$\n", "\n", "is slightly more involved.\n", "\n", "$\\blacktriangleright$ We decide to replace the positivity constraint $a + b \\, \\sigma \\, \\sqrt{1 - \\rho^2} > 0$ with the condition\n", "\n", "$$\n", "a > 0\n", "$$\n", "\n", "which is stronger, but we accept to make this restriction.\n", "\n", "$\\blacktriangleright$ There would be other constraints, relate to no-arbitrage conditions: we want to guarantee that the interpolation and extrapolation of the observed market variances provided by the function $w_\\mathrm{SVI}$ does not generate arbitrage opportunities (in other words: we want the call price $K \\mapsto Call_{\\mathrm{BS}} \\Bigl(T, K, F_0^T, B(0,T), \\frac 1{\\sqrt{T}} \\sqrt{w_\\mathrm{SVI} \\Bigl(\\log \\frac K{F_0^T}\\Bigr)} \\Bigr)$ to be a true call price without arbitrage).\n", "\n", "This point will be discussed in detail in a future lecture.\n", "\n", "$\\blacktriangleright$ For the moment, we just accept to neglect such no-arbitrage constraints and fit the SVI parameterisation as it is.\n", "\n", "$\\blacktriangleright$ Finally, we will cap the positive parameters $a,b,\\sigma$ at some reasonably high levels $a_{\\mathrm{max}}, b_{\\mathrm{max}}, \\sigma_{\\mathrm{max}}$.\n", "\n", "#### We make a try with the function least_squares from the library scipy.optimize" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import scipy.optimize as opt\n", "\n", "def SVI(k, a, b , rho, k_bar, sig):\n", " total_variance = a + b*( rho*(k - k_bar) + np.sqrt( (k - k_bar)*(k - k_bar) + sig*sig) )\n", " \n", " return total_variance\n", "\n", "def SVI_two_arguments(theta, k):\n", " ## Attention à l'ordre des paramètres...\n", " a, b , rho, k_bar, sig = theta\n", " \n", " return SVI(k, a, b , rho, k_bar, sig)\n", "\n", "def fct_least_squares(theta, log_mon, tot_implied_variance):\n", " \"\"\"\n", " theta : the vector of SVI parameters\n", " log_mon : log-forward moneyness k (float or double)\n", " tot_implied_variance : the target total implied variance (float or double)\n", " \n", " returns : the objective function to inject in the least-square problem\n", " \"\"\"\n", " return SVI_two_arguments(theta, log_mon) - tot_implied_variance" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$\\blacktriangleright$ We have to choose a starting point for the minimum search:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "## Our target is the vector of total variances from (call or put) price quotes\n", "mkt_tot_variance = maturity * vols_calls**2\n", "\n", "## Choice of initial point\n", "a_init = np.min(mkt_tot_variance) / 2\n", "b_init = 0.1\n", "rho_init = -0.8\n", "k_bar_init = 0.\n", "sig_init = 0.1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$\\blacktriangleright$ Let us have a look at our initial guess for SVI:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "## Recall that our vector of market values for log-forward moneyness: mkt_log_mon\n", "## has been defined above\n", "\n", "################################\n", "## Plot of initial SVI guess\n", "################################\n", "plt.figure(figsize=(9,6))\n", "\n", "total_variances_init = SVI( ???? , a_init, b_init, rho_init, k_bar_init, sig_init)\n", "\n", "plt.plot(mkt_log_mon, total_variances_init,\n", " color='royalblue', linewidth=1.5, \n", " label=r\"$a$=%1.2f, $b$=%1.2f, $\\rho$=%1.2f, $k_{bar}$=%1.2f, $\\sigma$=%1.2f\" %(a_init,b_init,rho_init,k_bar_init,sig_init))\n", "\n", "plt.plot(mkt_log_mon, mkt_tot_variance,\n", " color='black', linewidth=0., marker=\"+\", markersize=14, label=\"Market total variance\")\n", "\n", "plt.axvline(0., linestyle=\"--\", linewidth=0.5, color=\"k\")\n", "\n", "plt.xlabel(r\"log-forward moneyness $k$\", fontsize=12)\n", "plt.ylabel(\"Total variance\", fontsize=12)\n", "plt.title(r\"Initial SVI guess\", fontsize=15)\n", "\n", "plt.legend(loc=9, fontsize=13, bbox_to_anchor=(1.6, 1.0), ncol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### $\\blacktriangleright$ TO DO: run the function least_squares from scipy.optimize" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "################################################\n", "## The bounds we impose on the SVI parameters\n", "## \n", "## Reminder: the order is a, b , rho, k_bar, sig\n", "################################################\n", "lower = np.array([0., 0., -1., -0.5, 0.])\n", "\n", "upper = np.array([np.max(mkt_tot_variance), 2., 1., 0.5, 1.])\n", "\n", "SVI_param_bounds = (lower, upper)\n", "\n", "theta_init = np.array([a_init, b_init, rho_init, k_0_init, sig_init])\n", "\n", "###################################################################\n", "## TO DO: apply the least squares method\n", "## \n", "## - to the objective function to be minimized\n", "## - using theta_init as the starting point for the search\n", "## - imposing the bounds we defined above\n", "###################################################################\n", "\n", "result = opt.least_squares(fun = ???? ,\n", " x0 = ???? , \n", " bounds = ???? ,\n", " args = (mkt_log_mon, mkt_tot_variance), #additional arguments for the LS objective\n", " verbose = 1)\n", "\n", "## The optimized parameters\n", "a_star, b_star, rho_star, k_bar_star, sig_star = result.x" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$\\blacktriangleright$ Have a look at our SVI fit of implied variance:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#######################################\n", "## Plot of SVI fit from least squares\n", "##\n", "## TO DO: complete with the plot of the\n", "## calibrated SVI and the market implied \n", "## variances for comparison\n", "#######################################\n", "\n", "total_variances_fit = ????\n", "\n", "plt.figure(figsize=(9,6))\n", "\n", "plt.plot(mkt_log_mon, ????,\n", " color='royalblue', linewidth=1.5, \n", " label=r\"$a$=%1.2f, $b$=%1.2f, $\\rho$=%1.2f, $k_{bar}$=%1.2f, $\\sigma$=%1.2f\" %(a_star, b_star, rho_star, k_bar_star, sig_star))\n", "\n", "plt.plot(mkt_log_mon, ???? ,\n", " color='black', linewidth=0., marker=\"+\", markersize=14, label=\"Market total variance\")\n", "\n", "\n", "plt.axvline(0., linestyle=\"--\", linewidth=0.5, color=\"k\")\n", "\n", "plt.xlabel(r\"log-forward moneyness $k$\", fontsize=12)\n", "plt.ylabel(\"Total variance\", fontsize=12)\n", "plt.title(r\"SVI fit from least-squares\", fontsize=15)\n", "\n", "plt.legend(loc=9, fontsize=12, bbox_to_anchor=(1.6, 1.0), ncol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### How does the result look like?\n", "\n", "The fit of implied volatility:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#####################################\n", "## Plot of SVI fit of implied vol\n", "#####################################\n", "implied_vols_fit = ????\n", "\n", "plt.figure(figsize=(9,6))\n", "\n", "plt.plot(mkt_log_mon, ???? ,\n", " color='royalblue', linewidth=1.5, \n", " label=r\"$a$=%1.2f, $b$=%1.2f, $\\rho$=%1.2f, $k_{bar}$=%1.2f, $\\sigma$=%1.2f\" %(a_star, b_star, rho_star, k_bar_star, sig_star))\n", "\n", "plt.plot(mkt_log_mon, ???? ,\n", " color='black', linewidth=0., marker=\"+\", markersize=14, label=\"Market implied volatility\")\n", "\n", "plt.axvline(0., linestyle=\"--\", linewidth=0.5, color=\"k\")\n", "\n", "plt.xlabel(r\"log-forward moneyness $k$\", fontsize=12)\n", "plt.ylabel(\"Implied vol\", fontsize=12)\n", "plt.title(r\"SVI fit of implied volatility\", fontsize=15)\n", "\n", "plt.legend(loc=9, fontsize=12, bbox_to_anchor=(1.6, 1.0), ncol=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### $\\blacktriangleright$ Delayed option quotes on the SP500 index\n", "\n", "can be downloaded freely from the the [CBOE quotes platform](http://www.cboe.com/delayedquote/quote-table) (index symbol: SPX).\n", "\n", "You can download option quotes for different maturities (the standard monthly maturity for the SPX option market being the third Friday of each month) and try the same SVI calibration as above.\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.6.5" } }, "nbformat": 4, "nbformat_minor": 2 }