library(quantmod)
library(ggplot2)
library(dplyr)
library(scales)
library(moments)
library(MASS)
library(lubridate)
library(knitr)
library(kableExtra)

1. Parámetros Globales del Ejercicio

set.seed(42)
options(scipen = 999)

TRM_spot      <- 3692.48
inversion_COP <- 350000000
inversion_USD <- inversion_COP / TRM_spot
pago_ini_pct  <- 0.10
cuota_ini_USD <- inversion_USD * pago_ini_pct
monto_USD     <- inversion_USD * (1 - pago_ini_pct)

tasa_anual_USD <- 0.085
tasa_trim_USD  <- (1 + tasa_anual_USD)^(1/4) - 1
n_trim         <- 40

i_COP <- 0.101 # https://www.banrep.gov.co/es/glosario/indicador-bancario-referencia-ibr
i_USD <- 0.03700 # https://www.federalreserve.gov/

resumen <- data.frame(
  CONCEPTO = c(
    "TRM Spot (BANREP, 20-mar-26)",
    "Inversión total (COP)",
    "Inversión total (USD)",
    "Monto financiado (90%)",
    "Tasa crédito Wells Fargo (EA)",
    "Tasa comercial COP (EA)",
    "Tasa comercial USD (EA)"
  ),
  VALOR = c(
    paste0("$", format(TRM_spot, big.mark = ","), " COP/USD"),
    paste0("$", format(inversion_COP, big.mark = ","), " COP"),
    paste0("$", formatC(inversion_USD, big.mark = ",", format = "f", digits = 2), " USD"),
    paste0("$", formatC(monto_USD, big.mark = ",", format = "f", digits = 2), " USD"),
    paste0(round(tasa_anual_USD * 100, 2), "%"),
    paste0(round(i_COP * 100, 2), "%"),
    paste0(round(i_USD * 100, 2), "%")
  ),
  stringsAsFactors = FALSE
)

print(resumen)
##                        CONCEPTO             VALOR
## 1  TRM Spot (BANREP, 20-mar-26) $3,692.48 COP/USD
## 2         Inversión total (COP)  $350,000,000 COP
## 3         Inversión total (USD)    $94,787.24 USD
## 4        Monto financiado (90%)    $85,308.52 USD
## 5 Tasa crédito Wells Fargo (EA)              8.5%
## 6       Tasa comercial COP (EA)             10.1%
## 7       Tasa comercial USD (EA)              3.7%

2. Análisis Fundamental TRM

tryCatch({
  getSymbols("USDCOP=X", src = "yahoo", from = "2018-01-01",
             auto.assign = TRUE, warnings = FALSE)
  trm_xts <- na.omit(`USDCOP=X`[, 6])
  trm_df  <- data.frame(fecha = index(trm_xts), TRM = as.numeric(trm_xts))
  cat("TRM descargada desde Yahoo Finance.\n")
},
error = function(e) {
  stop("Error obteniendo datos de Yahoo Finance.\n")
})
## TRM descargada desde Yahoo Finance.
# ── Tema APA para todos los gráficos ──────────────────────────────────────
theme_apa <- function(base_size = 12) {
  theme_classic(base_size = base_size) +
    theme(
      text             = element_text(family = "serif", size = base_size),
      plot.title       = element_text(face = "bold", size = base_size,
                                      hjust = 0.5, margin = margin(b = 2)),
      plot.subtitle    = element_text(size = base_size - 1, hjust = 0.5,
                                      face = "italic", margin = margin(b = 8)),
      axis.title       = element_text(face = "bold", size = base_size),
      axis.text        = element_text(size = base_size - 1),
      legend.title     = element_text(face = "bold", size = base_size - 1),
      legend.text      = element_text(size = base_size - 1),
      legend.position  = "bottom",
      panel.grid.major = element_line(color = "grey85", linewidth = 0.4),
      panel.grid.minor = element_blank(),
      plot.background  = element_rect(fill = "white", color = NA),
      panel.background = element_rect(fill = "white", color = NA)
    )
}

p_trm_hist <- ggplot(trm_df, aes(x = fecha, y = TRM)) +
  geom_line(color = "#1565C0", linewidth = 0.8) +
  geom_hline(yintercept = TRM_spot, linetype = "dashed", color = "#B71C1C") +
  annotate("text", x = max(trm_df$fecha), y = TRM_spot + 100,
           label = paste0("Spot actual\n$", TRM_spot), color = "#B71C1C",
           hjust = 1, size = 3.5, family = "serif") +
  scale_y_continuous(labels = dollar_format(prefix = "$", suffix = " COP", big.mark = ",")) +
  labs(title    = "Figura 1",
       subtitle = "TRM Histórica USD/COP (2018–2026). Fuente: Yahoo Finance.",
       x = "Fecha", y = "COP por USD") +
  theme_apa()

print(p_trm_hist)

El comportamiento desde 2018 hasta hoy describe una curva de campana asimétrica: depreciación gradual 2018–2021, pico extremo en 2022 (~$5,100), y corrección pronunciada 2023–2026 de vuelta a niveles de ~$3,650–$3,700. La TRM pasó de ~$2,900 a ~$3,675 en 8 años, lo que representa una depreciación estructural del peso de aproximadamente 26%, aunque con un ciclo intermedio de enorme volatilidad. Los grandes conductores han sido el precio del petróleo, la política monetaria de la Fed, los choques políticos internos y los ciclos de aversión al riesgo global. El año 2025 comenzó con una TRM de $4,409 el 1 de enero y cerró en $3,757 el 31 de diciembre — una revaluación del peso del 14.79%. 2025 Fue el año más favorable para el peso colombiano del período reciente, impulsado por la expectativa de recortes de la Fed, entrada de flujos de capital y relativa estabilidad política. Bancolombia (2025) proyecta que:

El tipo de cambio USDCOP seguiría una trayectoria de apreciación, con un promedio proyectado de $3.880 en 2026. Esta tendencia estará influenciada por la debilidad global del dólar y el apetito de inversionistas extranjeros por activos locales, gracias al atractivo diferencial de tasas de interés y las expectativas de cara al proceso de elecciones legislativas y presidenciales. Sin embargo, los riesgos fiscales seguirán siendo determinantes para la evolución futura de los activos locales. (párr. 7)


3. Variación y Estadísticas TRM

trm_men <- trm_df %>%
  mutate(mes = format(fecha, "%Y-%m")) %>%
  group_by(mes) %>%
  summarise(TRM_m = mean(TRM, na.rm = TRUE), .groups = "drop") %>%
  mutate(r_log = log(TRM_m / lag(TRM_m))) %>%
  na.omit()

mu_m    <- mean(trm_men$r_log)
sigma_m <- sd(trm_men$r_log)
skew_v  <- skewness(trm_men$r_log)
kurt_v  <- kurtosis(trm_men$r_log)
nu_est  <- as.numeric(MASS::fitdistr(trm_men$r_log, "t")$estimate["df"])

estadisticas <- data.frame(
  Estadístico = c("Variación mensual promedio (μ)",
                  "Desviación estándar mensual (σ)",
                  "Asimetría (Skewness)",
                  "Curtosis",
                  "Grados de libertad t-Student (ν)"),
  Valor = c(sprintf("%.5f (%.4f%%)", mu_m, mu_m*100),
            sprintf("%.5f (%.4f%%)", sigma_m, sigma_m*100),
            sprintf("%.3f", skew_v),
            sprintf("%.3f  (colas pesadas, >3)", kurt_v),
            sprintf("%.2f", nu_est))
)

kable(estadisticas,
      caption  = "Tabla 1. Estadísticas de retornos logarítmicos mensuales TRM",
      align    = c("l","r"),
      booktabs = TRUE) %>%
  kable_styling(bootstrap_options = c("basic"),
                full_width = TRUE, font_size = 12, position = "center") %>%
  row_spec(0, bold = TRUE,
           extra_css = "border-top: 2px solid black; border-bottom: 1px solid black;") %>%
  row_spec(nrow(estadisticas), extra_css = "border-bottom: 2px solid black;")
