formatear_cop <- function(x) {
  scales::label_number(big.mark = ".", decimal.mark = ",", prefix = "$", accuracy = 1)(x)
}

formatear_usd <- function(x) {
  scales::label_dollar(prefix = "USD ", big.mark = ",", decimal.mark = ".", accuracy = 0.01)(x)
}

formatear_pct <- function(x) {
  scales::label_percent(accuracy = 0.01, decimal.mark = ",")(x)
}

formatear_trm <- function(x) {
  scales::label_number(big.mark = ".", decimal.mark = ",", accuracy = 0.01)(x)
}

calcular_credito_frances <- function(monto_inicial_usd, tasa_periodica, numero_periodos) {
  cuota_usd <- monto_inicial_usd * tasa_periodica / (1 - (1 + tasa_periodica)^(-numero_periodos))

  saldo <- monto_inicial_usd
  tabla <- vector("list", numero_periodos)

  for (periodo in seq_len(numero_periodos)) {
    interes_usd <- saldo * tasa_periodica
    abono_capital_usd <- cuota_usd - interes_usd
    saldo_final_usd <- max(saldo - abono_capital_usd, 0)

    tabla[[periodo]] <- tibble::tibble(
      periodo = periodo,
      cuota_usd = cuota_usd,
      interes_usd = interes_usd,
      abono_capital_usd = abono_capital_usd,
      saldo_inicial_usd = saldo,
      saldo_final_usd = saldo_final_usd
    )

    saldo <- saldo_final_usd
  }

  dplyr::bind_rows(tabla)
}

obtener_trm_mensual_fred <- function() {
  url_fred <- "https://fred.stlouisfed.org/graph/fredgraph.csv?id=COLCCUSMA02STM"

  datos <- readr::read_csv(url_fred, show_col_types = FALSE) |>
    janitor::clean_names()

  names(datos) <- c("fecha", "trm_mensual")

  datos <- datos |>
    dplyr::mutate(
      fecha = suppressWarnings(lubridate::ymd(fecha)),
      trm_mensual = suppressWarnings(as.numeric(trm_mensual))
    ) |>
    dplyr::filter(!is.na(fecha), !is.na(trm_mensual)) |>
    dplyr::arrange(fecha)

  datos
}

obtener_forward_publico_banrep <- function(ruta_archivo, url_descarga) {
  if (!file.exists(ruta_archivo)) {
    dir.create(dirname(ruta_archivo), recursive = TRUE, showWarnings = FALSE)
    download.file(url_descarga, destfile = ruta_archivo, mode = "wb")
  }

  readxl::read_excel(
    ruta_archivo,
    sheet = "5. DevaluacionesSector",
    skip = 7,
    na = c("#N/A", "NA", "")
  ) |>
    janitor::clean_names() |>
    dplyr::mutate(fecha = as.Date(fecha))
}

simular_bmg_normal <- function(spot_inicial, media_log_mensual, volatilidad_mensual, meses, simulaciones) {
  choques <- matrix(rnorm(meses * simulaciones), nrow = meses, ncol = simulaciones)
  rendimientos <- (media_log_mensual - 0.5 * volatilidad_mensual^2) + volatilidad_mensual * choques
  trayectorias <- apply(rendimientos, 2, cumsum)
  trayectorias <- exp(trayectorias) * spot_inicial
  rbind(rep(spot_inicial, simulaciones), trayectorias)
}

simular_bmg_t_student <- function(spot_inicial, media_log_mensual, volatilidad_mensual, meses, simulaciones, grados_libertad) {
  choques_crudos <- matrix(rt(meses * simulaciones, df = grados_libertad), nrow = meses, ncol = simulaciones)
  choques_ajustados <- choques_crudos / sqrt(grados_libertad / (grados_libertad - 2))
  rendimientos <- (media_log_mensual - 0.5 * volatilidad_mensual^2) + volatilidad_mensual * choques_ajustados
  trayectorias <- apply(rendimientos, 2, cumsum)
  trayectorias <- exp(trayectorias) * spot_inicial
  rbind(rep(spot_inicial, simulaciones), trayectorias)
}

resumen_vector <- function(x) {
  tibble::tibble(
    promedio = mean(x, na.rm = TRUE),
    mediana = median(x, na.rm = TRUE),
    minimo = min(x, na.rm = TRUE),
    p5 = quantile(x, 0.05, na.rm = TRUE),
    p95 = quantile(x, 0.95, na.rm = TRUE),
    maximo = max(x, na.rm = TRUE)
  )
}

obtener_resumen_trayectorias <- function(matriz_trm) {
  tibble::tibble(
    mes = 0:(nrow(matriz_trm) - 1),
    p5 = apply(matriz_trm, 1, quantile, probs = 0.05, na.rm = TRUE),
    p50 = apply(matriz_trm, 1, quantile, probs = 0.50, na.rm = TRUE),
    p95 = apply(matriz_trm, 1, quantile, probs = 0.95, na.rm = TRUE)
  )
}

