One way to avoid look-ahead bias is to iterate over the data, repeatedly fitting and forecasting, while ensuring that the model is only ever fit to historical data. This can be laborious and error-prone. With the `{rugarch}`

package a better option is to use the `ugarchroll()`

function which will “roll” along the data, fitting and predicting as it goes.

A moving (or rolling) window of fixed length moves along the data, fitting and testing the model as it goes. You need not refit the model at each time step.

```
specification <- ugarchspec(
mean.model = list(armaOrder = c(0, 0)),
variance.model = list(model = "sGARCH"),
distribution.model = "sstd"
)
rolling <- ugarchroll(
specification,
data = TATASTEEL,
n.start = 500,
refit.every = 50,
refit.window = "moving"
)
```

The new parameters required to get this working are: `n.start`

, `refit.window`

and `refit.every`

. What do these parameters mean?

`n.start`

— number of time steps in window;`refit.window`

— type of window (`"moving"`

,`"recursive"`

or`"expanding"`

);`refit.every`

— number of time steps between refitting the model.

The refit frequency should be determined by how dynamic your data is. If the nature of the data often changes then you’ll want to refit more frequently (smaller `refit.every`

).

Let’s take a look at the results of the rolling fit.

```
rolling
```

```
*-------------------------------------*
* GARCH Roll *
*-------------------------------------*
No.Refits : 20
Refit Horizon : 50
No.Forecasts : 984
GARCH Model : sGARCH(1,1)
Distribution : sstd
Forecast Density:
Mu Sigma Skew Shape Shape(GIG) Realized
2018-01-10 0.0023 0.0138 1.1313 4.9884 0 0.0042
2018-01-11 0.0023 0.0137 1.1313 4.9884 0 -0.0062
2018-01-12 0.0023 0.0136 1.1313 4.9884 0 -0.0012
2018-01-15 0.0023 0.0136 1.1313 4.9884 0 0.0150
2018-01-16 0.0023 0.0135 1.1313 4.9884 0 -0.0165
2018-01-17 0.0023 0.0136 1.1313 4.9884 0 0.0108
..........................
Mu Sigma Skew Shape Shape(GIG) Realized
2021-12-24 0.0035 0.0268 1.0265 5.0376 0 -0.0084
2021-12-27 0.0035 0.0263 1.0265 5.0376 0 0.0067
2021-12-28 0.0035 0.0257 1.0265 5.0376 0 0.0022
2021-12-29 0.0035 0.0251 1.0265 5.0376 0 -0.0100
2021-12-30 0.0035 0.0248 1.0265 5.0376 0 -0.0125
2021-12-31 0.0035 0.0246 1.0265 5.0376 0 0.0095
Elapsed: 9.273352 secs
```

Where does the number of refits come from? It’s determined by the parameters listed above. We can check:

```
(nrow(TATASTEEL) - 500) %/% 50
```

```
[1] 19
```

And, of course, you need to add 1 to that for the initial fit.

The resulting object has a specialised `plot()`

method that allows you to access various views via the `which`

parameter. Let’s start by comparing the predicted and realised returns.

The agreement is not terribly good. However, for a GARCH model the focus is more on modelling the volatility than the returns themselves. So let’s compare the predicted and realised volatilities.

A Value at Risk (VaR) plot shows how the estimated risk associated with this asset changes over time. Exceedances (indicated by red diamonds) indicate days when the actual (negative) return is worse than that predicted by the model.

Finally, since we are refitting the model periodically the model coefficients also change with time.

Now let’s generate a VaR report using the backtest data.

```
report(rolling, type="VaR", VaR.alpha = 0.01, conf.level = 0.95)
```

```
VaR Backtest Report
===========================================
Model: sGARCH-sstd
Backtest Length: 984
Data:
==========================================
alpha: 1%
Expected Exceed: 9.8
Actual VaR Exceed: 19
Actual %: 1.9%
Unconditional Coverage (Kupiec)
Null-Hypothesis: Correct Exceedances
LR.uc Statistic: 6.77
LR.uc Critical: 3.841
LR.uc p-value: 0.009
Reject Null: YES
Conditional Coverage (Christoffersen)
Null-Hypothesis: Correct Exceedances and
Independence of Failures
LR.cc Statistic: 7.552
LR.cc Critical: 5.991
LR.cc p-value: 0.023
Reject Null: YES
```

The `VaR.alpha`

parameter is the tail probability and the `conf.level`

parameter specifies the confidence level for the conditional coverage test.

The report presents results for two tests:

*Kupiec Test*— Checks whether the number of exceedances is consistent with the confidence level of the VaR model.*Christoffersen Test*— Checks whether the number of exceedances is consistent with the confidence level of the VaR model and if the exceedances are independent.

The report indicates that both of these tests fail at the specified levels (both reject the null hypothesis).

The Forecast Performance Measures report gives another view on the backtest, providing some metrics on the models’ ability to predict the returns. It generates the following statistics:

`MSE`

— Mean Square Error;`MAE`

— Mean Absolute Error; and`DAC`

— Directional Accuracy.

```
report(rolling, type="fpm")
```

```
GARCH Roll Mean Forecast Performance Measures
---------------------------------------------
Model : sGARCH
No.Refits : 20
No.Forecasts: 984
Stats
MSE 0.0006922
MAE 0.0194600
DAC 0.4878000
```

Where do those metrics come from? Here’s how you can calculate the MSE manually:

```
predictions <- as.data.frame(rolling)
error <- predictions$Realized - predictions$Mu
mean(error^2)
```

```
[1] 0.0006921929
```

Since we built multiple models the coefficients are returned in a list, with an element in the list corresponding to each model.

```
coefficients <- coef(rolling)
```

How many models were built?

```
length(coefficients)
```

```
[1] 20
```

The coefficients for the first model:

```
first(coefficients)
```

```
$index
[1] "2018-01-09"
$coef
Estimate Std. Error t value Pr(>|t|)
mu 2.257771e-03 7.945969e-04 2.841404e+00 4.491535e-03
omega 1.988778e-13 2.393334e-06 8.309653e-08 9.999999e-01
alpha1 7.568099e-03 1.257178e-03 6.019912e+00 1.745115e-09
beta1 9.900562e-01 2.529260e-04 3.914411e+03 0.000000e+00
skew 1.131275e+00 7.216578e-02 1.567605e+01 0.000000e+00
shape 4.988402e+00 1.602756e+00 3.112390e+00 1.855791e-03
```

The coefficients for the last model:

```
last(coefficients)
```

```
$index
[1] "2021-11-12"
$coef
Estimate Std. Error t value Pr(>|t|)
mu 3.535206e-03 1.242210e-03 2.845901 4.428595e-03
omega 4.762838e-05 2.356481e-05 2.021165 4.326267e-02
alpha1 6.263835e-02 3.369429e-02 1.859020 6.302433e-02
beta1 8.833512e-01 4.021345e-02 21.966562 0.000000e+00
skew 1.026492e+00 5.380914e-02 19.076543 0.000000e+00
shape 5.037618e+00 1.157051e+00 4.353844 1.337709e-05
```

Being able to interrogate each of the individual models is useful because we can see if there are insignificant model coefficients. These data are complimentary to the model coefficient plots above.

Now we’ll repeat the process for another model, replacing `"sstd"`

with `"std"`

and using AR(1) rather than constant mean.

```
specification <- ugarchspec(
mean.model = list(armaOrder = c(1, 0), include.mean = TRUE),
variance.model = list(model = "sGARCH"),
distribution.model = "std"
)
rolling <- ugarchroll(
specification,
data = TATASTEEL,
n.start = 500,
refit.every = 50,
refit.window = "moving"
)
```

What is the Value at Risk performance?

```
report(rolling, type="VaR", VaR.alpha = 0.01, conf.level = 0.95)
```

