Periods of high or low volatility tend to persist. Stock market volatility is typically tied to fear. Fear of recession, fear that the markets are over-priced, fear of new government regulation, fear of a pandemic1 This report has been updated to include the partial impact from COVID-19. Please see the prior report for comparison. Link: pre-COVID-19, etc. These type of fears often result in a negative feedback loop. The stock market starts to drop, market experts predict further declines, people panic and sell, the losses result in more fear, people continue to sell, etc. The panic builds on itself until it is exhausted, often weeks or months later. It is for this reason that analyzing volatility is important.
Many models, like regression, assume constant variance. With assets returns this would not be optimal, as volatility varies during certain periods of time and depends on past variance. The GARCH process2 generalized autoregressive conditional heteroskedasticity, being autoregressive, depends on past squared observations and past variances to model current variance.
In this report I will analyze several GARCH models, describe the selection process, and provide predictions for future observations.
I used the Quantmod package to retrieve RCL’s daily prices from January 3, 2000 through March 23, 2020. The default source is Yahoo Finance and the data is structured as an xts object3 extensible time series. I selected this date range because it includes two major recessions - the dotcom bubble, post 1999, and the “Great Recession” of 2008. It also includes the initial impact from COVID-19.
I chose to analyze Royal Caribbean due to its extreme sensitivity to economic events. The cruise line industry is very capital intensive. The cost to build a ship can range from $250 million to over $1 billion and it can take several years to complete. For example, Royal Caribbean’s Oasis of the Seas cost $1.4 billion and took more than two years to build4 the vessel was ordered in Feb 2006 and was completed in Oct 2009. Cruises are also a highly discretionary purchase. During recessions people often elect to skip vacations. This results in a steep decline in revenue, which is particularly troublesome for an industry with very high fixed costs. As a result the stock can be very volatile.
#Load project libraries
library(PerformanceAnalytics)
library(quantmod)
library(rugarch)
library(astsa)
library(tseries)
library(knitr)
library(kableExtra)
#Retrieve RCL Data
getSymbols("RCL",from = "2000-01-01",to = "2020-03-24")
[1] "RCL"
#Dsiplay 6 most recent dates
kable(as.data.frame(RCL[(nrow(RCL)-5):nrow(RCL),])
)%>% kable_styling(full_width=F, position = "left")
| RCL.Open | RCL.High | RCL.Low | RCL.Close | RCL.Volume | RCL.Adjusted | |
|---|---|---|---|---|---|---|
| 2020-03-16 | 29.90 | 35.48 | 29.28 | 29.94 | 25379100 | 29.94 |
| 2020-03-17 | 31.22 | 31.44 | 26.83 | 27.66 | 18129200 | 27.66 |
| 2020-03-18 | 26.02 | 26.30 | 19.25 | 22.33 | 24554800 | 22.33 |
| 2020-03-19 | 21.76 | 24.15 | 20.55 | 22.41 | 17031500 | 22.41 |
| 2020-03-20 | 24.45 | 28.10 | 22.00 | 23.81 | 22221500 | 23.81 |
| 2020-03-23 | 24.03 | 29.32 | 22.77 | 28.19 | 20278800 | 28.19 |
# Plot adjusted closing prices
plot(Ad(RCL),col=4)
RCL Adjusted Closing Prices
We can see from the stock chart that the price is not stationary. The mean is not constant.
#Retrieve YTD RCL Data
RCLytd <- getSymbols("RCL",from = "2019-12-31",to = "2020-03-24",
auto.assign = FALSE)
# Plot adjusted closing prices
plot(Ad(RCLytd),col=4)
RCL Adjusted Closing Prices YTD
Here we can see the dramatic impact from COVID-19.
#Calculate and view RCL LogReturns
RCLLogReturns <- diff(log(Ad(RCL)))
RCLLogReturns <- RCLLogReturns[-1]
plot(RCLLogReturns, col=6)
Log Returns
To make it stationary we can model the daily returns. Since the daily change is typically small we can approximate with log returns.
\[\nabla(X_t)\approx r_t\]
Mean: 0.000027
Standard Deviation: 0.031
The mean is almost 0 and the daily standard deviation is just over 3%.
# Compute annualized standard deviation
sqrt(252) * sd(RCLLogReturns) #Annual SD 0.4854384
sqrt(252) * sd(RCLLogReturns["2001"]) #2001 annualized 0.7719925
sqrt(252) * sd(RCLLogReturns["2008"]) #2008 annualized 0.8202522
sqrt(252) * sd(RCLLogReturns["2013"]) #2013 annualized 0.2459386
sqrt(252) * sd(RCLLogReturns["2020"]) #2020 annualized 1.343722
However, the standard deviation differs over time. The annualized standard deviation is 48.5%5 calculated as \(\sqrt{252}\) x daily \(\sigma\) , where \(252\) is the number of trading days in a year. The annualized standard deviation for 2001 was 77.2%, for 2008 it was 82%, for 2013 it was 24.6%, and for 2020 it is 134.4%.
adf.test(RCLLogReturns) #ADF Test
Augmented Dickey-Fuller Test
data: RCLLogReturns
Dickey-Fuller = -14.575, Lag order = 17, p-value = 0.01
alternative hypothesis: stationary
Based on the Augmented Dickey-Fuller test, with a p-value <0.05, we can accept the alternative hypothesis that the log returns are stationary.
#Chart RCL Returns distribution vs Normal Distribution
chart.Histogram(RCLLogReturns, methods = c("add.normal", "add.density"),
colorset=c("gray","red","blue"))
RCL Distribution in Red
Normal Distribution in Blue
The log returns are not normally distributed. They have a higher peak, fatter tails, and are negatively skewed. This suggests that using a skewed Student’s T distribution may be appropriate..
acf2(RCLLogReturns) #ACF/PACF of logged returns
Logged Returns
Autocorrelation Function
Partial Autocorrelation Function
The ACF/PACF of the Log Returns do not show any clear patterns and would be considered white noise.
acf2(RCLLogReturns^2) #ACF/PACF of squared logged returns
Squared Logged Returns
Autocorrelation Function
Partial Autocorrelation Function
The ACF/PACF of the squared log returns show many significant values indicating that there is a higher-order dependence structure. This indicates that a GARCH model would be appropriate.
#Constant mean, standard garch(1,1) model, sstd
garchspec6 <- ugarchspec(
mean.model = list(armaOrder=c(0,0)),
variance.model = list(model = "sGARCH",garchOrder=c(1,1)),
distribution.model ="sstd")
garchfit6 <- ugarchfit(data=RCLLogReturns,
spec=garchspec6)
infocriteria(garchfit6) #AIC -4.750270
Akaike -4.750270
Bayes -4.742561
Shibata -4.750273
Hannan-Quinn -4.747570
| Number | ARMA order | GARCH order | Distribution | AIC |
|---|---|---|---|---|
| 1 | (0,0) | (1,1) | Normal | -4.591540 |
| 2 | (0,0) | (0,1) | Normal | -4.137421 |
| 3 | (0,0) | (1,0) | Normal | -4.323246 |
| 4 | (0,0) | (2,1) | Normal | -4.591130 |
| 5 | (0,0) | (2,2) | Normal | -4.591130 |
| 6 | (0,0) | (1,1) | SSTD | -4.750270 |
| 7 | (1,0) AR(1) | (1,1) | SSTD | -4.751446 |
| 8 | (0,1) MA(1) | (1,1) | SSTD | -4.751481 |
I evaluated eight models and used the AIC value to make my selection. Although the AR(1)+GARCH and MA(1)+GARCH had the lowest AIC values their added complexity didn’t improve the model much, and so I selected model six6 GARCH(1,1) with Skewed Student’s T Distribution. Increasing the GARCH order beyond (1,1) also did not improve the model much.
#Compute Standardized Residuals
stdRCLret <- residuals(garchfit6, standardize=TRUE)
mean(stdRCLret)
[1] -0.02606268
sd(stdRCLret)
[1] 1.016573
The mean of the standardized residuals is close to 0 and the standard deviation is close to 1. The standardized residuals are assumed to be i.i.d. N(0,1)
plot(stdRCLret, col="orange")
Standardized Residuals
There is one large outlier (when the stock dropped 50% in 2001). Other than that, the standardized residuals appear to have a constant mean and variance with no clear pattern.
#ACF of Standardized Residuals
plot(garchfit6, which=10)
ACF of Standardized Residuals
The ACF of the standardized residuals would be considered a white noise pattern
#ACF of Squared Standardized Residuals
plot(garchfit6, which=11)
ACF of Squared Standardized Residuals
The ACF of the squared standardized residuals would be considered a white noise pattern. Although one is significant, the majority are not.
#QQ Plot standardized residuals
plot(garchfit6, which=9)
QQ Plot of Standardized Residuals
The QQ plot looks mostly normal, with the exception of a few outliers. The one large outlier dramatically alters the plot slope.
#Empirical Density of Standardized Residuals
plot(garchfit6, which=8)
Empirical Density of Standardized Residuals
The standardized residuals are not exactly normally distributed, but they are close (with the exception of a few outliers)
round(garchfit6@fit$matcoef,6) #Coefficents and P values
Estimate Std. Error t value Pr(>|t|)
mu 0.000746 0.000253 2.944198 0.003238
omega 0.000003 0.000008 0.400919 0.688480
alpha1 0.051377 0.033446 1.536114 0.124510
beta1 0.947580 0.033250 28.498843 0.000000
skew 1.021471 0.017066 59.855060 0.000000
shape 4.420089 0.320901 13.774003 0.000000
The GARCH(1,1) model with constant mean is represented by:
\[R_t =\mu +e_t\]
\[e_t\sim N(0,\sigma^2_t)\]
\[\sigma^2_t = \alpha_0 + \alpha_1e^2_{t-1}+\beta_1\sigma^2_{t-1}\] (Note: I have ignored the shape and skew for this analysis)
Based on the coefficients we arrive at the following model:7 However, the p-values for omega and alpha are >0.05. Since the coefficients are not significant they would likely be eliminated
\[R_t = 0.00074 + e_t\] \[e_t\sim N(0,\hat{\sigma}_t^2)\] \[\hat{\sigma}_t^2=0.000003+0.051377e^2_{t-1}+0.94758\hat{\sigma}^2_{t-1}\]
#Unconditional Variance
garchuncvar <- uncvariance(garchfit6)
sqrt(garchuncvar) #long run SD 0.0546621
The model predicts long-run standard deviation of 0.0558 calculated as \(\sqrt{\frac{\alpha_0}{1-\alpha-\beta}}\), which is above the actual daily standard deviation of 0.031.
#Predicted Volatilities
garchvol <-sigma(garchfit6)
plot(garchvol,colorset=tim8equal)
Estimated Volatilities
This chart shows the predicted volatilities. We see the large spikes in 2001, 2008, and 2020, which correspond to the dramatic price declines.
# Compare absolute returns versus predictions
plotvol <- plot(abs(RCLLogReturns), col = "steelblue1")
plotvol <- addSeries(garchvol, col = "black", on = 1, lwd=2)
plotvol
Estimated Volatility vs. Absolute Returns
Here we can see the predicted volatilities relative to the absolute value of returns. The patterns match closely.
#Chart Rolling 1 month volatility versus predictions
plotvol<-chart.RollingPerformance(R = RCLLogReturns,
width = 22,
FUN = "sd.annualized",
scale = 252,
colorset="steelblue1",
xlim=as.Date(c("2000-01-09", "2020-01-18")))
plotvol <- addSeries(sqrt(252)*garchvol, col = "red", on = 1, lwd=2)
plotvol
RCL annualized volatility, rolling 1 month
The model seems to fit the data very well, which can be visualized when the predictions are annualized. This chart compares the actual annualized volatility, calculated on a rolling 1 month basis (shown in Light Blue), compared to predicted annualized volatility (shown in Red).
Rolling window of 1 month (22 trading days)
For the next ten days the model predicts:
garchforecast6 <- ugarchforecast(fitORspec = garchfit6,n.ahead=1000)
cbind(head(fitted(garchforecast6),10),head(sigma(garchforecast6),10))
2020-03-23 2020-03-23
T+1 0.0007456264 0.1213640
T+2 0.0007456264 0.1213135
T+3 0.0007456264 0.1212631
T+4 0.0007456264 0.1212126
T+5 0.0007456264 0.1211622
T+6 0.0007456264 0.1211119
T+7 0.0007456264 0.1210615
T+8 0.0007456264 0.1210112
T+9 0.0007456264 0.1209610
T+10 0.0007456264 0.1209107
# Plot annualized Forecasts, no simulation
fd <- rollapply(RCLLogReturns,width=22,FUN = "sd")
fd <-fd*sqrt(252)
fd=as.ts(fd)
ts.plot(fd,xlim=c(4000,6085),ylim=c(0,2.25), col=4, ylab="Annualized standard deviation")
zs=sqrt(252)*sigma(garchforecast6)
af<-ts(zs, start=5085)
points(af,type="l",col=2)
Sigma Forecast
This chart compares the actual annualized volatility (calculated on a rolling 1 month basis and shown in blue) compared to the annualized forecasted volatility (shown in red). It predicts a slow decline from the heigthened COVID-19 volatility.
I have also used the model coefficients to forecast 4 iterations of 10 years of simulated returns, volatilities, and stock prices.9 Uses a random number generator to simulate the noise, \(\epsilon_t\))
#Simulate 10 years of returns
simgarchspec <- garchspec6
setfixed(simgarchspec) <-as.list(coef(garchfit6))
simgarch <- ugarchpath(spec=simgarchspec,m.sim=4,n.sim=10*252,rseed=12345)
simret<-fitted(simgarch)
plot.zoo(simret, col="purple")
10 years of Simulated Returns
#10 years of Simulated Volatilities
plot.zoo(sigma(simgarch), main="10 years of Simulated Volatilities", col="springgreen4", lwd=1)
10 years of Simulated Volatilities
Future Price = \(P_{t+h}=P_te^{(r_{t+1}+r_{t+2}+...+r_{t+h})}\)
simprices<-exp(apply(simret,2,"cumsum"))
matplot(simprices,type="l",lwd=3)
10 years of Simulated Prices