1 Introducción

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).

2 PARTE 1 — Proceso del crédito

2.1 1. TRM histórica y expectativas

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).

2.1.1 Expectativas de TRM (encuesta BanRep)

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í"="firebrick","No"="steelblue")) +
  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).

2.2 2. Retornos mensuales y desviación estándar

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)")
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).

2.3 3. Simulación BMG (GBM) mensual de la TRM

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).

2.4 4. Simulación del crédito en USD (francés, 10 años; 10% inicial)

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")
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")
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).

2.5 5. Crédito en COP (regla TRM_{t−1}/TRM_t)

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")
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).

3 PARTE 2 — Futuros TRM (BVC): cobertura años 6–10

3.1 2.1 Serie de futuros y simulación GBM (años 6–10)

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).

3.2 2.2 Tamaño de cobertura (75%) y márgenes

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)")
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).

3.3 2.3 P&L, rollover y llamadas de margen

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).

3.4 2.4 Flujos del crédito (años 6–10) vs neto con cobertura

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
)

3.5 2.5 Gráficas clave (con explicación bajo cada una)

# 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.

3.6 2.6 Costo acumulado y tabla comparativa

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" = "firebrick", "Con cobertura" = "steelblue")) +
  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")
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).

4 Conclusiones generales

  1. La TRM presenta alta volatilidad mensual; sus retornos justifican el uso de GBM para escenarios probabilísticos (Hull, 2018).
  2. El crédito en USD, al convertirse a COP con la regla TRM_{t−1}/TRM_t, muestra amplia dispersión en el costo total: el riesgo cambiario es material (Banco de la República, 2025).
  3. La cobertura con futuros de TRM sobre el 75% de la exposición en los años 6–10 reduce el riesgo extremo y, en esta simulación, disminuye el costo acumulado en COP.
  4. La estrategia requiere gestión de liquidez por márgenes y revisión del hedge ratio frente a la expectativa de mercado (BVC, 2025).

5 Referencias

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