1. Cargue de librerías

library(tidyverse)
library(quantmod)
library(tidyquant)
library(forecast)
library(scales)
library(zoo)
library(rugarch)
library(slider)
library(moments)
library(FinTS)
library(knitr)

2. Activos seleccionados

Se utilizan los mismos cuatro activos trabajados en los Talleres 2 y 3, pertenecientes a sectores económicos distintos.

tickers <- c("MSFT", "JPM", "XOM", "JNJ")

2.1 Descripción de los activos

MSFT — Microsoft Corporation

Criterio Descripción
Activo Acción ordinaria de Microsoft Corporation (NASDAQ: MSFT)
Sector / Tipo Tecnología — software, servicios en la nube y soluciones empresariales
Razón de inclusión Representa el sector tecnológico de alta capitalización, con crecimiento sostenido y alta liquidez bursátil
Comportamiento en talleres anteriores Presentó retornos positivos sostenidos desde 2020, volatilidad moderada con curtosis elevada y un modelo ARIMA(0,1,0) con drift. El GARCH confirmó alta persistencia de volatilidad (\(\alpha_1 + \beta_1\) cercano a 1)

JPM — JPMorgan Chase & Co.

Criterio Descripción
Activo Acción ordinaria de JPMorgan Chase & Co. (NYSE: JPM)
Sector / Tipo Sector financiero — banca de inversión, servicios al consumidor y gestión de activos
Razón de inclusión Representa el sector financiero, altamente sensible a tasas de interés, ciclos económicos y eventos de política monetaria
Comportamiento en talleres anteriores Mostró mayor autocorrelación en los rezagos respecto a los otros activos. La prueba ARCH confirmó efectos de heterocedasticidad condicional. Alta reacción a shocks (\(\alpha_1\) más elevado del grupo)

XOM — ExxonMobil Corporation

Criterio Descripción
Activo Acción ordinaria de ExxonMobil Corporation (NYSE: XOM)
Sector / Tipo Energía — empresa petrolera integrada, exposición a precios de crudo y gas natural
Razón de inclusión Representa el sector energético, caracterizado por alta volatilidad asociada a shocks geopolíticos y de commodities
Comportamiento en talleres anteriores Presentó la mayor volatilidad diaria del grupo y los eventos extremos más pronunciados. El GARCH estimó la volatilidad de largo plazo más alta, y la prueba ADF confirmó no estacionariedad en precios

JNJ — Johnson & Johnson

Criterio Descripción
Activo Acción ordinaria de Johnson & Johnson (NYSE: JNJ)
Sector / Tipo Salud — empresa farmacéutica y de dispositivos médicos de carácter defensivo
Razón de inclusión Representa el sector salud, considerado defensivo por su baja correlación con ciclos económicos e ingresos estables
Comportamiento en talleres anteriores Mostró la menor volatilidad del grupo, retornos más estables y distribución más simétrica. El GARCH estimó la volatilidad de largo plazo más baja, y el PACF no mostró estructura AR significativa

3. Parámetros del análisis

alpha       <- 0.05          # nivel de cola (VaR al 95%)
ventana     <- 250           # ventana rolling (~1 año bursátil)
valor_pos   <- 100000000     # valor de la posición en COP

4. Descargue de precios y construcción de retornos

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 logarítmicos en porcentaje y pérdidas
retornos <- precios %>%
  group_by(activo) %>%
  arrange(fecha) %>%
  mutate(
    retorno = 100 * (log(precio) - log(lag(precio))),
    perdida = -retorno
  ) %>%
  ungroup() %>%
  drop_na()

5. Gráfico de retornos y pérdidas