```
VaR Backtest Report
===========================================
Model: sGARCH-std
Backtest Length: 984
Data:
==========================================
alpha: 1%
Expected Exceed: 9.8
Actual VaR Exceed: 15
Actual %: 1.5%
Unconditional Coverage (Kupiec)
Null-Hypothesis: Correct Exceedances
LR.uc Statistic: 2.355
LR.uc Critical: 3.841
LR.uc p-value: 0.125
Reject Null: NO
Conditional Coverage (Christoffersen)
Null-Hypothesis: Correct Exceedances and
Independence of Failures
LR.cc Statistic: 3.845
LR.cc Critical: 5.991
LR.cc p-value: 0.146
Reject Null: NO
```

Looks a bit better! Fewer exceedances and both tests are now passing, meaning that the null hypotheses (exceedances consistent with the specified confidence level) should not be rejected.

An expanding or recursive window includes _all_previous data.

A moving window is generally a good option because the model is being retrained on the same volume of data each time. However, perhaps you want to train each model on *all* previous data? In this case use an `"expanding"`

refit window.

```
rolling <- ugarchroll(
specification,
data = TATASTEEL,
n.start = 500,
refit.window = "expanding",
refit.every = 50
)
```

You can also do backtesting with the `{tsgarch}`

package.

```
specification <- garch_modelspec(
TATASTEEL,
model = "gjrgarch",
distribution = "sstd",
constant = FALSE,
order = c(1, 0)
)
```

```
backtest <- tsbacktest(
specification,
start = 500,
h = 1,
estimate_every = 50,
rolling = TRUE
)
```

At present there do not appear to be utilities comparable to those in `{rugarch}`

for analysing the results of the backtest and this currently needs to be done by hand.

Let’s build a portfolio consisting of Tata Steel and a risk-free asset (which might be government bonds, treasury bills or cash).

Start by building a simple GARCH model for Tata Steel that will enable us to determine how volatility (or risk) changes with time.

```
specification <- ugarchspec(
distribution.model = "norm",
mean.model = list(armaOrder = c(0, 0)),
variance.model = list(model = "sGARCH")
)
fit <- ugarchfit(data = TATASTEEL, spec = specification)
```

Suppose that we want to target a portfolio with 20% annualised volatility. We’ll use the annualised volatility of Tata Steel to derive the proportion that this stock should form in the portfolio. First we need the annualised volatility.

```
annualised <- sqrt(252) * sigma(fit)
```

Now use the inverse of the annualised volatility to find the required proportion of Tata Steel in the portfolio. Since the portfolio will consist of just Tata Steel and a risk-free asset (which by definition has zero volatility), the portfolio volatility depends exclusively on Tata Steel.

```
weights <- 0.20 / annualised
```

Now we can compare the weighting of Tata Steel in the portfolio to its annualised volatility. Observe that as the volatility of the stock increases it should form a smaller proportion of the portfolio.

]]>The art of the econometrician consists in finding the set of assumptions which are both sufficiently specific and sufficiently realistic to allow him to take the best possible advantage of the data available to him. Edmond Malinvaud, “Statistical Methods of Econometrics”

If you have insight into the values or ranges for specific parameters then you can use the `setfixed()`

and `setbounds()`

functions to set these before fitting the model.

Let’s start by creating a reference model using a Standard GARCH model with Skewed Student-t Distribution fore residuals and an AR(1) model for the mean.

```
specification <- ugarchspec(
mean.model = list(armaOrder = c(1, 0)),
variance.model = list(model = "sGARCH"),
distribution.model = "sstd"
)
specification
```

```
*---------------------------------*
* GARCH Model Spec *
*---------------------------------*
Conditional Variance Dynamics
------------------------------------
GARCH Model : sGARCH(1,1)
Variance Targeting : FALSE
Conditional Mean Dynamics
------------------------------------
Mean Model : ARFIMA(1,0,0)
Include Mean : TRUE
GARCH-in-Mean : FALSE
Conditional Distribution
------------------------------------
Distribution : sstd
Includes Skew : TRUE
Includes Shape : TRUE
Includes Lambda : FALSE
```

Fit that to the data and check the coefficients.

```
fit <- ugarchfit(data = TATASTEEL, spec = specification)
```

```
Estimate Std. Error t value Pr(>|t|)
mu 0.001 0.001 2.500 0.012
ar1 -0.031 0.025 -1.242 0.214
omega 0.000 0.000 1.471 0.141
alpha1 0.038 0.005 7.270 0.000
beta1 0.951 0.007 134.613 0.000
skew 1.045 0.036 28.858 0.000
shape 5.072 0.657 7.721 0.000
```

Neither of the values for `ar1`

or `omega`

appear to be significantly different to zero.

Let’s test the `setfixed()`

function. Restrict the model by imposing fixed values for `ar1`

and `omega`

.

```
restricted <- specification
setfixed(restricted) <- list(ar1 = 0, omega = 0)
restricted
```

```
*---------------------------------*
* GARCH Model Spec *
*---------------------------------*
Conditional Variance Dynamics
------------------------------------
GARCH Model : sGARCH(1,1)
Variance Targeting : FALSE
Conditional Mean Dynamics
------------------------------------
Mean Model : ARFIMA(1,0,0)
Include Mean : TRUE
GARCH-in-Mean : FALSE
Conditional Distribution
------------------------------------
Distribution : sstd
Includes Skew : TRUE
Includes Shape : TRUE
Includes Lambda : FALSE
```

Fit the restricted model.

```
fit <- ugarchfit(data = TATASTEEL, spec = restricted)
```

```
Estimate Std. Error t value Pr(>|t|)
mu 0.001 0.001 2.242 0.025
ar1 0.000 NA NA NA
omega 0.000 NA NA NA
alpha1 0.030 0.004 7.257 0.000
beta1 0.969 0.004 237.205 0.000
skew 1.041 0.034 30.432 0.000
shape 5.723 0.717 7.978 0.000
```

Note that the values for `ar1`

and `omega`

are those prescribed but that the remaining parameters have been fit to the data. Since these parameters are not being fit to the data we get neither a `\(t\)`

nor `\(p\)`

-value.

Suppose that, rather than specific values, you want to constrain a parameter to values in a particular range.

```
ranges <- specification
setbounds(ranges) <- list(shape = c(10, 100))
```

That range encompasses the value obtained for `shape`

in the reference model.

Estimate the bound constrained model.

```
fit <- ugarchfit(data = TATASTEEL, spec = ranges)
coef(fit)
```

```
NULL
```

Hmmmm. There are no coefficients? What has gone wrong here?

This is a somewhat subtle problem. The values assigned to the model parameters are obtained by numerical optimisation. The optimisation procedure assumes initial values for the model parameters and then iteratively refines those values until it converges on an optimal solution. Where do those initial values come from? They are chosen empirically as “good” (but also random) values. What has happened here is that the initial value for `shape`

lies outside the range specified by `setbounds()`

, so the optimisation algorithm finds that the initial set of parameter values doesn’t satisfy the constraints. And because the initial parameters are invalid it cannot proceed. 💡 The default initial values are generally very good but they can fail when constraints are imposed!

We can fix this by using `setstart()`

to set an initial value for the `shape`

parameter that lies within the permitted range.

```
setstart(ranges) <- list(shape = 50)
```

This can be done for multiple parameters. Now fit the model and check the coefficients.

```
fit <- ugarchfit(data = TATASTEEL, spec = ranges)
coef(fit)
```

```
mu ar1 omega alpha1 beta1
1.475421e-03 -2.510039e-02 7.619083e-06 3.601476e-02 9.483590e-01
skew shape
1.039648e+00 1.000000e+01
```

Aha! Valid parameters. And the value obtained for `shape`

lies within the specified range.

Let’s start by modelling the ACC returns using a GJR GARCH model with a Skewed Student-t Distribution.