construir_flujos_cobertura <- function(tabla_credito_usd, matriz_trm, tasa_forward_anual, porcentaje_cobertura, pagos_por_anio) {
  meses_pago <- seq(3, nrow(tabla_credito_usd) * 3, by = 3)
  trm_en_pagos <- matriz_trm[meses_pago + 1, , drop = FALSE]

  cuotas_usd <- tabla_credito_usd$cuota_usd
  costos_sin_cobertura <- colSums(trm_en_pagos * cuotas_usd)

  tabla_credito_anual <- tabla_credito_usd |>
    dplyr::mutate(anio = ceiling(periodo / pagos_por_anio)) |>
    dplyr::group_by(anio) |>
    dplyr::summarise(cuota_anual_usd = sum(cuota_usd), .groups = "drop")

  anios_cubiertos <- 6:9
  meses_inicio_forward <- c(60, 72, 84, 96)

  forward_por_anio <- purrr::map2_dfc(
    meses_inicio_forward,
    anios_cubiertos,
    function(mes_inicio, anio_actual) {
      spot_inicio <- matriz_trm[mes_inicio + 1, ]
      tasa_forward <- spot_inicio * (1 + tasa_forward_anual)
      tibble::tibble(!!paste0("anio_", anio_actual) := tasa_forward)
    }
  )

  costos_con_cobertura <- costos_sin_cobertura
  eventos_protegidos <- list()

  for (indice in seq_along(anios_cubiertos)) {
    anio_actual <- anios_cubiertos[indice]
    periodos_anio <- which(ceiling(tabla_credito_usd$periodo / pagos_por_anio) == anio_actual)
    meses_anio <- periodos_anio * 3
    spot_promedio_anio <- colMeans(matriz_trm[meses_anio + 1, , drop = FALSE])
    forward_anio <- forward_por_anio[[indice]]
    cuota_anual_usd <- tabla_credito_anual$cuota_anual_usd[tabla_credito_anual$anio == anio_actual]

    costo_sin_cobertura_anio <- cuota_anual_usd * spot_promedio_anio
    costo_con_cobertura_anio <- cuota_anual_usd * (
      porcentaje_cobertura * forward_anio +
        (1 - porcentaje_cobertura) * spot_promedio_anio
    )

    costos_con_cobertura <- costos_con_cobertura - costo_sin_cobertura_anio + costo_con_cobertura_anio

    eventos_protegidos[[indice]] <- tibble::tibble(
      anio = anio_actual,
      ahorro_cobertura_cop = costo_sin_cobertura_anio - costo_con_cobertura_anio,
      protegido = (costo_con_cobertura_anio <= costo_sin_cobertura_anio)
    )
  }

  list(
    costos_sin_cobertura = costos_sin_cobertura,
    costos_con_cobertura = costos_con_cobertura,
    forward_por_anio = forward_por_anio,
    eventos_protegidos = dplyr::bind_rows(eventos_protegidos)
  )
}

1 1. Resumen

Esta practica desarrolla una estrategia integral para financiar la compra de maquinaria amarilla por COP 350 millones, mediante un credito denominado en dolares, y analizar una cobertura cambiaria con forwards de divisas sobre el 75% de la exposicion, a partir del sexto ano.

La estructura que se adopta es la siguiente:

  • Compra total: COP $350.000.000.
  • Cuota inicial: 10,00%.
  • Plazo del credito: 10 anos.
  • Sistema de amortizacion: frances con cuotas trimestrales.
  • Cobertura: 4 forwards anuales desde el ano 6 hasta el ano 9.

Supuesto central de trabajo: para el credito en USD se usa una tasa efectiva anual de 9,75%. Para el forward teorico se usan tasas comerciales de referencia de Estados Unidos y Colombia. Para el componente de mercado se emplea la serie publica del mercado forward USD-COP de BanRep/SET-FX para el rango mayor a 180 dias.

2 2. Carga de datos y supuestos

datos_trm_mensual <- obtener_trm_mensual_fred() |>
  dplyr::filter(fecha >= as.Date(params$fecha_inicio_historia))

datos_forward_publico <- obtener_forward_publico_banrep(
  ruta_archivo = params$ruta_forward_banrep,
  url_descarga = params$url_forward_banrep
)

resumen_forward_largo <- datos_forward_publico |>
  dplyr::filter(stringr::str_to_lower(rango) == "mayor a 180") |>
  dplyr::group_by(fecha) |>
  dplyr::summarise(
    devaluacion_mercado = mean(mercado, na.rm = TRUE),
    devaluacion_total = mean(total, na.rm = TRUE),
    .groups = "drop"
  ) |>
  dplyr::filter(!is.na(devaluacion_mercado)) |>
  dplyr::arrange(fecha)

fecha_forward_referencia <- max(resumen_forward_largo$fecha, na.rm = TRUE)
devaluacion_implicita_mercado <- resumen_forward_largo |>
  dplyr::filter(fecha == fecha_forward_referencia) |>
  dplyr::pull(devaluacion_mercado)