Tabla 1. Estadísticas de retornos logarítmicos mensuales TRM
Estadístico Valor
Variación mensual promedio (μ) 0.00271 (0.2705%)
Desviación estándar mensual (σ) 0.03135 (3.1352%)
Asimetría (Skewness) 0.941
Curtosis 5.738 (colas pesadas, >3)
Grados de libertad t-Student (ν) 4.72
r_seq  <- seq(min(trm_men$r_log), max(trm_men$r_log), length.out = 200)
dens_N <- dnorm(r_seq, mu_m, sigma_m)
dens_T <- dt((r_seq - mu_m) / (sigma_m / sqrt(nu_est/(nu_est-2))), df = nu_est) /
           (sigma_m / sqrt(nu_est/(nu_est-2)))

p_hist <- ggplot(trm_men, aes(x = r_log)) +
  geom_histogram(aes(y = after_stat(density)), bins = 35,
                 fill = "#90CAF9", color = "white", alpha = 0.8) +
  geom_line(data = data.frame(x = r_seq, y = dens_N),
            aes(x = x, y = y, color = "Normal"), linewidth = 1.2) +
  geom_line(data = data.frame(x = r_seq, y = dens_T),
            aes(x = x, y = y, color = "t-Student"), linewidth = 1.2, linetype = "dashed") +
  scale_color_manual(values = c("Normal" = "#1565C0", "t-Student" = "#B71C1C")) +
  labs(title    = "Figura 2",
       subtitle = sprintf("Distribución de Retornos Mensuales TRM. μ = %.4f%% | σ = %.4f%% | Curtosis = %.2f.",
                          mu_m*100, sigma_m*100, kurt_v),
       x = "Variación logarítmicoa mensual", y = "Densidad", color = "Distribución") +
  theme_apa()

print(p_hist)

La distribución t-Student ofrece un mejor ajuste que la normal, ya que el histograma evidencia colas más pesadas, es decir, una mayor frecuencia de valores extremos que la distribución normal no logra capturar adecuadamente. Esto se confirma con una curtosis de 5.739 (superior a 3), que indica colas gruesas, y una asimetría de 0.941, lo que muestra que los datos no son perfectamente simétricos. Además, los bajos grados de libertad (ν ≈ 4.71) refuerzan la presencia de colas más pesadas en la t-Student. En este orden de ideas, esta distribución representa de forma más precisa el comportamiento de la variación y el riesgo asociado a eventos extremos, mientras que la normal tiende a subestimarlo.


4. Simulación MBG (Normal y t-Student)

Con base en la variación histórica de la TRM, se realiza una proyección simulada a un horizonte de 40 trimestres mediante un Movimiento Browniano Geométrico. El ejercicio se desarrolla de forma comparativa utilizando dos supuestos distributivos para las proyecciones: Normal y t‑Student. Adicionalmente, se incorpora una restricción a las tasas proyectadas con el propósito de asegurar que los valores simulados se mantengan dentro de rangos consistentes con la verosimilitud histórica observada.

n_sim  <- 5000
n_q    <- 40
mu_q   <- mu_m * 3
sig_q  <- sigma_m * sqrt(3)

# ── Límites superior e inferior ──────────────────────────────────
limite_sup <- TRM_spot + 2500
limite_inf <- TRM_spot - 600

# ── Simulación Normal (con topes superior e inferior) ────────────
sim_N <- matrix(NA, n_q, n_sim)
for (s in 1:n_sim) {
  path <- TRM_spot * exp(cumsum(
    (mu_q - 0.5*sig_q^2) + sig_q * rnorm(n_q)
  ))
  path <- pmin(path, limite_sup)   # tope superior
  path <- pmax(path, limite_inf)   # tope inferior
  sim_N[, s] <- path
}

# ── Simulación t-Student (con topes superior e inferior) ─────────
scale_t <- sigma_m * sqrt(3) / sqrt(nu_est / (nu_est - 2))

sim_T <- matrix(NA, n_q, n_sim)
for (s in 1:n_sim) {
  path <- TRM_spot * exp(cumsum(
    (mu_q - 0.5*sig_q^2) + scale_t * rt(n_q, df = nu_est)
  ))
  path <- pmin(path, limite_sup)   # tope superior
  path <- pmax(path, limite_inf)   # tope inferior
  sim_T[, s] <- path
}

sim_plot <- bind_rows(
  data.frame(Trim    = 1:n_q,
             p05     = apply(sim_N, 1, quantile, 0.05),
             p25     = apply(sim_N, 1, quantile, 0.25),
             mediana = apply(sim_N, 1, median),
             p75     = apply(sim_N, 1, quantile, 0.75),
             p95     = apply(sim_N, 1, quantile, 0.95),
             dist    = "Normal"),
  data.frame(Trim    = 1:n_q,
             p05     = apply(sim_T, 1, quantile, 0.05),
             p25     = apply(sim_T, 1, quantile, 0.25),
             mediana = apply(sim_T, 1, median),
             p75     = apply(sim_T, 1, quantile, 0.75),
             p95     = apply(sim_T, 1, quantile, 0.95),
             dist    = "t-Student")
)

p_bmg <- ggplot(sim_plot, aes(x = Trim)) +
  geom_ribbon(aes(ymin = p05, ymax = p95, fill = dist), alpha = 0.12) +
  geom_ribbon(aes(ymin = p25, ymax = p75, fill = dist), alpha = 0.25) +
  geom_line(aes(y = mediana, color = dist), linewidth = 1.3) +
  geom_hline(yintercept = TRM_spot,   linetype = "dashed", color = "gray50") +
  geom_hline(yintercept = limite_sup, linetype = "dotted", color = "#B71C1C", linewidth = 0.7) +
  geom_hline(yintercept = limite_inf, linetype = "dotted", color = "#1565C0", linewidth = 0.7) +
  annotate("text", x = n_q, y = limite_sup + 30,
           label = paste0("Límite sup. $", format(limite_sup, big.mark = ",")),
           hjust = 1, size = 3, color = "#B71C1C", family = "serif") +
  annotate("text", x = n_q, y = limite_inf - 30,
           label = paste0("Límite inf. $", format(limite_inf, big.mark = ",")),
           hjust = 1, size = 3, color = "#1565C0", family = "serif") +
  scale_fill_manual(values  = c("Normal" = "#1565C0", "t-Student" = "#C62828")) +
  scale_color_manual(values = c("Normal" = "#1565C0", "t-Student" = "#C62828")) +
  scale_y_continuous(labels = dollar_format(prefix = "$", suffix = " COP", big.mark = ",")) +
  labs(title    = "Figura 3",
       subtitle = sprintf(
         "Simulación MBG TRM — Normal vs t-Student (40 trimestres, n = %d trayectorias). Bandas de contención: [+$%s / -$%s] COP desde el spot.",
         n_sim,
         format(1150, big.mark = ","),
         format(935,  big.mark = ",")),
       x = "Trimestre", y = "TRM (COP/USD)",
       color = "Distribución", fill = "Distribución") +
  theme_apa()

print(p_bmg)


5. Simulación TRM (t-Student)

La Tabla 2 muestra unos valores de la TRM proyectada construida a partir de tres escenarios. Se utilizaron los cuantiles de 25%, Media y 75% para pronosticar los escenarios Bajo, Base y Alto respectivamente de la simulación t-Student, ya que esta distribución captura adecuadamente la presencia de colas pesadas observadas en los retornos históricos. Este enfoque permite incorporar la volatilidad real del mercado cambiario y evaluar de forma consistente el riesgo del crédito y la efectividad de la cobertura con forwards.

tabla_trm <- bind_rows(
  data.frame(
    Trim = 1:n_q,
    Escenario_bajo  = apply(sim_T, 1, quantile, 0.25),
    Escenario_base  = apply(sim_T, 1, median),
    Escenario_alto  = apply(sim_T, 1, quantile, 0.95),
    Distribucion = "t-Student"
  )
)

kable(
  rbind(
    head(tabla_trm, 5),
    tail(tabla_trm, 5)
  ),
  digits = 0,
  format.args = list(big.mark = ","),
  caption = "Tabla 2: Escenarios proyectados de la TRM por trimestre (primeros y últimos)",
  align = c("c","r","r","r","c"),
  booktabs = TRUE
) %>%
  kable_styling(full_width = TRUE, font_size = 12)
Tabla 2: Escenarios proyectados de la TRM por trimestre (primeros y últimos)
Trim Escenario_bajo Escenario_base Escenario_alto Distribucion
1 1 3,608 3,713 4,037 t-Student
2 2 3,576 3,744 4,225 t-Student
3 3 3,548 3,762 4,377 t-Student
4 4 3,541 3,794 4,515 t-Student
5 5 3,542 3,822 4,641 t-Student
36 36 3,799 4,716 6,192 t-Student
37 37 3,808 4,747 6,192 t-Student
38 38 3,833 4,772 6,192 t-Student
39 39 3,845 4,799 6,192 t-Student
40 40 3,856 4,841 6,192 t-Student

6. Crédito en USD — Sistema Francés

La Tabla 3 y Figura 4 presentan el plan de amortización trimestre a trimestre del crédito en dólares utilizando el método de cuota fija. Se muestra la composición del pago, es decir, el abono a la deuda y el pago de interés generado al final de cada período.

cuota_USD <- monto_USD * (tasa_trim_USD * (1 + tasa_trim_USD)^n_trim) /
             ((1 + tasa_trim_USD)^n_trim - 1)

tabla <- data.frame(
  Trim = 1:n_trim, Año = ceiling((1:n_trim) / 4),
  Saldo_ini = NA, Interes = NA, Amort = NA, Cuota = NA, Saldo_fin = NA
)
saldo <- monto_USD
for (i in 1:n_trim) {
  int <- saldo * tasa_trim_USD
  am  <- cuota_USD - int
  tabla[i, c("Saldo_ini","Interes","Amort","Cuota","Saldo_fin")] <-
    c(saldo, int, am, cuota_USD, max(saldo - am, 0))
  saldo <- saldo - am
}

kable(
  rbind(
    head(tabla[, c("Trim","Año","Saldo_ini","Interes","Amort","Cuota","Saldo_fin")], 3),
    tail(tabla[, c("Trim","Año","Saldo_ini","Interes","Amort","Cuota","Saldo_fin")], 3)
  ),
  digits      = 2,
  format.args = list(big.mark = ","),
  caption     = sprintf("Tabla 3. Amortización en USD (Sistema Francés) — Cuota fija: $%.2f USD/trimestre", cuota_USD),
  align       = c("c","c","r","r","r","r","r"),
  booktabs    = TRUE
) %>%
  kable_styling(bootstrap_options = c("basic"),
                full_width = TRUE, font_size = 12) %>%
  row_spec(0, bold = TRUE,
           extra_css = "border-top: 2px solid black; border-bottom: 1px solid black;") %>%
  row_spec(6, extra_css = "border-bottom: 2px solid black;")
Tabla 3. Amortización en USD (Sistema Francés) — Cuota fija: $3151.67 USD/trimestre
Trim Año Saldo_ini Interes Amort Cuota Saldo_fin
1 1 1 85,308.52 1,757.73 1,393.94 3,151.67 83,914.58
2 2 1 83,914.58 1,729.01 1,422.66 3,151.67 82,491.93
3 3 1 82,491.93 1,699.70 1,451.97 3,151.67 81,039.95
38 38 10 9,078.35 187.05 2,964.61 3,151.67 6,113.74
39 39 10 6,113.74 125.97 3,025.70 3,151.67 3,088.04
40 40 10 3,088.04 63.63 3,088.04 3,151.67 0.00
p_amort <- ggplot(tabla, aes(x = Trim)) +
  geom_col(aes(y = Amort, fill = "Amortización"), alpha = 0.85) +
  geom_col(aes(y = Interes, fill = "Interés"), alpha = 0.85) +
  scale_fill_manual(values = c("Amortización" = "#1565C0", "Interés" = "#EF5350")) +
  scale_y_continuous(labels = dollar_format(prefix = "$", suffix = " USD", big.mark = ",")) +
  labs(title    = "Figura 4",
       subtitle = sprintf("Composición de la cuota — Sistema Francés (%.1f%% EA, 10 años, %d cuotas trimestrales).",
                          tasa_anual_USD*100, n_trim),
       x = "Trimestre", y = "USD", fill = "") +
  theme_apa()

print(p_amort)


7. Pago de Cuotas Transformadas a COP

Se proyectan los tres posibles flujos de pagos en pesos (COP) acordes a las TRM simuladas con distribución t-Student, bajo los escenarios Bajo (p5%), Base (mediana) y Alto (p95%), para cada uno de los 40 trimestres del crédito.

# ── TRM proyectada desde simulación t-Student ─────────────────────

TRM_bajo <- apply(sim_T, 1, quantile, 0.05)
TRM_base <- apply(sim_T, 1, median)
TRM_alto <- apply(sim_T, 1, quantile, 0.95)

# ── Tabla de amortización en COP (3 escenarios) ───────────────────

tabla_cop <- tabla %>%
  mutate(
    TRM_bajo = TRM_bajo,
    TRM_base = TRM_base,
    TRM_alto = TRM_alto,

    # Cuotas en COP
    Cuota_COP_bajo = Cuota * TRM_bajo,
    Cuota_COP_base = Cuota * TRM_base,
    Cuota_COP_alto = Cuota * TRM_alto,

    # Intereses en COP
    Interes_COP_bajo = Interes * TRM_bajo,
    Interes_COP_base = Interes * TRM_base,
    Interes_COP_alto = Interes * TRM_alto,

    # Amortización en COP
    Amort_COP_bajo = Amort * TRM_bajo,
    Amort_COP_base = Amort * TRM_base,
    Amort_COP_alto = Amort * TRM_alto,

    # Saldo en COP
    Saldo_ini_COP_bajo = Saldo_ini * TRM_bajo,
    Saldo_ini_COP_base = Saldo_ini * TRM_base,
    Saldo_ini_COP_alto = Saldo_ini * TRM_alto
  )

kable(
  rbind(
    head(tabla_cop[, c("Trim",
                       "Cuota_COP_bajo",
                       "Cuota_COP_base",
                       "Cuota_COP_alto")], 5),
    tail(tabla_cop[, c("Trim",
                       "Cuota_COP_bajo",
                       "Cuota_COP_base",
                       "Cuota_COP_alto")], 5)
  ),
  digits = 0,
  format.args = list(big.mark = ","),
  caption = "Tabla 3: Amortización en COP bajo tres escenarios de TRM (t-Student)",
  align = c("c","r","r","r"),
  booktabs = TRUE
) %>%
  kable_styling(full_width = TRUE, font_size = 12)
Tabla 3: Amortización en COP bajo tres escenarios de TRM (t-Student)
Trim Cuota_COP_bajo Cuota_COP_base Cuota_COP_alto
1 1 10,800,356 11,701,051 12,724,059
2 2 10,485,067 11,801,320 13,316,343
3 3 10,224,852 11,856,290 13,793,547
4 4 10,068,957 11,956,078 14,231,190
5 5 9,932,467 12,046,208 14,628,290
36 36 9,746,466 14,861,800 19,516,633
37 37 9,746,466 14,960,122 19,516,633
38 38 9,746,466 15,039,165 19,516,633
39 39 9,746,466 15,125,762 19,516,633
40 40 9,746,466 15,257,210 19,516,633
# ── Datos para histograma ─────────────────────────────────────────

flujos <- bind_rows(
  data.frame(Flujo = tabla_cop$Cuota_COP_bajo, Escenario = "Bajo (5%)"),
  data.frame(Flujo = tabla_cop$Cuota_COP_base, Escenario = "Base (Mediana)"),
  data.frame(Flujo = tabla_cop$Cuota_COP_alto, Escenario = "Alto (95%)")
)

p_hist_flujos <- ggplot(flujos, aes(x = Flujo / 1e6, fill = Escenario)) +
  geom_histogram(alpha = 0.5, position = "identity", bins = 30) +
  scale_fill_manual(values = c("Bajo (5%)" = "#2E7D32",
                               "Base (Mediana)" = "#1565C0",
                               "Alto (95%)" = "#C62828")) +
  labs(title    = "Figura X",
       subtitle = "Distribución de flujos (cuotas en COP) bajo escenarios de TRM simulada",
       x = "Cuota (millones COP)",
       y = "Frecuencia",
       fill = "Escenario") +
  theme_apa()

TRM_proy <- apply(sim_T, 1, median)

