<hr>![]()
# Optiver lecture: Trading options as a market maker## Theory application by Nicola Menardoβ2nd Dec 2021βI found the lacture by Robbert Puller very interesting as I already had a good grasp of how options worked and the Black-Sholes formula coming in to the lecture but learning about implied volatility skew was very interesting to me.βI wanted to explore this concept further and by visualising it and by writing an algorithms that hedges the option exposure the way it was explained in the lecture.ββThe analysis consists out of 5 steps:1. Retrieving and formatting data2. Calculating implied volatility3. Visualizing volatility skew and its movements4. Calculating the hedge to be market neutral5. Visualizing the position taking for hedging2nd Dec 2021
I found the lacture by Robbert Puller very interesting as I already had a good grasp of how options worked and the Black-Sholes formula coming in to the lecture but learning about implied volatility skew was very interesting to me.
I wanted to explore this concept further and by visualising it and by writing an algorithms that hedges the option exposure the way it was explained in the lecture.
The analysis consists out of 5 steps:
# QuantBook Analysis Tool # For more information see [https://www.quantconnect.com/docs/research/overview]qb = QuantBook()β# In this notebook I will look at the SPY# Initialize the securities objects spy = qb.AddEquity("SPY")spy_options = qb.AddOption("SPY")β# We need to specify the stikes that are to our interest# Looking at 10 stikes under up to 15 stikes over the ATM point# With timedelt we filter for options expiring in els than 30 daysspy_options.SetFilter(-10,+15, timedelta(0), timedelta(30))# Define the period we're interested in.# Looking at 30 minutes herestart_time = datetime(2021, 11, 30, 9, 30) end_time = datetime(2021, 11, 30, 10, 0)β# Looking at 20 minutes of minute-resolution dataunderlying_history = qb.History(spy.Symbol, start_time, end_time, Resolution.Minute)option_history = qb.GetOptionHistory(spy.Symbol, start_time, end_time, Resolution.Minute)β# Get the 1 year risk-free rate from the yield curve.# The only available resolution is daily. We'll deal with this lateryield_curve = qb.AddData(USTreasuryYieldCurveRate, "USTYCR")oneyear_rate=qb.History(yield_curve.Symbol, start_time-timedelta(1), end_time, Resolution.Daily).oneyear.droplevel(0)data = option_history.GetAllData()data.head()print(f"Option expiries in this dataset: \n {option_history.GetExpiryDates()}\n")print(f"Selected strike prices: \n {option_history.GetStrikes()}\n")print(f"Row indexers: \n {data.index.names}\n")print(f"Columns: \n {data.columns}\n")### For this example I'm only interested in the bid-ask option data of the call options. We also know that we have only one option expiry set which allows us to drop some indexers. Let's regoranize the DataFrame to simplify things.# Only keep the bid and ask columns and drop the expiry indexes since we work with only one expiry and one underlyingreordered_data = data.unstack(level=4)[['bidclose', 'askclose']].droplevel([0,3])β# Only keep call options and drop the index afterwardscall_data = reordered_data[reordered_data.index.isin(["Call"], level=1)].droplevel(1).Tβ# split up the bid and ask prices in separate DataFramescall_bid = call_data[call_data.index.isin(['bidclose'], level=0)].droplevel(0)call_ask = call_data[call_data.index.isin(['askclose'], level=0)].droplevel(0)βcall_bid.head()# We can also we can simplify the dataframe of the underlying time seriesunderlying_data = underlying_history[['bidclose', 'askclose']].droplevel(0)underlying_data.head()S = spot price of the underlying assetβX = strike priceβt = time to expirationβr = interest rateβ$\sigma$ = implied volatilityβ-X*e^{-rt}*N(d_2)) β![\Large d_1=\frac{\ln(\frac{S}{A}+[r+\frac{\sigma^2}{2}]t)}{\sigma\sqrt{t}}](https://latex.codecogs.com/svg.latex?\Large&space;d_1=\frac{\ln\frac{S}{A}+[r+\frac{\sigma^2}{2}]t}{\sigma\sqrt{t}}) β<br /><br />})<br />ββSource: https://www.sciencedirect.com/science/article/abs/pii/0378426695000143S = spot price of the underlying asset
X = strike price
t = time to expiration
r = interest rate
= implied volatility
Source: https://www.sciencedirect.com/science/article/abs/pii/0378426695000143
### Let's start with calculating the implied volatility for one strike:import numpy as npβ# Begin by listing all the variables we have to our disposalβC = call_bid.iloc[:, 0]S = underlying_data['bidclose']X = pd.Series(np.ones(len(underlying_data)) * call_bid.columns[0], index=underlying_data.index)t = pd.Series(np.ones(len(underlying_data)) * ((data.index[0][0] - call_bid.index[0]).days / 252),index=underlying_data.index) # Assuming 252 days in a yearr = pd.Series(np.ones(len(underlying_data)) * oneyear_rate.values[0], index=underlying_data.index)βBS = pd.concat([C, S, X, t, r], axis=1)BS.columns = ['C', 'S', 'X', 't', 'r']BS.head()β# Applying the formula above to calculate Implied Volatility (IV)IV = ((((2*np.pi)**0.5)/(BS.S+BS.X)) * (BS.C-((BS.S-BS.X)/2) + ((((BS.C-((BS.S-BS.X)/2)))**2)-((BS.S-BS.X)/np.pi))**0.5)) / (t**0.5)BS['IV'] = IVBS.head()### Cool! Now we need to handle multiple strike pricesIVs = list()βfor strike_index in range(len(call_bid.columns)): C = call_bid.iloc[:, strike_index] S = underlying_data['bidclose'] X = pd.Series(np.ones(len(underlying_data)) * call_bid.columns[strike_index], index=underlying_data.index) t = pd.Series(np.ones(len(underlying_data)) * ((data.index[0][0] - call_bid.index[0]).days / 252),index=underlying_data.index) # Assuming 252 days in a year r = pd.Series(np.ones(len(underlying_data)) * oneyear_rate.values[0], index=underlying_data.index) BS = pd.concat([C, S, X, t, r], axis=1) BS.columns = ['C', 'S', 'X', 't', 'r']β IV = ((((2*np.pi)**0.5)/(BS.S+BS.X)) * (BS.C-((BS.S-BS.X)/2) + ((((BS.C-((BS.S-BS.X)/2)))**2)-((BS.S-BS.X)/np.pi))**0.5)) / (t**0.5)β IVs.append(IV)βIV_series = pd.concat(IVs, axis=1)IV_series.columns = call_bid.columnsIV_series.head()### Now we can visualize the evolution of the volatility skew over a 30-minute period!import matplotlib.pyplot as plt# from mpl_toolkits import mplot3dfig = plt.figure(figsize=(15,10))βplt.plot(IV_series.T*100)βplt.suptitle("Evolution of implied volatility skew", size = 20, y=1)plt.title("SPY options on 2021-11-30 from 9:30 to 10:00")βplt.ylabel("Implied Volatility (%)")plt.xlabel("Strike price")βlbl = [i.strftime("%H-%M") for i in IV_series.index]plt.legend(lbl, loc=(1.01,0.1), title="Time")βplt.show()# In-the-money option seem to have lower volatility of the IV itselffig = plt.figure(figsize=(15,10))βplt.plot(IV_series*100)βplt.suptitle("Evolution of implied volatility over time, per strike", size = 20, y=1)plt.title("SPY options on 2021-11-30 from 9:30 to 10:00")βplt.ylabel("Implied Volatility (%)")plt.xlabel("Time (day-hour-minute)")βlbl = ["$"+str(int(i)) for i in IV_series.columns]plt.legend(lbl, loc=(1.01,0.2), title="Strike price")plt.show()# Corners on the curved (bottom) parts of the lines seem to be pricing inefficiencies that get correctedβfig = plt.figure(figsize=(15,10))ax = fig.add_subplot(111, projection='3d')βnrows = IV_series.shape[0]ncols = IV_series.shape[1]βx = np.array([[i.minute] * ncols for i in (IV_series.index - timedelta(minutes=1))]).ravel() # x coordinates y = np.array([i for i in IV_series.columns] * nrows) # y coordinates z = np.array(IV_series*100).ravel() # z coordinatesdx = np.ones(nrows*ncols) # length along x-axis dy = np.ones(nrows*ncols) # length along y-axis dz = np.array(IV_series*100).ravel() # length along z-axis βax.set_xlabel("Time (Minutes)")ax.set_ylabel("Strike price")ax.set_zlabel("Implied volatility (%)")βplt.suptitle("Evolution of implied volatility skew", size = 20, y=1)plt.title("SPY options on 2021-11-30 from 9:30 to 10:00")βax.text2D(-0.05,-0.025,s="Inefficiencies ->", color='green', size=13)ax.text2D(-0.007,-0.04,s="Inefficiencies ->", color='green', size=13)βax.plot(x, y, z, dx, dy, dz)plt.show()## Step 4: Calculate the hedge to be market-neutral# Let's go back to a singe option to simulate hedging it# DataFrame from step 3 with strike of 453.0BS['IV'] = ((((2*np.pi)**0.5)/(BS.S+BS.X)) * (BS.C-((BS.S-BS.X)/2) + ((((BS.C-((BS.S-BS.X)/2)))**2)-((BS.S-BS.X)/np.pi))**0.5)) / (t**0.5)BS.head()### To know how much of the underlying we need to sell to hedge our position if we were to buy this option we need to look at the option's delta.β### The delta is the rate of chnage of the option's price relative to the change in the underlying asset's price.β)βNote: If the underlying is not dividend-payingNote: If the underlying is not dividend-paying
# Source: https://aaronschlegel.me/measure-sensitivity-derivatives-greeks-python.htmlimport scipy.stats as sidef delta_call(S, X, t, r, IV): d1 = (np.log(S / X) + (r + 0.5 * IV ** 2) * t) / (IV * np.sqrt(t)) delta_call = si.norm.cdf(d1, 0.0, 1.0) return delta_call# With a delta of 0.74 we need to buy 74 option for over option contract bought# (given that the lot size of option contracts is 100)BS['delta'] = BS.apply(lambda x: delta_call(x.S, x.X, x.t, x.r, x.IV), axis=1)BS.head()## Step 5: Visualizing the position taking for hedgingfig, (ax1, ax2) = plt.subplots(2,1, sharex=True)βax1.plot(BS.delta)ax1.set_title("Evolution of an option's delta", size=10)ax1.set_ylabel("Delta")ax1.set_xlabel("Time (Day, Hour, Mintue)")βax2.plot(BS.S)ax2.set_title("Price of the underlying (SPY)", size=10)ax2.set_ylabel("Price")ax2.set_xlabel("Time (Day, Hour, Mintue)")βfig.subplots_adjust(hspace=0.4)plt.show()# If the option is bought at 09:31, then we would want to sell 74.38 sharespf = BS.delta *(-1)β# At 09:32 the stock price seems to have gone up, as did the delta# This means we would want to sell an additional 0.02 shares# Etc. for every minute that goes bytrades = pf.diff()trades[0] = -0.01 # BS.delta[0] *(-1) For visual purposestrades = pd.concat([trades, BS.S], axis=1)trades.columns = ['trades', 'price'] βprint(pf[0])trades.head()trades[0] = -0.01 # For visual purposesβfig = plt.figure(figsize=(15,5))βplt.plot(BS.S)plt.title("Price of the underlying and trades to remain hedged", size=20)plt.ylabel("SPY Price")plt.xlabel("Time (Day, Hour, Mintue)")βββplt.scatter(trades[trades.trades>0].index, trades[trades.trades>0].price, s=abs(trades[trades.trades>0].trades)*50000, color='green', label='Buys')plt.scatter(trades[trades.trades<0].index, trades[trades.trades<0].price, s=abs(trades[trades.trades<0].trades)*50000, color='red', label='Sells', alpha=0.9)βplt.legend(loc=(0.9,0.2), title = 'Trades')βplt.show()### Is this hedging resulting in profitable trades?# Liquidate the portfolio in the last tradetrades.iloc[0,0] = BS.delta[0] *(-1)trades.iloc[-1,0] = sum(trades.trades) *(-1)trades.head()# Simulate PnL of hedging # Watch out: Here we assumed no transaction costs, no spread costs and perfect liquidity for our tradesPnL = sum(trades.trades*(trades.price/trades.price[0]))print(f"{round(PnL*100, 2)}%")