devaluacion_implicita_total <- resumen_forward_largo |>
  dplyr::filter(fecha == fecha_forward_referencia) |>
  dplyr::pull(devaluacion_total)

forward_teorico_1_anio <- params$trm_actual *
  ((1 + params$tasa_comercial_cop_forward) / (1 + params$tasa_comercial_usd_forward))

forward_mercado_1_anio <- params$trm_actual * (1 + devaluacion_implicita_mercado)
supuestos_trabajo <- tibble::tibble(
  supuesto = c(
    "Valor de la maquinaria en COP",
    "TRM spot de referencia",
    "TRM esperada a 12 meses",
    "Tasa credito USD efectiva anual",
    "Tasa comercial USD para paridad forward",
    "Tasa comercial COP para paridad forward",
    "Cobertura forward",
    "Plazo del credito",
    "Frecuencia de pago",
    "Ultima fecha del archivo publico forward",
    "Devaluacion implicita de mercado (>180 dias)",
    "Forward teorico a 1 ano",
    "Forward de mercado a 1 ano"
  ),
  valor = c(
    formatear_cop(params$monto_maquinaria_cop),
    paste0(formatear_trm(params$trm_actual), " COP/USD"),
    paste0(formatear_trm(params$trm_esperada_12m), " COP/USD"),
    formatear_pct(params$tasa_credito_usd_efectiva_anual),
    formatear_pct(params$tasa_comercial_usd_forward),
    formatear_pct(params$tasa_comercial_cop_forward),
    formatear_pct(params$porcentaje_cobertura_forward),
    paste(params$plazo_credito_anios, "anos"),
    "Trimestral",
    as.character(fecha_forward_referencia),
    formatear_pct(devaluacion_implicita_mercado),
    paste0(formatear_trm(forward_teorico_1_anio), " COP/USD"),
    paste0(formatear_trm(forward_mercado_1_anio), " COP/USD")
  )
)

knitr::kable(supuestos_trabajo, align = c("l", "r"), caption = "Supuestos principales del ejercicio") |>
  kableExtra::kable_styling(full_width = FALSE)
Supuestos principales del ejercicio
supuesto valor
Valor de la maquinaria en COP $350.000.000
TRM spot de referencia 3.668,89 COP/USD
TRM esperada a 12 meses 3.808,50 COP/USD
Tasa credito USD efectiva anual 9,75%
Tasa comercial USD para paridad forward 6,75%
Tasa comercial COP para paridad forward 9,87%
Cobertura forward 75,00%
Plazo del credito 10 anos
Frecuencia de pago Trimestral
Ultima fecha del archivo publico forward 2025-02-04
Devaluacion implicita de mercado (>180 dias) 5,14%
Forward teorico a 1 ano 3.776,12 COP/USD
Forward de mercado a 1 ano 3.857,53 COP/USD

3 3. Proceso inicial del credito

3.1 3.1 Analisis fundamental de la TRM

El punto de partida del ejercicio es una TRM de referencia de COP 3.669 por dolar. El escenario base de expectativas a 12 meses se fija en COP 3.808 por dolar, lo que implica una depreciacion esperada cercana a:

depreciacion_esperada_12m <- params$trm_esperada_12m / params$trm_actual - 1
formatear_pct(depreciacion_esperada_12m)
## [1] "3,81%"

3.1.1 Lectura economica

  1. Presion macro cambiaria: cuando el mercado anticipa mayor inflacion, presiones fiscales o una trayectoria de tasas locales mas alta, la TRM tiende a incorporar una prima por riesgo.
  2. Diferencial de tasas: si las tasas comerciales en Colombia son superiores a las estadounidenses, el forward teorico suele ubicarse por encima del spot.
  3. Diferencia entre encuesta y mercado: la expectativa de encuesta puede ser mas baja que el forward de mercado, porque el forward incorpora diferenciales de tasas, liquidez y cobertura.

3.1.2 Comparacion puntual de referentes

comparacion_referentes <- tibble::tibble(
  referencia = c("Spot actual", "Encuesta a 12 meses", "Forward teorico 1 ano", "Forward mercado 1 ano"),
  valor_cop_usd = c(params$trm_actual, params$trm_esperada_12m, forward_teorico_1_anio, forward_mercado_1_anio)
)

knitr::kable(
  comparacion_referentes |>
    dplyr::mutate(valor_cop_usd = paste0(formatear_trm(valor_cop_usd), " COP/USD")),
  align = c("l", "r"),
  caption = "Comparacion entre spot, expectativa y forwards"
) |>
  kableExtra::kable_styling(full_width = FALSE)
Comparacion entre spot, expectativa y forwards
referencia valor_cop_usd
Spot actual 3.668,89 COP/USD
Encuesta a 12 meses 3.808,50 COP/USD
Forward teorico 1 ano 3.776,12 COP/USD
Forward mercado 1 ano 3.857,53 COP/USD

3.2 3.2 Simulacion del credito de la maquinaria con tasa extranjera