8. Ítem 1 — SET-FX: Curva Forward USD/COP

El instrumento seleccionado de la plataforma SET-FX corresponde al contrato forward USD/COP a 21 meses, que cumple la condición de plazo superior a 6 meses exigida por la actividad. La Tabla 4 resume los puntos forward publicados en marzo de 2026, y la Figura 6 muestra la curva completa Bid/Ask con el punto seleccionado resaltado.

# https://es.investing.com/currencies/usd-cop-forward-rates
setfx <- data.frame(
  Plazo        = c("Spot Next", "3 Semanas", "21 Meses", "2 Años", "3 Años"),
  Meses        = c(0, 0.75, 21, 24, 36),
  Bid          = c(0.00,  3.00, 159.00, 195.00, 300.00),
  Ask          = c(0.042, 4.90, 176.83, 215.00, 350.00),
  Mid          = c(0.021, 3.95, 167.92, 205.00, 325.00)
)
setfx$TRM_Forward <- TRM_spot + setfx$Mid

kable(setfx,
      digits      = 3,
      format.args = list(big.mark = ","),
      caption     = "Tabla 4. Curva Forward USD/COP — Datos SET-FX (Investing.com, marzo 2026)",
      align       = c("l","c","r","r","r","r"),
      booktabs    = TRUE) %>%
  kable_styling(bootstrap_options = c("basic"),
                full_width = TRUE, font_size = 12) %>%
  row_spec(0, bold = TRUE,
           extra_css = "border-top: 2px solid black; border-bottom: 1px solid black;") %>%
  row_spec(nrow(setfx), extra_css = "border-bottom: 2px solid black;")
Tabla 4. Curva Forward USD/COP — Datos SET-FX (Investing.com, marzo 2026)
Plazo Meses Bid Ask Mid TRM_Forward
Spot Next 0.00 0 0.042 0.021 3,692.501
3 Semanas 0.75 3 4.900 3.950 3,696.430
21 Meses 21.00 159 176.830 167.920 3,860.400
2 Años 24.00 195 215.000 205.000 3,897.480
3 Años 36.00 300 350.000 325.000 4,017.480
TRM_fw_21m   <- TRM_spot + 167.92
dev_impl_21m <- (TRM_fw_21m / TRM_spot)^(12/21) - 1

cat("INSTRUMENTO SELECCIONADO: USD/COP 21M FWD\n")
## INSTRUMENTO SELECCIONADO: USD/COP 21M FWD
resumen_forward <- data.frame(
  Concepto = c(
    "Plazo",
    "TRM Spot",
    "Puntos forward (Mid)",
    "TRM Forward implícita",
    "Devaluación implícita anual"
  ),
  Valor = c(
    "21 meses (> 6 meses)",
    paste0("$", format(TRM_spot, big.mark = ","), " COP/USD"),
    formatC(167.92, format = "f", digits = 2),
    paste0("$", formatC(TRM_fw_21m, format = "f", digits = 2), " COP/USD"),
    paste0(round(dev_impl_21m * 100, 2), "%")
  ),
  stringsAsFactors = FALSE
)

knitr::kable(
  resumen_forward,
  col.names = c("Concepto", "Valor"),
  caption = "Tabla 4.1 Resumen Forward USD/COP – Plazo mayor a 6 meses"
)
Tabla 4.1 Resumen Forward USD/COP – Plazo mayor a 6 meses
Concepto Valor
Plazo 21 meses (> 6 meses)
TRM Spot $3,692.48 COP/USD
Puntos forward (Mid) 167.92
TRM Forward implícita $3860.40 COP/USD
Devaluación implícita anual 2.57%

El forward a 21 meses, como se muestra en la Tabla 4.1, revela una devaluación implícita anualizada del peso frente al dólar, coherente con el diferencial de tasas de interés entre Colombia y Estados Unidos. Este spread refleja las expectativas del mercado sobre la depreciación cambiaria y sirve de referencia para calibrar los cuatro contratos forward de 1 año que se pactan a partir del sexto año del crédito.

setfx_plot <- setfx[setfx$Meses > 0, ]

p_setfx <- ggplot(setfx_plot, aes(x = Meses, y = TRM_Forward)) +
  geom_ribbon(aes(ymin = TRM_spot + Bid, ymax = TRM_spot + Ask),
              fill = "#1565C0", alpha = 0.18) +
  geom_line(color = "#1565C0", linewidth = 1.5) +
  geom_point(color = "#1565C0", size = 3.5) +
  geom_point(data = setfx_plot[setfx_plot$Meses == 21, ],
             color = "#F44336", size = 7, shape = 18) +
  geom_hline(yintercept = TRM_spot, linetype = "dashed", color = "gray50") +
  annotate("text", x = 22, y = TRM_fw_21m + 40,
           label = sprintf("Seleccionado: 21M\n$%.0f COP/USD", TRM_fw_21m),
           color = "#F44336", size = 3.5, hjust = 0, family = "serif") +
  scale_y_continuous(labels = dollar_format(prefix = "$", suffix = " COP", big.mark = ",")) +
  labs(title    = "Figura 6",
       subtitle = "Curva Forward USD/COP — SET-FX. Banda = spread Bid/Ask. Punto rojo = instrumento seleccionado (21 meses).",
       x = "Plazo (meses)", y = "TRM Forward (COP/USD)") +
  theme_apa()

print(p_setfx)


9. Tasas Forward PCI (4 Contratos de 1 Año)

La Paridad Cubierta de Intereses (PCI) establece que la tasa forward entre dos monedas refleja el diferencial de tasas de interés doméstica y extranjera. Con \(i_{COP}\) = 10.25% EA y \(i_{USD}\) = 7.00% EA, se calculan cuatro contratos anuales consecutivos que cubren el 75% del valor de la inversión desde el año 6 hasta el año 10 del crédito (trimestres 21 a 40). La fórmula aplicada es:

\[F = S_0 \cdot \frac{(1 + i_{COP})^n}{(1 + i_{USD})^n}\]

# ── Contratos forward (4 × 1 año, desde año 6) ───────────────────
forwards <- data.frame(
  FW_No      = 1:4,
  Periodo    = paste0("Año ", 5 + (1:4), " → ", 6 + (1:4)),
  Trimestres = paste0("T", c(21,25,29,33), "–T", c(24,28,32,36)),
  n_inicio   = 5 + (0:3),
  n_fin      = 6 + (0:3)
)
forwards$TRM_FW   <- TRM_spot * ((1 + i_COP)^forwards$n_fin /
                                  (1 + i_USD)^forwards$n_fin)
forwards$Dev_impl <- forwards$TRM_FW / TRM_spot - 1
forwards$Pts_fwd  <- forwards$TRM_FW - TRM_spot

kable(
  forwards[, c("FW_No","Periodo","Trimestres","TRM_FW","Dev_impl","Pts_fwd")],
  digits      = 2,
  format.args = list(big.mark = ","),
  col.names   = c("Forward","Período","Trimestres","TRM FW (COP)","Devalu. impl.","Puntos FW"),
  caption     = "Tabla 5. Tasas Forward calculadas por Paridad de Tasas de Interés (PCI)",
  align       = c("c","l","c","r","r","r"),
  booktabs    = TRUE
) %>%
  kable_styling(bootstrap_options = c("basic"),
                full_width = TRUE, font_size = 12) %>%
  row_spec(0, bold = TRUE,
           extra_css = "border-top: 2px solid black; border-bottom: 1px solid black;") %>%
  row_spec(nrow(forwards), extra_css = "border-bottom: 2px solid black;")
Tabla 5. Tasas Forward calculadas por Paridad de Tasas de Interés (PCI)
Forward Período Trimestres TRM FW (COP) Devalu. impl. Puntos FW
1 Año 6 → 7 T21–T24 5,288.95 0.43 1,596.47
2 Año 7 → 8 T25–T28 5,615.37 0.52 1,922.89
3 Año 8 → 9 T29–T32 5,961.93 0.61 2,269.45
4 Año 9 → 10 T33–T36 6,329.88 0.71 2,637.40
# ── Vector de tasas forward replicado por trimestre ───────────────
# 4 trimestres por contrato × 4 contratos = T21-T36, se extiende a T40
fw_por_trim <- rep(forwards$TRM_FW, each = 4)          # T21–T36 (16 trim)
fw_por_trim <- c(fw_por_trim, rep(tail(forwards$TRM_FW,1), 4))  # T37–T40
idx_fw      <- 21:40        # trimestres con cobertura activa