```
specification <- ugarchspec(
distribution.model = "sstd",
mean.model = list(armaOrder = c(0, 0)),
variance.model = list(model = "gjrGARCH")
)
fit <- ugarchfit(data = ACC, spec = specification)
coef(fit)
```

```
mu omega alpha1 beta1 gamma1 skew
2.939585e-04 9.298925e-06 7.510285e-03 9.290941e-01 7.535600e-02 1.069799e+00
shape
6.093299e+00
```

The specification of `mean.model`

implies a time-independent average return.

The GARCH-in-mean (GARCH-M) model integrates a risk term into the equation for the average return:

$$ \mu_t = \mu + \lambda\sigma_t. $$

If `\(\lambda > 0\)`

then higher returns are associated with greater risk and *vice versa*. 🚨 We are no longer assuming a constant mean. In this model the mean changes with time.

To convert the reference model above into a GARCH-in-mean model we need to add some parameters to the `mean.model`

argument:

`archm`

— whether to include volatility in the mean; and`archpow`

— the exponent of`\(\sigma_t\)`

.

```
specification <- ugarchspec(
distribution.model = "sstd",
mean.model = list(armaOrder = c(0, 0), archm = TRUE, archpow = 1),
variance.model = list(model = "gjrGARCH")
)
fit <- ugarchfit(data = ACC, spec = specification)
coef(fit)
```

```
mu archm omega alpha1 beta1
-3.240077e-03 2.117885e-01 1.125499e-05 6.353074e-03 9.217580e-01
gamma1 skew shape
7.913028e-02 1.071063e+00 6.019235e+00
```

The value of `\(\lambda\)`

is given by `archm`

.

An alternative formulation of the model sets

$$ \mu_t = \mu + \lambda\sigma_t^2. $$

```
specification <- ugarchspec(
distribution.model = "sstd",
mean.model = list(armaOrder = c(0, 0), archm = TRUE, archpow = 2),
variance.model = list(model = "gjrGARCH")
)
fit <- ugarchfit(data = ACC, spec = specification)
coef(fit)
```

```
mu archm omega alpha1 beta1
-0.0012773434 5.4817434401 0.0000114815 0.0060710444 0.9208525678
gamma1 skew shape
0.0798243808 1.0697491862 6.0407995156
```

The GARCH-in-mean model explicitly creates a dependency between risk and reward. An alternative approach is to use a model based on the correlation between successive returns. The AR(1) model looks like this:

$$ \mu_t = \mu + \rho\mu_{t-1}. $$

The behaviour of this model depends on the sign and magnitude of `\(\rho\)`

:

`\(\rho > 0\)`

— positive autocorrelation; if previous value above (below) long term mean then current value will be above (below) mean too (indicates under-reaction);`\(\rho < 0\)`

— negative autocorrelation; if previous value above (below) long term mean then current value will be below (above) mean (indicates over-reaction);`\(|\rho| < 1\)`

— mean reversion; after a shock the values decay to the mean;`\(|\rho| ~ 1\)`

— momentum; effects of a shock are more persistent.

If AR coefficient is > 0: “MOMENTUM” higher/lower average return followed by higher/lower average return. Market under-reacts, so still reacting next day. If |AR| coefficient is < 1: mean reversion If AR coefficient is < 0: “REVERSION” higher/lower average return followed by lower/higher average return. Market over-reacts, so correcting next day.

Create an AR(1) GJR GARCH model.

```
specification <- ugarchspec(
distribution.model = "sstd",
mean.model = list(armaOrder = c(1, 0)),
variance.model = list(model = "gjrGARCH")
)
fit <- ugarchfit(data = TATASTEEL, spec = specification)
ar1_mean <- fitted(fit)
ar1_vol <- sigma(fit)
coef(fit)
```

```
mu ar1 omega alpha1 beta1
1.310861e-03 -3.064634e-02 7.481843e-06 1.054478e-02 9.516358e-01
gamma1 skew shape
5.655076e-02 1.050852e+00 5.050698e+00
```

The negative value for `ar1`

implies that successive returns are anti-correlated: an above-average return is followed by a below-average return and *vice versa*.

This has positive `ar1`

. What does this mean?

```
fit <- ugarchfit(data = HDFC, spec = specification)
coef(fit)
```

```
mu ar1 omega alpha1 beta1 gamma1
6.018919e-04 1.797143e-02 7.484922e-06 2.516759e-02 9.108610e-01 7.901444e-02
skew shape
1.031812e+00 5.969515e+00
```

This has positive `ar1`

.

```
specification <- ugarchspec(
distribution.model = "sstd",
mean.model = list(armaOrder = c(0, 1)),
variance.model = list(model = "gjrGARCH")
)
fit <- ugarchfit(data = TATASTEEL, spec = specification)
coef(fit)
```

```
mu ma1 omega alpha1 beta1
1.311620e-03 -3.259906e-02 7.480174e-06 1.054758e-02 9.516881e-01
gamma1 skew shape
5.644991e-02 1.050552e+00 5.043684e+00
```

```
specification <- ugarchspec(
distribution.model = "sstd",
mean.model = list(armaOrder = c(1, 1)),
variance.model = list(model = "gjrGARCH")
)
fit <- ugarchfit(data = TATASTEEL, spec = specification)
coef(fit)
```

```
mu ar1 ma1 omega alpha1
1.336495e-03 4.934020e-01 -5.313694e-01 7.294276e-06 1.124307e-02
beta1 gamma1 skew shape
9.519920e-01 5.503656e-02 1.046955e+00 4.989622e+00
```

HOW IS THIS DONE WITH THE TSGARCH PACKAGE?

Let’s try with the `{tsgarch}`

package.

```
library(tsgarch)
```

As we proceeded from a GARCH-in-Mean model to AR(1), MA(1) and ARMA(1, 1) models the number of parameters increased. This means that the models became progressively more complicated (and flexible). In general one should strive for a parsimonius model: just complicated enough to get the job done.

]]>Create a model for the Tata Steel returns.

```
specification <- ugarchspec(
mean.model = list(armaOrder = c(0, 0)),
variance.model = list(model = "gjrGARCH"),
distribution.model = "std"
)
```

```
fit <- ugarchfit(data = TATASTEEL, spec = specification)
```

📢 The standardised residuals should have a mean close to 0 and a standard deviation close to 1.

Calculate the mean and standard deviation of the residuals:

```
mean(residuals(fit, standardize=TRUE))
```

```
[1] 0.01247863
```

```
sd(residuals(fit, standardize=TRUE))
```

```
[1] 0.9924797
```

Those are both close to the target values. Good start!

📢 The standardised returns have uniform variability.

First look at the original returns.

Compare to the returns standardise by the GARCH variability.

The standardised returns still have spikes, but the variability is more consistent and there is less evidence of volatility clustering.

📢 The standardised returns have uniform variability.

Take a look at the correlogram of the absolute returns.

As expected, the autocorrelation is maximum at zero lag. However there is a long tail of significant autocorrelations at greater lags.

Now consider the correlogram of the absolute standardised returns.

The only significant autocorrelation occurs at lag 0, which is precisely what we want: no lagged autocorrelation.

A qualitative comparison is a good start. But we can formalise this using a Ljung-Box test, which is a statistical test to determine whether autocorrelations are significantly different from zero. The null hypothesis for this test is that the data *are not* autocorrelated.

First apply the test to the absolute residuals.

```
Box.test(abs(residuals(fit)), 30, type = "Ljung-Box")
```

```
Box-Ljung test
data: abs(residuals(fit))
X-squared = 245.09, df = 30, p-value < 2.2e-16
```

The tiny `\(p\)`

-value indicates a statistically significant result. We can this reject the null hypothesis and conclude that the data *are* autocorrelated.

Now let’s apply the test to the absolute standardised residuals.

```
Box.test(abs(residuals(fit, standardize=TRUE)), 30, type = "Ljung-Box")
```

