library(readxl); library(dplyr); library(tidyr); library(lubridate); library(ggplot2)
# --- Funciones robustas de lectura y limpieza
leer_trm_diaria <- function(ruta = "TRM_diaria.xlsx", hoja_preferida = c("TRM_diaria","Hoja1","Sheet1")){
stopifnot(file.exists(ruta))
shs <- readxl::excel_sheets(ruta)
sheet <- if (length(intersect(hoja_preferida, shs))>0) intersect(hoja_preferida, shs)[1] else shs[1]
# intentos con distintos 'skip'
try_read <- function(skip){
df <- readxl::read_excel(ruta, sheet = sheet, skip = skip)
names(df) <- make.names(names(df), unique = TRUE)
df
}
candidatos_skip <- c(0, 1, 2, 3, 4)
for (sk in candidatos_skip){
df <- try_read(sk)
nms <- tolower(names(df))
# buscamos columnas tipo fecha y valor
id_fecha <- which(grepl("date|fecha", nms))[1]
id_val <- which(grepl("trm|valor|price|close|usd", nms))[1]
if (!is.na(id_fecha) && !is.na(id_val)){
df <- df[, c(id_fecha, id_val)]
names(df) <- c("date","TRM")
# conversión de fecha (excel serial o Date)
if (!inherits(df$date, "Date")) df$date <- as.Date(as.numeric(df$date), origin = "1899-12-30")
df$TRM <- suppressWarnings(as.numeric(df$TRM))
df <- df %>% filter(!is.na(date), !is.na(TRM)) %>% arrange(date)
if (nrow(df) > 200) return(df)
}
}
stop("No se pudo detectar columnas 'date' y 'TRM' en ", ruta)
}
trm <- leer_trm_diaria()
summary(trm$TRM)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 1748 1935 3016 3019 3856 5061
# Retornos diarios
trm_d <- trm %>% arrange(date) %>%
mutate(ret_d = TRM/lag(TRM) - 1,
ret_log_d = log(TRM/lag(TRM)))
vol_d <- sd(trm_d$ret_d, na.rm = TRUE)
vol_m <- vol_d * sqrt(21)
vol_a <- vol_d * sqrt(252)
data.frame(
media_diaria = mean(trm_d$ret_d, na.rm = TRUE),
desv_diaria = vol_d,
vol_mensual_imp = vol_m,
vol_anual_imp = vol_a
)
## media_diaria desv_diaria vol_mensual_imp vol_anual_imp
## 1 0.0001311802 0.006166817 0.02825991 0.09789519
trm_eom <- trm %>%
group_by(yy = year(date), mm = month(date)) %>%
slice_max(order_by = date, n = 1, with_ties = FALSE) %>%
ungroup() %>%
transmute(fecha = date, TRM)
trm_eom <- trm_eom %>% arrange(fecha) %>%
mutate(ret_m = TRM/lag(TRM) - 1,
ret_log_m = log(TRM/lag(TRM)))
stats_m_trm <- data.frame(
media_mensual = mean(trm_eom$ret_m, na.rm = TRUE),
desv_mensual = sd(trm_eom$ret_m, na.rm = TRUE)
)
stats_m_trm
## media_mensual desv_mensual
## 1 0.0001311802 0.006166817
ggplot(trm, aes(date, TRM)) +
geom_line() +
labs(title = "TRM histórica (COP/USD)", x = "Fecha", y = "TRM")
set.seed(123)
M <- 3000
Tn <- 120
S0 <- tail(trm$TRM, 1)
mu_m <- mean(trm_eom$ret_m, na.rm = TRUE)
sig_m <- sd(trm_eom$ret_m, na.rm = TRUE)
Z <- matrix(rnorm(M*Tn), nrow = M)
dr <- (mu_m - 0.5*sig_m^2)
df <- sig_m
TRM_mc <- matrix(NA_real_, nrow = M, ncol = Tn + 1)
TRM_mc[,1] <- S0
for (t in 1:Tn) {
TRM_mc[,t+1] <- TRM_mc[,t] * exp(dr + df * Z[,t])
}
TRM_mc <- TRM_mc[, -1] # M x 120
# Fan chart
pcts <- c(0.05,0.25,0.50,0.75,0.95)
fan <- sapply(1:Tn, function(j) quantile(TRM_mc[,j], probs = pcts, na.rm = TRUE))
fan_df <- as.data.frame(t(fan)); colnames(fan_df) <- paste0("p", pcts*100); fan_df$Mes <- 1:Tn
ggplot() +
geom_ribbon(data = fan_df, aes(x = Mes, ymin = p5, ymax = p95), alpha = 0.15) +
geom_ribbon(data = fan_df, aes(x = Mes, ymin = p25, ymax = p75), alpha = 0.25) +
geom_line(data = fan_df, aes(x = Mes, y = p50))
capex_usd <- 75000; down_pct <- 0.15; apr <- 0.085; years <- 10
n <- years*12; r_m <- apr/12
principal_usd <- capex_usd * (1 - down_pct)
cuota_usd <- principal_usd * r_m / (1 - (1 + r_m)^(-n))
# Cuota en COP simulada
cuota_cop_mc <- TRM_mc * cuota_usd # M x 120
data.frame(
principal_usd = principal_usd,
tasa_mensual = r_m,
cuota_usd = cuota_usd
)
## principal_usd tasa_mensual cuota_usd
## 1 63750 0.007083333 790.4088
leer_futuro <- function(ruta = "TRM FUTURO.xlsx"){
stopifnot(file.exists(ruta))
shs <- readxl::excel_sheets(ruta)
df <- readxl::read_excel(ruta, sheet = shs[1])
nms <- tolower(names(df))
id_fecha <- which(grepl("date|fecha", nms))[1]
id_p <- which(grepl("fut|precio|price|trm|close", nms))[1]
if (is.na(id_fecha) || is.na(id_p)) stop("No encuentro columnas fecha/precio en ", ruta)
df <- df[, c(id_fecha, id_p)]; names(df) <- c("fecha","Futuro")
if (!inherits(df$fecha,"Date")) df$fecha <- as.Date(as.numeric(df$fecha), origin = "1899-12-30")
df$Futuro <- suppressWarnings(as.numeric(df$Futuro))
df <- df %>% filter(!is.na(fecha), !is.na(Futuro)) %>% arrange(fecha)
df
}
fut_raw <- leer_futuro()
fut_eom <- fut_raw %>%
group_by(y=year(fecha), m=month(fecha)) %>%
slice_max(order_by = fecha, n = 1, with_ties = FALSE) %>%
ungroup() %>%
transmute(fecha, Futuro)
fut_eom <- fut_eom %>% arrange(fecha) %>%
mutate(ret_m = Futuro/lag(Futuro) - 1,
ret_log_m = log(Futuro/lag(Futuro)))
stats_m_fut <- data.frame(
media_mensual = mean(fut_eom$ret_m, na.rm = TRUE),
desv_mensual = sd(fut_eom$ret_m, na.rm = TRUE)
)
stats_m_fut
## media_mensual desv_mensual
## 1 0.004276231 0.03719266
# F0 por trayectoria usando S_60 simulado
rd <- 0.0873; rf <- 0.0436; Tm <- 1/12
S_start <- TRM_mc[,60] # TRM en mes 60 (inicio cobertura)
F0_vec <- S_start * exp((rd - rf) * Tm)
# Simulación del futuro 60 meses (años 6–10) con parámetros del futuro
set.seed(123)
Thedge <- 60
mu_f <- mean(fut_eom$ret_m, na.rm = TRUE)
sig_f <- sd(fut_eom$ret_m, na.rm = TRUE)
Zf <- matrix(rnorm(nrow(TRM_mc)*Thedge), nrow = nrow(TRM_mc))
drf <- (mu_f - 0.5*sig_f^2); dff <- sig_f
F_mc <- matrix(NA_real_, nrow = nrow(TRM_mc), ncol = Thedge + 1)
F_mc[,1] <- F0_vec
for (t in 1:Thedge) {
F_mc[,t+1] <- F_mc[,t] * exp(drf + dff * Zf[,t])
}
F_mc <- F_mc[ , -1] # M x 60
# Fan chart
pcts <- c(0.05,0.25,0.50,0.75,0.95)
fanF <- sapply(1:Thedge, function(j) quantile(F_mc[,j], probs = pcts, na.rm = TRUE))
fanF_df <- as.data.frame(t(fanF)); colnames(fanF_df) <- paste0("p", pcts*100); fanF_df$Mes <- 61:120
ggplot() +
geom_ribbon(data = fanF_df, aes(x = Mes, ymin = p5, ymax = p95), alpha = 0.15) +
geom_ribbon(data = fanF_df, aes(x = Mes, ymin = p25, ymax = p75), alpha = 0.25) +
geom_line(data = fanF_df, aes(x = Mes, y = p50))
tam_contrato_usd <- 50000
capex_cop <- 300e6
cover_pct <- 0.75
expo_cop <- cover_pct * capex_cop
expo_usd <- expo_cop / mean(S_start)
num_ctos <- max(1, round(expo_usd / tam_contrato_usd))
IM_pct <- 0.063; MM_ratio <- 0.75
IM_cto <- IM_pct * (mean(F0_vec) * tam_contrato_usd)
MM_cto <- MM_ratio * IM_cto
IM_tot <- IM_cto * num_ctos
MM_tot <- MM_cto * num_ctos
# P&L por diferencias
PnL_fut <- matrix(0, nrow = nrow(F_mc), ncol = ncol(F_mc))
for (t in 2:ncol(F_mc)){
dF <- F_mc[,t] - F_mc[,t-1]
PnL_fut[,t] <- dF * tam_contrato_usd * num_ctos # posición LONG por defecto
}
# Cuenta de margen
flow_margen <- matrix(0, nrow = nrow(F_mc), ncol = ncol(F_mc))
balance <- matrix(NA_real_, nrow = nrow(F_mc), ncol = ncol(F_mc))
balance[,1] <- IM_tot
for (t in 2:ncol(F_mc)){
balance[,t] <- balance[,t-1] + PnL_fut[,t]
need_topup <- balance[,t] < MM_tot
if (any(need_topup)){
topup <- IM_tot - balance[need_topup, t]
balance[need_topup, t] <- IM_tot
flow_margen[need_topup, t] <- flow_margen[need_topup, t] + topup
}
excess <- balance[,t] - IM_tot
need_withdraw <- excess > 0
if (any(need_withdraw)){
retiro <- excess[need_withdraw]
balance[need_withdraw, t] <- IM_tot
flow_margen[need_withdraw, t] <- flow_margen[need_withdraw, t] - retiro
}
}
avg_margin_cash <- mean(rowSums(flow_margen), na.rm = TRUE)
data.frame(num_contratos = num_ctos, IM_total = IM_tot, MM_total = MM_tot, caja_prom_margen = round(avg_margin_cash))
## num_contratos IM_total MM_total caja_prom_margen
## 1 1 12415896 9311922 -61875821
# Pagos de crédito en COP (61–120)
pay_cop_hedge <- cuota_cop_mc[,61:120, drop = FALSE] # M x 60
neto_cubierto <- pay_cop_hedge - PnL_fut # M x 60
# Efectividad (reducción de volatilidad)
vol_sin <- apply(pay_cop_hedge, 2, sd, na.rm = TRUE)
vol_con <- apply(neto_cubierto, 2, sd, na.rm = TRUE)
reducc <- 1 - (vol_con/vol_sin)
data.frame(reduccion_vol_media_pct = round(100*mean(reducc, na.rm = TRUE), 2))
## reduccion_vol_media_pct
## 1 -4624.78
pcts <- c(0.05,0.25,0.50,0.75,0.95)
pay_pctl <- sapply(1:60, function(j) quantile(pay_cop_hedge[,j], probs = pcts, na.rm = TRUE))
net_pctl <- sapply(1:60, function(j) quantile(neto_cubierto[,j], probs = pcts, na.rm = TRUE))
fan_pay <- as.data.frame(t(pay_pctl)); colnames(fan_pay) <- paste0("p", pcts*100); fan_pay$Mes <- 61:120
fan_net <- as.data.frame(t(net_pctl)); colnames(fan_net) <- paste0("p", pcts*100); fan_net$Mes <- 61:120
ggplot() +
geom_ribbon(data = fan_pay, aes(x = Mes, ymin = p5, ymax = p95), alpha = 0.12) +
geom_ribbon(data = fan_net, aes(x = Mes, ymin = p5, ymax = p95), alpha = 0.12) +
geom_line(data = fan_pay, aes(x = Mes, y = p50)) +
geom_line(data = fan_net, aes(x = Mes, y = p50))
Resumen de factores macro relevantes (inflación, diferencial de tasas, cuenta corriente, riesgo país, flujos de portafolio).
Expectativas a 12 meses: referenciar proyecciones de BanRep, FMI, y bancos de inversión.
Implicaciones para una empresa con pasivo en USD: riesgo de conversión a COP y sensibilidad de caja.
Parte 1. Crédito en dólares ###
Monto inicial requerido (300 millones COP): dato del enunciado.
Conversión a USD (TRM inicial ≈ 4000 COP/USD): suponemos una TRM inicial de 4.000 COP/USD.
Fuente: decisión de modelado (para simplificar, porque la TRM varía día a día).
Pago inicial (15%): indicado en el enunciado.
Tasa de interés en EE. UU. y plazo (10 años, sistema francés): tomados como condiciones del crédito internacional, elegidos de un banco de EE. UU. (ejemplo: tasas promedio de préstamos corporativos de la FED o un banco como JPMorgan/Bank of America).
Supuesto práctico: usamos un valor referencial para poder calcular la tabla de amortización.)*
TRM diaria (histórica): hoja TRM_diaria en tu archivo.
Descargada directamente de la Superintendencia Financiera de Colombia (SFC) o la BVC (Bolsa de Valores de Colombia), que publican la TRM oficial cada día.
Retornos y desviación estándar mensuales:
Calculados a partir de esa serie diaria (usando variaciones logarítmicas o simples, agrupadas por mes).
#Simulación GBM
Parámetros:
μ mensual ≈ 0,003589 (0,3589%) y
σ mensual ≈ 0,036867 (3,6867%)
Derivados de la serie histórica de la TRM (con los cálculos de media y desviación estándar mensuales).
Método: Browniano Geométrico para proyectar escenarios futuros de TRM
#Parte 3. Futuros TRM (BVC)
Subyacente: contrato futuro de TRM listado en la BVC (Bolsa de Valores de Colombia), por ejemplo el TRMM25F (TRM mensual con vencimiento a febrero de 2025).
Tamaño del contrato:
Estándar TRM = 50.000 USD.
Fuente: especificaciones oficiales de productos derivados en la BVC.
Margen inicial (6,3%) y margen de mantenimiento (75% del inicial):
Fuente: manual de la BVC – Derivados de Divisas (condiciones vigentes para futuros TRM).
Precios históricos del futuro (TRMM25F_Futuro(mes)):
Extraídos de información de la BVC (informes de precios de futuros).
se organizó de forma mensual para luego calcular retornos y σ.
Simulación GBM de futuros
Igual que con la TRM spot: partiendo de media y desviación estándar mensuales calculadas con la serie histórica del futuro TRM.
Primer mes (mes 61) se arranca con el valor teórico del futuro:
donde S = TRM spot del mes anterior, r_d = tasa de interés doméstica (Colombia), r_f = tasa de EE. UU., y T = 1/12 (un mes).
#Parte 4. Márgenes y PnL
PnL mensual de futuros:
con los precios simulados 𝐹𝑡
Saldo de márgenes, margin calls:
Fórmulas aplicadas mes a mes usando los parámetros oficiales de la BVC (6,3% inicial, 75% mantenimiento).
#Parte 5. Flujo del crédito vs cobertura
Flujo sin cobertura: cuotas USD (del crédito) × TRM simulada (Hoja Sim. TRM).
Flujo cubierto: cuotas USD × TRM simulada + PnL de futuros (Hoja5).
caja_prom_margen) capturan la
presión de caja del hedge.