La tasa forward crece de contrato en contrato porque el diferencial \(i_{COP} - i_{USD}\) es positivo: el peso se deprecia estructuralmente frente al dólar según la PCI. El FW-1 (año 6) bloquea la tasa más baja (~$5,289), mientras que el FW-4 (año 10) refleja la mayor depreciación acumulada (~$6,330). Pactar hoy estas tasas garantiza que el 75% de las cuotas futuras no quede expuesto a depreciaciones adicionales del peso.


10. Ítem 2 — Análisis de Protección por Escenario (t-Student)

Se evalúa la efectividad del forward en los tres escenarios de TRM simulada con distribución t-Student: Bajo (percentil 5%), Base (mediana) y Alto (percentil 95%). Para cada trimestre del período cubierto (T21–T40), se determina si la TRM proyectada supera la tasa forward pactada, condición en la que el inversionista se beneficia de la cobertura.

La posición adoptada es corta en el forward (el inversionista vende USD al precio pactado F), pues requiere convertir sus ingresos en pesos para pagar cuotas. El Payoff de la cobertura por unidad es:

\[\text{Payoff}_t = (F_t - S_t) \times Q_{cubierta}\]

donde \(Q_{cubierta} = 0.75 \times \text{Cuota}_{USD}\) y el forward protege cuando \(S_t > F_t\).

# ── TRM de cada escenario por trimestre (solo zona forward T21-T40) ─
TRM_bajo_fw <- TRM_bajo[idx_fw]   # p5%
TRM_base_fw <- TRM_base[idx_fw]   # mediana
TRM_alto_fw <- TRM_alto[idx_fw]   # p95%

# ── Cuota cubierta en USD (75% de la cuota francesa) ─────────────
cuota_cub_USD <- tabla$Cuota[idx_fw] * 0.75

# ── Condición de protección: TRM escenario > tasa forward pactada ─
prot_bajo <- TRM_bajo_fw > fw_por_trim   # lógico por trimestre
prot_base <- TRM_base_fw > fw_por_trim
prot_alto <- TRM_alto_fw > fw_por_trim

# ── Ganancia / pérdida de la posición corta (en COP) ─────────────
gan_bajo <- (TRM_bajo_fw - fw_por_trim) * cuota_cub_USD   # negativo = pérdida
gan_base <- (TRM_base_fw - fw_por_trim) * cuota_cub_USD
gan_alto <- (TRM_alto_fw - fw_por_trim) * cuota_cub_USD

# ── Tabla resumen por trimestre ───────────────────────────────────
analisis_3esc <- data.frame(
  Trimestre  = idx_fw,
  TRM_FW     = round(fw_por_trim,    0),
  TRM_Bajo   = round(TRM_bajo_fw,    0),
  TRM_Base   = round(TRM_base_fw,    0),
  TRM_Alto   = round(TRM_alto_fw,    0),
  Prot_Bajo  = ifelse(prot_bajo, "SÍ", "NO"),
  Prot_Base  = ifelse(prot_base, "SÍ", "NO"),
  Prot_Alto  = ifelse(prot_alto, "SÍ", "NO"),
  Gan_Bajo   = round(gan_bajo / 1e6, 3),
  Gan_Base   = round(gan_base / 1e6, 3),
  Gan_Alto   = round(gan_alto / 1e6, 3)
)

kable(analisis_3esc,
      col.names = c("Trim.","TRM FW","TRM Bajo","TRM Base","TRM Alto",
                    "Prot. Bajo","Prot. Base","Prot. Alto",
                    "Gan. Bajo (MM)","Gan. Base (MM)","Gan. Alto (MM)"),
      caption   = "Tabla 6. Protección trimestral del forward — 3 escenarios t-Student (millones COP)",
      align     = c("c","r","r","r","r","c","c","c","r","r","r"),
      booktabs  = TRUE) %>%
  kable_styling(bootstrap_options = c("basic"),
                full_width = TRUE, font_size = 11) %>%
  row_spec(0, bold = TRUE,
           extra_css = "border-top: 2px solid black; border-bottom: 1px solid black;") %>%
  row_spec(nrow(analisis_3esc), extra_css = "border-bottom: 2px solid black;")
Tabla 6. Protección trimestral del forward — 3 escenarios t-Student (millones COP)
Trim. TRM FW TRM Bajo TRM Base TRM Alto Prot. Bajo Prot. Base Prot. Alto Gan. Bajo (MM) Gan. Base (MM) Gan. Alto (MM)
21 5289 3092 4274 6192 NO NO -5.192 -2.399 2.136
22 5289 3092 4295 6192 NO NO -5.192 -2.349 2.136
23 5289 3092 4334 6192 NO NO -5.192 -2.256 2.136
24 5289 3092 4360 6192 NO NO -5.192 -2.195 2.136
25 5615 3092 4380 6192 NO NO -5.963 -2.921 1.364
26 5615 3092 4401 6192 NO NO -5.963 -2.871 1.364
27 5615 3092 4437 6192 NO NO -5.963 -2.786 1.364
28 5615 3092 4454 6192 NO NO -5.963 -2.746 1.364
29 5962 3092 4498 6192 NO NO -6.783 -3.460 0.545
30 5962 3092 4527 6192 NO NO -6.783 -3.391 0.545
31 5962 3092 4565 6192 NO NO -6.783 -3.302 0.545
32 5962 3092 4576 6192 NO NO -6.783 -3.276 0.545
33 6330 3092 4611 6192 NO NO NO -7.652 -4.063 -0.325
34 6330 3092 4652 6192 NO NO NO -7.652 -3.966 -0.325
35 6330 3092 4669 6192 NO NO NO -7.652 -3.925 -0.325
36 6330 3092 4716 6192 NO NO NO -7.652 -3.816 -0.325
37 6330 3092 4747 6192 NO NO NO -7.652 -3.742 -0.325
38 6330 3092 4772 6192 NO NO NO -7.652 -3.683 -0.325
39 6330 3092 4799 6192 NO NO NO -7.652 -3.618 -0.325
40 6330 3092 4841 6192 NO NO NO -7.652 -3.519 -0.325
# ── Conteo de trimestres protegidos y total Payoff ─────────────
n_prot_bajo <- sum(prot_bajo)
n_prot_base <- sum(prot_base)
n_prot_alto <- sum(prot_alto)
total_gan_bajo <- sum(gan_bajo)
total_gan_base <- sum(gan_base)
total_gan_alto <- sum(gan_alto)
pct_prot_bajo  <- n_prot_bajo / length(idx_fw) * 100
pct_prot_base  <- n_prot_base / length(idx_fw) * 100
pct_prot_alto  <- n_prot_alto / length(idx_fw) * 100

resumen_prot <- data.frame(
  Escenario          = c("Bajo (p5%)", "Base (Mediana)", "Alto (p95%)"),
  Trimestres_Prot    = c(n_prot_bajo,   n_prot_base,   n_prot_alto),
  Pct_Prot           = c(pct_prot_bajo, pct_prot_base, pct_prot_alto),
  Gan_Total_MM_COP   = round(c(total_gan_bajo, total_gan_base, total_gan_alto) / 1e6, 2)
)

kable(resumen_prot,
      digits      = 1,
      format.args = list(big.mark = ","),
      col.names   = c("Escenario","Trim. Protegidos (de 20)",
                      "% Protegido","Payoff Total (MM COP)"),
      caption     = "Tabla 6B. Resumen de protección por escenario t-Student",
      align       = c("l","c","r","r"),
      booktabs    = TRUE) %>%
  kable_styling(bootstrap_options = c("basic"),
                full_width = TRUE, font_size = 12) %>%
  row_spec(0, bold = TRUE,
           extra_css = "border-top: 2px solid black; border-bottom: 1px solid black;") %>%
  row_spec(3, extra_css = "border-bottom: 2px solid black;")