```
Box-Ljung test
data: abs(residuals(fit, standardize = TRUE))
X-squared = 18.982, df = 30, p-value = 0.9404
```

Now the `\(p\)`

-value is close to 1 and the result is certainly not statistically significant. We cannot rejected the null hypotheses. So, although we cannot conclusively state that there *is* autocorrelation, it does seem unlikely given the test result.

These are three simple tests that can be applied to a model to assess its validity. All of these tests are, however, based on in sample data. A more definitive result would be based on using the model on unseen (out of sample) data.

]]>We can use a significance test to determine whether the value of each parameter is *significantly* different from its null value. For many parameters the null value will simply be 0 but, as we will see, there are some parameters for which this is not true.

Parameter estimates will invariably be non-zero. This is because those estimates are obtained using numerical techniques. And those techniques might converge on a value of 0.001 or 0.000000857 (or any other really tiny value) for a parameter where the *true* value is *precisely* zero.

We can use a statistical test to determine whether the estimated value is *significantly* different from zero (“significant” in the sense that it is still different from zero taking into account the uncertainty associated with the numerical method).

Let’s start by building a GJR GARCH model for the Tata Steel returns, using a Skewed Student-t Distribution and modelling the mean as an AR(1) process.

```
flexible <- ugarchspec(
mean.model = list(armaOrder = c(1, 0)),
variance.model = list(model = "gjrGARCH"),
distribution.model = "sstd"
)
overfit <- ugarchfit(data = TATASTEEL, spec = flexible)
```

This model has eight parameters. Is that too many?

```
coef(overfit)
```

```
mu ar1 omega alpha1 beta1
1.310861e-03 -3.064634e-02 7.481843e-06 1.054478e-02 9.516358e-01
gamma1 skew shape
5.655076e-02 1.050852e+00 5.050698e+00
```

Looking at the parameter estimates alone is not enough to make a decision on the validity of the parameter estimates. A few of the estimates are pretty close to zero. But are they close enough that they are *effectively* zero? We need more information to make a decision on this point. Specifically, we need to know what the associated uncertainties are because these will provide us with a suitable scale to evaluate “close enough”.

We have to make a relatively obscure incantation to get the information that we need, looking at the `matcoef`

element on the `fit`

slot of the model object. This gives us the parameter estimates, the standard errors, t-statistics and `\(p\)`

-values.

```
round(overfit@fit$matcoef, 3)
```

```
Estimate Std. Error t value Pr(>|t|)
mu 0.001 0.001 2.319 0.020
ar1 -0.031 0.024 -1.258 0.208
omega 0.000 0.000 2.561 0.010
alpha1 0.011 0.003 3.127 0.002
beta1 0.952 0.006 147.758 0.000
gamma1 0.057 0.016 3.566 0.000
skew 1.051 0.037 28.517 0.000
shape 5.051 0.658 7.676 0.000
```

The t-statistic is the ratio of the parameter estimate to the standard error. Larger (absolute) values are more significant. A rule of thumb for assessing the t-statistic:

If the t-statistic is greater than 2 then reject the null hypothesis at the 5% level.

This means that if the parameter estimate differs from the null value by more than two standard errors then it is regarded as significant.

The default null hypothesis is that the parameter is zero (that is, the null value is zero). Most of the parameter estimates are significant. For example, the value for `shape`

seems to be obviously non-zero, and if we consider the associated standard error then we see that it differs from zero by a few standard errors. It’s a safe bet. The value for `alpha1`

is rather small and one might be tempted to think that it’s effectively zero. However, if you take into account the associated standard error then you see that it’s actually a few standard errors away from zero. So it’s also safely significant.

Things are different with the estimate for `ar1`

though. The value is just a little more than one standard error away from zero and so is not statistically significant (it also has a large `\(p\)`

-value, which indicates the same thing).

The estimate for `skew`

appears to be significant, but the t-statistic and associated `\(p\)`

-value are misleading. The null value used by default is 0.

```
(1.050847 - 0) / 0.036851
```

```
[1] 28.51611
```

However, in the case of `skew`

we should be using a null value of 1 (for a symmetric distribution with no skew).

```
(1.050847 - 1) / 0.036851
```

```
[1] 1.3798
```

The estimate for `skew`

does not differ from 1 by more than two standard errors, so this parameter should also be considered insignificant.

An better model might replace the Skewed Student-t Distribution with a plain Student-t Distribution and use a constant mean model. We could, equivalently, have used `setfixed()`

to set the value for `ar1`

to zero, but that would defeat the purpose of using an AR(1) model!

```
specification <- ugarchspec(
mean.model = list(armaOrder = c(0, 0)),
variance.model = list(model = "gjrGARCH"),
distribution.model = "std"
)
```

```
fit <- ugarchfit(data = TATASTEEL, spec = specification)
```

```
Estimate Std. Error t value Pr(>|t|)
mu 0.000932 0.000531 1.755574 0.079161
omega 0.000007 0.000003 2.066620 0.038770
alpha1 0.011020 0.000918 12.001779 0.000000
beta1 0.952105 0.006375 149.347874 0.000000
gamma1 0.056788 0.016077 3.532193 0.000412
shape 5.138337 0.675746 7.603951 0.000000
```

Now all of the model parameters are significant.

Suppose that you’ve created two models for the same data. How do you determine which model is better?

Calculate the mean squared prediction error for the mean.

```
mean(residuals(fit) ** 2)
```

```
[1] 0.0006138637
```

```
mean(residuals(overfit) ** 2)
```

```
[1] 0.0006126068
```

The `overfit`

model looks slightly better. However, this comparison is *deeply* misleading because we are comparing the models on the basis of their performance on the training data and, of course, the more flexible (and overfit) model is going to perform better!

You can also compare models using the model likelihood, a measure of how likely the data are given a specific set of parameters. A higher likelihood suggests that the model is a better description of the data. The likelihood in isolation is not particularly useful (it’s not measured on an absolute scale, so it’s hard to independently identify a “good” likelihood). Normally you’d compare the likelihoods of two or more models.

```
likelihood(fit)
```

```
[1] 3491.23
```

```
likelihood(overfit)
```

```
[1] 3493.11
```

The `overfit`

model has a higher likelihood and so *appears* to be a better representation of the data. However, here we run into the same problem as in the previous comparison: likelihood can be misleading because it is calculated using in-sample data and does not factor in how well the model performs on unseen data. A more flexible (and potentially overfit) model will probably achieve a higher likelihood.

Another approach is to use an information criterion. These criteria balance model complexity (number of model parameters) against how well a model describes the data (the likelihood). There are various such criteria. A lower (more negative) information criterion indicates a better (and more parsimonious) model.

How many parameters does each model have?

```
length(coef(fit))
```

```
[1] 6
```

```
length(coef(overfit))
```

```
[1] 8
```

The `overfit`

model has more parameters. It’s more flexible and thus able to capture more information from the data. However, at the same time, it’s also more prone to overfitting.

```
infocriteria(fit)
```

```
Akaike -4.697076
Bayes -4.675637
Shibata -4.697108
Hannan-Quinn -4.689085
```

```
infocriteria(overfit)
```

```
Akaike -4.696913
Bayes -4.668328
Shibata -4.696971
Hannan-Quinn -4.686258
```

This paints a different picture of the models. Now `fit`

is better because it has lower (more negative) values for all information criteria.

Above, by a rather circuitous path, we determined that a constant mean model is better than an AR(1) model for the Tata Steel data. Could we have arrived at this conclusion via another route? Probably.

Take a look at the autocorrelation and partial autocorrelation for the Tata Steel returns.

The returns data do not have any significant autocorrelation (except at lag 0) or partial autocorrelation. This suggests that a constant mean or white noise model, ARMA(0, 0), would be appropriate.

Somewhat surprisingly this appears to be the case for many of the returns time series. However, there are a few assets that do have significant autocorrelation or partial autocorrelation at non-zero lags. For example, Marico Ltd.

