Skip to content

Instantly share code, notes, and snippets.

@danwagnerco
Created September 7, 2024 16:03
Show Gist options
  • Save danwagnerco/b5faa603b15a318b74212c06fef11311 to your computer and use it in GitHub Desktop.
Save danwagnerco/b5faa603b15a318b74212c06fef11311 to your computer and use it in GitHub Desktop.
How much value is there in attempting to out-model your counterparty?
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
" <div id=\"BRu2Fx\"></div>\n",
" <script type=\"text/javascript\" data-lets-plot-script=\"library\">\n",
" if(!window.letsPlotCallQueue) {\n",
" window.letsPlotCallQueue = [];\n",
" }; \n",
" window.letsPlotCall = function(f) {\n",
" window.letsPlotCallQueue.push(f);\n",
" };\n",
" (function() {\n",
" var script = document.createElement(\"script\");\n",
" script.type = \"text/javascript\";\n",
" script.src = \"https://cdn.jsdelivr.net/gh/JetBrains/[email protected]/js-package/distr/lets-plot.min.js\";\n",
" script.onload = function() {\n",
" window.letsPlotCall = function(f) {f();};\n",
" window.letsPlotCallQueue.forEach(function(f) {f();});\n",
" window.letsPlotCallQueue = [];\n",
" \n",
" };\n",
" script.onerror = function(event) {\n",
" window.letsPlotCall = function(f) {}; // noop\n",
" window.letsPlotCallQueue = [];\n",
" var div = document.createElement(\"div\");\n",
" div.style.color = 'darkred';\n",
" div.textContent = 'Error loading Lets-Plot JS';\n",
" document.getElementById(\"BRu2Fx\").appendChild(div);\n",
" };\n",
" var e = document.getElementById(\"BRu2Fx\");\n",
" e.appendChild(script);\n",
" })()\n",
" </script>\n",
" "
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import math\n",
"import QuantLib as ql # 1.35\n",
"import polars as pl # 0.20.13\n",
"from lets_plot import * # 4.3.3\n",
"LetsPlot.setup_html()"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"def black_scholes_euro_price(spot: float,\n",
" strike: float,\n",
" rfr: float,\n",
" vol: float,\n",
" maturity_days: int,\n",
" tenor: int = 365,\n",
" option_type: str = 'call') -> float:\n",
" \"\"\"\n",
" Calculate the Black-Scholes price for a European call or put option.\n",
" \n",
" Parameters:\n",
" - spot: Current stock price (S)\n",
" - strike: Strike price (K)\n",
" - rfr: Risk-free interest rate (r) as a decimal (e.g., 0.05 for 5%)\n",
" - vol: Volatility of the underlying stock (σ) as a decimal (e.g., 0.2 for 20%)\n",
" - maturity_days: Time to maturity in days (T)\n",
" - tenor: Number of days in a year (e.g., 365 for daily)\n",
" - option_type: 'call' or 'put'\n",
" \n",
" Returns:\n",
" - Option price\n",
" \"\"\"\n",
" # Convert maturity from days to years\n",
" maturity = maturity_days / tenor\n",
"\n",
" # Calculate d1 and d2\n",
" d1 = (math.log(spot / strike) + (rfr + 0.5 * vol ** 2) * maturity) / (vol * math.sqrt(maturity))\n",
" d2 = d1 - vol * math.sqrt(maturity)\n",
"\n",
" # Calculate N(d1) and N(d2) for call, or N(-d1) and N(-d2) for put\n",
" cdf = ql.CumulativeNormalDistribution()\n",
" if option_type.lower() == 'call':\n",
" N_d1 = cdf(d1)\n",
" N_d2 = cdf(d2)\n",
" # Call option price\n",
" price = spot * N_d1 - strike * math.exp(-rfr * maturity) * N_d2\n",
" elif option_type.lower() == 'put':\n",
" N_minus_d1 = cdf(-d1)\n",
" N_minus_d2 = cdf(-d2)\n",
" # Put option price\n",
" price = strike * math.exp(-rfr * maturity) * N_minus_d2 - spot * N_minus_d1\n",
" else:\n",
" raise ValueError(\"option_type must be 'call' or 'put'\")\n",
"\n",
" return price\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"# Let's plot the Black-Scholes Euro price for the URA\n",
"# call option in Jan'26 against a varying implied volatility\n",
"# value to better understand what the market is offering\n",
"# us right now (well not technically right now, technically\n",
"# as of the close on Fri Sep 6th, 2024, but you know what\n",
"# I mean)."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"spot = 23.18\n",
"strike = 20\n",
"risk_free_rate = 0.0533\n",
"maturity_days = 496\n",
"tenor = 365\n",
"option_type = 'call'\n",
"implied_vols = [\n",
" 0.25,\n",
" 0.26,\n",
" 0.27,\n",
" 0.28,\n",
" 0.29,\n",
" 0.30,\n",
" 0.31,\n",
" 0.32,\n",
" 0.33,\n",
" 0.34,\n",
" 0.35,\n",
" 0.36,\n",
" 0.37,\n",
" 0.38,\n",
" 0.39,\n",
" 0.40\n",
"]"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
" <div id=\"QwIhdN\"></div>\n",
" <script type=\"text/javascript\" data-lets-plot-script=\"plot\">\n",
" (function() {\n",
" var plotSpec={\n",
"\"data\":{\n",
"\"implied_vol\":[0.25,0.26,0.27,0.28,0.29,0.3,0.31,0.32,0.33,0.34,0.35,0.36,0.37,0.38,0.39,0.4],\n",
"\"price\":[5.35970318421672,5.432328142434251,5.506376153646071,5.581708642535213,5.658201834535841,5.735744952652185,5.814238637813466,5.893593569811271,5.973729265687588,6.054573033650895,6.136059062472778,6.218127628438625,6.300724404055801,6.383799854734672,6.467308711492144,6.551209509362076]\n",
"},\n",
"\"mapping\":{\n",
"\"x\":\"implied_vol\",\n",
"\"y\":\"price\"\n",
"},\n",
"\"data_meta\":{\n",
"},\n",
"\"ggtitle\":{\n",
"\"text\":\"BS Euro Prices vs. Implied Volatility\"\n",
"},\n",
"\"theme\":{\n",
"\"legend_position\":\"bottom\",\n",
"\"plot_title\":{\n",
"\"size\":18.0,\n",
"\"hjust\":0.5,\n",
"\"blank\":false\n",
"}\n",
"},\n",
"\"kind\":\"plot\",\n",
"\"scales\":[{\n",
"\"name\":\"Implied Volatility\",\n",
"\"aesthetic\":\"x\"\n",
"},{\n",
"\"name\":\"Option Price\",\n",
"\"aesthetic\":\"y\"\n",
"}],\n",
"\"layers\":[{\n",
"\"geom\":\"line\",\n",
"\"mapping\":{\n",
"},\n",
"\"data_meta\":{\n",
"},\n",
"\"color\":\"blue\",\n",
"\"data\":{\n",
"}\n",
"}],\n",
"\"metainfo_list\":[]\n",
"};\n",
" var plotContainer = document.getElementById(\"QwIhdN\");\n",
" window.letsPlotCall(function() {{\n",
" LetsPlot.buildPlotFromProcessedSpecs(plotSpec, -1, -1, plotContainer);\n",
" }});\n",
" })();\n",
" </script>"
],
"text/plain": [
"<lets_plot.plot.core.PlotSpec at 0x7f8c90ba2050>"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"bs_euro_option_prices = []\n",
"for implied_vol in implied_vols:\n",
" price = black_scholes_euro_price(spot,\n",
" strike,\n",
" risk_free_rate,\n",
" implied_vol,\n",
" maturity_days,\n",
" tenor,\n",
" option_type)\n",
" bs_euro_option_prices.append(price)\n",
"\n",
"data = {\n",
" 'implied_vol': implied_vols,\n",
" 'price': bs_euro_option_prices\n",
"}\n",
"df = pl.DataFrame(data)\n",
"\n",
"ggplot(df, aes(x='implied_vol', y='price')) + \\\n",
" geom_line(color='blue') + \\\n",
" ggtitle('BS Euro Prices vs. Implied Volatility') + \\\n",
" labs(x='Implied Volatility', y='Option Price') + \\\n",
" theme(plot_title=element_text(size=18, hjust=0.5), legend_position='bottom')"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"# The market pricing is 6.00x6.40 for this option with\n",
"# non-zero volume (13 contracts traded on Fri Sep 6th)\n",
"# so we are certain that we can buy the 20C for 6.40\n",
"# and sell the 20C for 6.00.\n",
"#\n",
"# Using the interactive chart above, we see that at\n",
"# 0.34 implied vol, the BS Euro price is 6.05, close enough\n",
"# to the 6.00 bid, and at 0.38 implied vol, the BS Euro\n",
"# price is 6.38, close enough to the 6.40 ask.\n",
"#\n",
"# From this we can infer that your counterparty's model\n",
"# is predicting that 0.36 implied vol is roughly \"fair\"\n",
"# and in order to trade profitably against you, they will\n",
"# buy the option from you at a 0.34 implied vol or they will\n",
"# sell the option to you at a 0.38 implied vol, a 2 vol point\n",
"# edge in their favor in either direction.\n",
"#\n",
"# Remember that your counterparty is not a directional trader\n",
"# and this volatility edge is how they stay in business. Your\n",
"# trade will be delta-hedged on their book, while your trade\n",
"# is likely a levered directional bet (i.e. a delta bet).\n",
"#\n",
"# Let's assume you decide to take the trade and fill at 6.40.\n",
"# Suppose that 0.34 implied vol is \"fair\" and that the market\n",
"# will continue to demand a 2 vol point edge in either direction:\n",
"# What will the projected price of the BS Euro option be at\n",
"# a variety of underlying prices 90 calendar days from now, and\n",
"# how much difference in that price exists between \"fair\"\n",
"# implied volatility at 0.34 and our counterparty's 2 vol point edge?"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"spots = [\n",
" 15,\n",
" 16,\n",
" 17,\n",
" 18,\n",
" 19,\n",
" 20,\n",
" 21,\n",
" 22,\n",
" 23,\n",
" 24,\n",
" 25,\n",
" 26,\n",
" 27,\n",
" 28,\n",
" 29,\n",
" 30\n",
"]\n",
"strike = 20\n",
"risk_free_rate = 0.0533\n",
"maturity_days = 406 # 90 calendar days forward from now\n",
"tenor = 365\n",
"option_type = 'call'\n",
"fair_implied_vol = 0.34\n",
"edge_implied_vol = 0.32 # 2 vol pts below 0.34 \"fair\" implied vol"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
" <div id=\"35scxG\"></div>\n",
" <script type=\"text/javascript\" data-lets-plot-script=\"plot\">\n",
" (function() {\n",
" var plotSpec={\n",
"\"data\":{\n",
"\"spot\":[15.0,16.0,17.0,18.0,19.0,20.0,21.0,22.0,23.0,24.0,25.0,26.0,27.0,28.0,29.0,30.0,15.0,16.0,17.0,18.0,19.0,20.0,21.0,22.0,23.0,24.0,25.0,26.0,27.0,28.0,29.0,30.0],\n",
"\"price\":[0.9489119309295426,1.306026313770456,1.7298818515560912,2.2186723204813097,2.7691058878838444,3.3768661085887697,4.0370242515954295,4.744380213431745,5.493726019351287,6.280036314916746,7.098595714057575,7.945074916119321,8.815567429263192,9.706597508711308,10.615108191839576,11.53843649391819,0.8365605815431771,1.1771991621764624,1.5879032176753798,2.0673346246465583,2.612292948639068,3.2182571060878633,3.8798899180178186,4.591465661672345,5.347205373155996,6.141521009376321,6.969178858756253,7.825396673345155,8.705889691972214,9.606879483883334,10.525077391654833,11.457651948222225],\n",
"\"dataset\":[\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"fair\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\",\"edge\"]\n",
"},\n",
"\"mapping\":{\n",
"},\n",
"\"data_meta\":{\n",
"},\n",
"\"ggtitle\":{\n",
"\"text\":\"BS Euro Prices vs. Spot Price at T+90\"\n",
"},\n",
"\"theme\":{\n",
"\"legend_position\":\"bottom\",\n",
"\"plot_title\":{\n",
"\"size\":18.0,\n",
"\"hjust\":0.5,\n",
"\"blank\":false\n",
"}\n",
"},\n",
"\"kind\":\"plot\",\n",
"\"scales\":[{\n",
"\"aesthetic\":\"color\",\n",
"\"values\":[\"gray\",\"magenta\"]\n",
"},{\n",
"\"name\":\"Spot Price\",\n",
"\"aesthetic\":\"x\"\n",
"},{\n",
"\"name\":\"Option Price\",\n",
"\"aesthetic\":\"y\"\n",
"}],\n",
"\"layers\":[{\n",
"\"geom\":\"line\",\n",
"\"mapping\":{\n",
"\"x\":\"spot\",\n",
"\"y\":\"price\",\n",
"\"color\":\"dataset\"\n",
"},\n",
"\"data_meta\":{\n",
"},\n",
"\"data\":{\n",
"}\n",
"}],\n",
"\"metainfo_list\":[]\n",
"};\n",
" var plotContainer = document.getElementById(\"35scxG\");\n",
" window.letsPlotCall(function() {{\n",
" LetsPlot.buildPlotFromProcessedSpecs(plotSpec, -1, -1, plotContainer);\n",
" }});\n",
" })();\n",
" </script>"
],
"text/plain": [
"<lets_plot.plot.core.PlotSpec at 0x7f8c90ba1240>"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"bs_fair_euro_option_prices = []\n",
"bs_edge_euro_option_prices = []\n",
"for spot in spots:\n",
" fair_price = black_scholes_euro_price(\n",
" spot,\n",
" strike,\n",
" risk_free_rate,\n",
" fair_implied_vol,\n",
" maturity_days,\n",
" tenor,\n",
" option_type\n",
" )\n",
" bs_fair_euro_option_prices.append(fair_price)\n",
"\n",
" edge_price = black_scholes_euro_price(\n",
" spot,\n",
" strike,\n",
" risk_free_rate,\n",
" edge_implied_vol,\n",
" maturity_days,\n",
" tenor,\n",
" option_type\n",
" )\n",
" bs_edge_euro_option_prices.append(edge_price)\n",
"\n",
"fair_data = {\n",
" 'spot': spots,\n",
" 'price': bs_fair_euro_option_prices,\n",
" 'dataset': ['fair'] * len(spots)\n",
"}\n",
"fair_df = pl.DataFrame(fair_data)\n",
"\n",
"edge_data = {\n",
" 'spot': spots,\n",
" 'price': bs_edge_euro_option_prices,\n",
" 'dataset': ['edge'] * len(spots)\n",
"}\n",
"edge_df = pl.DataFrame(edge_data)\n",
"\n",
"combined_df = pl.concat([fair_df, edge_df])\n",
"\n",
"ggplot(combined_df) + \\\n",
" geom_line(aes(x='spot', y='price', color='dataset')) + \\\n",
" scale_color_manual(values=['gray', 'magenta']) + \\\n",
" ggtitle('BS Euro Prices vs. Spot Price at T+90') + \\\n",
" labs(x='Spot Price', y='Option Price') + \\\n",
" theme(plot_title=element_text(size=18, hjust=0.5), legend_position='bottom')\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"# So how much damage does this 2 vol point edge really do?\n",
"# At 90 days, almost none lol. You would be better off spending\n",
"# your time improving your forecasts for price than hand-wringing\n",
"# over the \"fair\" price of an option as a retail trader.\n",
"#\n",
"# If you're a professional working on a volatility trading\n",
"# desk, that's a different story. This edge can be meaningful\n",
"# as you trade large quantities of contracts (e.g. 1000+) and \n",
"# have the capital necessary to hedge your deltas effectively.\n",
"# Time spent building a faster, more accurate volatility model\n",
"# for the surface of a particular security can be lucrative."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "dans-magic-house-uvW-KuUb-py3.10",
"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.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment