library(quantmod)
library(ggplot2)
library(knitr)
library(dplyr)
library(lubridate)
library(gridExtra)
library(readr)
library(tidyr)
# Obtener datos históricos de la TRM (USD/COP)
getSymbols("USDCOP=X", src = "yahoo", from = "2022-01-01", to = Sys.Date())
## [1] "USDCOP=X"
trm_daily <- Cl(`USDCOP=X`)
colnames(trm_daily) <- "TRM"
# Convertir a data frame para ggplot
df_trm <- data.frame(
date = index(trm_daily),
TRM = as.numeric(trm_daily$TRM)
)
# Gráfico de la TRM
ggplot(df_trm, aes(x = date, y = TRM)) +
geom_line(color = "steelblue", linewidth = 0.8)+
labs(
title = "Histórico TRM USD/COP",
x = "Fecha",
y = "COP por USD"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
axis.title.y = element_text(margin = margin(r = 10)),
axis.title.x = element_text(margin = margin(t = 10))
)
Al mirar el gráfico histórico de la TRM desde 2022 se nota que el dólar ha sido muy volátil. Hubo un pico muy alto en 2022-2023, cuando llegó incluso por encima de los $4.800. Desde entonces ha tenido subidas y bajadas, pero parece que en los últimos meses ha bajado un poco y se mantiene en un rango más estable.
Factores internacionales que inciden en la TRM
Factores internos en Colombia
library(ggplot2)
# Crear el data frame con las expectativas
expectativas_trm <- data.frame(
Fecha = c("Sep-2025", "Dic-2025", "Sep-2026", "Dic-2026", "Sep-2027"),
Promedio = c(3968, 4056, 4053, 4061, 4090),
Minimo = c(3900, 3869, 3391, 3522, 3555),
Maximo = c(4113, 4250, 4350, 4400, 4500)
)
# Convertir Fecha en factor y crear columna de énfasis sin dplyr
expectativas_trm$Fecha <- factor(expectativas_trm$Fecha, levels = expectativas_trm$Fecha)
expectativas_trm$Enfasis <- ifelse(expectativas_trm$Fecha == "Sep-2026", "Sí", "No")
# Gráfico
ggplot(expectativas_trm, aes(x = Fecha, y = Promedio, color = Enfasis)) +
geom_point(size = 3) +
geom_errorbar(aes(ymin = Minimo, ymax = Maximo),
width = 0.2, linewidth = 0.8) +
scale_color_manual(values = c("Sí" = "firebrick", "No" = "steelblue")) +
labs(
title = "Expectativas de la TRM (USD/COP)",
x = "Fecha",
y = "COP por USD",
caption = "Fuente: Encuesta de Expectativas BanRep (2025)",
color = NULL
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5),
axis.title.y = element_text(margin = margin(r = 10)),
axis.title.x = element_text(margin = margin(t = 10)),
plot.caption = element_text(hjust = 0, face = "italic", size = 10),
legend.position = "none"
)
Según la Encuesta de Expectativas de Analistas Económicos del Banco de la República (2025), el mercado espera que la TRM se mantenga entre $3.391 y $4.350 para los próximos doce meses con un valor promedio de $4.053
# Calcular retornos logarítmicos diarios
retornos_log <- diff(log(trm_daily))
retornos_log <- na.omit(retornos_log)
# Data frame de retornos
retornos_df <- data.frame(
Fecha = index(retornos_log),
Retorno = as.numeric(retornos_log)
)
# Agregar columna Año-Mes
retornos_df <- retornos_df %>%
mutate(AñoMes = format(Fecha, "%Y-%m"))
# Calcular retornos mensuales (compuestos)
retornos_mes <- retornos_df %>%
group_by(AñoMes) %>%
summarise(
RetornoMensual = exp(sum(Retorno, na.rm = TRUE)) - 1
)
#TABLA 1: Resumen estadístico
promedio_mensual <- mean(retornos_mes$RetornoMensual, na.rm = TRUE)
desv_mensual <- sd(retornos_mes$RetornoMensual, na.rm = TRUE)
tabla_resumen <- data.frame(
Estadístico = c("Promedio mensual", "Desviación estándar mensual"),
Valor = c(round(promedio_mensual * 100, 3), round(desv_mensual * 100, 3))
)
kable(tabla_resumen, caption = "Resumen estadístico de retornos mensuales TRM (%)")
Estadístico | Valor |
---|---|
Promedio mensual | -0.039 |
Desviación estándar mensual | 3.469 |
#GRÁFICO: Retornos mensuales
ggplot(retornos_mes, aes(x = AñoMes, y = RetornoMensual * 100)) +
geom_col(fill = "steelblue") +
geom_hline(yintercept = 0, color = "red", linetype = "dashed") +
labs(
title = "Retornos mensuales TRM (USD/COP)",
x = "Periodo",
y = "Retorno mensual (%)"
) +
theme_minimal(base_size = 13) +
theme(
axis.text.x = element_text(angle = 45, hjust = 1, size = 8, margin = margin(t = 5)),
plot.title = element_text(face = "bold", hjust = 0.5)
)
options(scipen = 999)
set.seed(1234)
# Parámetros iniciales desde el punto 2
s0 <- as.numeric(tail(trm_daily, 1)) # Último valor de la TRM
mu <- mean(retornos_mes$RetornoMensual, na.rm = TRUE)
sigma <- sd(retornos_mes$RetornoMensual, na.rm = TRUE)
# Parámetros de simulación
N <- 5000 # Número de trayectorias
T <- 120 # Horizonte: 120 meses
mb <- matrix(ncol = N, nrow = T)
mb[1, ] <- rep(s0, N)
# Simulación MBG mensual
for (i in 1:N) {
for (t in 2:T) {
mb[t, i] <- mb[t - 1, i] * exp((mu - 0.5 * sigma^2) + sigma * rnorm(1))
}
}
#Gráfico de trayectorias simuladas
matplot(
mb,
type = "l",
lty = 1,
main = "Simulación MBG mensual TRM (USD/COP)",
xlab = "Mes",
ylab = "TRM simulada",
lwd = 1
)
# Trayectoria promedio en rojo
lines(rowMeans(mb), col = "red", lwd = 2)
# Cálculo de percentiles (intervalo de confianza)
q1 <- quantile(mb[T, ], 0.975)
q2 <- quantile(mb[T, ], 0.025)
#Distribución final
plot(
density(mb[T, ]),
main = "Distribución simulada de TRM al mes 120",
xlab = "TRM simulada",
lwd = 2
)
abline(v = q1, col = "blue", lwd = 2)
abline(v = q2, col = "green", lwd = 2)
Simulación MBG mensual (gráfico 1): muestra la alta dispersión de escenarios de TRM a 120 meses. Hay trayectorias que se aprecian bajistas e incluso algunas muy alcistas, evidenciando alta volatilidad en el largo plazo.
Distribución de TRM al mes 120 (gráfico 2): la distribución es asimétrica a la derecha, con mayor probabilidad de observar TRM en el rango 2.500–4.000, pero con cola larga que implica riesgo de escenarios devaluatorios muy extremos (> 7.000).
# Parametros iniciales
trm_spot <- as.numeric(last(trm_daily))
monto_cop <- 500000000
inicial_pct <- 0.10
precio_usd <- monto_cop / trm_spot
# Condiciones del crédito
tasa_anual <- 0.06 #TASA NOMINAL MES VENCIDO
tasa_mensual <- tasa_anual / 12 #TASA MENSUAL
n_periodos <- 10 * 12 # 10 años, pagos mensuales
# Monto financiado en USD
loan_usd <- precio_usd * (1 - inicial_pct)
# Fórmula de cuota fija (francés)
pago_mensual_usd <- (tasa_mensual * loan_usd) / (1 - (1 + tasa_mensual)^(-n_periodos))
# Creación de tabla de amortización
amort <- data.frame(
periodo = 1:n_periodos,
saldo = numeric(n_periodos),
interes = numeric(n_periodos),
principal = numeric(n_periodos),
pago = rep(pago_mensual_usd, n_periodos)
)
saldo <- loan_usd
for (t in 1:n_periodos) {
interes_t <- saldo * tasa_mensual
principal_t <- pago_mensual_usd - interes_t
saldo <- saldo - principal_t
amort$saldo[t] <- max(saldo, 0)
amort$interes[t] <- interes_t
amort$principal[t] <- principal_t
}
# Mostrar los primeros 10 periodos de la amortización
head(amort, 10) %>%
knitr::kable(digits = 2, caption = "Amortización tipo francés — primeros 10 pagos")
periodo | saldo | interes | principal | pago |
---|---|---|---|---|
1 | 115838.9 | 582.75 | 711.19 | 1293.95 |
2 | 115124.2 | 579.19 | 714.75 | 1293.95 |
3 | 114405.9 | 575.62 | 718.32 | 1293.95 |
4 | 113683.9 | 572.03 | 721.92 | 1293.95 |
5 | 112958.4 | 568.42 | 725.53 | 1293.95 |
6 | 112229.2 | 564.79 | 729.15 | 1293.95 |
7 | 111496.4 | 561.15 | 732.80 | 1293.95 |
8 | 110760.0 | 557.48 | 736.46 | 1293.95 |
9 | 110019.8 | 553.80 | 740.15 | 1293.95 |
10 | 109276.0 | 550.10 | 743.85 | 1293.95 |
# Mostrar resumen total en tabla
total_pagado <- sum(amort$pago)
total_interes <- sum(amort$interes)
resumen_credito <- data.frame(
Concepto = c("Monto financiado (USD)",
"Pago mensual (USD)",
"Total pagado en 10 años (USD)",
"Interés total pagado (USD)"),
Valor = c(round(loan_usd, 2),
round(pago_mensual_usd, 2),
round(total_pagado, 2),
round(total_interes, 2))
)
knitr::kable(resumen_credito,
digits = 2,
caption = "Resumen del crédito en dólares — Sistema francés")
Concepto | Valor |
---|---|
Monto financiado (USD) | 116550.12 |
Pago mensual (USD) | 1293.95 |
Total pagado en 10 años (USD) | 155273.43 |
Interés total pagado (USD) | 38723.31 |
# Crear tabla de pagos en USD
pagos_usd <- amort$pago
# Convertir cada pago mensual a COP según cada trayectoria simulada
pagos_cop_sim <- matrix(nrow = nrow(amort), ncol = ncol(mb))
for (i in 1:ncol(mb)) {
pagos_cop_sim[, i] <- pagos_usd * mb[, i] # usa cada TRM simulada mes a mes
}
# Calcular métricas de interés
total_cop_sim <- colSums(pagos_cop_sim) # total pagado en COP por simulación
# Resumen de percentiles y media
resumen_cop <- data.frame(
Escenario = c("Pesimista (p95)", "Promedio", "Optimista (p5)"),
TotalPagadoCOP = c(
quantile(total_cop_sim, 0.95),
mean(total_cop_sim),
quantile(total_cop_sim, 0.05)
)
)
knitr::kable(resumen_cop, digits = 0,
caption = "Total pagado en COP bajo diferentes escenarios de TRM")
Escenario | TotalPagadoCOP | |
---|---|---|
95% | Pesimista (p95) | 829387334 |
Promedio | 588301179 | |
5% | Optimista (p5) | 407251655 |
# Histograma de los pagos totales en COP (valores completos)
hist(total_cop_sim,
breaks = 50,
col = "skyblue",
border = "white",
main = "Distribución de pagos totales en COP",
xlab = "Total pagado (COP)",
ylab = "Frecuencia")
## Warning in breaks[-1L] + breaks[-nB]: NAs producidos por enteros excedidos
# Añadir líneas verticales para media y percentiles
media <- mean(total_cop_sim)
p5 <- quantile(total_cop_sim, 0.05)
p95 <- quantile(total_cop_sim, 0.95)
abline(v = media, col = "orange", lwd = 2, lty = 2)
abline(v = p5, col = "green", lwd = 2, lty = 2)
abline(v = p95, col = "red", lwd = 2, lty = 2)
legend("topright",
legend = c("Media", "Optimista (p5)", "Pesimista (p95)"),
col = c("orange", "green", "red"), lwd = 2, lty = 2)
El histograma muestra la distribución de los pagos totales del crédito en pesos colombianos bajo diferentes trayectorias simuladas de la TRM. La línea naranja punteada indica el pago promedio que representa el valor esperado del total a pagar considerando la volatilidad histórica de la TRM. La línea verde (p5) marca un escenario optimista, en el que los pagos totales podrían ser más bajos que el valor promedio, mientras que la línea roja (p95) representa un escenario pesimista, donde los pagos podrían ascender más allá del valor promedio. Esto evidencia que existe un rango de incertidumbre significativo, reflejando la sensibilidad del crédito frente a la variación del tipo de cambio.
#Cargar datos
datos_futuro <- read_csv2(file.choose())
## ℹ Using "','" as decimal and "'.'" as grouping mark. Use `read_delim()` for more control.
## Rows: 37 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ";"
## chr (2): Fecha, Contrato
## num (1): Precio cierre
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
datos_futuro <- datos_futuro %>%
mutate(
Fecha = dmy(Fecha),
Precio_cierre = as.numeric(gsub(",", ".", `Precio cierre`))
) %>%
arrange(Fecha)
#2. Gráfico histórico
ggplot(datos_futuro, aes(x = Fecha, y = Precio_cierre)) +
geom_line(color = "darkgreen", linewidth = 1) +
geom_point(color = "black", size = 1) +
labs(
title = "Histórico Precio Futuro sobre TRM TRXU25F",
x = "Fecha",
y = "Precio de Cierre"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", hjust = 0.5)
)
#3. Cálculo de retornos mensuales y desviación estándar
# Crear columna de mes y año para agrupar
datos_futuro <- datos_futuro %>%
mutate(
ano_mes = floor_date(Fecha, "month")
)
# Calcular retornos logarítmicos diarios
datos_futuro <- datos_futuro %>%
mutate(
retorno_diario = log(Precio_cierre / lag(Precio_cierre))
)
# Calcular retornos mensuales (suma de log-retornos dentro de cada mes)
retornos_mensuales <- datos_futuro %>%
group_by(ano_mes) %>%
summarise(retorno_mensual = sum(retorno_diario, na.rm = TRUE))
# Calcular estadísticas
promedio_mensual <- mean(retornos_mensuales$retorno_mensual, na.rm = TRUE)
desviacion_mensual <- sd(retornos_mensuales$retorno_mensual, na.rm = TRUE)
# Mostrar resultados
cat("Promedio de retornos mensuales: ", round(promedio_mensual, 6), "\n")
## Promedio de retornos mensuales: -0.009205
cat("Desviación estándar mensual: ", round(desviacion_mensual, 6), "\n")
## Desviación estándar mensual: 0.024115
set.seed(123)
# Variables de entrada
tasa_usd_anual <- 0.05 # rf usa
tasa_cop_anual <- 0.12 # rf colombia
horizonte_meses <- 120 # 10 años
n_caminos <- 5000
spot_ultimo <- tail(datos_futuro$Precio_cierre, 1)
#Calcular forward para el primer mes (T = 1/12 años)
T1 <- 1/12
forward_primer_mes <- spot_ultimo * exp((tasa_cop_anual - tasa_usd_anual) * T1)
# Simulación múltiple MBG pero: fijamos t = 1 (primer mes simulado) = forward_primer_mes
mu_mensual <- promedio_mensual
sigma_mensual <- desviacion_mensual
n_meses <- horizonte_meses
# Matriz de simulaciones
matriz_sim <- matrix(NA, nrow = n_meses + 1, ncol = n_caminos)
matriz_sim[1, ] <- spot_ultimo
matriz_sim[2, ] <- forward_primer_mes
# Generar los demás pasos a partir de t=2 -> t=n_meses
for (j in 1:n_caminos) {
for (i in 3:(n_meses + 1)) {
epsilon <- rnorm(1)
matriz_sim[i, j] <- matriz_sim[i - 1, j] *
exp((mu_mensual - 0.5 * sigma_mensual^2) + sigma_mensual * epsilon)
}
}
# Data frame largo para ggplot
fechas_sim <- seq(from = max(datos_futuro$Fecha) %m+% months(1),
by = "1 month",
length.out = n_meses + 1)
df_sim <- data.frame(
Fecha = rep(fechas_sim, times = n_caminos),
Precio_sim = as.vector(matriz_sim),
Camino = rep(1:n_caminos, each = n_meses + 1)
)
#Promedio por fecha (línea destacada)
df_prom <- df_sim %>%
group_by(Fecha) %>%
summarise(Promedio = mean(Precio_sim, na.rm = TRUE))
#Modelo simple de rollover mensual (precios de contrato near-month)
serie_promedio_spot <- df_prom$Promedio
n <- length(serie_promedio_spot)
# Calcular forwards 1m sobre la serie promedio de spot
forwards_1m <- serie_promedio_spot * exp((tasa_cop_anual - tasa_usd_anual) * (1/12))
# Modelación rollover
precio_apertura_contrato <- forwards_1m[1:(n-1)]
precio_cierre_contrato <- serie_promedio_spot[2:n]
pnl_rollover <- precio_cierre_contrato - precio_apertura_contrato
df_rollover <- data.frame(
Fecha_apertura = fechas_sim[1:(n-1)],
Apertura = precio_apertura_contrato,
Cierre = precio_cierre_contrato,
PnL = pnl_rollover
)
# Gráfico: todas las trayectorias (gris), promedio (azul) y bloque años 6-10 resaltado ---
inicio_rango <- fechas_sim[1] %m+% years(5)
fin_rango <- fechas_sim[1] %m+% years(10)
ggplot() +
annotate("rect", xmin = inicio_rango, xmax = fin_rango, ymin = -Inf, ymax = Inf,
alpha = 0.18, fill = "orange") +
geom_line(data = df_sim, aes(x = Fecha, y = Precio_sim, group = Camino, color = "Trayectorias"),
alpha = 0.28, linewidth = 0.4) +
geom_line(data = df_prom, aes(x = Fecha, y = Promedio, color = "Promedio"), linewidth = 1.1) +
geom_point(data = data.frame(Fecha = fechas_sim[2], Valor = forward_primer_mes),
aes(x = Fecha, y = Valor, color = "Forward 1m (primer mes)"), size = 2) +
scale_color_manual(values = c("Trayectorias" = "gray",
"Promedio" = "steelblue",
"Forward 1m (primer mes)" = "red")) +
labs(title = "Simulación BMG del futuro TRM — 10 años (años 6–10 resaltados)",
subtitle = "Forward 1er mes usado como insumo inicial; rollover mensual modelado sobre serie promedio",
x = "Fecha", y = "Precio") +
theme_minimal() +
theme(legend.position = "top", plot.title = element_text(face = "bold", hjust = 0.5))
# Supuestos iniciales (traídos de la parte 1)
trm_spot <- as.numeric(last(trm_daily)) # TRM spot al momento de la compra
monto_cop <- 500000000 # 500 millones COP
inicial_pct <- 0.10 # 10% de cuota inicial
# Valor de la maquinaria en USD
precio_usd <- monto_cop / trm_spot
# Monto financiado en USD (90% restante)
loan_usd <- precio_usd * (1 - inicial_pct)
# Cobertura: 75% del monto financiado
porc_cubierto <- 0.75
exposicion_usd <- loan_usd * porc_cubierto
# Parámetros del contrato de futuros
tamano_contrato_usd <- 1000 # Tamaño del contrato
margen_inicial_pct <- 0.08 # 8%
margen_mantenimiento_pct <- 0.03 # 3%
# Número de contratos (usando ceiling para cubrir >=75%)
numero_contratos <- ceiling(exposicion_usd / tamano_contrato_usd)
# Valor nocional de la posición
valor_nocional_usd <- numero_contratos * tamano_contrato_usd
# Márgenes en USD
margen_inicial_usd <- valor_nocional_usd * margen_inicial_pct
margen_mantenimiento_usd <- valor_nocional_usd * margen_mantenimiento_pct
# Mostrar resultados
cat("Valor total maquinaria: ", round(precio_usd, 2), "USD\n")
## Valor total maquinaria: 129500.1 USD
cat("Monto financiado (90%): ", round(loan_usd, 2), "USD\n")
## Monto financiado (90%): 116550.1 USD
cat("Exposición cubierta (75%): ", round(exposicion_usd, 2), "USD\n")
## Exposición cubierta (75%): 87412.59 USD
cat("Número de contratos necesarios: ", numero_contratos, "\n")
## Número de contratos necesarios: 88
cat("Valor nocional cubierto: ", round(valor_nocional_usd, 2), "USD\n")
## Valor nocional cubierto: 88000 USD
cat("Margen inicial requerido: ", round(margen_inicial_usd, 2), "USD\n")
## Margen inicial requerido: 7040 USD
cat("Margen de mantenimiento: ", round(margen_mantenimiento_usd, 2), "USD\n")
## Margen de mantenimiento: 2640 USD
#Selección de últimos 48 meses
n_total <- nrow(df_prom)
inicio <- n_total - 48
fin <- n_total
serie_precio <- df_prom$Promedio
fechas <- df_prom$Fecha
#Inicialización
saldo_margen <- margen_inicial_usd
contratos_actuales <- numero_contratos
valor_nocional_usd <- contratos_actuales * tamano_contrato_usd
#Data frame para almacenar resultados
registro <- data.frame(
Fecha = as.Date(character()),
Contratos = integer(),
Precio_mes = numeric(),
Precio_sig_mes = numeric(),
PnL_USD = numeric(),
Saldo_margen_antes = numeric(),
Llamada_margen = numeric(),
Ajuste_posicion = numeric(),
Saldo_margen_despues = numeric(),
stringsAsFactors = FALSE
)
# Medias móviles para decisión de rollover
media_corta <- stats::filter(serie_precio, rep(1/3, 3), sides = 1)
media_larga <- stats::filter(serie_precio, rep(1/12, 12), sides = 1)
for(i in inicio:(fin - 1)) {
fecha <- fechas[i]
precio_t <- serie_precio[i]
precio_t1 <- serie_precio[i + 1]
# P&L en COP y luego en USD
pnl_cop_contrato <- (precio_t1 - precio_t) * tamano_contrato_usd
pnl_usd_contrato <- pnl_cop_contrato / precio_t1
pnl_total_usd <- pnl_usd_contrato * contratos_actuales
saldo_antes <- saldo_margen
saldo_margen <- saldo_margen + pnl_total_usd
# Verificar llamada de margen
llamada_margen <- 0
if(saldo_margen < margen_mantenimiento_usd) {
llamada_margen <- margen_inicial_usd - saldo_margen
saldo_margen <- saldo_margen + llamada_margen
}
# Regla de cambio de posición
ajuste_posicion <- 0
ma_corta <- media_corta[i + 1]
ma_larga <- media_larga[i + 1]
if(!is.na(ma_corta) && !is.na(ma_larga)) {
nuevos_contratos <- contratos_actuales
if(ma_corta > ma_larga) {
nuevos_contratos <- contratos_actuales + 1
} else if(ma_corta < ma_larga) {
nuevos_contratos <- max(0, contratos_actuales - 1)
}
if(nuevos_contratos != contratos_actuales) {
delta_nocional <- (nuevos_contratos - contratos_actuales) * tamano_contrato_usd
ajuste_posicion <- delta_nocional * 0.08
saldo_margen <- saldo_margen - ajuste_posicion
contratos_actuales <- nuevos_contratos
valor_nocional_usd <- contratos_actuales * tamano_contrato_usd
margen_inicial_usd <- valor_nocional_usd * 0.08
margen_mantenimiento_usd <- valor_nocional_usd * 0.03
if(saldo_margen < margen_mantenimiento_usd) {
extra_call <- margen_inicial_usd - saldo_margen
llamada_margen <- llamada_margen + extra_call
saldo_margen <- saldo_margen + extra_call
}
}
}
# Registrar fila
registro <- registro %>% add_row(
Fecha = fecha,
Contratos = contratos_actuales,
Precio_mes = precio_t,
Precio_sig_mes = precio_t1,
PnL_USD = pnl_total_usd,
Saldo_margen_antes = saldo_antes,
Llamada_margen = llamada_margen,
Ajuste_posicion = -ajuste_posicion, # negativo si es depósito
Saldo_margen_despues = saldo_margen
)
}
# Calcular flujo neto
registro <- registro %>%
mutate(
Flujo_PnL = PnL_USD,
Flujo_Llamada = -Llamada_margen,
Flujo_Ajuste = Ajuste_posicion,
Flujo_Neto = Flujo_PnL + Flujo_Llamada + Flujo_Ajuste
)
# Mostrar primeras filas
print(head(registro, 12))
## Fecha Contratos Precio_mes Precio_sig_mes PnL_USD Saldo_margen_antes
## 1 2031-10-09 87 2065.560 2047.227 -788.0342 7040.000
## 2 2031-11-09 86 2047.227 2028.193 -816.4531 6331.966
## 3 2031-12-09 85 2028.193 2008.171 -857.4497 5595.513
## 4 2032-01-09 84 2008.171 1988.154 -855.8240 4818.063
## 5 2032-02-09 83 1988.154 1969.458 -797.4087 4042.239
## 6 2032-03-09 82 1969.458 1951.143 -779.0900 3324.830
## 7 2032-04-09 81 1951.143 1933.258 -758.5774 2625.740
## 8 2032-05-09 80 1933.258 1916.551 -706.0930 6640.000
## 9 2032-06-09 79 1916.551 1898.805 -747.6892 6013.907
## 10 2032-07-09 78 1898.805 1880.345 -775.5687 5346.218
## 11 2032-08-09 77 1880.345 1863.365 -710.7726 4650.649
## 12 2032-09-09 76 1863.365 1846.478 -704.2104 4019.876
## Llamada_margen Ajuste_posicion Saldo_margen_despues Flujo_PnL Flujo_Llamada
## 1 0.000 80 6331.966 -788.0342 0.000
## 2 0.000 80 5595.513 -816.4531 0.000
## 3 0.000 80 4818.063 -857.4497 0.000
## 4 0.000 80 4042.239 -855.8240 0.000
## 5 0.000 80 3324.830 -797.4087 0.000
## 6 0.000 80 2625.740 -779.0900 0.000
## 7 4692.837 80 6640.000 -758.5774 -4692.837
## 8 0.000 80 6013.907 -706.0930 0.000
## 9 0.000 80 5346.218 -747.6892 0.000
## 10 0.000 80 4650.649 -775.5687 0.000
## 11 0.000 80 4019.876 -710.7726 0.000
## 12 0.000 80 3395.666 -704.2104 0.000
## Flujo_Ajuste Flujo_Neto
## 1 80 -708.0342
## 2 80 -736.4531
## 3 80 -777.4497
## 4 80 -775.8240
## 5 80 -717.4087
## 6 80 -699.0900
## 7 80 -5371.4145
## 8 80 -626.0930
## 9 80 -667.6892
## 10 80 -695.5687
## 11 80 -630.7726
## 12 80 -624.2104
# --- Gráficos ---
# 1. Saldo de margen
g1 <- ggplot(registro, aes(x = Fecha, y = Saldo_margen_despues)) +
geom_line(color = "steelblue", size = 1) +
geom_hline(yintercept = max(margen_inicial_usd, 0), linetype = "dashed", color = "darkgreen") +
geom_hline(yintercept = max(margen_mantenimiento_usd, 0), linetype = "dotted", color = "red") +
labs(title = "Saldo de la cuenta de margen (USD)",
y = "Saldo en margen (USD)",
x = "") +
theme_minimal()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
# 2. Flujo neto
g2 <- ggplot(registro, aes(x = Fecha, y = Flujo_Neto)) +
geom_col(fill = "gray") +
labs(title = "Flujos netos mensuales (USD)",
y = "Flujo neto",
x = "") +
theme_minimal()
# 3. Número de contratos
g3 <- ggplot(registro, aes(x = Fecha, y = Contratos)) +
geom_step(color = "orange", size = 1) +
labs(title = "Número de contratos por mes",
y = "Contratos",
x = "") +
theme_minimal()
library(gridExtra)
grid.arrange(g1, g2, g3, ncol = 1)
El gráfico superior muestra un patrón cíclico donde el saldo de la cuenta de margen va cayendo hasta niveles cercanos al margen de mantenimiento, lo que genera llamados de margen recurrentes para volver al margen inicial.La frecuencia de las llamadas al margen indica que el movimiento del subyacente ha estado en contra de la cobertura en varias ocasiones, pero no en un grado extremo que vacíe la cuenta por completo. Esto muestra que el tamaño de la posición y el margen inicial absorben la volatilidad sin disparar demasiadas llamadas de margen.
Los flujos negativos se concentran en meses de fuertes movimientos adversos, lo que coincide con los eventos de ajuste de margen. La magnitud de estos flujos (algunos meses cercanos a -4.000 USD) es relevante porque impacta la liquidez de la empresa. Sin embargo, los meses intermedios muestran flujos pequeños o cercanos a cero, lo que indica que la estrategia de cobertura no genera pérdidas constantes sino que responde a episodios específicos de volatilidad.
La línea descendente en el gráfico inferior refleja una reducción gradual del número de contratos, lo que es consistente con la amortización del crédito subyacente.Cada rollover ajusta la posición para evitar sobrecobertura, ya que evita pagar margen innecesario sobre nocional que ya no corresponde a deuda viva.
# Asegurar formato fechas en registro
registro$Fecha <- as.Date(registro$Fecha)
registro <- registro %>%
mutate(TRM_mes = Precio_sig_mes)
# Calcular flujo neto de margen en COP
registro <- registro %>%
mutate(
FlujoNeto_COP = Flujo_Neto * TRM_mes
)
# Calcular pago del crédito en COP
registro <- registro %>%
mutate(
PagoCredito_USD = pago_mensual_usd,
PagoCredito_COP = PagoCredito_USD * TRM_mes
)
# Flujo mensual sin cobertura (solo pago del crédito en COP)
registro <- registro %>%
mutate(
Flujo_sin_cobertura_COP = -PagoCredito_COP
)
# Flujo mensual con cobertura: pago del crédito menos efecto del futuro
registro <- registro %>%
mutate(
Flujo_con_cobertura_COP = -PagoCredito_COP - (-FlujoNeto_COP)
) %>%
mutate(
Flujo_con_cobertura_COP = -PagoCredito_COP + FlujoNeto_COP
)
# Resumen
registro <- registro %>%
arrange(Fecha) %>%
mutate(
Acum_sin_cobertura = cumsum(Flujo_sin_cobertura_COP),
Acum_con_cobertura = cumsum(Flujo_con_cobertura_COP),
Dif_mensual = Flujo_con_cobertura_COP - Flujo_sin_cobertura_COP,
Acum_ahorro = Acum_sin_cobertura - Acum_con_cobertura # positivo si hedge ahorra COP
)
# Tabla resumen: totales en período
total_sin <- last(registro$Acum_sin_cobertura)
total_con <- last(registro$Acum_con_cobertura)
ahorro_total <- total_sin - total_con
porc_ahorro <- ahorro_total / abs(total_sin) * 100
resumen <- data.frame(
Item = c("Costo total sin cobertura (COP)", "Costo total con cobertura (COP)", "Ahorro neto (COP)", "Ahorro neto (%)"),
Valor = c(total_sin, total_con, ahorro_total, porc_ahorro)
)
# Mostrar resumen con formato
resumen$Valor <- round(resumen$Valor, 0)
print(resumen)
## Item Valor
## 1 Costo total sin cobertura (COP) -103210427
## 2 Costo total con cobertura (COP) -180390593
## 3 Ahorro neto (COP) 77180166
## 4 Ahorro neto (%) 75
#Gráficos
# 1) Serie mensual: pago crédito (COP), efecto hedge (FlujoNeto_COP), y neto
g_mes <- ggplot(registro, aes(x = Fecha)) +
geom_col(aes(y = -PagoCredito_COP, fill = "Pago crédito (COP)"), width = 25) +
geom_line(aes(y = FlujoNeto_COP, color = "Flujos netos futuros (COP)"), size = 1) +
geom_line(aes(y = Flujo_con_cobertura_COP, color = "Flujo neto con cobertura (COP)"), size = 1, linetype = "dashed") +
scale_y_continuous(labels = scales::comma) +
scale_color_manual(name = "", values = c("Flujos netos futuros (COP)" = "darkgreen", "Flujo neto con cobertura (COP)" = "steelblue")) +
scale_fill_manual(name = "", values = c("Pago crédito (COP)" = "gray70")) +
labs(title = "Comparación mensual: costo crédito vs flujos futuros (COP)",
subtitle = "Columnas: pago crédito mensual en COP (negativo). Línea verde: flujos netos del futuro. Línea punteada: costo neto con cobertura.",
x = "", y = "COP") +
theme_minimal() +
theme(legend.position = "top")
# 2) Acumulado comparativo
df_acum <- registro %>%
select(Fecha, Acum_sin_cobertura, Acum_con_cobertura) %>%
pivot_longer(cols = c(Acum_sin_cobertura, Acum_con_cobertura),
names_to = "Escenario",
values_to = "Acumulado") %>%
mutate(Escenario = ifelse(Escenario == "Acum_sin_cobertura", "Sin cobertura", "Con cobertura"))
g_acum <- ggplot(df_acum, aes(x = Fecha, y = Acumulado, color = Escenario)) +
geom_line(size = 1) +
scale_y_continuous(labels = scales::comma) +
scale_color_manual(values = c("Sin cobertura" = "firebrick", "Con cobertura" = "steelblue")) +
labs(title = "Costo acumulado en COP: sin cobertura vs con cobertura",
subtitle = paste0("Periodo: ", format(first(registro$Fecha), "%Y-%m"), " a ", format(last(registro$Fecha), "%Y-%m")),
x = "", y = "COP") +
theme_minimal() +
theme(legend.position = "top")
# ostrar gráficos
print(g_mes)
print(g_acum)
#Resumen
cat("\nResumen numérico (últimos 48 meses):\n")
##
## Resumen numérico (últimos 48 meses):
cat("Costo total SIN cobertura (COP): ", format(round(total_sin,0), big.mark = ","), "\n")
## Costo total SIN cobertura (COP): -103,210,427
cat("Costo total CON cobertura (COP): ", format(round(total_con,0), big.mark = ","), "\n")
## Costo total CON cobertura (COP): -180,390,593
cat("Ahorro neto aportado por la cobertura (COP): ", format(round(ahorro_total,0), big.mark = ","), "\n")
## Ahorro neto aportado por la cobertura (COP): 77,180,166
cat("Ahorro neto porcentual sobre costo sin cobertura: ", round(porc_ahorro,2), "%\n\n")
## Ahorro neto porcentual sobre costo sin cobertura: 74.78 %
El primer gráfico muestra que los flujos netos con cobertura (línea azul) son más negativos que los pagos del crédito y que los propios flujos de futuros (línea verde). Esto evidencia que las liquidaciones de los contratos generaron salidas de caja adicionales en la mayoría de los meses, especialmente en los de rollover, donde los picos se hacen más pronunciados. En términos prácticos, la cobertura no suavizó la serie de pagos, sino que aumentó su volatilidad y profundizó las pérdidas en varios momentos.
El segundo gráfico refuerza esta conclusión: el costo acumulado con cobertura (línea azul) cae a un ritmo más acelerado que el escenario sin cobertura (línea roja), ampliando la brecha entre ambos escenarios. Esto significa que, a lo largo del tiempo, la cobertura fue generando un efecto financiero adverso, sumando costo en lugar de compensar el riesgo de tipo de cambio.
El cálculo de retornos y desviaciones mensuales permitió entender la volatilidad del futuro TRM y fue una base sólida para la simulación de escenarios.
Las trayectorias de MBG mostraron un rango amplio de posibles precios, y la línea promedio permitió ver la tendencia esperada
Exposición y márgenes claros: Se cuantificó la exposición necesaria para cubrir el 75% de la deuda en USD y se incorporaron márgenes iniciales y de mantenimiento, mostrando el impacto real en liquidez.
La simulación de la cuenta de margen evidenció la necesidad de capital adicional para mantener la posición y ajustar la cobertura según la expectativa de mercado.
Aunque la cobertura protegió contra escenarios adversos de TRM, el costo total fue mayor y generó flujos de caja más exigentes
Banco de la República de Colombia. (2025). Resultados mensuales: Expectativas de analistas económicos. Recuperado de https://www.banrep.gov.co/es/resultados-mensuales-expectativas-analistas-economicos
Investing.com. (2025). U.S. 10-Year Bond Yield. Recuperado de https://www.investing.com/rates-bonds/u.s.-10-year-bond-yield-streaming-chart
Investing.com. (2025). Colombia 10-Year Bond Yield. Recuperado de https://www.investing.com/rates-bonds/colombia-10-year-bond-yield
Bolsa de Valores de Colombia. (2025). TRXU25F — resumen [Mercado de derivados]. Recuperado de https://www.bvc.com.co/derivados/trxu25f?tab=resumen