Here we can see (marginally) significant peaks at lags 1 and 2. Now a constant mean model is probably not the right choice.

```
specification <- ugarchspec(
mean.model = list(armaOrder = c(1, 1)),
variance.model = list(model = "sGARCH"),
distribution.model = "sstd"
)
marico <- ugarchfit(data = MARICO, spec = specification)
```

Using the AR(1, 1) above we find significant estimates for both `ar1`

and `ma1`

model parameters.

```
round(marico@fit$matcoef, 3)
```

```
Estimate Std. Error t value Pr(>|t|)
mu 0.001 0.000 1.917 0.055
ar1 0.459 0.124 3.692 0.000
ma1 -0.572 0.114 -5.032 0.000
omega 0.000 0.000 2.921 0.003
alpha1 0.154 0.045 3.432 0.001
beta1 0.586 0.117 4.998 0.000
skew 1.079 0.040 27.048 0.000
shape 4.897 0.614 7.971 0.000
```

Things are not always that simple though. Here are the correlation plots for Titan Company Ltd.

There’s a (marginally) significant peak at lag 1. Now, although a constant mean model results in all model parameters being significant, attempting an AR(1), MA(1) or ARMA(1, 1) model gives significant estimates for the mean model but the `omega`

estimate is no longer significant.

```
specification <- ugarchspec(
mean.model = list(armaOrder = c(1, 0)),
variance.model = list(model = "sGARCH"),
distribution.model = "sstd"
)
titan <- ugarchfit(data = TITAN, spec = specification)
```

```
round(titan@fit$matcoef, 3)
```

```
Estimate Std. Error t value Pr(>|t|)
mu 0.002 0.000 3.582 0.000
ar1 -0.069 0.022 -3.116 0.002
omega 0.000 0.000 1.356 0.175
alpha1 0.033 0.018 1.898 0.058
beta1 0.920 0.048 19.120 0.000
skew 1.078 0.036 29.930 0.000
shape 3.195 0.294 10.864 0.000
```

Once you start fiddling with these thing (and you should), it becomes apparent that it’s a rather deep rabbit hole and it can become somewhat confusing.

Let’s try with the `{tsgarch}`

package.

```
library(tsgarch)
```

```
specification <- garch_modelspec(
TATASTEEL,
model = "gjrgarch",
distribution = "sstd",
constant = TRUE
)
fit <- estimate(specification)
```

You don’t need to work as hard to examine the statistical properties of the parameter estimates: just use the `summary()`

method.

```
summary(fit)
```

```
GJRGARCH Model Summary
Coefficients:
Estimate Std. Error t value Pr(>|t|)
mu 1.278e-03 5.847e-04 2.186 0.02878 *
omega 7.330e-06 3.997e-06 1.834 0.06663 .
alpha1 1.041e-02 8.412e-03 1.238 0.21588
gamma1 5.769e-02 1.818e-02 3.173 0.00151 **
beta1 9.515e-01 1.258e-02 75.618 < 2e-16 ***
skew 1.053e+00 3.720e-02 28.320 < 2e-16 ***
shape 5.131e+00 7.000e-01 7.329 2.31e-13 ***
persistence 9.914e-01 7.185e-03 137.982 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
N: 1484
V(initial): 0.0006137, V(unconditional): 0.0008561
Persistence: 0.9914
LogLik: 3492.21, AIC: -6968.42, BIC: -6926
```

The `AIC()`

and `BIC()`

generic functions yield the Akaike (AIC) and Bayesian (BIC) Information Criteria.

```
AIC(fit)
```

```
[1] -6968.419
```

```
BIC(fit)
```

```
[1] -6925.999
```

Comparing these values to those obtained earlier it’s apparent that these metrics are being normalised differently, underlining the fact that they can only be used for making comparisons.

]]>The reason for this is that a large negative return produces a significant reduction in market value. This, in turn, results in higher leverage. Why? Leverage is the ratio of debt to equity. Presumably a large negative return will not have an immediate impact on debt. But equity (assessed as the market value of all outstanding shares) will drop. Lower equity translates into higher leverage, making the company appear to be more reliant on debt.

To deal with the leverage asymmetry we need to have separate expressions for variance following positive and negative returns.

In a previous post we defined the model variance as

$$
\sigma_t^2 = \omega + \alpha e_{t-1}^2 + \beta \sigma_{t-1}^2.
$$
This will still apply when the residual for the previous time step, `\(e_{t-1}\)`

, is positive. But for negative `\(e_{t-1}\)`

the coefficient for the squared residual has a correction factor, `\(\gamma\)`

:

$$ \sigma_t^2 = \omega + (\alpha + \gamma) e_{t-1}^2 + \beta \sigma_{t-1}^2. $$ The coefficient of the squared residual is

`\(\alpha\)`

when`\(e_{t-1} \geq 0\)`

and`\(\alpha + \gamma\)`

when`\(e_{t-1} < 0\)`

.

This is the GJR (Glosten-Jagannathan-Runkle) GARCH model (original publication can be found here). The GJR GARCH model is based on the Standard GARCH model but also includes the leverage effect. When `\(\gamma = 0\)`

it reduces to the Standard GARCH model.

Start by modelling the ACC returns using a Standard GARCH (`"sGARCH"`

) model with a Normal Distribution.

```
specification <- ugarchspec(
distribution.model = "norm",
mean.model = list(armaOrder = c(0, 0)),
variance.model = list(model = "sGARCH")
)
fit <- ugarchfit(data = ACC, spec = specification)
coef(fit)
```

```
mu omega alpha1 beta1
7.203541e-04 1.741503e-05 6.185551e-02 8.862047e-01
```

The resulting model has fit the parameters `\(\mu\)`

, `\(\omega\)`

, `\(\alpha\)`

and `\(\beta\)`

. The coefficient of the squared residuals in the volatility is 0.0619 for both positive and negative returns.

A news impact curve can be used to illustrate the symmetric response to negative and positive returns.

Now, using the same distribution but changing to a GJR GARCH (`"gjrGARCH"`

) model.

```
specification <- ugarchspec(
distribution.model = "norm",
mean.model = list(armaOrder = c(0, 0)),
variance.model = list(model = "gjrGARCH")
)
fit <- ugarchfit(data = ACC, spec = specification)
coef(fit)
```

```
mu omega alpha1 beta1 gamma1
2.928785e-04 1.148255e-05 1.971500e-06 9.219841e-01 9.466123e-02
```

This model also has a value for `\(\gamma\)`

. The coefficient of the squared residuals is now 0.00000197 for positive residuals and 0.0947 for negative residuals. Negative returns clearly have a *much* larger impact on volatility!

The news impact curve shows the asymmetry. Somewhat unrealistically this implies that positive returns have essentially no impact. That doesn’t seem quite right.

Given the asymmetry of the returns it would make more sense to use a skewed distribution, changing from `"norm"`

to `"sstd"`

.

```
specification <- ugarchspec(
distribution.model = "sstd",
mean.model = list(armaOrder = c(0, 0)),
variance.model = list(model = "gjrGARCH")
)
fit <- ugarchfit(data = ACC, spec = specification)
coef(fit)
```

```
mu omega alpha1 beta1 gamma1 skew
2.939585e-04 9.298925e-06 7.510285e-03 9.290941e-01 7.535600e-02 1.069799e+00
shape
6.093299e+00
```

Now positive returns do have an impact but it’s much smaller than that from negative returns.

Compare the volatility derived from the GJR GARCH model with a skewed distribution (blue) to that of the Standard GARCH model (red) with a Normal Distribution.

Midway through 2018 there’s a large positive return. This has an impact on the Standard GARCH model (red) but very little effect on the GJR GARCH model (blue). Looking at the return time series it’s apparent that there also really isn’t an appreciable increase in volatility at that time. However, in the first quarter of 2020 there’s a large negative return, which is accompanied by a significant escalation in volatility, which is reflected in both the Standard GARCH and GJR GARCH models.

