library(tidyverse)
library(quantmod)
library(tidyquant)
library(forecast)
library(scales)
library(zoo)
library(rugarch)
library(slider)
library(moments)
library(FinTS)
library(knitr)
tickers <- c("MSFT", "JPM", "XOM", "JNJ")
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 |
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
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()
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()
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 (%)")
| 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 |
\[VaR_{\alpha}^{hist} = Q_{1-\alpha}(L_t)\]
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")
| activo | VaR_pct | VaR_COP |
|---|---|---|
| JNJ | 1.7227 | 1722712 |
| JPM | 2.8657 | 2865699 |
| MSFT | 2.8069 | 2806928 |
| XOM | 3.1793 | 3179349 |
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()
\[VaR_{\alpha}^{norm} = -(\mu_t + z_{\alpha}\sigma_t)\]
donde \(z_{\alpha}\) es el cuantil de la normal estándar para el nivel \(\alpha\).
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")
| activo | VaR_pct | VaR_COP |
|---|---|---|
| JNJ | 1.9768 | 1976815 |
| JPM | 3.1478 | 3147848 |
| MSFT | 3.0135 | 3013536 |
| XOM | 3.3234 | 3323436 |
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()
\[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
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")
| 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 |
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()
\[ES_{\alpha} = E[L \mid L > VaR_{\alpha}]\]
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")
| 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 |
\[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")
| activo | ES_norm | ES_COP |
|---|---|---|
| JNJ | 2.4891 | 2489086 |
| JPM | 3.9620 | 3962040 |
| MSFT | 3.7954 | 3795431 |
| XOM | 4.1839 | 4183851 |
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")
| Activo | ES_GARCH_pct | ES_GARCH_COP |
|---|---|---|
| MSFT | 4.9868 | 4986842 |
| JPM | 4.9156 | 4915624 |
| XOM | 4.7030 | 4702985 |
| JNJ | 3.2861 | 3286131 |
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 (%)")
| 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()
\[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)
)
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%)")
| 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 |
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()
\[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")
| 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 |
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 (%)")
| 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 |
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.
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.
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.
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.
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.
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.
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 |
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. ```