Updating VIX and Realized Vol Post
In a previous post, from way back in August of 2017, we explored the relationship between the VIX and the past, realized volatility of the S&P 500 and reproduced some an interesting work from AQR on the meaning of the VIX.
With the recent market and VIX rollercoaster, this seemed a good time to revisit the old post, update some code and see if we can tweak the data visualizations to shed some light on the recent market activity. Today's post, along with a plethora of code and Shiny apps, is also available at the Reproducible Finance book/blog global headquarters.
Import prices, calculate returns and rolling volatility
By way of brief reminder, we first want to import data on SP500 and VIX prices since 2010, then calculate the rolling standard deviation of SP500 20-day and 60-day returns. In the previous post, we used the rollapply() function to accomplish this. Today, we will use the roll_sd() function from the RcppRoll package. That will allow us to live in the tibble world instead of the xts world, and it will mean we have a reproducible example from each of those worlds in case we need them for future work.
Let’s get to it.
library(RcppRoll) library(timetk) library(tibbletime) library(tidyquant) library(tidyverse) library(broom)
We import prices with the same code as before.
symbols <- c("^GSPC", "^VIX")
prices <-
getSymbols(symbols, src = 'yahoo', from = "2010-01-01",
auto.assign = TRUE, warnings = FALSE) %>%
map(~Ad(get(.))) %>%
reduce(merge) %>%
`colnames<-`(c("sp500", "vix"))
Next we convert that object to a tibble using tk_tbl(preserve_index = TRUE, rename_index = "date") from the timetk package. Now we can use dplyr's mutate() function to add a colum for returns with mutate(sp500_returns = (log(sp500) - log(lag(sp500)))), and then a column for the rolling 20-day volatility with mutate(sp500_roll_20 = roll_sd(sp500_returns, 20, fill = NA, align = "right"). I want to annualize the rolling volatility (as the AQR piece did) so will then mutate the 20-day rolling vol with sp500_roll_20 = (round((sqrt(252) * sp500_roll_20 * 100), 2)).
sp500_vix_rolling_vol <-
prices %>%
tk_tbl(preserve_index = TRUE, rename_index = "date") %>%
mutate(sp500_returns = (log(sp500) - log(lag(sp500)))) %>%
replace_na(list(sp500_returns = 0)) %>%
mutate(sp500_roll_20 = roll_sd(sp500_returns, 20, fill = NA, align = "right"),
sp500_roll_20 = (round((sqrt(252) * sp500_roll_20 * 100), 2))) %>%
na.omit()
head(sp500_vix_rolling_vol)
## date sp500 vix sp500_returns sp500_roll_20
## 1 2010-02-01 1089 22.6 0.0142 15.8
## 2 2010-02-02 1103 21.5 0.0129 16.6
## 3 2010-02-03 1097 21.6 -0.00549 16.6
## 4 2010-02-04 1063 26.1 -0.0316 19.7
## 5 2010-02-05 1066 26.1 0.00289 19.6
## 6 2010-02-08 1057 26.5 -0.00890 19.6
Have a quick peek at our new data object and make sure the origin of each column is clear.
Visualizing Realized Vol and Vix
As we did before, let’s start with a scatterplot to show 20-day trailing volatility on the x-axis and the VIX on the y-axis. This is nothing more than updating our July 2017 work with new data through to February of 2018. In other words, we haven’t done anything yet that we couldn’t have accomplished by re-running the old script.
sp500_vix_rolling_vol %>%
ggplot(aes(x = sp500_roll_20, y = vix)) +
geom_point(colour = "light blue") +
geom_smooth(method='lm', se = FALSE, color = "pink", size = .5) +
ggtitle("Vix versus 20-Day Realized Vol") +
xlab("Realized vol preceding 20 trading days: 2010 - Present") +
ylab("Vix") +
# Add a '%' sign to the axes without having to rescale.
scale_y_continuous(labels = function(x){ paste0(x, "%") }) +
scale_x_continuous(labels = function(x){ paste0(x, "%") }) +
theme(plot.title = element_text(hjust = 0.5))
Same as before, we see a strong relationship between preceding volatility and the VIX. Now let’s see how that relationship has look over the last three months, from November 2017 to February 2018. We do that by adding filter(date >= "2017-11-01").
sp500_vix_rolling_vol %>%
group_by(date) %>%
filter(date >= "2017-11-01") %>%
ggplot(aes(x = sp500_roll_20, y = vix)) +
geom_point(color = "cornflowerblue") +
geom_smooth(method='lm', se = FALSE, color = "pink", size = .5) +
ggtitle("Vix versus 20-Day Realized Vol: Nov 2017 - Present ") +
xlab("Realized vol preceding 20 trading days") +
ylab("Vix") +
# Add a '%' sign to the axes without having to rescale.
scale_y_continuous(labels = function(x){ paste0(x, "%") }) +
scale_x_continuous(labels = function(x){ paste0(x, "%") }) +
theme(plot.title = element_text(hjust = 0.5))
Alright, we can see 5 observations way off to the upper right, where realized 20-day vol and the VIX have spiked to ~20%. Are those data points from the week of February 5th, 2018? We can find out by adding an aesthetic to color the points by date. We do that with ggplot(aes(x = sp500_roll_20, y = vix, color = date)).
sp500_vix_rolling_vol %>%
group_by(date) %>%
filter(date >= "2017-11-01") %>%
ggplot(aes(x = sp500_roll_20, y = vix, color = date)) +
geom_point() +
geom_smooth(method='lm', se = FALSE, color = "pink", size = .5) +
ggtitle("Vix versus 20-Day Realized Vol shaded by date: Nov 2017 - Present") +
xlab("Realized vol preceding 20 trading days") +
ylab("Vix") +
# Add a '%' sign to the axes without having to rescale.
scale_y_continuous(labels = function(x){ paste0(x, "%") }) +
scale_x_continuous(labels = function(x){ paste0(x, "%") }) +
theme(plot.title = element_text(hjust = 0.5))
Since we grouped by date and set the points to color by date, the dots are getting a lighter shade of blue as they move toward the present. It shows that in November, all was calm and quiet - look at the dark blue circles. Then, the points start to creep up and to the right - realized vol is increasing and the VIX is increasing. We expect them to move together, though AQR’s original point is that the VIX really is a reflection of past realized volatility, whereas many have hypothesized that the VIX caused market volatility last week. I’ll leave that one to the experts.
Let’s look at another chart to put last week in perspective. We will look at our data since 2010 and shade the points by date. This should contextualize last week.
sp500_vix_rolling_vol %>%
group_by(date) %>%
ggplot(aes(x = sp500_roll_20, y = vix, color = date)) +
geom_point() +
geom_smooth(method='lm', se = FALSE, color = "pink", size = .5) +
ggtitle("Vix versus 20-Day Realized Vol shaded by date ") +
xlab("Realized vol preceding 20 trading days") +
ylab("Vix") +
scale_y_continuous(labels = function(x){ paste0(x, "%") }) +
scale_x_continuous(labels = function(x){ paste0(x, "%") }) +
theme(plot.title = element_text(hjust = 0.5))
Ok, the light blue dots, those from 2017 and 2018 are still quite clustered at the low VIX low realized vol part of the chart, though some are indeed beginning to explore riskier territory. Our most extreme readings are darker blue - they are from 2011-2013. If we wish to isolate just one year - say, 2012 - we can do so with filter(date >= "2011-12-31" & date <= "2013-01-01").
sp500_vix_rolling_vol %>%
group_by(date) %>%
filter(date >= "2011-12-31" & date <= "2013-01-01") %>%
ggplot(aes(x = sp500_roll_20, y = vix, color = date)) +
geom_point() +
geom_smooth(method='lm', se = FALSE, color = "pink", size = .5) +
ggtitle("Vix versus 20-Day Realized Vol: 2012") +
xlab("Realized vol preceding 20 trading days") +
ylab("Vix") +
# Add a '%' sign to the axes without having to rescale.
scale_y_continuous(labels = function(x){ paste0(x, "%") }) +
scale_x_continuous(labels = function(x){ paste0(x, "%") }) +
theme(plot.title = element_text(hjust = 0.5))
Finally, let’s rerun our regression of the VIX on 20-day trailing volatility and peek at the results.
sp500_vix_rolling_vol %>%
do(model_20 = lm(vix ~ sp500_roll_20, data = .)) %>%
tidy(model_20)
## term estimate std.error statistic p.value
## 2 sp500_roll_20 0.7177137 0.01055153 68.01984 0
sp500_vix_rolling_vol %>%
do(model_20 = lm(vix ~ sp500_roll_20, data = .)) %>%
glance(model_20) %>%
select(r.squared)
## r.squared
## 1 0.6960897
We can see a coefficient of .71 for rolling 20-day vol and an R-squared of .69, which is the same as we observed back in July 2017 and consistent with the original AQR research that got us started.
That’s all for today - thanks for reading!
PE&VC: Researcher, Advisor, Fundraiser – Private Consulting Company
6 年cc Martin Davies Guan Seng Khoo, PhD