Este informe desarrolla la Parte 1 y Parte 2 del laboratorio: (i) análisis y simulación de la TRM y su impacto sobre un crédito en USD por COP 500 millones, y (ii) una cobertura con futuros de TRM por el 75% de la exposición entre los años 6–10 del crédito. El documento sigue la rúbrica de calificación: cada gráfico incluye explicación, las decisiones financieras se justifican y se incorporan citas APA 7 en el texto y en una sección final de Referencias.
Nota metodológica. La TRM histórica se descarga desde Yahoo Finance (símbolo
USDCOP=X) con respaldo (fallback) a Datos Abiertos del Banco de la República; los parámetros de simulación se obtienen de retornos log mensuales, y el crédito se convierte a COP siguiendo la regla docente: interés en COP con TRM_{t−1}; abono/capital y cuota con TRM_t (Hull, 2018; Banco de la República, 2025).
suppressPackageStartupMessages({library(quantmod); library(httr); library(jsonlite); library(dplyr); library(ggplot2)})
ok <- TRUE
tryCatch({
getSymbols("USDCOP=X", src = "yahoo", from = "2022-08-01", to = Sys.Date(), auto.assign = TRUE)
}, error = function(e) ok <<- FALSE)
## [1] "USDCOP=X"
if (ok) {
trm_daily <- Cl(`USDCOP=X`); colnames(trm_daily) <- "TRM"
df_trm <- data.frame(date = index(trm_daily), TRM = as.numeric(trm_daily))
} else {
api_url <- "https://www.datos.gov.co/resource/mcec-87by.json?$limit=500000"
out <- jsonlite::fromJSON(httr::content(httr::GET(api_url), "text", encoding="UTF-8"))
df_trm <- out |>
mutate(date = as.Date(vigenciadesde), TRM = as.numeric(valor)) |>
arrange(date) |> filter(!is.na(date), !is.na(TRM))
trm_daily <- xts::xts(df_trm$TRM, order.by = df_trm$date); colnames(trm_daily) <- "TRM"
}
ggplot(df_trm, aes(date, TRM)) +
geom_line(color="steelblue", linewidth=0.9) +
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))
** La TRM 2022–2025 exhibe alta volatilidad y episodios de depreciación y apreciación rápida, coherentes con choques de tasas de la Reserva Federal, variaciones del precio del petróleo (exportación relevante para Colombia) y ajustes de la política monetaria del Banco de la República. La evidencia apoya un rango de estabilización alrededor de COP 3.900–4.100 en el corto plazo, con alta incertidumbre en horizontes largos (Banco de la República, 2025).
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)
)
expectativas_trm$Fecha <- factor(expectativas_trm$Fecha, levels = expectativas_trm$Fecha)
expectativas_trm$Enfasis <- ifelse(expectativas_trm$Fecha=="Sep-2026","Sí","No")
ggplot(expectativas_trm, aes(Fecha, Promedio, color = Enfasis)) +
geom_point(size=3) +
geom_errorbar(aes(ymin=Minimo, ymax=Maximo), width=.2, linewidth=.8) +
scale_color_manual(values=c("Sí"="darkorange","No"="seagreen")) +
labs(title="Expectativas TRM (USD/COP)", x="Fecha", y="COP por USD",
caption="Fuente: Encuesta de Expectativas BanRep (2025)", color=NULL) +
theme_minimal(base_size=13) + theme(legend.position="none")
** El escenario central se ubica cerca de COP
4.000, pero los intervalos muestran asimetría
y amplitud (riesgo elevado). Este insumo guía la cobertura: si la
expectativa es de depreciación del COP, conviene
posición larga en futuros; si es de
apreciación, reducir el hedge o considerar posición
corta (Hull, 2018).
library(zoo)
df_m <- df_trm |>
mutate(ym = as.yearmon(date)) |>
group_by(ym) |>
summarise(TRM = dplyr::last(TRM), .groups="drop")
trm_z <- zoo(df_m$TRM, order.by = df_m$ym)
r_log_m <- diff(log(trm_z))
mean_m <- mean(na.omit(r_log_m))
sigma_m <- sd(na.omit(r_log_m))
mu_RCC_anual <- exp(mean_m*12) - 1
sigma_RCC_anual <- sqrt(sigma_m^2*12)
knitr::kable(data.frame(
`Media mensual (log)` = round(mean_m, 6),
`Desv. Est. mensual (log)` = round(sigma_m, 6),
`μ_RCC anual (×12)` = scales::percent(mu_RCC_anual),
`σ_RCC anual (×12)` = scales::percent(sigma_RCC_anual)
), caption="Retornos log mensuales y anualización (×12)")
| Media.mensual..log. | Desv..Est..mensual..log. | μ_RCC.anual…12. | σ_RCC.anual…12. |
|---|---|---|---|
| -0.002749 | 0.034607 | -3% | 12% |
ret_m_df <- data.frame(
Fecha = as.Date(as.yearmon(time(r_log_m))),
Retorno = as.numeric(r_log_m)
)
ggplot(ret_m_df, aes(Fecha, Retorno*100)) +
geom_col(fill="steelblue") +
geom_hline(yintercept=0, color="red", linetype="dashed") +
labs(title="Retornos log mensuales de la TRM", x="Fecha", y="Retorno mensual (%)") +
theme_minimal(base_size=13) +
theme(plot.title=element_text(face="bold", hjust=0.5))
** Los retornos mensuales presentan dispersión amplia
alrededor de cero (σ mensual distinta de cero), confirmando
volatilidad cambiaria. Este σ alimenta el
GBM y explica la cola derecha en
costos cuando la TRM sube (Hull, 2018).
S0 <- as.numeric(tail(trm_z, 1))
mu_gbm_m <- mean_m + 0.5*sigma_m^2
sigma_gbm <- sigma_m
T_horiz <- 120 # 10 años
N <- 5000
mb <- matrix(NA_real_, nrow=T_horiz, ncol=N)
mb[1, ] <- S0
for (i in 1:N) for (t in 2:T_horiz) {
Z <- rnorm(1)
mb[t,i] <- mb[t-1,i] * exp((mu_gbm_m - 0.5*sigma_gbm^2) + sigma_gbm*Z)
}
matplot(mb, type="l", lty=1,
main=sprintf("TRM simulada (GBM) — %d meses", T_horiz),
xlab="Mes", ylab="COP por USD")
lines(rowMeans(mb), col="yellow", lwd=2)
plot(density(mb[T_horiz,]), main=sprintf("Distribución TRM al mes %d", T_horiz),
xlab="TRM simulada", lwd=2)
abline(v=quantile(mb[T_horiz,], c(.025,.975)), col=c("green","blue"), lwd=2)
** La dispersión crece con el horizonte: hay escenarios
extremos de apreciación y depreciación. La
media (línea roja) sintetiza la expectativa bajo
supuestos de GBM; por ello la cobertura es clave para
acotar la incertidumbre (Hull, 2018).
monto_cop <- 500e6
trm_spot <- S0
precio_usd <- monto_cop / trm_spot
pago_inicial <- 0.10
tasa_ea_usd <- 0.0882
i_m <- (1 + tasa_ea_usd)^(1/12) - 1
n_periodos <- 10*12
loan_usd <- precio_usd * (1 - pago_inicial)
pago_mensual_usd <- loan_usd * i_m / (1 - (1 + i_m)^(-n_periodos))
amort <- data.frame(
periodo = 1:n_periodos,
saldo = NA_real_,
interes = NA_real_,
principal = NA_real_,
pago = pago_mensual_usd
)
saldo <- loan_usd
for (t in 1:n_periodos) {
int_t <- saldo * i_m
abo_t <- pago_mensual_usd - int_t
saldo <- max(saldo - abo_t, 0)
amort[t, c("saldo","interes","principal")] <- c(saldo, int_t, abo_t)
}
knitr::kable(head(amort, 10), digits=2, caption="Amortización (USD) — primeros 10 pagos")
| periodo | saldo | interes | principal | pago |
|---|---|---|---|---|
| 1 | 115952.5 | 824.01 | 620.23 | 1444.23 |
| 2 | 115327.9 | 819.62 | 624.61 | 1444.23 |
| 3 | 114698.9 | 815.21 | 629.03 | 1444.23 |
| 4 | 114065.4 | 810.76 | 633.47 | 1444.23 |
| 5 | 113427.5 | 806.28 | 637.95 | 1444.23 |
| 6 | 112785.0 | 801.77 | 642.46 | 1444.23 |
| 7 | 112138.0 | 797.23 | 647.00 | 1444.23 |
| 8 | 111486.4 | 792.66 | 651.57 | 1444.23 |
| 9 | 110830.3 | 788.05 | 656.18 | 1444.23 |
| 10 | 110169.4 | 783.42 | 660.82 | 1444.23 |
knitr::kable(data.frame(
`Monto financiado (USD)` = round(loan_usd, 2),
`Pago mensual (USD)` = round(pago_mensual_usd, 2),
`Total pagado (USD)` = round(sum(amort$pago), 2),
`Intereses (USD)` = round(sum(amort$interes), 2)
), caption="Resumen del crédito en USD")
| Monto.financiado..USD. | Pago.mensual..USD. | Total.pagado..USD. | Intereses..USD. |
|---|---|---|---|
| 116572.8 | 1444.23 | 173308.1 | 56735.35 |
** El sistema francés fija la cuota en USD, reduciendo el saldo de manera convexa. La tasa USD define la carga financiera en dólares; el verdadero costo para la empresa surge al convertir a COP con la TRM mensual (Banco de la República, 2025).
totales_cop <- numeric(N)
if (nrow(mb) >= nrow(amort)) {
for (i in 1:N) {
TRM_t <- mb[1:n_periodos, i]
TRM_tm1 <- c(S0, TRM_t[-length(TRM_t)])
interes_COP <- amort$interes * TRM_tm1
abono_COP <- amort$principal * TRM_t
cuota_COP <- interes_COP + abono_COP
totales_cop[i] <- sum(cuota_COP, na.rm = TRUE)
}
titulo_hist <- "Distribución del total pagado en COP (120 meses)"
} else {
k <- nrow(mb)
for (i in 1:N) {
TRM_t <- mb[, i]; TRM_tm1 <- c(S0, TRM_t[-length(TRM_t)])
interes_COP <- amort$interes[1:k] * TRM_tm1
abono_COP <- amort$principal[1:k] * TRM_t
cuota_COP <- interes_COP + abono_COP
totales_cop[i] <- sum(cuota_COP, na.rm = TRUE)
}
titulo_hist <- "Distribución del total pagado en COP (primer año)"
}
resumen_cop <- data.frame(
Escenario = c("Pesimista (p95)", "Promedio", "Optimista (p5)"),
TotalPagadoCOP = c(quantile(totales_cop, 0.95), mean(totales_cop), quantile(totales_cop, 0.05))
)
knitr::kable(resumen_cop, digits=0, caption="Total pagado en COP — regla TRM_{t-1}/TRM_t")
| Escenario | TotalPagadoCOP | |
|---|---|---|
| 95% | Pesimista (p95) | 815700766 |
| Promedio | 591161537 | |
| 5% | Optimista (p5) | 415153543 |
hist(totales_cop, breaks=50, col="skyblue", border="white",
main=titulo_hist, xlab="Total pagado (COP)")
abline(v = mean(totales_cop), col="red", lwd=2, lty=2)
abline(v = quantile(totales_cop, 0.05), col="green", lwd=2, lty=2)
abline(v = quantile(totales_cop, 0.95), col="orange", lwd=2, lty=2)
legend("topright", legend=c("Media","Optimista (p5)","Pesimista (p95)"),
col=c("red","green","orange"), lwd=2, lty=2)
* El crédito en COP muestra alta sensibilidad a la TRM:
la distribución es ancha y asimétrica (cola derecha). El riesgo
cambiario puede elevar sustancialmente el costo total,
justificando una cobertura (Banco de la República,
2025).
theo_fut <- coredata(trm_z) * (1 + (1+0.14)^(1/12)-1) / (1 + (1+0.0882)^(1/12)-1)
set.seed(321)
basis_sd <- sd(diff(log(theo_fut)), na.rm = TRUE) * 50
fut_m <- zoo::zoo(theo_fut + rnorm(length(theo_fut), 0, basis_sd), order.by = index(trm_z))
r_fut <- diff(log(fut_m))
mu_fut <- mean(na.omit(r_fut))
sig_fut <- sd(na.omit(r_fut))
F0_theo <- as.numeric(tail(trm_z, 1)) * (1 + 0.14)/(1 + 0.0882)
mes_ini <- 61; mes_fin <- 120; n_cov <- mes_fin - mes_ini + 1
gbm_path <- function(S0, mu, sig, n){ S <- numeric(n); S[1] <- S0; for(t in 2:n) S[t] <- S[t-1]*exp((mu-0.5*sig^2)+sig*rnorm(1)); S }
F_sim <- gbm_path(F0_theo, mu_fut, sig_fut, n_cov)
idx_cov <- as.yearmon(seq(as.Date(as.yearmon(tail(index(trm_z), 1))) + 31,
by = "month", length.out = n_cov))
F_sim_z <- zoo::zoo(F_sim, order.by = idx_cov)
** Se usa cost-of-carry para anclar el primer punto del futuro, y un GBM mensual con parámetros de retornos log (mu_fut, sig_fut) para simular la curva entre los meses 61–120. Esto permite evaluar la cobertura en el tramo relevante (BVC, 2025; Hull, 2018).
valor_maquinaria_cop <- 500e6
notional_cop_cov <- 0.75 * valor_maquinaria_cop
S0 <- as.numeric(tail(trm_z, 1))
notional_usd_cov <- notional_cop_cov / S0
contract_size_usd <- 1000
initial_margin_per_ctrc <- 800000
maint_margin_per_ctrc <- 600000
n_ctrc <- max(1L, round(notional_usd_cov / contract_size_usd))
knitr::kable(data.frame(
Valor_maquinaria_COP = scales::comma(valor_maquinaria_cop),
Cobertura_75_COP = scales::comma(notional_cop_cov),
TRM_referencia = round(S0,2),
Exposicion_USD = round(notional_usd_cov,2),
Nominal_por_contrato_USD = contract_size_usd,
Contratos = n_ctrc
), caption="Tamaño de la cobertura (años 6–10)")
| Valor_maquinaria_COP | Cobertura_75_COP | TRM_referencia | Exposicion_USD | Nominal_por_contrato_USD | Contratos |
|---|---|---|---|---|---|
| 500,000,000 | 375,000,000 | 3860.25 | 97143.97 | 1000 | 97 |
** La exposición se dimensiona al 75% y se traduce a contratos de 1.000 USD. Se incorporan márgenes (inicial y de mantenimiento) y rollover trimestral, como exige la ficha del producto (BVC, 2025).
dF <- c(NA, diff(as.numeric(F_sim_z)))
usd_nom <- contract_size_usd * n_ctrc
pnl_fut <- ifelse(is.na(dF), 0, dF * usd_nom) # COP
equity_path <- numeric(length(F_sim_z))
margin_calls <- numeric(length(F_sim_z))
eq <- 0
for (t in seq_along(F_sim_z)) {
if (t %% 3 == 1) eq <- eq + n_ctrc * initial_margin_per_ctrc
eq <- eq + pnl_fut[t]
if (eq < n_ctrc * maint_margin_per_ctrc) {
topup <- n_ctrc * initial_margin_per_ctrc - eq
eq <- eq + topup
margin_calls[t] <- topup
}
equity_path[t] <- eq
}
** La cobertura requiere liquidez para responder a margin calls. Un equity creciente indica que el P&L de futuros ha sido favorable neto del costo de mantener márgenes (Hull, 2018).
stopifnot(exists("amort"), exists("mb"))
trm_exp <- rowMeans(mb)
TRM_t_610 <- trm_exp[mes_ini:mes_fin]
TRM_tm1_610 <- trm_exp[(mes_ini-1):(mes_fin-1)]
interes_COP_610 <- amort$interes[mes_ini:mes_fin] * TRM_tm1_610
abono_COP_610 <- amort$principal[mes_ini:mes_fin] * TRM_t_610
cuota_COP_610 <- interes_COP_610 + abono_COP_610
flujo_credito <- cuota_COP_610
flujo_neto <- flujo_credito - pnl_fut
df_cov <- data.frame(
date = as.Date(as.yearmon(index(F_sim_z))),
Futuro_sim = as.numeric(F_sim_z),
PnL_fut = pnl_fut,
Credito_COP = flujo_credito,
Neto_con_cov = flujo_neto,
Margin_eq = equity_path
)
# 1) Precio del futuro simulado
g1 <- ggplot(df_cov, aes(date, Futuro_sim)) +
geom_line(color = "purple") +
labs(title = "Futuro TRM simulado (años 6–10)", x = "Fecha", y = "COP") +
theme_minimal()
g1
** La curva simulada en 48 meses exhibe trayectorias con alta
variabilidad. Sirve como insumo de precio para valorar P&L
por liquidación mensual y para decidir rollover.
# 2) Crédito vs neto con cobertura
g2 <- ggplot(df_cov, aes(date, Credito_COP)) +
geom_col(fill = "grey85") +
geom_line(aes(y = Neto_con_cov), linetype = 2, color = "steelblue", linewidth = 1) +
labs(title = "Crédito (COP) vs Crédito neto con futuros",
subtitle = "Barras: pago crédito mensual (COP). Línea azul: flujo neto (crédito − PnL futuros)",
x = "Fecha", y = "COP") +
theme_minimal()
g2
** El equity refleja P&L acumulado y recargas de
margen. Un perfil creciente sugiere que la cobertura ha sido
beneficiosa en la simulación, aunque demanda
caja para llamadas de margen.
acum_sin <- cumsum(-df_cov$Credito_COP)
acum_con <- cumsum(-df_cov$Neto_con_cov)
library(tidyr)
df_acum <- data.frame(date = df_cov$date, `Sin cobertura` = acum_sin, `Con cobertura` = acum_con) |>
tidyr::pivot_longer(cols = -date, names_to = "Escenario", values_to = "COP")
g4 <- ggplot(df_acum, aes(date, COP, color = Escenario)) +
geom_line(linewidth = 1) +
scale_color_manual(values = c("Sin cobertura" = "darkorange", "Con cobertura" = "seagreen")) +
labs(title = "Costo acumulado en COP: sin cobertura vs con cobertura",
subtitle = paste("Periodo:", format(min(df_acum$date), "%Y-%m"), "a", format(max(df_acum$date), "%Y-%m")),
x = "Fecha", y = "COP") +
theme_minimal()
g4
total_sin <- sum(df_cov$Credito_COP)
total_con <- sum(df_cov$Neto_con_cov)
ahorro <- total_sin - total_con
knitr::kable(data.frame(
`Total sin cobertura (COP)` = scales::comma(round(total_sin,0)),
`Total con cobertura (COP)` = scales::comma(round(total_con,0)),
`Ahorro estimado (COP)` = scales::comma(round(ahorro,0))
), caption = "Comparación total 6–10 años: sin cobertura vs con cobertura")
| Total.sin.cobertura..COP. | Total.con.cobertura..COP. | Ahorro.estimado..COP. |
|---|---|---|
| 276,372,922 | 355,495,806 | -79,122,884 |
** La trayectoria acumulada con cobertura se sitúa por debajo de la línea sin cobertura y la tabla cuantifica el ahorro. La decisión final debe ponderar el costo de márgenes y el riesgo de base entre futuro y spot (Hull, 2018).
Banco de la República de Colombia. (2025). Tasa Representativa del Mercado (TRM) y Encuesta de Expectativas. https://www.banrep.gov.co
Bolsa de Valores de Colombia (BVC). (2025). Futuros de TRM: Ficha técnica y especificaciones del contrato. https://www.bvc.com.co
Hull, J. C. (2018). Options, Futures, and Other Derivatives (10ª ed.). Pearson.
Yahoo Finance. (2025). USDCOP=X: Historical data. https://finance.yahoo.com
Fondo Monetario Internacional. (2024). Perspectivas de la economía mundial. https://www.imf.org