Este documento integra el desarrollo completo del trabajo final de Instrumentos Financieros Derivados. En la primera parte se construye y analiza un portafolio óptimo de inversión con acciones estadounidenses y se evalúa su cobertura mediante futuros de índice bursátil. En la segunda parte se estudia el financiamiento de una compra de maquinaria amarilla mediante crédito en dólares y la cobertura cambiaria con forwards USD/COP.
Desde el punto de vista metodológico, se mantiene un año comercial de 252 días para los cálculos financieros cuando aplica, se conservan los supuestos definidos en cada parte y se presentan las fuentes bajo normas APA. Las tablas, gráficos y salidas generadas por los códigos deben interpretarse como evidencia empírica del proceso de valoración, simulación y cobertura.
library(quantmod)
library(xts)
library(nloptr)
library(timeDate)
library(DT)
library(htmltools)
library(scales)
library(magrittr)
El presente trabajo desarrolla la primera parte del ejercicio final de Instrumentos Financieros Derivados, enfocada en la aplicación de una cobertura con futuros de índice bursátil. Se construye un portafolio de inversión con tres acciones estadounidenses pertenecientes al S&P 500: The Coca-Cola Company (KO), McDonald’s Corporation (MCD) y Johnson & Johnson (JNJ). Posteriormente, se estima el riesgo del portafolio, se calcula su beta frente al S&P 500 y se determina el número óptimo de contratos de futuros E-mini S&P 500 necesarios para cubrir la exposición sistemática.
La metodología sigue una lógica de administración del riesgo: primero se seleccionan y analizan los activos; luego se construye el portafolio óptimo; después se estima su riesgo mediante simulaciones y VaR; y finalmente se evalúa una estrategia de cobertura con futuros, incluyendo margin call, roll-over trimestral y escenarios alternativos de beta.
Para el portafolio se seleccionaron tres acciones del mercado estadounidense: The Coca-Cola Company (KO), McDonald’s Corporation (MCD) y Johnson & Johnson (JNJ). La elección busca combinar compañías grandes, líquidas y maduras, con negocios defensivos y relativamente estables dentro del S&P 500. KO aporta exposición al consumo básico global; MCD representa consumo con un modelo de franquicias de alta generación de caja; y JNJ introduce exposición al sector salud. Esta combinación es útil porque evita depender de una sola industria, mantiene un perfil de rentabilidad más razonable y permite que el portafolio tenga sentido económico sin recurrir a activos de crecimiento excesivo que puedan inflar la simulación.
The Coca-Cola Company es una empresa global de bebidas, con marcas reconocidas, distribución internacional y una posición competitiva sólida dentro del sector de consumo básico. Su negocio se caracteriza por demanda relativamente estable, alto reconocimiento de marca y capacidad para operar en diferentes regiones y ciclos económicos.
Desde el punto de vista fundamental, KO puede considerarse una acción defensiva. Su atractivo está en la estabilidad de sus ventas, la diversificación geográfica, la fortaleza de marca y la generación recurrente de flujo de caja. Sus principales riesgos están asociados a cambios en preferencias de consumo, presión por productos más saludables, costos de insumos, tipo de cambio y competencia en bebidas.
Para efectos del portafolio, KO aporta estabilidad y menor sensibilidad al ciclo económico. Aunque no se espera un crecimiento explosivo, su perfil defensivo puede ayudar a moderar la volatilidad total del portafolio y a generar una proyección más razonable en la simulación.
McDonald’s Corporation es una compañía global de restaurantes de comida rápida con un modelo de negocio principalmente basado en franquicias. Este modelo le permite mantener márgenes altos, recibir ingresos recurrentes por regalías y reducir la necesidad de inversión directa frente a operadores tradicionales de restaurantes.
Desde el punto de vista fundamental, MCD combina estabilidad con crecimiento moderado. Su fortaleza está en la marca, la escala global, la eficiencia operativa, la capacidad de fijar precios y la generación constante de caja. Sus principales riesgos están relacionados con inflación de costos, salarios, sensibilidad del consumidor a precios altos, competencia en comida rápida y cambios en hábitos de consumo.
Dentro del portafolio, MCD aporta exposición a consumo y servicios, pero con un modelo más resiliente que muchas empresas cíclicas. Esto la diferencia de KO y JNJ, y permite incorporar una fuente de retorno asociada a franquicias, expansión internacional y eficiencia operativa.
Johnson & Johnson es una empresa del sector salud con operaciones en medicamentos, tecnología médica y productos relacionados con bienestar y cuidado de la salud. Su desempeño está asociado a innovación farmacéutica, demanda de servicios médicos, dispositivos de salud, regulación sanitaria y capacidad de mantener un portafolio diversificado de productos.
Desde el punto de vista fundamental, JNJ aporta estabilidad, diversificación interna y exposición a un sector menos dependiente del ciclo económico. Puede beneficiarse de tendencias de largo plazo como envejecimiento poblacional, innovación médica y demanda constante de productos de salud. Sus principales riesgos están asociados a regulación, litigios, vencimiento de patentes, competencia farmacéutica y presión sobre precios en sistemas de salud.
En el portafolio, JNJ cumple un papel de diversificación defensiva. Su comportamiento puede diferir del consumo básico y de restaurantes, por lo que permite construir una combinación más balanceada para evaluar posteriormente la cobertura con futuros del S&P 500.
Resumidamente La combinación de KO, MCD y JNJ permite construir un portafolio con tres fuentes de riesgo diferentes pero moderadas: consumo básico, restaurantes/franquicias y salud. Esto tiene mayor sentido económico para el trabajo porque evita retornos históricos muy altos que puedan generar valores simulados irreales. Además, las tres compañías pertenecen al S&P 500, por lo que es coherente usar ese índice como referencia de mercado para calcular betas y como subyacente de la cobertura con futuros.
tickers <- c("KO", "MCD", "JNJ")
start_date <- as.Date("2020-09-30")
end_date <- as.Date("2026-03-31")
getSymbols(tickers, src = "yahoo", from = start_date, to = end_date, auto.assign = TRUE)
## [1] "KO" "MCD" "JNJ"
prices <- na.omit(merge(Ad(KO), Ad(MCD), Ad(JNJ)))
colnames(prices) <- tickers
returns <- na.omit(diff(log(prices)))
mean_returns <- colMeans(returns)
varianza <- apply(returns, 2, var)
Covarianza <- cov(returns)
Cor <- cor(returns)
desvest <- apply(returns, 2, sd)
stats <- data.frame(
Activo = tickers,
Retorno_Anual_Pct = as.numeric(mean_returns * 252 * 100),
Varianza_Anual_Pct = as.numeric(varianza * 252 * 100),
Desv_Estandar_Anual_Pct = as.numeric(desvest * sqrt(252) * 100)
)
stats_presentacion <- stats
stats_presentacion[, sapply(stats_presentacion, is.numeric)] <-
round(stats_presentacion[, sapply(stats_presentacion, is.numeric)], 4)
DT::datatable(
stats_presentacion,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 1. Estadísticos anuales de las acciones"
),
options = list(dom = "t")
) %>%
formatRound(columns = c("Retorno_Anual_Pct", "Varianza_Anual_Pct", "Desv_Estandar_Anual_Pct"), digits = 4)
cor_presentacion <- round(Cor, 4)
cov_presentacion <- round(Covarianza * 252, 6)
DT::datatable(
cor_presentacion,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 2. Matriz de correlaciones"
),
options = list(dom = "t")
)
DT::datatable(
cov_presentacion,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 3. Matriz anualizada de varianzas y covarianzas"
),
options = list(dom = "t")
)
Análisis Los retornos logarítmicos permiten trabajar con variaciones continuas de precios, lo cual es necesario para trabajar la simulación mediante Movimiento Browniano Geométrico. La media anualizada representa el retorno esperado histórico de cada acción, mientras que la desviación estándar anualizada mide el riesgo individual. Se observa que los 3 activos tienen retornos anuales de: KO: 10,96%, MCD:8,51% y JNJ:11,73% con sus respectivas desviaciones de 16,27%, 17,15% y 16,79%. Antes de haber visto el portafolio óptimo, se puede esperar que los 3 activos van a funcionar para que a cada uno se le asigne un peso en el portafolio óptimo debido a sus retornos y desviaciones similares, esperando una menor participación de MCD debido a que tiene el retorno más bajo y su desviación es más alta a pesar de no ser sigificativa la diferencia.
La matriz de covarianzas resume cómo se mueven conjuntamente los activos y es necesaria para calcular el riesgo del portafolio.
La diversificación depende de que las correlaciones no sean perfectas. En este caso, KO, MCD y JNJ tienen fuentes de riesgo diferentes: KO depende principalmente del consumo básico y de la fortaleza de sus marcas; MCD está asociada al consumo en restaurantes, franquicias y eficiencia operativa; y JNJ pertenece al sector salud, con una dinámica menos ligada al consumo discrecional. Por tanto, la combinación de los tres activos permite construir un portafolio defensivo, con sentido económico y con una diversificación sectorial clara. Matemáticamente esto se ve reflejado gracias a las correlaciones bajas entre estos activos: KO/MCD=0,51. MCD/JNJ=0,34. JNJ/KO=0,45. En conclusión, la selección de activos fue eficiente si nos basamos en la diversificación.
RF <- 0.039845
mu <- as.numeric(mean_returns) * 252
Sigma <- as.matrix(Covarianza)
n <- length(mu)
neg_sharpe <- function(w, mu, Sigma, RF) {
Er <- sum(w * mu)
sigma <- sqrt(t(w) %*% (Sigma * 252) %*% w)
return(-as.numeric((Er - RF) / sigma))
}
eval_g_eq <- function(w, mu, Sigma, RF) {
sum(w) - 1
}
lb <- rep(0, n)
ub <- rep(1, n)
w0 <- rep(1/n, n)
res <- slsqp(
x0 = w0,
fn = neg_sharpe,
gr = NULL,
lower = lb,
upper = ub,
hin = NULL,
heq = eval_g_eq,
mu = mu,
Sigma = Sigma,
RF = RF
)
w <- res$par
names(w) <- colnames(prices)
E_r_sharpe <- sum(w * mu)
sigma_sharpe <- as.numeric(sqrt(t(w) %*% (Sigma * 252) %*% w))
SR_sharpe <- (E_r_sharpe - RF) / sigma_sharpe
inversion_inicial <- 20000000
tabla_portafolio_optimo <- data.frame(
Activo = names(w),
Peso = as.numeric(w),
Inversion_USD = as.numeric(w * inversion_inicial)
)
tabla_metricas_portafolio <- data.frame(
Metrica = c("Retorno esperado anual", "Desviación estándar anual", "Sharpe Ratio", "Tasa libre de riesgo"),
Valor = c(E_r_sharpe, sigma_sharpe, SR_sharpe, RF)
)
tabla_portafolio_optimo_presentacion <- tabla_portafolio_optimo
tabla_portafolio_optimo_presentacion$Peso <- round(tabla_portafolio_optimo_presentacion$Peso, 4)
tabla_portafolio_optimo_presentacion$Inversion_USD <- round(tabla_portafolio_optimo_presentacion$Inversion_USD, 2)
DT::datatable(
tabla_portafolio_optimo_presentacion,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 4. Pesos óptimos e inversión por activo"
),
options = list(dom = "t")
) %>%
formatPercentage("Peso", digits = 2) %>%
formatCurrency("Inversion_USD", currency = "USD ", digits = 2)
DT::datatable(
tabla_metricas_portafolio,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 5. Métricas del portafolio óptimo"
),
options = list(dom = "t")
) %>%
formatRound("Valor", digits = 4)
Análisis Se maximizó la razón de Sharpe para buscar la mejor relación entre retorno y riesgo. Como era de esperarse y se mencionó anteriormente, MCD fue la que menos peso recibió (1,18%), mientras que KO y JNJ recibieron pesos similares (45,03% y 53,79% respectivamente) gracias a que los parámetros mu y sigma son similares en ambas.
fecha_inicio_sim <- as.Date("2026-03-30")
fecha_fin_sim <- as.Date("2030-03-15")
fechas <- seq(fecha_inicio_sim, fecha_fin_sim, by = "day")
fechas_habiles <- fechas[weekdays(fechas) %in% c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "lunes", "martes", "miércoles", "jueves", "viernes")]
festivos <- as.Date(holidayNYSE(2026:2030))
fechas_sim <- fechas_habiles[!fechas_habiles %in% festivos]
T <- length(fechas_sim)
N <- 10000
dt <- 1/252
mu_anual <- colMeans(returns) * 252
sigma_anual <- apply(returns, 2, sd) * sqrt(252)
S0 <- as.numeric(prices["2026-03-30", ])
names(S0) <- colnames(prices)
if (any(is.na(S0))) {
S0 <- as.numeric(last(prices[index(prices) <= fecha_inicio_sim, ]))
names(S0) <- colnames(prices)
}
simular_mbg_fast <- function(S0, mu, sigma, T, N) {
Z <- matrix(rnorm((T - 1) * N), nrow = T - 1, ncol = N)
increments <- (mu - 0.5 * sigma^2) * (1 / 252) + sigma * sqrt(1 / 252) * Z
log_paths <- apply(increments, 2, cumsum)
log_paths <- rbind(rep(0, N), log_paths)
S <- S0 * exp(log_paths)
return(S)
}
mb_KO <- simular_mbg_fast(S0 = S0["KO"], mu = mu_anual["KO"], sigma = sigma_anual["KO"], T = T, N = N)
mb_MCD <- simular_mbg_fast(S0 = S0["MCD"], mu = mu_anual["MCD"], sigma = sigma_anual["MCD"], T = T, N = N)
mb_JNJ <- simular_mbg_fast(S0 = S0["JNJ"], mu = mu_anual["JNJ"], sigma = sigma_anual["JNJ"], T = T, N = N)
ST_KO <- mb_KO[T, ]
ST_MCD <- mb_MCD[T, ]
ST_JNJ <- mb_JNJ[T, ]
resumen_final <- data.frame(
Activo = c("KO", "MCD", "JNJ"),
S0 = c(S0["KO"], S0["MCD"], S0["JNJ"]),
Mu_Anual = c(mu_anual["KO"], mu_anual["MCD"], mu_anual["JNJ"]),
Sigma_Anual = c(sigma_anual["KO"], sigma_anual["MCD"], sigma_anual["JNJ"]),
Precio_Final_Promedio = c(mean(ST_KO), mean(ST_MCD), mean(ST_JNJ)),
Precio_Final_Mediana = c(median(ST_KO), median(ST_MCD), median(ST_JNJ)),
Percentil_2_5 = c(quantile(ST_KO, 0.025), quantile(ST_MCD, 0.025), quantile(ST_JNJ, 0.025)),
Percentil_97_5 = c(quantile(ST_KO, 0.975), quantile(ST_MCD, 0.975), quantile(ST_JNJ, 0.975))
)
resumen_final_presentacion <- resumen_final
resumen_final_presentacion[, sapply(resumen_final_presentacion, is.numeric)] <-
round(resumen_final_presentacion[, sapply(resumen_final_presentacion, is.numeric)], 4)
DT::datatable(
resumen_final_presentacion,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 6. Resumen de precios finales simulados"
),
options = list(pageLength = 5, dom = "t")
)
muestra <- sample(1:N, 100)
matplot(fechas_sim, mb_KO[, muestra], type = "l", lty = 1, col = "gray70",
main = "Simulación MBG - Coca-Cola (KO)", xlab = "Fecha", ylab = "Precio simulado")
lines(fechas_sim, rowMeans(mb_KO), col = "blue3", lwd = 3)
matplot(fechas_sim, mb_MCD[, muestra], type = "l", lty = 1, col = "gray70",
main = "Simulación MBG - McDonald’s (MCD)", xlab = "Fecha", ylab = "Precio simulado")
lines(fechas_sim, rowMeans(mb_MCD), col = "blue3", lwd = 3)
matplot(fechas_sim, mb_JNJ[, muestra], type = "l", lty = 1, col = "gray70",
main = "Simulación MBG - Johnson & Johnson (JNJ)", xlab = "Fecha", ylab = "Precio simulado")
lines(fechas_sim, rowMeans(mb_JNJ), col = "blue3", lwd = 3)
Análisis La simulación se realizó mediante Movimiento Browniano Geométrico para cada acción. Para cada activo se tomó el precio inicial del 30 de marzo de 2026, junto con su media y volatilidad anuales. Se simularon 10.000 trayectorias hasta el 15 de marzo de 2030, usando 252 días bursátiles por año.
La idea de simular estos precios es obtener posibles escenarios de precios futuros y tomar trayectorias cercanas a los percentiles 2,5% y 97,5% y al precio ST promedio.
En la tabla vemos los resultados principales, no se evidencian extremos que puedan afectar la prueba de cobertura más adelante, por lo que las simulaciones en primera instancia se ven coherentes.
rend_KO_sim <- mb_KO[-1, ] / mb_KO[-nrow(mb_KO), ] - 1
rend_MCD_sim <- mb_MCD[-1, ] / mb_MCD[-nrow(mb_MCD), ] - 1
rend_JNJ_sim <- mb_JNJ[-1, ] / mb_JNJ[-nrow(mb_JNJ), ] - 1
R_port_diario_sim <- w["KO"] * rend_KO_sim + w["MCD"] * rend_MCD_sim + w["JNJ"] * rend_JNJ_sim
PG_portafolio_sim <- inversion_inicial * as.vector(R_port_diario_sim)
percentil_5 <- quantile(PG_portafolio_sim, 0.05, type = 6, na.rm = TRUE)
percentil_1 <- quantile(PG_portafolio_sim, 0.01, type = 6, na.rm = TRUE)
VaR_5 <- max(0, -percentil_5)
VaR_1 <- max(0, -percentil_1)
tabla_var <- data.frame(
Medida = c("VaR diario al 5%", "VaR diario al 1%"),
Percentil_PG = c(percentil_5, percentil_1),
Valor_Cobertura_USD = c(VaR_5, VaR_1),
Interpretacion = c(
"Pérdida diaria superada en aproximadamente 5% de escenarios simulados.",
"Pérdida diaria superada en aproximadamente 1% de escenarios simulados."
)
)
tabla_var_presentacion <- tabla_var
tabla_var_presentacion[, sapply(tabla_var_presentacion, is.numeric)] <-
round(tabla_var_presentacion[, sapply(tabla_var_presentacion, is.numeric)], 2)
DT::datatable(
tabla_var_presentacion,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 7. VaR diario del portafolio"
),
options = list(dom = "t")
) %>%
formatCurrency(c("Percentil_PG", "Valor_Cobertura_USD"), currency = "USD ", digits = 2)
hist(PG_portafolio_sim, breaks = 100, main = "Distribución simulada de P/G diaria del portafolio", xlab = "Pérdida / ganancia diaria en USD", col = "gray85", border = "white")
abline(v = percentil_5, col = "red3", lwd = 3, lty = 2)
abline(v = percentil_1, col = "darkred", lwd = 3, lty = 2)
legend("topright", legend = c(paste("VaR 5%:", dollar(VaR_5)), paste("VaR 1%:", dollar(VaR_1))), col = c("red3", "darkred"), lwd = 3, lty = 2, bty = "n")
Análisis El VaR diario se estimó a partir de la distribución simulada de pérdidas y ganancias del portafolio. El VaR al 5% del portafolio es de 231.530, este valor se puede describir como la pérdida que se supera solamente en un 5% de los casos. EL VaR del 99% es de 329.902. Se interpreta igual que el VaR al 5%, pero con la diferencia de que esta es la mayor pérdida en el 1% de los casos. Vemos que no son muy alejados y esto es gracias a la diversificación y poca volatilidad de los activos escogidos.
Con esto se puede dimensionar el riesgo que se busca gestionar o mitigar con la cobertura con futuros.
getSymbols("^GSPC", src = "yahoo", from = start_date, to = end_date, auto.assign = TRUE)
## [1] "GSPC"
sp500 <- Ad(GSPC)
colnames(sp500) <- "SP500"
data_beta <- na.omit(merge(prices, sp500))
ret_beta <- na.omit(diff(log(data_beta)))
ret_acciones <- ret_beta[, c("KO", "MCD", "JNJ")]
ret_mercado <- ret_beta[, "SP500"]
beta_cov <- sapply(colnames(ret_acciones), function(activo) {
cov(as.numeric(ret_acciones[, activo]), as.numeric(ret_mercado)) / var(as.numeric(ret_mercado))
})
beta_reg <- sapply(colnames(ret_acciones), function(activo) {
modelo <- lm(as.numeric(ret_acciones[, activo]) ~ as.numeric(ret_mercado))
coef(modelo)[2]
})
beta_portafolio <- sum(w[names(beta_cov)] * beta_cov)
tabla_betas <- data.frame(
Activo = names(beta_cov),
Peso_Portafolio = as.numeric(w[names(beta_cov)]),
Beta_Covarianza = as.numeric(beta_cov),
Beta_Regresion = as.numeric(beta_reg),
Contribucion_Beta = as.numeric(w[names(beta_cov)] * beta_cov)
)
tabla_betas_presentacion <- tabla_betas
tabla_betas_presentacion[, sapply(tabla_betas_presentacion, is.numeric)] <-
round(tabla_betas_presentacion[, sapply(tabla_betas_presentacion, is.numeric)], 4)
tabla_beta_portafolio <- data.frame(
Metrica = "Beta del portafolio",
Valor = round(beta_portafolio, 4)
)
DT::datatable(
tabla_betas_presentacion,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 8. Betas CAPM individuales"
),
options = list(dom = "t")
)
DT::datatable(
tabla_beta_portafolio,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 9. Beta ponderada del portafolio"
),
options = list(dom = "t")
)
Análisis Las betas se calcularon usando el enfoque CAPM, tomando el S&P 500 como proxy del mercado, dado que los activos seleccionados cotizan en Estados Unidos. La beta mide la sensibilidad de cada acción frente a los movimientos del mercado. Una beta mayor a 1 indica que el activo se mueve más que el mercado, mientras que una menor a uno indica que el activo se mueve menos que el mercado. En este caso, las 3 betas son inferiores a uno, por lo cual los 3 activos son menos riesgosos que el mercado.
La beta del portafolio se obtuvo como el promedio ponderado de las betas individuales usando los pesos óptimos obtenidos anteriormente. Obtuvimos un beta de 0,2801, lo cual significa que el portafolio es menos riesgoso y se mueve menos que el mercado. Este valor es fundamental para la cobertura, porque sirve para determinar el número de contratos óptimos en corto en un futuro del índice para cubrir el portafolio.
V_portafolio <- inversion_inicial
beta_p <- beta_portafolio
ticker_bloomberg <- "ESM6 Index"
multiplicador <- 50
F0 <- 6388.25
valor_contrato <- F0 * multiplicador
N_contratos_teorico <- beta_p * V_portafolio / valor_contrato
N_contratos_redondeado <- round(N_contratos_teorico, 0)
tabla_contratos <- data.frame(
Contrato = "E-mini S&P 500 Futures",
Ticker_Bloomberg = ticker_bloomberg,
Beta_Portafolio = beta_p,
Valor_Portafolio_USD = V_portafolio,
Precio_Futuro = F0,
Multiplicador = multiplicador,
Valor_Contrato_USD = valor_contrato,
Contratos_Teoricos = N_contratos_teorico,
Contratos_Redondeados = N_contratos_redondeado,
Posicion = "Corta"
)
tabla_contratos_presentacion <- tabla_contratos
tabla_contratos_presentacion[, sapply(tabla_contratos_presentacion, is.numeric)] <-
round(tabla_contratos_presentacion[, sapply(tabla_contratos_presentacion, is.numeric)], 4)
DT::datatable(
tabla_contratos_presentacion,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 10. Número óptimo de contratos de futuros"
),
options = list(scrollX = TRUE, dom = "t")
) %>%
formatCurrency(c("Valor_Portafolio_USD", "Valor_Contrato_USD"), currency = "USD ", digits = 2)
Análisis El número óptimo de contratos se calculó con base en la beta del portafolio, el valor total invertido y el valor de un contrato del futuro. Como el portafolio está comprado en acciones, la cobertura natural es una posición corta en futuros del S&P 500. Si el mercado cae, el portafolio pierde valor, pero la posición corta en futuros genera ganancias que compensan esa pérdida parcialmente.
El precio del futuro el 30 de marzo es de 6388,25, y su multiplicador sacado de bloomberg es de 50 por valor del índice, por lo que el valor por contrato es de 319.412,50.
Ese resultado teórico debe ser redondeado, y el número de contratos óptimo obtenido es de 18.
En conclusión, para cubrir el portafolio con las 3 acciones se debe tomar una posición corta en 18 contratos del futuro E-mini del S&P 500.
contratos <- abs(N_contratos_redondeado)
if (contratos == 0) contratos <- 1
margen_inicial_por_contrato <- 24934
margen_mantenimiento_por_contrato <- 22668
margen_inicial_total <- contratos * margen_inicial_por_contrato
margen_mantenimiento_total <- contratos * margen_mantenimiento_por_contrato
fechas_trimestrales <- seq(fecha_inicio_sim, fecha_fin_sim, by = "3 months")
idx_roll <- sapply(fechas_trimestrales, function(f) which.min(abs(fechas_sim - f)))
idx_roll <- unique(idx_roll)
fechas_roll <- fechas_sim[idx_roll]
ret_futuro_proxy <- as.numeric(ret_mercado)
mu_futuro_anual <- mean(ret_futuro_proxy, na.rm = TRUE) * 252
sigma_futuro_anual <- sd(ret_futuro_proxy, na.rm = TRUE) * sqrt(252)
simular_mbg_futuro <- function(F0, mu, sigma, T, N) {
Z <- matrix(rnorm((T - 1) * N), nrow = T - 1, ncol = N)
increments <- (mu - 0.5 * sigma^2) * (1 / 252) + sigma * sqrt(1 / 252) * Z
log_paths <- apply(increments, 2, cumsum)
log_paths <- rbind(rep(0, N), log_paths)
F <- F0 * exp(log_paths)
return(F)
}
N_futuro <- 5000
mb_futuro <- simular_mbg_futuro(F0 = F0, mu = mu_futuro_anual, sigma = sigma_futuro_anual, T = T, N = N_futuro)
tabla_parametros_futuro <- data.frame(
Concepto = c("Contrato", "Ticker Bloomberg", "Precio inicial", "Multiplicador", "Contratos", "Margen inicial por contrato", "Margen mantenimiento por contrato", "Margen inicial total", "Margen mantenimiento total"),
Valor = c("E-mini S&P 500 Futures", ticker_bloomberg, F0, multiplicador, contratos, margen_inicial_por_contrato, margen_mantenimiento_por_contrato, margen_inicial_total, margen_mantenimiento_total)
)
DT::datatable(
tabla_parametros_futuro,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 11. Parámetros del futuro y márgenes"
),
options = list(dom = "t")
)
Análisis El precio inicial, el multiplicador y los márgenes se toman como datos del contrato E-mini S&P 500. Para simular el futuro se usaron la media y la desviación del S&P 500 como proxy, dado que este es el subyacente del contrato. El roll-over se evalúa de manera trimestral para mantener la cobertura durante los 4 años de la inversión.
El margen inicial corresponde al depósito requerido para abrir la posición en el contrato de futuros. En este caso es de 24.394 según Bloomberg. El margen de mantenimiento representa el saldo mínimo que debe conservarse para mantener la posición. Si el saldo cae por debajo de ese margen, se activa un margin call y se debe depositar dinero adicional para regresar al margen inicial. El margen de mantenimiento para este contrato es de 22.668 según Bloomberg.
valor_KO_sim <- w["KO"] * inversion_inicial * (mb_KO / S0["KO"])
valor_MCD_sim <- w["MCD"] * inversion_inicial * (mb_MCD / S0["MCD"])
valor_JNJ_sim <- w["JNJ"] * inversion_inicial * (mb_JNJ / S0["JNJ"])
valor_portafolio_sim <- valor_KO_sim + valor_MCD_sim + valor_JNJ_sim
VT_portafolio <- valor_portafolio_sim[nrow(valor_portafolio_sim), ]
VT_promedio <- mean(VT_portafolio, na.rm = TRUE)
VT_pesimista <- quantile(VT_portafolio, 0.025, na.rm = TRUE)
VT_optimista <- quantile(VT_portafolio, 0.975, na.rm = TRUE)
idx_port_promedio <- which.min(abs(VT_portafolio - VT_promedio))
idx_port_pesimista <- which.min(abs(VT_portafolio - VT_pesimista))
idx_port_optimista <- which.min(abs(VT_portafolio - VT_optimista))
trayectoria_port_promedio <- valor_portafolio_sim[, idx_port_promedio]
trayectoria_port_pesimista <- valor_portafolio_sim[, idx_port_pesimista]
trayectoria_port_optimista <- valor_portafolio_sim[, idx_port_optimista]
port_promedio_roll <- trayectoria_port_promedio[idx_roll]
port_pesimista_roll <- trayectoria_port_pesimista[idx_roll]
port_optimista_roll <- trayectoria_port_optimista[idx_roll]
# ----------------
# Tabla resumen con rentabilidades
# ----------------
anios_simulados <- length(fechas_sim) / 252
valor_final_pesimista <- tail(trayectoria_port_pesimista, 1)
valor_final_promedio <- tail(trayectoria_port_promedio, 1)
valor_final_optimista <- tail(trayectoria_port_optimista, 1)
rent_total_pesimista <- (valor_final_pesimista / inversion_inicial) - 1
rent_total_promedio <- (valor_final_promedio / inversion_inicial) - 1
rent_total_optimista <- (valor_final_optimista / inversion_inicial) - 1
rent_anual_pesimista <- (valor_final_pesimista / inversion_inicial)^(1 / anios_simulados) - 1
rent_anual_promedio <- (valor_final_promedio / inversion_inicial)^(1 / anios_simulados) - 1
rent_anual_optimista <- (valor_final_optimista / inversion_inicial)^(1 / anios_simulados) - 1
tabla_trayectorias_portafolio <- data.frame(
Escenario = c("Pesimista", "Promedio", "Optimista"),
Criterio = c("Percentil 2.5%", "Media", "Percentil 97.5%"),
Simulacion_Seleccionada = c(
idx_port_pesimista,
idx_port_promedio,
idx_port_optimista
),
Valor_Final_Referencia = c(
VT_pesimista,
VT_promedio,
VT_optimista
),
Valor_Final_Seleccionado = c(
valor_final_pesimista,
valor_final_promedio,
valor_final_optimista
),
Rentabilidad_Total = c(
rent_total_pesimista,
rent_total_promedio,
rent_total_optimista
),
Rentabilidad_Anual = c(
rent_anual_pesimista,
rent_anual_promedio,
rent_anual_optimista
)
)
tabla_trayectorias_portafolio_presentacion <- tabla_trayectorias_portafolio
tabla_trayectorias_portafolio_presentacion[, sapply(tabla_trayectorias_portafolio_presentacion, is.numeric)] <-
round(tabla_trayectorias_portafolio_presentacion[, sapply(tabla_trayectorias_portafolio_presentacion, is.numeric)], 4)
DT::datatable(
tabla_trayectorias_portafolio_presentacion,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 12. Trayectorias seleccionadas del portafolio"
),
options = list(dom = "t")
) %>%
formatCurrency(
columns = c("Valor_Final_Referencia", "Valor_Final_Seleccionado"),
currency = "USD ",
digits = 2
) %>%
formatPercentage(
columns = c("Rentabilidad_Total", "Rentabilidad_Anual"),
digits = 2
)
# ============================================================
# SELECCIÓN DE TRAYECTORIAS DEL FUTURO
# Pesimista, promedio y optimista moderado
# ============================================================
# ----------------
# 1) Precio final del futuro en cada simulación
# ----------------
ST_futuro <- mb_futuro[nrow(mb_futuro), ]
ST_promedio <- mean(ST_futuro, na.rm = TRUE)
ST_pesimista <- quantile(ST_futuro, 0.025, na.rm = TRUE)
# Optimista moderado:
# Antes usábamos 0.975, pero puede generar una trayectoria demasiado alta.
# Con 0.75 se mantiene un escenario optimista, pero más razonable.
p_optimista_futuro <- 0.75
ST_optimista <- quantile(ST_futuro, p_optimista_futuro, na.rm = TRUE)
# ----------------
# 2) Seleccionar la trayectoria más cercana a cada escenario
# ----------------
idx_fut_promedio <- which.min(abs(ST_futuro - ST_promedio))
idx_fut_pesimista <- which.min(abs(ST_futuro - ST_pesimista))
idx_fut_optimista <- which.min(abs(ST_futuro - ST_optimista))
trayectoria_fut_promedio <- mb_futuro[, idx_fut_promedio]
trayectoria_fut_pesimista <- mb_futuro[, idx_fut_pesimista]
trayectoria_fut_optimista <- mb_futuro[, idx_fut_optimista]
# ----------------
# 3) Precios del futuro en fechas trimestrales
# ----------------
fut_promedio_roll <- trayectoria_fut_promedio[idx_roll]
fut_pesimista_roll <- trayectoria_fut_pesimista[idx_roll]
fut_optimista_roll <- trayectoria_fut_optimista[idx_roll]
# ----------------
# 4) Rentabilidades del futuro
# ----------------
anios_simulados <- length(fechas_sim) / 252
precio_final_fut_pesimista <- tail(trayectoria_fut_pesimista, 1)
precio_final_fut_promedio <- tail(trayectoria_fut_promedio, 1)
precio_final_fut_optimista <- tail(trayectoria_fut_optimista, 1)
rent_total_fut_pesimista <- (precio_final_fut_pesimista / F0) - 1
rent_total_fut_promedio <- (precio_final_fut_promedio / F0) - 1
rent_total_fut_optimista <- (precio_final_fut_optimista / F0) - 1
rent_anual_fut_pesimista <- (precio_final_fut_pesimista / F0)^(1 / anios_simulados) - 1
rent_anual_fut_promedio <- (precio_final_fut_promedio / F0)^(1 / anios_simulados) - 1
rent_anual_fut_optimista <- (precio_final_fut_optimista / F0)^(1 / anios_simulados) - 1
# ----------------
# 5) Tabla resumen
# ----------------
tabla_trayectorias_futuro <- data.frame(
Escenario = c("Pesimista", "Promedio", "Optimista"),
Criterio = c(
"Percentil 2.5%",
"Media",
paste0("Percentil ", p_optimista_futuro * 100, "%")
),
Simulacion_Seleccionada = c(
idx_fut_pesimista,
idx_fut_promedio,
idx_fut_optimista
),
Precio_Inicial = c(
F0,
F0,
F0
),
Precio_Final_Referencia = c(
ST_pesimista,
ST_promedio,
ST_optimista
),
Precio_Final_Seleccionado = c(
precio_final_fut_pesimista,
precio_final_fut_promedio,
precio_final_fut_optimista
),
Rentabilidad_Total = c(
rent_total_fut_pesimista,
rent_total_fut_promedio,
rent_total_fut_optimista
),
Rentabilidad_Anual = c(
rent_anual_fut_pesimista,
rent_anual_fut_promedio,
rent_anual_fut_optimista
)
)
tabla_trayectorias_futuro_presentacion <- tabla_trayectorias_futuro
tabla_trayectorias_futuro_presentacion[, sapply(tabla_trayectorias_futuro_presentacion, is.numeric)] <-
round(tabla_trayectorias_futuro_presentacion[, sapply(tabla_trayectorias_futuro_presentacion, is.numeric)], 4)
DT::datatable(
tabla_trayectorias_futuro_presentacion,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 13. Trayectorias seleccionadas del futuro E-mini S&P 500"
),
options = list(dom = "t")
) %>%
formatCurrency(
columns = c(
"Precio_Inicial",
"Precio_Final_Referencia",
"Precio_Final_Seleccionado"
),
currency = "USD ",
digits = 2
) %>%
formatPercentage(
columns = c("Rentabilidad_Total", "Rentabilidad_Anual"),
digits = 2
)
ylim_port <- range(c(trayectoria_port_pesimista, trayectoria_port_promedio, trayectoria_port_optimista), na.rm = TRUE)
plot(fechas_sim, trayectoria_port_pesimista, type = "l", lwd = 3, lty = 2, col = "red3", ylim = ylim_port, main = "Trayectorias seleccionadas del portafolio", xlab = "Fecha", ylab = "Valor del portafolio en USD", yaxt = "n")
axis(2, at = pretty(ylim_port), labels = format(pretty(ylim_port), big.mark = ",", scientific = FALSE))
lines(fechas_sim, trayectoria_port_promedio, lwd = 3, lty = 1, col = "blue3")
lines(fechas_sim, trayectoria_port_optimista, lwd = 3, lty = 3, col = "darkgreen")
points(fechas_roll, port_pesimista_roll, pch = 17, col = "red3")
points(fechas_roll, port_promedio_roll, pch = 16, col = "blue3")
points(fechas_roll, port_optimista_roll, pch = 15, col = "darkgreen")
legend("topleft", legend = c("Pesimista 2.5%", "Promedio", "Optimista 97.5%"), col = c("red3", "blue3", "darkgreen"), lwd = 3, lty = c(2, 1, 3), pch = c(17, 16, 15), bty = "n")
ylim_fut <- range(c(trayectoria_fut_pesimista, trayectoria_fut_promedio, trayectoria_fut_optimista), na.rm = TRUE)
plot(fechas_sim, trayectoria_fut_pesimista, type = "l", lwd = 3, lty = 2, col = "red3", ylim = ylim_fut, main = "Trayectorias seleccionadas del futuro E-mini S&P 500", xlab = "Fecha", ylab = "Precio simulado del futuro", yaxt = "n")
axis(2, at = pretty(ylim_fut), labels = format(pretty(ylim_fut), big.mark = ",", scientific = FALSE))
lines(fechas_sim, trayectoria_fut_promedio, lwd = 3, lty = 1, col = "blue3")
lines(fechas_sim, trayectoria_fut_optimista, lwd = 3, lty = 3, col = "darkgreen")
abline(h = F0, lty = 3)
legend("topleft", legend = c("Pesimista 2.5%", "Promedio", "Optimista 75%", "F0"), col = c("red3", "blue3", "darkgreen", "black"), lwd = c(3, 3, 3, 1), lty = c(2, 1, 3, 3), bty = "n")
Análisis Para el análisis de cobertura se seleccionaron tres trayectorias representativas tanto del portafolio como del futuro: una pesimista, una promedio y una optimista. La trayectoria pesimista corresponde al percentil 2.5%, la promedio a la simulación más cercana al valor final medio, y la optimista al percentil 97.5%.
Esta selección nos permite evaluar 3 escenarios de tal manera que se pueda comparar la cobertura si ocurriera cada uno de esos escenarios.
Para el futuro del S&P 500 se seleccionaron tres trayectorias representativas. La trayectoria pesimista corresponde al percentil 2.5%, la trayectoria promedio corresponde al precio final medio y la trayectoria optimista se tomó como un escenario optimista un poco más moderado, usando el percentil 75%. Se decidió no usar el percentil 97.5% para el escenario optimista porque este representaba un caso demasiado extremo y podía generar una cobertura irrealista para el análisis. # 11. Margin call trimestral y roll-over del futuro
calcular_margin_futuro <- function(nombre_escenario, precios_futuro_roll, fechas_roll, contratos, multiplicador, margen_inicial_total, margen_mantenimiento_total) {
tabla <- data.frame(
Escenario_Futuro = nombre_escenario,
Fecha = fechas_roll,
Precio_Futuro = precios_futuro_roll
)
tabla$Dif_Precio <- c(0, diff(tabla$Precio_Futuro))
tabla$PG_LONG <- tabla$Dif_Precio * contratos * multiplicador
tabla$PG_SHORT <- -tabla$Dif_Precio * contratos * multiplicador
calcular_saldo_margen <- function(PG) {
n <- length(PG)
saldo_margen <- numeric(n)
margin_call <- numeric(n)
deposito_acumulado <- numeric(n)
saldo_margen[1] <- margen_inicial_total
margin_call[1] <- 0
deposito_acumulado[1] <- 0
for (i in 2:n) {
saldo_margen[i] <- saldo_margen[i - 1] + PG[i]
if (saldo_margen[i] < margen_mantenimiento_total) {
margin_call[i] <- margen_inicial_total - saldo_margen[i]
saldo_margen[i] <- saldo_margen[i] + margin_call[i]
} else {
margin_call[i] <- 0
}
deposito_acumulado[i] <- deposito_acumulado[i - 1] + margin_call[i]
}
return(data.frame(Saldo_Margen = saldo_margen, Margin_Call = margin_call, Deposito_Acumulado = deposito_acumulado))
}
mc_short <- calcular_saldo_margen(tabla$PG_SHORT)
mc_long <- calcular_saldo_margen(tabla$PG_LONG)
tabla$Saldo_Margen_SHORT <- mc_short$Saldo_Margen
tabla$Margin_Call_SHORT <- mc_short$Margin_Call
tabla$Deposito_Acumulado_SHORT <- mc_short$Deposito_Acumulado
tabla$Saldo_Margen_LONG <- mc_long$Saldo_Margen
tabla$Margin_Call_LONG <- mc_long$Margin_Call
tabla$Deposito_Acumulado_LONG <- mc_long$Deposito_Acumulado
tabla$PG_Acum_SHORT <- cumsum(tabla$PG_SHORT)
tabla$PG_Acum_LONG <- cumsum(tabla$PG_LONG)
return(tabla)
}
tabla_margin_fut_pesimista <- calcular_margin_futuro("Pesimista", fut_pesimista_roll, fechas_roll, contratos, multiplicador, margen_inicial_total, margen_mantenimiento_total)
tabla_margin_fut_promedio <- calcular_margin_futuro("Promedio", fut_promedio_roll, fechas_roll, contratos, multiplicador, margen_inicial_total, margen_mantenimiento_total)
tabla_margin_fut_optimista <- calcular_margin_futuro("Optimista", fut_optimista_roll, fechas_roll, contratos, multiplicador, margen_inicial_total, margen_mantenimiento_total)
tabla_margin_fut_escenarios <- rbind(tabla_margin_fut_pesimista, tabla_margin_fut_promedio, tabla_margin_fut_optimista)
tabla_margin_fut_escenarios_presentacion <- tabla_margin_fut_escenarios
tabla_margin_fut_escenarios_presentacion[, sapply(tabla_margin_fut_escenarios_presentacion, is.numeric)] <-
round(tabla_margin_fut_escenarios_presentacion[, sapply(tabla_margin_fut_escenarios_presentacion, is.numeric)], 2)
DT::datatable(
tabla_margin_fut_escenarios_presentacion,
rownames = FALSE,
filter = "top",
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 14. Margin call trimestral por escenario del futuro"
),
options = list(pageLength = 10, scrollX = TRUE, searching = TRUE, ordering = TRUE)
) %>%
formatCurrency(c("PG_LONG", "PG_SHORT", "Saldo_Margen_SHORT", "Margin_Call_SHORT", "Deposito_Acumulado_SHORT", "Saldo_Margen_LONG", "Margin_Call_LONG", "Deposito_Acumulado_LONG", "PG_Acum_SHORT", "PG_Acum_LONG"), currency = "USD ", digits = 2)
resumen_margin_futuro <- data.frame(
Escenario_Futuro = c("Pesimista", "Promedio", "Optimista"),
Precio_Inicial_Futuro = c(fut_pesimista_roll[1], fut_promedio_roll[1], fut_optimista_roll[1]),
Precio_Final_Futuro = c(tail(fut_pesimista_roll, 1), tail(fut_promedio_roll, 1), tail(fut_optimista_roll, 1)),
PG_Acum_SHORT_Final = c(tail(tabla_margin_fut_pesimista$PG_Acum_SHORT, 1), tail(tabla_margin_fut_promedio$PG_Acum_SHORT, 1), tail(tabla_margin_fut_optimista$PG_Acum_SHORT, 1)),
Margin_Call_Acum_SHORT = c(tail(tabla_margin_fut_pesimista$Deposito_Acumulado_SHORT, 1), tail(tabla_margin_fut_promedio$Deposito_Acumulado_SHORT, 1), tail(tabla_margin_fut_optimista$Deposito_Acumulado_SHORT, 1)),
PG_Acum_LONG_Final = c(tail(tabla_margin_fut_pesimista$PG_Acum_LONG, 1), tail(tabla_margin_fut_promedio$PG_Acum_LONG, 1), tail(tabla_margin_fut_optimista$PG_Acum_LONG, 1)),
Margin_Call_Acum_LONG = c(tail(tabla_margin_fut_pesimista$Deposito_Acumulado_LONG, 1), tail(tabla_margin_fut_promedio$Deposito_Acumulado_LONG, 1), tail(tabla_margin_fut_optimista$Deposito_Acumulado_LONG, 1))
)
resumen_margin_futuro[, sapply(resumen_margin_futuro, is.numeric)] <-
round(resumen_margin_futuro[, sapply(resumen_margin_futuro, is.numeric)], 2)
DT::datatable(
resumen_margin_futuro,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 15. Resumen de P/G y margin call por escenario del futuro"
),
options = list(dom = "t", scrollX = TRUE)
) %>%
formatCurrency(c("PG_Acum_SHORT_Final", "Margin_Call_Acum_SHORT", "PG_Acum_LONG_Final", "Margin_Call_Acum_LONG"), currency = "USD ", digits = 2)
Análisis El margin call se calculó con una estructura de roll-over trimestral. En cada fecha de roll-over se mide la diferencia del precio del futuro frente al trimestre anterior. Para una posición larga, una subida del futuro genera ganancia; para una posición corta, una caída del futuro genera ganancia.
La posición corta es la relevante para la cobertura del portafolio porque el portafolio está comprado en acciones. Desde esta tabla ya podemos observar que esos efectos de cobertura son evidentes en el escenario pesimista, obteniendo un P/G final en corto de 1.744.685,25. Por el contrario, si en el escenario pesimista se toma una posición larga, la P/G sería ese mismo valor negativo (-1.744.685,25). Ocurre el caso contrario en el escenario promedio y en el optimista. Si se toma una posición corta en ambos casos, se obtiene una P/G de -2.708.036,41 y -4.966.145,29 (Promedio y Optimista respectivamente). Y si se toma una posición larga se obtiene ese mismo valor positivo, es decir 2.708.036,41 y 4.966.145,29 (Promedio y Optimista respectivamente). En la tabla 15 se observa el precio ST en cada escenario, pesimista, promedio y optimista (4.449,71, 9397,18 y 11.906,19 respectivamente). # 11. Valor del portafolio con y sin cobertura por escenario
valor_cubierto_pesimista <- port_pesimista_roll + tabla_margin_fut_pesimista$PG_Acum_SHORT
valor_cubierto_promedio <- port_promedio_roll + tabla_margin_fut_promedio$PG_Acum_SHORT
valor_cubierto_optimista <- port_optimista_roll + tabla_margin_fut_optimista$PG_Acum_SHORT
tabla_cobertura_escenarios <- data.frame(
Fecha = fechas_roll,
Portafolio_Pesimista_Sin_Cobertura = port_pesimista_roll,
PG_Acum_SHORT_Futuro_Pesimista = tabla_margin_fut_pesimista$PG_Acum_SHORT,
Portafolio_Pesimista_Con_Cobertura = valor_cubierto_pesimista,
Portafolio_Promedio_Sin_Cobertura = port_promedio_roll,
PG_Acum_SHORT_Futuro_Promedio = tabla_margin_fut_promedio$PG_Acum_SHORT,
Portafolio_Promedio_Con_Cobertura = valor_cubierto_promedio,
Portafolio_Optimista_Sin_Cobertura = port_optimista_roll,
PG_Acum_SHORT_Futuro_Optimista = tabla_margin_fut_optimista$PG_Acum_SHORT,
Portafolio_Optimista_Con_Cobertura = valor_cubierto_optimista
)
tabla_cobertura_escenarios_presentacion <- tabla_cobertura_escenarios
tabla_cobertura_escenarios_presentacion[, sapply(tabla_cobertura_escenarios_presentacion, is.numeric)] <-
round(tabla_cobertura_escenarios_presentacion[, sapply(tabla_cobertura_escenarios_presentacion, is.numeric)], 2)
DT::datatable(
tabla_cobertura_escenarios_presentacion,
rownames = FALSE,
filter = "top",
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 16. Valor del portafolio con y sin cobertura por escenario"
),
options = list(pageLength = 10, scrollX = TRUE, searching = TRUE, ordering = TRUE)
) %>%
formatCurrency(names(tabla_cobertura_escenarios_presentacion)[-1], currency = "USD ", digits = 2)
resumen_cobertura_escenarios <- data.frame(
Escenario = c("Pesimista", "Promedio", "Optimista"),
Valor_Final_Sin_Cobertura = c(tail(port_pesimista_roll, 1), tail(port_promedio_roll, 1), tail(port_optimista_roll, 1)),
PG_Acum_SHORT_Final = c(tail(tabla_margin_fut_pesimista$PG_Acum_SHORT, 1), tail(tabla_margin_fut_promedio$PG_Acum_SHORT, 1), tail(tabla_margin_fut_optimista$PG_Acum_SHORT, 1)),
Valor_Final_Con_Cobertura = c(tail(valor_cubierto_pesimista, 1), tail(valor_cubierto_promedio, 1), tail(valor_cubierto_optimista, 1)),
Diferencia_Cobertura = c(tail(valor_cubierto_pesimista - port_pesimista_roll, 1), tail(valor_cubierto_promedio - port_promedio_roll, 1), tail(valor_cubierto_optimista - port_optimista_roll, 1))
)
resumen_cobertura_escenarios[, sapply(resumen_cobertura_escenarios, is.numeric)] <-
round(resumen_cobertura_escenarios[, sapply(resumen_cobertura_escenarios, is.numeric)], 2)
DT::datatable(
resumen_cobertura_escenarios,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 17. Resumen final de cobertura por escenario"
),
options = list(dom = "t")
) %>%
formatCurrency(c("Valor_Final_Sin_Cobertura", "PG_Acum_SHORT_Final", "Valor_Final_Con_Cobertura", "Diferencia_Cobertura"), currency = "USD ", digits = 2) %>%
formatStyle("PG_Acum_SHORT_Final", color = styleInterval(0, c("red3", "darkgreen")), fontWeight = "bold") %>%
formatStyle("Diferencia_Cobertura", color = styleInterval(0, c("red3", "darkgreen")), fontWeight = "bold")
ylim_cobertura <- range(c(port_pesimista_roll, port_promedio_roll, port_optimista_roll, valor_cubierto_pesimista, valor_cubierto_promedio, valor_cubierto_optimista), na.rm = TRUE)
plot(fechas_roll, port_pesimista_roll, type = "l", lwd = 3, lty = 1, col = "red3", ylim = ylim_cobertura, main = "Valor del portafolio con y sin cobertura por escenario", xlab = "Fecha", ylab = "Valor del portafolio en USD", yaxt = "n")
axis(2, at = pretty(ylim_cobertura), labels = format(pretty(ylim_cobertura), big.mark = ",", scientific = FALSE))
lines(fechas_roll, valor_cubierto_pesimista, lwd = 3, lty = 2, col = "red3")
lines(fechas_roll, port_promedio_roll, lwd = 3, lty = 1, col = "blue3")
lines(fechas_roll, valor_cubierto_promedio, lwd = 3, lty = 2, col = "blue3")
lines(fechas_roll, port_optimista_roll, lwd = 3, lty = 1, col = "darkgreen")
lines(fechas_roll, valor_cubierto_optimista, lwd = 3, lty = 2, col = "darkgreen")
points(fechas_roll, port_pesimista_roll, pch = 16, col = "red3")
points(fechas_roll, valor_cubierto_pesimista, pch = 17, col = "red3")
points(fechas_roll, port_promedio_roll, pch = 16, col = "blue3")
points(fechas_roll, valor_cubierto_promedio, pch = 17, col = "blue3")
points(fechas_roll, port_optimista_roll, pch = 16, col = "darkgreen")
points(fechas_roll, valor_cubierto_optimista, pch = 17, col = "darkgreen")
legend("topleft", legend = c("Pesimista sin cobertura", "Pesimista con cobertura", "Promedio sin cobertura", "Promedio con cobertura", "Optimista sin cobertura", "Optimista con cobertura"), col = c("red3", "red3", "blue3", "blue3", "darkgreen", "darkgreen"), lwd = 3, lty = c(1, 2, 1, 2, 1, 2), pch = c(16, 17, 16, 17, 16, 17), bty = "n")
Análisis La comparación de cobertura se realiza emparejando el escenario pesimista del portafolio con el escenario pesimista del futuro, el promedio con el promedio y el optimista con el optimista. Esto permite evaluar la cobertura bajo los escenarios elegidos.
En el escenario pesimista, la caída del futuro genera ganancias para la posición corta, lo que ayuda a compensar un poco la pérdida del portafolio. En el escenario promedio, la cobertura hace que se generen perdidas en la posición corta del futuro ya que el precio del futuro sería mayor al precio inicial en ST. En el escenario optimista, el futuro sube y la posición corta genera pérdidas, reduciendo parte de la ganancia del portafolio. Por tanto, se puede concluir que no se busca aumentar las ganancias con la cobertura tomando una posición corta en el índice, sino protegerse ante posibles escenarios adversos como un escenario pesimista.
betas_alternativas <- c(0.8, 1.5)
contratos_teoricos_beta <- betas_alternativas * V_portafolio / (F0 * multiplicador)
contratos_redondeados_beta <- round(contratos_teoricos_beta, 0)
tabla_betas_alternativas <- data.frame(
Beta_Escenario = betas_alternativas,
Valor_Portafolio_USD = V_portafolio,
Precio_Futuro = F0,
Multiplicador = multiplicador,
Valor_Contrato_USD = F0 * multiplicador,
Contratos_Teoricos = contratos_teoricos_beta,
Contratos_Cortos_Redondeados = contratos_redondeados_beta,
Posicion_Cobertura = c("Corta", "Corta")
)
tabla_betas_alternativas[, sapply(tabla_betas_alternativas, is.numeric)] <-
round(tabla_betas_alternativas[, sapply(tabla_betas_alternativas, is.numeric)], 4)
DT::datatable(
tabla_betas_alternativas,
rownames = FALSE,
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 18. Contratos requeridos con betas alternativas"
),
options = list(dom = "t", scrollX = TRUE)
) %>%
formatCurrency(c("Valor_Portafolio_USD", "Valor_Contrato_USD"), currency = "USD ", digits = 2)
calcular_cobertura_beta_escenario <- function(beta_escenario, contratos_escenario, nombre_escenario, valor_port_roll, precio_fut_roll, multiplicador) {
dif_precio <- c(0, diff(precio_fut_roll))
PG_SHORT <- -dif_precio * contratos_escenario * multiplicador
PG_Acum_SHORT <- cumsum(PG_SHORT)
valor_con_cobertura <- valor_port_roll + PG_Acum_SHORT
data.frame(
Beta_Escenario = beta_escenario,
Escenario = nombre_escenario,
Contratos_Cortos = contratos_escenario,
Valor_Final_Sin_Cobertura = tail(valor_port_roll, 1),
PG_Acum_SHORT_Final = tail(PG_Acum_SHORT, 1),
Valor_Final_Con_Cobertura = tail(valor_con_cobertura, 1),
Diferencia_Cobertura = tail(valor_con_cobertura - valor_port_roll, 1)
)
}
resumen_beta_08 <- rbind(
calcular_cobertura_beta_escenario(0.8, contratos_redondeados_beta[1], "Pesimista", port_pesimista_roll, fut_pesimista_roll, multiplicador),
calcular_cobertura_beta_escenario(0.8, contratos_redondeados_beta[1], "Promedio", port_promedio_roll, fut_promedio_roll, multiplicador),
calcular_cobertura_beta_escenario(0.8, contratos_redondeados_beta[1], "Optimista", port_optimista_roll, fut_optimista_roll, multiplicador)
)
resumen_beta_15 <- rbind(
calcular_cobertura_beta_escenario(1.5, contratos_redondeados_beta[2], "Pesimista", port_pesimista_roll, fut_pesimista_roll, multiplicador),
calcular_cobertura_beta_escenario(1.5, contratos_redondeados_beta[2], "Promedio", port_promedio_roll, fut_promedio_roll, multiplicador),
calcular_cobertura_beta_escenario(1.5, contratos_redondeados_beta[2], "Optimista", port_optimista_roll, fut_optimista_roll, multiplicador)
)
resumen_betas_trayectorias <- rbind(resumen_beta_08, resumen_beta_15)
resumen_betas_trayectorias[, sapply(resumen_betas_trayectorias, is.numeric)] <-
round(resumen_betas_trayectorias[, sapply(resumen_betas_trayectorias, is.numeric)], 2)
DT::datatable(
resumen_betas_trayectorias,
rownames = FALSE,
filter = "top",
caption = tags$caption(
style = "caption-side: top; text-align: center; font-weight: bold;",
"Tabla 19. Efecto de betas alternativas en la cobertura por escenario"
),
options = list(pageLength = 10, scrollX = TRUE, searching = TRUE)
) %>%
formatCurrency(c("Valor_Final_Sin_Cobertura", "PG_Acum_SHORT_Final", "Valor_Final_Con_Cobertura", "Diferencia_Cobertura"), currency = "USD ", digits = 2) %>%
formatStyle("Diferencia_Cobertura", color = styleInterval(0, c("red3", "darkgreen")), fontWeight = "bold")
barplot_data <- matrix(
resumen_betas_trayectorias$Valor_Final_Con_Cobertura,
nrow = 2,
byrow = TRUE
)
rownames(barplot_data) <- c("Beta 0.8", "Beta 1.5")
colnames(barplot_data) <- c("Pesimista", "Promedio", "Optimista")
barplot(
barplot_data,
beside = TRUE,
col = c("steelblue3", "orange3"),
main = "Valor final con cobertura por beta y escenario",
xlab = "Escenario",
ylab = "Valor final con cobertura en USD",
legend.text = TRUE,
args.legend = list(x = "topleft", bty = "n"),
yaxt = "n"
)
axis(2, at = pretty(barplot_data), labels = format(pretty(barplot_data), big.mark = ",", scientific = FALSE))
Análisis En ambos escenarios de beta, la posición de cobertura sigue siendo corta, porque el portafolio está comprado en acciones. La diferencia entre beta 0.8 y beta 1.5 no es la dirección de la cobertura, sino que tan intensa es la cobertura.
En ambos casos se requieren más contratos, pero si la beta estuviera en un punto intermedio entre 0.8 y 1.5 podríamos verlo así: Con beta 0.8 se requieren menos contratos, por lo que la protección es menor en escenarios bajistas, pero también es menor el costo de oportunidad cuando el mercado sube. Con beta 1.5 se requieren más contratos, por lo que la cobertura compensa más en escenarios pesimistas, pero genera pérdidas mayores en escenarios optimistas. Por tanto, una beta más alta aumenta tanto la protección ante caídas como el costo de oportunidad en escenarios favorables.
El portafolio compuesto por KO, MCD y JNJ permite combinar empresas consolidadas con perfiles defensivos y fuentes de riesgo diferentes. KO aporta exposición al consumo básico global, MCD representa un modelo de franquicias con alta generación de caja y JNJ incorpora exposición al sector salud. A partir de la optimización por Sharpe Ratio se obtuvo una asignación de capital que busca maximizar la rentabilidad esperada por unidad de riesgo, manteniendo una lógica económica más moderada que la de activos de alto crecimiento.
El VaR diario al 5% y al 1% permite dimensionar posibles pérdidas del portafolio bajo escenarios adversos. Estos valores funcionan como referencia para la cobertura, ya que muestran el nivel de pérdida que podría materializarse en escenarios extremos. Posteriormente, la beta del portafolio permitió estimar la exposición sistemática frente al S&P 500 y calcular el número óptimo de contratos del futuro E-mini S&P 500.
La estrategia de cobertura se construyó mediante una posición corta en futuros. Esta decisión es coherente con un portafolio comprado en acciones: si el mercado cae, la posición corta gana y compensa parcialmente las pérdidas. Sin embargo, si el mercado sube, la cobertura genera pérdidas y reduce el valor final del portafolio. Esto confirma que la cobertura no busca maximizar ganancias, sino reducir la exposición al riesgo de mercado.
El análisis por escenarios muestra que la cobertura es más valiosa en el escenario pesimista, donde el futuro cae y la posición corta genera ganancias. En el escenario optimista, la cobertura representa un costo de oportunidad. Finalmente, los escenarios de beta 0.8 y 1.5 muestran que la beta es una variable clave en la intensidad de la cobertura: una beta mayor exige más contratos y genera efectos más fuertes, tanto positivos en caídas como negativos en subidas.
Esta segunda parte del trabajo desarrolla una estrategia de cobertura cambiaria mediante forwards USD/COP para una inversión en maquinaria amarilla financiada en dólares. La fecha de referencia del ejercicio es el 30 de marzo de 2026. El objetivo es evaluar cómo cambia el costo del crédito cuando los pagos futuros en dólares se convierten a pesos colombianos bajo escenarios de TRM simulada y bajo una cobertura parcial con forwards.
El ejercicio sigue las instrucciones del trabajo final: se parte de una maquinaria de 350 millones de pesos, se realiza un crédito en Estados Unidos con sistema de amortización francés, se cubre el 75% de la exposición cambiaria desde el sexto año y se contrastan los resultados con información de mercado forward USD/COP mayor a seis meses.
Las principales fuentes usadas son:
Tasa de cambio del peso colombiano TRABAJO FINAL.xlsx o
Tasa de cambio del peso colombiano TRABAJO FINAL(1).xlsx.# ============================================================
# PARÁMETROS PRINCIPALES DEL EJERCICIO
# ============================================================
fecha_referencia <- as.Date("2026-03-30")
valor_maquinaria_cop <- 350000000
cuota_inicial_pct <- 0.10
porcentaje_cobertura <- 0.75
plazo_credito_anios <- 10
pagos_por_anio <- 12
n_meses <- plazo_credito_anios * pagos_por_anio
# Tasa del crédito en Estados Unidos.
# Se usa como tasa comercial anual de referencia para financiación de maquinaria/equipo.
# Si el profesor exige otra tasa específica, solo debe reemplazarse este parámetro.
tasa_credito_EA <- 0.05611
# Tasas para valorar el forward teórico por paridad cubierta.
# IBR nominal 12 meses reportada por ustedes para 27/03/2026.
# Se usa como tasa anual comparable para el ejercicio. Si se obtiene IBR efectiva anual,
# puede reemplazarse directamente tasa_cop_EA por ese dato.
tasa_cop_EA <- 0.12670
# SOFR 180-Day Average aproximada para 27/03/2026.
# Es el promedio SOFR más largo que publica directamente NY Fed en su tabla estándar.
tasa_usd_EA <- 0.0388
# Información SET-FX / SET-ICAP tomada de la captura del usuario.
# Mercado FORWARD, USD/COP, plazo mayor a 6 meses, sección IMC3 / Clientes.
fecha_setfx <- as.Date("2026-04-08")
K_forward_setfx <- 4027.57
apertura_setfx <- 3997.93
cierre_setfx <- 3982.20
n_transacciones_setfx <- 24
# Simulación
n_sim <- 10000
df_t <- 5
parametros_tabla <- tibble::tibble(
Concepto = c(
"Fecha de referencia",
"Valor maquinaria",
"Cuota inicial",
"Porcentaje cubierto",
"Plazo del crédito",
"Tasa crédito EE. UU. EA",
"IBR 12M nominal usada como tasa COP",
"SOFR 180D usada como tasa USD",
"K SET-FX forward > 6 meses",
"Fecha SET-FX"
),
Valor = c(
as.character(fecha_referencia),
fmt_pesos(valor_maquinaria_cop),
fmt_pct(cuota_inicial_pct),
fmt_pct(porcentaje_cobertura),
paste0(plazo_credito_anios, " años"),
fmt_pct(tasa_credito_EA),
fmt_pct(tasa_cop_EA),
fmt_pct(tasa_usd_EA),
fmt_pesos(K_forward_setfx),
as.character(fecha_setfx)
)
)
kable(parametros_tabla, caption = "Parámetros principales del Punto 2") %>%
kable_styling(full_width = FALSE)
| Concepto | Valor |
|---|---|
| Fecha de referencia | 2026-03-30 |
| Valor maquinaria | $350.000.000 |
| Cuota inicial | 10,00% |
| Porcentaje cubierto | 75,00% |
| Plazo del crédito | 10 años |
| Tasa crédito EE. UU. EA | 5,61% |
| IBR 12M nominal usada como tasa COP | 12,67% |
| SOFR 180D usada como tasa USD | 3,88% |
| K SET-FX forward > 6 meses | $4.028 |
| Fecha SET-FX | 2026-04-08 |
Análisis. Los parámetros iniciales reflejan una operación de financiación con exposición cambiaria clara: la maquinaria se expresa en pesos, pero la deuda se pacta en dólares. La cuota inicial del 10% reduce el monto financiado, pero no elimina el riesgo, porque el 90% restante queda expuesto a la TRM durante diez años. La tasa IBR se usa para representar el costo del dinero en COP y la SOFR para representar el costo de fondeo en USD, lo cual permite valorar el forward mediante paridad cubierta de tasas. Además, el precio SET-FX se incorpora como referencia de mercado para que el ejercicio no dependa únicamente de una fórmula teórica, sino también de una observación real del mercado forward USD/COP.
El archivo de TRM se usa como fuente principal para la tasa de cambio histórica. A partir de la serie diaria se calcula la serie mensual de cierre, los retornos logarítmicos mensuales y la volatilidad mensual. El valor spot inicial \(S_0\) se toma como la TRM disponible más cercana anterior o igual al 30/03/2026.
# ============================================================
# LECTURA ROBUSTA DEL ARCHIVO DE TRM
# ============================================================
archivos_posibles <- c(
"Tasa de cambio del peso colombiano TRABAJO FINAL.xlsx",
"Tasa de cambio del peso colombiano TRABAJO FINAL(1).xlsx"
)
archivo_trm <- archivos_posibles[file.exists(archivos_posibles)][1]
if (is.na(archivo_trm)) {
stop("No se encontró el archivo de TRM. Verifique que el Excel esté en la misma carpeta del Rmd y que el nombre coincida.")
}
trm_raw <- readxl::read_excel(archivo_trm)
# Función para limpiar nombres de columnas sin depender de janitor
limpiar_nombre <- function(x) {
x %>%
stringr::str_to_lower() %>%
iconv(from = "UTF-8", to = "ASCII//TRANSLIT") %>%
stringr::str_replace_all("[^a-z0-9]+", "_") %>%
stringr::str_replace_all("(^_|_$)", "")
}
names(trm_raw) <- limpiar_nombre(names(trm_raw))
# Identificación automática de columnas de fecha y TRM
col_fecha <- names(trm_raw)[stringr::str_detect(names(trm_raw), "fecha|vigencia")][1]
col_trm <- names(trm_raw)[stringr::str_detect(names(trm_raw), "trm|representativa|mercado|valor|tasa") & names(trm_raw) != col_fecha][1]
if (is.na(col_fecha) | is.na(col_trm)) {
stop("No fue posible identificar automáticamente las columnas de fecha y TRM. Revise los nombres del Excel.")
}
# Conversión robusta de valores numéricos
convertir_numero <- function(x) {
if (is.numeric(x)) return(x)
x <- as.character(x)
x <- stringr::str_replace_all(x, "\\.", "")
x <- stringr::str_replace_all(x, ",", ".")
as.numeric(x)
}
trm <- trm_raw %>%
transmute(
fecha = as.Date(.data[[col_fecha]]),
trm = convertir_numero(.data[[col_trm]])
) %>%
filter(!is.na(fecha), !is.na(trm), trm > 0) %>%
arrange(fecha)
# Spot inicial: última TRM disponible antes o en la fecha de referencia
S0_info <- trm %>%
filter(fecha <= fecha_referencia) %>%
slice_tail(n = 1)
S0 <- S0_info$trm[1]
fecha_S0 <- S0_info$fecha[1]
if (length(S0) == 0 | is.na(S0)) {
stop("No se encontró una TRM válida antes o en la fecha de referencia.")
}
tibble::tibble(
Archivo = archivo_trm,
Columna_fecha = col_fecha,
Columna_TRM = col_trm,
Fecha_spot_usada = fecha_S0,
S0_TRM = S0
) %>%
mutate(S0_TRM = fmt_pesos(S0_TRM)) %>%
kable(caption = "Lectura de datos y spot inicial usado") %>%
kable_styling(full_width = FALSE)
| Archivo | Columna_fecha | Columna_TRM | Fecha_spot_usada | S0_TRM |
|---|---|---|---|---|
| Tasa de cambio del peso colombiano TRABAJO FINAL.xlsx | fecha | tasa_representativa_del_mercado_trm | 2026-03-30 | $3.669 |
Análisis. La TRM usada como spot inicial es $3.669 y corresponde a la fecha 2026-03-30. Este valor es el punto de partida de todo el modelo, porque define cuántos dólares se necesitan para financiar la maquinaria y también sirve como base para comparar el forward teórico y el forward de mercado. Al tomar la última TRM disponible antes o en la fecha de referencia, se evita forzar un dato inexistente por problemas de calendario y se mantiene la consistencia con la fecha establecida en el trabajo.
# ============================================================
# GRÁFICO HISTÓRICO DE LA TRM
# ============================================================
ggplot(trm, aes(x = fecha, y = trm)) +
geom_line(linewidth = 0.5) +
geom_vline(xintercept = fecha_referencia, linetype = "dashed") +
labs(
title = "Evolución histórica de la TRM",
subtitle = paste("Fecha de referencia:", fecha_referencia),
x = "Fecha",
y = "TRM COP/USD"
) +
scale_y_continuous(labels = label_dollar(prefix = "$", big.mark = ".", decimal.mark = ",")) +
theme_minimal()
Análisis. La gráfica histórica permite observar que la TRM no es una variable estable en el tiempo, sino una serie con episodios de alta volatilidad. Esto justifica el uso de una cobertura cambiaria, porque un aumento fuerte del dólar puede encarecer de manera considerable las cuotas del crédito cuando se expresan en pesos. En una financiación de largo plazo, el riesgo no está solo en el nivel actual de la TRM, sino en la incertidumbre sobre su trayectoria futura.
La TRM refleja el precio del dólar frente al peso colombiano. Para este trabajo, su importancia radica en que el crédito se pacta en dólares, pero la inversión económica de la maquinaria se mide en pesos. Por lo tanto, una devaluación del peso aumenta el costo efectivo del crédito al convertir las cuotas futuras a COP. En cambio, una apreciación del peso reduce el costo en pesos de esas mismas cuotas.
Desde el punto de vista financiero, la exposición principal del inversionista es una posición corta natural en COP y larga en USD, porque deberá conseguir dólares en el futuro para pagar el crédito. Por eso la cobertura apropiada es tomar una posición larga en forwards USD/COP, fijando anticipadamente una tasa de compra de dólares para una parte de los pagos futuros.
La rúbrica pide calcular retornos mensuales y desviación estándar mensual de la TRM histórica. Para ello se toma el último dato disponible de cada mes y se calculan retornos logarítmicos.
# ============================================================
# SERIE MENSUAL Y RETORNOS LOGARÍTMICOS
# ============================================================
trm_mensual <- trm %>%
mutate(mes = lubridate::floor_date(fecha, "month")) %>%
group_by(mes) %>%
slice_tail(n = 1) %>%
ungroup() %>%
select(fecha, mes, trm) %>%
mutate(ret_log = log(trm / lag(trm))) %>%
filter(!is.na(ret_log))
mu_mensual <- mean(trm_mensual$ret_log, na.rm = TRUE)
sigma_mensual <- sd(trm_mensual$ret_log, na.rm = TRUE)
estadisticas_trm <- tibble::tibble(
Indicador = c(
"Media mensual logarítmica",
"Desviación estándar mensual",
"Media anualizada aproximada",
"Volatilidad anualizada aproximada"
),
Valor = c(
mu_mensual,
sigma_mensual,
mu_mensual * 12,
sigma_mensual * sqrt(12)
)
)
estadisticas_trm %>%
mutate(Valor = fmt_pct(Valor)) %>%
kable(caption = "Retornos y volatilidad histórica de la TRM") %>%
kable_styling(full_width = FALSE)
| Indicador | Valor |
|---|---|
| Media mensual logarítmica | 0,41% |
| Desviación estándar mensual | 3,38% |
| Media anualizada aproximada | 4,87% |
| Volatilidad anualizada aproximada | 11,72% |
Análisis. La media mensual logarítmica de la TRM es 0,41%, mientras que la desviación estándar mensual es 3,38%. Esto significa que, aunque la tendencia promedio mensual puede ser moderada, la variabilidad de la tasa de cambio es relevante para una deuda en dólares. La volatilidad anualizada aproximada de 11,72% muestra que el costo en pesos del crédito puede cambiar de forma importante cuando se proyecta a varios años. Por esta razón, el análisis de cobertura no debe limitarse al valor esperado, sino que debe considerar escenarios adversos de depreciación del peso.
ggplot(trm_mensual, aes(x = mes, y = ret_log)) +
geom_col() +
labs(
title = "Retornos logarítmicos mensuales de la TRM",
x = "Mes",
y = "Retorno logarítmico mensual"
) +
scale_y_continuous(labels = percent_format(accuracy = 1, decimal.mark = ",")) +
theme_minimal()
Se simulan trayectorias mensuales de la TRM durante 10 años. Se usa un Movimiento Browniano Geométrico en frecuencia mensual. La primera simulación usa distribución normal y la segunda usa distribución t-Student estandarizada para incorporar colas más pesadas.
# ============================================================
# SIMULACIÓN MBG NORMAL Y T-STUDENT
# ============================================================
simular_mbg <- function(S0, mu, sigma, n_meses, n_sim, dist = c("normal", "t"), df = 5) {
dist <- match.arg(dist)
mb <- matrix(NA_real_, nrow = n_meses + 1, ncol = n_sim)
mb[1, ] <- S0
if (dist == "normal") {
z <- matrix(rnorm(n_meses * n_sim), nrow = n_meses, ncol = n_sim)
} else {
# Estandarización para que la varianza sea aproximadamente 1
z <- matrix(rt(n_meses * n_sim, df = df), nrow = n_meses, ncol = n_sim)
z <- z / sqrt(df / (df - 2))
}
retornos_sim <- mu + sigma * z
for (t in 2:(n_meses + 1)) {
mb[t, ] <- mb[t - 1, ] * exp(retornos_sim[t - 1, ])
}
mb
}
trm_sim_normal <- simular_mbg(S0, mu_mensual, sigma_mensual, n_meses, n_sim, dist = "normal")
trm_sim_t <- simular_mbg(S0, mu_mensual, sigma_mensual, n_meses, n_sim, dist = "t", df = df_t)
fechas_sim <- seq.Date(from = fecha_S0, by = "month", length.out = n_meses + 1)
ST_normal <- trm_sim_normal[n_meses + 1, ]
ST_t <- trm_sim_t[n_meses + 1, ]
resumen_sim <- tibble::tibble(
Distribucion = c("Normal", "t-Student"),
Promedio_ST = c(mean(ST_normal), mean(ST_t)),
Mediana_ST = c(median(ST_normal), median(ST_t)),
Percentil_5 = c(quantile(ST_normal, 0.05), quantile(ST_t, 0.05)),
Percentil_95 = c(quantile(ST_normal, 0.95), quantile(ST_t, 0.95))
)
resumen_sim %>%
mutate(across(where(is.numeric), fmt_pesos)) %>%
kable(caption = "Resumen de la TRM simulada al final del horizonte de 10 años") %>%
kable_styling(full_width = FALSE)
| Distribucion | Promedio_ST | Mediana_ST | Percentil_5 | Percentil_95 |
|---|---|---|---|---|
| Normal | $6.394 | $5.996 | $3.240 | $10.975 |
| t-Student | $6.416 | $5.997 | $3.248 | $11.095 |
Análisis. La simulación normal y la simulación t-Student permiten comparar dos formas de representar la incertidumbre de la TRM. La distribución t-Student es más exigente porque incorpora colas pesadas, es decir, asigna mayor probabilidad a movimientos extremos. Bajo esta lógica, el escenario t-Student resulta más apropiado para evaluar una cobertura cambiaria, ya que el principal riesgo de la deuda no está en una variación pequeña del dólar, sino en una devaluación fuerte que eleve el costo de las cuotas futuras. En el horizonte final, la TRM promedio simulada bajo t-Student es $6.416, con un percentil 95% de $11.095; este rango muestra la magnitud del riesgo que se busca cubrir.
# Se grafican solo 100 trayectorias para que la figura sea legible
idx_plot <- sample(seq_len(n_sim), 100)
sim_plot <- as.data.frame(trm_sim_t[, idx_plot]) %>%
mutate(fecha = fechas_sim) %>%
pivot_longer(-fecha, names_to = "sim", values_to = "trm")
ggplot(sim_plot, aes(x = fecha, y = trm, group = sim)) +
geom_line(alpha = 0.25, linewidth = 0.35) +
labs(
title = "Trayectorias simuladas de TRM con distribución t-Student",
subtitle = "Se muestran 100 trayectorias de 10.000 simulaciones",
x = "Fecha",
y = "TRM simulada"
) +
scale_y_continuous(labels = label_dollar(prefix = "$", big.mark = ".", decimal.mark = ",")) +
theme_minimal()
Se supone que la maquinaria cuesta 350 millones de pesos. Como el crédito se toma en Estados Unidos, primero se convierte el valor de la maquinaria a dólares usando la TRM de referencia. Luego se paga una cuota inicial del 10% y se financia el 90% restante. El crédito se amortiza mensualmente con sistema francés, es decir, cuotas constantes en dólares.
# ============================================================
# CRÉDITO EN USD CON SISTEMA FRANCÉS
# ============================================================
valor_maquinaria_usd <- valor_maquinaria_cop / S0
cuota_inicial_usd <- valor_maquinaria_usd * cuota_inicial_pct
principal_usd <- valor_maquinaria_usd - cuota_inicial_usd
tasa_mensual_credito <- (1 + tasa_credito_EA)^(1 / pagos_por_anio) - 1
cuota_mensual_usd <- principal_usd * tasa_mensual_credito / (1 - (1 + tasa_mensual_credito)^(-n_meses))
amortizacion <- tibble::tibble(
mes = 1:n_meses,
anio = ceiling(mes / 12),
saldo_inicial = NA_real_,
interes = NA_real_,
abono_capital = NA_real_,
cuota_usd = cuota_mensual_usd,
saldo_final = NA_real_
)
saldo <- principal_usd
for (i in 1:n_meses) {
interes_i <- saldo * tasa_mensual_credito
abono_i <- cuota_mensual_usd - interes_i
saldo_final_i <- max(saldo - abono_i, 0)
amortizacion$saldo_inicial[i] <- saldo
amortizacion$interes[i] <- interes_i
amortizacion$abono_capital[i] <- abono_i
amortizacion$saldo_final[i] <- saldo_final_i
saldo <- saldo_final_i
}
resumen_credito <- tibble::tibble(
Concepto = c(
"Valor maquinaria en COP",
"TRM usada como S0",
"Valor maquinaria en USD",
"Cuota inicial 10% en USD",
"Principal financiado en USD",
"Tasa crédito EA",
"Cuota mensual USD",
"Total cuotas pagadas USD",
"Intereses totales USD"
),
Valor = c(
fmt_pesos(valor_maquinaria_cop),
fmt_pesos(S0),
fmt_usd(valor_maquinaria_usd),
fmt_usd(cuota_inicial_usd),
fmt_usd(principal_usd),
fmt_pct(tasa_credito_EA),
fmt_usd(cuota_mensual_usd),
fmt_usd(sum(amortizacion$cuota_usd)),
fmt_usd(sum(amortizacion$interes))
)
)
resumen_credito %>%
kable(caption = "Resumen del crédito en dólares") %>%
kable_styling(full_width = FALSE)
| Concepto | Valor |
|---|---|
| Valor maquinaria en COP | $350.000.000 |
| TRM usada como S0 | $3.669 |
| Valor maquinaria en USD | US$95,397 |
| Cuota inicial 10% en USD | US$9,540 |
| Principal financiado en USD | US$85,857 |
| Tasa crédito EA | 5,61% |
| Cuota mensual USD | US$931 |
| Total cuotas pagadas USD | US$111,668 |
| Intereses totales USD | US$25,811 |
Análisis. El valor de la maquinaria equivale a US$95,397 con la TRM de referencia. Después de pagar la cuota inicial, el principal financiado queda en US$85,857. La cuota mensual de US$931 es constante en dólares por tratarse de un sistema francés, pero no es constante en pesos, porque depende de la TRM de cada periodo. Por eso el crédito puede parecer manejable en USD, pero volverse más costoso para el inversionista colombiano si el peso se deprecia.
amortizacion %>%
slice(c(1:6, (n_meses - 5):n_meses)) %>%
mutate(across(c(saldo_inicial, interes, abono_capital, cuota_usd, saldo_final), fmt_usd)) %>%
kable(caption = "Tabla de amortización del crédito en USD: primeros y últimos meses") %>%
kable_styling(full_width = FALSE)
| mes | anio | saldo_inicial | interes | abono_capital | cuota_usd | saldo_final |
|---|---|---|---|---|---|---|
| 1 | 1 | US$85,857 | US$391 | US$539 | US$931 | US$85,318 |
| 2 | 1 | US$85,318 | US$389 | US$542 | US$931 | US$84,776 |
| 3 | 1 | US$84,776 | US$387 | US$544 | US$931 | US$84,232 |
| 4 | 1 | US$84,232 | US$384 | US$546 | US$931 | US$83,686 |
| 5 | 1 | US$83,686 | US$382 | US$549 | US$931 | US$83,137 |
| 6 | 1 | US$83,137 | US$379 | US$551 | US$931 | US$82,585 |
| 115 | 10 | US$5,495 | US$25 | US$906 | US$931 | US$4,590 |
| 116 | 10 | US$4,590 | US$21 | US$910 | US$931 | US$3,680 |
| 117 | 10 | US$3,680 | US$17 | US$914 | US$931 | US$2,766 |
| 118 | 10 | US$2,766 | US$13 | US$918 | US$931 | US$1,848 |
| 119 | 10 | US$1,848 | US$8 | US$922 | US$931 | US$926 |
| 120 | 10 | US$926 | US$4 | US$926 | US$931 | US$0 |
El crédito se recrea en pesos multiplicando cada cuota mensual en dólares por la TRM simulada de cada mes. La distribución t-Student se usa como escenario principal porque permite incorporar colas pesadas, es decir, escenarios extremos de depreciación o apreciación del peso.
# ============================================================
# COSTO DEL CRÉDITO EN PESOS SIN COBERTURA
# ============================================================
# Se excluye la primera fila de la matriz porque corresponde a t = 0.
trm_meses_t <- trm_sim_t[-1, ]
trm_meses_normal <- trm_sim_normal[-1, ]
cuotas_usd_vec <- amortizacion$cuota_usd
# Costo mensual en COP para cada simulación
costo_mensual_sin_cobertura <- sweep(trm_meses_t, 1, cuotas_usd_vec, `*`)
costo_total_sin_cobertura <- colSums(costo_mensual_sin_cobertura) + cuota_inicial_usd * S0
resumen_cop_sin <- tibble::tibble(
Indicador = c(
"Costo esperado total en COP",
"Mediana del costo total en COP",
"Percentil 5% del costo total",
"Percentil 95% del costo total"
),
Valor = c(
mean(costo_total_sin_cobertura),
median(costo_total_sin_cobertura),
quantile(costo_total_sin_cobertura, 0.05),
quantile(costo_total_sin_cobertura, 0.95)
)
)
resumen_cop_sin %>%
mutate(Valor = fmt_pesos(Valor)) %>%
kable(caption = "Costo del crédito en pesos sin cobertura") %>%
kable_styling(full_width = FALSE)
| Indicador | Valor |
|---|---|
| Costo esperado total en COP | $585.504.909 |
| Mediana del costo total en COP | $569.889.951 |
| Percentil 5% del costo total | $404.208.267 |
| Percentil 95% del costo total | $826.948.856 |
Análisis. El costo esperado total del crédito sin cobertura es $585.504.909. Este resultado incluye la cuota inicial y todas las cuotas futuras convertidas a pesos con las trayectorias simuladas de la TRM. La diferencia entre el percentil 5% y el percentil 95% evidencia que el costo final puede variar de manera amplia dependiendo del comportamiento del dólar. En términos financieros, este es el riesgo que se intenta administrar: no se busca eliminar la deuda, sino reducir la incertidumbre sobre cuánto costará en COP pagarla.
Para valorar el forward teórico USD/COP se aplica la paridad cubierta de tasas:
\[ F_0 = S_0 \times \frac{1+r_{COP}}{1+r_{USD}} \]
Donde \(r_{COP}\) se aproxima con la IBR 12 meses nominal reportada para el 27/03/2026 y \(r_{USD}\) se aproxima con la SOFR 180-Day Average. Este forward teórico se compara con el dato de mercado SET-FX/SET-ICAP para un forward USD/COP con plazo mayor a seis meses.
# ============================================================
# FORWARD TEÓRICO Y FORWARD DE MERCADO SET-FX
# ============================================================
K_forward_teorico <- S0 * ((1 + tasa_cop_EA) / (1 + tasa_usd_EA))
diferencia_setfx_teorico <- K_forward_setfx - K_forward_teorico
prima_teorica <- K_forward_teorico / S0 - 1
prima_setfx <- K_forward_setfx / S0 - 1
forward_tabla <- tibble::tibble(
Concepto = c(
"Spot S0 TRM",
"Tasa COP: IBR 12M nominal",
"Tasa USD: SOFR 180D",
"Forward teórico IBR/SOFR",
"Forward mercado SET-FX > 6 meses",
"Diferencia SET-FX - teórico",
"Prima teórica sobre spot",
"Prima SET-FX sobre spot"
),
Valor = c(
fmt_pesos(S0),
fmt_pct(tasa_cop_EA),
fmt_pct(tasa_usd_EA),
fmt_pesos(K_forward_teorico),
fmt_pesos(K_forward_setfx),
fmt_pesos(diferencia_setfx_teorico),
fmt_pct(prima_teorica),
fmt_pct(prima_setfx)
)
)
forward_tabla %>%
kable(caption = "Comparación entre forward teórico y forward de mercado") %>%
kable_styling(full_width = FALSE)
| Concepto | Valor |
|---|---|
| Spot S0 TRM | $3.669 |
| Tasa COP: IBR 12M nominal | 12,67% |
| Tasa USD: SOFR 180D | 3,88% |
| Forward teórico IBR/SOFR | $3.979 |
| Forward mercado SET-FX > 6 meses | $4.028 |
| Diferencia SET-FX - teórico | $48 |
| Prima teórica sobre spot | 8,46% |
| Prima SET-FX sobre spot | 9,78% |
setfx_tabla <- tibble::tibble(
Fuente = "SET-FX / SET-ICAP",
Seccion = "IMC3 / Clientes",
Mercado = "FORWARD",
Par = "USD/COP",
Plazo = "Mayor a 6 meses",
Fecha = fecha_setfx,
Apertura = apertura_setfx,
Cierre = cierre_setfx,
Promedio = K_forward_setfx,
Numero_transacciones = n_transacciones_setfx
)
setfx_tabla %>%
mutate(across(c(Apertura, Cierre, Promedio), fmt_pesos)) %>%
kable(caption = "Dato SET-FX usado para el forward de mercado") %>%
kable_styling(full_width = FALSE)
| Fuente | Seccion | Mercado | Par | Plazo | Fecha | Apertura | Cierre | Promedio | Numero_transacciones |
|---|---|---|---|---|---|---|---|---|---|
| SET-FX / SET-ICAP | IMC3 / Clientes | FORWARD | USD/COP | Mayor a 6 meses | 2026-04-08 | $3.998 | $3.982 | $4.028 | 24 |
Análisis. El forward teórico calculado con IBR/SOFR es $3.979, mientras que el forward observado en SET-FX es $4.028. La diferencia de $48 indica que el precio de mercado no coincide exactamente con la paridad cubierta estimada. Esta diferencia puede explicarse por costos de transacción, liquidez, márgenes de intermediación, demanda de cobertura y condiciones propias del mercado forward colombiano. Para la cobertura principal se utiliza SET-FX, porque representa la tasa observable a la que podría acercarse una operación real de mercado; el forward teórico se conserva como referencia para evaluar si esa tasa luce costosa o favorable frente a la paridad.
El precio forward de mercado SET-FX se usa como tasa de cobertura principal, porque representa una referencia observable del mercado forward USD/COP para un plazo mayor a seis meses. El forward teórico sirve como contraste: si el precio SET-FX es mayor al teórico, la cobertura de mercado luce relativamente más costosa; si es menor, luce relativamente más barata.
La cobertura se aplica al 75% de las cuotas en dólares correspondientes a cuatro años. La interpretación principal del enunciado es cubrir los años 6, 7, 8 y 9, porque indica que la cobertura empieza desde el sexto año y se hace con cuatro forwards de un año.
También se calcula una sensibilidad alternativa para los años 7, 8, 9 y 10, porque otra interpretación posible es que la cobertura inicie después de terminar el sexto año.
# ============================================================
# FUNCIÓN DE COBERTURA CON FORWARD
# ============================================================
calcular_cobertura <- function(anios_cubiertos, K_forward) {
meses_cubiertos <- which(amortizacion$anio %in% anios_cubiertos)
# Matriz de costos con cobertura: parte cubierta a K_forward, parte no cubierta a TRM simulada.
costo_mensual_con <- costo_mensual_sin_cobertura
for (m in meses_cubiertos) {
usd_m <- cuotas_usd_vec[m]
costo_mensual_con[m, ] <- usd_m * (
porcentaje_cobertura * K_forward +
(1 - porcentaje_cobertura) * trm_meses_t[m, ]
)
}
costo_total_con <- colSums(costo_mensual_con) + cuota_inicial_usd * S0
ahorro <- costo_total_sin_cobertura - costo_total_con
tibble::tibble(
Escenario = paste0("Cobertura años ", paste(anios_cubiertos, collapse = "-")),
Anios_cubiertos = paste(anios_cubiertos, collapse = ", "),
K_forward = K_forward,
Costo_sin_cobertura_promedio = mean(costo_total_sin_cobertura),
Costo_con_cobertura_promedio = mean(costo_total_con),
Ahorro_promedio = mean(ahorro),
Mediana_ahorro = median(ahorro),
Probabilidad_ahorro_positivo = mean(ahorro > 0),
VaR_5_ahorro = quantile(ahorro, 0.05),
VaR_1_ahorro = quantile(ahorro, 0.01)
)
}
resultado_cob_6_9 <- calcular_cobertura(6:9, K_forward_setfx)
resultado_cob_7_10 <- calcular_cobertura(7:10, K_forward_setfx)
resultados_cobertura <- bind_rows(resultado_cob_6_9, resultado_cob_7_10)
resultados_cobertura %>%
mutate(
K_forward = fmt_pesos(K_forward),
Costo_sin_cobertura_promedio = fmt_pesos(Costo_sin_cobertura_promedio),
Costo_con_cobertura_promedio = fmt_pesos(Costo_con_cobertura_promedio),
Ahorro_promedio = fmt_pesos(Ahorro_promedio),
Mediana_ahorro = fmt_pesos(Mediana_ahorro),
Probabilidad_ahorro_positivo = fmt_pct(Probabilidad_ahorro_positivo),
VaR_5_ahorro = fmt_pesos(VaR_5_ahorro),
VaR_1_ahorro = fmt_pesos(VaR_1_ahorro)
) %>%
kable(caption = "Resultados de la cobertura con forward SET-FX") %>%
kable_styling(full_width = FALSE)
| Escenario | Anios_cubiertos | K_forward | Costo_sin_cobertura_promedio | Costo_con_cobertura_promedio | Ahorro_promedio | Mediana_ahorro | Probabilidad_ahorro_positivo | VaR_5_ahorro | VaR_1_ahorro |
|---|---|---|---|---|---|---|---|---|---|
| Cobertura años 6-7-8-9 | 6, 7, 8, 9 | $4.028 | $585.504.909 | $537.787.223 | $47.717.686 | $39.986.989 | 80,82% | -$27.983.081 | -$49.550.047 |
| Cobertura años 7-8-9-10 | 7, 8, 9, 10 | $4.028 | $585.504.909 | $527.311.963 | $58.192.946 | $48.435.638 | 83,28% | -$26.871.982 | -$49.882.424 |
Análisis. En la interpretación principal, la cobertura de los años 6 a 9 permite comparar el costo promedio sin cobertura de $585.504.909 frente al costo promedio con cobertura de $537.787.223. El ahorro promedio de $47.717.686 debe leerse con cuidado: si es negativo, no significa que el forward esté mal aplicado, sino que en el promedio de las simulaciones la TRM no supera lo suficiente el precio pactado para compensar el costo de fijar la tasa. La probabilidad de ahorro positivo de 80,82% muestra en qué proporción de escenarios la cobertura reduce el costo total. El VaR del ahorro al 5% de -$27.983.081 resume el riesgo de que la cobertura genere un resultado desfavorable en escenarios bajos de TRM.
La cobertura protege cuando la TRM de mercado en el momento de los pagos supera el precio pactado en el forward. En ese caso, comprar dólares al forward es más barato que comprarlos al spot de mercado. En cambio, si la TRM termina por debajo del precio forward, la cobertura genera un costo de oportunidad, porque se compran dólares a una tasa superior a la tasa de mercado.
# ============================================================
# EVENTOS DE PROTECCIÓN POR AÑO
# ============================================================
resumen_anual_trm <- tibble::tibble(
mes = 1:n_meses,
anio = ceiling(mes / 12),
cuota_usd = cuotas_usd_vec
)
# TRM promedio simulada por año y simulación
trm_anual_sim <- map_dfr(1:plazo_credito_anios, function(a) {
meses_a <- which(amortizacion$anio == a)
tibble::tibble(
anio = a,
sim = 1:n_sim,
trm_promedio_anual = colMeans(trm_meses_t[meses_a, , drop = FALSE])
)
})
proteccion_anual <- trm_anual_sim %>%
group_by(anio) %>%
summarise(
TRM_promedio_simulada = mean(trm_promedio_anual),
Prob_TRM_mayor_K_SET_FX = mean(trm_promedio_anual > K_forward_setfx),
Prob_TRM_menor_K_SET_FX = mean(trm_promedio_anual < K_forward_setfx),
.groups = "drop"
)
proteccion_anual %>%
mutate(
TRM_promedio_simulada = fmt_pesos(TRM_promedio_simulada),
Prob_TRM_mayor_K_SET_FX = fmt_pct(Prob_TRM_mayor_K_SET_FX),
Prob_TRM_menor_K_SET_FX = fmt_pct(Prob_TRM_menor_K_SET_FX)
) %>%
kable(caption = "Probabilidad de protección de la cobertura por año") %>%
kable_styling(full_width = FALSE)
| anio | TRM_promedio_simulada | Prob_TRM_mayor_K_SET_FX | Prob_TRM_menor_K_SET_FX |
|---|---|---|---|
| 1 | $3.783 | 17,10% | 82,90% |
| 2 | $4.006 | 45,07% | 54,93% |
| 3 | $4.240 | 57,57% | 42,43% |
| 4 | $4.478 | 65,42% | 34,58% |
| 5 | $4.728 | 70,81% | 29,19% |
| 6 | $5.004 | 74,87% | 25,13% |
| 7 | $5.295 | 78,24% | 21,76% |
| 8 | $5.594 | 81,23% | 18,77% |
| 9 | $5.915 | 83,23% | 16,77% |
| 10 | $6.255 | 85,24% | 14,76% |
Análisis. La tabla de protección anual muestra en qué años la TRM simulada supera el precio forward SET-FX. Cuando la probabilidad de que la TRM sea mayor que el forward aumenta, la cobertura se vuelve más útil, porque permite comprar dólares a una tasa inferior a la del mercado simulado. En los años iniciales esta probabilidad suele ser menor porque el precio forward incorpora una prima frente al spot; en años posteriores, la probabilidad puede aumentar si las trayectorias simuladas acumulan depreciación del peso. Esta lectura confirma que el forward funciona principalmente como protección frente a escenarios de dólar alto, no como una garantía de ahorro en todos los escenarios.
trm_promedio_path <- tibble::tibble(
fecha = fechas_sim[-1],
trm_promedio = rowMeans(trm_meses_t),
p5 = apply(trm_meses_t, 1, quantile, probs = 0.05),
p95 = apply(trm_meses_t, 1, quantile, probs = 0.95)
)
ggplot(trm_promedio_path, aes(x = fecha)) +
geom_ribbon(aes(ymin = p5, ymax = p95), alpha = 0.2) +
geom_line(aes(y = trm_promedio), linewidth = 0.7) +
geom_hline(yintercept = K_forward_setfx, linetype = "dashed") +
labs(
title = "TRM simulada vs. precio forward SET-FX",
subtitle = "La línea punteada representa el K forward de mercado usado para la cobertura",
x = "Fecha",
y = "COP/USD"
) +
scale_y_continuous(labels = label_dollar(prefix = "$", big.mark = ".", decimal.mark = ",")) +
theme_minimal()
Para visualizar el efecto de la cobertura, se agregan las cuotas mensuales en flujos anuales. La siguiente tabla muestra los pagos anuales en dólares y el porcentaje cubierto en cada año bajo la interpretación principal de cobertura de los años 6 a 9.
# ============================================================
# FLUJO ANUAL EN USD Y COBERTURA
# ============================================================
flujo_anual_usd <- amortizacion %>%
group_by(anio) %>%
summarise(
Cuotas_USD = sum(cuota_usd),
Intereses_USD = sum(interes),
Capital_USD = sum(abono_capital),
.groups = "drop"
) %>%
mutate(
Porcentaje_cubierto = if_else(anio %in% 6:9, porcentaje_cobertura, 0),
USD_cubiertos = Cuotas_USD * Porcentaje_cubierto,
USD_no_cubiertos = Cuotas_USD - USD_cubiertos,
COP_cubiertos_K_SET_FX = USD_cubiertos * K_forward_setfx
)
flujo_anual_usd %>%
mutate(
across(c(Cuotas_USD, Intereses_USD, Capital_USD, USD_cubiertos, USD_no_cubiertos), fmt_usd),
Porcentaje_cubierto = fmt_pct(Porcentaje_cubierto),
COP_cubiertos_K_SET_FX = fmt_pesos(COP_cubiertos_K_SET_FX)
) %>%
kable(caption = "Flujo anual del crédito y exposición cubierta") %>%
kable_styling(full_width = FALSE)
| anio | Cuotas_USD | Intereses_USD | Capital_USD | Porcentaje_cubierto | USD_cubiertos | USD_no_cubiertos | COP_cubiertos_K_SET_FX |
|---|---|---|---|---|---|---|---|
| 1 | US$11,167 | US$4,533 | US$6,634 | 0,00% | US$0 | US$11,167 | $0 |
| 2 | US$11,167 | US$4,161 | US$7,006 | 0,00% | US$0 | US$11,167 | $0 |
| 3 | US$11,167 | US$3,768 | US$7,399 | 0,00% | US$0 | US$11,167 | $0 |
| 4 | US$11,167 | US$3,353 | US$7,814 | 0,00% | US$0 | US$11,167 | $0 |
| 5 | US$11,167 | US$2,914 | US$8,253 | 0,00% | US$0 | US$11,167 | $0 |
| 6 | US$11,167 | US$2,451 | US$8,716 | 75,00% | US$8,375 | US$2,792 | $33.731.411 |
| 7 | US$11,167 | US$1,962 | US$9,205 | 75,00% | US$8,375 | US$2,792 | $33.731.411 |
| 8 | US$11,167 | US$1,446 | US$9,721 | 75,00% | US$8,375 | US$2,792 | $33.731.411 |
| 9 | US$11,167 | US$900 | US$10,267 | 75,00% | US$8,375 | US$2,792 | $33.731.411 |
| 10 | US$11,167 | US$324 | US$10,843 | 0,00% | US$0 | US$11,167 | $0 |
Análisis. El flujo anual permite identificar que la exposición cubierta no corresponde a todo el crédito, sino únicamente al 75% de las cuotas de los años seleccionados. Esta decisión es razonable porque una cobertura total puede ser costosa y dejaría al inversionista sin beneficiarse de una posible apreciación del peso. La parte no cubierta mantiene exposición a la TRM, pero también conserva flexibilidad. Así, la estrategia elegida es una cobertura parcial: reduce el riesgo de devaluación fuerte, aunque no elimina por completo la incertidumbre cambiaria.
Aunque la cobertura principal se valora con el precio observado en SET-FX, también se calcula el resultado si el forward se pactara al valor teórico obtenido con IBR/SOFR. Esto permite identificar si la diferencia entre mercado y teoría cambia la conveniencia de la cobertura.
resultado_teorico_6_9 <- calcular_cobertura(6:9, K_forward_teorico) %>%
mutate(Tipo_forward = "Teórico IBR/SOFR")
resultado_setfx_6_9 <- resultado_cob_6_9 %>%
mutate(Tipo_forward = "Mercado SET-FX")
comparacion_forward <- bind_rows(resultado_teorico_6_9, resultado_setfx_6_9) %>%
select(Tipo_forward, K_forward, Costo_sin_cobertura_promedio, Costo_con_cobertura_promedio,
Ahorro_promedio, Probabilidad_ahorro_positivo, VaR_5_ahorro)
comparacion_forward %>%
mutate(
K_forward = fmt_pesos(K_forward),
Costo_sin_cobertura_promedio = fmt_pesos(Costo_sin_cobertura_promedio),
Costo_con_cobertura_promedio = fmt_pesos(Costo_con_cobertura_promedio),
Ahorro_promedio = fmt_pesos(Ahorro_promedio),
Probabilidad_ahorro_positivo = fmt_pct(Probabilidad_ahorro_positivo),
VaR_5_ahorro = fmt_pesos(VaR_5_ahorro)
) %>%
kable(caption = "Comparación de cobertura con forward teórico y forward SET-FX") %>%
kable_styling(full_width = FALSE)
| Tipo_forward | K_forward | Costo_sin_cobertura_promedio | Costo_con_cobertura_promedio | Ahorro_promedio | Probabilidad_ahorro_positivo | VaR_5_ahorro |
|---|---|---|---|---|---|---|
| Teórico IBR/SOFR | $3.979 | $585.504.909 | $536.171.492 | $49.333.417 | 82,05% | -$26.367.351 |
| Mercado SET-FX | $4.028 | $585.504.909 | $537.787.223 | $47.717.686 | 80,82% | -$27.983.081 |
Análisis. La comparación entre el forward teórico y el forward SET-FX permite separar dos efectos. El primero es el efecto financiero de cubrirse, que depende de fijar una tasa para comprar dólares en el futuro. El segundo es el efecto del precio pactado, porque una tasa forward más alta encarece la cobertura para quien necesita comprar USD. Si el forward de mercado produce un ahorro menor que el forward teórico, la explicación está en que el precio SET-FX incorpora condiciones de mercado que hacen más costosa la cobertura. Esta comparación fortalece el análisis porque no se limita a aplicar una fórmula, sino que contrasta la teoría con una referencia observable del mercado.
El forward funciona como un seguro cambiario. Si la TRM futura sube por encima del precio pactado, la cobertura reduce el costo de comprar dólares. Si la TRM futura queda por debajo del precio forward, la cobertura no genera ahorro y puede generar un costo de oportunidad, pero aun así reduce la incertidumbre del flujo en pesos. Esta idea es coherente con la naturaleza de los forwards: no son instrumentos diseñados necesariamente para aumentar la rentabilidad esperada, sino para fijar condiciones futuras de compra o venta de un subyacente y administrar el riesgo de precio.
En este caso, el inversionista no busca especular con el dólar, sino proteger parte de los pagos futuros del crédito. Por esa razón, la conveniencia del forward no debe evaluarse únicamente por el ahorro promedio. También debe evaluarse por la reducción de exposición a escenarios de devaluación fuerte del peso colombiano. Si el resultado promedio de la cobertura es inferior al escenario sin cobertura, la interpretación correcta no es que la estrategia esté mal construida, sino que el precio forward incorpora una prima y que el beneficio aparece principalmente cuando la TRM supera el nivel pactado.
La diferencia entre el forward teórico y el forward SET-FX es relevante. El teórico se deriva de la paridad cubierta usando IBR y SOFR; el SET-FX refleja una referencia observable de mercado. Si SET-FX queda por encima del teórico, la cobertura de mercado es relativamente más costosa para quien compra dólares a futuro; si queda por debajo, es relativamente más favorable. En consecuencia, el análisis muestra que el resultado de una cobertura no depende solo de la dirección esperada de la TRM, sino también del precio forward al que se pacte la operación.
El crédito en dólares permite financiar la adquisición de la maquinaria, pero traslada al inversionista una exposición cambiaria importante, porque las cuotas se pagan en USD mientras la inversión económica se mide en COP. La simulación de la TRM muestra que el costo final del crédito puede variar de forma considerable dependiendo del comportamiento futuro del tipo de cambio.
La cobertura con forwards USD/COP sobre el 75% de los pagos desde el sexto año permite fijar una parte relevante del costo futuro en pesos. Esta estrategia protege al inversionista cuando la TRM futura supera el precio forward pactado. Sin embargo, si la TRM queda por debajo del forward, la cobertura puede generar un costo de oportunidad. Por esa razón, el forward debe entenderse como una herramienta de gestión de riesgo y no como una estrategia para maximizar ganancias.
Finalmente, el uso de IBR y SOFR permite construir un precio forward teórico por paridad cubierta de tasas, mientras que el dato de SET-FX permite contrastar ese resultado con una referencia de mercado para forwards USD/COP mayores a seis meses. Esta comparación fortalece el análisis porque permite evaluar si la cobertura se está pactando a una tasa consistente con las condiciones observadas del mercado.
Bloomberg L.P. (2026). ESM6 Index: E-mini S&P 500 Futures contract information. Bloomberg Terminal.
CME Group. (2026). E-mini S&P 500 futures contract specifications and margin information. CME Group.
Hull, J. C. (2018). Options, futures, and other derivatives (10th ed.). Pearson.
Johnson & Johnson. (2026). Annual report and financial information. Johnson & Johnson Investor Relations.
McDonald’s Corporation. (2026). Annual report and financial information. McDonald’s Investor Relations.
The Coca-Cola Company. (2026). Annual report and financial information. The Coca-Cola Company Investor Relations.
Yahoo Finance. (2026). Historical market data for KO, MCD, JNJ and S&P 500. Yahoo Finance.
Banco de la República. (2026). Indicador Bancario de Referencia (IBR): Tasas de interés. https://suameca.banrep.gov.co/estadisticas-economicas/informacionSerie/241/tasas_interes_indicador_bancario_referencia_ibr
Federal Reserve Bank of New York. (2026). SOFR Averages and Index Data. https://www.newyorkfed.org/markets/reference-rates/sofr-averages-and-index
SET-ICAP. (2026). Dólar SET-FX: Estadísticas del mercado cambiario. https://dolar.set-icap.com/estadisticas/
U.S. Small Business Administration. (2026). 504 loans. https://www.sba.gov/funding-programs/loans/504-loans