Para concretar el ejercicio se adopta una simulacion academica de credito comercial en USD referenciado al programa SBA 7(a), que permite financiar maquinaria y equipo en Estados Unidos. Como tasa del ejercicio se usa Prime + 300 pb, equivalente a 9,75% efectiva anual, lo cual deja una base publica y defendible para la simulacion.

valor_maquinaria_usd <- params$monto_maquinaria_cop / params$trm_actual
cuota_inicial_usd <- valor_maquinaria_usd * params$porcentaje_cuota_inicial
monto_financiado_usd <- valor_maquinaria_usd - cuota_inicial_usd

numero_periodos <- params$plazo_credito_anios * params$pagos_por_anio
tasa_trimestral_credito <- (1 + params$tasa_credito_usd_efectiva_anual)^(1 / params$pagos_por_anio) - 1

tabla_credito_usd <- calcular_credito_frances(
  monto_inicial_usd = monto_financiado_usd,
  tasa_periodica = tasa_trimestral_credito,
  numero_periodos = numero_periodos
) |>
  dplyr::mutate(
    anio = ceiling(periodo / params$pagos_por_anio),
    cuota_inicial_usd = cuota_inicial_usd
  )

resumen_credito_usd <- tibble::tibble(
  concepto = c(
    "Valor total de la maquinaria en USD",
    "Cuota inicial en USD",
    "Monto financiado en USD",
    "Tasa trimestral del credito",
    "Cuota trimestral fija en USD",
    "Pago total del credito en USD"
  ),
  valor = c(
    formatear_usd(valor_maquinaria_usd),
    formatear_usd(cuota_inicial_usd),
    formatear_usd(monto_financiado_usd),
    formatear_pct(tasa_trimestral_credito),
    formatear_usd(unique(tabla_credito_usd$cuota_usd)),
    formatear_usd(sum(tabla_credito_usd$cuota_usd))
  )
)
knitr::kable(resumen_credito_usd, align = c("l", "r"), caption = "Resumen del credito en dolares") |>
  kableExtra::kable_styling(full_width = FALSE)
Resumen del credito en dolares
concepto valor
Valor total de la maquinaria en USD USD 95,396.70
Cuota inicial en USD USD 9,539.67
Monto financiado en USD USD 85,857.03
Tasa trimestral del credito 2,35%
Cuota trimestral fija en USD USD 3,336.17
Pago total del credito en USD USD 133,446.65
tabla_grafica_amortizacion <- tabla_credito_usd |>
  dplyr::select(periodo, interes_usd, abono_capital_usd) |>
  tidyr::pivot_longer(-periodo, names_to = "componente", values_to = "valor_usd")

ggplot(tabla_grafica_amortizacion, aes(x = periodo, y = valor_usd, color = componente)) +
  geom_line(linewidth = 1.1) +
  labs(
    title = "Comportamiento del credito en USD",
    subtitle = "Sistema frances con cuotas trimestrales",
    x = "Periodo trimestral",
    y = "USD",
    color = "Componente"
  ) +
  scale_y_continuous(labels = scales::label_dollar(prefix = "USD ")) +
  theme_minimal()

3.3 3.3 Recreacion del credito en pesos y analisis del comportamiento en COP

tabla_credito_cop <- tabla_credito_usd |>
  dplyr::mutate(
    cuota_cop_spot = cuota_usd * params$trm_actual,
    cuota_cop_encuesta_12m = cuota_usd * params$trm_esperada_12m,
    interes_cop_spot = interes_usd * params$trm_actual,
    saldo_final_cop_spot = saldo_final_usd * params$trm_actual
  )

resumen_credito_cop <- tibble::tibble(
  concepto = c(
    "Cuota trimestral en COP con spot actual",
    "Cuota trimestral en COP con TRM esperada a 12 meses",
    "Diferencia por cuota entre ambos escenarios",
    "Monto financiado equivalente en COP"
  ),
  valor = c(
    formatear_cop(unique(tabla_credito_cop$cuota_cop_spot)),
    formatear_cop(unique(tabla_credito_cop$cuota_cop_encuesta_12m)),
    formatear_cop(unique(tabla_credito_cop$cuota_cop_encuesta_12m - tabla_credito_cop$cuota_cop_spot)),
    formatear_cop(monto_financiado_usd * params$trm_actual)
  )
)
knitr::kable(resumen_credito_cop, align = c("l", "r"), caption = "Credito transformado a pesos") |>
  kableExtra::kable_styling(full_width = FALSE)
Credito transformado a pesos
concepto valor
Cuota trimestral en COP con spot actual $12.240.027
Cuota trimestral en COP con TRM esperada a 12 meses $12.705.789
Diferencia por cuota entre ambos escenarios $465.762
Monto financiado equivalente en COP $315.000.000

El credito en USD tiene cuota fija en dolares, pero en pesos presenta un comportamiento incierto porque cada pago depende de la TRM vigente. Por tanto:

  • si el peso se deprecia, el costo efectivo en COP aumenta;
  • si el peso se aprecia, el costo efectivo en COP disminuye;
  • la cobertura con forwards busca reducir esa variabilidad sobre la porcion cubierta.

3.4 3.4 Retornos mensuales, desviacion estandar y simulacion BMG

datos_retornos <- datos_trm_mensual |>
  dplyr::mutate(
    retorno_simple = trm_mensual / dplyr::lag(trm_mensual) - 1,
    retorno_logaritmico = log(trm_mensual / dplyr::lag(trm_mensual))
  ) |>
  dplyr::filter(!is.na(retorno_logaritmico))

media_log_mensual <- mean(datos_retornos$retorno_logaritmico)
desviacion_mensual <- sd(datos_retornos$retorno_logaritmico)

resumen_retornos <- tibble::tibble(
  metrica = c("Numero de observaciones", "Media logaritmica mensual", "Desviacion estandar mensual"),
  valor = c(
    nrow(datos_retornos),
    formatear_pct(media_log_mensual),
    formatear_pct(desviacion_mensual)
  )
)
knitr::kable(resumen_retornos, align = c("l", "r"), caption = "Resumen de retornos mensuales de la TRM proxy") |>
  kableExtra::kable_styling(full_width = FALSE)
Resumen de retornos mensuales de la TRM proxy
metrica valor
Numero de observaciones 193
Media logaritmica mensual 0,32%
Desviacion estandar mensual 3,09%
ggplot(datos_retornos, aes(x = fecha, y = retorno_simple)) +
  geom_line(linewidth = 0.8) +
  geom_hline(yintercept = 0, linetype = "dashed") +
  labs(
    title = "Retornos mensuales de la TRM proxy",
    subtitle = "Serie mensual COP/USD utilizada para la simulacion",
    x = "Fecha",
    y = "Retorno mensual"
  ) +
  scale_y_continuous(labels = scales::label_percent(decimal.mark = ",")) +
  theme_minimal()

meses_simulacion <- params$plazo_credito_anios * 12

matriz_trm_normal <- simular_bmg_normal(
  spot_inicial = params$trm_actual,
  media_log_mensual = media_log_mensual,
  volatilidad_mensual = desviacion_mensual,
  meses = meses_simulacion,
  simulaciones = params$numero_simulaciones
)

matriz_trm_t <- simular_bmg_t_student(
  spot_inicial = params$trm_actual,
  media_log_mensual = media_log_mensual,
  volatilidad_mensual = desviacion_mensual,
  meses = meses_simulacion,
  simulaciones = params$numero_simulaciones,
  grados_libertad = params$grados_libertad_t
)

resumen_trayectorias_normal <- obtener_resumen_trayectorias(matriz_trm_normal)
resumen_trayectorias_t <- obtener_resumen_trayectorias(matriz_trm_t)

resumen_terminal <- tibble::tibble(
  distribucion = c("Normal", "T-Student"),
  promedio_terminal = c(mean(matriz_trm_normal[nrow(matriz_trm_normal), ]), mean(matriz_trm_t[nrow(matriz_trm_t), ])),
  mediana_terminal = c(median(matriz_trm_normal[nrow(matriz_trm_normal), ]), median(matriz_trm_t[nrow(matriz_trm_t), ])),
  p95_terminal = c(quantile(matriz_trm_normal[nrow(matriz_trm_normal), ], 0.95), quantile(matriz_trm_t[nrow(matriz_trm_t), ], 0.95))
)
knitr::kable(
  resumen_terminal |>
    dplyr::mutate(
      promedio_terminal = formatear_cop(promedio_terminal),
      mediana_terminal = formatear_cop(mediana_terminal),
      p95_terminal = formatear_cop(p95_terminal)
    ),
  align = c("l", "r", "r", "r"),
  caption = "Resumen de la TRM terminal simulada"
) |>
  kableExtra::kable_styling(full_width = FALSE)
Resumen de la TRM terminal simulada
distribucion promedio_terminal mediana_terminal p95_terminal
Normal $5.365 $5.075 $8.703
T-Student $5.399 $5.105 $8.899
datos_bandas_normal <- resumen_trayectorias_normal |>
  dplyr::mutate(modelo = "Normal")

ggplot(datos_bandas_normal, aes(x = mes)) +
  geom_ribbon(aes(ymin = p5, ymax = p95), alpha = 0.20) +
  geom_line(aes(y = p50), linewidth = 1) +
  labs(
    title = "Bandas de simulacion BMG - innovaciones normales",
    x = "Mes",
    y = "COP por USD"
  ) +
  scale_y_continuous(labels = scales::label_number(big.mark = ".", decimal.mark = ",")) +
  theme_minimal()

datos_bandas_t <- resumen_trayectorias_t |>
  dplyr::mutate(modelo = "T-Student")

ggplot(datos_bandas_t, aes(x = mes)) +
  geom_ribbon(aes(ymin = p5, ymax = p95), alpha = 0.20) +
  geom_line(aes(y = p50), linewidth = 1) +
  labs(
    title = "Bandas de simulacion BMG - innovaciones T-Student",
    subtitle = "La distribucion T-Student capta colas pesadas",
    x = "Mes",
    y = "COP por USD"
  ) +
  scale_y_continuous(labels = scales::label_number(big.mark = ".", decimal.mark = ",")) +
  theme_minimal()

