One of the fundamental assumptions in the classical linear regression model is homoskedasticity, meaning the variance of the error term remains constant over time. In financial data, however, this assumption rarely holds, what is commonly observed instead is heteroskedasticity, where the variance changes over time. To accommodate this condition, the GARCH model offers a more suitable approach than ordinary OLS regression. This concept is often encountered for the first time by students studying time series econometrics.
The GARCH (Generalized Autoregressive Conditional Heteroskedasticity) model describes the variance of the current error term as not being fixed, but instead following an ARMA (Autoregressive Moving Average) process.
\[y_t = x_t + \epsilon_t\]
where \(y_t\) is the stock return, \(x_t\) is the mean-reverting process, and \(\epsilon_t\) is the error term. The error term is defined as \(\epsilon_t = \sqrt{\delta_t^2}\, z_t\), where \(z_t \sim D(0,1)\) and \(D\) is a specified distribution. The variance \(\delta_t^2\) is time-varying and follows an ARMA process as follows:
\[\delta_t^2 = \omega + \alpha_1 \epsilon_{t-1}^2 + \dots + \alpha_q \epsilon_{t-q}^2 + \beta_1 \delta_{t-1}^2 + \dots + \beta_p \delta_{t-p}^2\]
I use TLKM (Telkom Indonesia) stock data. It is downloaded through the quantmod package.
library(quantmod)
tlkm <- getSymbols("TLKM.JK", from="2005-09-14", to="2026-06-17", auto.assign = F)
head(tlkm)
## TLKM.JK.Open TLKM.JK.High TLKM.JK.Low TLKM.JK.Close TLKM.JK.Volume TLKM.JK.Adjusted
## 2005-09-14 1040 1050 1010 1010 160237500 430.3688
## 2005-09-15 1010 1030 990 1000 123292500 426.1077
## 2005-09-16 1010 1030 1010 1030 74040000 438.8909
## 2005-09-19 1030 1060 1030 1060 127490000 451.6741
## 2005-09-20 1050 1050 1010 1040 110680000 443.1520
## 2005-09-21 1030 1030 1020 1020 118792500 434.6299
tail(tlkm)
## TLKM.JK.Open TLKM.JK.High TLKM.JK.Low TLKM.JK.Close TLKM.JK.Volume TLKM.JK.Adjusted
## 2026-06-09 2510 2620 2510 2620 371055600 2620
## 2026-06-10 2500 2810 2500 2810 391732600 2810
## 2026-06-11 2870 2970 2810 2870 274793100 2870
## 2026-06-12 2910 2980 2860 2860 181531900 2860
## 2026-06-15 2900 2970 2890 2930 156789000 2930
## 2026-06-16 2700 2710 2580 NA 374544500 NA
chartSeries(tlkm, theme = "white", name = "TLKM Stock Price")
daily_ret <- (tlkm$TLKM.JK.Close - stats::lag(tlkm$TLKM.JK.Close)) / stats::lag(tlkm$TLKM.JK.Close)
Convert the data into a data frame and rename the rows and columns.
daily_ret <- data.frame(index(daily_ret), daily_ret)
colnames(daily_ret) <- c("date", "return")
daily_ret <- na.omit(daily_ret)
rownames(daily_ret) <- 1:nrow(daily_ret)
library(ggplot2)
p1 <- ggplot(daily_ret, aes(x=date, y=return))
p1 + geom_line(colour="steelblue") +
labs(title = "TLKM - Daily Return", x = "Date", y = "Return") +
theme_minimal()
As shown in the plot, the stock returns appear to be non-stationary. Although the mean is approximately zero, the volatility exhibits random fluctuations. In the next step, a histogram of the returns will be presented and compared with the normal distribution.
p2 <- ggplot(daily_ret)
p2 + geom_histogram(aes(x=return, y=..density..), binwidth = 0.005, color="steelblue", fill="grey", size=1) +
stat_function(fun = dnorm, args = list(mean = mean(daily_ret$return, na.rm = T), sd = sd(daily_ret$return, na.rm = T)), size=1) +
labs(title = "TLKM - Distribution of Daily Return", x = "Return", y = "Density") +
theme_minimal()
1. Skewness The histogram suggests that the distribution of TLKM returns is not perfectly symmetric. Visually, the peak of the histogram is located slightly to the left, while the right tail appears somewhat longer, indicating a slight positive (right) skewness. However, the deviation from the normal distribution is not particularly pronounced. Positive skewness implies that TLKM returns tend to generate positive outcomes on average, although the degree of skewness remains relatively low.
2. Kurtosis The plot indicates positive kurtosis relative to the normal distribution. This is evident from the histogram’s significantly higher and sharper peak (reaching a density of approximately 32) compared with the theoretical normal curve, whose peak is only around a density of 20. In addition, the histogram exhibits heavier tails on both sides than the normal distribution. Kurtosis can be interpreted as a measure of risk. A high kurtosis value is associated with greater risk because it indicates a higher probability of extreme returns, whether exceptionally large gains or losses (heavy-tailed or fat-tailed behavior). Conversely, a low kurtosis value suggests a more moderate level of risk, as the likelihood of extreme returns is relatively lower.
library(moments)
ret_clean <- as.numeric(na.omit(daily_ret$return))
skewness(ret_clean)
## [1] 0.2976702
kurtosis(ret_clean)
## [1] 7.821611
jarque.test(ret_clean)
##
## Jarque-Bera Normality Test
##
## data: ret_clean
## JB = 5017.5, p-value < 2.2e-16
## alternative hypothesis: greater
Hypotheses
H₀: The return data are normally distributed.
H₁: The return data are not normally distributed.
Decision The Jarque–Bera (JB) statistic is 5017.5 with a p-value < 2.2 × 10⁻¹⁶. Since the p-value is smaller than the significance level (α = 0.05), H₀ is rejected.
Conclusion The return data are not normally distributed at a statistically significant level. This result is supported by the positive skewness value (0.298) and the high kurtosis value (7.82), indicating that the distribution exhibits fat tails (heavy tails) compared with a normal distribution. As a result, extreme returns are more likely to occur than would be expected under normality.
Convert the data frame back to an xts object, and then apply the rollapply() function to compute rolling volatility. For the monthly rolling volatility analysis, a window of 20 trading days is used to represent one month.
library(PerformanceAnalytics)
library(xts)
daily_ret_xts <- xts(daily_ret[,-1], order.by=daily_ret[,1])
realizedvol <- rollapply(daily_ret_xts, width = 20, FUN=sd.annualized)
Convert back the xts data to dataframe.
vol <- data.frame(index(realizedvol), realizedvol)
colnames(vol) <- c("date", "volatility")
Plot the monthly volatility.
p3 <- ggplot(vol, aes(x=date, y=volatility))
p3 +
geom_line( color="steelblue")
We can see that most of the time the volatility is around 0.25, while
there are several times the volatility becomes very high — particularly
around 2008-2009, 2011, 2020, and toward the end of the data period
(2025-2026), with the highest spike reaching close to 1.00.
cutoff_date <- as.Date("2026-04-28")
n_train <- sum(daily_ret$date <= cutoff_date)
n_test <- 14
n_total <- n_train + n_test
train_ret <- daily_ret$return[1:n_train]
test_ret <- daily_ret$return[(n_train+1):n_total]
library(tseries)
library(forecast)
# ADF test
adf_result <- adf.test(train_ret)
print(adf_result)
##
## Augmented Dickey-Fuller Test
##
## data: train_ret
## Dickey-Fuller = -17.635, Lag order = 17, p-value = 0.01
## alternative hypothesis: stationary
Defining the model specification using the ugarchspec()
function.
variance_models <- c("sGARCH", "eGARCH", "gjrGARCH")
garch_orders <- list(c(1,1), c(1,2), c(2,1))
arma_orders <- list(c(0,0), c(2,2))
distributions <- c("norm", "std", "sstd")
model_grid <- expand.grid(
variance.model = variance_models,
garch_p = sapply(garch_orders, `[`, 1),
garch_q = sapply(garch_orders, `[`, 2),
arma_p = sapply(arma_orders, `[`, 1),
arma_q = sapply(arma_orders, `[`, 2),
distribution = distributions,
stringsAsFactors = FALSE
)
model_grid <- unique(model_grid)
cat("Number of Candidate Model Combinations:", nrow(model_grid), "\n")
## Number of Candidate Model Combinations: 144
head(model_grid, 10)
## variance.model garch_p garch_q arma_p arma_q distribution
## 1 sGARCH 1 1 0 0 norm
## 2 eGARCH 1 1 0 0 norm
## 3 gjrGARCH 1 1 0 0 norm
## 7 sGARCH 2 1 0 0 norm
## 8 eGARCH 2 1 0 0 norm
## 9 gjrGARCH 2 1 0 0 norm
## 10 sGARCH 1 2 0 0 norm
## 11 eGARCH 1 2 0 0 norm
## 12 gjrGARCH 1 2 0 0 norm
## 16 sGARCH 2 2 0 0 norm
library(rugarch)
results_list <- list()
for (i in 1:nrow(model_grid)) {
row <- model_grid[i, ]
if (any(is.na(row))) next
spec <- tryCatch({
ugarchspec(
variance.model = list(model = row$variance.model, garchOrder = c(row$garch_p, row$garch_q)),
mean.model = list(armaOrder = c(row$arma_p, row$arma_q)),
distribution.model = row$distribution
)
}, error = function(e) NULL)
if (is.null(spec)) next
fit <- tryCatch({
ugarchfit(spec = spec, data = train_ret, solver = "hybrid")
}, error = function(e) NULL)
if (is.null(fit)) next
if (fit@fit$convergence != 0) next
ic <- infocriteria(fit)
coef_table <- fit@fit$matcoef
pvalues <- coef_table[, "Pr(>|t|)"]
all_significant <- all(pvalues < 0.05, na.rm = TRUE)
n_insignificant <- sum(pvalues >= 0.05, na.rm = TRUE)
resid_std <- residuals(fit, standardize = TRUE)
lb_resid <- Box.test(resid_std, lag = 10, type = "Ljung-Box")
lb_resid_sq <- Box.test(resid_std^2, lag = 10, type = "Ljung-Box")
results_list[[i]] <- data.frame(
model_id = i,
variance.model = row$variance.model,
garch_order = paste0("(", row$garch_p, ",", row$garch_q, ")"),
arma_order = paste0("(", row$arma_p, ",", row$arma_q, ")"),
distribution = row$distribution,
AIC = as.numeric(ic[1]),
BIC = as.numeric(ic[2]),
all_param_significant = all_significant,
n_insignificant_param = n_insignificant,
lb_resid_pvalue = lb_resid$p.value,
lb_resid_sq_pvalue = lb_resid_sq$p.value,
converged = TRUE
)
}
cat("Total Models Successfully Fitted:", sum(!sapply(results_list, is.null)), "from", nrow(model_grid), "\n")
## Total Models Successfully Fitted: 144 from 144
results_df <- do.call(rbind, results_list)
results_df <- results_df[order(results_df$AIC), ]
rownames(results_df) <- 1:nrow(results_df)
print(results_df)
## model_id variance.model garch_order arma_order distribution AIC BIC all_param_significant
## 1 141 gjrGARCH (1,2) (2,2) sstd -5.233594 -5.218141 FALSE
## 2 93 gjrGARCH (1,2) (2,2) std -5.233156 -5.218990 FALSE
## 3 138 gjrGARCH (2,1) (2,2) sstd -5.233017 -5.216277 FALSE
## 4 137 eGARCH (2,1) (2,2) sstd -5.232979 -5.216238 FALSE
## 5 144 gjrGARCH (2,2) (2,2) sstd -5.232949 -5.214921 FALSE
## 6 143 eGARCH (2,2) (2,2) sstd -5.232646 -5.214618 FALSE
## 7 139 sGARCH (1,2) (2,2) sstd -5.232552 -5.218386 FALSE
## 8 135 gjrGARCH (1,1) (2,2) sstd -5.232512 -5.218346 FALSE
## 9 90 gjrGARCH (2,1) (2,2) std -5.232507 -5.217054 FALSE
## 10 96 gjrGARCH (2,2) (2,2) std -5.232480 -5.215739 FALSE
## 11 89 eGARCH (2,1) (2,2) std -5.232437 -5.216984 FALSE
## 12 142 sGARCH (2,2) (2,2) sstd -5.232157 -5.216704 FALSE
## 13 140 eGARCH (1,2) (2,2) sstd -5.232124 -5.216671 FALSE
## 14 95 eGARCH (2,2) (2,2) std -5.232103 -5.215363 FALSE
## 15 87 gjrGARCH (1,1) (2,2) std -5.232100 -5.219223 FALSE
## 16 91 sGARCH (1,2) (2,2) std -5.232099 -5.219221 FALSE
## 17 129 gjrGARCH (1,2) (0,2) sstd -5.231793 -5.218916 FALSE
## 18 94 sGARCH (2,2) (2,2) std -5.231705 -5.217539 FALSE
## 19 92 eGARCH (1,2) (2,2) std -5.231608 -5.217443 FALSE
## 20 125 eGARCH (2,1) (0,2) sstd -5.231528 -5.217362 FALSE
## 21 133 sGARCH (1,1) (2,2) sstd -5.231468 -5.218590 FALSE
## 22 81 gjrGARCH (1,2) (0,2) std -5.231313 -5.219723 FALSE
## 23 126 gjrGARCH (2,1) (0,2) sstd -5.231207 -5.217042 FALSE
## 24 131 eGARCH (2,2) (0,2) sstd -5.231194 -5.215741 FALSE
## 25 132 gjrGARCH (2,2) (0,2) sstd -5.231164 -5.215711 FALSE
## 26 136 sGARCH (2,1) (2,2) sstd -5.231073 -5.216908 FALSE
## 27 85 sGARCH (1,1) (2,2) std -5.231029 -5.219440 FALSE
## 28 77 eGARCH (2,1) (0,2) std -5.230948 -5.218070 FALSE
## 29 127 sGARCH (1,2) (0,2) sstd -5.230826 -5.219237 TRUE
## 30 134 eGARCH (1,1) (2,2) sstd -5.230734 -5.216569 FALSE
## 31 84 gjrGARCH (2,2) (0,2) std -5.230638 -5.216473 FALSE
## 32 88 sGARCH (2,1) (2,2) std -5.230635 -5.217757 FALSE
## 33 83 eGARCH (2,2) (0,2) std -5.230612 -5.216447 FALSE
## 34 78 gjrGARCH (2,1) (0,2) std -5.230602 -5.217725 FALSE
## 35 123 gjrGARCH (1,1) (0,2) sstd -5.230585 -5.218995 FALSE
## 36 128 eGARCH (1,2) (0,2) sstd -5.230529 -5.217651 FALSE
## 37 130 sGARCH (2,2) (0,2) sstd -5.230432 -5.217555 FALSE
## 38 86 eGARCH (1,1) (2,2) std -5.230270 -5.217392 FALSE
## 39 79 sGARCH (1,2) (0,2) std -5.230266 -5.219964 FALSE
## 40 75 gjrGARCH (1,1) (0,2) std -5.230137 -5.219834 FALSE
## 41 80 eGARCH (1,2) (0,2) std -5.229970 -5.218380 FALSE
## 42 82 sGARCH (2,2) (0,2) std -5.229872 -5.218282 FALSE
## 43 117 gjrGARCH (1,2) (2,0) sstd -5.229721 -5.216843 FALSE
## 44 121 sGARCH (1,1) (0,2) sstd -5.229623 -5.219321 TRUE
## 45 113 eGARCH (2,1) (2,0) sstd -5.229539 -5.215374 FALSE
## 46 69 gjrGARCH (1,2) (2,0) std -5.229240 -5.217650 FALSE
## 47 124 sGARCH (2,1) (0,2) sstd -5.229229 -5.217639 FALSE
## 48 119 eGARCH (2,2) (2,0) sstd -5.229203 -5.213750 FALSE
## 49 114 gjrGARCH (2,1) (2,0) sstd -5.229111 -5.214945 FALSE
## 50 120 gjrGARCH (2,2) (2,0) sstd -5.229089 -5.213636 FALSE
## 51 73 sGARCH (1,1) (0,2) std -5.229077 -5.220063 FALSE
## 52 65 eGARCH (2,1) (2,0) std -5.228974 -5.216096 FALSE
## 53 122 eGARCH (1,1) (0,2) sstd -5.228933 -5.217343 TRUE
## 54 115 sGARCH (1,2) (2,0) sstd -5.228784 -5.217194 TRUE
## 55 76 sGARCH (2,1) (0,2) std -5.228683 -5.218381 FALSE
## 56 71 eGARCH (2,2) (2,0) std -5.228636 -5.214471 FALSE
## 57 72 gjrGARCH (2,2) (2,0) std -5.228559 -5.214394 FALSE
## 58 116 eGARCH (1,2) (2,0) sstd -5.228524 -5.215646 FALSE
## 59 66 gjrGARCH (2,1) (2,0) std -5.228506 -5.215629 FALSE
## 60 111 gjrGARCH (1,1) (2,0) sstd -5.228497 -5.216907 FALSE
## 61 74 eGARCH (1,1) (0,2) std -5.228425 -5.218123 FALSE
## 62 118 sGARCH (2,2) (2,0) sstd -5.228390 -5.215512 FALSE
## 63 67 sGARCH (1,2) (2,0) std -5.228175 -5.217873 FALSE
## 64 63 gjrGARCH (1,1) (2,0) std -5.228050 -5.217748 FALSE
## 65 68 eGARCH (1,2) (2,0) std -5.227975 -5.216385 FALSE
## 66 70 sGARCH (2,2) (2,0) std -5.227781 -5.216191 FALSE
## 67 109 sGARCH (1,1) (2,0) sstd -5.227566 -5.217264 TRUE
## 68 112 sGARCH (2,1) (2,0) sstd -5.227172 -5.215582 FALSE
## 69 61 sGARCH (1,1) (2,0) std -5.226972 -5.217958 FALSE
## 70 110 eGARCH (1,1) (2,0) sstd -5.226896 -5.215307 FALSE
## 71 64 sGARCH (2,1) (2,0) std -5.226577 -5.216275 FALSE
## 72 62 eGARCH (1,1) (2,0) std -5.226400 -5.216098 FALSE
## 73 105 gjrGARCH (1,2) (0,0) sstd -5.216616 -5.206314 FALSE
## 74 57 gjrGARCH (1,2) (0,0) std -5.216299 -5.207285 FALSE
## 75 102 gjrGARCH (2,1) (0,0) sstd -5.215990 -5.204401 FALSE
## 76 108 gjrGARCH (2,2) (0,0) sstd -5.215944 -5.203066 FALSE
## 77 101 eGARCH (2,1) (0,0) sstd -5.215924 -5.204334 FALSE
## 78 103 sGARCH (1,2) (0,0) sstd -5.215725 -5.206711 FALSE
## 79 54 gjrGARCH (2,1) (0,0) std -5.215636 -5.205334 FALSE
## 80 99 gjrGARCH (1,1) (0,0) sstd -5.215620 -5.206606 FALSE
## 81 107 eGARCH (2,2) (0,0) sstd -5.215596 -5.202719 FALSE
## 82 60 gjrGARCH (2,2) (0,0) std -5.215585 -5.203996 FALSE
## 83 53 eGARCH (2,1) (0,0) std -5.215490 -5.205188 FALSE
## 84 51 gjrGARCH (1,1) (0,0) std -5.215333 -5.207607 FALSE
## 85 106 sGARCH (2,2) (0,0) sstd -5.215331 -5.205029 FALSE
## 86 59 eGARCH (2,2) (0,0) std -5.215162 -5.203572 FALSE
## 87 55 sGARCH (1,2) (0,0) std -5.215129 -5.207403 FALSE
## 88 104 eGARCH (1,2) (0,0) sstd -5.214950 -5.204648 FALSE
## 89 58 sGARCH (2,2) (0,0) std -5.214735 -5.205721 FALSE
## 90 97 sGARCH (1,1) (0,0) sstd -5.214722 -5.206996 FALSE
## 91 56 eGARCH (1,2) (0,0) std -5.214518 -5.205503 FALSE
## 92 100 sGARCH (2,1) (0,0) sstd -5.214388 -5.205374 FALSE
## 93 49 sGARCH (1,1) (0,0) std -5.214130 -5.207691 FALSE
## 94 52 sGARCH (2,1) (0,0) std -5.213799 -5.206072 FALSE
## 95 98 eGARCH (1,1) (0,0) sstd -5.213563 -5.204549 FALSE
## 96 50 eGARCH (1,1) (0,0) std -5.213199 -5.205472 FALSE
## 97 45 gjrGARCH (1,2) (2,2) norm -5.163641 -5.150764 FALSE
## 98 48 gjrGARCH (2,2) (2,2) norm -5.162991 -5.147538 FALSE
## 99 42 gjrGARCH (2,1) (2,2) norm -5.162164 -5.147999 FALSE
## 100 43 sGARCH (1,2) (2,2) norm -5.161251 -5.149661 FALSE
## 101 39 gjrGARCH (1,1) (2,2) norm -5.161191 -5.149601 FALSE
## 102 46 sGARCH (2,2) (2,2) norm -5.160857 -5.147979 FALSE
## 103 33 gjrGARCH (1,2) (0,2) norm -5.159744 -5.149442 FALSE
## 104 41 eGARCH (2,1) (2,2) norm -5.159267 -5.145102 FALSE
## 105 36 gjrGARCH (2,2) (0,2) norm -5.159031 -5.146153 FALSE
## 106 47 eGARCH (2,2) (2,2) norm -5.158880 -5.143426 FALSE
## 107 37 sGARCH (1,1) (2,2) norm -5.158368 -5.148066 FALSE
## 108 40 sGARCH (2,1) (2,2) norm -5.157973 -5.146383 FALSE
## 109 30 gjrGARCH (2,1) (0,2) norm -5.157844 -5.146254 FALSE
## 110 44 eGARCH (1,2) (2,2) norm -5.157568 -5.144690 TRUE
## 111 21 gjrGARCH (1,2) (2,0) norm -5.157312 -5.147010 FALSE
## 112 27 gjrGARCH (1,1) (0,2) norm -5.157198 -5.148184 FALSE
## 113 31 sGARCH (1,2) (0,2) norm -5.156939 -5.147925 TRUE
## 114 24 gjrGARCH (2,2) (2,0) norm -5.156580 -5.143702 FALSE
## 115 34 sGARCH (2,2) (0,2) norm -5.156545 -5.146243 FALSE
## 116 29 eGARCH (2,1) (0,2) norm -5.156410 -5.144820 FALSE
## 117 35 eGARCH (2,2) (0,2) norm -5.156060 -5.143182 FALSE
## 118 18 gjrGARCH (2,1) (2,0) norm -5.155349 -5.143759 FALSE
## 119 15 gjrGARCH (1,1) (2,0) norm -5.154831 -5.145816 FALSE
## 120 32 eGARCH (1,2) (0,2) norm -5.154511 -5.144209 TRUE
## 121 19 sGARCH (1,2) (2,0) norm -5.154344 -5.145329 TRUE
## 122 38 eGARCH (1,1) (2,2) norm -5.154316 -5.142726 FALSE
## 123 17 eGARCH (2,1) (2,0) norm -5.154304 -5.142714 FALSE
## 124 23 eGARCH (2,2) (2,0) norm -5.153969 -5.141092 FALSE
## 125 22 sGARCH (2,2) (2,0) norm -5.153949 -5.143647 FALSE
## 126 25 sGARCH (1,1) (0,2) norm -5.153911 -5.146184 TRUE
## 127 28 sGARCH (2,1) (0,2) norm -5.153516 -5.144502 FALSE
## 128 20 eGARCH (1,2) (2,0) norm -5.152365 -5.142063 TRUE
## 129 13 sGARCH (1,1) (2,0) norm -5.151368 -5.143641 TRUE
## 130 16 sGARCH (2,1) (2,0) norm -5.150973 -5.141959 FALSE
## 131 26 eGARCH (1,1) (0,2) norm -5.150890 -5.141876 FALSE
## 132 14 eGARCH (1,1) (2,0) norm -5.148764 -5.139750 TRUE
## 133 9 gjrGARCH (1,2) (0,0) norm -5.146408 -5.138682 FALSE
## 134 12 gjrGARCH (2,2) (0,0) norm -5.145664 -5.135362 FALSE
## 135 6 gjrGARCH (2,1) (0,0) norm -5.144813 -5.135799 FALSE
## 136 3 gjrGARCH (1,1) (0,0) norm -5.144458 -5.138019 FALSE
## 137 7 sGARCH (1,2) (0,0) norm -5.143296 -5.136857 TRUE
## 138 5 eGARCH (2,1) (0,0) norm -5.142929 -5.133915 FALSE
## 139 10 sGARCH (2,2) (0,0) norm -5.142901 -5.135175 FALSE
## 140 11 eGARCH (2,2) (0,0) norm -5.142535 -5.132233 FALSE
## 141 1 sGARCH (1,1) (0,0) norm -5.140871 -5.135720 TRUE
## 142 8 eGARCH (1,2) (0,0) norm -5.140813 -5.133087 TRUE
## 143 4 sGARCH (2,1) (0,0) norm -5.140529 -5.134091 FALSE
## 144 2 eGARCH (1,1) (0,0) norm -5.137800 -5.131361 TRUE
## n_insignificant_param lb_resid_pvalue lb_resid_sq_pvalue converged
## 1 2 5.185710e-01 0.0075345795 TRUE
## 2 3 5.134986e-01 0.0072251224 TRUE
## 3 3 4.842929e-01 0.0060239125 TRUE
## 4 4 5.815320e-01 0.0139502267 TRUE
## 5 4 5.038223e-01 0.0083694040 TRUE
## 6 5 5.837130e-01 0.0136096779 TRUE
## 7 2 4.822864e-01 0.0093578863 TRUE
## 8 2 5.085510e-01 0.0024048812 TRUE
## 9 4 5.033615e-01 0.0054914421 TRUE
## 10 5 5.086769e-01 0.0078586832 TRUE
## 11 4 5.901616e-01 0.0125303153 TRUE
## 12 5 4.823968e-01 0.0093561510 TRUE
## 13 2 5.268222e-01 0.0106393659 TRUE
## 14 2 5.921218e-01 0.0122005019 TRUE
## 15 3 5.017636e-01 0.0023252933 TRUE
## 16 3 4.615978e-01 0.0090530844 TRUE
## 17 1 1.201188e-02 0.0119685484 TRUE
## 18 6 4.615403e-01 0.0090555815 TRUE
## 19 2 5.252602e-01 0.0095539494 TRUE
## 20 3 2.625604e-02 0.0242012934 TRUE
## 21 2 4.701595e-01 0.0034386209 TRUE
## 22 1 1.232016e-02 0.0114709968 TRUE
## 23 2 8.916161e-03 0.0106107345 TRUE
## 24 3 2.635439e-02 0.0237325904 TRUE
## 25 3 1.070087e-02 0.0137158125 TRUE
## 26 3 4.704001e-01 0.0034389348 TRUE
## 27 3 4.488587e-01 0.0033492858 TRUE
## 28 3 2.752070e-02 0.0232849477 TRUE
## 29 0 8.710254e-03 0.0142018877 TRUE
## 30 3 4.908686e-01 0.0023013422 TRUE
## 31 3 1.152272e-02 0.0127594779 TRUE
## 32 4 4.488440e-01 0.0033490441 TRUE
## 33 5 2.776056e-02 0.0228257892 TRUE
## 34 2 1.005200e-02 0.0095690996 TRUE
## 35 1 1.082066e-02 0.0040804343 TRUE
## 36 1 1.796570e-02 0.0177840830 TRUE
## 37 2 8.716667e-03 0.0142020254 TRUE
## 38 2 4.879056e-01 0.0020782503 TRUE
## 39 1 7.914752e-03 0.0138959408 TRUE
## 40 1 1.102588e-02 0.0039594251 TRUE
## 41 1 1.852208e-02 0.0171805913 TRUE
## 42 3 7.931926e-03 0.0138964345 TRUE
## 43 1 1.827764e-03 0.0120337055 TRUE
## 44 0 7.770401e-03 0.0056477672 TRUE
## 45 3 3.758730e-03 0.0248868165 TRUE
## 46 1 1.975946e-03 0.0115385308 TRUE
## 47 1 7.769421e-03 0.0056479351 TRUE
## 48 2 3.765558e-03 0.0244007330 TRUE
## 49 2 1.297959e-03 0.0108687128 TRUE
## 50 3 1.582128e-03 0.0137406945 TRUE
## 51 1 7.067687e-03 0.0056033631 TRUE
## 52 3 4.409548e-03 0.0243085985 TRUE
## 53 0 1.393400e-02 0.0039496567 TRUE
## 54 0 1.215013e-03 0.0145655708 TRUE
## 55 2 7.059732e-03 0.0055965554 TRUE
## 56 5 4.413783e-03 0.0238164746 TRUE
## 57 3 1.821432e-03 0.0127692725 TRUE
## 58 1 2.700078e-03 0.0180288513 TRUE
## 59 2 1.595349e-03 0.0097859968 TRUE
## 60 1 1.770037e-03 0.0043418949 TRUE
## 61 1 1.432482e-02 0.0037803549 TRUE
## 62 2 1.214463e-03 0.0145632328 TRUE
## 63 1 1.151411e-03 0.0143045693 TRUE
## 64 1 1.916829e-03 0.0041848908 TRUE
## 65 1 3.091936e-03 0.0177107124 TRUE
## 66 3 1.151384e-03 0.0143045838 TRUE
## 67 0 1.171393e-03 0.0061419212 TRUE
## 68 1 1.171845e-03 0.0061421311 TRUE
## 69 1 1.116181e-03 0.0061185333 TRUE
## 70 1 2.289113e-03 0.0040912759 TRUE
## 71 2 1.111361e-03 0.0061217696 TRUE
## 72 1 2.638733e-03 0.0039662494 TRUE
## 73 1 1.994693e-07 0.0110115669 TRUE
## 74 1 2.107461e-07 0.0105025675 TRUE
## 75 2 2.901608e-07 0.0092400211 TRUE
## 76 3 2.050935e-07 0.0115821535 TRUE
## 77 3 1.962498e-07 0.0131294286 TRUE
## 78 1 2.087515e-07 0.0152584166 TRUE
## 79 2 3.005526e-07 0.0085138165 TRUE
## 80 1 3.990480e-07 0.0049322990 TRUE
## 81 5 2.086215e-07 0.0129452856 TRUE
## 82 3 2.144920e-07 0.0108544113 TRUE
## 83 3 2.097661e-07 0.0129453670 TRUE
## 84 1 4.165371e-07 0.0047002622 TRUE
## 85 3 2.087904e-07 0.0152555470 TRUE
## 86 5 2.224962e-07 0.0127244384 TRUE
## 87 1 2.041680e-07 0.0152238717 TRUE
## 88 1 2.536697e-07 0.0101411456 TRUE
## 89 3 2.042940e-07 0.0152240481 TRUE
## 90 1 4.184510e-07 0.0075746407 TRUE
## 91 1 2.732633e-07 0.0098004733 TRUE
## 92 2 4.247831e-07 0.0075847619 TRUE
## 93 1 4.059275e-07 0.0076126657 TRUE
## 94 2 4.126640e-07 0.0076237897 TRUE
## 95 1 6.319910e-07 0.0028276197 TRUE
## 96 1 6.602119e-07 0.0025859683 TRUE
## 97 3 5.706352e-01 0.0074863365 TRUE
## 98 6 5.558027e-01 0.0077722916 TRUE
## 99 4 5.230327e-01 0.0050799780 TRUE
## 100 3 4.388787e-01 0.0089885351 TRUE
## 101 3 6.231636e-01 0.0037418447 TRUE
## 102 5 4.389765e-01 0.0089896951 TRUE
## 103 1 2.263937e-02 0.0135386990 TRUE
## 104 1 6.493449e-01 0.0007791645 TRUE
## 105 3 2.164340e-02 0.0141697341 TRUE
## 106 1 6.514592e-01 0.0007952378 TRUE
## 107 2 4.801186e-01 0.0050587137 TRUE
## 108 3 4.786646e-01 0.0050688641 TRUE
## 109 2 2.070475e-02 0.0102465874 TRUE
## 110 0 5.436601e-01 0.0015326693 TRUE
## 111 1 5.663182e-03 0.0132296262 TRUE
## 112 1 2.564831e-02 0.0069122155 TRUE
## 113 0 1.360048e-02 0.0159087392 TRUE
## 114 3 5.519364e-03 0.0136931320 TRUE
## 115 1 1.359215e-02 0.0159107734 TRUE
## 116 1 4.321274e-02 0.0025349008 TRUE
## 117 2 4.259404e-02 0.0024976388 TRUE
## 118 2 5.728801e-03 0.0098443535 TRUE
## 119 1 6.554377e-03 0.0068433973 TRUE
## 120 0 2.806472e-02 0.0040255014 TRUE
## 121 0 3.679077e-03 0.0159095359 TRUE
## 122 1 4.671744e-01 0.0007642640 TRUE
## 123 1 8.704112e-03 0.0027303277 TRUE
## 124 2 8.368315e-03 0.0026715175 TRUE
## 125 1 3.680324e-03 0.0159045419 TRUE
## 126 0 1.526652e-02 0.0097000126 TRUE
## 127 1 1.526964e-02 0.0096993772 TRUE
## 128 0 5.918144e-03 0.0039813122 TRUE
## 129 0 4.347386e-03 0.0098803706 TRUE
## 130 1 4.348894e-03 0.0098797116 TRUE
## 131 1 2.875954e-02 0.0015747197 TRUE
## 132 0 6.495442e-03 0.0014544257 TRUE
## 133 1 1.002229e-07 0.0116060700 TRUE
## 134 4 9.966958e-08 0.0116502701 TRUE
## 135 2 1.665095e-07 0.0086122003 TRUE
## 136 1 2.085025e-07 0.0068441502 TRUE
## 137 0 1.109425e-07 0.0166513944 TRUE
## 138 2 5.391542e-08 0.0011399199 TRUE
## 139 3 1.109653e-07 0.0166537748 TRUE
## 140 2 5.505417e-08 0.0011408835 TRUE
## 141 0 2.589079e-07 0.0109256715 TRUE
## 142 0 7.581678e-08 0.0017539141 TRUE
## 143 1 2.628736e-07 0.0109469107 TRUE
## 144 0 1.973116e-07 0.0007830113 TRUE
best_candidates <- results_df[
results_df$all_param_significant == TRUE &
results_df$lb_resid_pvalue > 0.05,
]
best_candidates <- best_candidates[order(best_candidates$AIC), ]
print(best_candidates)
## model_id variance.model garch_order arma_order distribution AIC BIC all_param_significant
## 110 44 eGARCH (1,2) (2,2) norm -5.157568 -5.14469 TRUE
## n_insignificant_param lb_resid_pvalue lb_resid_sq_pvalue converged
## 110 0 0.5436601 0.001532669 TRUE
best_model_row <- best_candidates[1, ]
cat("\nSelected Best Model:\n")
##
## Selected Best Model:
print(best_model_row)
## model_id variance.model garch_order arma_order distribution AIC BIC all_param_significant
## 110 44 eGARCH (1,2) (2,2) norm -5.157568 -5.14469 TRUE
## n_insignificant_param lb_resid_pvalue lb_resid_sq_pvalue converged
## 110 0 0.5436601 0.001532669 TRUE
###3.4 Refitting the Best Model (Full Details)
final_spec <- ugarchspec(
variance.model = list(model = best_model_row$variance.model,
garchOrder = as.numeric(unlist(strsplit(gsub("[()]", "", best_model_row$garch_order), ",")))),
mean.model = list(armaOrder = as.numeric(unlist(strsplit(gsub("[()]", "", best_model_row$arma_order), ",")))),
distribution.model = best_model_row$distribution
)
final_fit <- ugarchfit(spec = final_spec, data = train_ret, solver = "hybrid")
show(final_fit)
##
## *---------------------------------*
## * GARCH Model Fit *
## *---------------------------------*
##
## Conditional Variance Dynamics
## -----------------------------------
## GARCH Model : eGARCH(1,2)
## Mean Model : ARFIMA(2,0,2)
## Distribution : norm
##
## Optimal Parameters
## ------------------------------------
## Estimate Std. Error t value Pr(>|t|)
## mu 0.000371 0.000134 2.7756 0.005511
## ar1 0.098613 0.022398 4.4028 0.000011
## ar2 0.333692 0.046200 7.2228 0.000000
## ma1 -0.159584 0.018729 -8.5208 0.000000
## ma2 -0.444073 0.044835 -9.9047 0.000000
## omega -0.573180 0.153480 -3.7346 0.000188
## alpha1 -0.032360 0.010050 -3.2198 0.001283
## beta1 0.550916 0.008307 66.3182 0.000000
## beta2 0.375396 0.008189 45.8435 0.000000
## gamma1 0.277307 0.017319 16.0118 0.000000
##
## Robust Standard Errors:
## Estimate Std. Error t value Pr(>|t|)
## mu 0.000371 0.000139 2.6643 0.007715
## ar1 0.098613 0.017394 5.6694 0.000000
## ar2 0.333692 0.029340 11.3734 0.000000
## ma1 -0.159584 0.018698 -8.5348 0.000000
## ma2 -0.444073 0.027835 -15.9540 0.000000
## omega -0.573180 0.496563 -1.1543 0.248379
## alpha1 -0.032360 0.020909 -1.5476 0.121709
## beta1 0.550916 0.031473 17.5044 0.000000
## beta2 0.375396 0.031041 12.0936 0.000000
## gamma1 0.277307 0.084169 3.2946 0.000985
##
## LogLikelihood : 13089.59
##
## Information Criteria
## ------------------------------------
##
## Akaike -5.1576
## Bayes -5.1447
## Shibata -5.1576
## Hannan-Quinn -5.1531
##
## Weighted Ljung-Box Test on Standardized Residuals
## ------------------------------------
## statistic p-value
## Lag[1] 3.613 0.05733
## Lag[2*(p+q)+(p+q)-1][11] 6.914 0.06975
## Lag[4*(p+q)+(p+q)-1][19] 9.972 0.47290
## d.o.f=4
## H0 : No serial correlation
##
## Weighted Ljung-Box Test on Standardized Squared Residuals
## ------------------------------------
## statistic p-value
## Lag[1] 2.657 0.1031265
## Lag[2*(p+q)+(p+q)-1][8] 17.281 0.0005887
## Lag[4*(p+q)+(p+q)-1][14] 22.409 0.0005909
## d.o.f=3
##
## Weighted ARCH LM Tests
## ------------------------------------
## Statistic Shape Scale P-Value
## ARCH Lag[4] 20.21 0.500 2.000 6.929e-06
## ARCH Lag[6] 21.23 1.461 1.711 1.530e-05
## ARCH Lag[8] 23.10 2.368 1.583 1.626e-05
##
## Nyblom stability test
## ------------------------------------
## Joint Statistic: 1.4872
## Individual Statistics:
## mu 0.13207
## ar1 0.11225
## ar2 0.24090
## ma1 0.12390
## ma2 0.23862
## omega 0.15622
## alpha1 0.03131
## beta1 0.14231
## beta2 0.14340
## gamma1 0.12528
##
## Asymptotic Critical Values (10% 5% 1%)
## Joint Statistic: 2.29 2.54 3.05
## Individual Statistic: 0.35 0.47 0.75
##
## Sign Bias Test
## ------------------------------------
## t-value prob sig
## Sign Bias 0.1406 0.8882
## Negative Sign Bias 0.9073 0.3643
## Positive Sign Bias 0.4560 0.6484
## Joint Effect 1.3182 0.7248
##
##
## Adjusted Pearson Goodness-of-Fit Test:
## ------------------------------------
## group statistic p-value(g-1)
## 1 20 155.3 2.069e-23
## 2 30 180.6 7.731e-24
## 3 40 192.6 3.285e-22
## 4 50 216.6 6.074e-23
##
##
## Elapsed time : 0.5615649
roll_forecast <- numeric(n_test)
roll_vol <- numeric(n_test)
for (i in 1:n_test) {
current_train <- daily_ret$return[1:(n_train + i - 1)]
fit_i <- ugarchfit(spec = final_spec, data = current_train, solver = "hybrid")
fc_i <- ugarchforecast(fit_i, n.ahead = 1)
roll_forecast[i] <- as.numeric(fitted(fc_i))
roll_vol[i] <- as.numeric(sigma(fc_i))
}
roll_forecast_df <- data.frame(
date = daily_ret$date[(n_train+1):n_total],
actual = as.numeric(test_ret),
forecast_return = roll_forecast,
forecast_volatility = roll_vol
)
print(roll_forecast_df)
## date actual forecast_return forecast_volatility
## 1 2026-04-29 0.017730496 4.651142e-03 0.02149271
## 2 2026-04-30 -0.020905923 2.997116e-03 0.02078090
## 3 2026-05-05 -0.003460208 2.068923e-03 0.02248247
## 4 2026-05-06 0.006944444 4.368123e-03 0.02015160
## 5 2026-05-07 0.010344828 1.770465e-03 0.01910518
## 6 2026-05-08 0.010238908 1.040182e-03 0.01852027
## 7 2026-05-11 0.000000000 -5.934344e-04 0.01794982
## 8 2026-05-12 -0.003378378 -5.410031e-04 0.01646965
## 9 2026-05-13 0.003389831 6.922314e-05 0.01587411
## 10 2026-05-14 0.000000000 1.481725e-04 0.01504411
## 11 2026-05-15 0.000000000 -1.078672e-04 0.01405658
## 12 2026-05-18 0.040540541 2.605497e-04 0.01325645
## 13 2026-05-19 0.000000000 -2.257561e-03 0.01821965
## 14 2026-05-20 0.006493506 -4.475730e-03 0.01487671
roll_forecast_df$realized_vol <- abs(roll_forecast_df$actual)
print(roll_forecast_df)
## date actual forecast_return forecast_volatility realized_vol
## 1 2026-04-29 0.017730496 4.651142e-03 0.02149271 0.017730496
## 2 2026-04-30 -0.020905923 2.997116e-03 0.02078090 0.020905923
## 3 2026-05-05 -0.003460208 2.068923e-03 0.02248247 0.003460208
## 4 2026-05-06 0.006944444 4.368123e-03 0.02015160 0.006944444
## 5 2026-05-07 0.010344828 1.770465e-03 0.01910518 0.010344828
## 6 2026-05-08 0.010238908 1.040182e-03 0.01852027 0.010238908
## 7 2026-05-11 0.000000000 -5.934344e-04 0.01794982 0.000000000
## 8 2026-05-12 -0.003378378 -5.410031e-04 0.01646965 0.003378378
## 9 2026-05-13 0.003389831 6.922314e-05 0.01587411 0.003389831
## 10 2026-05-14 0.000000000 1.481725e-04 0.01504411 0.000000000
## 11 2026-05-15 0.000000000 -1.078672e-04 0.01405658 0.000000000
## 12 2026-05-18 0.040540541 2.605497e-04 0.01325645 0.040540541
## 13 2026-05-19 0.000000000 -2.257561e-03 0.01821965 0.000000000
## 14 2026-05-20 0.006493506 -4.475730e-03 0.01487671 0.006493506
ggplot(roll_forecast_df, aes(x = date)) +
geom_line(aes(y = realized_vol, color = "Realized Volatility"), size = 1) +
geom_line(aes(y = forecast_volatility, color = "Forecast Volatility"), size = 1, linetype = "dashed") +
labs(title = "TLKM - Forecast vs Realized Volatility",
x = "Date", y = "Volatility", color = "") +
theme_minimal()
library(Metrics)
mae_vol <- mae(roll_forecast_df$realized_vol, roll_forecast_df$forecast_volatility)
rmse_vol <- rmse(roll_forecast_df$realized_vol, roll_forecast_df$forecast_volatility)
smape_vol <- smape(roll_forecast_df$realized_vol, roll_forecast_df$forecast_volatility) * 100
accuracy_summary <- data.frame(
Metric = c("MAE", "RMSE", "SMAPE (%)"),
Value = c(mae_vol, rmse_vol, smape_vol)
)
print(accuracy_summary)
## Metric Value
## 1 MAE 0.01283367
## 2 RMSE 0.01443154
## 3 SMAPE (%) 115.88702542