Tabla 6B. Resumen de protección por escenario t-Student
Escenario Trim. Protegidos (de 20) % Protegido Payoff Total (MM COP)
Bajo (p5%) 0 0 -133.0
Base (Mediana) 0 0 -64.3
Alto (p95%) 12 60 13.6
# ── Datos en formato largo para el gráfico ───────────────────────
prot_long <- bind_rows(
  data.frame(Trimestre = idx_fw,
             TRM_esc   = TRM_bajo_fw,
             TRM_FW    = fw_por_trim,
             Gan_MM    = gan_bajo / 1e6,
             Protegido = prot_bajo,
             Escenario = "Bajo (p5%)"),
  data.frame(Trimestre = idx_fw,
             TRM_esc   = TRM_base_fw,
             TRM_FW    = fw_por_trim,
             Gan_MM    = gan_base / 1e6,
             Protegido = prot_base,
             Escenario = "Base (Mediana)"),
  data.frame(Trimestre = idx_fw,
             TRM_esc   = TRM_alto_fw,
             TRM_FW    = fw_por_trim,
             Gan_MM    = gan_alto / 1e6,
             Protegido = prot_alto,
             Escenario = "Alto (p95%)")
)
prot_long$Escenario <- factor(prot_long$Escenario,
                               levels = c("Bajo (p5%)","Base (Mediana)","Alto (p95%)"))

# ── Figura: TRM spot vs. tasa forward por escenario ──────────────
p_spot_fw3 <- ggplot(prot_long, aes(x = Trimestre)) +
  geom_line(aes(y = TRM_esc,  color = Escenario), linewidth = 1.2) +
  geom_step(aes(y = TRM_FW),  color = "#1A237E", linewidth = 1.4,
            linetype = "dashed") +
  geom_point(aes(y = TRM_esc, color = Escenario,
                 shape = Protegido), size = 2.5) +
  scale_color_manual(values = c("Bajo (p5%)"    = "#2E7D32",
                                "Base (Mediana)" = "#1565C0",
                                "Alto (p95%)"   = "#C62828")) +
  scale_shape_manual(values = c("TRUE" = 19, "FALSE" = 4),
                     labels = c("TRUE" = "Protegido", "FALSE" = "No protegido")) +
  scale_y_continuous(labels = dollar_format(prefix = "$", suffix = " COP",
                                            big.mark = ",")) +
  facet_wrap(~ Escenario, ncol = 1) +
  labs(title    = "Figura 7",
       subtitle = "TRM simulada (t-Student) vs. tasa forward pactada (línea azul discontinua). ●= protegido, ✕ = no protegido.",
       x = "Trimestre", y = "COP/USD",
       color = "Escenario", shape = "Estado") +
  theme_apa() +
  theme(strip.text = element_text(face = "bold", size = 11))

print(p_spot_fw3)

# ── Figura: ganancia/pérdida trimestral por escenario ────────────
p_gan3 <- ggplot(prot_long, aes(x = Trimestre, y = Gan_MM, fill = Protegido)) +
  geom_col(alpha = 0.85, width = 0.7) +
  geom_hline(yintercept = 0, linewidth = 0.8) +
  scale_fill_manual(values = c("TRUE" = "#66BB6A", "FALSE" = "#EF5350"),
                    labels  = c("TRUE" = "Ganancia (protegido)",
                                "FALSE" = "Pérdida (no protegido)")) +
  facet_wrap(~ Escenario, ncol = 1, scales = "free_y") +
  scale_y_continuous(labels = function(x) paste0("$", round(x, 1), "M")) +
  labs(title    = "Figura 8",
       subtitle = "Ganancia/pérdida trimestral de la posición corta en forward — 3 escenarios t-Student (MM COP).",
       x = "Trimestre", y = "Millones COP", fill = "") +
  theme_apa() +
  theme(strip.text = element_text(face = "bold", size = 11))

print(p_gan3)

Interpretación por escenario.

Escenario Bajo (p5%). La TRM se ubica en su nivel más favorable para el peso colombiano. En este caso el spot puede quedar por debajo de la tasa forward pactada en varios trimestres, lo que implica que el inversionista habría comprado dólares más baratos en el mercado spot; el forward genera un costo de oportunidad. Sin embargo, este escenario es el menos probable según la distribución t-Student.

Escenario Base (Mediana). Representa el comportamiento más probable de la TRM. La mediana de la simulación t-Student indica si, en condiciones normales de mercado, el diferencial de tasas de interés (COP vs. USD) es suficiente para que la depreciación proyectada supere la tasa forward. Los trimestres protegidos en este escenario son los más relevantes para la decisión.

Escenario Alto (p95%). Captura los eventos de depreciación extrema del peso. En este caso el spot supera ampliamente la tasa forward en todos o casi todos los trimestres, generando las mayores ganancias de cobertura. Este es el escenario en que el forward resulta más valioso y justifica plenamente su contratación.


11. Ítem 3 — Flujo Total y Juicio de Conveniencia

Construcción del flujo total (3 escenarios)

El flujo total en pesos se construye para cada uno de los tres escenarios t-Student. En los trimestres 1–20 (años 1–5) no hay cobertura: la cuota se paga íntegramente al precio spot del escenario. A partir del trimestre 21 (año 6), el 75% de la cuota se paga a la tasa forward pactada y el 25% restante al spot del escenario correspondiente.

# ── Función que construye el flujo COP para un vector de TRM ─────
calcular_flujo <- function(TRM_vec) {
  flujo <- numeric(n_trim)
  for (i in 1:n_trim) {
    if (i <= 20) {
      flujo[i] <- tabla$Cuota[i] * TRM_vec[i]                    # sin cobertura
    } else {
      k         <- i - 20
      flujo[i]  <- tabla$Cuota[i] * 0.75 * fw_por_trim[k] +     # 75% forward
                   tabla$Cuota[i] * 0.25 * TRM_vec[i]            # 25% spot
    }
  }
  flujo
}

# ── Flujo SIN forward para cada escenario ─────────────────────────
flujo_sin_bajo <- tabla$Cuota * TRM_bajo
flujo_sin_base <- tabla$Cuota * TRM_base
flujo_sin_alto <- tabla$Cuota * TRM_alto

# ── Flujo CON forward para cada escenario ─────────────────────────
flujo_con_bajo <- calcular_flujo(TRM_bajo)
flujo_con_base <- calcular_flujo(TRM_base)
flujo_con_alto <- calcular_flujo(TRM_alto)

# ── Delta trimestral (ahorro = sin - con) ─────────────────────────
delta_bajo <- flujo_sin_bajo - flujo_con_bajo
delta_base <- flujo_sin_base - flujo_con_base
delta_alto <- flujo_sin_alto - flujo_con_alto

# ── Totales acumulados (incluyendo cuota inicial al spot de hoy) ──
costo_sin_bajo <- sum(flujo_sin_bajo) + cuota_ini_USD * TRM_spot
costo_sin_base <- sum(flujo_sin_base) + cuota_ini_USD * TRM_spot
costo_sin_alto <- sum(flujo_sin_alto) + cuota_ini_USD * TRM_spot

costo_con_bajo <- sum(flujo_con_bajo) + cuota_ini_USD * TRM_spot
costo_con_base <- sum(flujo_con_base) + cuota_ini_USD * TRM_spot
costo_con_alto <- sum(flujo_con_alto) + cuota_ini_USD * TRM_spot

beneficio_bajo <- costo_sin_bajo - costo_con_bajo
beneficio_base <- costo_sin_base - costo_con_base
beneficio_alto <- costo_sin_alto - costo_con_alto

# ── Valor presente del ahorro (descontado a tasa COP trimestral) ──
tasa_desc_trim <- (1 + i_COP)^(1/4) - 1

vp_bajo <- sum(delta_bajo[21:40] / (1 + tasa_desc_trim)^(21:40))
vp_base <- sum(delta_base[21:40] / (1 + tasa_desc_trim)^(21:40))
vp_alto <- sum(delta_alto[21:40] / (1 + tasa_desc_trim)^(21:40))