Let’s try with the `{tsgarch}`

package.

```
library(tsgarch)
```

Create a GJR GARCH model specification with a Skewed Student-t Distribution.

```
specification <- garch_modelspec(
ACC,
model = "gjrgarch",
distribution = "sstd",
constant = TRUE
)
```

```
fit <- estimate(specification)
```

Now we have access to the statistical details of the estimated parameters. 💡 Not all of them are significant so we should probably think a bit harder about this model!

```
summary(fit)
```

```
GJRGARCH Model Summary
Coefficients:
Estimate Std. Error t value Pr(>|t|)
mu 2.916e-04 4.345e-04 0.671 0.502213
omega 9.401e-06 3.615e-06 2.600 0.009316 **
alpha1 7.578e-03 9.968e-03 0.760 0.447142
gamma1 7.571e-02 2.155e-02 3.513 0.000444 ***
beta1 9.286e-01 1.984e-02 46.809 < 2e-16 ***
skew 1.070e+00 3.928e-02 27.233 < 2e-16 ***
shape 6.100e+00 9.066e-01 6.729 1.71e-11 ***
persistence 9.750e-01 1.158e-02 84.230 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
N: 1484
V(initial): 0.000339, V(unconditional): 0.0003761
Persistence: 0.975
LogLik: 3952, AIC: -7888, BIC: -7845.58
```

```
plot(fit)
```

It’s often said that

Markets take the stairs up, but the elevator down.

The implication is that markets usually increase in value gradually, like someone climbing up stairs step by step. But when they fall, they tend to do so very quickly, as if taking an elevator down. So, positive returns tend to be smaller than negative returns.

Let’s take a look at the distribution of returns for ACC.

The gray bars are a histogram of the returns. The blue curve is the empirical density, which should be compared with the red curve which is the corresponding Normal Distribution. It’s apparent that the empirical distribution is more peaked around zero and has “fat tails”. It’s also not symmetric around zero.

To cater for an asymmetric distribution of returns we can change the `distribution.model`

parameter from `"norm"`

(for a Normal Distribution) to `"sstd"`

(for a Skewed Student-t Distribution). The skewed distribution has two extra parameters:

`\(\nu\)`

(`shape`

) — determines the “fatness” and`\(\xi\)`

(`skew`

) — determines the “skewness”.

The Normal Distribution is a special case of the Skewed Student-t Distribution with `\(\xi = 1\)`

and `\(\nu = \infty\)`

.

Let’s first model those data using a Normal Distribution for the residuals.

```
specification <- ugarchspec(
distribution.model = "norm",
mean.model = list(armaOrder = c(0, 0)),
variance.model = list(model = "sGARCH")
)
fit <- ugarchfit(data = ACC, spec = specification)
coef(fit)
```

```
mu omega alpha1 beta1
7.203541e-04 1.741503e-05 6.185551e-02 8.862047e-01
```

What’s the model likelihood? 💡 This is a measure of how likely the data are given a specific set of parameters. Bigger is better.

```
likelihood(fit)
```

```
[1] 3884.409
```

Now compare a Skewed Student-t Distribution.

```
specification <- ugarchspec(
distribution.model = "sstd",
mean.model = list(armaOrder = c(0, 0)),
variance.model = list(model = "sGARCH")
)
fit <- ugarchfit(data = ACC, spec = specification)
coef(fit)
```

```
mu omega alpha1 beta1 skew shape
5.672354e-04 1.502448e-05 5.678190e-02 8.973779e-01 1.078959e+00 5.759355e+00
```

The parameters for the original model, `\(\mu\)`

, `\(\omega\)`

, `\(\alpha\)`

and `\(\beta\)`

, are still present. But now we also have `\(\nu\)`

and `\(\xi\)`

. The skewness, `\(\xi\)`

, indicates that the distribution is slightly asymmetric, while the shape, `\(\nu\)`

, suggests that the distribution has fatter tails than a Normal Distribution.

Get the likelihood for comparison.

```
likelihood(fit)
```

```
[1] 3944.611
```

A higher likelihood indicates that this is probably a better description of the data.

Choosing an appropriate distribution for the model residuals can make a big difference to model quality. Since the distribution of returns is generally not symmetric about zero a Skewed Student-t Distribution is often more appropriate than a Normal Distribution.

]]>The name itself feels intimidating (acronyms are a form of gatekeeping, right?), so let’s decompose it:

**Generalized**— It’s an expanded version of earlier models that were more limited in scope.**Autoregressive**— It uses past values to predict future values.**Conditional**— Its assumes that future volatility depends on information from the past.**Heteroskedasticity**— It allows volatility to change over time (volatility is not constant). This means that it can handle periods of high (where prices are volatile) and low (where prices are stable) volatility.

GARCH models are typically used in risk management, portfolio optimisation, and financial decision-making, giving insights into how volatile an asset might be in the future.

A GARCH model effectively has two components:

- a model for the
*average return*and - a model for the
*return standard deviation*(or volatility).

We’ll denote the observed returns by `\(R_t\)`

, where the subscript `\(t\)`

indicates that this is the return for time period `\(t\)`

. The observed returns are described by an *average return* model, `\(\bar{R}\)`

. The difference between the observations and the model is the *model residual* (or *prediction error*), `\(e_t\)`

, which is also time dependent so it gets a `\(t\)`

subscript too.

$$ R_t = \bar{R}_t + e_t. $$

A simple model might suppose that the returns are normally distributed and centred on the *average return*, `\(\mu\)`

. We can then write

$$ R_t = \mu + e_t $$

where the model residual is distributed as

$$ e_t \sim N(0, \sigma^2) $$

and `\(\sigma\)`

is the *return standard deviation*.

This model assumes that the variability in the returns is constant. However, in practice this is a poor assumption. It makes more sense to assume that the prediction errors are normally distributed with a variance that that changes with time, `\(\sigma_t\)`

:

$$ e_t \sim N(0, \sigma_t^2). $$

But now we need a model for `\(\sigma_t\)`

. The normal GARCH model sets

$$ \sigma_t^2 = \omega + \alpha e_{t-1}^2 + \beta \sigma_{t-1}^2. $$

The term with coefficient `\(\alpha\)`

depends on the square of the lagged model residual. The term with coefficient `\(\beta\)`

is the contribution of lagged volatility. This is the model for the *conditional variance*. It’s expected that the conditional variance will converge to a constant level, the *unconditional variance*, given by

$$ \frac{\omega}{1 - \alpha - \beta}. $$

The model has four parameters: `\(\mu\)`

, `\(\omega\)`

, `\(\alpha\)`

and `\(\beta\)`

. The Maximum Likelihood method will be used to estimate the set of parameter values which are most likely to have generated the observed returns.

We’ll use the same data as in the previous post in this series.

The `{rugarch}`

and `{tsgarch}`

packages will be used to build the models. The latter package is a partial rewrite of the former.

```
library(rugarch)
library(tsgarch)
```

First we need to set up the specifications of the model.

```
specification <- ugarchspec(
distribution.model = "norm",
mean.model = list(armaOrder = c(0, 0)),
variance.model = list(model = "sGARCH")
)
```

That specifies a standard GARCH model with constant mean. Dissecting the parameters:

`distribution.model`

— The distribution assumed for the returns, in this case a Normal Distribution. A Normal Distribution has both a mean and a variance. These are determined via the`mean.model`

and`variance.model`

parameters.`mean.model`

— The model used to describe the mean. In this case we have specified an ARMA model with neither an autoregressive (AR) or moving average (MA) component. Since there is neither an autoregressive or moving average component, this model assumes that the mean is constant with time, and doesn’t depend on previous returns or a moving average of previous returns.`variance.model`

