El precio del dólar estadounidense frente al peso colombiano es uno de los indicadores más observados de la economía nacional. El USD/COP de contado funciona como termómetro de los flujos internacionales: cuando el par sube, el peso se debilita, lo que suele asociarse a salida de dólares o a mayor demanda por la divisa; cuando baja, ocurre lo contrario.
El propósito del estudio del que hace parte esta base es estimar un rango de negociación, es decir, un soporte y una resistencia para el USD/COP en un horizonte determinado. No se busca pronosticar la dirección del movimiento, sino acotar su magnitud.
De ese propósito se desprende la pregunta que organiza el análisis descriptivo:
¿El comportamiento del USD/COP es homogéneo a lo largo del tiempo y entre estados del mercado, o existen grupos de días con perfiles estadísticos claramente distintos?
La respuesta importa de manera directa: si los indicadores difieren entre grupos, entonces un rango de amplitud fija sería inadecuado y el ancho de la banda tendría que depender del estado del mercado.
Advertencia metodológica. Las variables cualitativas de este caso se construyen categorizando información de la propia base, según criterios que se explicitan más adelante. El análisis es exploratorio y descriptivo: describe asociaciones observadas y no permite extraer conclusiones causales.
panel <- readRDS(file.path(RAIZ, "data_clean", "panel_limpio.rds"))
datos <- data.frame(
fecha = index(panel),
spot = as.numeric(panel$usdcop_spot),
retorno = as.numeric(panel$r_usdcop_spot) * 100,
volatilidad = as.numeric(panel$vol_realizada_21d),
carry = as.numeric(panel$carry_politica),
vix = as.numeric(panel$vix),
cds = as.numeric(panel$cds_colombia_5y)
)
cat("Observaciones:", nrow(datos), "| Periodo:",
format(min(datos$fecha)), "a", format(max(datos$fecha)), "\n")## Observaciones: 3008 | Periodo: 2015-01-02 a 2026-07-31
La base documentada en la Actividad 1.1.2 contiene 3.008 días de negociación y 46 variables, construidas a partir de LSEG Workspace y del Banco de la República. Para este caso se trabaja con un subconjunto de variables directamente ligadas a la pregunta.
datatable(head(datos, 60), rownames = FALSE,
options = list(pageLength = 8, dom = "tip", scrollX = TRUE),
caption = "Vista de la base de trabajo") |>
formatRound(c("spot","retorno","volatilidad","carry","vix","cds"), 2)| Variable | Descripcion | Naturaleza | Escala |
|---|---|---|---|
| retorno | Retorno logarítmico diario del USD/COP, en % | Cuantitativa | Razón |
| volatilidad | Volatilidad realizada a 21 días, anualizada en % | Cuantitativa | Razón |
| carry | Diferencial de tasas de política, BanRep menos Fed, en puntos porcentuales | Cuantitativa | Razón |
| regimen | Estado del mercado según tensión global y local | Cualitativa | Ordinal |
| periodo | Etapa del ciclo macroeconómico | Cualitativa | Ordinal |
| direccion | Signo del movimiento diario del peso | Cualitativa | Nominal |
# --- Régimen de mercado (ordinal) --------------------------------------------
q75_vix <- quantile(datos$vix, 0.75, na.rm = TRUE)
q75_cds <- quantile(datos$cds, 0.75, na.rm = TRUE)
datos$regimen <- with(datos, ifelse(
is.na(vix) | is.na(cds), NA,
ifelse(vix > q75_vix, "Aversión al riesgo",
ifelse(cds > q75_cds, "Estrés local", "Normal"))))
datos$regimen <- factor(datos$regimen,
levels = c("Normal", "Estrés local", "Aversión al riesgo"), ordered = TRUE)
# --- Periodo (ordinal) --------------------------------------------------------
anio <- as.integer(format(datos$fecha, "%Y"))
datos$periodo <- ifelse(anio <= 2019, "2015-2019 Pre-pandemia",
ifelse(anio <= 2021, "2020-2021 Pandemia",
"2022-2026 Ciclo de alzas"))
datos$periodo <- factor(datos$periodo,
levels = c("2015-2019 Pre-pandemia", "2020-2021 Pandemia",
"2022-2026 Ciclo de alzas"), ordered = TRUE)
# --- Dirección diaria del peso (nominal) --------------------------------------
datos$direccion <- with(datos, ifelse(
is.na(retorno), NA,
ifelse(retorno > 0, "Depreciación del peso",
ifelse(retorno < 0, "Apreciación del peso", "Sin cambio"))))
datos$direccion <- factor(datos$direccion,
levels = c("Apreciación del peso", "Sin cambio", "Depreciación del peso"))
cat("Umbrales: VIX >", round(q75_vix, 2), " | CDS >", round(q75_cds, 1), "pb\n")## Umbrales: VIX > 21.14 | CDS > 212.2 pb
Criterio del régimen. Un día se clasifica como aversión al riesgo cuando el VIX supera su tercer cuartil histórico, y como estrés local cuando quien lo supera es el CDS de Colombia. La evaluación es jerárquica: la tensión global se examina primero, de modo que la categoría de estrés local recoge los días de tensión propia del país sin tensión global acompañante.
Criterio del periodo. Se separan tres etapas macroeconómicas: los años previos a la pandemia, el choque de 2020 y 2021, y el ciclo de alzas de tasas posterior.
tabla_frecuencia <- function(x, etiqueta) {
x <- x[!is.na(x)]
ni <- table(x); fi <- prop.table(ni)
out <- data.frame(names(ni), as.integer(ni), round(as.numeric(fi), 4),
round(as.numeric(fi)*100, 2), cumsum(as.integer(ni)),
round(cumsum(as.numeric(fi))*100, 2))
names(out) <- c(etiqueta, "$n_i$", "$f_i$", "$\\%$", "$N_i$", "$F_i\\ \\%$")
out
}
moda_cual <- function(x) names(which.max(table(x)))kable(tabla_frecuencia(datos$regimen, "Régimen de mercado"),
caption = "Frecuencias — régimen de mercado", align = "lrrrrr")| Régimen de mercado | \(n_i\) | \(f_i\) | \(\%\) | \(N_i\) | \(F_i\ \%\) |
|---|---|---|---|---|---|
| Normal | 1850 | 0.6223 | 62.23 | 1850 | 62.23 |
| Estrés local | 375 | 0.1261 | 12.61 | 2225 | 74.84 |
| Aversión al riesgo | 748 | 0.2516 | 25.16 | 2973 | 100.00 |
## Moda: Normal
kable(tabla_frecuencia(datos$periodo, "Periodo"),
caption = "Frecuencias — periodo", align = "lrrrrr")| Periodo | \(n_i\) | \(f_i\) | \(\%\) | \(N_i\) | \(F_i\ \%\) |
|---|---|---|---|---|---|
| 2015-2019 Pre-pandemia | 1299 | 0.4318 | 43.18 | 1299 | 43.18 |
| 2020-2021 Pandemia | 518 | 0.1722 | 17.22 | 1817 | 60.41 |
| 2022-2026 Ciclo de alzas | 1191 | 0.3959 | 39.59 | 3008 | 100.00 |
## Moda: 2015-2019 Pre-pandemia
kable(tabla_frecuencia(datos$direccion, "Dirección diaria"),
caption = "Frecuencias — dirección diaria del peso", align = "lrrrrr")| Dirección diaria | \(n_i\) | \(f_i\) | \(\%\) | \(N_i\) | \(F_i\ \%\) |
|---|---|---|---|---|---|
| Apreciación del peso | 1501 | 0.4992 | 49.92 | 1501 | 49.92 |
| Sin cambio | 39 | 0.0130 | 1.30 | 1540 | 51.21 |
| Depreciación del peso | 1467 | 0.4879 | 48.79 | 3007 | 100.00 |
## Moda: Apreciación del peso
par(mfrow = c(1, 3), mar = c(6, 4, 3, 1))
barplot(table(datos$regimen), col = c(VERDE, ORO, ROJO), border = NA, las = 2,
main = "Régimen", ylab = "Días", cex.names = .75)
barplot(table(datos$periodo), col = AZUL, border = NA, las = 2,
main = "Periodo", cex.names = .75)
barplot(table(datos$direccion), col = c(VERDE, GRIS, ROJO), border = NA, las = 2,
main = "Dirección", cex.names = .75)Lectura. La moda del régimen es Normal, con cerca de dos tercios de las jornadas. La del periodo corresponde a la etapa previa a la pandemia, simplemente porque es la más extensa. En la dirección diaria la moda es apreciación del peso, pero con una ventaja mínima sobre la depreciación: el reparto es prácticamente equilibrado.
Antes de comparar entre grupos conviene examinar la forma de cada distribución, porque de ella depende qué indicadores son apropiados.
vars <- c("retorno", "volatilidad", "carry")
forma <- t(sapply(vars, function(v) {
x <- datos[[v]]; x <- x[!is.na(x)]
c(Media = mean(x), Mediana = median(x), `Desv. est.` = sd(x),
Asimetría = psych::skew(x), `Curtosis (exceso)` = psych::kurtosi(x))
}))
kable(round(forma, 3), caption = "Forma de las distribuciones")| Media | Mediana | Desv. est. | Asimetría | Curtosis (exceso) | |
|---|---|---|---|---|---|
| retorno | 0.009 | 0.000 | 0.869 | 0.482 | 7.044 |
| volatilidad | 12.751 | 11.734 | 5.198 | 3.011 | 18.704 |
| carry | 4.695 | 4.920 | 2.135 | 0.056 | -1.292 |
par(mfrow = c(1, 3), mar = c(4, 4, 3, 1))
hist(datos$retorno, breaks = 40, col = CLARO, border = "white",
main = "Retorno diario (%)", xlab = "")
hist(datos$volatilidad, breaks = 40, col = CLARO, border = "white",
main = "Volatilidad 21d (%)", xlab = "")
hist(datos$carry, breaks = 40, col = CLARO, border = "white",
main = "Carry de política (pp)", xlab = "")Decisión sobre los indicadores.
indic <- t(sapply(vars, function(v) {
x <- datos[[v]]; x <- x[!is.na(x)]
cv <- 100 * sd(x) / abs(mean(x))
# El coeficiente de variación solo es interpretable si la media se aleja de cero.
if (abs(mean(x)) < 0.1 * sd(x)) cv <- NA
c(n = length(x), Mínimo = min(x), Q1 = unname(quantile(x, .25)), Mediana = median(x),
Q3 = unname(quantile(x, .75)), Máximo = max(x), Media = mean(x),
`Desv. est.` = sd(x), RIC = IQR(x), `CV %` = cv)
}))
kable(round(indic, 3), caption = "Indicadores de posición, tendencia central y dispersión")| n | Mínimo | Q1 | Mediana | Q3 | Máximo | Media | Desv. est. | RIC | CV % | |
|---|---|---|---|---|---|---|---|---|---|---|
| retorno | 3007 | -6.967 | -0.468 | 0.000 | 0.464 | 9.048 | 0.009 | 0.869 | 0.932 | NA |
| volatilidad | 2987 | 4.468 | 9.511 | 11.734 | 15.246 | 53.842 | 12.751 | 5.198 | 5.735 | 40.768 |
| carry | 3008 | 1.650 | 2.550 | 4.920 | 6.630 | 8.420 | 4.695 | 2.135 | 4.080 | 45.474 |
Nota sobre el coeficiente de variación. Este indicador expresa la dispersión como proporción de la media, de modo que pierde sentido cuando la media se acerca a cero: el cociente se dispara sin que ello refleje mayor variabilidad. Es exactamente lo que ocurre con el retorno diario, cuya media es de 0,009 % frente a una desviación estándar de 0,869 %. Por esa razón se reporta como no disponible en esa fila y la dispersión se lee mediante la desviación estándar y el rango intercuartílico. En la volatilidad y en el carry, cuyas medias están lejos de cero, el coeficiente sí es informativo.
Esta es la tarea central del caso. Se contrastan los indicadores de las variables cuantitativas entre las categorías de las dos variables de agrupación.
db <- describeBy(datos[, c("retorno","volatilidad","carry")],
group = datos$regimen, mat = TRUE, digits = 3)
tab <- db[, c("group1","n","mean","median","sd","min","max","skew","kurtosis")]
names(tab) <- c("Régimen","n","Media","Mediana","Desv. est.","Mínimo","Máximo",
"Asimetría","Curtosis")
tab$Variable <- sub("[0-9]+$", "", rownames(db))
kable(tab[, c("Variable", names(tab)[1:9])], row.names = FALSE,
caption = "Indicadores por régimen de mercado (psych::describeBy)")| Variable | Régimen | n | Media | Mediana | Desv. est. | Mínimo | Máximo | Asimetría | Curtosis |
|---|---|---|---|---|---|---|---|---|---|
| retorno | Normal | 1849 | 0.008 | 0.000 | 0.748 | -3.183 | 3.332 | 0.062 | 1.308 |
| retorno | Estrés local | 375 | -0.095 | -0.147 | 0.954 | -2.813 | 3.285 | 0.376 | 0.744 |
| retorno | Aversión al riesgo | 748 | 0.067 | 0.027 | 1.065 | -6.967 | 9.048 | 0.862 | 10.705 |
| volatilidad | Normal | 1831 | 11.382 | 10.918 | 3.474 | 4.618 | 24.014 | 0.621 | 0.095 |
| volatilidad | Estrés local | 375 | 14.669 | 14.982 | 3.645 | 5.491 | 21.730 | -0.206 | -0.944 |
| volatilidad | Aversión al riesgo | 746 | 14.891 | 12.901 | 7.660 | 4.468 | 53.842 | 2.928 | 11.230 |
| carry | Normal | 1850 | 4.470 | 4.380 | 2.072 | 1.650 | 8.380 | 0.169 | -1.276 |
| carry | Estrés local | 375 | 6.801 | 7.170 | 1.340 | 2.920 | 8.420 | -0.437 | -1.230 |
| carry | Aversión al riesgo | 748 | 4.117 | 3.920 | 1.978 | 1.650 | 8.180 | 0.301 | -1.148 |
par(mfrow = c(1, 2), mar = c(7, 4, 3, 1))
boxplot(retorno ~ regimen, data = datos, col = c(VERDE, ORO, ROJO), las = 2,
main = "Retorno diario por régimen", ylab = "%", xlab = "", cex.axis = .75)
boxplot(volatilidad ~ regimen, data = datos, col = c(VERDE, ORO, ROJO), las = 2,
main = "Volatilidad por régimen", ylab = "%", xlab = "", cex.axis = .75)Interpretación. El contraste entre grupos revela el patrón más importante de todo el caso.
La media del retorno se mantiene cerca de cero en los tres regímenes, de modo que el estado del mercado no anticipa la dirección del movimiento. Sin embargo, la desviación estándar crece de forma sostenida al pasar de un régimen a otro: alrededor de 0,75 % en días normales, 0,95 % en estrés local y 1,07 % en aversión al riesgo. Es decir, el régimen no cambia hacia dónde se mueve el precio, pero sí cuánto se mueve.
La curtosis refuerza la lectura: en los días de aversión al riesgo alcanza un valor extraordinariamente alto, muy por encima del que se observa en los demás regímenes. Los episodios verdaderamente extremos se concentran en ese grupo.
En la volatilidad realizada la diferencia es de nivel: los regímenes de tensión superan claramente al régimen normal. Conviene notar que en aversión al riesgo la media supera con holgura a la mediana, señal de que ese grupo contiene los valores más extremos de toda la muestra.
El carry de política también distingue los regímenes, aunque por una razón de naturaleza distinta. Su mediana alcanza 7,17 puntos porcentuales en los días de estrés local, frente a 4,38 en los días normales y 3,92 en aversión al riesgo. No refleja una reacción del mercado sino la coincidencia con etapas de tasas de intervención elevadas: los episodios de tensión propia de Colombia se concentran en periodos de política monetaria restrictiva. Este contraste es útil porque muestra que las dos agrupaciones del caso no son redundantes: el régimen captura tensión financiera y el periodo captura la etapa del ciclo, y ambas dimensiones se cruzan.
db2 <- describeBy(datos[, c("retorno","volatilidad","carry")],
group = datos$periodo, mat = TRUE, digits = 3)
tab2 <- db2[, c("group1","n","mean","median","sd","min","max","skew","kurtosis")]
names(tab2) <- c("Periodo","n","Media","Mediana","Desv. est.","Mínimo","Máximo",
"Asimetría","Curtosis")
tab2$Variable <- sub("[0-9]+$", "", rownames(db2))
kable(tab2[, c("Variable", names(tab2)[1:9])], row.names = FALSE,
caption = "Indicadores por periodo (psych::describeBy)")| Variable | Periodo | n | Media | Mediana | Desv. est. | Mínimo | Máximo | Asimetría | Curtosis |
|---|---|---|---|---|---|---|---|---|---|
| retorno | 2015-2019 Pre-pandemia | 1298 | 0.025 | 0.011 | 0.858 | -3.439 | 3.332 | 0.027 | 1.211 |
| retorno | 2020-2021 Pandemia | 518 | 0.041 | 0.026 | 1.004 | -6.967 | 9.048 | 1.186 | 19.183 |
| retorno | 2022-2026 Ciclo de alzas | 1191 | -0.021 | -0.052 | 0.816 | -2.944 | 4.820 | 0.443 | 1.812 |
| volatilidad | 2015-2019 Pre-pandemia | 1278 | 12.917 | 12.115 | 4.319 | 5.072 | 24.332 | 0.446 | -0.620 |
| volatilidad | 2020-2021 Pandemia | 518 | 13.351 | 11.466 | 8.707 | 4.468 | 53.842 | 3.264 | 11.381 |
| volatilidad | 2022-2026 Ciclo de alzas | 1191 | 12.312 | 11.562 | 3.804 | 4.719 | 23.999 | 0.630 | -0.140 |
| carry | 2015-2019 Pre-pandemia | 1299 | 4.196 | 4.360 | 1.830 | 1.800 | 7.460 | 0.284 | -1.202 |
| carry | 2020-2021 Pandemia | 518 | 2.163 | 1.695 | 0.645 | 1.650 | 4.150 | 1.214 | 0.607 |
| carry | 2022-2026 Ciclo de alzas | 1191 | 6.342 | 6.420 | 1.381 | 2.920 | 8.420 | -0.191 | -1.028 |
par(mfrow = c(1, 2), mar = c(8, 4, 3, 1))
boxplot(volatilidad ~ periodo, data = datos, col = AZUL, las = 2,
main = "Volatilidad por periodo", ylab = "%", xlab = "", cex.axis = .7)
boxplot(carry ~ periodo, data = datos, col = ORO, las = 2,
main = "Carry de política por periodo", ylab = "pp", xlab = "", cex.axis = .7)Interpretación. El carry de política es la variable que mejor separa los periodos, y su trayectoria coincide con la historia monetaria conocida: alrededor de 4 puntos porcentuales antes de la pandemia, cerca de 2 durante el episodio de tasas bajas, y por encima de 6 en el ciclo de alzas posterior. La comparación entre grupos recupera con nitidez un hecho económico documentado.
En la volatilidad el contraste es más sutil e ilustra por qué la elección del indicador importa. Las medianas de los tres periodos son bastante parecidas, entre 11,5 % y 12,1 %, de modo que el comportamiento típico apenas cambió. Lo que sí cambió de forma drástica es la dispersión: durante la pandemia la desviación estándar más que duplica la de los otros dos periodos, y la curtosis del retorno pasa de valores cercanos a 1,2 y 1,8 en los periodos extremos a 19,2 durante la pandemia. Si el análisis se hubiera limitado a comparar medianas, ese episodio habría pasado inadvertido.
| Criterio | Separa por régimen | Separa por periodo |
|---|---|---|
| Media del retorno | No | No |
| Desv. est. del retorno | Sí, de forma marcada | Moderadamente |
| Curtosis del retorno | Sí, de forma marcada | Sí, de forma marcada |
| Volatilidad (mediana) | Sí | Poco |
| Carry (mediana) | Sí (máximo en estrés local) | Sí, de forma marcada |
Primero: la dirección no depende del grupo. En ninguna de las dos agrupaciones la media del retorno se aparta de manera apreciable de cero. Ni el estado del mercado ni la etapa macroeconómica ayudan a anticipar si el peso se apreciará o se depreciará mañana. Esto es coherente con la distribución casi equilibrada de la variable de dirección.
Segundo: la magnitud sí depende del grupo, y de manera sistemática. La desviación estándar del retorno crece de forma ordenada del régimen normal al de aversión al riesgo. El estado del mercado no informa sobre el signo, pero sí sobre la escala del movimiento.
Tercero: los extremos se concentran. La curtosis del retorno en los días de aversión al riesgo es varias veces mayor que en los demás grupos, y la volatilidad de ese mismo grupo muestra una media muy superior a su mediana. Los movimientos verdaderamente atípicos no están repartidos de forma uniforme en el tiempo.
Cuarto: la elección del indicador cambia la conclusión. En la comparación por periodo, las medianas de volatilidad sugieren estabilidad mientras que las desviaciones estándar revelan un episodio extraordinario. Describir una distribución asimétrica únicamente con su medida de centro oculta justamente lo que interesa.
El objetivo del proyecto es estimar un rango de negociación. Los resultados sostienen dos decisiones de diseño:
El centro del rango no debe intentar anticipar la dirección. Ningún grupo muestra una media de retorno distinguible de cero, de modo que un ancla de mercado resulta preferible a un pronóstico direccional.
El ancho del rango debe ser condicional. Como la dispersión difiere de manera clara entre regímenes, una banda de amplitud fija sería demasiado ancha en los días normales y demasiado estrecha justamente cuando el mercado se tensiona, que es cuando el error resulta más costoso.
El análisis es descriptivo y exploratorio. Las diferencias observadas entre grupos no autorizan conclusiones causales: no se afirma que el régimen provoque mayor volatilidad, sino que ambos fenómenos se observan de manera conjunta. Las variables de agrupación son construidas a partir de umbrales muestrales, de modo que su definición depende del periodo analizado y otra ventana temporal produciría cortes distintos. Finalmente, los tamaños de los grupos son desiguales, lo que exige prudencia al comparar indicadores sensibles a valores extremos.
Base documentada en la Actividad 1.1.2. Fuentes: LSEG Workspace y
Banco de la República. Procesamiento en R con los paquetes
psych, summarytools y
DT.