4 4. Proceso de forward

4.1 4.1 Uso de informacion del mercado SET-FX para un forward por encima de 6 meses

En el archivo publico de BanRep/SET-FX se usa el rango “mayor a 180” como aproximacion observable del mercado forward superior a 6 meses. Para llevarlo a un esquema operativo de cobertura anual se toma la devaluacion implicita anualizada de mercado y se aproxima un forward de 1 ano.

tabla_forward_referencia <- tibble::tibble(
  concepto = c(
    "Fecha de referencia del archivo publico",
    "Devaluacion implicita anualizada - Mercado",
    "Devaluacion implicita anualizada - Total",
    "Forward anual de mercado estimado desde el spot",
    "Forward anual teorico por paridad simple"
  ),
  valor = c(
    as.character(fecha_forward_referencia),
    formatear_pct(devaluacion_implicita_mercado),
    formatear_pct(devaluacion_implicita_total),
    paste0(formatear_trm(forward_mercado_1_anio), " COP/USD"),
    paste0(formatear_trm(forward_teorico_1_anio), " COP/USD")
  )
)

knitr::kable(
  tabla_forward_referencia,
  align = c("l", "r"),
  caption = "Calibracion del forward usando informacion publica BanRep/SET-FX"
) |>
  kableExtra::kable_styling(full_width = FALSE)
Calibracion del forward usando informacion publica BanRep/SET-FX
concepto valor
Fecha de referencia del archivo publico 2025-02-04
Devaluacion implicita anualizada - Mercado 5,14%
Devaluacion implicita anualizada - Total 5,15%
Forward anual de mercado estimado desde el spot 3.857,53 COP/USD
Forward anual teorico por paridad simple 3.776,12 COP/USD

4.2 4.2 Identificacion de eventos protegidos y no protegidos

resultado_cobertura_normal <- construir_flujos_cobertura(
  tabla_credito_usd = tabla_credito_usd,
  matriz_trm = matriz_trm_normal,
  tasa_forward_anual = devaluacion_implicita_mercado,
  porcentaje_cobertura = params$porcentaje_cobertura_forward,
  pagos_por_anio = params$pagos_por_anio
)

resultado_cobertura_t <- construir_flujos_cobertura(
  tabla_credito_usd = tabla_credito_usd,
  matriz_trm = matriz_trm_t,
  tasa_forward_anual = devaluacion_implicita_mercado,
  porcentaje_cobertura = params$porcentaje_cobertura_forward,
  pagos_por_anio = params$pagos_por_anio
)

probabilidades_proteccion_normal <- resultado_cobertura_normal$eventos_protegidos |>
  dplyr::group_by(anio) |>
  dplyr::summarise(
    probabilidad_protegido = mean(protegido),
    ahorro_promedio_cop = mean(ahorro_cobertura_cop),
    .groups = "drop"
  ) |>
  dplyr::mutate(distribucion = "Normal")

probabilidades_proteccion_t <- resultado_cobertura_t$eventos_protegidos |>
  dplyr::group_by(anio) |>
  dplyr::summarise(
    probabilidad_protegido = mean(protegido),
    ahorro_promedio_cop = mean(ahorro_cobertura_cop),
    .groups = "drop"
  ) |>
  dplyr::mutate(distribucion = "T-Student")

probabilidades_proteccion <- dplyr::bind_rows(
  probabilidades_proteccion_normal,
  probabilidades_proteccion_t
)
knitr::kable(
  probabilidades_proteccion |>
    dplyr::mutate(
      probabilidad_protegido = formatear_pct(probabilidad_protegido),
      ahorro_promedio_cop = formatear_cop(ahorro_promedio_cop)
    ),
  align = c("r", "r", "r", "l"),
  caption = "Eventos protegidos por ano y por distribucion"
) |>
  kableExtra::kable_styling(full_width = FALSE)
Eventos protegidos por ano y por distribucion
anio probabilidad_protegido ahorro_promedio_cop distribucion
6 34,91% -$1.172.501 Normal
7 35,34% -$1.238.839 Normal
8 34,84% -$1.298.472 Normal
9 34,44% -$1.342.967 Normal
6 34,71% -$1.143.268 T-Student
7 34,06% -$1.228.805 T-Student
8 33,69% -$1.301.925 T-Student
9 33,29% -$1.371.423 T-Student
ggplot(probabilidades_proteccion, aes(x = factor(anio), y = probabilidad_protegido, fill = distribucion)) +
  geom_col(position = "dodge") +
  labs(
    title = "Probabilidad de que la cobertura resulte favorable",
    subtitle = "Un evento se considera protegido cuando el costo con forward es menor o igual al costo sin cobertura",
    x = "Ano cubierto",
    y = "Probabilidad"
  ) +
  scale_y_continuous(labels = scales::label_percent(decimal.mark = ",")) +
  theme_minimal()

