1. GARCH Introduction

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\]

2. Data Exploration

2.1 Import Data

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

2.2 Plot the data

chartSeries(tlkm, theme = "white", name = "TLKM Stock Price")

2.3 Calculate Daily Return

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) 

2.4 Plot the Return

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.

2.5 Calculate the Monthly Volatility

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.

2.6 In-Sample - Out-Sample Split

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]

2.7 Stationarity Testing

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

3. Fit a GARCH Model

3.1 Candidate Model Specifications

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

3.2 Fitting All Candidate Models in a Loop

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

3.3 Best Model Selection

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

4. Forecast Volatility

4.1 Rolling 1-Step-Ahead Forecast

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

4.2 Forecast vs Realized Volatility

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()

5. Forecast Accuracy Evaluation

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