1. Cargue de librerías

Instalación de paquetes para modelos GARCH y pronóstico de volatilidad.

library(tidyverse)
library(quantmod)
library(tidyquant)
library(forecast)
library(tseries)
library(scales)
library(lubridate)
library(moments)
library(FinTS)
library(rugarch)
library(zoo)

2. Activos seleccionados

Trabajamos con los mismos activos del Taller 2.

tickers <- c("MSFT", "JPM", "XOM", "JNJ")
MSFT – Acción de Microsoft Corporation. Empresa tecnológica multinacional líder en software, servicios en la nube (Azure) y soluciones empresariales.
JPM – Acción de JPMorgan Chase & Co. El banco más grande de Estados Unidos por activos. Opera en banca de inversión, servicios financieros al consumidor y gestión de activos.
XOM – Acción de ExxonMobil Corporation. Una de las mayores empresas petroleras integradas del mundo. Sus ingresos están estrechamente ligados al precio del petróleo y el gas natural.
JNJ – Acción de Johnson & Johnson. Empresa farmacéutica y de dispositivos médicos considerada defensiva, con ingresos estables independientemente del ciclo económico.

3. Descargue de precios

Precios ajustados desde Yahoo Finance desde enero de 2020.

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

4. Construcción de retornos logarítmicos

Los retornos se expresan en porcentaje: \(r_t = 100 \times [\log(P_t) - \log(P_{t-1})]\)

retornos <- precios %>%
  group_by(activo) %>%
  arrange(fecha) %>%
  mutate(
    retorno_log = 100 * (log(precio) - log(lag(precio)))
  ) %>%
  ungroup() %>%
  drop_na()

5. Gráfico de retornos logarítmicos

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

6. Hechos estilizados

Estadísticas descriptivas de los retornos por activo.

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

6.1 Histogramas de retornos

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

6.2 Conclusión hechos estilizados

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.

7. ACF de retornos y retornos al cuadrado

La ACF de retornos al cuadrado permite detectar memoria en la volatilidad.

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

7.1 Conclusión ACF

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.

8. Prueba ARCH

Evaluamos si existe heterocedasticidad condicional en los retornos.

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

8.1 Conclusión prueba 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.

9. Estimación GARCH(1,1) con distribución normal

Especificamos y estimamos un GARCH(1,1) con media constante y errores normales.

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

9.1 Parámetros GARCH normal por activo

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

9.2 Volatilidad condicional estimada — Normal

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

10. Estimación GARCH(1,1) con distribución t-Student

La distribución t-Student permite colas más pesadas, más adecuada para retornos financieros.

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

10.1 Parámetros GARCH t-Student por activo

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

10.2 Volatilidad condicional estimada — t-Student

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

11. Comparación Normal vs t-Student

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

12. Modelos asimétricos: GJR-GARCH y EGARCH

El GJR-GARCH y EGARCH capturan el efecto apalancamiento: las malas noticias aumentan más la volatilidad que las buenas.

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

12.1 Comparación de todos los modelos por activo

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

12.2 Comparación de volatilidades por modelo

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

13. Diagnóstico de residuos

Verificamos si el modelo capturó correctamente la estructura de la volatilidad.

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

13.1 Prueba ARCH sobre residuos estandarizados

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

13.2 Conclusión diagnóstico

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.

14. Pronóstico de volatilidad a 20 días

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

15. Pronóstico rolling a un día

El pronóstico rolling actualiza el modelo día a día con nueva información disponible.

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

16. Comparación de volatilidad entre activos

Comparamos la volatilidad condicional estimada de todos los activos en un solo gráfico.

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

17. Tabla resumen de persistencia por activo

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

18. Discusión final

18.1 Diferencias de volatilidad entre activos y sectores

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.