4.2.1 Criterio de interpretacion

  • La cobertura protege cuando la TRM realizada durante el ano cubierto supera el forward pactado.
  • La cobertura no protege cuando la TRM realizada resulta menor que el forward pactado y, por tanto, se paga una prima implicita por haber asegurado el tipo de cambio.

4.3 4.3 Comparacion de flujo total: credito vs forward

flujo_total_sin_cobertura_normal <- cuota_inicial_usd * params$trm_actual + resultado_cobertura_normal$costos_sin_cobertura
flujo_total_con_cobertura_normal <- cuota_inicial_usd * params$trm_actual + resultado_cobertura_normal$costos_con_cobertura

flujo_total_sin_cobertura_t <- cuota_inicial_usd * params$trm_actual + resultado_cobertura_t$costos_sin_cobertura
flujo_total_con_cobertura_t <- cuota_inicial_usd * params$trm_actual + resultado_cobertura_t$costos_con_cobertura

comparacion_flujos <- tibble::tibble(
  distribucion = c("Normal", "Normal", "T-Student", "T-Student"),
  estrategia = c("Sin cobertura", "Con cobertura", "Sin cobertura", "Con cobertura"),
  promedio = c(
    mean(flujo_total_sin_cobertura_normal), mean(flujo_total_con_cobertura_normal),
    mean(flujo_total_sin_cobertura_t), mean(flujo_total_con_cobertura_t)
  ),
  mediana = c(
    median(flujo_total_sin_cobertura_normal), median(flujo_total_con_cobertura_normal),
    median(flujo_total_sin_cobertura_t), median(flujo_total_con_cobertura_t)
  ),
  p95 = c(
    quantile(flujo_total_sin_cobertura_normal, 0.95), quantile(flujo_total_con_cobertura_normal, 0.95),
    quantile(flujo_total_sin_cobertura_t, 0.95), quantile(flujo_total_con_cobertura_t, 0.95)
  )
)

resumen_ahorros <- tibble::tibble(
  distribucion = c("Normal", "T-Student"),
  probabilidad_de_ahorro = c(
    mean(flujo_total_con_cobertura_normal <= flujo_total_sin_cobertura_normal),
    mean(flujo_total_con_cobertura_t <= flujo_total_sin_cobertura_t)
  ),
  ahorro_promedio_cop = c(
    mean(flujo_total_sin_cobertura_normal - flujo_total_con_cobertura_normal),
    mean(flujo_total_sin_cobertura_t - flujo_total_con_cobertura_t)
  )
)
knitr::kable(
  comparacion_flujos |>
    dplyr::mutate(
      promedio = formatear_cop(promedio),
      mediana = formatear_cop(mediana),
      p95 = formatear_cop(p95)
    ),
  align = c("l", "l", "r", "r", "r"),
  caption = "Comparacion del flujo total de inversion"
) |>
  kableExtra::kable_styling(full_width = FALSE)
Comparacion del flujo total de inversion
distribucion estrategia promedio mediana p95
Normal Sin cobertura $632.876.410 $617.436.673 $855.739.177
Normal Con cobertura $637.929.188 $623.428.942 $858.268.109
T-Student Sin cobertura $635.368.340 $619.919.442 $870.981.116
T-Student Con cobertura $640.413.761 $625.278.594 $874.264.911
knitr::kable(
  resumen_ahorros |>
    dplyr::mutate(
      probabilidad_de_ahorro = formatear_pct(probabilidad_de_ahorro),
      ahorro_promedio_cop = formatear_cop(ahorro_promedio_cop)
    ),
  align = c("l", "r", "r"),
  caption = "Efectividad agregada de la cobertura"
) |>
  kableExtra::kable_styling(full_width = FALSE)
Efectividad agregada de la cobertura
distribucion probabilidad_de_ahorro ahorro_promedio_cop
Normal 21,44% -$5.052.778
T-Student 21,02% -$5.045.421
datos_cajas <- tibble::tibble(
  distribucion = c(
    rep("Normal", length(flujo_total_sin_cobertura_normal) + length(flujo_total_con_cobertura_normal)),
    rep("T-Student", length(flujo_total_sin_cobertura_t) + length(flujo_total_con_cobertura_t))
  ),
  estrategia = c(
    rep("Sin cobertura", length(flujo_total_sin_cobertura_normal)),
    rep("Con cobertura", length(flujo_total_con_cobertura_normal)),
    rep("Sin cobertura", length(flujo_total_sin_cobertura_t)),
    rep("Con cobertura", length(flujo_total_con_cobertura_t))
  ),
  flujo_total_cop = c(
    flujo_total_sin_cobertura_normal,
    flujo_total_con_cobertura_normal,
    flujo_total_sin_cobertura_t,
    flujo_total_con_cobertura_t
  )
)

