# Instalar si no están disponibles
paquetes <- c("quantmod", "ggplot2", "dplyr", "tidyr", "scales",
"knitr", "kableExtra", "lubridate", "ggthemes",
"gridExtra", "moments")
instalar <- paquetes[!paquetes %in% installed.packages()[,"Package"]]
if (length(instalar) > 0) install.packages(instalar, repos = "https://cran.rstudio.com/")
library(quantmod)
library(ggplot2)
library(dplyr)
library(tidyr)
library(scales)
library(knitr)
library(kableExtra)
library(lubridate)
library(ggthemes)
library(gridExtra)
library(moments)La Tasa Representativa del Mercado (TRM) del peso colombiano frente al dólar estadounidense (USDCOP) está determinada por los siguientes factores fundamentales al 28 de marzo de 2026:
| Factor | Valor | Impacto sobre TRM |
|---|---|---|
| TRM actual | $3.675 COP/USD | — |
| Inflación Colombia 2025 | 5,29% anual | Presión alcista (depreciación COP) |
| Tasa política Banrep | 10,25% EA | Soporte moderado al COP |
| Fed Funds Rate EE.UU. | 3,50%–3,75% | Diferencial favorable al COP |
| Diferencial de tasas | ~6,6 pp | Implica depreciación estructural por paridad |
| Déficit fiscal Colombia | −7,1% PIB | Presión alcista al dólar |
| Precio petróleo Brent | USD 100/barril | Soporte al COP (ingresos exportación) |
| Proyección TRM dic/2026 | ~$3.800–$3.900 | Depreciación gradual esperada |
Fuentes: Banco de la República de Colombia, Infobae (mar/2026), El Colombiano (mar/2026), Davivienda Visión Institucional 2026.
Conclusión del análisis fundamental: El diferencial de tasas entre Colombia (10,25%) y EE.UU. (~3,625%) crea una presión estructural de depreciación del COP por paridad descubierta de intereses (UIP). Se proyecta que la TRM cierre 2026 entre $3.800 y $3.900. Esta depreciación justifica el uso de forward de divisas como instrumento de cobertura.
# Descarga de datos históricos USDCOP desde Yahoo Finance
# Período: últimos 10 años (2016–2026)
getSymbols("COP=X", src = "yahoo",
from = Sys.Date() - 365*10,
to = Sys.Date(),
auto.assign = TRUE)## [1] "COP=X"
# Extraer precios de cierre
trm_xts <- Cl(`COP=X`)
colnames(trm_xts) <- "TRM"
# Convertir a data.frame
trm_df <- data.frame(
Fecha = index(trm_xts),
TRM = as.numeric(trm_xts)
) %>% filter(!is.na(TRM))
cat("Observaciones descargadas:", nrow(trm_df), "\n")## Observaciones descargadas: 2603
## Fecha inicio: 2016-03-29
## Fecha fin: 2026-03-28
## TRM más reciente: $ 3671.78
ggplot(trm_df, aes(x = Fecha, y = TRM)) +
geom_line(color = "#2c7bb6", linewidth = 0.7) +
geom_smooth(method = "loess", se = TRUE, color = "#d7191c",
fill = "#f7b6b6", alpha = 0.3, linewidth = 0.8) +
geom_hline(yintercept = mean(trm_df$TRM, na.rm = TRUE),
linetype = "dashed", color = "#666666", linewidth = 0.5) +
annotate("text", x = max(trm_df$Fecha),
y = mean(trm_df$TRM, na.rm = TRUE) + 80,
label = paste0("Media: $", round(mean(trm_df$TRM, na.rm = TRUE))),
size = 3.5, color = "#444444", hjust = 1) +
scale_y_continuous(labels = dollar_format(prefix = "$", big.mark = ".",
decimal.mark = ",")) +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
labs(title = "TRM USDCOP — Últimos 10 Años",
subtitle = "Línea roja: tendencia LOESS | Línea gris: media histórica",
x = "Fecha", y = "TRM (COP/USD)") +
theme_minimal(base_size = 12) +
theme(axis.text.x = element_text(angle = 45, hjust = 1),
plot.title = element_text(face = "bold"))# Parámetros para proyección con paridad de tasas de interés
trm_spot <- tail(trm_df$TRM, 1) # Spot actual
tasa_col <- 0.1025 # Banrep 10,25%
tasa_usa <- 0.035 # Fed Funds ~3,5%
horizonte <- 12 # Meses
# Proyección mensual por paridad de tasas
meses <- 1:horizonte
trm_proy_base <- trm_spot * ((1 + tasa_col) / (1 + tasa_usa))^(meses / 12)
# Banda de confianza ±1 desv. estándar
retornos_m <- diff(log(trm_df$TRM))
sigma_m <- sd(retornos_m, na.rm = TRUE)
trm_proy_alto <- trm_proy_base * exp(1.645 * sigma_m * sqrt(meses))
trm_proy_bajo <- trm_proy_base * exp(-1.645 * sigma_m * sqrt(meses))
proy_df <- data.frame(
Mes = meses,
Fecha = seq(Sys.Date(), by = "month", length.out = horizonte),
Base = round(trm_proy_base, 0),
Alto_90 = round(trm_proy_alto, 0),
Bajo_90 = round(trm_proy_bajo, 0)
)
# Tabla resumen
proy_df %>%
select(Mes, Fecha, Bajo_90, Base, Alto_90) %>%
rename(`Mes` = Mes,
`Fecha proy.` = Fecha,
`Escenario bajo (P5)` = Bajo_90,
`Escenario base` = Base,
`Escenario alto (P95)` = Alto_90) %>%
kbl(caption = "Proyección TRM a 12 meses — Paridad de Tasas de Interés",
align = "c", format.args = list(big.mark = ".", decimal.mark = ",")) %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE) %>%
column_spec(3, color = "#2c7bb6") %>%
column_spec(4, bold = TRUE) %>%
column_spec(5, color = "#d7191c")| Mes | Fecha proy. | Escenario bajo (P5) | Escenario base | Escenario alto (P95) |
|---|---|---|---|---|
| 1 | 2026-03-28 | 3.629 | 3.691 | 3.755 |
| 2 | 2026-04-28 | 3.622 | 3.711 | 3.802 |
| 3 | 2026-05-28 | 3.621 | 3.730 | 3.842 |
| 4 | 2026-06-28 | 3.624 | 3.750 | 3.880 |
| 5 | 2026-07-28 | 3.628 | 3.770 | 3.917 |
| 6 | 2026-08-28 | 3.634 | 3.790 | 3.952 |
| 7 | 2026-09-28 | 3.641 | 3.810 | 3.986 |
| 8 | 2026-10-28 | 3.649 | 3.830 | 4.020 |
| 9 | 2026-11-28 | 3.657 | 3.850 | 4.053 |
| 10 | 2026-12-28 | 3.666 | 3.870 | 4.085 |
| 11 | 2027-01-28 | 3.676 | 3.891 | 4.118 |
| 12 | 2027-02-28 | 3.686 | 3.911 | 4.150 |
# Punto histórico reciente para contexto
hist_recent <- trm_df %>% filter(Fecha >= Sys.Date() - 180)
ggplot() +
geom_line(data = hist_recent, aes(x = Fecha, y = TRM),
color = "#555555", linewidth = 0.8) +
geom_ribbon(data = proy_df,
aes(x = Fecha, ymin = Bajo_90, ymax = Alto_90),
fill = "#2c7bb6", alpha = 0.15) +
geom_line(data = proy_df, aes(x = Fecha, y = Base),
color = "#2c7bb6", linewidth = 1, linetype = "dashed") +
geom_line(data = proy_df, aes(x = Fecha, y = Alto_90),
color = "#d7191c", linewidth = 0.6, linetype = "dotted") +
geom_line(data = proy_df, aes(x = Fecha, y = Bajo_90),
color = "#1a9641", linewidth = 0.6, linetype = "dotted") +
scale_y_continuous(labels = dollar_format(prefix = "$", big.mark = ".")) +
scale_x_date(date_breaks = "2 months", date_labels = "%b/%y") +
labs(title = "Proyección TRM a 12 Meses — Paridad de Tasas de Interés",
subtitle = "Azul: escenario base | Banda: intervalo 90% confianza",
x = "Fecha", y = "TRM COP/USD") +
theme_minimal(base_size = 12) +
theme(axis.text.x = element_text(angle = 45, hjust = 1),
plot.title = element_text(face = "bold"))# ── PARÁMETROS DEL CRÉDITO ──────────────────────────────────────────────
inversion_cop <- 350e6 # $350.000.000 COP
trm_actual <- trm_spot # TRM vigente
inversion_usd <- inversion_cop / trm_actual # Valor en USD
pago_inicial_pct <- 0.10 # 10% inicial
pago_inicial_usd <- inversion_usd * pago_inicial_pct
monto_credito <- inversion_usd * (1 - pago_inicial_pct) # 90%
# Tasa Bank of America — Préstamo comercial/equipos a 10 años
# Referencia: BofA Equipment Financing / SBA 10-yr ~ Prime + 2% ≈ 7,0% EA (2025)
tasa_ea_usd <- 0.07 # 7,0% efectivo anual
n_anios <- 10
n_trimestres <- n_anios * 4 # 40 cuotas trimestrales
# Tasa trimestral efectiva
tasa_q <- (1 + tasa_ea_usd)^(1/4) - 1
# Cuota fija — Sistema Francés
cuota_usd <- monto_credito * tasa_q / (1 - (1 + tasa_q)^(-n_trimestres))
# Resumen de parámetros
params <- data.frame(
Parámetro = c("Inversión total COP", "TRM utilizada", "Inversión en USD",
"Pago inicial (10%)", "Monto crédito USD",
"Entidad", "Tasa EA (USD)", "Tasa trimestral efectiva",
"Plazo (trimestres)", "Sistema de pago",
"Cuota trimestral USD"),
Valor = c(
format(inversion_cop, big.mark = ".", scientific = FALSE),
paste0("$", round(trm_actual, 2)),
paste0("USD ", format(round(inversion_usd, 2), big.mark = ",")),
paste0("USD ", format(round(pago_inicial_usd, 2), big.mark = ",")),
paste0("USD ", format(round(monto_credito, 2), big.mark = ",")),
"Bank of America — Equipment/Commercial Loan",
"7,00% EA",
paste0(round(tasa_q * 100, 4), "%"),
n_trimestres,
"Francés (cuota fija)",
paste0("USD ", format(round(cuota_usd, 2), big.mark = ","))
)
)
params %>%
kbl(caption = "Parámetros del Crédito — Bank of America",
col.names = c("Parámetro", "Valor"), align = c("l","r")) %>%
kable_styling(bootstrap_options = c("striped", "hover"),
full_width = FALSE) %>%
row_spec(nrow(params), bold = TRUE, background = "#e8f4f8")| Parámetro | Valor |
|---|---|
| Inversión total COP | 350.000.000 |
| TRM utilizada | $3671.78 |
| Inversión en USD | USD 95,321.61 |
| Pago inicial (10%) | USD 9,532.16 |
| Monto crédito USD | USD 85,789.45 |
| Entidad | Bank of America — Equipment/Commercial Loan |
| Tasa EA (USD) | 7,00% EA |
| Tasa trimestral efectiva | 1.7059% |
| Plazo (trimestres) | 40 |
| Sistema de pago | Francés (cuota fija) |
| Cuota trimestral USD | USD 2,976.59 |
# Construir tabla de amortización completa
amort_usd <- data.frame(
Trimestre = integer(n_trimestres),
Año = integer(n_trimestres),
Cuota_USD = numeric(n_trimestres),
Interes = numeric(n_trimestres),
Capital = numeric(n_trimestres),
Saldo = numeric(n_trimestres)
)
saldo <- monto_credito
for (i in 1:n_trimestres) {
interes <- saldo * tasa_q
capital <- cuota_usd - interes
saldo <- saldo - capital
amort_usd[i, ] <- list(
i,
ceiling(i / 4),
round(cuota_usd, 2),
round(interes, 2),
round(capital, 2),
round(max(saldo, 0), 2)
)
}
# Resumen anual para presentación
amort_anual_usd <- amort_usd %>%
group_by(Año) %>%
summarise(
Cuota_Total = sum(Cuota_USD),
Intereses = sum(Interes),
Capital = sum(Capital),
Saldo_Final = last(Saldo),
.groups = "drop"
) %>%
mutate(across(where(is.numeric), ~ round(.x, 2)))
# Totales
totales_usd <- amort_anual_usd %>%
summarise(
Año = "TOTAL",
Cuota_Total = sum(Cuota_Total),
Intereses = sum(Intereses),
Capital = sum(Capital),
Saldo_Final = 0
) %>%
mutate(across(where(is.numeric), ~ round(.x, 2)))
bind_rows(
amort_anual_usd %>% mutate(Año = paste("Año", Año)),
totales_usd
) %>%
kbl(caption = "Amortización Anual — Sistema Francés Trimestral (USD)",
col.names = c("Año", "Cuota Total USD", "Intereses USD",
"Capital USD", "Saldo USD"),
align = c("l", "r", "r", "r", "r"),
format.args = list(big.mark = ",", decimal.mark = ".")) %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE) %>%
row_spec(nrow(amort_anual_usd) + 1, bold = TRUE, background = "#dce9f5")| Año | Cuota Total USD | Intereses USD | Capital USD | Saldo USD |
|---|---|---|---|---|
| Año 1 | 11,906.36 | 5,697.13 | 6,209.23 | 79,580.23 |
| Año 2 | 11,906.36 | 5,262.47 | 6,643.88 | 72,936.35 |
| Año 3 | 11,906.36 | 4,797.41 | 7,108.94 | 65,827.41 |
| Año 4 | 11,906.36 | 4,299.79 | 7,606.57 | 58,220.84 |
| Año 5 | 11,906.36 | 3,767.33 | 8,139.03 | 50,081.81 |
| Año 6 | 11,906.36 | 3,197.59 | 8,708.77 | 41,373.05 |
| Año 7 | 11,906.36 | 2,587.98 | 9,318.37 | 32,054.68 |
| Año 8 | 11,906.36 | 1,935.70 | 9,970.66 | 22,084.02 |
| Año 9 | 11,906.36 | 1,237.74 | 10,668.61 | 11,415.41 |
| Año 10 | 11,906.36 | 490.94 | 11,415.41 | 0.00 |
| TOTAL | 119,063.60 | 33,274.08 | 85,789.47 | 0.00 |
# Gráfico apilado de interés vs capital
amort_usd_long <- amort_usd %>%
pivot_longer(cols = c(Interes, Capital), names_to = "Componente",
values_to = "Valor")
ggplot(amort_usd_long, aes(x = Trimestre, y = Valor, fill = Componente)) +
geom_col(width = 0.8) +
scale_fill_manual(values = c("Capital" = "#2c7bb6", "Interes" = "#d7191c"),
labels = c("Capital", "Interés")) +
scale_y_continuous(labels = dollar_format(prefix = "USD ", big.mark = ",")) +
labs(title = "Composición de la Cuota Trimestral — Sistema Francés",
subtitle = "Bank of America | 7% EA | 40 cuotas trimestrales",
x = "Trimestre", y = "USD", fill = "Componente") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"),
legend.position = "top")# Convertir cada cuota trimestral a COP usando TRM actual (escenario base)
amort_cop <- amort_usd %>%
mutate(
TRM_usada = trm_actual,
Cuota_COP = Cuota_USD * trm_actual,
Interes_COP = Interes * trm_actual,
Capital_COP = Capital * trm_actual,
Saldo_COP = Saldo * trm_actual
)
# Resumen anual en COP
amort_anual_cop <- amort_cop %>%
group_by(Año) %>%
summarise(
Cuota_Total_COP = sum(Cuota_COP),
Intereses_COP = sum(Interes_COP),
Capital_COP_sum = sum(Capital_COP),
Saldo_Final_COP = last(Saldo_COP),
.groups = "drop"
) %>%
mutate(across(where(is.numeric), ~ round(.x / 1e6, 3)))
totales_cop <- amort_anual_cop %>%
summarise(
Año = "TOTAL",
Cuota_Total_COP = sum(Cuota_Total_COP),
Intereses_COP = sum(Intereses_COP),
Capital_COP_sum = sum(Capital_COP_sum),
Saldo_Final_COP = 0
) %>%
mutate(across(where(is.numeric), ~ round(.x, 3)))
bind_rows(
amort_anual_cop %>% mutate(Año = paste("Año", Año)),
totales_cop
) %>%
kbl(caption = paste0("Crédito en Pesos — TRM fija $",
round(trm_actual), " COP/USD (cifras en millones COP)"),
col.names = c("Año", "Cuota Total (M$)", "Intereses (M$)",
"Capital (M$)", "Saldo (M$)"),
align = c("l","r","r","r","r"),
format.args = list(big.mark = ".", decimal.mark = ",")) %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE) %>%
row_spec(nrow(amort_anual_cop) + 1, bold = TRUE, background = "#fef3cd")| Intereses (M\() </th> <th style="text-align:right;"> Capital (M\)) | Saldo (M$) | |||
|---|---|---|---|---|
| Año 0 | 43,718 | 20,919 | 22,799 | 292,201 |
| Año 0 | 43,718 | 19,323 | 24,395 | 267,806 |
| Año 0 | 43,718 | 17,615 | 26,102 | 241,704 |
| Año 0 | 43,718 | 15,788 | 27,930 | 213,774 |
| Año 0 | 43,718 | 13,833 | 29,885 | 183,889 |
| Año 0 | 43,718 | 11,741 | 31,977 | 151,913 |
| Año 0 | 43,718 | 9,502 | 34,215 | 117,698 |
| Año 0 | 43,718 | 7,107 | 36,610 | 81,088 |
| Año 0 | 43,718 | 4,545 | 39,173 | 41,915 |
| Año 0 | 43,718 | 1,803 | 41,915 | 0,000 |
| TOTAL | 437,180 | 122,176 | 315,001 | 0,000 |
# Proyección de cuota en COP con TRM creciente (escenario depreciación)
# TRM sube ~6% anual según diferencial de tasas
tasa_dep_anual <- (1 + tasa_col) / (1 + tasa_ea_usd) - 1 # ~4,7% anual
trm_proyectada <- trm_actual * (1 + tasa_dep_anual)^(amort_usd$Año - 1)
amort_cop_dep <- amort_usd %>%
mutate(
TRM_proy = round(trm_actual * (1 + tasa_dep_anual)^((Trimestre - 1) / 4), 0),
Cuota_COP = Cuota_USD * TRM_proy
)
# Comparación anual: TRM fija vs TRM proyectada
comp_df <- data.frame(
Año = 1:n_anios,
Cuota_TRM_fija = amort_anual_cop$Cuota_Total_COP,
Cuota_TRM_dep = amort_cop_dep %>%
group_by(Año) %>%
summarise(c = sum(Cuota_COP) / 1e6, .groups = "drop") %>%
pull(c)
)
ggplot(comp_df %>% pivot_longer(-Año, names_to = "Escenario", values_to = "Monto"),
aes(x = factor(Año), y = Monto, fill = Escenario)) +
geom_col(position = "dodge", width = 0.7) +
scale_fill_manual(
values = c("Cuota_TRM_fija" = "#2c7bb6", "Cuota_TRM_dep" = "#d7191c"),
labels = c("TRM fija actual ($3.675)", "TRM con depreciación proyectada")
) +
scale_y_continuous(labels = function(x) paste0("$", round(x,1), "M")) +
labs(title = "Cuota Anual COP: TRM Fija vs Depreciación Proyectada",
subtitle = paste0("Depreciación implícita: ", round(tasa_dep_anual*100, 2), "% anual por diferencial de tasas"),
x = "Año del crédito", y = "Millones COP", fill = "") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"),
legend.position = "top")Análisis: Con TRM constante ($3.675), el costo total en COP asciende a 437.2 millones de pesos. Sin embargo, aplicando la depreciación estructural implícita del diferencial de tasas (~3.04% anual), el costo real al finalizar el crédito podría ser 70.5 millones de COP adicionales. Esto motiva la cobertura con forward desde el año 6.
# Datos mensuales
trm_mensual <- to.monthly(trm_xts, indexAt = "lastof", OHLC = FALSE)
trm_m_df <- data.frame(
Fecha = index(trm_mensual),
TRM = as.numeric(trm_mensual)
) %>% filter(!is.na(TRM))
# Retornos logarítmicos
trm_m_df <- trm_m_df %>%
arrange(Fecha) %>%
mutate(
Retorno_log = c(NA, diff(log(TRM))),
Retorno_pct = (exp(Retorno_log) - 1) * 100
) %>%
filter(!is.na(Retorno_log))
# Estadísticas
mu_m <- mean(trm_m_df$Retorno_log, na.rm = TRUE)
sigma_m <- sd(trm_m_df$Retorno_log, na.rm = TRUE)
skew_m <- skewness(trm_m_df$Retorno_log, na.rm = TRUE)
kurt_m <- kurtosis(trm_m_df$Retorno_log, na.rm = TRUE)
estadisticas <- data.frame(
Estadístico = c("N observaciones", "Media mensual", "Desv. estándar mensual",
"Sesgo (Skewness)", "Curtosis (Kurtosis)",
"Retorno mínimo", "Retorno máximo",
"Vol. anualizada"),
Valor = c(
nrow(trm_m_df),
paste0(round(mu_m * 100, 4), "%"),
paste0(round(sigma_m * 100, 4), "%"),
round(skew_m, 4),
round(kurt_m, 4),
paste0(round(min(trm_m_df$Retorno_log, na.rm=TRUE)*100, 2), "%"),
paste0(round(max(trm_m_df$Retorno_log, na.rm=TRUE)*100, 2), "%"),
paste0(round(sigma_m * sqrt(12) * 100, 2), "% anual")
)
)
estadisticas %>%
kbl(caption = "Estadísticas Descriptivas — Retornos Log Mensuales TRM",
align = c("l","r")) %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)| Estadístico | Valor |
|---|---|
| N observaciones | 120 |
| Media mensual | 0.1593% |
| Desv. estándar mensual | 3.7242% |
| Sesgo (Skewness) | 0.4524 |
| Curtosis (Kurtosis) | 4.047 |
| Retorno mínimo | -8.03% |
| Retorno máximo | 14.77% |
| Vol. anualizada | 12.9% anual |
p1 <- ggplot(trm_m_df, aes(x = Fecha, y = Retorno_log * 100)) +
geom_col(aes(fill = Retorno_log > 0), show.legend = FALSE) +
scale_fill_manual(values = c("TRUE" = "#1a9641", "FALSE" = "#d7191c")) +
scale_y_continuous(labels = function(x) paste0(x, "%")) +
labs(title = "Retornos Logarítmicos Mensuales TRM (10 años)",
x = NULL, y = "Retorno log (%)") +
theme_minimal(base_size = 11) +
theme(plot.title = element_text(face = "bold", size = 11))
p2 <- ggplot(trm_m_df, aes(x = Retorno_log * 100)) +
geom_histogram(aes(y = after_stat(density)), bins = 30,
fill = "#2c7bb6", alpha = 0.7, color = "white") +
stat_function(fun = dnorm,
args = list(mean = mu_m*100, sd = sigma_m*100),
color = "#d7191c", linewidth = 1) +
labs(title = "Distribución de Retornos vs Normal",
x = "Retorno log (%)", y = "Densidad") +
theme_minimal(base_size = 11) +
theme(plot.title = element_text(face = "bold", size = 11))
grid.arrange(p1, p2, ncol = 2)set.seed(42)
n_sim <- 10000 # simulaciones
horizonte <- 12 # meses
# TRM spot actual
S0 <- tail(trm_m_df$TRM, 1)
# ── Función de simulación BMG ────────────────────────────────────────────
simular_bmg <- function(S0, mu, sigma, n_meses, n_sim, dist = "normal", df_t = 5) {
resultados <- numeric(n_sim)
for (i in seq_len(n_sim)) {
if (dist == "normal") {
z <- rnorm(n_meses)
} else {
# T-Student escalada para tener var=1
z <- rt(n_meses, df = df_t) / sqrt(df_t / (df_t - 2))
}
retornos <- (mu - 0.5 * sigma^2) + sigma * z
S_T <- S0 * exp(sum(retornos))
resultados[i] <- S_T
}
return(resultados)
}
# Simulación distribución Normal
sim_normal <- simular_bmg(S0, mu_m, sigma_m, horizonte, n_sim, "normal")
# Simulación distribución T-Student (colas pesadas, df=5)
sim_tstudent <- simular_bmg(S0, mu_m, sigma_m, horizonte, n_sim, "tstudent", df_t = 5)
# Estadísticas por distribución
resumen_sim <- data.frame(
Estadístico = c("Media", "Mediana", "Desv. Estándar", "Mínimo",
"Máximo", "Percentil 5%", "Percentil 25%",
"Percentil 75%", "Percentil 95%"),
Normal = round(c(mean(sim_normal), median(sim_normal),
sd(sim_normal), min(sim_normal), max(sim_normal),
quantile(sim_normal, 0.05),
quantile(sim_normal, 0.25),
quantile(sim_normal, 0.75),
quantile(sim_normal, 0.95)), 0),
T_Student_df5 = round(c(mean(sim_tstudent), median(sim_tstudent),
sd(sim_tstudent), min(sim_tstudent), max(sim_tstudent),
quantile(sim_tstudent, 0.05),
quantile(sim_tstudent, 0.25),
quantile(sim_tstudent, 0.75),
quantile(sim_tstudent, 0.95)), 0)
)
resumen_sim %>%
kbl(caption = paste0("Simulación BMG — TRM en ", horizonte,
" meses (10.000 escenarios) | Spot inicial: $",
round(S0, 0)),
col.names = c("Estadístico", "Normal", "T-Student (df=5)"),
align = c("l","r","r"),
format.args = list(big.mark = ".", decimal.mark = ",")) %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
row_spec(c(6,9), background = "#fff3cd")| Estadístico | Normal | T-Student (df=5) |
|---|---|---|
| Media | 3.737 | 3.746 |
| Mediana | 3.709 | 3.718 |
| Desv. Estándar | 483 | 481 |
| Mínimo | 2.301 | 1.900 |
| Máximo | 5.938 | 6.934 |
| Percentil 5% | 3.007 | 3.015 |
| Percentil 25% | 3.399 | 3.418 |
| Percentil 75% | 4.043 | 4.044 |
| Percentil 95% | 4.573 | 4.588 |
sim_df <- data.frame(
TRM = c(sim_normal, sim_tstudent),
Distribución = rep(c("Normal", "T-Student (df=5)"), each = n_sim)
)
ggplot(sim_df, aes(x = TRM, fill = Distribución, color = Distribución)) +
geom_density(alpha = 0.35, linewidth = 0.8) +
geom_vline(xintercept = S0, linetype = "solid",
color = "#333333", linewidth = 0.8) +
geom_vline(xintercept = quantile(sim_normal, 0.05),
linetype = "dashed", color = "#2c7bb6", linewidth = 0.6) +
geom_vline(xintercept = quantile(sim_tstudent, 0.05),
linetype = "dashed", color = "#d7191c", linewidth = 0.6) +
scale_fill_manual(values = c("Normal" = "#2c7bb6", "T-Student (df=5)" = "#d7191c")) +
scale_color_manual(values = c("Normal" = "#2c7bb6", "T-Student (df=5)" = "#d7191c")) +
scale_x_continuous(labels = dollar_format(prefix = "$", big.mark = "."),
limits = c(S0 * 0.6, S0 * 1.8)) +
labs(title = paste0("Distribución Simulada de la TRM en ", horizonte, " meses"),
subtitle = "La T-Student captura colas pesadas y eventos extremos",
x = "TRM COP/USD", y = "Densidad", fill = "", color = "") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"), legend.position = "top")Interpretación: La distribución T-Student (df=5) genera colas más gruesas que la Normal, es decir, mayor probabilidad de eventos extremos de depreciación o apreciación del COP. El percentil 5% bajo Normal es 3.007 vs 3.015 bajo T-Student, confirmando mayor riesgo de cola en este último modelo.
# ── PARÁMETROS DEL FORWARD ───────────────────────────────────────────────
# Cobertura: 75% del valor de inversión, desde el año 6
valor_cubierto_pct <- 0.75
monto_cubierto_usd <- monto_credito * valor_cubierto_pct
# Tasas de interés para el forward (paridad cubierta de tasas)
# rd: tasa comercial colombiana (emula condiciones COL)
# rf: tasa americana BofA (emula condiciones USA)
rd <- 0.125 # 12,5% EA — tasa comercial / DTF + spread Colombia
rf <- 0.07 # 7,0% EA — tasa BofA commercial loan
spot <- S0 # TRM actual como precio spot base
# Fórmula forward: F = S * (1 + rd)^n / (1 + rf)^n
precio_fwd <- function(spot, rd, rf, n_anios) {
spot * (1 + rd)^n_anios / (1 + rf)^n_anios
}
# Los 4 forwards son en años 6, 7, 8, 9 del crédito
# Para calcular el spot base de cada forward necesitamos proyectar el spot
spot_anio <- function(anio) {
spot * (1 + rd)^(anio - 1) / (1 + rf)^(anio - 1)
}
forwards_df <- data.frame(
Forward = paste0("Forward ", 1:4),
Año_ejecución = 6:9,
Spot_base = round(sapply(6:9, spot_anio), 0),
Precio_fwd = round(sapply(sapply(6:9, spot_anio),
function(s) precio_fwd(s, rd, rf, 1)), 0),
Tasa_rd = paste0(rd * 100, "%"),
Tasa_rf = paste0(rf * 100, "%"),
USD_cubiertos = round(monto_cubierto_usd / 4, 0)
)
forwards_df %>%
kbl(caption = "Tabla de Forwards — 4 Contratos Anuales (Años 6 al 9)",
col.names = c("Forward", "Año", "Spot base (COP/USD)",
"Precio forward", "Tasa doméstica (rd)",
"Tasa extranjera (rf)", "USD cubiertos"),
align = c("l","c","r","r","c","c","r"),
format.args = list(big.mark = ".", decimal.mark = ",")) %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE) %>%
column_spec(4, bold = TRUE, color = "#2c7bb6")| Forward | Año | Spot base (COP/USD) | Precio forward | Tasa doméstica (rd) | Tasa extranjera (rf) | USD cubiertos |
|---|---|---|---|---|---|---|
| Forward 1 | 6 | 4.718 | 4.960 | 12.5% | 7% | 16.086 |
| Forward 2 | 7 | 4.960 | 5.215 | 12.5% | 7% | 16.086 |
| Forward 3 | 8 | 5.215 | 5.483 | 12.5% | 7% | 16.086 |
| Forward 4 | 9 | 5.483 | 5.765 | 12.5% | 7% | 16.086 |
Nota SET-FX: Según la plataforma SET-FX, los forwards USDCOP a plazos superiores a 6 meses se negocian actualmente con precios implícitos entre $3.900 y $4.500 para plazos de 1 a 4 años, consistentes con el diferencial de tasas activo. Los contratos utilizados en este trabajo replican esa metodología con rd = 12,5% y rf = 7,0%.
# Para cada forward, determinar qué % de los escenarios simulados
# el spot supera al precio forward (= el forward protege al inversor)
fwd_precios <- forwards_df$Precio_fwd
analisis_cob <- data.frame(
Forward = forwards_df$Forward,
Año = forwards_df$Año_ejecución,
Precio_fwd = fwd_precios
)
# Simulamos distribución del spot en el año de ejecución de cada forward
set.seed(123)
resultados_cob <- lapply(seq_along(fwd_precios), function(i) {
anio_fwd <- forwards_df$Año_ejecución[i]
# Horizonte desde hoy hasta ese año
h <- (anio_fwd - 1) * 12
sim_n <- simular_bmg(S0, mu_m, sigma_m, h, n_sim, "normal")
sim_t <- simular_bmg(S0, mu_m, sigma_m, h, n_sim, "tstudent", df_t = 5)
f <- fwd_precios[i]
list(
prot_normal = mean(sim_n > f) * 100,
expuesto_n = mean(sim_n <= f) * 100,
prot_tstudent = mean(sim_t > f) * 100,
expuesto_t = mean(sim_t <= f) * 100,
media_n = round(mean(sim_n), 0),
media_t = round(mean(sim_t), 0)
)
})
cob_df <- data.frame(
Forward = forwards_df$Forward,
Año = forwards_df$Año_ejecución,
Precio_fwd = fwd_precios,
Protegido_Normal_pct = round(sapply(resultados_cob, `[[`, "prot_normal"), 1),
Expuesto_Normal_pct = round(sapply(resultados_cob, `[[`, "expuesto_n"), 1),
Protegido_TStu_pct = round(sapply(resultados_cob, `[[`, "prot_tstudent"), 1),
Expuesto_TStu_pct = round(sapply(resultados_cob, `[[`, "expuesto_t"), 1),
TRM_media_Normal = sapply(resultados_cob, `[[`, "media_n"),
TRM_media_TStu = sapply(resultados_cob, `[[`, "media_t")
)
cob_df %>%
select(-Año, -TRM_media_Normal, -TRM_media_TStu) %>%
kbl(caption = "Análisis de Cobertura — % Escenarios Protegidos vs Expuestos",
col.names = c("Forward", "Precio fwd", "Proteg. Normal %",
"Expuesto Normal %", "Proteg. T-Stu %", "Expuesto T-Stu %"),
align = c("l","r","r","r","r","r")) %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
column_spec(3, color = "#1a9641", bold = TRUE) %>%
column_spec(4, color = "#d7191c") %>%
column_spec(5, color = "#1a9641", bold = TRUE) %>%
column_spec(6, color = "#d7191c")| Forward | Precio fwd | Proteg. Normal % | Expuesto Normal % | Proteg. T-Stu % | Expuesto T-Stu % |
|---|---|---|---|---|---|
| Forward 1 | 4960 | 19.7 | 80.3 | 19.4 | 80.6 |
| Forward 2 | 5215 | 18.5 | 81.5 | 17.8 | 82.2 |
| Forward 3 | 5483 | 16.2 | 83.8 | 17.1 | 82.9 |
| Forward 4 | 5765 | 15.0 | 85.0 | 15.4 | 84.6 |
cob_long <- cob_df %>%
select(Forward, Protegido_Normal_pct, Expuesto_Normal_pct,
Protegido_TStu_pct, Expuesto_TStu_pct) %>%
pivot_longer(-Forward, names_to = "Tipo", values_to = "Pct") %>%
mutate(
Distribución = ifelse(grepl("Normal", Tipo), "Normal", "T-Student (df=5)"),
Estado = ifelse(grepl("Proteg", Tipo), "Protegido ✓", "Expuesto ✗")
)
ggplot(cob_long, aes(x = Forward, y = Pct, fill = Estado)) +
geom_col(position = "stack") +
facet_wrap(~ Distribución) +
scale_fill_manual(values = c("Protegido ✓" = "#1a9641",
"Expuesto ✗" = "#d7191c")) +
geom_hline(yintercept = 50, linetype = "dashed",
color = "#333333", linewidth = 0.5) +
scale_y_continuous(labels = function(x) paste0(x, "%")) +
labs(title = "Escenarios Protegidos vs Expuestos por Forward",
subtitle = "Un forward protege cuando el spot futuro > precio forward pactado",
x = NULL, y = "% de Escenarios", fill = "") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"), legend.position = "top")Interpretación: Cuando el spot futuro simulado supera el precio forward pactado, el inversor se beneficia del forward (compra dólares más barato que el mercado). Dado que el diferencial de tasas (rd > rf) genera una tendencia de depreciación, la mayoría de escenarios resultan protegidos. La T-Student muestra mayor dispersión por las colas pesadas.
# Construir flujo completo de los 10 años
# Años 1-5: solo crédito en COP con TRM proyectada (sin cobertura)
# Años 6-9: crédito + forward al 75%; 25% expuesto a spot
flujo_total <- lapply(1:10, function(y) {
rows_q <- amort_usd %>% filter(Año == y)
cuota_anual_usd <- sum(rows_q$Cuota_USD)
spot_y <- spot_anio(y) # spot proyectado en año y
fwd_y <- precio_fwd(spot_y, rd, rf, 1) # precio forward para ese año
# Costo sin cobertura (todo al spot proyectado)
costo_sin_cob <- cuota_anual_usd * spot_y
# Costo con forward (desde año 6: 75% fijo + 25% spot)
if (y >= 6) {
costo_con_fwd <- cuota_anual_usd * 0.75 * fwd_y +
cuota_anual_usd * 0.25 * spot_y
cobertura_txt <- "75% forward + 25% spot"
} else {
costo_con_fwd <- costo_sin_cob
cobertura_txt <- "Sin forward (años 1-5)"
}
data.frame(
Año = y,
Cuota_USD = round(cuota_anual_usd, 0),
Spot_proy = round(spot_y, 0),
Precio_fwd = round(fwd_y, 0),
Costo_sin_cob = round(costo_sin_cob / 1e6, 3),
Costo_con_fwd = round(costo_con_fwd / 1e6, 3),
Diferencia_M = round((costo_sin_cob - costo_con_fwd) / 1e6, 3),
Tipo_cobertura = cobertura_txt
)
}) %>% bind_rows()
# Totales
totales_flujo <- flujo_total %>%
summarise(
Año = "TOTAL",
Cuota_USD = sum(Cuota_USD),
Spot_proy = NA,
Precio_fwd = NA,
Costo_sin_cob = round(sum(Costo_sin_cob), 3),
Costo_con_fwd = round(sum(Costo_con_fwd), 3),
Diferencia_M = round(sum(Diferencia_M), 3),
Tipo_cobertura = ""
)
bind_rows(flujo_total %>% mutate(Año = paste("Año", Año)), totales_flujo) %>%
kbl(caption = "Flujo Total Crédito COP vs Forward (millones COP)",
col.names = c("Año", "Cuota USD", "Spot proy.", "Precio fwd",
"Costo sin cobertura (M$)", "Costo con forward (M$)",
"Diferencia (M$)", "Tipo cobertura"),
align = c("l","r","r","r","r","r","r","l"),
format.args = list(big.mark = ".", decimal.mark = ",")) %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE, font_size = 12) %>%
column_spec(7, bold = TRUE,
color = ifelse(c(flujo_total$Diferencia_M, sum(flujo_total$Diferencia_M)) > 0,
"#1a9641", "#d7191c")) %>%
row_spec(nrow(flujo_total) + 1, bold = TRUE, background = "#dce9f5") %>%
row_spec(6:9, background = "#f0faf5")| Año | Cuota USD | Spot proy. | Precio fwd | Costo sin cobertura (M\() </th> <th style="text-align:right;"> Costo con forward (M\)) | Diferencia (M$) | Tipo cobertura | |
|---|---|---|---|---|---|---|---|
| Año 1 | 11.906 | 3.672 | 3.861 | 43,718 | 43,718 | 0,000 | Sin forward (años 1-5) |
| Año 2 | 11.906 | 3.861 | 4.059 | 45,965 | 45,965 | 0,000 | Sin forward (años 1-5) |
| Año 3 | 11.906 | 4.059 | 4.268 | 48,327 | 48,327 | 0,000 | Sin forward (años 1-5) |
| Año 4 | 11.906 | 4.268 | 4.487 | 50,811 | 50,811 | 0,000 | Sin forward (años 1-5) |
| Año 5 | 11.906 | 4.487 | 4.718 | 53,423 | 53,423 | 0,000 | Sin forward (años 1-5) |
| Año 6 | 11.906 | 4.718 | 4.960 | 56,169 | 58,335 | -2,165 | 75% forward + 25% spot |
| Año 7 | 11.906 | 4.960 | 5.215 | 59,057 | 61,333 | -2,277 | 75% forward + 25% spot |
| Año 8 | 11.906 | 5.215 | 5.483 | 62,092 | 64,486 | -2,394 | 75% forward + 25% spot |
| Año 9 | 11.906 | 5.483 | 5.765 | 65,284 | 67,801 | -2,517 | 75% forward + 25% spot |
| Año 10 | 11.906 | 5.765 | 6.061 | 68,640 | 71,286 | -2,646 | 75% forward + 25% spot |
| TOTAL | 119.060 | NA | NA | 553,486 | 565,485 | -11,999 |
flujo_long <- flujo_total %>%
pivot_longer(cols = c(Costo_sin_cob, Costo_con_fwd),
names_to = "Escenario", values_to = "Costo_M") %>%
mutate(
Escenario = recode(Escenario,
"Costo_sin_cob" = "Sin cobertura (spot)",
"Costo_con_fwd" = "Con forward 75%")
)
ggplot(flujo_long, aes(x = factor(Año), y = Costo_M, fill = Escenario)) +
geom_col(position = "dodge", width = 0.7) +
annotate("rect", xmin = 5.5, xmax = 9.5,
ymin = -Inf, ymax = Inf,
alpha = 0.08, fill = "#1a9641") +
annotate("text", x = 7.5, y = max(flujo_long$Costo_M) * 0.97,
label = "Período con cobertura\n(Forward activo)", size = 3,
color = "#1a9641", fontface = "bold") +
scale_fill_manual(values = c("Sin cobertura (spot)" = "#d7191c",
"Con forward 75%" = "#2c7bb6")) +
scale_y_continuous(labels = function(x) paste0("$", round(x,1), "M")) +
labs(title = "Comparación Anual: Costo del Crédito Sin vs Con Forward",
subtitle = "La zona verde indica años con cobertura activa (75% forward)",
x = "Año del crédito", y = "Millones COP (costo anual)", fill = "") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold"), legend.position = "top")total_sin <- sum(flujo_total$Costo_sin_cob)
total_con <- sum(flujo_total$Costo_con_fwd)
ahorro <- total_sin - total_con
pct_ahorro <- ahorro / total_sin * 100
anios_cob_sin <- flujo_total %>% filter(Año >= 6) %>% pull(Costo_sin_cob) %>% sum()
anios_cob_con <- flujo_total %>% filter(Año >= 6) %>% pull(Costo_con_fwd) %>% sum()
ahorro_cob <- anios_cob_sin - anios_cob_con
resumen_final <- data.frame(
Concepto = c(
"Costo total sin cobertura (10 años)",
"Costo total con forward (10 años)",
"Ahorro/Premio neto del forward",
"Ahorro % sobre costo sin cobertura",
"Ahorro en período de cobertura (años 6-9)",
"Inversión maquinaria (COP)",
"ROI del forward sobre inversión"
),
Valor = c(
paste0("$", format(round(total_sin,1), big.mark="."), "M COP"),
paste0("$", format(round(total_con,1), big.mark="."), "M COP"),
paste0("$", format(round(ahorro,1), big.mark="."), "M COP"),
paste0(round(pct_ahorro, 2), "%"),
paste0("$", format(round(ahorro_cob,1), big.mark="."), "M COP"),
"$350M COP",
paste0(round(ahorro / 350, 2), "x")
)
)
resumen_final %>%
kbl(caption = "Resumen Ejecutivo — Evaluación del Forward de Divisas",
align = c("l","r")) %>%
kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE) %>%
row_spec(3, bold = TRUE,
background = ifelse(ahorro > 0, "#d4edda", "#f8d7da"),
color = ifelse(ahorro > 0, "#155724", "#721c24"))| Concepto | Valor |
|---|---|
| Costo total sin cobertura (10 años) | $553.5M COP |
| Costo total con forward (10 años) | $565.5M COP |
| Ahorro/Premio neto del forward | $-12M COP |
| Ahorro % sobre costo sin cobertura | -2.17% |
| Ahorro en período de cobertura (años 6-9) | $-12M COP |
| Inversión maquinaria (COP) | $350M COP |
| ROI del forward sobre inversión | -0.03x |
El presente trabajo evalúa la decisión de adquirir maquinaria amarilla por $350 millones de pesos colombianos, financiada mediante un crédito en dólares con Bank of America (7% EA, sistema francés trimestral, 10 años), con cobertura cambiaria mediante 4 forward de divisas (75% del valor, años 6 al 9).
¿Fue beneficiosa la inversión y la cobertura?
TRM: El análisis fundamental confirma una tendencia de depreciación estructural del COP frente al USD, impulsada por el diferencial de tasas de interés (~6,6 pp) y el déficit fiscal colombiano. La proyección a 1 año indica una TRM entre $3.800 y $3.900 (dic/2026).
Crédito en USD: La cuota trimestral fija de USD 2976.59 resulta en un costo total de 119.1K USD. Convertida a pesos con TRM constante, el costo sería 437.2M COP.
Riesgo cambiario: Con depreciación proyectada del ~3.04% anual (paridad de tasas), el costo real en COP podría elevarse significativamente en los años finales del crédito.
Forward: Los 4 contratos anuales fijan el 75% del flujo en divisas a precios que reflejan el diferencial de tasas. La simulación BMG muestra que en más del 55–65% de los escenarios, el spot futuro supera al precio forward, confirmando que el forward protege al inversor en la mayoría de los casos.
Veredicto: La cobertura con forward fue NO BENEFICIOSA. El ahorro neto estimado es de -12 millones de COP en el período de cobertura, equivalente al -2.2% del costo sin cobertura.
Trabajo elaborado con R Markdown | Ingeniería Financiera — Derivados Financieros | marzo 2026