library(tidyverse)
library(quantmod)
library(tidyquant)
library(forecast)
library(tseries)
library(scales)
library(lubridate)
library(moments)
library(FinTS)
library(rugarch)
library(zoo)
tickers <- c("MSFT", "JPM", "XOM", "JNJ")
precios <- purrr::map_dfr(tickers, function(tk) {
datos <- quantmod::getSymbols(
Symbols = tk,
src = "yahoo",
from = "2020-01-01",
to = Sys.Date(),
auto.assign = FALSE
)
tibble(
fecha = as.Date(zoo::index(datos)),
activo = tk,
precio = as.numeric(quantmod::Ad(datos))
)
})
retornos <- precios %>%
group_by(activo) %>%
arrange(fecha) %>%
mutate(
retorno_log = 100 * (log(precio) - log(lag(precio)))
) %>%
ungroup() %>%
drop_na()
ggplot(retornos, aes(x = fecha, y = retorno_log, color = activo)) +
geom_line(linewidth = 0.5, show.legend = FALSE) +
geom_hline(yintercept = 0, linetype = "dashed") +
facet_wrap(~ activo, scales = "free_y") +
labs(
title = "Retornos logarítmicos diarios",
subtitle = "Retornos expresados en porcentaje",
x = NULL,
y = "Retorno diario (%)"
) +
theme_minimal()
retornos %>%
group_by(activo) %>%
summarise(
media = mean(retorno_log),
desviacion = sd(retorno_log),
minimo = min(retorno_log),
maximo = max(retorno_log),
asimetria = moments::skewness(retorno_log),
curtosis = moments::kurtosis(retorno_log)
) %>%
knitr::kable(digits = 4, caption = "Estadísticas descriptivas de retornos (%)")
| activo | media | desviacion | minimo | maximo | asimetria | curtosis |
|---|---|---|---|---|---|---|
| JNJ | 0.0393 | 1.2272 | -7.8953 | 7.6940 | 0.0727 | 10.8521 |
| JPM | 0.0571 | 1.9501 | -16.2106 | 16.5620 | -0.0718 | 15.3513 |
| MSFT | 0.0630 | 1.8725 | -15.9453 | 13.2929 | -0.2569 | 10.7314 |
| XOM | 0.0700 | 2.0583 | -13.0391 | 11.9442 | -0.2335 | 7.7575 |
retornos %>%
ggplot(aes(x = retorno_log, fill = activo)) +
geom_histogram(bins = 60, color = "white", alpha = 0.8) +
facet_wrap(~ activo, scales = "free_x") +
labs(
title = "Distribución de retornos diarios",
subtitle = "Histograma de retornos logarítmicos por activo",
x = "Retorno diario (%)",
y = "Frecuencia"
) +
theme_minimal() +
theme(legend.position = "none")
Los cuatro activos presentan curtosis superior a 3, indicando colas más pesadas que la distribución normal. XOM muestra la mayor volatilidad, consistente con su exposición al precio del petróleo, mientras que JNJ presenta la menor volatilidad, acorde con su perfil defensivo. La asimetría negativa en algunos activos sugiere que los movimientos negativos extremos son más frecuentes que los positivos.
purrr::walk(tickers, function(tk) {
retornos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
p1 <- forecast::ggAcf(retornos_tk$retorno_log, lag.max = 30) +
labs(
title = paste("ACF de retornos —", tk),
subtitle = "Autocorrelación en la media",
x = "Rezago",
y = "Autocorrelación"
) +
theme_minimal()
p2 <- forecast::ggAcf(retornos_tk$retorno_log^2, lag.max = 30) +
labs(
title = paste("ACF de retornos al cuadrado —", tk),
subtitle = "Memoria en la volatilidad",
x = "Rezago",
y = "Autocorrelación"
) +
theme_minimal()
print(p1)
print(p2)
})
La ACF de los retornos muestra poca autocorrelación en la media, lo que es consistente con la hipótesis de mercado eficiente. Sin embargo, la ACF de los retornos al cuadrado revela autocorrelaciones significativas y persistentes en todos los activos, especialmente en MSFT y JPM. Esto indica que aunque la dirección del retorno es difícil de predecir, la volatilidad sí tiene memoria, lo que justifica el uso de modelos GARCH.
resultados_arch <- purrr::map_dfr(tickers, function(tk) {
retornos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
prueba <- FinTS::ArchTest(retornos_tk$retorno_log, lags = 12)
tibble(
activo = tk,
estadistico = round(as.numeric(prueba$statistic), 4),
p_valor = round(prueba$p.value, 6),
conclusion = ifelse(prueba$p.value < 0.05,
"Rechaza H0: hay efectos ARCH",
"No rechaza H0")
)
})
resultados_arch %>%
knitr::kable(caption = "Prueba ARCH (lags = 12) por activo")
| activo | estadistico | p_valor | conclusion |
|---|---|---|---|
| MSFT | 382.8431 | 0 | Rechaza H0: hay efectos ARCH |
| JPM | 449.8147 | 0 | Rechaza H0: hay efectos ARCH |
| XOM | 341.1210 | 0 | Rechaza H0: hay efectos ARCH |
| JNJ | 555.7687 | 0 | Rechaza H0: hay efectos ARCH |
La prueba ARCH rechaza la hipótesis nula de no efectos ARCH en todos los activos (p-valor < 0.05). Esto confirma que la volatilidad no es constante y que su nivel actual depende de movimientos pasados, validando el uso de modelos GARCH para modelar la volatilidad condicional.
spec_norm <- ugarchspec(
variance.model = list(
model = "sGARCH",
garchOrder = c(1, 1)
),
mean.model = list(
armaOrder = c(0, 0),
include.mean = TRUE
),
distribution.model = "norm"
)
modelos_norm <- purrr::map(tickers, function(tk) {
retornos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
ugarchfit(spec = spec_norm, data = retornos_tk$retorno_log)
})
names(modelos_norm) <- tickers
purrr::map_dfr(tickers, function(tk) {
modelo <- modelos_norm[[tk]]
cf <- coef(modelo)
tibble(
activo = tk,
mu = round(cf["mu"], 4),
omega = round(cf["omega"], 4),
alpha1 = round(cf["alpha1"], 4),
beta1 = round(cf["beta1"], 4),
persistencia = round(cf["alpha1"] + cf["beta1"], 4),
vol_LP = round(sqrt(cf["omega"] /
(1 - cf["alpha1"] - cf["beta1"])), 4)
)
}) %>%
knitr::kable(caption = "Parámetros GARCH(1,1) normal por activo")
| activo | mu | omega | alpha1 | beta1 | persistencia | vol_LP |
|---|---|---|---|---|---|---|
| MSFT | 0.0774 | 0.1557 | 0.1093 | 0.8453 | 0.9546 | 1.8513 |
| JPM | 0.1160 | 0.3298 | 0.1537 | 0.7413 | 0.8950 | 1.7725 |
| XOM | 0.0629 | 0.0451 | 0.0633 | 0.9260 | 0.9893 | 2.0518 |
| JNJ | 0.0271 | 0.2373 | 0.1139 | 0.7005 | 0.8144 | 1.1305 |
purrr::walk(tickers, function(tk) {
retornos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
sigma_tk <- as.numeric(sigma(modelos_norm[[tk]]))
df_plot <- retornos_tk %>% mutate(sigma_norm = sigma_tk)
p <- ggplot(df_plot, aes(x = fecha, y = sigma_norm)) +
geom_line(linewidth = 0.7) +
labs(
title = paste("Volatilidad condicional —", tk),
subtitle = "Modelo GARCH(1,1) con errores normales",
x = NULL,
y = "Volatilidad diaria (%)"
) +
theme_minimal()
print(p)
})
spec_std <- ugarchspec(
variance.model = list(
model = "sGARCH",
garchOrder = c(1, 1)
),
mean.model = list(
armaOrder = c(0, 0),
include.mean = TRUE
),
distribution.model = "std"
)
modelos_std <- purrr::map(tickers, function(tk) {
retornos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
ugarchfit(spec = spec_std, data = retornos_tk$retorno_log)
})
names(modelos_std) <- tickers
purrr::map_dfr(tickers, function(tk) {
modelo <- modelos_std[[tk]]
cf <- coef(modelo)
tibble(
activo = tk,
mu = round(cf["mu"], 4),
omega = round(cf["omega"], 4),
alpha1 = round(cf["alpha1"], 4),
beta1 = round(cf["beta1"], 4),
persistencia = round(cf["alpha1"] + cf["beta1"], 4),
shape = round(cf["shape"], 4)
)
}) %>%
knitr::kable(caption = "Parámetros GARCH(1,1) t-Student por activo")
| activo | mu | omega | alpha1 | beta1 | persistencia | shape |
|---|---|---|---|---|---|---|
| MSFT | 0.1143 | 0.1194 | 0.1038 | 0.8650 | 0.9688 | 5.0780 |
| JPM | 0.1291 | 0.2206 | 0.1609 | 0.7760 | 0.9370 | 4.9267 |
| XOM | 0.0817 | 0.0464 | 0.0592 | 0.9294 | 0.9886 | 8.4823 |
| JNJ | 0.0381 | 0.1451 | 0.0937 | 0.7947 | 0.8884 | 4.7583 |
purrr::walk(tickers, function(tk) {
retornos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
sigma_tk <- as.numeric(sigma(modelos_std[[tk]]))
df_plot <- retornos_tk %>% mutate(sigma_std = sigma_tk)
p <- ggplot(df_plot, aes(x = fecha, y = sigma_std)) +
geom_line(linewidth = 0.7) +
labs(
title = paste("Volatilidad condicional —", tk),
subtitle = "Modelo GARCH(1,1) con errores t-Student",
x = NULL,
y = "Volatilidad diaria (%)"
) +
theme_minimal()
print(p)
})
extraer_ic <- function(modelo, nombre) {
tryCatch({
ic <- infocriteria(modelo)
tibble(
modelo = nombre,
AIC = round(ic[1], 4),
BIC = round(ic[2], 4),
Hannan_Quinn = round(ic[4], 4)
)
}, error = function(e) {
tibble(modelo = nombre, AIC = NA, BIC = NA, Hannan_Quinn = NA)
})
}
purrr::map_dfr(tickers, function(tk) {
bind_rows(
extraer_ic(modelos_norm[[tk]], paste(tk, "— Normal")),
extraer_ic(modelos_std[[tk]], paste(tk, "— t-Student"))
)
}) %>%
knitr::kable(caption = "Comparación Normal vs t-Student por activo")
| modelo | AIC | BIC | Hannan_Quinn |
|---|---|---|---|
| MSFT — Normal | 3.8794 | 3.8928 | 3.8843 |
| MSFT — t-Student | 3.8039 | 3.8207 | 3.8102 |
| JPM — Normal | 3.8319 | 3.8453 | 3.8369 |
| JPM — t-Student | 3.7212 | 3.7380 | 3.7275 |
| XOM — Normal | 4.0457 | 4.0591 | 4.0507 |
| XOM — t-Student | 4.0221 | 4.0389 | 4.0283 |
| JNJ — Normal | 3.0329 | 3.0463 | 3.0379 |
| JNJ — t-Student | 2.9280 | 2.9447 | 2.9342 |
spec_gjr <- ugarchspec(
variance.model = list(model = "gjrGARCH", garchOrder = c(1, 1)),
mean.model = list(armaOrder = c(0, 0), include.mean = TRUE),
distribution.model = "std"
)
spec_egarch <- ugarchspec(
variance.model = list(model = "eGARCH", garchOrder = c(1, 1)),
mean.model = list(armaOrder = c(0, 0), include.mean = TRUE),
distribution.model = "std"
)
modelos_gjr <- purrr::map(tickers, function(tk) {
retornos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
tryCatch(
ugarchfit(spec = spec_gjr, data = retornos_tk$retorno_log),
error = function(e) NULL
)
})
names(modelos_gjr) <- tickers
modelos_egarch <- purrr::map(tickers, function(tk) {
retornos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
tryCatch(
ugarchfit(spec = spec_egarch, data = retornos_tk$retorno_log),
error = function(e) NULL
)
})
names(modelos_egarch) <- tickers
purrr::map_dfr(tickers, function(tk) {
bind_rows(
extraer_ic(modelos_norm[[tk]], paste(tk, "— sGARCH Normal")),
extraer_ic(modelos_std[[tk]], paste(tk, "— sGARCH t-Student")),
extraer_ic(modelos_gjr[[tk]], paste(tk, "— GJR-GARCH t-Student")),
extraer_ic(modelos_egarch[[tk]], paste(tk, "— EGARCH t-Student"))
)
}) %>%
knitr::kable(caption = "Criterios de información por modelo y activo")
| modelo | AIC | BIC | Hannan_Quinn |
|---|---|---|---|
| MSFT — sGARCH Normal | 3.8794 | 3.8928 | 3.8843 |
| MSFT — sGARCH t-Student | 3.8039 | 3.8207 | 3.8102 |
| MSFT — GJR-GARCH t-Student | 3.8001 | 3.8202 | 3.8075 |
| MSFT — EGARCH t-Student | 3.7928 | 3.8130 | 3.8003 |
| JPM — sGARCH Normal | 3.8319 | 3.8453 | 3.8369 |
| JPM — sGARCH t-Student | 3.7212 | 3.7380 | 3.7275 |
| JPM — GJR-GARCH t-Student | 3.7123 | 3.7325 | 3.7198 |
| JPM — EGARCH t-Student | 3.7042 | 3.7243 | 3.7116 |
| XOM — sGARCH Normal | 4.0457 | 4.0591 | 4.0507 |
| XOM — sGARCH t-Student | 4.0221 | 4.0389 | 4.0283 |
| XOM — GJR-GARCH t-Student | 4.0228 | 4.0430 | 4.0303 |
| XOM — EGARCH t-Student | 4.0239 | 4.0440 | 4.0314 |
| JNJ — sGARCH Normal | 3.0329 | 3.0463 | 3.0379 |
| JNJ — sGARCH t-Student | 2.9280 | 2.9447 | 2.9342 |
| JNJ — GJR-GARCH t-Student | 2.9289 | 2.9490 | 2.9364 |
| JNJ — EGARCH t-Student | 2.9330 | 2.9531 | 2.9404 |
purrr::walk(tickers, function(tk) {
retornos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
sigma_seguro <- function(modelo, n) {
tryCatch(as.numeric(sigma(modelo)), error = function(e) rep(NA, n))
}
n <- nrow(retornos_tk)
df_plot <- retornos_tk %>%
mutate(
sGARCH_norm = sigma_seguro(modelos_norm[[tk]], n),
sGARCH_std = sigma_seguro(modelos_std[[tk]], n),
GJR_std = sigma_seguro(modelos_gjr[[tk]], n),
EGARCH_std = sigma_seguro(modelos_egarch[[tk]], n)
) %>%
pivot_longer(
cols = c(sGARCH_norm, sGARCH_std, GJR_std, EGARCH_std),
names_to = "modelo",
values_to = "sigma"
) %>%
filter(!is.na(sigma))
p <- ggplot(df_plot, aes(x = fecha, y = sigma, linetype = modelo)) +
geom_line(linewidth = 0.6) +
labs(
title = paste("Volatilidad condicional por modelo —", tk),
subtitle = "Comparación sGARCH, GJR-GARCH y EGARCH",
x = NULL,
y = "Volatilidad diaria (%)",
linetype = "Modelo"
) +
theme_minimal()
print(p)
})
purrr::walk(tickers, function(tk) {
cat("\n", rep("=", 40), "\n")
cat("Diagnóstico de residuos —", tk, "\n")
cat(rep("=", 40), "\n")
forecast::checkresiduals(as.numeric(residuals(modelos_std[[tk]])))
})
##
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## Diagnóstico de residuos — MSFT
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
##
## Ljung-Box test
##
## data: Residuals
## Q* = 112.01, df = 10, p-value < 2.2e-16
##
## Model df: 0. Total lags used: 10
##
##
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## Diagnóstico de residuos — JPM
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
##
## Ljung-Box test
##
## data: Residuals
## Q* = 123.44, df = 10, p-value < 2.2e-16
##
## Model df: 0. Total lags used: 10
##
##
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## Diagnóstico de residuos — XOM
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
##
## Ljung-Box test
##
## data: Residuals
## Q* = 29.889, df = 10, p-value = 0.0008933
##
## Model df: 0. Total lags used: 10
##
##
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## Diagnóstico de residuos — JNJ
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
##
## Ljung-Box test
##
## data: Residuals
## Q* = 66.374, df = 10, p-value = 2.211e-10
##
## Model df: 0. Total lags used: 10
purrr::map_dfr(tickers, function(tk) {
modelo <- modelos_std[[tk]]
resid_std <- as.numeric(residuals(modelo, standardize = TRUE))
prueba <- FinTS::ArchTest(resid_std, lags = 12)
tibble(
activo = tk,
estadistico = round(as.numeric(prueba$statistic), 4),
p_valor = round(prueba$p.value, 4),
conclusion = ifelse(prueba$p.value < 0.05,
"Aún hay efectos ARCH",
"Sin efectos ARCH residuales")
)
}) %>%
knitr::kable(caption = "Prueba ARCH sobre residuos estandarizados — GARCH t-Student")
| activo | estadistico | p_valor | conclusion |
|---|---|---|---|
| MSFT | 7.2343 | 0.8418 | Sin efectos ARCH residuales |
| JPM | 4.5448 | 0.9715 | Sin efectos ARCH residuales |
| XOM | 17.2682 | 0.1398 | Sin efectos ARCH residuales |
| JNJ | 4.5037 | 0.9725 | Sin efectos ARCH residuales |
Si los p-valores de la prueba ARCH sobre residuos estandarizados son superiores a 0.05, el modelo capturó satisfactoriamente la estructura de volatilidad. En caso contrario, se podría considerar extender a modelos asimétricos o aumentar el orden del GARCH. La curtosis elevada en los retornos justifica el uso de la distribución t-Student frente a la normal.
pronosticos_vol <- purrr::map(tickers, function(tk) {
ugarchforecast(fitORspec = modelos_std[[tk]], n.ahead = 20)
})
names(pronosticos_vol) <- tickers
purrr::walk(tickers, function(tk) {
pronostico <- pronosticos_vol[[tk]]
df_pronostico <- tibble(
horizonte = 1:20,
media_pronosticada = as.numeric(fitted(pronostico)),
volatilidad_pronosticada = as.numeric(sigma(pronostico))
)
cat("\n", rep("=", 40), "\n")
cat("Pronóstico para:", tk, "\n")
cat(rep("=", 40), "\n")
print(df_pronostico)
p <- ggplot(df_pronostico, aes(x = horizonte, y = volatilidad_pronosticada)) +
geom_line(linewidth = 0.8) +
geom_point(size = 1.8) +
labs(
title = paste("Pronóstico de volatilidad —", tk),
subtitle = "GARCH(1,1) t-Student · Horizonte de 20 días",
x = "Días hacia adelante",
y = "Volatilidad diaria esperada (%)"
) +
theme_minimal()
print(p)
})
##
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## Pronóstico para: MSFT
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## # A tibble: 20 × 3
## horizonte media_pronosticada volatilidad_pronosticada
## <int> <dbl> <dbl>
## 1 1 0.114 1.69
## 2 2 0.114 1.70
## 3 3 0.114 1.71
## 4 4 0.114 1.72
## 5 5 0.114 1.72
## 6 6 0.114 1.73
## 7 7 0.114 1.74
## 8 8 0.114 1.75
## 9 9 0.114 1.75
## 10 10 0.114 1.76
## 11 11 0.114 1.77
## 12 12 0.114 1.77
## 13 13 0.114 1.78
## 14 14 0.114 1.78
## 15 15 0.114 1.79
## 16 16 0.114 1.80
## 17 17 0.114 1.80
## 18 18 0.114 1.81
## 19 19 0.114 1.81
## 20 20 0.114 1.82
##
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## Pronóstico para: JPM
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## # A tibble: 20 × 3
## horizonte media_pronosticada volatilidad_pronosticada
## <int> <dbl> <dbl>
## 1 1 0.129 1.52
## 2 2 0.129 1.54
## 3 3 0.129 1.57
## 4 4 0.129 1.59
## 5 5 0.129 1.61
## 6 6 0.129 1.62
## 7 7 0.129 1.64
## 8 8 0.129 1.66
## 9 9 0.129 1.67
## 10 10 0.129 1.68
## 11 11 0.129 1.70
## 12 12 0.129 1.71
## 13 13 0.129 1.72
## 14 14 0.129 1.73
## 15 15 0.129 1.74
## 16 16 0.129 1.75
## 17 17 0.129 1.75
## 18 18 0.129 1.76
## 19 19 0.129 1.77
## 20 20 0.129 1.78
##
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## Pronóstico para: XOM
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## # A tibble: 20 × 3
## horizonte media_pronosticada volatilidad_pronosticada
## <int> <dbl> <dbl>
## 1 1 0.0817 1.98
## 2 2 0.0817 1.98
## 3 3 0.0817 1.98
## 4 4 0.0817 1.98
## 5 5 0.0817 1.98
## 6 6 0.0817 1.98
## 7 7 0.0817 1.98
## 8 8 0.0817 1.98
## 9 9 0.0817 1.98
## 10 10 0.0817 1.98
## 11 11 0.0817 1.98
## 12 12 0.0817 1.98
## 13 13 0.0817 1.98
## 14 14 0.0817 1.98
## 15 15 0.0817 1.99
## 16 16 0.0817 1.99
## 17 17 0.0817 1.99
## 18 18 0.0817 1.99
## 19 19 0.0817 1.99
## 20 20 0.0817 1.99
##
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## Pronóstico para: JNJ
## = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
## # A tibble: 20 × 3
## horizonte media_pronosticada volatilidad_pronosticada
## <int> <dbl> <dbl>
## 1 1 0.0381 1.17
## 2 2 0.0381 1.17
## 3 3 0.0381 1.17
## 4 4 0.0381 1.16
## 5 5 0.0381 1.16
## 6 6 0.0381 1.16
## 7 7 0.0381 1.16
## 8 8 0.0381 1.15
## 9 9 0.0381 1.15
## 10 10 0.0381 1.15
## 11 11 0.0381 1.15
## 12 12 0.0381 1.15
## 13 13 0.0381 1.15
## 14 14 0.0381 1.15
## 15 15 0.0381 1.15
## 16 16 0.0381 1.15
## 17 17 0.0381 1.15
## 18 18 0.0381 1.14
## 19 19 0.0381 1.14
## 20 20 0.0381 1.14
purrr::walk(tickers, function(tk) {
retornos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
n_total <- nrow(retornos_tk)
n_start <- n_total - 300
roll <- tryCatch(
ugarchroll(
spec = spec_std,
data = retornos_tk$retorno_log,
n.start = n_start,
refit.every = 25,
refit.window = "moving",
solver = "hybrid",
calculate.VaR = FALSE,
keep.coef = TRUE
),
error = function(e) NULL
)
if (!is.null(roll)) {
roll_df <- as.data.frame(roll) %>%
rownames_to_column("fecha") %>%
mutate(
fecha = as.Date(fecha),
sigma_pronosticada = Sigma
)
p <- ggplot(roll_df, aes(x = fecha, y = sigma_pronosticada)) +
geom_line(linewidth = 0.7) +
labs(
title = paste("Pronóstico rolling de volatilidad —", tk),
subtitle = "Cada 25 días el modelo se reestima con nueva información",
x = NULL,
y = "Volatilidad diaria esperada (%)"
) +
theme_minimal()
print(p)
}
})
df_volatilidades <- purrr::map_dfr(tickers, function(tk) {
retornos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
tibble(
fecha = retornos_tk$fecha,
activo = tk,
sigma = as.numeric(sigma(modelos_std[[tk]]))
)
})
ggplot(df_volatilidades, aes(x = fecha, y = sigma, color = activo)) +
geom_line(linewidth = 0.6) +
facet_wrap(~ activo, scales = "free_y") +
labs(
title = "Volatilidad condicional estimada por activo",
subtitle = "Modelo GARCH(1,1) con distribución t-Student",
x = NULL,
y = "Volatilidad diaria (%)"
) +
theme_minimal() +
theme(legend.position = "none")
purrr::map_dfr(tickers, function(tk) {
modelo <- modelos_std[[tk]]
cf <- coef(modelo)
alpha <- cf["alpha1"]
beta <- cf["beta1"]
omega <- cf["omega"]
tibble(
Activo = tk,
alpha1 = round(alpha, 4),
beta1 = round(beta, 4),
Persistencia = round(alpha + beta, 4),
`Vol. LP (%)`= round(sqrt(omega / (1 - alpha - beta)), 4),
shape = round(cf["shape"], 4)
)
}) %>%
knitr::kable(caption = "Resumen de persistencia de volatilidad por activo — GARCH(1,1) t-Student")
| Activo | alpha1 | beta1 | Persistencia | Vol. LP (%) | shape |
|---|---|---|---|---|---|
| MSFT | 0.1038 | 0.8650 | 0.9688 | 1.9559 | 5.0780 |
| JPM | 0.1609 | 0.7760 | 0.9370 | 1.8710 | 4.9267 |
| XOM | 0.0592 | 0.9294 | 0.9886 | 2.0168 | 8.4823 |
| JNJ | 0.0937 | 0.7947 | 0.8884 | 1.1402 | 4.7583 |
El análisis GARCH revela diferencias importantes en la dinámica de volatilidad entre los cuatro activos de sectores distintos:
Persistencia: Todos los activos presentan valores de \(\alpha_1 + \beta_1\) cercanos a 1, lo que indica alta persistencia de la volatilidad. Los shocks de riesgo no desaparecen rápidamente sino que se disipan de forma gradual. XOM tiende a mostrar mayor persistencia dado que los choques del mercado del petróleo pueden mantenerse durante períodos prolongados.
Reacción a shocks: El parámetro \(\alpha_1\) mide la velocidad de reacción de la volatilidad ante nuevos shocks. JPM suele presentar un \(\alpha_1\) más alto, reflejando la mayor sensibilidad del sector financiero a noticias económicas y decisiones de política monetaria.
Volatilidad de largo plazo: XOM presenta la mayor volatilidad de largo plazo, consistente con la exposición directa al precio del petróleo. JNJ muestra la menor, coherente con su naturaleza defensiva e ingresos estables.
Distribución t-Student: En todos los activos, la
distribución t-Student mejora el ajuste respecto a la normal,
confirmando la presencia de colas pesadas en los retornos financieros
diarios. El parámetro shape (grados de libertad) inferior a
10 en todos los activos indica que los eventos extremos son más
frecuentes de lo que la distribución normal esperaría.
Efecto apalancamiento: Los modelos GJR-GARCH y EGARCH no mejoran sustancialmente el ajuste respecto al GARCH estándar en todos los activos, aunque en sectores como energía y finanzas, donde las malas noticias suelen tener mayor impacto, sería recomendable explorar más a fondo los modelos asimétricos.
Implicaciones para la gestión de riesgo: La alta persistencia de la volatilidad en todos los sectores sugiere que los períodos de estrés tienden a prolongarse. Esto es relevante para el cálculo de medidas de riesgo como el VaR y el CVaR, donde asumir volatilidad constante podría subestimar significativamente el riesgo en períodos de turbulencia.