install.packages(“kableExtra”)
library(quantmod)
library(ggplot2)
library(dplyr)
library(scales)
library(moments)
library(MASS)
library(lubridate)
library(knitr)
library(kableExtra)murcielago lindo
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.1025
i_USD <- 0.0700
cat(sprintf("TRM Spot (BANREP, 20-mar-26): $%s COP/USD\n", format(TRM_spot, big.mark=",")))## TRM Spot (BANREP, 20-mar-26): $3,692.48 COP/USD
## Inversión total: $350,000,000 COP
## Inversión total: $94787.24 USD
## Monto financiado (90%): $85308.52 USD
## Tasa crédito Wells Fargo: 8.50% EA
## Tasa comercial COP (i_COP): 10.25% EA
## Tasa comercial USD (i_USD): 7.00% EA
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 último año 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) afirma: “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).
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;")| Estadístico | Valor |
|---|---|
| Variación mensual promedio (μ) | 0.00270 (0.2700%) |
| Desviación estándar mensual (σ) | 0.03135 (3.1350%) |
| Asimetría (Skewness) | 0.941 |
| Curtosis | 5.739 (colas pesadas, >3) |
| Grados de libertad t-Student (ν) | 4.71 |
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 consecuencia, esta distribución representa de forma más precisa el comportamiento de los retornos y el riesgo asociado a eventos extremos, mientras que la normal tiende a subestimarlo.
murcielago lindo
n_sim <- 5000
n_q <- 40
mu_q <- mu_m * 3
sig_q <- sigma_m * sqrt(3)
sim_N <- matrix(NA, n_q, n_sim)
for (s in 1:n_sim)
sim_N[, s] <- TRM_spot * exp(cumsum(
(mu_q - 0.5*sig_q^2) + sig_q * rnorm(n_q)
))
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)
sim_T[, s] <- TRM_spot * exp(cumsum(
(mu_q - 0.5*sig_q^2) + scale_t * rt(n_q, df = nu_est)
))
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") +
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).", n_sim),
x = "Trimestre", y = "TRM (COP/USD)", color = "Distribución", fill = "Distribución") +
theme_apa()
print(p_bmg)##Prueba
# Límites de la TRM
lim_inf <- TRM_spot - 1000
lim_sup <- TRM_spot + 1000
# ---------------------------
# Simulación Normal (con límites)
# ---------------------------
sim_N <- matrix(NA, n_q, n_sim)
for (s in 1:n_sim) {
tray <- TRM_spot * exp(cumsum(
(mu_q - 0.5 * sig_q^2) + sig_q * rnorm(n_q)
))
sim_N[, s] <- pmin(pmax(tray, lim_inf), lim_sup)
}
# ---------------------------
# Simulación t-Student (con límites)
# ---------------------------
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) {
tray <- TRM_spot * exp(cumsum(
(mu_q - 0.5 * sig_q^2) + scale_t * rt(n_q, df = nu_est)
))
sim_T[, s] <- pmin(pmax(tray, lim_inf), lim_sup)
}
# ---------------------------
# Resumen por percentiles
# ---------------------------
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"
)
)
# ---------------------------
# Gráfica
# ---------------------------
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") +
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 (acotada ±1000) — Normal vs t-Student (40 trimestres, n = %d trayectorias).",
n_sim
),
x = "Trimestre",
y = "TRM (COP/USD)",
color = "Distribución",
fill = "Distribución"
) +
theme_apa()
print(p_bmg)murcielago lindo
murcielago lindo
tabla_trm <- bind_rows(
data.frame(
Trim = 1:n_q,
Escenario_bajo = apply(sim_T, 1, quantile, 0.05),
Escenario_base = apply(sim_T, 1, median),
Escenario_alto = apply(sim_T, 1, quantile, 0.95),
Distribucion = "t-Student"
)
)
kable(
tabla_trm,
digits = 0,
format.args = list(big.mark = ","),
caption = "Tabla: Escenarios proyectados de la TRM por trimestre (bajo, base y alto)",
align = c("c","r","r","r","c"),
booktabs = TRUE
) %>%
kable_styling(full_width = TRUE, font_size = 12)| Trim | Escenario_bajo | Escenario_base | Escenario_alto | Distribucion |
|---|---|---|---|---|
| 1 | 3,428 | 3,716 | 4,036 | t-Student |
| 2 | 3,311 | 3,740 | 4,224 | t-Student |
| 3 | 3,236 | 3,763 | 4,378 | t-Student |
| 4 | 3,201 | 3,790 | 4,485 | t-Student |
| 5 | 3,143 | 3,819 | 4,627 | t-Student |
| 6 | 3,123 | 3,853 | 4,692 | t-Student |
| 7 | 3,092 | 3,865 | 4,692 | t-Student |
| 8 | 3,054 | 3,886 | 4,692 | t-Student |
| 9 | 3,019 | 3,921 | 4,692 | t-Student |
| 10 | 3,005 | 3,948 | 4,692 | t-Student |
| 11 | 2,989 | 3,975 | 4,692 | t-Student |
| 12 | 2,985 | 4,005 | 4,692 | t-Student |
| 13 | 2,942 | 4,037 | 4,692 | t-Student |
| 14 | 2,920 | 4,063 | 4,692 | t-Student |
| 15 | 2,906 | 4,088 | 4,692 | t-Student |
| 16 | 2,880 | 4,109 | 4,692 | t-Student |
| 17 | 2,899 | 4,137 | 4,692 | t-Student |
| 18 | 2,881 | 4,159 | 4,692 | t-Student |
| 19 | 2,860 | 4,184 | 4,692 | t-Student |
| 20 | 2,827 | 4,220 | 4,692 | t-Student |
| 21 | 2,832 | 4,242 | 4,692 | t-Student |
| 22 | 2,835 | 4,276 | 4,692 | t-Student |
| 23 | 2,841 | 4,300 | 4,692 | t-Student |
| 24 | 2,835 | 4,342 | 4,692 | t-Student |
| 25 | 2,822 | 4,370 | 4,692 | t-Student |
| 26 | 2,845 | 4,392 | 4,692 | t-Student |
| 27 | 2,822 | 4,412 | 4,692 | t-Student |
| 28 | 2,812 | 4,434 | 4,692 | t-Student |
| 29 | 2,795 | 4,476 | 4,692 | t-Student |
| 30 | 2,788 | 4,506 | 4,692 | t-Student |
| 31 | 2,775 | 4,539 | 4,692 | t-Student |
| 32 | 2,770 | 4,557 | 4,692 | t-Student |
| 33 | 2,775 | 4,615 | 4,692 | t-Student |
| 34 | 2,774 | 4,635 | 4,692 | t-Student |
| 35 | 2,778 | 4,665 | 4,692 | t-Student |
| 36 | 2,793 | 4,692 | 4,692 | t-Student |
| 37 | 2,818 | 4,692 | 4,692 | t-Student |
| 38 | 2,817 | 4,692 | 4,692 | t-Student |
| 39 | 2,817 | 4,692 | 4,692 | t-Student |
| 40 | 2,799 | 4,692 | 4,692 | t-Student |
murcielago lindo
murcielago lindo
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")], 5),
tail(tabla[, c("Trim","Año","Saldo_ini","Interes","Amort","Cuota","Saldo_fin")], 5)
),
digits = 2,
format.args = list(big.mark = ","),
caption = sprintf("Tabla 2. 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(10, extra_css = "border-bottom: 2px solid black;")| 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 |
| 4 | 4 | 1 | 81,039.95 | 1,669.78 | 1,481.89 | 3,151.67 | 79,558.07 |
| 5 | 5 | 2 | 79,558.07 | 1,639.25 | 1,512.42 | 3,151.67 | 78,045.65 |
| 36 | 36 | 9 | 14,829.23 | 305.55 | 2,846.12 | 3,151.67 | 11,983.11 |
| 37 | 37 | 10 | 11,983.11 | 246.90 | 2,904.76 | 3,151.67 | 9,078.35 |
| 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 |
murcielago lindo
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)murcielago lindo
murcielago lindo
deprec_anual <- i_COP - i_USD
deprec_trim <- (1 + deprec_anual)^(1/4) - 1
TRM_proy <- TRM_spot * (1 + deprec_trim)^(0:39)
tabla_cop <- tabla %>%
mutate(TRM_proy = TRM_proy,
Cuota_COP = Cuota * TRM_proy,
Interes_COP = Interes * TRM_proy,
Amort_COP = Amort * TRM_proy,
Saldo_ini_COP = Saldo_ini * TRM_proy)
p_cop <- ggplot(tabla_cop, aes(x = Trim)) +
geom_col(aes(y = Amort_COP / 1e6, fill = "Amortización"), alpha = 0.85) +
geom_col(aes(y = Interes_COP / 1e6, fill = "Interés"), alpha = 0.85) +
scale_fill_manual(values = c("Amortización" = "#1565C0", "Interés" = "#EF5350")) +
scale_y_continuous(labels = function(x) paste0("$", x, "M")) +
labs(title = "Figura 5",
subtitle = sprintf("Crédito transformado a COP con depreciación diferencial PCI. TRM inicial: $%s | Depreciación: %.2f%% anual.",
format(TRM_spot, big.mark=","), deprec_anual*100),
x = "Trimestre", y = "Millones COP", fill = "") +
theme_apa()
print(p_cop)murcielago lindo
murcielago lindo
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 3. 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;")| 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 |
murcielago lindo
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
## -------------------------------------------
## 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. anual: 2.57%
murcielago lindo
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)murcielago lindo
murcielago lindo
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 4. 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;")| Forward | Período | Trimestres | TRM FW (COP) | Devalu. impl. | Puntos FW |
|---|---|---|---|---|---|
| 1 | Año 6 → 7 | T21-T24 | 4,418.62 | 0.20 | 726.14 |
| 2 | Año 7 → 8 | T25-T28 | 4,552.83 | 0.23 | 860.35 |
| 3 | Año 8 → 9 | T29-T32 | 4,691.12 | 0.27 | 998.64 |
| 4 | Año 9 → 10 | T33-T36 | 4,833.61 | 0.31 | 1,141.13 |
fw_por_trim <- rep(forwards$TRM_FW, each = 4)
fw_por_trim <- c(fw_por_trim, rep(tail(forwards$TRM_FW, 1), 4))
idx_fw <- 21:40murcielago lindo
murcielago lindo
cuota_cub_USD <- tabla$Cuota[idx_fw] * 0.75
prot_N <- sapply(seq_along(idx_fw), function(k)
mean(sim_N[idx_fw[k], ] > fw_por_trim[k]))
prot_T <- sapply(seq_along(idx_fw), function(k)
mean(sim_T[idx_fw[k], ] > fw_por_trim[k]))
gan_N <- sapply(seq_along(idx_fw), function(k)
mean((sim_N[idx_fw[k], ] - fw_por_trim[k]) * cuota_cub_USD[k]))
gan_T <- sapply(seq_along(idx_fw), function(k)
mean((sim_T[idx_fw[k], ] - fw_por_trim[k]) * cuota_cub_USD[k]))
analisis <- data.frame(
Trimestre = idx_fw,
TRM_FW = round(fw_por_trim, 0),
Spot_med_N = round(apply(sim_N[idx_fw, ], 1, median), 0),
Spot_med_T = round(apply(sim_T[idx_fw, ], 1, median), 0),
pct_prot_N = round(prot_N * 100, 1),
pct_prot_T = round(prot_T * 100, 1),
gan_MM_N = round(gan_N / 1e6, 3),
gan_MM_T = round(gan_T / 1e6, 3)
)
kable(analisis,
col.names = c("Trim.","TRM FW (COP)","Spot Med (N)",
"Spot Med (T)","% Prot. N","% Prot. T",
"Gan. MM (N)","Gan. MM (T)"),
caption = "Tabla 5. Análisis sistemático de protección por trimestre — Normal vs t-Student",
align = c("c","r","r","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(nrow(analisis), extra_css = "border-bottom: 2px solid black;")| Trim. | TRM FW (COP) | Spot Med (N) | Spot Med (T) | % Prot. N | % Prot. T | Gan. MM (N) | Gan. MM (T) |
|---|---|---|---|---|---|---|---|
| 21 | 4419 | 4240 | 4242 | 44.2 | 43.0 | -0.801 | -0.787 |
| 22 | 4419 | 4293 | 4276 | 45.7 | 44.6 | -0.771 | -0.751 |
| 23 | 4419 | 4314 | 4300 | 46.0 | 45.6 | -0.754 | -0.726 |
| 24 | 4419 | 4345 | 4342 | 47.4 | 47.1 | -0.739 | -0.696 |
| 25 | 4553 | 4387 | 4370 | 44.4 | 43.9 | -1.024 | -0.994 |
| 26 | 4553 | 4418 | 4392 | 45.9 | 45.0 | -0.998 | -0.978 |
| 27 | 4553 | 4453 | 4412 | 46.5 | 45.3 | -0.969 | -0.967 |
| 28 | 4553 | 4465 | 4434 | 47.0 | 46.2 | -0.953 | -0.946 |
| 29 | 4691 | 4515 | 4476 | 44.5 | 43.6 | -1.257 | -1.256 |
| 30 | 4691 | 4549 | 4506 | 45.6 | 44.3 | -1.246 | -1.236 |
| 31 | 4691 | 4541 | 4539 | 46.2 | 45.4 | -1.233 | -1.218 |
| 32 | 4691 | 4586 | 4557 | 46.9 | 46.0 | -1.216 | -1.202 |
| 33 | 4834 | 4607 | 4615 | 0.0 | 0.0 | -1.535 | -1.513 |
| 34 | 4834 | 4649 | 4635 | 0.0 | 0.0 | -1.514 | -1.494 |
| 35 | 4834 | 4686 | 4665 | 0.0 | 0.0 | -1.497 | -1.482 |
| 36 | 4834 | 4692 | 4692 | 0.0 | 0.0 | -1.478 | -1.465 |
| 37 | 4834 | 4692 | 4692 | 0.0 | 0.0 | -1.465 | -1.450 |
| 38 | 4834 | 4692 | 4692 | 0.0 | 0.0 | -1.452 | -1.436 |
| 39 | 4834 | 4692 | 4692 | 0.0 | 0.0 | -1.438 | -1.415 |
| 40 | 4834 | 4692 | 4692 | 0.0 | 0.0 | -1.423 | -1.401 |
murcielago lindo
p_prot <- ggplot(analisis, aes(x = Trimestre)) +
geom_col(aes(y = pct_prot_N, fill = "Normal"),
position = position_dodge(width = 0.6), width = 0.5, alpha = 0.8) +
geom_col(aes(y = pct_prot_T, fill = "t-Student"),
position = position_dodge(width = 0.6), width = 0.5, alpha = 0.8) +
geom_hline(yintercept = 50, linetype = "dashed", color = "#B71C1C", linewidth = 1) +
annotate("text", x = 21.5, y = 53, label = "Umbral 50%",
color = "#B71C1C", size = 3.5, family = "serif") +
scale_fill_manual(values = c("Normal" = "#1565C0", "t-Student" = "#2E7D32")) +
scale_y_continuous(limits = c(0, 100), labels = function(x) paste0(x, "%")) +
labs(title = "Figura 7",
subtitle = sprintf("Porcentaje de escenarios donde el forward protege. n = 5,000 simulaciones. Promedio: Normal = %.1f%%, t-Student = %.1f%%.",
mean(prot_N)*100, mean(prot_T)*100),
x = "Trimestre", y = "% escenarios protegidos", fill = "Distribución") +
theme_apa()
print(p_prot)murcielago lindo
sim_df <- bind_rows(
data.frame(Trim = idx_fw,
p05 = apply(sim_N[idx_fw,], 1, quantile, 0.05),
p25 = apply(sim_N[idx_fw,], 1, quantile, 0.25),
med = apply(sim_N[idx_fw,], 1, median),
p75 = apply(sim_N[idx_fw,], 1, quantile, 0.75),
p95 = apply(sim_N[idx_fw,], 1, quantile, 0.95),
dist = "Normal"),
data.frame(Trim = idx_fw,
p05 = apply(sim_T[idx_fw,], 1, quantile, 0.05),
p25 = apply(sim_T[idx_fw,], 1, quantile, 0.25),
med = apply(sim_T[idx_fw,], 1, median),
p75 = apply(sim_T[idx_fw,], 1, quantile, 0.75),
p95 = apply(sim_T[idx_fw,], 1, quantile, 0.95),
dist = "t-Student")
)
fw_line <- data.frame(Trim = idx_fw, TRM_FW = fw_por_trim)
p_spot_fw <- ggplot(sim_df[sim_df$dist == "t-Student", ], aes(x = Trim)) +
geom_ribbon(aes(ymin = p05, ymax = p95, fill = "Rango 5%-95%"), alpha = 0.15) +
geom_ribbon(aes(ymin = p25, ymax = p75, fill = "Rango 25%-75%"), alpha = 0.30) +
geom_line(aes(y = med, color = "Spot mediana (t-Student)"), linewidth = 1.2) +
geom_step(data = fw_line, aes(y = TRM_FW, color = "TRM Forward pactada"),
linewidth = 1.6, linetype = "dashed") +
scale_fill_manual(values = c("Rango 5%-95%" = "#EF9A9A",
"Rango 25%-75%" = "#EF5350")) +
scale_color_manual(values = c("Spot mediana (t-Student)" = "#B71C1C",
"TRM Forward pactada" = "#1A237E")) +
scale_y_continuous(labels = dollar_format(prefix = "$", suffix = " COP", big.mark = ",")) +
labs(title = "Figura 8",
subtitle = "TRM Spot simulada (t-Student) vs. tasa forward pactada. El forward protege cuando Spot > línea azul (T21–T40).",
x = "Trimestre", y = "COP/USD", color = "", fill = "") +
theme_apa()
print(p_spot_fw)murcielago lindo
murcielago lindo
flujo_sin <- tabla$Cuota * TRM_proy
flujo_con <- numeric(n_trim)
for (i in 1:n_trim) {
if (i <= 20) {
flujo_con[i] <- tabla$Cuota[i] * TRM_proy[i]
} else {
k <- i - 20
flujo_con[i] <- tabla$Cuota[i] * 0.75 * fw_por_trim[k] +
tabla$Cuota[i] * 0.25 * TRM_proy[i]
}
}
delta <- flujo_sin - flujo_con
costo_sin <- sum(flujo_sin) + cuota_ini_USD * TRM_spot
costo_con <- sum(flujo_con) + cuota_ini_USD * TRM_spot
beneficio <- costo_sin - costo_con
tasa_desc_trim <- (1 + i_COP)^(1/4) - 1
vp_ahorro <- sum(delta[21:40] / (1 + tasa_desc_trim)^(21:40))
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%)"),
Valor = c(format(round(costo_sin), big.mark = ","),
format(round(costo_con), big.mark = ","),
format(round(beneficio), big.mark = ","),
paste0(round(beneficio / costo_sin * 100, 2), "%"),
format(round(vp_ahorro), big.mark = ","))
)
kable(resumen_flujo,
caption = "Tabla 6. Resumen financiero: Costo del crédito con y sin cobertura forward",
align = c("l","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;")| Concepto | Valor |
|---|---|
| Total pagado SIN forward (COP) | 581,362,353 |
| Total pagado CON forward (COP) | 580,712,363 |
| Beneficio neto del forward (COP) | 649,990 |
| Beneficio como % del costo total | 0.11% |
| VP del beneficio (i_COP = 10.25%) | 122,580 |
murcielago lindo
flujo_long <- bind_rows(
data.frame(Trim = 1:n_trim, MM_COP = flujo_sin / 1e6, Tipo = "Sin Forward"),
data.frame(Trim = 1:n_trim, MM_COP = flujo_con / 1e6, Tipo = "Con Forward (75%)")
)
p_flujo <- ggplot(flujo_long, aes(x = Trim, y = MM_COP, color = Tipo)) +
geom_line(linewidth = 1.2) +
geom_vline(xintercept = 20.5, linetype = "dotted", color = "gray40", linewidth = 1) +
geom_ribbon(
data = data.frame(
Trim = 21:n_trim,
ymin = pmin(flujo_sin[21:n_trim], flujo_con[21:n_trim]) / 1e6,
ymax = pmax(flujo_sin[21:n_trim], flujo_con[21:n_trim]) / 1e6),
aes(x = Trim, ymin = ymin, ymax = ymax),
inherit.aes = FALSE, fill = "#A5D6A7", alpha = 0.4
) +
annotate("text", x = 21.5, y = max(flujo_sin / 1e6) * 0.96,
label = "Forward activo (75% cubierto)",
hjust = 0, size = 3.5, color = "#2E7D32", family = "serif") +
scale_color_manual(values = c("Sin Forward" = "#B71C1C", "Con Forward (75%)" = "#1565C0")) +
scale_y_continuous(labels = function(x) paste0("$", round(x, 1), "M")) +
labs(title = "Figura 9",
subtitle = "Flujo total de cuotas con y sin cobertura forward. Zona verde = ahorro generado por el forward.",
x = "Trimestre", y = "Cuota (millones COP)", color = "") +
theme_apa()
print(p_flujo)murcielago lindo
flujo_anual <- data.frame(
Año = 1:10,
Delta = tapply(delta, ceiling((1:n_trim) / 4), sum) / 1e6
)
flujo_anual$Acum <- cumsum(flujo_anual$Delta)
p_delta <- ggplot(flujo_anual, 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.3) +
geom_point(aes(y = Acum, color = "Ahorro acumulado"), size = 3) +
geom_hline(yintercept = 0, linewidth = 0.8) +
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("$", x, "M COP")) +
labs(title = "Figura 10",
subtitle = "Ahorro anual y acumulado del forward (millones COP). Verde = ahorro; Rojo = costo adicional.",
x = "Año del crédito", y = "Millones COP", color = "") +
theme_apa()
print(p_delta)murcielago lindo
murcielago lindo
var_95 <- quantile(
(sim_T[21, ] - fw_por_trim[1]) * tabla$Cuota[21] * 0.75,
probs = 0.05
)
veredicto <- data.frame(
Aspecto = c("Cobertura promedio (Normal)",
"Cobertura promedio (t-Student)",
"VaR 95% diferencial T21",
"Beneficio neto forward",
"VP del beneficio",
"¿Fue conveniente?"),
Resultado = c(
paste0(round(mean(prot_N)*100, 1), "% de escenarios protegidos"),
paste0(round(mean(prot_T)*100, 1), "% de escenarios protegidos"),
paste0("$", format(round(var_95), big.mark=","), " COP/trimestre"),
paste0("$", format(round(beneficio), big.mark=","), " COP"),
paste0("$", format(round(vp_ahorro), big.mark=","), " COP"),
ifelse(beneficio > 0,
"SÍ — El forward reduce costo y riesgo cambiario",
"Costo neto, pero reduce incertidumbre del flujo")
)
)
kable(veredicto,
caption = "Tabla 7. Resumen ejecutivo — Juicio de conveniencia del forward",
align = c("l","l"),
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;")| Aspecto | Resultado |
|---|---|
| Cobertura promedio (Normal) | 27.5% de escenarios protegidos |
| Cobertura promedio (t-Student) | 27% de escenarios protegidos |
| VaR 95% diferencial T21 | $-3,751,545 COP/trimestre |
| Beneficio neto forward | $649,990 COP |
| VP del beneficio | $122,580 COP |
| ¿Fue conveniente? | SÍ — El forward reduce costo y riesgo cambiario |
murcielago lindo
Bancolombia. (2025). Proyecciones económicas Colombia 2026. https://www.bancolombia.com/acerca-de/sala-prensa/noticias/economia-finanzas/proyecciones-economicas-colombia-2026