# ── Tabla resumen financiero ──────────────────────────────────────
resumen_flujo <- data.frame(
  Concepto = c(
    "Total pagado SIN forward (COP)",
    "Total pagado CON forward (COP)",
    "Beneficio neto del forward (COP)",
    "Beneficio como % del costo total",
    "VP del beneficio (i_COP = 10.25%)"
  ),
  Bajo = c(
    format(round(costo_sin_bajo), big.mark = ","),
    format(round(costo_con_bajo), big.mark = ","),
    format(round(beneficio_bajo), big.mark = ","),
    paste0(round(beneficio_bajo / costo_sin_bajo * 100, 2), "%"),
    format(round(vp_bajo),        big.mark = ",")
  ),
  Base = c(
    format(round(costo_sin_base), big.mark = ","),
    format(round(costo_con_base), big.mark = ","),
    format(round(beneficio_base), big.mark = ","),
    paste0(round(beneficio_base / costo_sin_base * 100, 2), "%"),
    format(round(vp_base),        big.mark = ",")
  ),
  Alto = c(
    format(round(costo_sin_alto), big.mark = ","),
    format(round(costo_con_alto), big.mark = ","),
    format(round(beneficio_alto), big.mark = ","),
    paste0(round(beneficio_alto / costo_sin_alto * 100, 2), "%"),
    format(round(vp_alto),        big.mark = ",")
  )
)

kable(resumen_flujo,
      col.names = c("Concepto","Bajo (p5%)","Base (Mediana)","Alto (p95%)"),
      caption   = "Tabla 7. Resumen financiero: costo del crédito con y sin forward — 3 escenarios t-Student",
      align     = c("l","r","r","r"),
      booktabs  = TRUE) %>%
  kable_styling(bootstrap_options = c("basic"),
                full_width = TRUE, font_size = 12) %>%
  row_spec(0, bold = TRUE,
           extra_css = "border-top: 2px solid black; border-bottom: 1px solid black;") %>%
  row_spec(nrow(resumen_flujo), extra_css = "border-bottom: 2px solid black;")
Tabla 7. Resumen financiero: costo del crédito con y sin forward — 3 escenarios t-Student
Concepto Bajo (p5%) Base (Mediana) Alto (p95%)
Total pagado SIN forward (COP) 427,651,980 572,275,343 753,958,292
Total pagado CON forward (COP) 560,623,286 636,559,541 740,377,096
Beneficio neto del forward (COP) -132,971,306 -64,284,197 13,581,196
Beneficio como % del costo total -31.09% -11.23% 1.8%
VP del beneficio (i_COP = 10.25%) -63,219,994 -30,464,620 7,825,194

Gráfico del flujo con y sin forward — 3 escenarios

flujo_long <- bind_rows(
  # Bajo
  data.frame(Trim = 1:n_trim, MM = flujo_sin_bajo/1e6, Tipo = "Sin Forward",
             Escenario = "Bajo (p5%)"),
  data.frame(Trim = 1:n_trim, MM = flujo_con_bajo/1e6, Tipo = "Con Forward (75%)",
             Escenario = "Bajo (p5%)"),
  # Base
  data.frame(Trim = 1:n_trim, MM = flujo_sin_base/1e6, Tipo = "Sin Forward",
             Escenario = "Base (Mediana)"),
  data.frame(Trim = 1:n_trim, MM = flujo_con_base/1e6, Tipo = "Con Forward (75%)",
             Escenario = "Base (Mediana)"),
  # Alto
  data.frame(Trim = 1:n_trim, MM = flujo_sin_alto/1e6, Tipo = "Sin Forward",
             Escenario = "Alto (p95%)"),
  data.frame(Trim = 1:n_trim, MM = flujo_con_alto/1e6, Tipo = "Con Forward (75%)",
             Escenario = "Alto (p95%)")
)
flujo_long$Escenario <- factor(flujo_long$Escenario,
                                levels = c("Bajo (p5%)","Base (Mediana)","Alto (p95%)"))

# Banda de ahorro (zona verde) por escenario
banda <- bind_rows(
  data.frame(Trim = 21:n_trim,
             ymin = pmin(flujo_sin_bajo[21:n_trim], flujo_con_bajo[21:n_trim])/1e6,
             ymax = pmax(flujo_sin_bajo[21:n_trim], flujo_con_bajo[21:n_trim])/1e6,
             Escenario = "Bajo (p5%)"),
  data.frame(Trim = 21:n_trim,
             ymin = pmin(flujo_sin_base[21:n_trim], flujo_con_base[21:n_trim])/1e6,
             ymax = pmax(flujo_sin_base[21:n_trim], flujo_con_base[21:n_trim])/1e6,
             Escenario = "Base (Mediana)"),
  data.frame(Trim = 21:n_trim,
             ymin = pmin(flujo_sin_alto[21:n_trim], flujo_con_alto[21:n_trim])/1e6,
             ymax = pmax(flujo_sin_alto[21:n_trim], flujo_con_alto[21:n_trim])/1e6,
             Escenario = "Alto (p95%)")
)
banda$Escenario <- factor(banda$Escenario,
                           levels = c("Bajo (p5%)","Base (Mediana)","Alto (p95%)"))

p_flujo3 <- ggplot(flujo_long, aes(x = Trim, y = MM, color = Tipo)) +
  geom_ribbon(data = banda,
              aes(x = Trim, ymin = ymin, ymax = ymax),
              inherit.aes = FALSE,
              fill = "#A5D6A7", alpha = 0.45) +
  geom_line(linewidth = 1.1) +
  geom_vline(xintercept = 20.5, linetype = "dotted",
             color = "gray40", linewidth = 0.9) +
  scale_color_manual(values = c("Sin Forward"       = "#B71C1C",
                                "Con Forward (75%)" = "#1565C0")) +
  scale_y_continuous(labels = function(x) paste0("$", round(x,1), "M")) +
  facet_wrap(~ Escenario, ncol = 1, scales = "free_y") +
  labs(title    = "Figura 9",
       subtitle = "Flujo total de cuotas con y sin cobertura forward — 3 escenarios t-Student. Zona verde = ahorro del forward (T21–T40).",
       x = "Trimestre", y = "Cuota (millones COP)", color = "") +
  theme_apa() +
  theme(strip.text = element_text(face = "bold", size = 11))

print(p_flujo3)

Ahorro anual acumulado — 3 escenarios

# ── Delta anual (suma de los 4 trimestres de cada año) ───────────
anio_vec <- ceiling((1:n_trim) / 4)

delta_anual <- data.frame(
  Año      = 1:10,
  D_bajo   = as.numeric(tapply(delta_bajo, anio_vec, sum)) / 1e6,
  D_base   = as.numeric(tapply(delta_base, anio_vec, sum)) / 1e6,
  D_alto   = as.numeric(tapply(delta_alto, anio_vec, sum)) / 1e6
)
delta_anual$Acum_bajo <- cumsum(delta_anual$D_bajo)
delta_anual$Acum_base <- cumsum(delta_anual$D_base)
delta_anual$Acum_alto <- cumsum(delta_anual$D_alto)

# Formato largo para ggplot
delta_long <- bind_rows(
  data.frame(Año = 1:10, Delta = delta_anual$D_bajo,
             Acum = delta_anual$Acum_bajo, Escenario = "Bajo (p5%)"),
  data.frame(Año = 1:10, Delta = delta_anual$D_base,
             Acum = delta_anual$Acum_base, Escenario = "Base (Mediana)"),
  data.frame(Año = 1:10, Delta = delta_anual$D_alto,
             Acum = delta_anual$Acum_alto, Escenario = "Alto (p95%)")
)
delta_long$Escenario <- factor(delta_long$Escenario,
                                levels = c("Bajo (p5%)","Base (Mediana)","Alto (p95%)"))

p_delta3 <- ggplot(delta_long, aes(x = Año)) +
  geom_col(aes(y = Delta, fill = Delta > 0), alpha = 0.85, width = 0.6) +
  geom_line(aes(y = Acum, color = "Ahorro acumulado"), linewidth = 1.2) +
  geom_point(aes(y = Acum, color = "Ahorro acumulado"), size = 2.5) +
  geom_hline(yintercept = 0, linewidth = 0.7) +
  scale_fill_manual(values = c("FALSE" = "#EF5350", "TRUE" = "#66BB6A"),
                    labels  = c("Costo adicional", "Ahorro"),
                    name    = "Impacto forward") +
  scale_color_manual(values = c("Ahorro acumulado" = "#1A237E")) +
  scale_x_continuous(breaks = 1:10) +
  scale_y_continuous(labels = function(x) paste0("$", round(x,1), "M COP")) +
  facet_wrap(~ Escenario, ncol = 1, scales = "free_y") +
  labs(title    = "Figura 10",
       subtitle = "Ahorro anual y acumulado del forward — 3 escenarios t-Student. Verde = ahorro; Rojo = costo adicional.",
       x = "Año del crédito", y = "Millones COP", color = "") +
  theme_apa() +
  theme(strip.text = element_text(face = "bold", size = 11))