— The model used to describe the variance. The “standard” GARCH model (`"sGARCH"`

) has been chosen. This model predicts volatility based on past volatility and past returns. This part of the model is what results in*volatility clustering*: if returns were volatile yesterday then they are likely to also be volatile today.

Next fit the model to the returns for Tata Steel, providing the model specification.

```
fit <- ugarchfit(data = TATASTEEL, spec = specification)
```

The fit object has a number of utility methods which can be used to interrogate the model.

The model is fully specified by its coefficients. As mentioned above, there are four model parameters.

```
coef(fit)
```

```
mu omega alpha1 beta1
1.484725e-03 1.242723e-05 4.152826e-02 9.373508e-01
```

The value of `mu`

is the average return, while `omega`

, `alpha1`

and `beta1`

correspond to `\(\omega\)`

, `\(\alpha\)`

and `\(\beta\)`

in the model above.

The model assumed that the average return is constant and this can be seen in the fitted value for `\(\mu\)`

.

```
fitted(fit)
```

Although the average return is assumed to be constant, the volatility changes with time.

```
sigma(fit)
```

This can be compared with the volatility moving averages from the previous post, but in this case we have been somewhat more sophisticated in actually fitting a model to the volatility. The period of higher volatility in the first half of 2020 corresponds to the large fluctuations that are visible in the returns in the same period. Higher volatility ⇆ more variable returns.

The conditional volatility is mean reverting and returns to the unconditional volatility over the long term.

```
sqrt(uncvariance(fit))
```

```
[1] 0.02425664
```

The unconditional variance is plotted as a dashed blue line above.

One of the primary applications of a model is to make predictions. Using `ugarchforecast()`

we can predict future returns and volatility.

```
forecast <- ugarchforecast(fit, n.ahead = 10)
```

```
*------------------------------------*
* GARCH Model Forecast *
*------------------------------------*
Model: sGARCH
Horizon: 10
Roll Steps: 0
Out of Sample: 0
0-roll forecast [T0=2021-12-31]:
Series Sigma
T+1 0.001485 0.02140
T+2 0.001485 0.02147
T+3 0.001485 0.02153
T+4 0.001485 0.02159
T+5 0.001485 0.02165
T+6 0.001485 0.02171
T+7 0.001485 0.02177
T+8 0.001485 0.02182
T+9 0.001485 0.02188
T+10 0.001485 0.02193
```

The forecast object also has a suite of methods. For example, you can extract just the volatilities.

```
sigma(forecast)
```

```
2021-12-31
T+1 0.02140490
T+2 0.02146904
T+3 0.02153165
T+4 0.02159276
T+5 0.02165241
T+6 0.02171065
T+7 0.02176750
T+8 0.02182301
T+9 0.02187721
T+10 0.02193013
```

Let’s perform a similar analysis using the `{tsgarch}`

package. Again we start with a model specification.

```
specification <- garch_modelspec(
TATASTEEL,
model = "garch",
constant = TRUE
)
```

That specifies a GARCH model with constant mean. Unlike with `{rugarch}`

the returns data are baked into the specification. Now fit the model.

```
fit <- estimate(specification)
```

And take a look at the model summary, which shows the estimates of the model parameters along with an indication of their statistical significance.

```
summary(fit)
```

```
GARCH Model Summary
Coefficients:
Estimate Std. Error t value Pr(>|t|)
mu 1.485e-03 5.957e-04 2.492 0.0127 *
omega 1.250e-05 4.774e-06 2.620 0.0088 **
alpha1 4.151e-02 8.758e-03 4.740 2.14e-06 ***
beta1 9.373e-01 1.388e-02 67.546 < 2e-16 ***
persistence 9.788e-01 8.482e-03 115.398 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
N: 1484
V(initial): 0.0006138, V(unconditional): 0.0005891
Persistence: 0.9788
LogLik: 3439.38, AIC: -6868.76, BIC: -6842.25
```

Plotting the model gives a lot more information than what was available from the `{rugarch}`

model.

```
plot(fit)
```

We have built a couple of GARCH models. We’ll dig into them further in upcoming posts.

]]>The plot below shows the daily returns for Tata Steel. It’s apparent that there is substantial variability in the returns from one day to the next and that there are also periods of higher or lower variability.

```
plot(TATASTEEL)
```

This view of the data is illuminating, but the granularity does obscure the underlying information. Some form or smoothing or averaging would help to make the picture clearer.

The `chart.RollingPerformance()`

function in the `PerformanceAnalytics`

package can be used to plot rolling volatility.

```
chart.RollingPerformance(
R = TATASTEEL,
width = 21,
FUN = "sd.annualized",
scale = 252,
main = "Rolling Annualised Volatility (1 month)"
)
```

This is a lot clearer than the raw time series plot and clearly shows the large peak in volatility in 2020.

Let’s examine the options passed to `chart.RollingPerformance()`

:

`R = TATASTEEL`

— an`xts`

time series of returns`width = 21`

— how many values to include in the rolling window (on average there are 21 trading days in a month)`FUN = "sd.annualized"`

— the`sd.annualized()`

function is used to calculate the annualised volatility`scale = 252`

— the granularity of the time series data, which is daily so that 252 is the number of periods in a year (the average number of trading days in a year).

We can further smooth the data using a three month rolling window by changing `width`

from 21 to 63.

```
chart.RollingPerformance(
R = TATASTEEL,
width = 63,
FUN = "sd.annualized",
main = "Rolling Annualised Volatility (3 month)"
)
```

For comparison, here’s the 3 month rolling annualised volatility for Yes Bank.

The `chart.RollingPerformance()`

function can be used to calculate a variety of rolling metrics. For example, you can also use it for a rolling annualised return. This time we’ll also look at multiple stocks on the same plot.

```
returns <- merge(ACC, YESBANK, TATASTEEL)
```

Annualised return is the default function, so we can omit the `FUN`

argument. The plot below shows the annualised returns for ACC (black), Yes Bank (red) and Tata Steel (blue).

```
chart.RollingPerformance(
R = returns,
width = 66,
colorset = c("black", "red", "blue"),
main = "Rolling Annualised Return (3 month)"
)
```

Of the three stocks Tata Steel shows the best annualised returns in recent years, peaking above 10% in 2021.

The `chart.RollingPerformance()`

function is useful for visualising rolling averages and can apply an arbitrary function to data in the rolling window.

I’m going to be writing a series of posts which will look at some applications of R (and perhaps Python) to financial modelling. We’ll start here by pulling some stock data into R, calculating the daily returns and then looking at correlations and simple volatility estimates.

I was looking for a chunky set of stock data, preferably something that was available at higher than daily time resolution. These data from Kaggle for NIFTY 100 stocks on the Indian Stock Market seemed a decent fit.

📌 Although I wanted intraday data I won’t be using it for the moment and will start by aggregatig the data up to daily resolution.

Data for each of the stocks is in a separate CSV file. Start by iterating over these using `map_dfr()`

and loading them into a single data frame.

```
FILES <- list.files("data", pattern=".csv$", full.names = TRUE)
load <- function(path) {
read_csv(path, show_col_types = FALSE) %>%
mutate(
symbol = sub(".csv$", "", basename(path)),
time = as.POSIXct(date),
date = as.Date(time)
) %>%
group_by(symbol, date) %>%
arrange(time) %>%
# Summarise the daily closing price.
summarise(
close = last(close),
.groups = "drop"
)
}
stocks <- map_dfr(FILES, load)
```

How many records do we have and what does the data look like?

```
nrow(stocks)
```

```
[1] 151932
```

```
head(stocks)
```

```
# A tibble: 6 × 3
symbol date close
<chr> <date> <dbl>
1 ACC 2015-02-02 1515.
2 ACC 2015-02-03 1512.
3 ACC 2015-02-04 1490
4 ACC 2015-02-05 1500.
5 ACC 2015-02-06 1513.
6 ACC 2015-02-09 1504.
```