ggplot(datos_cajas, aes(x = estrategia, y = flujo_total_cop, fill = estrategia)) +
  geom_boxplot(alpha = 0.75) +
  facet_wrap(~ distribucion, scales = "free_y") +
  labs(
    title = "Distribucion del flujo total de inversion",
    subtitle = "Comparacion entre la estrategia cubierta y no cubierta",
    x = NULL,
    y = "COP"
  ) +
  scale_y_continuous(labels = scales::label_number(big.mark = ".", decimal.mark = ",", prefix = "$")) +
  theme_minimal() +
  theme(legend.position = "none")

5 5. Conclusion y recomendacion final

prob_ahorro_normal <- resumen_ahorros$probabilidad_de_ahorro[resumen_ahorros$distribucion == "Normal"]
prob_ahorro_t <- resumen_ahorros$probabilidad_de_ahorro[resumen_ahorros$distribucion == "T-Student"]
ahorro_normal <- resumen_ahorros$ahorro_promedio_cop[resumen_ahorros$distribucion == "Normal"]
ahorro_t <- resumen_ahorros$ahorro_promedio_cop[resumen_ahorros$distribucion == "T-Student"]

cat(glue::glue(
  "<div class='caja-resumen'>\n",
  "<p><strong>Lectura financiera:</strong> el credito en USD permite financiar la maquinaria, pero deja la empresa expuesta a la volatilidad del dolar. En el escenario modelado, la cobertura con forwards sobre el 75% de la exposicion entre los anos 6 y 9 reduce la incertidumbre del flujo en COP y transfiere parte del riesgo cambiario al precio pactado del forward.</p>\n",
  "<p><strong>Hallazgo bajo normalidad:</strong> la probabilidad de que la cobertura reduzca el flujo total es de <strong>{formatear_pct(prob_ahorro_normal)}</strong>, con un ahorro promedio de <strong>{formatear_cop(ahorro_normal)}</strong>.</p>\n",
  "<p><strong>Hallazgo con colas pesadas:</strong> la probabilidad de mejora es de <strong>{formatear_pct(prob_ahorro_t)}</strong>, con un ahorro promedio de <strong>{formatear_cop(ahorro_t)}</strong>. Este escenario suele capturar mejor episodios extremos de depreciacion.</p>\n",
  "<p><strong>Juicio final:</strong> si la empresa prioriza estabilidad del flujo en pesos y quiere limitar el riesgo de depreciaciones fuertes, la estrategia con forward es defendible y tecnicamente consistente. Si el objetivo principal fuera minimizar el costo esperado en un entorno de apreciacion del peso, la cobertura parcial podria parecer menos conveniente en algunos escenarios. Por ello, la recomendacion mas robusta es mantener una <strong>cobertura parcial, no total</strong>, tal como se hizo en el ejercicio.</p>\n",
  "</div>"
))

Lectura financiera: el credito en USD permite financiar la maquinaria, pero deja la empresa expuesta a la volatilidad del dolar. En el escenario modelado, la cobertura con forwards sobre el 75% de la exposicion entre los anos 6 y 9 reduce la incertidumbre del flujo en COP y transfiere parte del riesgo cambiario al precio pactado del forward.

Hallazgo bajo normalidad: la probabilidad de que la cobertura reduzca el flujo total es de 21,44%, con un ahorro promedio de -$5.052.778.

Hallazgo con colas pesadas: la probabilidad de mejora es de 21,02%, con un ahorro promedio de -$5.045.421. Este escenario suele capturar mejor episodios extremos de depreciacion.

Juicio final: si la empresa prioriza estabilidad del flujo en pesos y quiere limitar el riesgo de depreciaciones fuertes, la estrategia con forward es defendible y tecnicamente consistente. Si el objetivo principal fuera minimizar el costo esperado en un entorno de apreciacion del peso, la cobertura parcial podria parecer menos conveniente en algunos escenarios. Por ello, la recomendacion mas robusta es mantener una cobertura parcial, no total, tal como se hizo en el ejercicio.

6 6. Fuentes utilizadas

Las siguientes fuentes sustentan el documento y deben citarse en la entrega final:

  1. Banco de la Republica - TRM y estadisticas economicas
    https://suameca.banrep.gov.co/estadisticas-economicas/informacionSerie/1/Tasa%20de%20cambio%20del%20peso%20colombiano
  2. Banco de la Republica - Encuesta Mensual de Expectativas Economicas (EME), marzo de 2026
    https://www.banrep.gov.co/sites/default/files/publicaciones/archivos/eme-marzo-2026.pdf
  3. Banco de la Republica - Informe diario y series historicas del mercado de forwards USD-COP
    https://www.banrep.gov.co/es/node/64065/printable/print
  4. SET-ICAP - Metodologia de la curva de forward points USD-COP
    https://www.set-icap.com/market-data/correccion-parametros-de-la-curva-de-forward-points-usd-cop/
  5. SBA de Estados Unidos - programa 7(a)
    https://www.sba.gov/funding-programs/loans/7a-loans
  6. FRED - serie mensual COP/USD
    https://fred.stlouisfed.org/series/COLCCUSMA02STM
  7. BBVA Research - contexto macro Colombia
    https://www.bbvaresearch.com/publicaciones/colombia-situacion-colombia-marzo-2026/