Build an implied volatility surface with Python
Build an implied volatility surface with Python
Build an implied volatility surface with Python
In today’s newsletter, I’m going to show you how to build an implied volatility surface using Python.
A volatility surface plots the level of implied volatility in 3D space. The days to expiration are on the X-axis, the strike price is on the Y-axis, and implied volatility is on the Z-axis.
Implied volatility is the market’s expectations of volatility over the life of an option. To find implied volatility you need three things: the market price of the option, a pricing model, and a root finder. You can then find the volatility that sets the price from the model equal to the price of the market with the root finder. "The volatility implied by the market." The volatility surface is found by repeating this for all options and plotting the results.
You can use the implied volatility from Yahoo Finance. In practice, traders calculate it themselves. You can too with The 46-Page Ultimate Guide to Pricing Options and Implied Volatility With Python.
Pricing models assume volatility is the same for all strike prices and maturities. Volatility surfaces show this assumption is not true. Volatility has skew (volatility is different across strike prices) and a term structure (volatility is different across maturities). Quants use volatility surfaces to help calibrate models and price OTC derivatives that don’t trade on exchanges.
Volatility surfaces prove the models wrong.
When you value an option, the variables in the model (e.g. stock price, time to expiration) are known except volatility, which is an estimate. If models were completely correct, the volatility surface across strike prices and maturities would be flat. In practice. This is not the case as you’ll see.
By the end of this issue, you will know how to:
- Get live options data
- Analyze volatility skew
- Analyze volatility structure
- Build an implied volatility surface
All in Python.
Step 1: Get live options data
Start by importing the libraries you need. I use yfinance to get options data for free.
1import numpy as np
2import pandas as pd
3import yfinance as yf
4import datetime as dt
5
6import matplotlib.pyplot as plt
yfinance returns data for all strikes for a single expiration at a time. It’s easier to work with all strikes and expirations at the same time so write a function to combine the expirations.
1def option_chains(ticker):
2 """
3 """
4 asset = yf.Ticker(ticker)
5 expirations = asset.options
6
7 chains = pd.DataFrame()
8
9 for expiration in expirations:
10 # tuple of two dataframes
11 opt = asset.option_chain(expiration)
12
13 calls = opt.calls
14 calls['optionType'] = "call"
15
16 puts = opt.puts
17 puts['optionType'] = "put"
18
19 chain = pd.concat([calls, puts])
20 chain['expiration'] = pd.to_datetime(expiration) + pd.DateOffset(hours=23, minutes=59, seconds=59)
21
22 chains = pd.concat([chains, chain])
23
24 chains["daysToExpiration"] = (chains.expiration - dt.datetime.today()).dt.days + 1
25
26 return chains
This function first gets all the expirations. Then it loops through each expiration and gets the option chain. It adds a column for option type, changes the expiration date to be at the end of the day, the combines each option chain together in a DataFrame
. Finally, it computes the number of days until expiration.
Step 2: Analyze skew and term structure
yfinance provides an estimate of implied volatility so you don’t have to compute it. This is ok for a quick analysis. In practice, quants derive their own implied volatility using custom models. If you want to learn how to do it yourself with Python, check out The 46-Page Ultimate Guide to Pricing Options and Implied Volatility With Python.
Start by downloading the data and getting the call options. The options data are in a pandas DataFrame
which makes it easy.
1options = option_chains("SPY")
2
3calls = options[options["optionType"] == "call"]
Next, pick an expiration so you can plot the volatility skew.
1# print the expirations
2set(calls.expiration)
3
4# select an expiration to plot
5calls_at_expiry = calls[calls["expiration"] == "2023-01-20 23:59:59"]
6
7# filter out low vols
8filtered_calls_at_expiry = calls_at_expiry[calls_at_expiry.impliedVolatility >= 0.001]
9
10# set the strike as the index so pandas plots nicely
11filtered_calls_at_expiry[["strike", "impliedVolatility"]].set_index("strike").plot(
12 title="Implied Volatility Skew", figsize=(7, 4)
13)
Notice two things. First, the data are messy. In practice, quants use their own models to calculate implied volatility. They also filter out outliers and use smoothing algorithms. The second thing to notice is that the implied volatility varies with each strike. In particular, it is lowest at the $400 strike, which is right around the stock price. This is known as volatility smile.
Next, build a volatility term structure. Pick a strike price to plot by expiration.
1# select an expiration to plot
2calls_at_strike = options[options["strike"] == 400.0]
3
4# filter out low vols
5filtered_calls_at_strike = calls_at_strike[calls_at_strike.impliedVolatility >= 0.001]
6
7# set the strike as the index so pandas plots nicely
8filtered_calls_at_strike[["expiration", "impliedVolatility"]].set_index("expiration").plot(
9 title="Implied Volatility Term Structure", figsize=(7, 4)
10)
Implied volatility is decreasing as the expiration dates get further out. This tells you the market expectation of volatility is lower in the future than it is today. You'll often see spikes in the term structure when big economic news is scheduled. The effect is caused by traders bidding up the prices of options in expectation of market swings.
Step 3: Plot a volatility surface
By putting both charts together, you get the volatility surface. In derivatives pricing and trading, volatility surfaces are very important. Quants use the surface to price and trade other more exotic derivatives and look for market mispricings. Volatility surfaces are also used to determine profit and loss by “marking trades to model.”
1# pivot the dataframe
2surface = (
3 calls[['daysToExpiration', 'strike', 'impliedVolatility']]
4 .pivot_table(values='impliedVolatility', index='strike', columns='daysToExpiration')
5 .dropna()
6)
7
8# create the figure object
9fig = plt.figure(figsize=(10, 8))
10
11# add the subplot with projection argument
12ax = fig.add_subplot(111, projection='3d')
13
14# get the 1d values from the pivoted dataframe
15x, y, z = surface.columns.values, surface.index.values, surface.values
16
17# return coordinate matrices from coordinate vectors
18X, Y = np.meshgrid(x, y)
19
20# set labels
21ax.set_xlabel('Days to expiration')
22ax.set_ylabel('Strike price')
23ax.set_zlabel('Implied volatility')
24ax.set_title('Call implied volatility surface')
25
26# plot
27ax.plot_surface(X, Y, z)
First, pivot the data using pandas. It puts the strike price in the rows, the days to expiration in the columns, and the implied volatility inside the table. NumPy’s meshgrid
method applies a function to every combination of inputs. In this case, use it to grab the coordinates for the plot. Finally, use plot_surface
to plot days to expiration on the X-axis, strike price on the Y-axis, and implied volatility on the Z-axis.
Well, that's it for today. I hope you enjoyed it.
See you again next week.