Options volatility

We will now examine implied volatility in the market. We do this by acquiring data on ticker symbol SPY, which is an exchange-traded fund (ETF) that replicates the S&P 500 index. Using the underlying stock and options price data, we calculate and plot the volatility curve or “smile.”


Load libraries

library(plyr)
library(dplyr)
library(tidyr)
library(tibble)
library(ggplot2)
library(rvest)
require(quantmod)
require(derivmkts)
library(knitr)


Define variables

We set the ticker symbol to SPY and the interest rate to 2.25 percent.

interest_rate <- 0.0225
symbol <- "SPY"


Get last closing price

We use the quantmod package to look up historical stock prices and save the most recent closing price.

price_df <- getSymbols(symbol, auto.assign=F, from=Sys.Date()-3) %>% as.data.frame()
names(price_df) <- names(price_df) %>% gsub(paste0(symbol, "\\."), "", .)
price_df <- price_df %>% 
  rownames_to_column(var="Date") %>% 
  mutate(Date=as.Date(Date)) %>% 
  arrange(desc(Date)) %>% slice(1)
stock_price <- price_df$Close
price_df %>% head() %>% kable()
Date Open High Low Close Volume Adjusted
2019-05-08 287.53 289.43 286.87 287.53 91412500 287.53


Get dividend info

We look up the empirical dividend stream and convert it into a yield percent (to plug into the options pricing model.)

div_df <- getDividends(symbol, from = Sys.Date()-365, to=Sys.Date()) %>% as.data.frame()
names(div_df) <- names(div_df) %>% gsub(paste0(symbol, "\\."), "", .)
div_df <- div_df %>% rownames_to_column(var="Date") %>% mutate(Date=as.Date(Date))
div_yield <- suppressWarnings( tryCatch( sum(div_df$div)/stock_price, error=function(e) 0))
div_yield
## [1] 0.01821375


Get options chain

We use the quantmod package to retrieve options prices from Yahoo Finance. The implied volatility values are often missing or incorrect, so we will calculate them ourselves using the Black-Scholes formula from the derivmkts package. We do that by looping through the list of options and using the necessary information as inputs - stock price, strike, option type and price, time to expiration, dividend yield, and interest rate.

### Get option prices
option_chain_list <- getOptionChain(Symbols = symbol, Exp = NULL)
option_chain_df <- data.frame()

for (i in 1:length(option_chain_list)){
  if (!is.null(option_chain_list[[i]]$calls) & !is.null(option_chain_list[[i]]$puts)){

    exp <- names(option_chain_list)[[i]] %>% as.Date(., format="%b.%d.%Y")
    t <- (as.numeric(exp-Sys.Date()))/365
    call_df <- option_chain_list[[i]]$calls %>% mutate(callput="call", time=t, exp=exp)
    put_df <- option_chain_list[[i]]$puts %>% mutate(callput="put", time=t, exp=exp)

    ### Calculate Implied Vol
    bidIV <- vector()
    askIV <- vector()
    for (k in 1:nrow(call_df)){
      bidIV[k] <- tryCatch(bscallimpvol(s=stock_price, k=call_df$Strike[k], r=interest_rate, tt=call_df$time[k],
                                        d=div_yield, price=call_df$Bid[k]), error=function(e) NA)
      askIV[k] <- tryCatch(bscallimpvol(s=stock_price, k=call_df$Strike[k], r=interest_rate, tt=call_df$time[k],
                                        d=div_yield, price=call_df$Ask[k]), error=function(e) NA)
    }
    call_df <- call_df %>% mutate(bidIV=suppressWarnings(as.numeric(bidIV)),
                                  askIV=suppressWarnings(as.numeric(askIV)), midIV=(bidIV+askIV)/2)
    bidIV <- vector()
    askIV <- vector()
    for (k in 1:nrow(put_df)){
      bidIV[k] <- tryCatch(bsputimpvol(s=stock_price, k=put_df$Strike[k], r=interest_rate, tt=put_df$time[k],
                                       d=div_yield, price=put_df$Bid[k]), error=function(e) NA)
      askIV[k] <- tryCatch(bsputimpvol(s=stock_price, k=put_df$Strike[k], r=interest_rate, tt=put_df$time[k],
                                       d=div_yield, price=put_df$Ask[k]), error=function(e) NA)
    }
    put_df <- put_df %>% mutate(bidIV=suppressWarnings(as.numeric(bidIV)),
                                askIV=suppressWarnings(as.numeric(askIV)), midIV=(bidIV+askIV)/2)

    option_chain_df <- rbind.fill(option_chain_df, call_df, put_df)
  }
}
option_chain_df <- option_chain_df %>% 
  filter(!is.na(midIV)) %>% mutate(underlying=symbol,
                                   stock_price=stock_price,
                                   price_date = price_df$Date)
option_chain_df %>% head() %>% kable()
Strike Last Chg Bid Ask Vol OI callput time exp bidIV askIV midIV underlying stock_price price_date
285.0 3.23 -1.2500000 3.16 3.20 33570 3226 call 0.0027397 2019-05-10 0.2644935 0.2726526 0.2685731 SPY 287.53 2019-05-08
286.0 2.41 -1.3599999 2.50 2.52 12691 941 call 0.0027397 2019-05-10 0.2704158 0.2739998 0.2722078 SPY 287.53 2019-05-08
287.5 1.56 -1.1600001 1.66 1.69 8908 4023 call 0.0027397 2019-05-10 0.2737221 0.2787196 0.2762208 SPY 287.53 2019-05-08
288.0 1.46 -0.9200001 1.42 1.45 50298 8764 call 0.0027397 2019-05-10 0.2734018 0.2784263 0.2759140 SPY 287.53 2019-05-08
295.0 0.04 -0.0400000 0.03 0.04 23826 33508 call 0.0027397 2019-05-10 0.2427960 0.2542577 0.2485269 SPY 287.53 2019-05-08
297.5 0.01 -0.0100000 0.00 0.01 49 4713 call 0.0027397 2019-05-10 0.0010000 0.2673100 0.1341550 SPY 287.53 2019-05-08


Plot volatility curve

We use ggplot to show the volatility smile and term structure for all options expirations between three and nine months out.

option_chain_df %>% 
  filter(exp>Sys.Date()+90, 
         exp<Sys.Date()+270, 
         Strike>=stock_price & callput=="call" | Strike<stock_price & callput=="put", 
         OI>1000, 
         Bid>=0.25) %>% 
  mutate(Expiration=as.character(exp), midIV=midIV*100) %>% 
  ggplot(aes(x=Strike, y=midIV, colour=Expiration)) + geom_line() + labs(title=paste0(symbol, " Options Implied Volatility by Expiration and Strike"), x="Strike Price", y="Mid-Market Implied Volatility")

We see that implied volatility is higher at lower strike prices than higher strike prices. This phenomenon is what options traders refer to as “volatility skew.”

Theoretically the Black-Scholes model considers volatility to be a single constant number. It assumes that stock price movement percentages are normally distributed.

If the options market agreed with that assumption, every strike price would have the same implied volatility. We can see from the plot that this is clearly not the case. Because the market believes the true underlying distribution of stock price movement has a “fatter” left tail than normal, lower strikes trade at a higher price, and therefore higher implied volatility, than Black-Scholes would calculate.