We could work with them en masse, but it’s going to be easier to split into a list of data frames, one for each stock.

```
stocks <- split(stocks, stocks$symbol)
length(stocks)
```

```
[1] 87
```

```
names(stocks)
```

```
[1] "ACC" "ADANIENT" "ADANIPORTS" "AMBUJACEM" "APOLLOHOSP"
[6] "ASIANPAINT" "AUROPHARMA" "AXISBANK" "BAJAJ-AUTO" "BAJAJFINSV"
[11] "BAJFINANCE" "BANKBARODA" "BERGEPAINT" "BHARTIARTL" "BIOCON"
[16] "BOSCHLTD" "BPCL" "BRITANNIA" "CADILAHC" "CHOLAFIN"
[21] "CIPLA" "COALINDIA" "COLPAL" "DABUR" "DIVISLAB"
[26] "DLF" "DRREDDY" "EICHERMOT" "GAIL" "GODREJCP"
[31] "GRASIM" "HAVELLS" "HCLTECH" "HDFC" "HDFCBANK"
[36] "HEROMOTOCO" "HINDALCO" "HINDPETRO" "HINDUNILVR" "ICICIBANK"
[41] "IGL" "INDUSINDBK" "INDUSTOWER" "INFY" "IOC"
[46] "ITC" "JINDALSTEL" "JSWSTEEL" "JUBLFOOD" "KOTAKBANK"
[51] "LT" "LUPIN" "M_M" "MARICO" "MARUTI"
[56] "MCDOWELL-N" "MUTHOOTFIN" "NAUKRI" "NESTLEIND" "NIFTY50"
[61] "NIFTYBANK" "NMDC" "NTPC" "ONGC" "PEL"
[66] "PIDILITIND" "PIIND" "PNB" "POWERGRID" "RELIANCE"
[71] "SAIL" "SBIN" "SHREECEM" "SIEMENS" "SUNPHARMA"
[76] "TATACONSUM" "TATAMOTORS" "TATASTEEL" "TCS" "TECHM"
[81] "TITAN" "TORNTPHARM" "ULTRACEMCO" "UPL" "VEDL"
[86] "WIPRO" "YESBANK"
```

There are 87 distinct stocks and a number of names there that you might recognise.

Now convert them into individual `xts`

objects and concatenate to form a single `xts`

object with the closing time series for each of the stocks.

```
stocks <- stocks %>%
map(~{
ts <- xts(x = .[["close"]], order.by = .[["date"]])
colnames(ts) <- unique(.$symbol)
ts
})
stocks <- do.call(merge, stocks) %>% na.omit()
stocks <- stocks["2016-01-01/2022-01-01"]
```

We now have a multivariate time series. How many individual series and how many observations?

```
dim(stocks)
```

```
[1] 1485 87
```

So that’s 87 time series and 1485 observations for each of them. Each time series looks like this:

```
head(stocks$TATASTEEL)
```

```
TATASTEEL
2016-01-04 259.00
2016-01-05 272.95
2016-01-06 268.75
2016-01-07 251.00
2016-01-08 254.55
2016-01-11 252.90
```

We can print the closing prices but it’s more useful to have a plot.

For analytical purposes the absolute daily closing price is not as interesting as the daily return, which can be easily calculated but we’ll use `CalculateReturns()`

from `{PerformanceAnalytics}`

for convenience.

```
returns <- CalculateReturns(stocks) %>% na.omit()
```

Plot the corresponding return time series.

We are going to be building time series models for the returns, so it would be prudent to check whether the time series data are stationary.

```
library(tseries)
adf.test(TATASTEEL, alternative = "stationary")
```

```
Augmented Dickey-Fuller Test
data: TATASTEEL
Dickey-Fuller = -10.003, Lag order = 11, p-value = 0.01
alternative hypothesis: stationary
```

For the Augmented Dickey-Fuller (ADF) test the null hypothesis is non-stationarity. Since the outcome of the test is significant we can reject this hypothesis and state that the series is indeed stationary.

```
library(urca)
ur.kpss(TATASTEEL)
```

```
#######################################
# KPSS Unit Root / Cointegration Test #
#######################################
The value of the test statistic is: 0.2072
```

In contrast to the ADF test, the Kwiatkowski-Phillips-Schmidt-Shin (KPSS) test has stationarity as the null hypothesis. Since the result of the test is insignificant we should not contemplate rejected this hypothesis.

The results of both tests suggest that the returns time series is stationary.

There’s a lot of diversity in the return time series across the collection of stocks. To illustrate, here are the series for a collection of them.

ACC has a roughly consistent low level of return variability over the six years. By contrast, Yes Bank has periods of increased variability starting in the latter part of 2018.

It would be useful if we had a number (or numbers) to describe the relative level of variability. Volatility is a statistical measure of the dispersion of returns and is calculated as either the standard deviation or variance of the returns. In general, the higher the volatility, the riskier the security.

```
# Pull out the returns for two specific stocks.
#
ACC <- returns$ACC
YESBANK <- returns$YESBANK
```

Let’s start by calculating the standard deviations of those stocks over the entire data period.

```
# Daily volatility.
#
sd(ACC)
```

```
[1] 0.0184169
```

```
sd(YESBANK)
```

```
[1] 0.04682746
```

As expected the volatility of Yes Bank is higher than that of ACC.

These values can be thought of as the average *daily* variation in the stock’s return. However, what’s more commonly used is the *annualised* value, which takes into account the fact that there are on average 252 trading days in each year and reflects the average variation in the stock’s return over the course of a year.

```
# Annualised volatility.
#
sqrt(252) * sd(ACC)
```

```
[1] 0.2923593
```

```
sqrt(252) * sd(YESBANK)
```

```
[1] 0.7433629
```

It’s instructive to look at the volatility for shorter periods of time. Let’s compare the volatility of those two stocks in 2017 and 2020.

```
sqrt(252) * sd(ACC["2017"])
```

```
[1] 0.2344968
```

```
sqrt(252) * sd(YESBANK["2017"])
```

```
[1] 0.2665718
```

In 2017 the volatilities of ACC and Yes Bank are comparable.

```
sqrt(252) * sd(ACC["2020"])
```

```
[1] 0.3900236
```

```
sqrt(252) * sd(YESBANK["2020"])
```

```
[1] 1.283764
```

However, in 2020 Yes Bank is substantially more volatile than ACC.

Of course you can make these volatility calculations progressively more granular. However, as time intervals get shorter you have less data, so each of the individual measurements becomes less reliable. We’ll be modelling the time variation of volatility in upcoming posts.

The returns might include significant outliers that would likely bias any derived results. We can use winsorisation to reduce the effect of extreme outliers in the data. The `Return.clean()`

function from `{PerformanceAnalytics}`

can be used to apply `clean.boudt()`

to the data.

```
yesbank <- Return.clean(YESBANK, method = "boudt")
```

In the plot below the raw data are in black and the cleaned data are in blue. The extrema are reduced in magnitude in the cleaned data.

This can be appreciated by looking at the range (minimum and maximum values) of the rase and cleaned data.

```
range(YESBANK)
```

```
[1] -0.5537634 0.5964912
```

```
range(yesbank)
```

```
[1] -0.1621622 0.1601474
```

Why is volatility important? It is an fundamental consideration in predicting future returns. Stocks with lower volatility are less likely to have substantial changes in return from day to day. However, stocks with higher volatility are literally more “volatile”: their returns can vary substantially from day to day.

As we have seen, volatility varies from stock to stock (some companies are more volatile than others) and from period to period (a company may have times of low and high volatility).

Volatility needs to be factored into future predictions. Of course, we also don’t know what future volatilities will be, so in addition to predicting future returns we will probably also want to predict future volatilities. We’ll see how this can be done in an upcoming post on GARCH models.

]]>