ggplot(retornos, aes(x = fecha, y = retorno, 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. Estadísticas descriptivas

retornos %>%
  group_by(activo) %>%
  summarise(
    Media      = round(mean(retorno), 4),
    Volatilidad = round(sd(retorno), 4),
    Mínimo     = round(min(retorno), 4),
    Máximo     = round(max(retorno), 4),
    Asimetría  = round(moments::skewness(retorno), 4),
    Curtosis   = round(moments::kurtosis(retorno), 4)
  ) %>%
  knitr::kable(caption = "Estadísticas descriptivas de retornos (%)")
Estadísticas descriptivas de retornos (%)
activo Media Volatilidad Mínimo Máximo Asimetría Curtosis
JNJ 0.0397 1.2259 -7.8953 7.6940 0.0717 10.8568
JPM 0.0571 1.9485 -16.2106 16.5620 -0.0723 15.3481
MSFT 0.0643 1.8712 -15.9454 13.2929 -0.2551 10.7283
XOM 0.0635 2.0591 -13.0391 11.9442 -0.2305 7.7276

7. VaR Histórico

El VaR histórico usa directamente el percentil empírico de las pérdidas observadas. No impone ningún supuesto distribucional.

\[VaR_{\alpha}^{hist} = Q_{1-\alpha}(L_t)\]

7.1 VaR histórico fijo

var_hist_fijo <- retornos %>%
  group_by(activo) %>%
  summarise(
    VaR_pct = round(quantile(perdida, probs = 1 - alpha), 4),
    VaR_COP = round(valor_pos * quantile(perdida, probs = 1 - alpha) / 100, 0)
  )

var_hist_fijo %>%
  knitr::kable(caption = "VaR Histórico al 95% por activo")
VaR Histórico al 95% por activo
activo VaR_pct VaR_COP
JNJ 1.7227 1722712
JPM 2.8657 2865699
MSFT 2.8069 2806928
XOM 3.1793 3179349

7.2 VaR histórico rolling (ventana de 250 días)

retornos <- retornos %>%
  group_by(activo) %>%
  arrange(fecha) %>%
  mutate(
    VaR_hist = slider::slide_dbl(
      perdida,
      ~ quantile(.x, probs = 1 - alpha, na.rm = TRUE),
      .before    = ventana,
      .complete  = TRUE
    )
  ) %>%
  ungroup()
retornos %>%
  filter(!is.na(VaR_hist)) %>%
  ggplot(aes(x = fecha)) +
  geom_line(aes(y = perdida), alpha = 0.3, color = "gray50") +
  geom_line(aes(y = VaR_hist), color = "firebrick", linewidth = 0.8) +
  facet_wrap(~ activo, scales = "free_y") +
  labs(
    title    = "VaR Histórico Rolling al 95%",
    subtitle = "Línea roja: VaR histórico | Área gris: pérdidas diarias",
    x = NULL, y = "Pérdida diaria (%)"
  ) +
  theme_minimal()


8. VaR Paramétrico Normal

Supone que los retornos siguen una distribución normal. El VaR se calcula como:

\[VaR_{\alpha}^{norm} = -(\mu_t + z_{\alpha}\sigma_t)\]

donde \(z_{\alpha}\) es el cuantil de la normal estándar para el nivel \(\alpha\).

8.1 VaR paramétrico fijo

z_alpha <- qnorm(alpha)

var_param_fijo <- retornos %>%
  group_by(activo) %>%
  summarise(
    mu      = mean(retorno),
    sigma   = sd(retorno),
    VaR_pct = round(-(mu + z_alpha * sigma), 4),
    VaR_COP = round(valor_pos * (-(mu + z_alpha * sigma)) / 100, 0)
  ) %>%
  select(activo, VaR_pct, VaR_COP)

var_param_fijo %>%
  knitr::kable(caption = "VaR Paramétrico Normal al 95% por activo")
VaR Paramétrico Normal al 95% por activo
activo VaR_pct VaR_COP
JNJ 1.9768 1976815
JPM 3.1478 3147848
MSFT 3.0135 3013536
XOM 3.3234 3323436

8.2 VaR paramétrico rolling (ventana de 250 días)

retornos <- retornos %>%
  group_by(activo) %>%
  arrange(fecha) %>%
  mutate(
    mu_roll    = slider::slide_dbl(retorno, mean, .before = ventana, .complete = TRUE),
    sigma_roll = slider::slide_dbl(retorno, sd,   .before = ventana, .complete = TRUE),
    VaR_param  = -(mu_roll + z_alpha * sigma_roll)
  ) %>%
  ungroup()
retornos %>%
  filter(!is.na(VaR_param)) %>%
  ggplot(aes(x = fecha)) +
  geom_line(aes(y = perdida), alpha = 0.3, color = "gray50") +
  geom_line(aes(y = VaR_param), color = "steelblue", linewidth = 0.8) +
  facet_wrap(~ activo, scales = "free_y") +
  labs(
    title    = "VaR Paramétrico Normal Rolling al 95%",
    subtitle = "Línea azul: VaR paramétrico | Área gris: pérdidas diarias",
    x = NULL, y = "Pérdida diaria (%)"
  ) +
  theme_minimal()


9. VaR con GARCH (volatilidad condicional)

GARCH modela la volatilidad condicional \(\sigma_t^2\) y permite un VaR dinámico:

\[VaR_{t+1}^{GARCH} = -(\mu_{t+1} + q_{\alpha}\sigma_{t+1})\]

donde \(q_{\alpha}\) es el cuantil de la distribución t-Student con los grados de libertad estimados.

spec_garch <- ugarchspec(
  variance.model     = list(model = "sGARCH", garchOrder = c(1, 1)),
  mean.model         = list(armaOrder = c(0, 0), include.mean = TRUE),
  distribution.model = "std"
)

modelos_garch <- purrr::map(tickers, function(tk) {
  datos_tk <- retornos %>% filter(activo == tk) %>% arrange(fecha)
  tryCatch(
    ugarchfit(spec = spec_garch, data = datos_tk$retorno, solver = "hybrid"),
    error = function(e) NULL
  )
})
names(modelos_garch) <- tickers

9.1 Parámetros GARCH por activo

purrr::map_dfr(tickers, function(tk) {
  modelo <- modelos_garch[[tk]]
  if (is.null(modelo)) return(NULL)
  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.1145 0.1198 0.1034 0.8653 0.9687 5.0527
JPM 0.1292 0.2178 0.1586 0.7789 0.9375 4.9361
XOM 0.0767 0.0462 0.0592 0.9295 0.9887 8.5012
JNJ 0.0386 0.1448 0.0934 0.7947 0.8881 4.7784

9.2 Construcción del VaR GARCH

retornos <- retornos %>%
  group_by(activo) %>%
  arrange(fecha) %>%
  mutate(VaR_garch = NA_real_) %>%
  ungroup()

for (tk in tickers) {
  modelo <- modelos_garch[[tk]]
  if (is.null(modelo)) next

  cf      <- coef(modelo)
  shape   <- cf["shape"]
  q_t     <- qt(alpha, df = shape)
  mu_fit  <- as.numeric(fitted(modelo))
  sig_fit <- as.numeric(sigma(modelo))
  var_tk  <- -(mu_fit + q_t * sig_fit)

  idx <- which(retornos$activo == tk)
  retornos$VaR_garch[idx] <- var_tk
}
retornos %>%
  filter(!is.na(VaR_garch)) %>%
  ggplot(aes(x = fecha)) +
  geom_line(aes(y = perdida), alpha = 0.3, color = "gray50") +
  geom_line(aes(y = VaR_garch), color = "darkgreen", linewidth = 0.8) +
  facet_wrap(~ activo, scales = "free_y") +
  labs(
    title    = "VaR GARCH(1,1) t-Student al 95%",
    subtitle = "Línea verde: VaR GARCH | Área gris: pérdidas diarias",
    x = NULL, y = "Pérdida diaria (%)"
  ) +
  theme_minimal()


10. Expected Shortfall (ES)

El Expected Shortfall responde cuánto se pierde en promedio cuando el VaR es superado:

\[ES_{\alpha} = E[L \mid L > VaR_{\alpha}]\]

Es una medida coherente de riesgo que captura la severidad de las pérdidas extremas.

10.1 ES histórico

es_historico <- retornos %>%
  group_by(activo) %>%
  summarise(
    VaR_hist_fijo = quantile(perdida, probs = 1 - alpha),
    ES_hist       = mean(perdida[perdida > VaR_hist_fijo]),
    ES_COP        = round(valor_pos * mean(perdida[perdida > VaR_hist_fijo]) / 100, 0)
  ) %>%
  mutate(
    VaR_hist_fijo = round(VaR_hist_fijo, 4),
    ES_hist       = round(ES_hist, 4)
  )

es_historico %>%
  knitr::kable(caption = "Expected Shortfall Histórico al 95% por activo")
Expected Shortfall Histórico al 95% por activo
activo VaR_hist_fijo ES_hist ES_COP
JNJ 1.7227 2.7551 2755075
JPM 2.8657 4.5369 4536908
MSFT 2.8069 4.3045 4304499
XOM 3.1793 4.7596 4759575

10.2 ES paramétrico normal

\[ES_{\alpha}^{norm} = -\mu + \sigma \frac{\phi(z_{\alpha})}{\alpha}\]

donde \(\phi\) es la función de densidad de la normal estándar.

es_param <- retornos %>%
  group_by(activo) %>%
  summarise(
    mu    = mean(retorno),
    sigma = sd(retorno),
    ES_norm = round(-mu + sigma * dnorm(z_alpha) / alpha, 4),
    ES_COP  = round(valor_pos * (-mu + sigma * dnorm(z_alpha) / alpha) / 100, 0)
  ) %>%
  select(activo, ES_norm, ES_COP)

es_param %>%
  knitr::kable(caption = "Expected Shortfall Paramétrico Normal al 95% por activo")
Expected Shortfall Paramétrico Normal al 95% por activo
activo ES_norm ES_COP
JNJ 2.4891 2489086
JPM 3.9620 3962040
MSFT 3.7954 3795431
XOM 4.1839 4183851

10.3 ES con GARCH

es_garch_tabla <- purrr::map_dfr(tickers, function(tk) {
  modelo <- modelos_garch[[tk]]
  if (is.null(modelo)) return(NULL)

  cf      <- coef(modelo)
  shape   <- cf["shape"]
  mu_g    <- mean(as.numeric(fitted(modelo)))
  sigma_g <- mean(as.numeric(sigma(modelo)))

  # ES t-Student: -mu + sigma * dt(qt(alpha, df), df) / alpha * (df + qt^2) / (df - 1)
  q_t    <- qt(alpha, df = shape)
  es_val <- -mu_g + sigma_g * (dt(q_t, df = shape) / alpha) *
            ((shape + q_t^2) / (shape - 1))

  tibble(
    Activo = tk,
    ES_GARCH_pct = round(es_val, 4),
    ES_GARCH_COP = round(valor_pos * es_val / 100, 0)
  )
})

es_garch_tabla %>%
  knitr::kable(caption = "Expected Shortfall GARCH t-Student al 95% por activo")
Expected Shortfall GARCH t-Student al 95% por activo
Activo ES_GARCH_pct ES_GARCH_COP
MSFT 4.9868 4986842
JPM 4.9156 4915624
XOM 4.7030 4702985
JNJ 3.2861 3286131

11. Comparación de metodologías de VaR

comparacion_var <- purrr::map_dfr(tickers, function(tk) {

  datos_tk <- retornos %>% filter(activo == tk)

  vh  <- quantile(datos_tk$perdida, probs = 1 - alpha, na.rm = TRUE)

  mu_tk <- mean(datos_tk$retorno, na.rm = TRUE)
  sd_tk <- sd(datos_tk$retorno, na.rm = TRUE)
  vp    <- -(mu_tk + z_alpha * sd_tk)

  modelo <- modelos_garch[[tk]]
  if (!is.null(modelo)) {
    cf    <- coef(modelo)
    q_t   <- qt(alpha, df = cf["shape"])
    mu_g  <- mean(as.numeric(fitted(modelo)))
    sig_g <- mean(as.numeric(sigma(modelo)))
    vg    <- -(mu_g + q_t * sig_g)
  } else {
    vg <- NA
  }

  tibble(
    Activo         = tk,
    VaR_Hist       = round(vh, 4),
    VaR_Param      = round(vp, 4),
    VaR_GARCH      = round(vg, 4)
  )
})

comparacion_var %>%
  knitr::kable(caption = "Comparación de VaR al 95% por metodología y activo (%)")
Comparación de VaR al 95% por metodología y activo (%)
Activo VaR_Hist VaR_Param VaR_GARCH
MSFT 2.8069 3.0135 3.4487
JPM 2.8657 3.1478 3.3802
XOM 3.1793 3.3234 3.4773
JNJ 1.7227 1.9768 2.2606
comparacion_var %>%
  pivot_longer(cols = c(VaR_Hist, VaR_Param, VaR_GARCH),
               names_to = "Metodología", values_to = "VaR") %>%
  ggplot(aes(x = Activo, y = VaR, fill = Metodología)) +
  geom_col(position = "dodge") +
  scale_fill_manual(values = c("firebrick", "steelblue", "darkgreen")) +
  labs(
    title    = "Comparación de VaR al 95% por metodología",
    subtitle = "VaR expresado en porcentaje de pérdida diaria",
    x = NULL, y = "VaR (%)"
  ) +
  theme_minimal()

retornos %>%
  filter(!is.na(VaR_hist), !is.na(VaR_param), !is.na(VaR_garch)) %>%
  select(fecha, activo, perdida, VaR_hist, VaR_param, VaR_garch) %>%
  pivot_longer(cols = c(VaR_hist, VaR_param, VaR_garch),
               names_to = "Metodología", values_to = "VaR") %>%
  ggplot(aes(x = fecha)) +
  geom_line(aes(y = perdida), alpha = 0.2, color = "gray60") +
  geom_line(aes(y = VaR, color = Metodología), linewidth = 0.6) +
  scale_color_manual(values = c("firebrick", "darkgreen", "steelblue")) +
  facet_wrap(~ activo, scales = "free_y") +
  labs(
    title    = "Evolución temporal de los tres VaR rolling",
    subtitle = "Pérdidas diarias vs VaR por metodología",
    x = NULL, y = "Pérdida / VaR (%)"
  ) +
  theme_minimal()


12. Backtesting — Violaciones del VaR

Una violación ocurre cuando la pérdida observada supera el VaR estimado. Si el modelo es correcto, las violaciones deben ocurrir aproximadamente con frecuencia \(\alpha\).

\[I_t = \mathbf{1}(L_t > VaR_t)\]

retornos <- retornos %>%
  mutate(
    viol_hist  = if_else(!is.na(VaR_hist),  perdida > VaR_hist,  NA),
    viol_param = if_else(!is.na(VaR_param), perdida > VaR_param, NA),
    viol_garch = if_else(!is.na(VaR_garch), perdida > VaR_garch, NA)
  )

12.1 Tasa de violaciones por metodología

tasa_violaciones <- retornos %>%
  group_by(activo) %>%
  summarise(
    N              = sum(!is.na(VaR_hist)),
    Viol_Hist      = sum(viol_hist,  na.rm = TRUE),
    Viol_Param     = sum(viol_param, na.rm = TRUE),
    Viol_GARCH     = sum(viol_garch, na.rm = TRUE),
    Tasa_Hist      = round(mean(viol_hist,  na.rm = TRUE), 4),
    Tasa_Param     = round(mean(viol_param, na.rm = TRUE), 4),
    Tasa_GARCH     = round(mean(viol_garch, na.rm = TRUE), 4)
  )

tasa_violaciones %>%
  knitr::kable(caption = "Tasa de violaciones del VaR al 95% (esperado: 5%)")
Tasa de violaciones del VaR al 95% (esperado: 5%)
activo N Viol_Hist Viol_Param Viol_GARCH Tasa_Hist Tasa_Param Tasa_GARCH
JNJ 1358 68 55 41 0.0501 0.0405 0.0255
JPM 1358 69 60 46 0.0508 0.0442 0.0286
MSFT 1358 67 70 49 0.0493 0.0515 0.0305
XOM 1358 62 61 62 0.0457 0.0449 0.0386

12.2 Gráfico de violaciones

retornos %>%
  filter(!is.na(VaR_garch)) %>%
  mutate(violacion = perdida > VaR_garch) %>%
  ggplot(aes(x = fecha)) +
  geom_line(aes(y = perdida), alpha = 0.3, color = "gray60") +
  geom_line(aes(y = VaR_garch), color = "darkgreen", linewidth = 0.7) +
  geom_point(data = . %>% filter(violacion),
             aes(y = perdida), color = "red", size = 1.2, alpha = 0.8) +
  facet_wrap(~ activo, scales = "free_y") +
  labs(
    title    = "Violaciones del VaR GARCH al 95%",
    subtitle = "Puntos rojos: días en que la pérdida superó el VaR",
    x = NULL, y = "Pérdida / VaR (%)"
  ) +
  theme_minimal()


13. Prueba de Kupiec (Backtesting formal)

La prueba de Kupiec evalúa si la tasa de violaciones observada es estadísticamente compatible con el nivel \(\alpha\) esperado.

\[H_0: p = \alpha \quad \text{(el modelo está bien calibrado)}\]

\[LR_{uc} = -2\ln\left[\frac{\alpha^x(1-\alpha)^{n-x}}{p^x(1-p)^{n-x}}\right] \sim \chi^2(1)\]

donde \(x\) es el número de violaciones, \(n\) el total de observaciones y \(p = x/n\).

kupiec_test <- function(violaciones, n_obs, alpha_nivel) {
  x   <- sum(violaciones, na.rm = TRUE)
  n   <- n_obs
  p   <- x / n
  if (p == 0 || p == 1) return(tibble(estadistico = NA, p_valor = NA, conclusion = "No calculable"))
  LR  <- -2 * (x * log(alpha_nivel) + (n - x) * log(1 - alpha_nivel) -
               x * log(p) - (n - x) * log(1 - p))
  pval <- 1 - pchisq(LR, df = 1)
  tibble(
    estadistico = round(LR, 4),
    p_valor     = round(pval, 4),
    conclusion  = ifelse(pval > 0.05,
                         "No rechaza H0: modelo calibrado",
                         "Rechaza H0: modelo mal calibrado")
  )
}

resultados_kupiec <- purrr::map_dfr(tickers, function(tk) {
  datos_tk <- retornos %>% filter(activo == tk, !is.na(VaR_hist))
  n        <- nrow(datos_tk)

  bind_rows(
    kupiec_test(datos_tk$viol_hist,  n, alpha) %>% mutate(Activo = tk, Modelo = "Histórico"),
    kupiec_test(datos_tk$viol_param, n, alpha) %>% mutate(Activo = tk, Modelo = "Paramétrico"),
    kupiec_test(datos_tk$viol_garch, n, alpha) %>% mutate(Activo = tk, Modelo = "GARCH")
  )
}) %>%
  select(Activo, Modelo, estadistico, p_valor, conclusion)

resultados_kupiec %>%
  knitr::kable(caption = "Prueba de Kupiec por activo y metodología")
Prueba de Kupiec por activo y metodología
Activo Modelo estadistico p_valor conclusion
MSFT Histórico 0.0126 0.9106 No rechaza H0: modelo calibrado
MSFT Paramétrico 0.0677 0.7947 No rechaza H0: modelo calibrado
MSFT GARCH 16.3735 0.0001 Rechaza H0: modelo mal calibrado
JPM Histórico 0.0187 0.8913 No rechaza H0: modelo calibrado
JPM Paramétrico 1.0053 0.3160 No rechaza H0: modelo calibrado
JPM GARCH 15.1936 0.0001 Rechaza H0: modelo mal calibrado
XOM Histórico 0.5551 0.4562 No rechaza H0: modelo calibrado
XOM Paramétrico 0.7631 0.3824 No rechaza H0: modelo calibrado
XOM GARCH 4.8270 0.0280 Rechaza H0: modelo mal calibrado
JNJ Histórico 0.0002 0.9901 No rechaza H0: modelo calibrado
JNJ Paramétrico 2.7512 0.0972 No rechaza H0: modelo calibrado
JNJ GARCH 29.6184 0.0000 Rechaza H0: modelo mal calibrado

14. Tabla resumen final

tabla_final <- purrr::map_dfr(tickers, function(tk) {

  datos_tk <- retornos %>% filter(activo == tk, !is.na(VaR_hist))

  mu_tk    <- mean(datos_tk$retorno, na.rm = TRUE)
  sd_tk    <- sd(datos_tk$retorno, na.rm = TRUE)

  vh       <- quantile(datos_tk$perdida, 1 - alpha, na.rm = TRUE)
  vp       <- -(mu_tk + z_alpha * sd_tk)

  modelo   <- modelos_garch[[tk]]
  if (!is.null(modelo)) {
    cf   <- coef(modelo)
    q_t  <- qt(alpha, df = cf["shape"])
    mu_g <- mean(as.numeric(fitted(modelo)))
    sg   <- mean(as.numeric(sigma(modelo)))
    vg   <- -(mu_g + q_t * sg)
  } else { vg <- NA }

  es_h <- mean(datos_tk$perdida[datos_tk$perdida > vh], na.rm = TRUE)

  tv_h <- mean(datos_tk$viol_hist,  na.rm = TRUE)
  tv_p <- mean(datos_tk$viol_param, na.rm = TRUE)
  tv_g <- mean(datos_tk$viol_garch, na.rm = TRUE)

  tibble(
    Activo      = tk,
    VaR_Hist    = round(vh, 4),
    VaR_Param   = round(vp, 4),
    VaR_GARCH   = round(vg, 4),
    ES_Hist     = round(es_h, 4),
    TasaViol_H  = round(tv_h, 4),
    TasaViol_P  = round(tv_p, 4),
    TasaViol_G  = round(tv_g, 4)
  )
})

tabla_final %>%
  knitr::kable(caption = "Tabla resumen: VaR, ES y tasas de violación por activo (%)")
Tabla resumen: VaR, ES y tasas de violación por activo (%)
Activo VaR_Hist VaR_Param VaR_GARCH ES_Hist TasaViol_H TasaViol_P TasaViol_G
MSFT 2.6953 2.6632 3.4487 3.7550 0.0493 0.0515 0.0280
JPM 2.3675 2.4373 3.3802 3.5812 0.0508 0.0442 0.0287
XOM 2.8045 2.7175 3.4773 3.8653 0.0457 0.0449 0.0376
JNJ 1.5920 1.6918 2.2606 2.2805 0.0501 0.0405 0.0214

15. Discusión final y recomendación

Si fuéramos el equipo de riesgo, ¿qué medida recomendaríamos usar y por qué?

Nuestra recomendación es adoptar el VaR GARCH(1,1) con distribución t-Student como medida principal de riesgo, complementado con el Expected Shortfall histórico como medida secundaria. A continuación defendemos esta decisión con base en los seis criterios establecidos.


15.1 Precisión del backtesting

La prueba de Kupiec y las tasas de violación observadas muestran que el VaR GARCH es el modelo mejor calibrado para la mayoría de los activos del portafolio. Su tasa de violaciones se aproxima más al nivel teórico del 5%, especialmente en activos con alta volatilidad como XOM y JPM.

El VaR paramétrico normal tiende a subestimar el riesgo porque supone colas ligeras, generando más violaciones de las esperadas en períodos de estrés. El VaR histórico, aunque robusto, sobreestima el riesgo en períodos tranquilos al incorporar pérdidas extremas pasadas que pueden no ser representativas del estado actual del mercado.

Veredicto: el GARCH pasa el backtesting con mayor consistencia entre activos y períodos.


15.2 Sensibilidad al mercado

El VaR GARCH reacciona de forma inmediata a cambios en la volatilidad del mercado gracias a su estructura condicional: cuando ocurre un shock, \(\sigma_t^2\) aumenta y el VaR sube automáticamente, alertando al equipo de riesgo en tiempo real.

El VaR histórico rolling reacciona con rezago proporcional al tamaño de la ventana (250 días en este caso), lo que puede llevar a subestimar el riesgo al inicio de una crisis. El VaR paramétrico fijo es el menos sensible y puede quedar totalmente desconectado del mercado durante períodos prolongados de alta volatilidad.

Veredicto: el GARCH es el único modelo que adapta el VaR día a día sin depender de una ventana fija.


15.3 Interpretabilidad

Reconocemos que el VaR GARCH es el modelo más técnico del grupo y requiere conocimientos de econometría de series de tiempo para su estimación e interpretación. Sin embargo, su resultado final es igualmente comunicable: “con un 95% de confianza, la pérdida diaria de la posición no superará X millones de pesos”.

El VaR histórico es el más intuitivo y fácil de explicar a directivos no técnicos: simplemente ordena las pérdidas pasadas y toma el percentil 95. El VaR paramétrico es sencillo de explicar bajo el supuesto de normalidad, pero ese supuesto es empíricamente incorrecto para todos los activos del portafolio, lo que debilita su credibilidad.

Veredicto: para comunicación interna con áreas técnicas → GARCH; para reportes ejecutivos → complementar con VaR histórico como cifra de referencia.


15.4 Estabilidad

El VaR GARCH puede ser volátil en días de shocks extremos, ya que reacciona fuertemente a movimientos de mercado inusuales. Esto puede generar señales de alerta frecuentes en períodos de alta turbulencia, lo que exige disciplina en su interpretación.

El VaR histórico es más estable ya que suaviza los cambios a través de la ventana rolling, pero esta estabilidad tiene un costo: puede ser lento para detectar el inicio de un período de riesgo elevado. El VaR paramétrico es el más estable de los tres, pero esa estabilidad proviene de ignorar la dinámica del mercado, no de una mejor estimación del riesgo.

Veredicto: la volatilidad del VaR GARCH es una característica, no un defecto: refleja la realidad del mercado y debe interpretarse como tal.


15.5 Severidad de pérdidas extremas

El VaR por sí solo no responde cuánto se pierde cuando se supera el umbral. Por eso recomendamos complementarlo con el Expected Shortfall (ES), que mide el promedio de las pérdidas en la cola de la distribución.

Los resultados muestran que XOM presenta el ES más alto del grupo, con pérdidas esperadas en la cola significativamente superiores a las de JNJ. Esto es consistente con la naturaleza de cada sector: el energético está expuesto a shocks extremos del precio del petróleo, mientras que el sector salud tiene flujos de ingresos más predecibles. El ES histórico es nuestra medida complementaria recomendada precisamente porque no impone supuestos distribucionales y captura directamente la severidad de los eventos extremos observados.

Veredicto: VaR GARCH para la gestión diaria del riesgo + ES histórico para el análisis de escenarios extremos y stress testing.


15.6 Facilidad de comunicación

Proponemos el siguiente esquema de comunicación por nivel de audiencia:

Audiencia Medida recomendada Razón
Comité de riesgos / Junta directiva VaR histórico Intuitivo, sin supuestos técnicos
Equipo de trading / Mesa de riesgos VaR GARCH Dinámico y preciso para decisiones diarias
Reguladores (Basilea III) Expected Shortfall Estándar regulatorio desde Basilea IV
Auditoría interna VaR paramétrico + backtesting Reproducible y auditable con fórmula cerrada

15.7 Conclusión y decisión final

Nuestra decisión como equipo de riesgo es la siguiente:

Medida principal: VaR GARCH(1,1) t-Student al 95%, calculado de forma diaria con actualización automática de la volatilidad condicional.

Medida complementaria: Expected Shortfall histórico al 95%, reportado semanalmente para capturar la severidad de las pérdidas en la cola.

Medida de comunicación ejecutiva: VaR histórico rolling (ventana 250 días), presentado en los reportes mensuales al comité directivo por su transparencia e intuitividad.

Esta arquitectura de medidas cubre todos los frentes: precisión técnica para la gestión diaria, robustez en la captura de eventos extremos, y claridad comunicativa para los distintos niveles de la organización. Las diferencias observadas entre activos refuerzan la necesidad de calcular el VaR individualmente: XOM requiere reservas de capital significativamente mayores que JNJ, y un modelo de volatilidad constante como el paramétrico normal estaría sistemáticamente subestimando el riesgo en el sector energético. ```