PQN #019: Build an implied volatility surface with Python.

PQN #019: 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:

  1. Get live options data
  2. Analyze volatility skew
  3. Analyze volatility structure
  4. 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.

import numpy as np
import pandas as pd
import yfinance as yf
import datetime as dt

import 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.

def option_chains(ticker):
    """
    """
    asset = yf.Ticker(ticker)
    expirations = asset.options
    
    chains = pd.DataFrame()
    
    for expiration in expirations:
        # tuple of two dataframes
        opt = asset.option_chain(expiration)
        
        calls = opt.calls
        calls['optionType'] = "call"
        
        puts = opt.puts
        puts['optionType'] = "put"
        
        chain = pd.concat([calls, puts])
        chain['expiration'] = pd.to_datetime(expiration) + pd.DateOffset(hours=23, minutes=59, seconds=59)
        
        chains = pd.concat([chains, chain])
    
    chains["daysToExpiration"] = (chains.expiration - dt.datetime.today()).dt.days + 1
    
    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.

options = option_chains("SPY")

calls = options[options["optionType"] == "call"]

Next, pick an expiration so you can plot the volatility skew.

# print the expirations
set(calls.expiration)

# select an expiration to plot
calls_at_expiry = calls[calls["expiration"] == "2023-01-20 23:59:59"]

# filter out low vols
filtered_calls_at_expiry = calls_at_expiry[calls_at_expiry.impliedVolatility >= 0.001]

# set the strike as the index so pandas plots nicely
filtered_calls_at_expiry[["strike", "impliedVolatility"]].set_index("strike").plot(
    title="Implied Volatility Skew", figsize=(7, 4)
)
PQN #019: Build an implied volatility surface with Python

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.

# select an expiration to plot
calls_at_strike = options[options["strike"] == 400.0]

# filter out low vols
filtered_calls_at_strike = calls_at_strike[calls_at_strike.impliedVolatility >= 0.001]

# set the strike as the index so pandas plots nicely
filtered_calls_at_strike[["expiration", "impliedVolatility"]].set_index("expiration").plot(
    title="Implied Volatility Term Structure", figsize=(7, 4)
)
PQN #019: Build an implied volatility surface with Python

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.”

# pivot the dataframe
surface = (
    calls[['daysToExpiration', 'strike', 'impliedVolatility']]
    .pivot_table(values='impliedVolatility', index='strike', columns='daysToExpiration')
    .dropna()
)

# create the figure object
fig = plt.figure(figsize=(10, 8))

# add the subplot with projection argument
ax = fig.add_subplot(111, projection='3d')

# get the 1d values from the pivoted dataframe
x, y, z = surface.columns.values, surface.index.values, surface.values

# return coordinate matrices from coordinate vectors
X, Y = np.meshgrid(x, y)

# set labels
ax.set_xlabel('Days to expiration')
ax.set_ylabel('Strike price')
ax.set_zlabel('Implied volatility')
ax.set_title('Call implied volatility surface')

# plot
ax.plot_surface(X, Y, z)
PQN #019: Build an implied volatility surface with Python

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.