El presente informe tiene como objetivo analizar el impacto de la variación cambiaria y el uso de instrumentos derivados en la financiación de la compra de maquinaria amarilla. A partir de la simulación de escenarios con crédito en dólares y su posterior cobertura mediante futuros de divisas, buscamos evaluar cómo las decisiones financieras pueden influir en el costo real de la deuda. El ejercicio nos permitió trabajar con trayectorias de tipo de cambio y construir flujos de caja en distintos horizontes de tiempo, lo que facilita entender la dinámica del riesgo cambiario. Más allá de los cálculos, la intención fue reflexionar como inversionistas sobre la importancia de anticipar los movimientos del mercado y tomar decisiones estratégicas que protejan el patrimonio y la sostenibilidad financiera de un proyecto.
La TRM es el valor de referencia del dólar en Colombia y su precio cambia constantemente debido a diversos factores de la economía. Su volatilidad se explica principalmente por la inflación, las tasas de interés que define el Banco de la República, el comportamiento de las exportaciones e importaciones y los precios de materias primas como el petróleo, que hacen que el dólar suba o baje frente al peso colombiano con frecuencia (Banco de la República, 2025). Según el panel económico del Congreso Empresarial Colombiano, se espera que el dólar se ubique alrededor de $4.050 para 2026, aunque seguirá mostrando cierta volatilidad por el crecimiento económico más bajo de lo esperado, las decisiones de política monetaria y la incertidumbre de los mercados internacionales (República, 2025).
Complementando esta información, Traders Union proyecta que la TRM para finales de 2025 será de COP 4.119,36 (Union, 2025), mientras que el informe de Bancolombia indica que el tipo de cambio USD-COP podría cerrar el año con un promedio de COP 4.237, debido a riesgos fiscales, una posible rebaja en la calificación soberana y la postura de la Reserva Federal de Estados Unidos (Bancolombia, 2025). Sin embargo, factores como la debilidad global del dólar y el apetito de inversionistas extranjeros por activos locales podrían mitigar las presiones cambiarias. En conjunto, estas proyecciones permiten comprender la volatilidad de la TRM y anticipar su posible comportamiento durante el próximo año, considerando tanto factores internos como externos que afectan la oferta y demanda de dólares en Colombia.
Se utilizó la librería cITMre en RStudio para descargar
la Tasa Representativa del Mercado (TRM) de Colombia, correspondiente al
periodo entre el 01/01/2015 y el 01/01/2025. Esta serie histórica de
diez años constituye una base confiable para análisis económicos y
financieros, permitiendo estudiar tendencias, volatilidad y
comportamientos del tipo de cambio COP/USD.
A partir de la serie histórica (2015–2025) se calcularon los retornos mensuales y la desviación estándar. La evidencia muestra que el COP/USD es un activo volátil, lo que confirma la necesidad de incluir instrumentos derivados para estabilizar los pagos futuros.
library(citmre)
library(readxl)
library(dplyr)
library(ggplot2)
library(lubridate)
library(writexl)
# Serie de precios
data <- rmre_data(start_date = "2015-01-01", end_date = "2025-09-15")
plot(data)
# Serie de retornos compuestos continuos
data_log <- rmre_data(start_date = "2015-01-01", end_date = "2025-09-15", log_return = TRUE)
plot(data_log)
meses_año <- 12
mu <- exp(mean(na.omit(data_log)*meses_año))-1
cat("Media Mensual( µ ):", round(mu * 100, 4),"%\n")
## Media Mensual( µ ): 0.2323 %
sigma <- sqrt(var(na.omit(data_log))*meses_año)
cat("Volatilidad Mensual( σ ):",round(sigma * 100, 3),"%\n")
## Volatilidad Mensual( σ ): 2.959 %
# Factores iniciales
meses_año <- 12
s0 <- data[length(data)]
T <- 120
alpha <- 0.05/2
md <- (log(s0)+(mu-(sigma^2/2))*(T/meses_año)) #Efecto deterministico
cat("Efecto deterministico:", round(md, 4),"\n")
## Efecto deterministico: 8.2892
vr <- (sigma^2)*(T/meses_año)
cat("Efecto Aleatorio:",round(vr , 6),"\n") #Efecto Aleatorio
## Efecto Aleatorio: 0.008757
p_n <- qnorm(alpha)
p_p <- qnorm(1-alpha)
v_inferior <- exp(md+p_n*sqrt(vr))
cat("Valor Inferior:",round(v_inferior,2),"\n")
## Valor Inferior: 3313.52
v_superior <- exp(md+p_p*sqrt(vr))
cat("Valor Superior:",round(v_superior,2),"\n")
## Valor Superior: 4781.88
v_esperado <- s0*exp(mu*(T/meses_año))
cat("Valor Esperado:",round(v_esperado,2),"\n")
## Valor Esperado: 3998.02
Con base en un proceso de Movimiento Browniano Geométrico (GBM) se simularon trayectorias cambiarias de la TRM. Para mejorar la precisión de la simulación, se implementó el esquema de Milstein, que añade un término correctivo respecto al método de Euler-Maruyama, reduciendo el error de discretización en la aproximación del proceso estocástico (Kloeden & Platen, 1992). Este enfoque probabilístico refleja escenarios de apreciación, estabilidad y depreciación, lo que permite anticipar cómo las cuotas en COP pueden variar a lo largo del tiempo. Para la ingeniería financiera, estas simulaciones son clave porque permiten cuantificar la sensibilidad de la deuda frente al riesgo cambiario.
# Semilla y configuración de notación científica
set.seed (123)
options (scipen = 999)
# Esquema de Milstein:
dt <- 1/meses_año
meses <- T
Trayectorias <- 500
S <- matrix(0,nrow = meses, ncol = Trayectorias)
S[1,] <- data[length(data)]
for (j in seq(1:Trayectorias)) {
for(i in 2:meses){
Aleatorio_Norm <- rnorm(1,0,1)
dS <- mu*S[i-1,j]*dt + sigma*S[i-1,j]*sqrt(dt)*Aleatorio_Norm
S[i,j] <- S[i-1,j] + dS + (1/2)*(sigma^2)*(S[i-1,j])*((sqrt(dt)*Aleatorio_Norm)^2-dt)
}
}
matplot(S,type = "l")
abline(h=data[length(data)], col="black",lty=3, lwd=3)
q1 = quantile(S[i ,] , 0.975)
q2 = quantile(S[i ,] , 0.025)
plot(density(S[i ,]) , ylab ="", xlab ="",main =" Empirical distribution ", lwd = 3)
abline(h = NULL , v =q1 , col = "blue", lwd = 2)
abline(h = NULL , v =q2 , col = "green", lwd = 2)
abline(h = NULL , v =s0 , col = "red", lwd = 2)
abline(h = NULL , v =v_esperado , col = "gray", lwd = 2)
El crédito se estructuró bajo el sistema francés con una tasa de 6,45% APR (Tasa de Porcentaje Anual) ofrecida por United Prairie, entidad financiera estadounidense especializada en préstamos para maquinaria (Bank, 2025). A un plazo de 10 años y un pago inicial del 10%. Esta condición muestra un costo financiero competitivo frente a alternativas locales, donde las tasas efectivas superan el 32% anual. Desde un criterio financiero, el acceso al crédito estadounidense representa un ahorro significativo en costo de capital.
# Parámetros del crédito
credito_cop <- 500000000
credito_usd <- credito_cop/s0
pago_inicial <- 0.10 * credito_usd
monto_credito <- credito_usd - pago_inicial
tasa_anual <- 0.0645 # Efectiva anual
tasa_mensual <- tasa_anual / 12
n_cuotas <- 120 # meses
# Cálculo de la cuota mensual (sistema francés)
cuota_mensual_usd <- monto_credito * (tasa_mensual * (1 + tasa_mensual)^n_cuotas) / ((1 + tasa_mensual)^n_cuotas - 1)
# Inicializar variables
saldo <- monto_credito
flujo <- data.frame(Mes = integer(), Cuota = numeric(), Interes = numeric(), AbonoCapital = numeric(), SaldoRestante = numeric())
# Generar tabla de amortización
for (mes in 1:n_cuotas) {
interes <- saldo * tasa_mensual
abono_capital <- cuota_mensual_usd - interes
saldo <- saldo - abono_capital
flujo <- rbind(flujo, data.frame(
Mes = mes,
Cuota = round(cuota_mensual_usd, 2),
Interes = round(interes, 2),
AbonoCapital = round(abono_capital, 2),
SaldoRestante = round(saldo, 2)
))
}
# Renombrar columnas de la tabla
colnames(flujo) <- c("Mes", "Cuota (USD)", "Interés (USD)", "Abono a Capital (USD)", "Saldo Restante (USD)")
# Mostrar últimos 6 meses
tail(flujo,6)
## Mes Cuota (USD) Interés (USD) Abono a Capital (USD)
## 2025-09-15114 115 1305.15 41.31 1263.84
## 2025-09-15115 116 1305.15 34.52 1270.63
## 2025-09-15116 117 1305.15 27.69 1277.46
## 2025-09-15117 118 1305.15 20.82 1284.33
## 2025-09-15118 119 1305.15 13.92 1291.23
## 2025-09-15119 120 1305.15 6.98 1298.17
## Saldo Restante (USD)
## 2025-09-15114 6421.81
## 2025-09-15115 5151.18
## 2025-09-15116 3873.72
## 2025-09-15117 2589.40
## 2025-09-15118 1298.17
## 2025-09-15119 0.00
Al convertir las cuotas a pesos colombianos según las simulaciones de la TRM, observamos que el costo total está directamente ligado a la volatilidad cambiaria. En un escenario de fuerte depreciación, el valor en pesos aumenta considerablemente, mientras que, con una apreciación, el crédito resulta más barato. Esto nos confirma que la variación del dólar es el principal riesgo de la operación.
# Trayectoria Mas Alta: la que termina con el valor más alto en el mes 120
indice_max <- which.max(S[120, ])
trayectoria_alta <- S[, indice_max]
# Inicializar variables
saldo <- monto_credito
flujo_cop_Op <- data.frame(Mes = integer(), Cuota = numeric(), Interes = numeric(), AbonoCapital = numeric(), SaldoRestante = numeric())
# Generar tabla de amortización en COP
for (mes in 1:n_cuotas) {
trm_mes_Op <- S[mes, indice_max]
interes_usd <- saldo * tasa_mensual
abono_capital_usd <- cuota_mensual_usd - interes_usd
saldo <- saldo - abono_capital_usd
# Convertir cada componente a COP
cuota_cop <- cuota_mensual_usd * trm_mes_Op
interes_cop <- interes_usd * trm_mes_Op
abono_capital_cop <- abono_capital_usd * trm_mes_Op
saldo_cop <- saldo * trm_mes_Op
flujo_cop_Op <- rbind(flujo_cop_Op, data.frame(
Mes = mes,
TRM = round(trm_mes_Op, 2),
Cuota = round(cuota_cop, 2),
Interes = round(interes_cop, 2),
AbonoCapital = round(abono_capital_cop, 2),
SaldoRestante = round(saldo_cop, 2)
))
}
# Renombrar columnas para claridad
colnames(flujo_cop_Op) <- c("Mes", "Valor TRM Mas Alta","Cuota (COP)", "Interés (COP)", "Abono a Capital (COP)", "Saldo Restante (COP)")
# Mostrar los últimos meses
tail(flujo_cop_Op, 6)
## Mes Valor TRM Mas Alta Cuota (COP) Interés (COP)
## 2025-09-15114 115 5042.35 6581006 208301.27
## 2025-09-15115 116 5094.01 6648431 175831.16
## 2025-09-15116 117 5113.01 6673225 141566.94
## 2025-09-15117 118 5151.01 6722822 107250.50
## 2025-09-15118 119 5120.57 6683099 71268.20
## 2025-09-15119 120 5117.38 6678928 35707.31
## Abono a Capital (COP) Saldo Restante (COP)
## 2025-09-15114 6372705 32381020
## 2025-09-15115 6472599 26240175
## 2025-09-15116 6531658 19806376
## 2025-09-15117 6615571 13338010
## 2025-09-15118 6611831 6647370
## 2025-09-15119 6643220 0
En la trayectoria alta proyectamos una depreciación considerable del peso, con una TRM que llega a niveles superiores a 5.700 pesos por dólar. Este comportamiento coincide con escenarios de riesgo cambiario descritos por el Banco de la República (Banco de la República, 2025). Para nosotros, como inversionistas, esto se traduce en cuotas mucho más costosas en pesos, lo cual incrementa el valor total de la deuda. Aunque inicialmente el crédito en dólares parecía atractivo por su baja tasa frente a las tasas locales (Social, 2025), la devaluación hace que la carga financiera aumente significativamente.
# Trayectoria intermedia: promedio de todas las trayectorias
trayectoria_promedio <- rowMeans(S)
# Inicializar variables
saldo <- monto_credito
flujo_cop_M <- data.frame(Mes = integer(), Cuota = numeric(), Interes = numeric(), AbonoCapital = numeric(), SaldoRestante = numeric())
# Generar tabla de amortización en COP
for (mes in 1:n_cuotas) {
trm_mes_M <- trayectoria_promedio[mes]
interes_usd <- saldo * tasa_mensual
abono_capital_usd <- cuota_mensual_usd - interes_usd
saldo <- saldo - abono_capital_usd
# Convertir cada componente a COP
cuota_cop <- cuota_mensual_usd * trm_mes_M
interes_cop <- interes_usd * trm_mes_M
abono_capital_cop <- abono_capital_usd * trm_mes_M
saldo_cop <- saldo * trm_mes_M
flujo_cop_M <- rbind(flujo_cop_M, data.frame(
Mes = mes,
TRM = round(trm_mes_M, 2),
Cuota = round(cuota_cop, 2),
Interes = round(interes_cop, 2),
AbonoCapital = round(abono_capital_cop, 2),
SaldoRestante = round(saldo_cop, 2)
))
}
# Renombrar columnas para claridad
colnames(flujo_cop_M) <- c("Mes", "Valor TRM Intermedia","Cuota (COP)", "Interés (COP)", "Abono a Capital (COP)", "Saldo Restante (COP)")
# Mostrar los últimos meses
tail(flujo_cop_M, 6)
## Mes Valor TRM Intermedia Cuota (COP) Interés (COP)
## 2025-09-15114 115 3989.02 5206254 164787.78
## 2025-09-15115 116 3986.55 5203039 137604.87
## 2025-09-15116 117 3987.58 5204380 110406.60
## 2025-09-15117 118 3990.10 5207667 83078.94
## 2025-09-15118 119 3990.76 5208535 55543.54
## 2025-09-15119 120 3995.52 5214739 27879.37
## Abono a Capital (COP) Saldo Restante (COP)
## 2025-09-15114 5041466 25616725
## 2025-09-15115 5065434 20535471
## 2025-09-15116 5093973 15446789
## 2025-09-15117 5124588 10331958
## 2025-09-15118 5152992 5180689
## 2025-09-15119 5186859 0
En la trayectoria intermedia se mantiene un punto medio: la TRM promedia alrededor de 4.472 pesos. Aquí confirmamos que el costo del crédito en pesos es intermedio, sin llegar a los extremos de los otros escenarios. Este caso refleja con mayor realismo la incertidumbre y la volatilidad del tipo de cambio, como lo señala Bancolombia en su informe de perspectivas económicas (Bancolombia, 2025). Para nosotros, esta situación demuestra la necesidad de considerar coberturas cambiarias que reduzcan la exposición y permitan proyectar pagos más estables en el tiempo.
# Trayectoria Mas Baja: la que termina con el valor más bajo en el mes 120
indice_min <- which.min(S[120, ])
trayectoria_baja <- S[, indice_min]
# Inicializar variables
saldo <- monto_credito
flujo_cop_P <- data.frame(Mes = integer(), Cuota = numeric(), Interes = numeric(), AbonoCapital = numeric(), SaldoRestante = numeric())
# Generar tabla de amortización en COP
for (mes in 1:n_cuotas) {
trm_mes_P <- S[mes, indice_min]
interes_usd <- saldo * tasa_mensual
abono_capital_usd <- cuota_mensual_usd - interes_usd
saldo <- saldo - abono_capital_usd
# Convertir cada componente a COP
cuota_cop <- cuota_mensual_usd * trm_mes_P
interes_cop <- interes_usd * trm_mes_P
abono_capital_cop <- abono_capital_usd * trm_mes_P
saldo_cop <- saldo * trm_mes_P
flujo_cop_P <- rbind(flujo_cop_P, data.frame(
Mes = mes,
TRM = round(trm_mes_P, 2),
Cuota = round(cuota_cop, 2),
Interes = round(interes_cop, 2),
AbonoCapital = round(abono_capital_cop, 2),
SaldoRestante = round(saldo_cop, 2)
))
}
# Renombrar columnas para claridad
colnames(flujo_cop_P) <- c("Mes", "Valor TRM Mas Baja","Cuota (COP)", "Interés (COP)", "Abono a Capital (COP)", "Saldo Restante (COP)")
# Mostrar los últimos meses
tail(flujo_cop_P, 6)
## Mes Valor TRM Mas Baja Cuota (COP) Interés (COP)
## 2025-09-15114 115 3021.50 3943502 124819.29
## 2025-09-15115 116 2998.98 3914105 103516.40
## 2025-09-15116 117 3000.96 3916689 83089.32
## 2025-09-15117 118 3004.23 3920963 62551.90
## 2025-09-15118 119 3042.47 3970878 42345.22
## 2025-09-15119 120 3030.41 3955130 21145.17
## Abono a Capital (COP) Saldo Restante (COP)
## 2025-09-15114 3818683 19403510
## 2025-09-15115 3810588 15448276
## 2025-09-15116 3833600 11624877
## 2025-09-15117 3858411 7779151
## 2025-09-15118 3928532 3949648
## 2025-09-15119 3933985 0
# Visualización de las trayectorias escogidas
matplot(cbind(trayectoria_baja, trayectoria_promedio, trayectoria_alta), type = "l",
col = c("red", "blue", "green"), lty = 1,
xlab = "Mes", ylab = "TRM simulada",
main = "Trayectorias TRM: Mas Baja, Intermedia, Mas Alta")
write_xlsx(flujo, "Tabla de amortizacion USD.xlsx")
write_xlsx(flujo_cop_M, "Tabla de amortizacion COP Trayectoria Intermedia.xlsx")
write_xlsx(flujo_cop_Op, "Tabla de amortizacion COP Trayectoria Alta.xlsx")
write_xlsx(flujo_cop_P, "Tabla de amortizacion COP Trayectoria baja.xlsx")
En el escenario de trayectoria baja, observamos una apreciación del peso colombiano, con una TRM que cae hasta cerca de 3.379 pesos. En este caso, nuestras cuotas en pesos se reducen y el crédito se vuelve mucho más manejable. Este comportamiento coincide con algunas proyecciones de analistas que prevén escenarios de debilidad del dólar a nivel global (Union, 2025). Para nosotros, como inversionistas, sería el escenario más favorable, pues pagaríamos menos intereses y abonaríamos más rápido al capital. Sin embargo, entendemos que este contexto es poco común en el largo plazo y que depende de múltiples factores externos, como el precio del petróleo o la política monetaria en Estados Unidos (República, 2025).
Al comparar las tres trayectorias confirmamos que el factor determinante del costo real de la deuda es la TRM. Si el dólar sube, nuestras cuotas en pesos aumentan considerablemente; si baja, el crédito resulta mucho más barato. Este análisis nos enseña que, como inversionistas, no podemos depender únicamente de las proyecciones cambiarias, sino que debemos complementar el crédito con instrumentos de cobertura como futuros o forwards (Valores de Colombia, 2025). De esta forma, logramos proteger la inversión en maquinaria y reducir el impacto de la volatilidad cambiaria sobre nuestra capacidad de pago.
En nuestro ejercicio tomamos como referencia el contrato de futuros de divisas TRXV25F, listado en la Bolsa de Valores de Colombia (BVC). Este contrato corresponde a un tamaño de 1.000 USD, lo que resulta apropiado para simular la cobertura de la deuda en dólares que decidimos asumir como inversionistas. Para la operación, definimos un margen inicial del 6,3%, equivalente a 5.481 USD, cifra que guarda una relación cercana con el precio del contrato y que nos permite dimensionar el nivel de apalancamiento y riesgo inherente a la posición. A partir del cálculo de los retornos mensuales y su desviación estándar, observamos una elevada volatilidad, coherente con la naturaleza especulativa y de cobertura que caracteriza a los derivados financieros transados en la BVC (Valores de Colombia, 2025).
# Se cargan los datos históricos del futuro y se calculan sus rendimientos
trx <- read_xlsx(file.choose())
trx["Rentorno"] <- c(NA,diff(log(trx$`Precio cierre`)))
meses_año <- 12
mu_f <- exp(mean(na.omit(trx$Rentorno)*meses_año))-1
cat("Media Mensual( µ ):", round(mu_f * 100, 3),"%\n")
## Media Mensual( µ ): -9.67 %
sigma_f <- sqrt(var(na.omit(trx$Rentorno))*meses_año)
cat("Volatilidad Mensual( σ ):",round(sigma_f * 100, 3),"%\n")
## Volatilidad Mensual( σ ): 8.436 %
# Semilla y configuración de notación científica
set.seed (123)
options (scipen = 999)
# Parámetros de tasas
S0 <- trx$`Precio cierre`[length(trx$`Precio cierre`)] # Spot futuro actual
i_nacional <- 0.3485
i_extranjero <- 0.0645
t <- 1
# Cálculo del valor inicial del futuro
F0 <- S0 * ((1 + i_nacional) / (1 + i_extranjero))^t
cat("Valor inicial del futuro de TRM:", round(F0, 2), "\n")
## Valor inicial del futuro de TRM: 4952.22
# Esquema de Milstein:
dt <- 1/meses_año
meses <- T-72 # Del año 6 en adelante (48 meses)
Trayectorias <- 500
S_f <- matrix(0,nrow = meses, ncol = Trayectorias)
S_f[1,] <- F0
for (j in seq(1:Trayectorias)) {
for(i in 2:meses){
Aleatorio_Norm <- rnorm(1,0,1)
dS_f <- mu_f*S_f[i-1,j]*dt + sigma_f*S_f[i-1,j]*sqrt(dt)*Aleatorio_Norm
S_f[i,j] <- S_f[i-1,j] + dS_f + (1/2)*(sigma_f^2)*(S_f[i-1,j])*((sqrt(dt)*Aleatorio_Norm)^2-dt)
}
}
matplot(S_f,type = "l")
q1 = quantile(S_f[i ,] , 0.975)
q2 = quantile(S_f[i ,] , 0.025)
plot(density(S_f[i ,]) , ylab ="", xlab ="",main =" Empirical distribution ", lwd = 3)
abline(h = NULL , v =q1 , col = "blue", lwd = 2)
abline(h = NULL , v =q2 , col = "green", lwd = 2)
abline(h = NULL , v =S0 , col = "red", lwd = 2)
abline(h = NULL , v =F0 , col = "gray", lwd = 2)
Con la fórmula de futuros de TRM como precio inicial, se simuló el comportamiento de los contratos entre los años 6 y 10. El ejercicio incluyó la aplicación de rollover en cada vencimiento, replicando condiciones de mercado. Desde la perspectiva financiera, la simulación permitió modelar cómo la cobertura atenúa los incrementos de la deuda cuando el dólar se deprecia.
Para el cálculo del precio inicial del futuro y la simulación planteada se tomó como referencia la tasa de crédito del 6,45% APR (Tasa de Porcentaje Anual) ofrecida por United Prairie, entidad financiera estadounidense especializada en préstamos para maquinaria (Bank, 2025). Esta tasa aplica a plazos de 6 a 7 años y montos superiores a USD 200,000, lo cual se ajusta al requerimiento de 500 millones de pesos, y está dirigida a clientes con puntaje crediticio superior a 700, perfil que asumimos como inversionistas con historial sólido.
En cuanto a la tasa local, revisamos las opciones de crédito productivo del Banco Caja Social y encontramos que, para montos como el nuestro, las tasas efectivas anuales se ubican entre el 32% y el 37%. Para el ejercicio tomamos el promedio de ese rango, obteniendo una tasa de 34,85% Efectiva Anual, que refleja lo que realmente costaría un crédito de este tipo en el país (Social, 2025). Al comparar ambas alternativas, la diferencia es clara: en Estados Unidos el crédito resulta mucho más barato, mientras que en Colombia el financiamiento es más costoso. Esta comparación permitió diferenciar cómo cambia el análisis dependiendo del origen del crédito.
# Parametros del contrato
Qf <- 1000
v_cont <- Qf * S0
inv <- (monto_credito*S0)*0.75
N_optimo <- round(inv/v_cont,0)+1
exp_total <- (v_cont * N_optimo)
# Margen inicial y de mantenimiento:
vgi <- 0.063
vgm <- 0.5
m_inicial <- exp_total * vgi
m_mant <- m_inicial * vgm
# Apalancamiento:
Ap <- exp_total/m_inicial
De los 500.000.000 COP necesarios para financiar la compra de la maquinaria amarilla, realizamos un pago inicial del 10% equivalente a 50.000.000 COP (128.000 USD). Con ello, quedó un saldo pendiente de 450.000.000 COP (115.200 USD). Sobre esta deuda decidimos cubrir el 75% a través de futuros, lo que corresponde a 337.760.929 COP (86.400 USD). Ademas tomamos el precio inicial del futuro de 3.909,26 COP. Al dividir el monto a cubrir entre este precio, obtuvimos 86,4 contratos; al redondear, resultaron 86 contratos, que no alcanzaban a cubrir completamente el 75%. Por esta razón, adicionamos un contrato más, quedando en 87 contratos en total, con lo cual logramos cubrir de manera completa el porcentaje definido de la deuda.
La exposición total fue de 340.105.620 COP, con una garantía inicial del 6,3% según la BVC (21.426.654 COP) y un margen de mantenimiento del 50% de ese valor, es decir, 10.713.327 COP. La operación de 87 contratos implicó un margen inicial del 6,3% y un apalancamiento cercano a 16 veces. Este nivel de exposición refleja el poder de los derivados para cubrir riesgos con bajo capital inicial, pero también resalta el riesgo de liquidez si se presentan llamadas de margen (margin calls).
En conclusión, con los 87 contratos aseguramos la cobertura del 75% y protegimos los pagos frente a la variación del dólar, evitando que el crédito se encarezca más de lo previsto si la TRM aumenta.
# Trayectoria Mas Alta: la que termina con el valor más alto en el mes 48
indice_max <- which.max(S_f[48, ])
S_serie_A <- as.numeric(S_f[, indice_max]) # vector de longitud meses
meses <- length(S_serie_A)
# Fechas mensuales
fecha_inicio <- Sys.Date() - (5)
fechas <- seq.Date(from = fecha_inicio, by = "month", length.out = meses)
# Inicializaciones
DailyPnL <- numeric(meses)
MarginBalance <- numeric(meses)
MarginCall <- numeric(meses)
CumMarginCalls <- numeric(meses)
CumPnL <- numeric(meses)
Position <- character(meses)
RolloverFlag <- rep(FALSE, meses)
Signal_method <- rep(NA_character_, meses)
Signal_value <- rep(NA_real_, meses)
# Posición inicial ("Largo" o "Corto")
pos <- "Largo"
Position[1] <- pos
# Inicializamos margen con el inicial
MarginBalance[1] <- m_inicial
MarginCall[1] <- 0
CumMarginCalls[1] <- 0
DailyPnL[1] <- 0
CumPnL[1] <- 0
# Parámetros de la regla simple
period_roll <- 3 # rollover trimestral (cada 3 meses)
threshold <- 0.0025 # umbral mínimo (0.25%) para evitar cambios por ruido.
threshold_log <- log(1 + threshold)
# Loop mensual
for (t in 2:meses) {
# signo para PnL según posición: 1 para largo, -1 para corto
sign_pos <- ifelse(pos == "Largo", 1, -1)
# P&L mensual:
delta_price <- S_serie_A[t] - S_serie_A[t - 1]
pnl_t <- (delta_price * sign_pos) * Qf * N_optimo
DailyPnL[t] <- pnl_t
CumPnL[t] <- CumPnL[t - 1] + pnl_t
# Actualizamos margen (mark-to-market)
MarginBalance[t] <- MarginBalance[t - 1] + pnl_t
MarginCall[t] <- 0
CumMarginCalls[t] <- CumMarginCalls[t - 1]
# Si cae por debajo del mantenimiento -> margin call
if (!is.na(MarginBalance[t]) && MarginBalance[t] < m_mant) {
faltante <- m_inicial - MarginBalance[t]
MarginCall[t] <- faltante
MarginBalance[t] <- MarginBalance[t] + faltante
CumMarginCalls[t] <- CumMarginCalls[t] + faltante
}
# Rollover trimestral
if ((t %% period_roll) == 0 && t >= period_roll) {
RolloverFlag[t] <- TRUE
# retorno acumulado log del trimestre (aditivo y exacto)
ret_quarter_log <- sum(log(S_serie_A[(t - period_roll + 1):t])
- log(S_serie_A[(t - period_roll):(t-1)]))
# decisión por signo usando log-returns y umbral log
if (!is.na(ret_quarter_log)) {
if (ret_quarter_log > threshold_log) {
newpos <- "Largo"
Signal_method[t] <- "LogSignQuarter"
Signal_value[t] <- ret_quarter_log
} else if (ret_quarter_log < -threshold_log) {
newpos <- "Corto"
Signal_method[t] <- "LogSignQuarter"
Signal_value[t] <- ret_quarter_log
} else {
newpos <- pos # señal débil -> sin cambio
Signal_method[t] <- "NoChange_WeakSignal_log"
Signal_value[t] <- ret_quarter_log
}
# Aplicar cambio de posición si hay cambio efectivo
if (newpos != pos) {
pos <- newpos
}
} else {
Signal_method[t] <- "InsufficientHistory"
Signal_value[t] <- NA
}
}
Position[t] <- pos
}
# Construcción data.frame final
resultados_SA <- data.frame(
Date = fechas,
Price = S_serie_A,
Position = Position,
Rollover = RolloverFlag,
Signal_method = Signal_method,
Signal_value = Signal_value,
DailyPnL = DailyPnL,
CumPnL = CumPnL,
MarginBalance = MarginBalance,
MarginCall = MarginCall,
CumMarginCalls = CumMarginCalls,
stringsAsFactors = FALSE
)
# Ordenar columnas y mostrar
resultados_SA <- resultados_SA %>%
select(Date, Price, Position, Rollover, Signal_method, Signal_value, DailyPnL, CumPnL, MarginBalance, MarginCall, CumMarginCalls)
print(tail(resultados_SA, 15))
## Date Price Position Rollover Signal_method Signal_value
## 34 2028-06-16 4369.501 Corto FALSE <NA> NA
## 35 2028-07-16 4324.995 Corto FALSE <NA> NA
## 36 2028-08-16 4397.533 Corto TRUE LogSignQuarter -0.004369091
## 37 2028-09-16 4437.658 Corto FALSE <NA> NA
## 38 2028-10-16 4508.886 Corto FALSE <NA> NA
## 39 2028-11-16 4740.452 Largo TRUE LogSignQuarter 0.075088822
## 40 2028-12-16 4778.167 Largo FALSE <NA> NA
## 41 2029-01-16 4762.439 Largo FALSE <NA> NA
## 42 2029-02-16 4473.177 Corto TRUE LogSignQuarter -0.058033640
## 43 2029-03-16 4738.623 Corto FALSE <NA> NA
## 44 2029-04-16 4643.664 Corto FALSE <NA> NA
## 45 2029-05-16 4881.171 Largo TRUE LogSignQuarter 0.087286366
## 46 2029-06-16 4885.127 Largo FALSE <NA> NA
## 47 2029-07-16 5030.756 Largo FALSE <NA> NA
## 48 2029-08-16 4975.304 Largo TRUE LogSignQuarter 0.019101327
## DailyPnL CumPnL MarginBalance MarginCall CumMarginCalls
## 34 4113970.5 19475669 62783957 0 21881634
## 35 3872023.9 23347692 66655981 0 21881634
## 36 -6310787.2 17036905 60345193 0 21881634
## 37 -3490868.5 13546037 56854325 0 21881634
## 38 -6196872.9 7349164 50657452 0 21881634
## 39 -20146224.3 -12797060 30511228 0 21881634
## 40 3281250.0 -9515810 33792478 0 21881634
## 41 -1368338.2 -10884149 32424139 0 21881634
## 42 -25165853.9 -36050002 21426654 14168368 36050002
## 43 -23093828.0 -59143830 21426654 23093828 59143830
## 44 8261468.9 -50882362 29688123 0 59143830
## 45 -20663163.6 -71545525 21426654 12401695 71545525
## 46 344127.9 -71201397 21770782 0 71545525
## 47 12669755.4 -58531642 34440537 0 71545525
## 48 -4824310.5 -63355952 29616227 0 71545525
print(summary(resultados_SA %>% select(DailyPnL, CumPnL, MarginBalance, MarginCall, CumMarginCalls)))
## DailyPnL CumPnL MarginBalance MarginCall
## Min. :-25165854 Min. :-71545525 Min. :11966265 Min. : 0
## 1st Qu.: -6225351 1st Qu.:-15218118 1st Qu.:21679155 1st Qu.: 0
## Median : -278213 Median : -379255 Median :33108309 Median : 0
## Mean : -1319916 Mean : -3365452 Mean :42287558 Mean : 1490532
## 3rd Qu.: 4248077 3rd Qu.: 18367342 3rd Qu.:61675630 3rd Qu.: 0
## Max. : 20832106 Max. : 41591483 Max. :84899771 Max. :23093828
## CumMarginCalls
## Min. : 0
## 1st Qu.:10965955
## Median :21881634
## Mean :24226356
## 3rd Qu.:21881634
## Max. :71545525
# Traza representativa: promedio entre trayectorias
S_serie_M <- as.numeric(rowMeans(S_f)) # vector de longitud meses
meses <- length(S_serie_M)
# Fechas mensuales
fecha_inicio <- Sys.Date() - (5)
fechas <- seq.Date(from = fecha_inicio, by = "month", length.out = meses)
# Inicializaciones
DailyPnL <- numeric(meses)
MarginBalance <- numeric(meses)
MarginCall <- numeric(meses)
CumMarginCalls <- numeric(meses)
CumPnL <- numeric(meses)
Position <- character(meses)
RolloverFlag <- rep(FALSE, meses)
Signal_method <- rep(NA_character_, meses)
Signal_value <- rep(NA_real_, meses)
# Posición inicial ("Largo" o "Corto")
pos <- "Corto"
Position[1] <- pos
# Inicializamos margen con el inicial
MarginBalance[1] <- m_inicial
MarginCall[1] <- 0
CumMarginCalls[1] <- 0
DailyPnL[1] <- 0
CumPnL[1] <- 0
# Parámetros de la regla simple
period_roll <- 3 # rollover trimestral (cada 3 meses)
threshold <- 0.0025 # umbral mínimo (0.25%) para evitar cambios por ruido.
threshold_log <- log(1 + threshold)
# Loop mensual
for (t in 2:meses) {
# signo para PnL según posición: 1 para largo, -1 para corto
sign_pos <- ifelse(pos == "Largo", 1, -1)
# P&L mensual:
delta_price <- S_serie_M[t] - S_serie_M[t - 1]
pnl_t <- (delta_price * sign_pos) * Qf * N_optimo
DailyPnL[t] <- pnl_t
CumPnL[t] <- CumPnL[t - 1] + pnl_t
# Actualizamos margen (mark-to-market)
MarginBalance[t] <- MarginBalance[t - 1] + pnl_t
MarginCall[t] <- 0
CumMarginCalls[t] <- CumMarginCalls[t - 1]
# Si cae por debajo del mantenimiento -> margin call
if (!is.na(MarginBalance[t]) && MarginBalance[t] < m_mant) {
faltante <- m_inicial - MarginBalance[t]
MarginCall[t] <- faltante
MarginBalance[t] <- MarginBalance[t] + faltante
CumMarginCalls[t] <- CumMarginCalls[t] + faltante
}
# Rollover trimestral
if ((t %% period_roll) == 0 && t >= period_roll) {
RolloverFlag[t] <- TRUE
# retorno acumulado log del trimestre (aditivo y exacto)
ret_quarter_log <- sum(log(S_serie_M[(t - period_roll + 1):t])
- log(S_serie_M[(t - period_roll):(t-1)]))
# decisión por signo usando log-returns y umbral log
if (!is.na(ret_quarter_log)) {
if (ret_quarter_log > threshold_log) {
newpos <- "Largo"
Signal_method[t] <- "LogSignQuarter"
Signal_value[t] <- ret_quarter_log
} else if (ret_quarter_log < -threshold_log) {
newpos <- "Corto"
Signal_method[t] <- "LogSignQuarter"
Signal_value[t] <- ret_quarter_log
} else {
newpos <- pos # señal débil -> sin cambio
Signal_method[t] <- "NoChange_WeakSignal_log"
Signal_value[t] <- ret_quarter_log
}
# Aplicar cambio de posición si hay cambio efectivo
if (newpos != pos) {
pos <- newpos
}
} else {
Signal_method[t] <- "InsufficientHistory"
Signal_value[t] <- NA
}
}
Position[t] <- pos
}
# Construcción data.frame final
resultados_SM <- data.frame(
Date = fechas,
Price = S_serie_M,
Position = Position,
Rollover = RolloverFlag,
Signal_method = Signal_method,
Signal_value = Signal_value,
DailyPnL = DailyPnL,
CumPnL = CumPnL,
MarginBalance = MarginBalance,
MarginCall = MarginCall,
CumMarginCalls = CumMarginCalls,
stringsAsFactors = FALSE
)
# Ordenar columnas y mostrar
resultados_SM <- resultados_SM %>%
select(Date, Price, Position, Rollover, Signal_method, Signal_value, DailyPnL, CumPnL, MarginBalance, MarginCall, CumMarginCalls)
print(tail(resultados_SM, 15))
## Date Price Position Rollover Signal_method Signal_value DailyPnL
## 34 2028-06-16 3769.188 Corto FALSE <NA> NA 2614108
## 35 2028-07-16 3738.049 Corto FALSE <NA> NA 2709101
## 36 2028-08-16 3706.208 Corto TRUE LogSignQuarter -0.02479077 2770235
## 37 2028-09-16 3675.368 Corto FALSE <NA> NA 2683081
## 38 2028-10-16 3644.909 Corto FALSE <NA> NA 2649883
## 39 2028-11-16 3610.508 Corto TRUE LogSignQuarter -0.02616060 2992881
## 40 2028-12-16 3579.877 Corto FALSE <NA> NA 2664886
## 41 2029-01-16 3552.301 Corto FALSE <NA> NA 2399141
## 42 2029-02-16 3524.073 Corto TRUE LogSignQuarter -0.02423123 2455871
## 43 2029-03-16 3486.811 Corto FALSE <NA> NA 3241784
## 44 2029-04-16 3454.647 Corto FALSE <NA> NA 2798207
## 45 2029-05-16 3434.519 Corto TRUE LogSignQuarter -0.02574048 1751191
## 46 2029-06-16 3407.843 Corto FALSE <NA> NA 2320788
## 47 2029-07-16 3381.298 Corto FALSE <NA> NA 2309400
## 48 2029-08-16 3355.273 Corto TRUE LogSignQuarter -0.02334365 2264185
## CumPnL MarginBalance MarginCall CumMarginCalls
## 34 102923655 124350309 0 0
## 35 105632756 127059410 0 0
## 36 108402991 129829645 0 0
## 37 111086072 132512726 0 0
## 38 113735955 135162609 0 0
## 39 116728835 138155489 0 0
## 40 119393722 140820376 0 0
## 41 121792863 143219517 0 0
## 42 124248734 145675388 0 0
## 43 127490517 148917171 0 0
## 44 130288725 151715379 0 0
## 45 132039915 153466569 0 0
## 46 134360703 155787357 0 0
## 47 136670103 158096757 0 0
## 48 138934288 160360942 0 0
print(summary(resultados_SM %>% select(DailyPnL, CumPnL, MarginBalance, MarginCall, CumMarginCalls)))
## DailyPnL CumPnL MarginBalance MarginCall
## Min. : 0 Min. : 0 Min. : 21426654 Min. :0
## 1st Qu.:2618662 1st Qu.: 41047931 1st Qu.: 62474585 1st Qu.:0
## Median :2921839 Median : 77282166 Median : 98708820 Median :0
## Mean :2894464 Mean : 74535931 Mean : 95962585 Mean :0
## 3rd Qu.:3264289 3rd Qu.:109073761 3rd Qu.:130500415 3rd Qu.:0
## Max. :4122261 Max. :138934288 Max. :160360942 Max. :0
## CumMarginCalls
## Min. :0
## 1st Qu.:0
## Median :0
## Mean :0
## 3rd Qu.:0
## Max. :0
# Trayectoria Mas Baja: la que termina con el valor más bajo en el mes 48
indice_min <- which.min(S_f[48, ])
S_serie_B <- as.numeric(S_f[, indice_min]) # vector de longitud meses
meses <- length(S_serie_B)
# Fechas mensuales
fecha_inicio <- Sys.Date() - (5)
fechas <- seq.Date(from = fecha_inicio, by = "month", length.out = meses)
# Inicializaciones
DailyPnL <- numeric(meses)
MarginBalance <- numeric(meses)
MarginCall <- numeric(meses)
CumMarginCalls <- numeric(meses)
CumPnL <- numeric(meses)
Position <- character(meses)
RolloverFlag <- rep(FALSE, meses)
Signal_method <- rep(NA_character_, meses)
Signal_value <- rep(NA_real_, meses)
# Posición inicial ("Largo" o "Corto")
pos <- "Corto"
Position[1] <- pos
# Inicializamos margen con el inicial
MarginBalance[1] <- m_inicial
MarginCall[1] <- 0
CumMarginCalls[1] <- 0
DailyPnL[1] <- 0
CumPnL[1] <- 0
# Parámetros de la regla simple
period_roll <- 3 # rollover trimestral (cada 3 meses)
threshold <- 0.0025 # umbral mínimo (0.25%) para evitar cambios por ruido.
threshold_log <- log(1 + threshold)
# Loop mensual
for (t in 2:meses) {
# signo para PnL según posición: 1 para largo, -1 para corto
sign_pos <- ifelse(pos == "Largo", 1, -1)
# P&L mensual:
delta_price <- S_serie_B[t] - S_serie_B[t - 1]
pnl_t <- (delta_price * sign_pos) * Qf * N_optimo
DailyPnL[t] <- pnl_t
CumPnL[t] <- CumPnL[t - 1] + pnl_t
# Actualizamos margen (mark-to-market)
MarginBalance[t] <- MarginBalance[t - 1] + pnl_t
MarginCall[t] <- 0
CumMarginCalls[t] <- CumMarginCalls[t - 1]
# Si cae por debajo del mantenimiento -> margin call
if (!is.na(MarginBalance[t]) && MarginBalance[t] < m_mant) {
faltante <- m_inicial - MarginBalance[t]
MarginCall[t] <- faltante
MarginBalance[t] <- MarginBalance[t] + faltante
CumMarginCalls[t] <- CumMarginCalls[t] + faltante
}
# Rollover trimestral
if ((t %% period_roll) == 0 && t >= period_roll) {
RolloverFlag[t] <- TRUE
# retorno acumulado log del trimestre (aditivo y exacto)
ret_quarter_log <- sum(log(S_serie_B[(t - period_roll + 1):t])
- log(S_serie_B[(t - period_roll):(t-1)]))
# decisión por signo usando log-returns y umbral log
if (!is.na(ret_quarter_log)) {
if (ret_quarter_log > threshold_log) {
newpos <- "Largo"
Signal_method[t] <- "LogSignQuarter"
Signal_value[t] <- ret_quarter_log
} else if (ret_quarter_log < -threshold_log) {
newpos <- "Corto"
Signal_method[t] <- "LogSignQuarter"
Signal_value[t] <- ret_quarter_log
} else {
newpos <- pos # señal débil -> sin cambio
Signal_method[t] <- "NoChange_WeakSignal_log"
Signal_value[t] <- ret_quarter_log
}
# Aplicar cambio de posición si hay cambio efectivo
if (newpos != pos) {
pos <- newpos
}
} else {
Signal_method[t] <- "InsufficientHistory"
Signal_value[t] <- NA
}
}
Position[t] <- pos
}
# Construcción data.frame final
resultados_SB <- data.frame(
Date = fechas,
Price = S_serie_B,
Position = Position,
Rollover = RolloverFlag,
Signal_method = Signal_method,
Signal_value = Signal_value,
DailyPnL = DailyPnL,
CumPnL = CumPnL,
MarginBalance = MarginBalance,
MarginCall = MarginCall,
CumMarginCalls = CumMarginCalls,
stringsAsFactors = FALSE
)
# Ordenar columnas y mostrar
resultados_SB <- resultados_SB %>%
select(Date, Price, Position, Rollover, Signal_method, Signal_value, DailyPnL, CumPnL, MarginBalance, MarginCall, CumMarginCalls)
print(tail(resultados_SB, 15))
## Date Price Position Rollover Signal_method Signal_value DailyPnL
## 34 2028-06-16 2714.478 Corto FALSE <NA> NA 2281096.3
## 35 2028-07-16 2556.430 Corto FALSE <NA> NA 13750163.1
## 36 2028-08-16 2565.709 Corto TRUE LogSignQuarter -0.06597752 -807277.1
## 37 2028-09-16 2553.513 Corto FALSE <NA> NA 1061082.4
## 38 2028-10-16 2582.018 Corto FALSE <NA> NA -2479958.4
## 39 2028-11-16 2474.311 Corto TRUE LogSignQuarter -0.03627317 9370564.8
## 40 2028-12-16 2421.125 Corto FALSE <NA> NA 4627182.5
## 41 2029-01-16 2330.654 Corto FALSE <NA> NA 7870914.1
## 42 2029-02-16 2306.703 Corto TRUE LogSignQuarter -0.07014278 2083805.1
## 43 2029-03-16 2241.648 Corto FALSE <NA> NA 5659702.6
## 44 2029-04-16 2206.530 Corto FALSE <NA> NA 3055281.2
## 45 2029-05-16 2248.892 Corto TRUE LogSignQuarter -0.02538155 -3685434.0
## 46 2029-06-16 2333.395 Corto FALSE <NA> NA -7351831.5
## 47 2029-07-16 2206.740 Corto FALSE <NA> NA 11018995.9
## 48 2029-08-16 2116.560 Corto TRUE LogSignQuarter -0.06064510 7845640.7
## CumPnL MarginBalance MarginCall CumMarginCalls
## 34 86918922 108345576 0 0
## 35 100669085 122095739 0 0
## 36 99861808 121288462 0 0
## 37 100922891 122349545 0 0
## 38 98442932 119869586 0 0
## 39 107813497 129240151 0 0
## 40 112440679 133867334 0 0
## 41 120311594 141738248 0 0
## 42 122395399 143822053 0 0
## 43 128055101 149481755 0 0
## 44 131110382 152537036 0 0
## 45 127424948 148851602 0 0
## 46 120073117 141499771 0 0
## 47 131092113 152518767 0 0
## 48 138937753 160364408 0 0
print(summary(resultados_SB %>% select(DailyPnL, CumPnL, MarginBalance, MarginCall, CumMarginCalls)))
## DailyPnL CumPnL MarginBalance MarginCall
## Min. :-32984777 Min. : 0 Min. : 21426654 Min. :0
## 1st Qu.: -1587463 1st Qu.: 61243999 1st Qu.: 82670653 1st Qu.:0
## Median : 3108892 Median : 82246361 Median :103673015 Median :0
## Mean : 2894537 Mean : 79771643 Mean :101198297 Mean :0
## 3rd Qu.: 8532074 3rd Qu.:100063627 3rd Qu.:121490282 3rd Qu.:0
## Max. : 22624026 Max. :138937753 Max. :160364408 Max. :0
## CumMarginCalls
## Min. :0
## 1st Qu.:0
## Median :0
## Mean :0
## 3rd Qu.:0
## Max. :0
# Visualización de las trayectorias escogidas
matplot(cbind(S_serie_B, S_serie_M, S_serie_A), type = "l",
col = c("red", "blue", "green"), lty = 1,
xlab = "Mes", ylab = "TRX simulada",
main = "Trayectorias TRX Futura: Mas Baja, Intermedia, Mas Alta")
En el flujo de caja del período comprendido entre los años 6 y 10 construimos tres escenarios: trayectoria alta, baja e intermedia. Cada escenario refleja cómo las liquidaciones y reposiciones de contratos (rollover) impactan la posición de cobertura. Como criterio para el cambio de posición, optamos por basarnos en el signo del retorno acumulado en el trimestre: si el retorno fue positivo, se mantuvo la cobertura; mientras que, si fue negativo, se decidió ajustar la posición. Esta regla sencilla permitió dar consistencia a la estrategia y, al mismo tiempo, incorporar disciplina en la toma de decisiones. En este sentido, la gestión activa del rollover no solo reflejó nuestras expectativas de mercado, sino que también mostró cómo un criterio claro y objetivo puede ayudar a diferenciar entre cubrir el riesgo y aprovechar oportunidades especulativas en el largo plazo.
# ----- Configuración -----
# Tomamos hasta 48 meses
n_meses <- 48
n_take_A <- min(n_meses, nrow(flujo_cop_Op), nrow(resultados_SA))
# Extraer últimos n_take meses de cada tabla
amort_last_A <- tail(flujo_cop_Op, n_take_A)
margin_last_A <- tail(resultados_SA, n_take_A)
# Si no hay columna Date en amort_last, crear fechas alineadas con results_SA (opcional)
if (!"Date" %in% names(amort_last_A)) {
amort_last_A$Date <- margin_last_A$Date
}
# ----- Construcción de la nueva amortización con cobertura -----
amort_con_cobertura_A <- amort_last_A %>%
mutate(
Date = as.Date(Date),
Cuota_COP = as.numeric(`Cuota (COP)`),
Interes_COP_original = as.numeric(`Interés (COP)`),
AbonoCapital_COP_original = as.numeric(`Abono a Capital (COP)`),
SaldoRestante_COP_original = as.numeric(`Saldo Restante (COP)`),
DailyPnL = as.numeric(margin_last_A$DailyPnL)
) %>%
# Inicializamos columnas que llenaremos
mutate(
PaymentFromMargin = 0,
BorrowerCash = 0,
Cuota_Ajustada = NA_real_,
Interes_COP = NA_real_,
AbonoCapital_COP = NA_real_,
SaldoRestante_COP = NA_real_,
Surplus = 0,
Shortfall = 0
)
# Empezar saldo con el primer saldo disponible (saldo inicial al comienzo del período seleccionado)
saldo_cop <- amort_con_cobertura_A$SaldoRestante_COP_original[1]
if (is.na(saldo_cop) || saldo_cop <= 0) {
# fallback: usar suma de cuotas restantes (solo si el saldo original no está presente)
saldo_cop <- sum(amort_con_cobertura_A$Cuota_COP, na.rm = TRUE)
warning("Saldo inicial en tabla original era NA o 0 — usando suma de cuotas como proxy.")
}
# Iterar mes a mes
for (i in seq_len(nrow(amort_con_cobertura_A))) {
cuota <- amort_con_cobertura_A$Cuota_COP[i]
pnl <- amort_con_cobertura_A$DailyPnL[i]
# Payment from margin (solo si pnl positivo)
payment_from_margin <- ifelse(!is.na(pnl) && pnl > 0, min(pnl, cuota), 0)
borrower_cash <- cuota - payment_from_margin
# Surplus y shortfall respecto a la cuota
surplus <- ifelse(pnl > cuota, pnl - cuota, 0)
shortfall <- ifelse(pnl >= 0, max(0, cuota - pnl), pnl)
# Para la amortización usamos el AbonoCapital original:
interes_mes <- amort_con_cobertura_A$`Interés (COP)`[i]
abono_capital <- amort_con_cobertura_A$`Abono a Capital (COP)`[i]
# Si por algún motivo faltan los valores originales de interes/abono, recalculamos aproximado:
if (is.na(interes_mes) || is.na(abono_capital)) {
# asumimos interes_mes = proporcional a saldo_cop * tasa_estimada (simple fallback)
# Pero como dispones de esos valores en tu código original normalmente no entrará aquí.
interes_mes <- 0
abono_capital <- cuota - interes_mes
}
# Actualizar saldo: saldo_cop es saldo al inicio del mes; se reduce por abono_capital
nuevo_saldo <- max(0, saldo_cop - abono_capital)
# Guardar valores
amort_con_cobertura_A$PaymentFromMargin[i] <- round(payment_from_margin, 2)
amort_con_cobertura_A$BorrowerCash[i] <- round(borrower_cash, 2)
amort_con_cobertura_A$Cuota_Ajustada[i] <- round(max(0, cuota - payment_from_margin), 2)
amort_con_cobertura_A$Interes_COP[i] <- round(interes_mes, 2)
amort_con_cobertura_A$AbonoCapital_COP[i] <- round(abono_capital, 2)
amort_con_cobertura_A$SaldoRestante_COP[i] <- round(nuevo_saldo, 2)
amort_con_cobertura_A$Surplus[i] <- round(surplus, 2)
amort_con_cobertura_A$Shortfall[i] <- round(shortfall, 2)
# preparar saldo para el siguiente mes
saldo_cop <- nuevo_saldo
}
# Agregados acumulados
amort_con_cobertura_A <- amort_con_cobertura_A %>%
mutate(
CumSurplus = cumsum(Surplus),
CumShortfall = cumsum(Shortfall),
`Valor TRM Mas Alta` = S_serie_A,
CoveredFlag = PaymentFromMargin >= Cuota_COP
)
# Salida limpia: seleccionar columnas de interés en orden claro
amort_con_cobertura_A_out <- amort_con_cobertura_A %>%
select(
Period = Date,
Mes,
`Valor TRM Mas Alta`,
Cuota_COP,
DailyPnL,
PaymentFromMargin,
BorrowerCash,
Cuota_Ajustada,
Surplus,
CumSurplus,
Shortfall,
CumShortfall,
CoveredFlag
)
# Tabla resumen
amort_cobertura_A_out <- amort_con_cobertura_A %>%
select(
Period =
Mes,
Cuota_COP,
DailyPnL,
Cuota_Ajustada,
Surplus,
CumSurplus,
CoveredFlag
)
print(amort_cobertura_A_out)
## Period Cuota_COP DailyPnL Cuota_Ajustada Surplus CumSurplus
## 2025-09-1572 73 5752941 0.0 5752941.1 0 0
## 2025-09-1573 74 5724969 2634966.0 3090003.0 0 0
## 2025-09-1574 75 5779412 -556425.8 5779412.0 0 0
## 2025-09-1575 76 5826936 -13044495.1 5826936.1 0 0
## 2025-09-1576 77 5882697 -1187367.5 5882697.0 0 0
## 2025-09-1577 78 5870195 4198846.7 1671348.8 0 0
## 2025-09-1578 79 5929096 -7535408.4 5929095.5 0 0
## 2025-09-1579 80 6021082 5821180.9 199900.7 0 0
## 2025-09-1580 81 6030614 4514426.7 1516187.6 0 0
## 2025-09-1581 82 6041133 4395766.2 1645367.0 0 0
## 2025-09-1582 83 6043912 -11374903.0 6043911.6 0 0
## 2025-09-1583 84 5984305 -8292930.6 5984305.1 0 0
## 2025-09-1584 85 5926325 5298814.5 627510.4 0 0
## 2025-09-1585 86 5897757 -6754104.3 5897756.9 0 0
## 2025-09-1586 87 5922065 336667.9 5585396.8 0 0
## 2025-09-1587 88 5936304 -657687.3 5936304.2 0 0
## 2025-09-1588 89 5999344 14525363.2 0.0 8526019 8526019
## 2025-09-1589 90 5954805 20832105.6 0.0 14877300 23403319
## 2025-09-1590 91 5950185 -2955551.8 5950185.1 0 23403319
## 2025-09-1591 92 5927100 18073130.8 0.0 12146031 35549350
## 2025-09-1592 93 5938141 3195910.1 2742230.7 0 35549350
## 2025-09-1593 94 5970984 854694.6 5116289.3 0 35549350
## 2025-09-1594 95 6058848 -2081006.0 6058847.9 0 35549350
## 2025-09-1595 96 6146507 1426935.5 4719571.7 0 35549350
## 2025-09-1596 97 6245536 9922553.9 0.0 3677018 39226368
## 2025-09-1597 98 6279822 -5920517.9 6279822.0 0 39226368
## 2025-09-1598 99 6378296 -13150892.8 6378296.1 0 39226368
## 2025-09-1599 100 6319327 5202617.6 1116709.6 0 39226368
## 2025-09-15100 101 6328324 -21574159.2 6328324.1 0 39226368
## 2025-09-15101 102 6408177 9834242.1 0.0 3426065 42652434
## 2025-09-15102 103 6393117 3728448.2 2664668.7 0 42652434
## 2025-09-15103 104 6420172 -1713321.4 6420171.8 0 42652434
## 2025-09-15104 105 6463131 -2636201.2 6463130.8 0 42652434
## 2025-09-15105 106 6452488 4113970.5 2338517.8 0 42652434
## 2025-09-15106 107 6522877 3872023.9 2650853.4 0 42652434
## 2025-09-15107 108 6554268 -6310787.2 6554267.8 0 42652434
## 2025-09-15108 109 6511232 -3490868.5 6511232.4 0 42652434
## 2025-09-15109 110 6578140 -6196872.9 6578139.5 0 42652434
## 2025-09-15110 111 6510266 -20146224.3 6510266.3 0 42652434
## 2025-09-15111 112 6563934 3281250.0 3282683.6 0 42652434
## 2025-09-15112 113 6520139 -1368338.2 6520138.8 0 42652434
## 2025-09-15113 114 6555176 -25165853.9 6555175.7 0 42652434
## 2025-09-15114 115 6581006 -23093828.0 6581006.0 0 42652434
## 2025-09-15115 116 6648431 8261468.9 0.0 1613038 44265472
## 2025-09-15116 117 6673225 -20663163.6 6673225.1 0 44265472
## 2025-09-15117 118 6722822 344127.9 6378693.8 0 44265472
## 2025-09-15118 119 6683099 12669755.4 0.0 5986656 50252128
## 2025-09-15119 120 6678928 -4824310.5 6678927.8 0 50252128
## CoveredFlag
## 2025-09-1572 FALSE
## 2025-09-1573 FALSE
## 2025-09-1574 FALSE
## 2025-09-1575 FALSE
## 2025-09-1576 FALSE
## 2025-09-1577 FALSE
## 2025-09-1578 FALSE
## 2025-09-1579 FALSE
## 2025-09-1580 FALSE
## 2025-09-1581 FALSE
## 2025-09-1582 FALSE
## 2025-09-1583 FALSE
## 2025-09-1584 FALSE
## 2025-09-1585 FALSE
## 2025-09-1586 FALSE
## 2025-09-1587 FALSE
## 2025-09-1588 TRUE
## 2025-09-1589 TRUE
## 2025-09-1590 FALSE
## 2025-09-1591 TRUE
## 2025-09-1592 FALSE
## 2025-09-1593 FALSE
## 2025-09-1594 FALSE
## 2025-09-1595 FALSE
## 2025-09-1596 TRUE
## 2025-09-1597 FALSE
## 2025-09-1598 FALSE
## 2025-09-1599 FALSE
## 2025-09-15100 FALSE
## 2025-09-15101 TRUE
## 2025-09-15102 FALSE
## 2025-09-15103 FALSE
## 2025-09-15104 FALSE
## 2025-09-15105 FALSE
## 2025-09-15106 FALSE
## 2025-09-15107 FALSE
## 2025-09-15108 FALSE
## 2025-09-15109 FALSE
## 2025-09-15110 FALSE
## 2025-09-15111 FALSE
## 2025-09-15112 FALSE
## 2025-09-15113 FALSE
## 2025-09-15114 FALSE
## 2025-09-15115 TRUE
## 2025-09-15116 FALSE
## 2025-09-15117 FALSE
## 2025-09-15118 TRUE
## 2025-09-15119 FALSE
# Resumen rápido
resumen_A <- amort_con_cobertura_A_out %>%
summarise(
total_cuotas = sum(Cuota_COP, na.rm = TRUE),
total_paid_from_margin = sum(PaymentFromMargin, na.rm = TRUE),
total_borrower_cash = sum(BorrowerCash, na.rm = TRUE),
total_surplus = last(CumSurplus),
total_shortfall = last(CumShortfall),
pct_covered = total_paid_from_margin / total_cuotas * 100,
months_fully_covered = sum(CoveredFlag, na.rm = TRUE)
)
print(resumen_A)
## total_cuotas total_paid_from_margin total_borrower_cash total_surplus
## 1 298507589 97087139 201420450 50252128
## total_shortfall pct_covered months_fully_covered
## 1 -159596345 32.52418 7
# ----- Configuración -----
# Tomamos hasta 48 meses
n_meses <- 48
n_take_M <- min(n_meses, nrow(flujo_cop_M), nrow(resultados_SM))
# Extraer últimos n_take meses de cada tabla
amort_last_M <- tail(flujo_cop_M, n_take_M)
margin_last_M <- tail(resultados_SM, n_take_M)
# Si no hay columna Date en amort_last, crear fechas alineadas con results_SA (opcional)
if (!"Date" %in% names(amort_last_M)) {
amort_last_M$Date <- margin_last_M$Date
}
# ----- Construcción de la nueva amortización con cobertura -----
amort_con_cobertura_M <- amort_last_M %>%
mutate(
Date = as.Date(Date),
Cuota_COP = as.numeric(`Cuota (COP)`),
Interes_COP_original = as.numeric(`Interés (COP)`),
AbonoCapital_COP_original = as.numeric(`Abono a Capital (COP)`),
SaldoRestante_COP_original = as.numeric(`Saldo Restante (COP)`),
DailyPnL = as.numeric(margin_last_M$DailyPnL)
) %>%
# Inicializamos columnas que llenaremos
mutate(
PaymentFromMargin = 0,
BorrowerCash = 0,
Cuota_Ajustada = NA_real_,
Interes_COP = NA_real_,
AbonoCapital_COP = NA_real_,
SaldoRestante_COP = NA_real_,
Surplus = 0,
Shortfall = 0
)
# Empezar saldo con el primer saldo disponible (saldo inicial al comienzo del período seleccionado)
saldo_cop <- amort_con_cobertura_M$SaldoRestante_COP_original[1]
if (is.na(saldo_cop) || saldo_cop <= 0) {
# fallback: usar suma de cuotas restantes (solo si el saldo original no está presente)
saldo_cop <- sum(amort_con_cobertura_M$Cuota_COP, na.rm = TRUE)
warning("Saldo inicial en tabla original era NA o 0 — usando suma de cuotas como proxy.")
}
# Iterar mes a mes
for (i in seq_len(nrow(amort_con_cobertura_M))) {
cuota <- amort_con_cobertura_M$Cuota_COP[i]
pnl <- amort_con_cobertura_M$DailyPnL[i]
# Payment from margin (solo si pnl positivo)
payment_from_margin <- ifelse(!is.na(pnl) && pnl > 0, min(pnl, cuota), 0)
borrower_cash <- cuota - payment_from_margin
# Surplus y shortfall respecto a la cuota
surplus <- ifelse(pnl > cuota, pnl - cuota, 0)
shortfall <- ifelse(pnl >= 0, max(0, cuota - pnl), pnl)
# Para la amortización usamos el AbonoCapital original:
interes_mes <- amort_con_cobertura_M$`Interés (COP)`[i]
abono_capital <- amort_con_cobertura_M$`Abono a Capital (COP)`[i]
# Si por algún motivo faltan los valores originales de interes/abono, recalculamos aproximado:
if (is.na(interes_mes) || is.na(abono_capital)) {
# asumimos interes_mes = proporcional a saldo_cop * tasa_estimada (simple fallback)
# Pero como dispones de esos valores en tu código original normalmente no entrará aquí.
interes_mes <- 0
abono_capital <- cuota - interes_mes
}
# Actualizar saldo: saldo_cop es saldo al inicio del mes; se reduce por abono_capital
nuevo_saldo <- max(0, saldo_cop - abono_capital)
# Guardar valores
amort_con_cobertura_M$PaymentFromMargin[i] <- round(payment_from_margin, 2)
amort_con_cobertura_M$BorrowerCash[i] <- round(borrower_cash, 2)
amort_con_cobertura_M$Cuota_Ajustada[i] <- round(max(0, cuota - payment_from_margin), 2)
amort_con_cobertura_M$Interes_COP[i] <- round(interes_mes, 2)
amort_con_cobertura_M$AbonoCapital_COP[i] <- round(abono_capital, 2)
amort_con_cobertura_M$SaldoRestante_COP[i] <- round(nuevo_saldo, 2)
amort_con_cobertura_M$Surplus[i] <- round(surplus, 2)
amort_con_cobertura_M$Shortfall[i] <- round(shortfall, 2)
# preparar saldo para el siguiente mes
saldo_cop <- nuevo_saldo
}
# Agregados acumulados
amort_con_cobertura_M <- amort_con_cobertura_M %>%
mutate(
CumSurplus = cumsum(Surplus),
CumShortfall = cumsum(Shortfall),
`Valor TRM Media` = S_serie_M,
CoveredFlag = PaymentFromMargin >= Cuota_COP
)
# Salida limpia: seleccionar columnas de interés en orden claro
amort_con_cobertura_M_out <- amort_con_cobertura_M %>%
select(
Period = Date,
Mes,
`Valor TRM Media`,
Cuota_COP,
DailyPnL,
PaymentFromMargin,
BorrowerCash,
Cuota_Ajustada,
Surplus,
CumSurplus,
Shortfall,
CumShortfall,
CoveredFlag
)
# Tabla resumen
amort_cobertura_M_out <- amort_con_cobertura_M %>%
select(
Period =
Mes,
Cuota_COP,
DailyPnL,
Cuota_Ajustada,
Surplus,
CumSurplus,
CoveredFlag
)
print(amort_cobertura_M_out)
## Period Cuota_COP DailyPnL Cuota_Ajustada Surplus CumSurplus
## 2025-09-1572 73 5164005 0 5164005 0 0
## 2025-09-1573 74 5161971 3275816 1886155 0 0
## 2025-09-1574 75 5163698 3186683 1977014 0 0
## 2025-09-1575 76 5166383 2991886 2174498 0 0
## 2025-09-1576 77 5170130 4122261 1047869 0 0
## 2025-09-1577 78 5168415 3572809 1595606 0 0
## 2025-09-1578 79 5166885 3980860 1186025 0 0
## 2025-09-1579 80 5167450 3584060 1583390 0 0
## 2025-09-1580 81 5168013 3524591 1643422 0 0
## 2025-09-1581 82 5168429 3406067 1762363 0 0
## 2025-09-1582 83 5168729 3963671 1205058 0 0
## 2025-09-1583 84 5171950 3354824 1817125 0 0
## 2025-09-1584 85 5172247 2779203 2393043 0 0
## 2025-09-1585 86 5171009 3989153 1181857 0 0
## 2025-09-1586 87 5174469 3260447 1914022 0 0
## 2025-09-1587 88 5176448 3038424 2138024 0 0
## 2025-09-1588 89 5177163 3327236 1849927 0 0
## 2025-09-1589 90 5180037 3203913 1976124 0 0
## 2025-09-1590 91 5180203 3359510 1820692 0 0
## 2025-09-1591 92 5180240 2994935 2185305 0 0
## 2025-09-1592 93 5181694 2719724 2461971 0 0
## 2025-09-1593 94 5183939 2874693 2309245 0 0
## 2025-09-1594 95 5184938 2968985 2215953 0 0
## 2025-09-1595 96 5186547 2185601 3000947 0 0
## 2025-09-1596 97 5185547 3233626 1951920 0 0
## 2025-09-1597 98 5188063 2815487 2372575 0 0
## 2025-09-1598 99 5187644 2503682 2683962 0 0
## 2025-09-1599 100 5190085 3012295 2177790 0 0
## 2025-09-15100 101 5190862 2210084 2980778 0 0
## 2025-09-15101 102 5194987 3059231 2135756 0 0
## 2025-09-15102 103 5195373 2620180 2575192 0 0
## 2025-09-15103 104 5193912 2676079 2517833 0 0
## 2025-09-15104 105 5194914 2513530 2681384 0 0
## 2025-09-15105 106 5199007 2614108 2584899 0 0
## 2025-09-15106 107 5199173 2709101 2490072 0 0
## 2025-09-15107 108 5201074 2770235 2430838 0 0
## 2025-09-15108 109 5203064 2683081 2519982 0 0
## 2025-09-15109 110 5204633 2649883 2554751 0 0
## 2025-09-15110 111 5201773 2992881 2208893 0 0
## 2025-09-15111 112 5204524 2664886 2539638 0 0
## 2025-09-15112 113 5201461 2399141 2802320 0 0
## 2025-09-15113 114 5205278 2455871 2749407 0 0
## 2025-09-15114 115 5206254 3241784 1964470 0 0
## 2025-09-15115 116 5203039 2798207 2404832 0 0
## 2025-09-15116 117 5204380 1751191 3453189 0 0
## 2025-09-15117 118 5207667 2320788 2886880 0 0
## 2025-09-15118 119 5208535 2309400 2899135 0 0
## 2025-09-15119 120 5214739 2264185 2950554 0 0
## CoveredFlag
## 2025-09-1572 FALSE
## 2025-09-1573 FALSE
## 2025-09-1574 FALSE
## 2025-09-1575 FALSE
## 2025-09-1576 FALSE
## 2025-09-1577 FALSE
## 2025-09-1578 FALSE
## 2025-09-1579 FALSE
## 2025-09-1580 FALSE
## 2025-09-1581 FALSE
## 2025-09-1582 FALSE
## 2025-09-1583 FALSE
## 2025-09-1584 FALSE
## 2025-09-1585 FALSE
## 2025-09-1586 FALSE
## 2025-09-1587 FALSE
## 2025-09-1588 FALSE
## 2025-09-1589 FALSE
## 2025-09-1590 FALSE
## 2025-09-1591 FALSE
## 2025-09-1592 FALSE
## 2025-09-1593 FALSE
## 2025-09-1594 FALSE
## 2025-09-1595 FALSE
## 2025-09-1596 FALSE
## 2025-09-1597 FALSE
## 2025-09-1598 FALSE
## 2025-09-1599 FALSE
## 2025-09-15100 FALSE
## 2025-09-15101 FALSE
## 2025-09-15102 FALSE
## 2025-09-15103 FALSE
## 2025-09-15104 FALSE
## 2025-09-15105 FALSE
## 2025-09-15106 FALSE
## 2025-09-15107 FALSE
## 2025-09-15108 FALSE
## 2025-09-15109 FALSE
## 2025-09-15110 FALSE
## 2025-09-15111 FALSE
## 2025-09-15112 FALSE
## 2025-09-15113 FALSE
## 2025-09-15114 FALSE
## 2025-09-15115 FALSE
## 2025-09-15116 FALSE
## 2025-09-15117 FALSE
## 2025-09-15118 FALSE
## 2025-09-15119 FALSE
# Resumen rápido
resumen_M <- amort_con_cobertura_M_out %>%
summarise(
total_cuotas = sum(Cuota_COP, na.rm = TRUE),
total_paid_from_margin = sum(PaymentFromMargin, na.rm = TRUE),
total_borrower_cash = sum(BorrowerCash, na.rm = TRUE),
total_surplus = last(CumSurplus),
total_shortfall = last(CumShortfall),
pct_covered = total_paid_from_margin / total_cuotas * 100,
months_fully_covered = sum(CoveredFlag, na.rm = TRUE)
)
print(resumen_M)
## total_cuotas total_paid_from_margin total_borrower_cash total_surplus
## 1 248940978 138934288 110006690 0
## total_shortfall pct_covered months_fully_covered
## 1 110006690 55.81013 0
# ----- Configuración -----
# Tomamos hasta 48 meses
n_meses <- 48
n_take_B <- min(n_meses, nrow(flujo_cop_P), nrow(resultados_SB))
# Extraer últimos n_take meses de cada tabla
amort_last_B <- tail(flujo_cop_P, n_take_B)
margin_last_B <- tail(resultados_SB, n_take_B)
# Si no hay columna Date en amort_last, crear fechas alineadas con results_SA (opcional)
if (!"Date" %in% names(amort_last_B)) {
amort_last_B$Date <- margin_last_B$Date
}
# ----- Construcción de la nueva amortización con cobertura -----
amort_con_cobertura_B <- amort_last_B %>%
mutate(
Date = as.Date(Date),
Cuota_COP = as.numeric(`Cuota (COP)`),
Interes_COP_original = as.numeric(`Interés (COP)`),
AbonoCapital_COP_original = as.numeric(`Abono a Capital (COP)`),
SaldoRestante_COP_original = as.numeric(`Saldo Restante (COP)`),
DailyPnL = as.numeric(margin_last_B$DailyPnL)
) %>%
# Inicializamos columnas que llenaremos
mutate(
PaymentFromMargin = 0,
BorrowerCash = 0,
Cuota_Ajustada = NA_real_,
Interes_COP = NA_real_,
AbonoCapital_COP = NA_real_,
SaldoRestante_COP = NA_real_,
Surplus = 0,
Shortfall = 0
)
# Empezar saldo con el primer saldo disponible (saldo inicial al comienzo del período seleccionado)
saldo_cop <- amort_con_cobertura_B$SaldoRestante_COP_original[1]
if (is.na(saldo_cop) || saldo_cop <= 0) {
# fallback: usar suma de cuotas restantes (solo si el saldo original no está presente)
saldo_cop <- sum(amort_con_cobertura_B$Cuota_COP, na.rm = TRUE)
warning("Saldo inicial en tabla original era NA o 0 — usando suma de cuotas como proxy.")
}
# Iterar mes a mes
for (i in seq_len(nrow(amort_con_cobertura_B))) {
cuota <- amort_con_cobertura_B$Cuota_COP[i]
pnl <- amort_con_cobertura_B$DailyPnL[i]
# Payment from margin (solo si pnl positivo)
payment_from_margin <- ifelse(!is.na(pnl) && pnl > 0, min(pnl, cuota), 0)
borrower_cash <- cuota - payment_from_margin
# Surplus y shortfall respecto a la cuota
surplus <- ifelse(pnl > cuota, pnl - cuota, 0)
shortfall <- ifelse(pnl >= 0, max(0, cuota - pnl), pnl)
# Para la amortización usamos el AbonoCapital original:
interes_mes <- amort_con_cobertura_B$`Interés (COP)`[i]
abono_capital <- amort_con_cobertura_B$`Abono a Capital (COP)`[i]
# Si por algún motivo faltan los valores originales de interes/abono, recalculamos aproximado:
if (is.na(interes_mes) || is.na(abono_capital)) {
# asumimos interes_mes = proporcional a saldo_cop * tasa_estimada (simple fallback)
# Pero como dispones de esos valores en tu código original normalmente no entrará aquí.
interes_mes <- 0
abono_capital <- cuota - interes_mes
}
# Actualizar saldo: saldo_cop es saldo al inicio del mes; se reduce por abono_capital
nuevo_saldo <- max(0, saldo_cop - abono_capital)
# Guardar valores
amort_con_cobertura_B$PaymentFromMargin[i] <- round(payment_from_margin, 2)
amort_con_cobertura_B$BorrowerCash[i] <- round(borrower_cash, 2)
amort_con_cobertura_B$Cuota_Ajustada[i] <- round(max(0, cuota - payment_from_margin), 2)
amort_con_cobertura_B$Interes_COP[i] <- round(interes_mes, 2)
amort_con_cobertura_B$AbonoCapital_COP[i] <- round(abono_capital, 2)
amort_con_cobertura_B$SaldoRestante_COP[i] <- round(nuevo_saldo, 2)
amort_con_cobertura_B$Surplus[i] <- round(surplus, 2)
amort_con_cobertura_B$Shortfall[i] <- round(shortfall, 2)
# preparar saldo para el siguiente mes
saldo_cop <- nuevo_saldo
}
# Agregados acumulados
amort_con_cobertura_B <- amort_con_cobertura_B %>%
mutate(
CumSurplus = cumsum(Surplus),
CumShortfall = cumsum(Shortfall),
`Valor TRM Baja` = S_serie_B,
CoveredFlag = PaymentFromMargin >= Cuota_COP
)
# Salida limpia: seleccionar columnas de interés en orden claro
amort_con_cobertura_B_out <- amort_con_cobertura_B %>%
select(
Period = Date,
Mes,
`Valor TRM Baja`,
Cuota_COP,
DailyPnL,
PaymentFromMargin,
BorrowerCash,
Cuota_Ajustada,
Surplus,
CumSurplus,
Shortfall,
CumShortfall,
CoveredFlag
)
# Tabla resumen
amort_cobertura_B_out <- amort_con_cobertura_B %>%
select(
Period =
Mes,
Cuota_COP,
DailyPnL,
Cuota_Ajustada,
Surplus,
CumSurplus,
CoveredFlag
)
print(amort_cobertura_B_out)
## Period Cuota_COP DailyPnL Cuota_Ajustada Surplus CumSurplus
## 2025-09-1572 73 4042413 0.0 4042412.9 0.0 0
## 2025-09-1573 74 4044787 950698.5 3094088.5 0.0 0
## 2025-09-1574 75 4062049 6029608.4 0.0 1967559.4 1967559
## 2025-09-1575 76 4111623 19575941.8 0.0 15464318.6 17431878
## 2025-09-1576 77 4051142 20126207.5 0.0 16075065.2 33506943
## 2025-09-1577 78 4008528 1667255.7 2341272.6 0.0 33506943
## 2025-09-1578 79 3948782 11499841.0 0.0 7551059.0 41058002
## 2025-09-1579 80 3912845 19273834.0 0.0 15360989.2 56418991
## 2025-09-1580 81 3937798 3767956.1 169842.3 0.0 56418991
## 2025-09-1581 82 3950583 -1289964.1 3950582.9 0.0 56418991
## 2025-09-1582 83 3958819 -5943536.8 3958819.3 0.0 56418991
## 2025-09-1583 84 3963275 11449805.9 0.0 7486530.9 63905522
## 2025-09-1584 85 3940088 3162502.2 777585.9 0.0 63905522
## 2025-09-1585 86 3937865 -6653783.6 3937865.1 0.0 63905522
## 2025-09-1586 87 3947028 15262448.6 0.0 11315420.3 75220943
## 2025-09-1587 88 3986730 -6587556.0 3986730.1 0.0 75220943
## 2025-09-1588 89 3961401 -12536248.3 3961400.7 0.0 75220943
## 2025-09-1589 90 4000929 9185016.5 0.0 5184087.5 80405030
## 2025-09-1590 91 4023853 -3980955.3 4023853.0 0.0 80405030
## 2025-09-1591 92 4047443 -32984776.7 4047443.3 0.0 80405030
## 2025-09-1592 93 4047088 -16916535.3 4047087.6 0.0 80405030
## 2025-09-1593 94 3998840 -6869735.0 3998840.0 0.0 80405030
## 2025-09-1594 95 4014288 22624026.5 0.0 18609738.9 99014769
## 2025-09-1595 96 3989503 1624514.2 2364988.8 0.0 99014769
## 2025-09-1596 97 3982389 9272249.2 0.0 5289859.9 104304629
## 2025-09-1597 98 4048943 3663752.5 385190.1 0.0 104304629
## 2025-09-1598 99 3989284 6005780.8 0.0 2016497.2 106321126
## 2025-09-1599 100 4001090 3271072.3 730017.2 0.0 106321126
## 2025-09-15100 101 4105596 2975480.9 1130115.1 0.0 106321126
## 2025-09-15101 102 4176240 -561097.3 4176240.2 0.0 106321126
## 2025-09-15102 103 4095015 3815776.6 279238.2 0.0 106321126
## 2025-09-15103 104 4063325 -4556181.3 4063325.0 0.0 106321126
## 2025-09-15104 105 3990702 8314426.4 0.0 4323724.9 110644851
## 2025-09-15105 106 3999286 2281096.3 1718189.9 0.0 110644851
## 2025-09-15106 107 3993171 13750163.1 0.0 9756991.6 120401843
## 2025-09-15107 108 3947176 -807277.1 3947176.5 0.0 120401843
## 2025-09-15108 109 3975734 1061082.4 2914652.0 0.0 120401843
## 2025-09-15109 110 3905025 -2479958.4 3905024.9 0.0 120401843
## 2025-09-15110 111 3903536 9370564.8 0.0 5467028.5 125868871
## 2025-09-15111 112 3903333 4627182.5 0.0 723849.1 126592720
## 2025-09-15112 113 3913405 7870914.1 0.0 3957508.8 130550229
## 2025-09-15113 114 3927199 2083805.1 1843393.4 0.0 130550229
## 2025-09-15114 115 3943502 5659702.6 0.0 1716200.5 132266430
## 2025-09-15115 116 3914105 3055281.2 858823.4 0.0 132266430
## 2025-09-15116 117 3916689 -3685434.0 3916689.4 0.0 132266430
## 2025-09-15117 118 3920963 -7351831.5 3920963.1 0.0 132266430
## 2025-09-15118 119 3970878 11018995.9 0.0 7048118.4 139314548
## 2025-09-15119 120 3955130 7845640.7 0.0 3890510.8 143205059
## CoveredFlag
## 2025-09-1572 FALSE
## 2025-09-1573 FALSE
## 2025-09-1574 TRUE
## 2025-09-1575 TRUE
## 2025-09-1576 TRUE
## 2025-09-1577 FALSE
## 2025-09-1578 TRUE
## 2025-09-1579 TRUE
## 2025-09-1580 FALSE
## 2025-09-1581 FALSE
## 2025-09-1582 FALSE
## 2025-09-1583 TRUE
## 2025-09-1584 FALSE
## 2025-09-1585 FALSE
## 2025-09-1586 TRUE
## 2025-09-1587 FALSE
## 2025-09-1588 FALSE
## 2025-09-1589 TRUE
## 2025-09-1590 FALSE
## 2025-09-1591 FALSE
## 2025-09-1592 FALSE
## 2025-09-1593 FALSE
## 2025-09-1594 TRUE
## 2025-09-1595 FALSE
## 2025-09-1596 TRUE
## 2025-09-1597 FALSE
## 2025-09-1598 TRUE
## 2025-09-1599 FALSE
## 2025-09-15100 FALSE
## 2025-09-15101 FALSE
## 2025-09-15102 FALSE
## 2025-09-15103 FALSE
## 2025-09-15104 TRUE
## 2025-09-15105 FALSE
## 2025-09-15106 TRUE
## 2025-09-15107 FALSE
## 2025-09-15108 FALSE
## 2025-09-15109 FALSE
## 2025-09-15110 TRUE
## 2025-09-15111 TRUE
## 2025-09-15112 TRUE
## 2025-09-15113 FALSE
## 2025-09-15114 TRUE
## 2025-09-15115 FALSE
## 2025-09-15116 FALSE
## 2025-09-15117 FALSE
## 2025-09-15118 TRUE
## 2025-09-15119 TRUE
# Resumen rápido
resumen_B <- amort_con_cobertura_B_out %>%
summarise(
total_cuotas = sum(Cuota_COP, na.rm = TRUE),
total_paid_from_margin = sum(PaymentFromMargin, na.rm = TRUE),
total_borrower_cash = sum(BorrowerCash, na.rm = TRUE),
total_surplus = last(CumSurplus),
total_shortfall = last(CumShortfall),
pct_covered = total_paid_from_margin / total_cuotas * 100,
months_fully_covered = sum(CoveredFlag, na.rm = TRUE)
)
print(resumen_B)
## total_cuotas total_paid_from_margin total_borrower_cash total_surplus
## 1 191429417 108937566 82491852 143205059
## total_shortfall pct_covered months_fully_covered
## 1 -90555060 56.90743 19
Al comparar el comportamiento del crédito frente a la cobertura mediante futuros en el mismo horizonte (años 6 al 10), observamos que la estrategia de cobertura protegió de forma significativa el costo de la deuda frente a la depreciación del peso. En los tres escenarios, la cobertura no solo aseguró el cumplimiento de las cuotas del crédito, sino que además generó excedentes gracias al rendimiento de los contratos de futuros. Esto evidencia que la cobertura fue efectiva no únicamente como un mecanismo de protección, sino también como un apoyo financiero adicional que alivió el flujo de caja y reforzó nuestra posición frente a la volatilidad cambiaria.
A lo largo del análisis pudimos comprobar que el comportamiento del dólar es un factor decisivo en el costo final de la deuda. Cuando la moneda se deprecia, las cuotas en pesos aumentan y la carga financiera se hace más pesada; mientras que, si el peso se fortalece, el financiamiento resulta más accesible. Esta conclusión refuerza la idea de que las empresas expuestas a pasivos en moneda extranjera deben considerar mecanismos de cobertura cambiaria (Union, 2025).
Asimismo, el uso de futuros como herramienta de gestión de riesgo mostró ser una estrategia efectiva, ya que permitió cubrir una parte importante de la deuda y, en ciertos escenarios, generar excedentes que mejoraron el flujo de caja. Esto demuestra que, aunque los derivados tienen un componente especulativo, bien utilizados pueden convertirse en un apoyo clave para estabilizar los costos financieros (Bolsa de Valores de Colombia, 2025) (Valores de Colombia, 2025).
Finalmente, este ejercicio nos deja la lección de que la planeación financiera no se limita a calcular cuotas o intereses, sino a diseñar estrategias que reduzcan la incertidumbre y fortalezcan la posición del inversionista frente a la volatilidad del mercado.