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.”
library(plyr)
library(dplyr)
library(tidyr)
library(tibble)
library(ggplot2)
library(rvest)
require(quantmod)
require(derivmkts)
library(knitr)
We set the ticker symbol to SPY and the interest rate to 2.25 percent.
interest_rate <- 0.0225
symbol <- "SPY"
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 |
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
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 |
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.