The following vignette demonstrates the steps needed to visualize implied volatility surface.
Note that the quantmod
library allows you to download
the option chain at each point of time. This means that for each
underlying, there is a chain of options that move accordingly, depending
on liquidity, as the underlying moves. This covers different strike
prices and maturities. The following works with the source code of
getOptionChain
for the SPY ETF:
library(quantmod)
library(lubridate)
library(plyr)
library(plotly)
rm(list = ls())
tic <- "SPY"
`getOptionChain` <-
function(Symbols, Exp=NULL, src="yahoo", ...) {
Call <- paste("getOptionChain",src,sep=".")
if(missing(Exp)) {
do.call(Call, list(Symbols=Symbols, ...))
} else {
do.call(Call, list(Symbols=Symbols, Exp=Exp, ...))
}
}
getOptionChain.yahoo <- function(Symbols, Exp, ...)
{
if(!requireNamespace("jsonlite", quietly=TRUE))
stop("package:",dQuote("jsonlite"),"cannot be loaded.")
NewToOld <- function(x) {
if(is.null(x) || length(x) < 1)
return(NULL)
# clean up colnames, in case there's weirdness in the JSON
names(x) <- tolower(gsub("[[:space:]]", "", names(x)))
# set cleaned up colnames to current output colnames
d <- with(x, data.frame(Strike=strike, Last=lastprice, Chg=change,
Bid=bid, Ask=ask, Vol=volume, OI=openinterest,
row.names=contractsymbol, stringsAsFactors=FALSE))
# remove commas from the numeric data
d[] <- lapply(d, gsub, pattern=",", replacement="", fixed=TRUE)
d[] <- lapply(d, type.convert, as.is=TRUE)
d
}
# Don't check the expiry date if we're looping over dates we just scraped
checkExp <- !hasArg(".expiry.known") || !match.call(expand.dots=TRUE)$.expiry.known
# Construct URL
urlExp <- paste0("https://query2.finance.yahoo.com/v7/finance/options/", Symbols[1])
# Add expiry date to URL
if(!checkExp)
urlExp <- paste0(urlExp, "?&date=", Exp)
# Fetch data (jsonlite::fromJSON will handle connection)
tbl <- jsonlite::fromJSON(urlExp)
# Only return nearest expiry (default served by Yahoo Finance), unless the user specified Exp
if(!missing(Exp) && checkExp) {
all.expiries <- tbl$optionChain$result$expirationDates[[1]]
all.expiries.posix <- .POSIXct(as.numeric(all.expiries), tz="UTC")
# this is a recursive command
if(is.null(Exp)) {
# Return all expires if Exp = NULL
out <- lapply(all.expiries, getOptionChain.yahoo, Symbols=Symbols, .expiry.known=TRUE)
# Expiry format was "%b %Y", but that's not unique with weeklies. Change
# format to "%b.%d.%Y" ("%Y-%m-%d wouldn't be good, since names should
# start with a letter or dot--naming things is hard).
return(setNames(out, format(all.expiries.posix, "%b.%d.%Y")))
}
else {
# Ensure data exist for user-provided expiry date(s)
if(inherits(Exp, "Date"))
valid.expiries <- as.Date(all.expiries.posix) %in% Exp
else if(inherits(Exp, "POSIXt"))
valid.expiries <- all.expiries.posix %in% Exp
else if(is.character(Exp)) {
expiry.range <- range(unlist(lapply(Exp, .parseISO8601, tz="UTC")))
valid.expiries <- all.expiries.posix >= expiry.range[1] &
all.expiries.posix <= expiry.range[2]
}
if(all(!valid.expiries))
stop("Provided expiry date(s) not found. Available dates are: ",
paste(as.Date(all.expiries.posix), collapse=", "))
expiry.subset <- all.expiries[valid.expiries]
if(length(expiry.subset) == 1)
return(getOptionChain.yahoo(Symbols, expiry.subset, .expiry.known=TRUE))
else {
out <- lapply(expiry.subset, getOptionChain.yahoo, Symbols=Symbols, .expiry.known=TRUE)
# See comment above regarding the output names
return(setNames(out, format(all.expiries.posix[valid.expiries], "%b.%d.%Y")))
}
}
}
dftables <- lapply(tbl$optionChain$result$options[[1]][,c("calls","puts")], `[[`, 1L)
#dftables <- mapply(NewToOld, x=dftables, SIMPLIFY=FALSE)
fix_date <- function(x) {
if(class(x) == "list")
return(NULL)
x$expiration <- .POSIXct(as.numeric(x$expiration), tz="UTC")
x$lastTradeDate <- .POSIXct(as.numeric(x$lastTradeDate), tz="UTC")
x <- x[,sort(names(x))]
return(x)
}
dftables <- lapply(dftables,fix_date)
dftables <- dftables[!sapply(dftables,is.null)]
dftables
}
# EXAMPLE to get all expiration in a single data.frame object
ds <- getOptionChain.yahoo(tic,NULL)
ds <- lapply(ds, function(ds_i) lapply(1:length(ds_i),
function(i) data.frame(Type = names(ds_i)[i], ds_i[[i]])) )
ds <- lapply(ds, function(ds_i) do.call(plyr::rbind.fill,ds_i) )
ds <- do.call(plyr::rbind.fill,ds)
ds$Date <- date(ds$lastTradeDate )
ds$Expiration <- date(ds$expiration)
We have about r
ncol(ds)` variables in the data with 7283
observations.
head(ds)
smile.T <- function(T.) {
# find the closest expiration data around T.
which.exp <- ds[which.min( abs( (ds$Expiration - ds$Date)/250 - T.) ),"Expiration"]
T. <- min(unique(which.exp - ds$Date)/250)
ds.i <- ds[ds$Expiration %in% which.exp,]
dim(ds.i)
ds.i <- ds.i[with(ds.i,order(Type,strike)),]
ds.l <- dlply(ds.i,"Type",function(x) x[,c("strike","lastPrice")] )
ds.i <- Reduce(function(...) merge(..., by = "strike"), ds.l )
dim(ds.i)
names(ds.i) <- c("K","C","P")
x <- lm(I(C-P) ~ I(-K), data = ds.i)[[1]]
disc <- as.numeric(x[2])
PVF <- as.numeric(x[1])
r <- (-log(disc)/T.)
# compare with BS formula
bs <- function(PVF, K, disc, T., sigma) {
values <- c(2)
d1 <- log(PVF/(K*disc))/(sigma*sqrt(T.)) + 0.5*sigma*sqrt(T.)
d2 <- d1 - sigma * sqrt(T.)
values[1] <- PVF*pnorm(d1) - K*disc*pnorm(d2)
values[2] <- K*disc*pnorm(-d2) - PVF*pnorm(-d1)
return(values)
}
K <- ds.i[,"K"]
C <- ds.i[,"C"]
P <- ds.i[,"P"]
Sigma <- numeric()
Sigma2 <- numeric()
for(i in 1:length(K)) {
# cat(i,"\n")
sigma.i <- try(uniroot(function(x) bs(PVF, K[i], disc, T.,x)[1] - C[i], c(-1,2))$root, silent = T)
if(inherits(sigma.i,"try-error"))
sigma.i <- NA
sigma.j <- try(uniroot(function(x) bs(PVF, K[i], disc, T.,x)[2] - P[i], c(-1,2))$root, silent = T)
if(inherits(sigma.j,"try-error"))
sigma.j <- NA
Sigma <- c(Sigma,sigma.i) # call prices
Sigma2 <- c(Sigma2,sigma.j) # put pries
}
smile <- data.frame(Sigma = Sigma,K)
smile <- smile[!smile$Sigma < 0.05,]
smile$T. <- T.
smile <- na.omit(smile)
# {
# plot(Sigma~K, data =smile, pch = 20, cex = 0.5, main = "Call Options", ylab = expression(sigma) )
# lines(fitted(loess(Sigma~K,data = smile,span = 1))~smile$K, col = 2)
# grid(10)
# }
smile2 <- data.frame(Sigma = Sigma2,K)
smile2 <- smile2[!smile2$Sigma < 0.05,]
smile2$T. <- T.
smile2 <- na.omit(smile2)
# {
# plot(Sigma~K, data =smile2, pch = 20, cex = 0.5, main = "Put Options", ylab = expression(sigma))
# lines(fitted(loess(Sigma~K,data = smile2,span = 1))~smile2$K, col = 2)
# grid(10)
# }
list(smile,smile2,PVF,T.)
}
T.seq <- seq(3,36,by = 3)/12
smile.l <- lapply(T.seq, smile.T)
smile.call <- lapply(smile.l, function(x) x[[1]] )
smile.put <- lapply(smile.l, function(x) x[[2]] )
smile.pvf <- sapply(smile.l, function(x) x[[3]] )
smile.T <- sapply(smile.l, function(x) x[[4]] )
smile.call <- unique(Reduce(rbind,smile.call))
smile.put <- unique(Reduce(rbind,smile.put))
# add underlying price
S_0 <- as.numeric(last(get(getSymbols(tic))[,6]))
smile.call$INM <- log(S_0/smile.call$K)
smile.put$INM <- log(S_0/smile.put$K)
smile.call <- smile.call[smile.call$INM < 0,]
smile.put <- smile.put[smile.put$INM > 0,]
plot_ds1 <- smile.call[smile.call$T. %in% min(smile.call$T.),]
plot_ds2 <- smile.put[smile.put$T. %in% min(smile.put$T.),]
plot_ds <- rbind(plot_ds1,plot_ds2)
plot_ds <- plot_ds[order(plot_ds$K),]
plot(Sigma~K, data =plot_ds, pch = 20, cex = 0.5, main = "Volatility Smile for OTM Calls and Puts",
ylab = expression(sigma) )
lines(fitted(loess(Sigma~K,data = plot_ds,span = 1))~plot_ds$K, col = 2)
grid(10)
Finally, we use plotly
to visualize the surface
model <- loess(Sigma~K+T.,data = smile.call, control = loess.control(surface = "interpolate"))
K.seq <- seq(range(smile.call$K)[1],range(smile.call$K)[2],by = 1)
T.seq <- seq(range(smile.call$T.)[1],range(smile.call$T.)[2],by = 1/12)
X <- expand.grid(K.seq,T.seq)
names(X) <- c("K","T.")
Z <- predict(model,X)
{
f <- list(
family = "Courier New, monospace",
size = 11,
color = "#7f7f7f"
)
axx <- list(
title = "Exercise Price",
titlefont = f
)
axy <- list(
title = "Expiration (Years)",
titlefont = f
)
axz <- list(
title = "Implied Volatility",
titlefont = f
)
p <- plot_ly(x = K.seq, y = T.seq, z = Z) %>% add_surface()
p <- layout(p,title = paste("Volatility Surface for", tic),scene = list(xaxis=axx,yaxis=axy,zaxis=axz))
p
}
## This version of 'bslib' is designed to work with 'shiny' >= 1.6.0.
## Please upgrade via install.packages('shiny').