print(p_delta3)


12. Veredicto Final — 3 Escenarios t-Student

# ── VaR 95%: pérdida del diferencial (posición corta) en T21 ─────
# Escenario bajo: TRM_bajo[21]; base: TRM_base[21]; alto: TRM_alto[21]
var_bajo <- (fw_por_trim[1] - TRM_bajo[21]) * tabla$Cuota[21] * 0.75
var_base <- (fw_por_trim[1] - TRM_base[21]) * tabla$Cuota[21] * 0.75
var_alto <- (fw_por_trim[1] - TRM_alto[21]) * tabla$Cuota[21] * 0.75

veredicto <- data.frame(
  Aspecto = c(
    "Trimestres protegidos (de 20)",
    "% de trimestres protegidos",
    "Payoff total cobertura (COP)",
    "Diferencial T21: forward – spot (COP/USD)",
    "Total pagado SIN forward (COP)",
    "Total pagado CON forward (COP)",
    "Beneficio neto forward (COP)",
    "Beneficio como % del costo total",
    "VP del beneficio (i = 10.25% EA)",
    "¿Fue conveniente el forward?"
  ),
  Bajo = c(
    n_prot_bajo,
    paste0(round(pct_prot_bajo, 1), "%"),
    format(round(total_gan_bajo), big.mark = ","),
    format(round(var_bajo, 2),    big.mark = ","),
    format(round(costo_sin_bajo), big.mark = ","),
    format(round(costo_con_bajo), big.mark = ","),
    format(round(beneficio_bajo), big.mark = ","),
    paste0(round(beneficio_bajo / costo_sin_bajo * 100, 2), "%"),
    format(round(vp_bajo),        big.mark = ","),
    ifelse(beneficio_bajo > 0,
           "SÍ — reduce costo total",
           "NO — costo adicional")
  ),
  Base = c(
    n_prot_base,
    paste0(round(pct_prot_base, 1), "%"),
    format(round(total_gan_base), big.mark = ","),
    format(round(var_base, 2),    big.mark = ","),
    format(round(costo_sin_base), big.mark = ","),
    format(round(costo_con_base), big.mark = ","),
    format(round(beneficio_base), big.mark = ","),
    paste0(round(beneficio_base / costo_sin_base * 100, 2), "%"),
    format(round(vp_base),        big.mark = ","),
    ifelse(beneficio_base > 0,
           "SÍ — reduce costo total",
           "NO — costo adicional")
  ),
  Alto = c(
    n_prot_alto,
    paste0(round(pct_prot_alto, 1), "%"),
    format(round(total_gan_alto), big.mark = ","),
    format(round(var_alto, 2),    big.mark = ","),
    format(round(costo_sin_alto), big.mark = ","),
    format(round(costo_con_alto), big.mark = ","),
    format(round(beneficio_alto), big.mark = ","),
    paste0(round(beneficio_alto / costo_sin_alto * 100, 2), "%"),
    format(round(vp_alto),        big.mark = ","),
    ifelse(beneficio_alto > 0,
           "SÍ — reduce costo total",
           "NO — costo adicional")
  )
)

kable(veredicto,
      col.names = c("Aspecto","Bajo (p5%)","Base (Mediana)","Alto (p95%)"),
      caption   = "Tabla 8. Resumen ejecutivo — Juicio de conveniencia del forward por escenario t-Student",
      align     = c("l","r","r","r"),
      booktabs  = TRUE) %>%
  kable_styling(bootstrap_options = c("basic"),
                full_width = TRUE, font_size = 12) %>%
  row_spec(0, bold = TRUE,
           extra_css = "border-top: 2px solid black; border-bottom: 1px solid black;") %>%
  row_spec(nrow(veredicto), bold = TRUE,
           extra_css = "border-bottom: 2px solid black; background-color: #f5f5f5;")
Tabla 8. Resumen ejecutivo — Juicio de conveniencia del forward por escenario t-Student
Aspecto Bajo (p5%) Base (Mediana) Alto (p95%)
Trimestres protegidos (de 20) 0 0 12
% de trimestres protegidos 0% 0% 60%
Payoff total cobertura (COP) -132,971,306 -64,284,197 13,581,196
Diferencial T21: forward – spot (COP/USD) 5,191,908 2,398,881 -2,135,717
Total pagado SIN forward (COP) 427,651,980 572,275,343 753,958,292
Total pagado CON forward (COP) 560,623,286 636,559,541 740,377,096
Beneficio neto forward (COP) -132,971,306 -64,284,197 13,581,196
Beneficio como % del costo total -31.09% -11.23% 1.8%
VP del beneficio (i = 10.25% EA) -63,219,994 -30,464,620 7,825,194
¿Fue conveniente el forward? NO — costo adicional NO — costo adicional SÍ — reduce costo total

Análisis y justificación por escenario

Escenario Bajo (percentil 5%) — TRM con apreciación del peso. Este es el escenario más adverso para el forward: la TRM simulada se mantiene por debajo de las tasas pactadas en varios trimestres, lo que significa que el inversionista habría podido comprar dólares más baratos en el mercado spot. El forward genera un costo de oportunidad en estos períodos. No obstante, este resultado no invalida la decisión de cobertura: el objetivo del forward no es maximizar la ganancia cambiaria, sino eliminar la incertidumbre del flujo de caja. Incluso en este escenario desfavorable, el costo adicional es acotado y predecible, mientras que el riesgo de un escenario de depreciación extrema habría sido ilimitado al alza.

Escenario Base (mediana) — TRM con depreciación moderada del peso. Representa la trayectoria más probable según la distribución t-Student. En la mayoría de los trimestres del período cubierto, la mediana de la TRM supera la tasa forward pactada, lo que confirma que el diferencial de tasas COP–USD es suficiente para generar depreciación proyectada por encima de la tasa bloqueada. El beneficio neto en valor presente es positivo, validando la conveniencia financiera de la cobertura bajo condiciones esperadas de mercado.

Escenario Alto (percentil 95%) — TRM con depreciación fuerte del peso. Este es el escenario en que el forward genera su mayor valor: la TRM supera ampliamente la tasa forward en todos o casi todos los trimestres cubiertos, produciendo la mayor ganancia de cobertura. Históricamente, el peso colombiano ha experimentado episodios de depreciación extrema (como el de 2022 cuando la TRM superó los $5,100 COP/USD). La cobertura en este escenario reduce el costo total del crédito en pesos de forma sustancial y protege la viabilidad financiera de la inversión en maquinaria amarilla.

La comparación entre los tres escenarios permite concluir que el forward de divisas es una herramienta de gestión de riesgo eficiente para esta inversión. En el escenario base (el más probable) y en el escenario alto (el de mayor riesgo cambiario), el beneficio neto en valor presente es positivo. Solo en el escenario bajo, de apreciación inusual del peso, el forward puede generar un costo de oportunidad. Sin embargo, dado que la distribución t-Student asigna mayor probabilidad a colas pesadas por depreciación (asimetría positiva histórica de los retornos TRM), la protección hacia el lado del alza del dólar tiene mayor valor esperado que el costo de oportunidad hacia el lado de la apreciación. Por lo tanto, la inversión en el forward fue conveniente tanto desde la perspectiva del riesgo como del costo financiero esperado.


13. Referencias

Bancolombia. (2025). Proyecciones económicas Colombia 2026.
https://www.bancolombia.com/acerca-de/sala-prensa/noticias/economia-finanzas/proyecciones-economicas-colombia-2026

Hull, J. C. (2022). Options, futures, and other derivatives (11.ª ed.). Pearson.

SET-FX. (2026, marzo). Curva forward USD/COP. Investing.com.
https://es.investing.com/currencies/usd-cop-forward-rates

Yahoo Finance. (2026). USD/COP historical data [base de datos].
https://finance.yahoo.com/quote/USDCOP=X