2 Metodologia

2.1 Serie de tiempo

Una serie de tiempo es una secuencia de observaciones de una variable específica registradas en intervalos de tiempo regulares y ordenadas cronológicamente (Box & Jenkins, 1970) Estas observaciones pueden estar espaciadas en forma mensual, trimestral, semanal o diaria, dependiendo del fenómeno bajo estudio. En el contexto financiero y económico, las series de tiempo permiten analizar el comportamiento histórico de variables como precios de acciones, índices bursátiles, tipos de cambio e indicadores macroeconómicos, con el objetivo de identificar patrones subyacentes y realizar pronósticos sobre su evolución futura (DataCamp, 2024).

El análisis de series temporales se fundamenta en el estudio de diferentes componentes que caracterizan el comportamiento de los datos a través del tiempo. La tendencia representa el patrón de largo plazo que muestra la dirección general de la serie, ya sea creciente, decreciente o constante. La estacionalidad corresponde a fluctuaciones periódicas que se repiten en intervalos fijos de tiempo, como cambios mensuales o trimestrales provocados por factores cíclicos. El componente cíclico refleja oscilaciones de largo plazo que no tienen un período fijo y suelen estar relacionadas con ciclos económicos. Finalmente, el componente aleatorio o residual representa la variabilidad inexplicable que permanece después de eliminar los otros componentes.

La importancia del análisis de series de tiempo radica en su capacidad para extraer información representativa sobre las relaciones subyacentes entre los datos, permitiendo realizar predicciones sobre momentos no observados, ya sea en el futuro (pronósticos), en el pasado (extrapolación retrógrada) o en momentos intermedios (interpolación). En el ámbito financiero, estas predicciones son fundamentales para la toma de decisiones estratégicas, la gestión de riesgos y la planificación de inversiones (DataCamp, 2024).

2.2 Modelo ARIMA

El modelo ARIMA (AutoRegressive Integrated Moving Average) es una de las herramientas estadísticas más utilizadas y versátiles para el análisis y pronóstico de series temporales (DataCamp, 2024). Este modelo combina tres componentes fundamentales que trabajan de manera conjunta para capturar la estructura de dependencia temporal de los datos (DataCamp, 2024). El componente Autorregresivo (AR) establece que el valor actual de la serie puede ser explicado como una función lineal de sus valores pasados, es decir, la variable en el momento t depende de sus valores en momentos anteriores (DataCamp, 2024). El componente Integrado (I) se refiere al proceso de diferenciación aplicado a la serie para lograr estacionariedad, removiendo tendencias y componentes no estacionarios (DataCamp, 2024). El componente Media Móvil (MA) modela la dependencia entre una observación y los errores de pronóstico de observaciones pasadas (DataCamp, 2024).

La notación del modelo ARIMA se expresa como ARIMA(p,d,q), donde p representa el orden del componente autorregresivo en la ecuacion 1 vemos AR(p):

\[ y_t = c + \phi_1 y_{t-1} + \phi_2 y_{t-2} + \ldots + \phi_p y_{t-p} + \varepsilon_t \tag{1} \]

donde \(\varepsilon_t\) es ruido blanco.

d indica el número de diferenciaciones necesarias para alcanzar la estacionariedad, en la ecuacion 2 y 3 vemos dos ejemplos de diferenciacion \(d=1\) y \(d=2\):

Primera diferenciación (\(d = 1\)):

\[ y'_t = y_t - y_{t-1} \tag{2} \]

Segunda diferenciación (\(d = 2\)):

\[ y''_t = (y_t - y_{t-1}) - (y_{t-1} - y_{t-2}) = y_t - 2y_{t-1} + y_{t-2} \tag{3} \]

y q denota el orden del componente de medias móviles en la ecuacion 3 vemos el modelo MA(q) (DataCamp, 2024).

\[ y_t = c + \varepsilon_t + \theta_1 \varepsilon_{t-1} + \theta_2 \varepsilon_{t-2} + \ldots + \theta_q \varepsilon_{t-q} \tag{4} \]

y el modelo ARMA(\(p, q\)) general la vemos en la ecuación 5:

\[ y_t = c + \phi_1 y_{t-1} + \phi_2 y_{t-2} + \cdots + \phi_p y_{t-p} + \varepsilon_t + \theta_1 \varepsilon_{t-1} + \theta_2 \varepsilon_{t-2} + \cdots + \theta_q \varepsilon_{t-q} \tag{5} \]

Un modelo ARIMA(3,1,0), por ejemplo, indica que se utiliza un proceso autorregresivo de orden 3, con una diferenciación aplicada y sin componente de medias móviles (Otexts, s.f.).

La importancia de los modelos ARIMA en la predicción de series temporales radica en múltiples aspectos (DataCamp, 2024). Primero, proporcionan un marco teórico sólido basado en la teoría de procesos estocásticos que permite describir las autocorrelaciones presentes en los datos (Otexts, s.f.). Segundo, son especialmente efectivos para capturar patrones complejos de dependencia temporal, incluyendo inercias y efectos de retardo que caracterizan muchas variables económicas y financieras (DataCamp, 2024). Tercero, ofrecen flexibilidad para adaptarse a diferentes tipos de series mediante la selección apropiada de sus parámetros (DataCamp, 2024).

En el contexto financiero, los modelos ARIMA han demostrado ser herramientas valiosas para predecir precios de acciones, índices bursátiles, tipos de cambio y rendimientos de portafolios (DataCamp, 2024). Su capacidad para manejar series temporales que exhiben autocorrelación compleja y para incorporar la diferenciación cuando existe no estacionariedad los hace ideales para el análisis de mercados financieros caracterizados por alta volatilidad (DataCamp, 2024).

2.3 Metodología de Box-Jenkins

La estimación de modelos ARIMA se realiza tradicionalmente mediante la metodología de Box-Jenkins, un procedimiento sistemático e iterativo que consta de cuatro etapas fundamentales (Box & Jenkins, 1970). Esta metodología, desarrollada por los estadísticos George E.P. Box y Gwilym Jenkins, proporciona un enfoque estructurado para identificar, estimar y validar el modelo más apropiado para una serie temporal específica .

2.3.1 Primera Etapa: Identificación del Modelo

La primera etapa corresponde a la identificación del modelo . En esta fase inicial se debe asegurar que la serie sea estacionaria, es decir, que sus propiedades estadísticas como la media, varianza y autocorrelación se mantengan constantes a través del tiempo (NumXL, 2024). Para verificar la estacionariedad, se aplica la prueba de Dickey-Fuller Aumentada (ADF), cuya hipótesis nula establece que la serie posee una raíz unitaria y por tanto no es estacionaria (NumXL, 2024).

La prueba ADF formula la siguiente hipótesis:

  • \(H₀\): La serie tiene raíz unitaria (no es estacionaria)
  • \(H₁\): La serie no tiene raíz unitaria (es estacionaria)

Si la prueba ADF arroja un p-valor inferior al nivel de significancia (típicamente 0.05), se rechaza la hipótesis nula y se concluye que la serie es estacionaria (NumXL, 2024).

Cuando la serie original no es estacionaria, se aplica el proceso de diferenciación. La diferenciación de primer orden elimina tendencias lineales, mientras que diferenciaciones de orden superior pueden remover tendencias polinomiales más complejas . Para series con componente estacional, se puede aplicar diferenciación estacional además de la regular . Una vez alcanzada la estacionariedad, se procede a identificar los órdenes p y q del modelo mediante el análisis de las funciones de autocorrelación (ACF) y autocorrelación parcial (PACF) .

La función de autocorrelación (ACF) mide la correlación entre las observaciones de una serie temporal separadas por k períodos, proporcionando información sobre la estructura de dependencia temporal . La función de autocorrelación parcial (PACF) mide la correlación entre observaciones separadas por k períodos después de ajustar por la influencia de los rezagos intermedios . En términos prácticos, patrones específicos en la ACF y PACF sugieren diferentes especificaciones del modelo :

  • Un decaimiento exponencial en la ACF junto con cortes abruptos en la PACF sugiere un modelo AR
  • El patrón inverso (cortes abruptos en ACF y decaimiento en PACF) indica un modelo MA
  • Patrones decrecientes en ambas funciones indican un modelo ARMA

2.3.2 Segunda Etapa: Estimación de Parámetros

se calculan criterios de información que permiten comparar la calidad de ajuste de múltiples especificaciones de modelos de forma consistente y penalizando sistemáticamente la complejidad innecesaria. El Criterio de Información de Akaike (AIC) constituye una herramienta fundamental en la selección de modelos, ya que balancea dos objetivos frecuentemente en conflicto: lograr un buen ajuste a los datos observados y mantener la parsimonia del modelo. El AIC se calcula considerando el valor máximo de la función de verosimilitud logarítmica y añadiendo una penalidad que es función del número total de parámetros estimados. Esta estructura incentiva la inclusión de variables y parámetros únicamente cuando contribuyen de forma sustancial a mejorar el ajuste, evitando el sobreajuste que resultaría de incluir complejidad innecesaria. Al comparar modelos competidores, aquel que presenta un valor de AIC más bajo es preferido bajo este criterio, puesto que minimiza una combinación ponderada del error de predicción y la complejidad del modelo (Akaike, 1974; Burnham & Anderson, 2002). El AIC es particularmente útil en contextos con muestras de tamaño moderado a grande, aunque tiende a favorecer modelos ligeramente más complejos que especificaciones alternativas más simples.

El Criterio de Información Bayesiano (BIC), también conocido como Criterio de Schwarz, introduce una penalidad por complejidad más severa que el AIC, especialmente relevante cuando el tamaño de la muestra es grande. A diferencia del AIC, el BIC multiplica la penalidad por el número de parámetros por el logaritmo natural del tamaño muestral, lo que significa que, conforme el número de observaciones crece, la penalidad por incluir parámetros adicionales aumenta progresivamente. Por esta razón, el BIC tiende a favorecer modelos más parsimonios que el AIC, seleccionando especificaciones con menos parámetros cuando existen múltiples alternativas disponibles (Schwarz, 1978; Burnham & Anderson, 2002). Esta característica hace que el BIC sea especialmente útil en problemas de selección de modelos donde existe la preocupación de que especificaciones más complejas puedan conducir a sobreajuste y reducir la capacidad de generalización en datos futuros. Al igual que con el AIC, un valor de BIC menor indica mejor desempeño, y en problemas de comparación entre modelos competidores, se prefiere aquel con BIC mínimo.

2.3.3 Tercera Etapa: Diagnóstico y Validación del Modelo

La tercera etapa corresponde al diagnóstico y validación del modelo . Esta fase crítica determina si el modelo estimado es adecuado para describir los datos y realizar pronósticos . La validación involucra varios aspectos fundamentales :

Primero, se verifica la significancia estadística de los parámetros estimados mediante pruebas t, asegurando que todos los coeficientes sean estadísticamente diferentes de cero . Segundo, se comprueba que los parámetros cumplan las condiciones de estacionariedad para la parte AR y de invertibilidad para la parte MA .

El análisis de los residuales constituye un elemento central en la validación del modelo . Los residuales deben comportarse como ruido blanco, es decir, deben ser independientes entre sí, tener media cero, varianza constante y seguir una distribución normal . Para verificar la independencia de los residuales se aplica la prueba de Ljung-Box, que evalúa de forma conjunta si existe autocorrelación significativa en los residuos.

  • Hipótesis nula (\(H_0\)):
    Los residuos son independientes y no están correlacionados (es decir, no hay autocorrelación serial).

\[ H_0: \rho_1 = \rho_2 = \dots = \rho_k = 0 \]

  • Hipótesis alternativa (\(H_1\)):
    Los residuos exhiben autocorrelación (al menos una \(\rho_j \neq 0\)).

\[ H_1: \exists\, j \in \{1, \dots, k\} \mid \rho_j \neq 0 \]

donde \(\rho_j\) es el coeficiente de autocorrelación de los residuos en el retardo \(j\).

Si el p-valor de la prueba Ljung-Box es mayor al nivel de significancia (típicamente 0.05), no se rechaza la hipótesis nula y se concluye que los residuos son ruido blanco, validando la adecuación del modelo.

Para comparar modelos alternativos y seleccionar el más apropiado, se utilizan criterios de información como el AIC (Criterio de Información de Akaike), AICc (AIC corregido) y BIC (Criterio de Información Bayesiano) . Estos criterios evalúan la bondad de ajuste del modelo considerando tanto la verosimilitud de los datos como la complejidad del modelo, aplicando una penalización por el número de parámetros para evitar el sobreajuste . El modelo óptimo es aquel que minimiza el valor del criterio de información seleccionado . El AICc es preferible cuando el tamaño de la muestra es pequeño en relación con el número de parámetros, mientras que el BIC tiende a seleccionar modelos más parsimoniosos .

2.3.4 Cuarta Etapa: Predicción o Pronóstico

La cuarta etapa es la predicción o pronóstico . Una vez que se ha identificado, estimado y validado un modelo apropiado, se utiliza para generar pronósticos de valores futuros de la serie . Los modelos ARIMA proporcionan pronósticos puntuales junto con intervalos de confianza que cuantifican la incertidumbre asociada a las predicciones (DataCamp, 2024). La amplitud de estos intervalos de confianza aumenta conforme se incrementa el horizonte de pronóstico, reflejando la mayor incertidumbre sobre valores más distantes en el futuro (DataCamp, 2024).

Esta metodología iterativa permite refinar sucesivamente el modelo hasta obtener una especificación que capture adecuadamente la estructura de dependencia temporal de los datos, proporcione residuales que satisfagan los supuestos requeridos y genere pronósticos confiables para la toma de decisiones .

3 Descripción de la serie temporal

3.1 Analisis financiero

El iCOLCAP representa el primer y más significativo Exchange Traded Fund (ETF) de acciones colombianas a nivel mundial, lanzado al mercado el 6 de julio de 2011 por BlackRock (BlackRock, 2025). La administración profesional del fondo está a cargo de BlackRock Fund Advisors (BFA), mientras que la sociedad administradora designada es Citivalores S.A. Comisionista de Bolsa, asegurando una gestión robusta y conforme a la regulación colombiana (Citivalores S.A., 2024).

En cuanto a sus características técnicas fundamentales, el iCOLCAP opera bajo el nemotécnico ICOLCAP con código ISIN CORB6PA00015 (Citivalores S.A., 2025). El fondo mantiene aproximadamente $7.67 billones de pesos colombianos en activos bajo gestión, distribuidos en 402,160,000 unidades en circulación (TradingView, 2025; Citivalores S.A., 2025). La comisión de gestión asciende al 0.48% anual, mientras que el porcentaje de administración se establece en 0.44% (BlackRock, 2025; BlackRock, s.f.). Estos costos operativos son factores determinantes en el tracking error respecto al índice de referencia.

library(xts)
library(plotly)
library(TTR)
library(readxl)
library(dplyr)

Base_datos <- read_excel("ICOLCAP.xlsx") %>%
  rename(Fecha = 1, Cierre = 2)

Serie <- xts(Base_datos$Cierre, order.by = Base_datos$Fecha)
names(Serie) <- "Cierre"  

accion <- Serie %>% na.omit()


accion$SMA_20 <- SMA(accion$Cierre, n = 20)
accion$SMA_50 <- SMA(accion$Cierre, n = 50)


max_valor <- max(accion$Cierre, na.rm = TRUE)
min_valor <- min(accion$Cierre, na.rm = TRUE)
max_fecha <- index(accion)[which.max(accion$Cierre)]
min_fecha <- index(accion)[which.min(accion$Cierre)]

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

fig <- plot_ly() %>%
  add_lines(
    x = index(accion),
    y = accion$Cierre,
    name = "BRENT",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>COLCAP</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}/barril<br>",
      "<extra></extra>"
    )
  )

accion_df <- data.frame(
  Fecha = index(accion),
  Cierre = coredata(accion$Cierre),
  SMA_20 = coredata(accion$SMA_20),
  SMA_50 = coredata(accion$SMA_50)
)

fig <- plot_ly(accion_df, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "COLCAP",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>PETRÓLEO BRENT</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}/barril<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              MSCI COLCAP (2015-2025)</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Crisis del petróleo: caída histórica de precios</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO (USD/BARRIL)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.2f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(index(accion)), "%d %b %Y"), 
          " - ", format(max(index(accion)), "%d %b %Y"), " | ",
          "Máximo: $", round(max_valor, 2), "/bbl | ",
          "Mínimo: $", round(min_valor, 2), "/bbl | ",
          "Variación: ", sprintf("%+.1f%%", (tail(accion$Cierre, 1) - head(accion$Cierre, 1)) / head(accion$Cierre, 1) * 100)
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig
accion <- Serie %>% na.omit()

La composición sectorial del portafolio revela una concentración significativa en el sector financiero, que representa el 54.35% de los activos totales, seguido por servicios públicos (utilities) con 19.45%, materiales con 14.25%, energía con 8.87%, inmobiliario con 1.83%, liquidez con 0.77%, consumo discrecional con 0.49% y comunicación con 0.01% (BlackRock, 2025). Esta distribución sectorial evidencia una fuerte dependencia del desempeño del sistema financiero colombiano, factor crítico para el análisis de riesgo y diversificación.

library(readr)
library(dplyr)
library(plotly)
library(stringr)

holdings <- read_csv("ICOLCAP_holdings.csv", skip = 2, locale = locale(encoding = "UTF-8", decimal_mark = ",", grouping_mark = "."))


names(holdings) <- c("Ticker", "Name", "Sector", "Asset_Class", "Market_Value", 
                     "Weight", "Notional_Value", "Shares", "Price", "Location", 
                     "Exchange", "Currency", "FX_Rate", "Market_Currency")

holdings <- holdings %>%
  filter(!is.na(Ticker) & Ticker != "" & Ticker != "-") %>%
  mutate(
    Market_Value = as.numeric(gsub("\\.", "", Market_Value)),
    Weight = as.numeric(gsub(",", ".", Weight)),
    Sector = case_when(
      grepl("Financiero", Sector) ~ "FINANCIALS",
      grepl("Público", Sector) ~ "UTILITIES",
      grepl("Energía", Sector) ~ "ENERGY",
      grepl("Material", Sector) ~ "BASIC MATERIALS",
      grepl("Inmobiliario", Sector) ~ "REAL ESTATE",
      grepl("Efectivo", Sector) ~ "CASH",
      grepl("Consumo Frecuente", Sector) ~ "CONSUMER STAPLES",
      grepl("Consumo discrecional", Sector) ~ "CONSUMER DISCRETIONARY",
      grepl("Comunicación", Sector) ~ "COMMUNICATION",
      grepl("Tecnología", Sector) ~ "TECHNOLOGY",
      grepl("Otro", Sector) ~ "OTHER",
      TRUE ~ Sector
    )
  ) %>%
  filter(!is.na(Weight) & Weight > 0)

css_colors <- list(
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37",
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  warm_brown = "#8B4513",
  elegant_brown = "#A0522D",
  rojo_oscuro = "#8B0000",
  verde_oscuro = "#006400",
  azul_oscuro = "#00008B"
)




sector_distribution <- holdings %>%
  group_by(Sector) %>%
  summarise(
    Total_Weight = sum(Weight, na.rm = TRUE),
    Companies = n(),
    Avg_Weight = mean(Weight, na.rm = TRUE),
    Max_Weight = max(Weight, na.rm = TRUE),
    Min_Weight = min(Weight[Weight > 0], na.rm = TRUE),
    Total_Value = sum(Market_Value, na.rm = TRUE)
  ) %>%
  mutate(
    Percentage = Total_Weight / sum(Total_Weight) * 100
  ) %>%
  arrange(desc(Total_Weight))

sector_colors <- c(
  "FINANCIALS" = css_colors$oro_premium,
  "UTILITIES" = css_colors$oro_lujo,
  "ENERGY" = css_colors$oro_antiguo,
  "BASIC MATERIALS" = css_colors$warm_brown,
  "REAL ESTATE" = css_colors$elegant_brown,
  "CASH" = css_colors$grilla_oscura,
  "CONSUMER STAPLES" = css_colors$rojo_oscuro,
  "CONSUMER DISCRETIONARY" = css_colors$verde_oscuro,
  "COMMUNICATION" = css_colors$azul_oscuro,
  "OTHER" = "#4A4A4A"
)

fig_sector_pie <- plot_ly(
  sector_distribution,
  labels = ~Sector,
  values = ~Total_Weight,
  type = 'pie',
  marker = list(
    colors = sector_colors[as.character(sector_distribution$Sector)],
    line = list(color = css_colors$fondo_negro, width = 2)
  ),
  textinfo = 'label+percent',
  textposition = 'inside',
  insidetextorientation = 'radial',
  hole = 0.4,
  hovertemplate = paste(
    "<b style='font-family:Cinzel; color:#D4AF37'>%{label}</b><br>",
    "Peso total: <b>%{value:.2f}%</b><br>",
    "Empresas: %{customdata[0]}<br>",
    "Valor total: $%{customdata[1]:,.0f}<br>",
    "<extra></extra>"
  ),
  customdata = matrix(c(
    sector_distribution$Companies,
    sector_distribution$Total_Value
  ), ncol = 2),
  pull = ifelse(sector_distribution$Sector == "FINANCIALS", 0.2, 0)
) %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; font-size:28px; color:#D4AF37;'>
              ✦ ICOLCAP - DISTRIBUCIÓN POR SECTORES ✦</span><br>
              <span style='font-family:Playfair Display; font-size:16px; color:#FFF8DC;'>
              Composición del índice colombiano</span>",
      x = 0.5,
      y = 0.95,
      xanchor = 'center',
      yanchor = 'top'
    ),
    
    showlegend = TRUE,
    legend = list(
      orientation = "v",
      x = 1.1,
      y = 0.5,
      font = list(
        family = 'Cinzel',
        size = 12,
        color = css_colors$texto_oro
      ),
      bgcolor = 'rgba(26, 26, 26, 0.7)',
      bordercolor = css_colors$oro_premium,
      borderwidth = 1,
      title = list(text = "<b>SECTORES</b>")
    ),
    
    paper_bgcolor = css_colors$fondo_negro,
    plot_bgcolor = css_colors$panel_negro,
    
    annotations = list(
      list(
        text = paste0("<b>", round(sum(sector_distribution$Total_Weight), 1), "%</b><br>TOTAL"),
        x = 0.5,
        y = 0.5,
        font = list(
          family = 'Cinzel',
          size = 20,
          color = css_colors$oro_lujo
        ),
        showarrow = FALSE,
        bgcolor = 'rgba(26, 26, 26, 0.8)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 2,
        borderpad = 10,
        xref = 'paper',
        yref = 'paper'
      )
    ),
    
    margin = list(l = 50, r = 200, t = 100, b = 80)
  )

fig_sector_bars <- plot_ly(
  sector_distribution,
  x = ~Total_Weight,
  y = ~reorder(Sector, Total_Weight),
  type = 'bar',
  orientation = 'h',
  marker = list(
    color = sector_colors[as.character(sector_distribution$Sector)],
    line = list(color = css_colors$fondo_negro, width = 1)
  ),
  text = ~paste0(round(Total_Weight, 1), "%"),
  textposition = 'outside',
  hovertemplate = paste(
    "<b style='font-family:Cinzel; color:#D4AF37'>%{y}</b><br>",
    "Peso: <b>%{x:.2f}%</b><br>",
    "Empresas: %{customdata[0]}<br>",
    "Valor total: $%{customdata[1]:,.0f}<br>",
    "<extra></extra>"
  ),
  customdata = matrix(c(
    sector_distribution$Companies,
    sector_distribution$Total_Value
  ), ncol = 2)
) %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; font-size:24px; color:#D4AF37;'>
              ✦ PESO POR SECTOR - ICOLCAP ✦</span>",
      x = 0.5
    ),
    
    xaxis = list(
      title = list(
        text = "<b>PESO EN EL ÍNDICE (%)</b>",
        font = list(family = 'Cinzel', color = css_colors$texto_oro)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(color = css_colors$texto_claro),
      zerolinecolor = css_colors$grilla_oscura,
      showgrid = TRUE,
      ticksuffix = "%"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>SECTOR</b>",
        font = list(family = 'Cinzel', color = css_colors$texto_oro)
      ),
      tickfont = list(color = css_colors$texto_claro, size = 12)
    ),
    
    paper_bgcolor = css_colors$fondo_negro,
    plot_bgcolor = css_colors$panel_negro,
    
    hoverlabel = list(
      bgcolor = css_colors$panel_negro,
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro)
    ),
    
    margin = list(l = 150, r = 50, t = 80, b = 50)
  )

top_15 <- holdings %>%
  arrange(desc(Weight)) %>%
  head(15) %>%
  mutate(
    Display_Name = paste0(Ticker, " - ", str_trunc(Name, 25))
  )

company_colors <- colorRampPalette(c(css_colors$warm_brown, css_colors$oro_antiguo, css_colors$oro_lujo))(nrow(top_15))

fig_top_15 <- plot_ly(
  top_15,
  x = ~Weight,
  y = ~reorder(Display_Name, Weight),
  type = 'bar',
  orientation = 'h',
  marker = list(
    color = company_colors,
    line = list(color = css_colors$fondo_negro, width = 1),
    gradient = list(
      type = "vertical",
      color = company_colors
    )
  ),
  text = ~paste0(round(Weight, 1), "%"),
  textposition = 'outside',
  hovertemplate = paste(
    "<b style='font-family:Cinzel; color:#D4AF37'>%{customdata[0]}</b><br>",
    "Nombre: %{customdata[1]}<br>",
    "Sector: <b>%{customdata[2]}</b><br>",
    "Peso: <b>%{x:.2f}%</b><br>",
    "Valor: $%{customdata[3]:,.0f}<br>",
    "Precio: $%{customdata[4]:,.0f}<br>",
    "<extra></extra>"
  ),
  customdata = matrix(c(
    top_15$Ticker,
    top_15$Name,
    top_15$Sector,
    top_15$Market_Value,
    top_15$Price
  ), ncol = 5)
) %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; font-size:24px; color:#D4AF37;'>
              ✦ TOP 15 HOLDINGS - ICOLCAP ✦</span><br>
              <span style='font-family:Playfair Display; font-size:14px; color:#FFF8DC;'>
              Principales posiciones por peso en el índice</span>",
      x = 0.5,
      y = 0.95
    ),
    
    xaxis = list(
      title = list(
        text = "<b>PESO EN EL ÍNDICE (%)</b>",
        font = list(family = 'Cinzel', color = css_colors$texto_oro)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(color = css_colors$texto_claro),
      zerolinecolor = css_colors$grilla_oscura,
      showgrid = TRUE,
      ticksuffix = "%",
      range = c(0, max(top_15$Weight) * 1.1)
    ),
    
    yaxis = list(
      title = list(
        text = "<b>EMPRESA</b>",
        font = list(family = 'Cinzel', color = css_colors$texto_oro)
      ),
      tickfont = list(color = css_colors$texto_claro, size = 10)
    ),
    
    paper_bgcolor = css_colors$fondo_negro,
    plot_bgcolor = css_colors$panel_negro,
    
    hoverlabel = list(
      bgcolor = css_colors$panel_negro,
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 250, r = 50, t = 100, b = 50),
    
    annotations = list(
      list(
        x = 0.5,
        y = -0.15,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Top 15 representa",
          round(sum(top_15$Weight), 1),
          "% del índice | Total holdings:",
          nrow(holdings)
        ),
        showarrow = FALSE,
        font = list(
          family = 'Inter',
          size = 12,
          color = css_colors$texto_claro
        ),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig_treemap <- plot_ly(
  type = "treemap",
  labels = holdings$Name,
  parents = rep("ICOLCAP", nrow(holdings)),
  values = holdings$Weight,
  text = holdings$Ticker,
  textinfo = "label+text+value",
  texttemplate = "<b>%{text}</b><br>%{label}<br>%{value:.1f}%",
  hovertemplate = paste(
    "<b>%{label}</b><br>",
    "Ticker: %{text}<br>",
    "Sector: %{customdata[0]}<br>",
    "Peso: %{value:.2f}%<br>",
    "Valor: $%{customdata[1]:,.0f}<br>",
    "<extra></extra>"
  ),
  customdata = matrix(c(
    holdings$Sector,
    holdings$Market_Value
  ), ncol = 2),
  marker = list(
    colorscale = list(
      list(0, css_colors$warm_brown),
      list(0.3, css_colors$oro_antiguo),
      list(0.6, css_colors$oro_premium),
      list(1, css_colors$oro_lujo)
    ),
    line = list(color = css_colors$fondo_negro, width = 1),
    showscale = FALSE
  ),
  pathbar = list(visible = TRUE),
  branchvalues = "total"
) %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; font-size:24px; color:#D4AF37;'>
              ✦ MAPA DE HOLDINGS - ICOLCAP ✦</span><br>
              <span style='font-family:Playfair Display; font-size:14px; color:#FFF8DC;'>
              Visualización jerárquica por tamaño de participación</span>",
      x = 0.5,
      y = 0.98
    ),
    paper_bgcolor = css_colors$fondo_negro,
    margin = list(l = 20, r = 20, t = 100, b = 20)
  )

concentration_groups <- data.frame(
  Group = c("Top 5", "Top 10", "Resto"),
  Weight = c(
    sum(head(holdings$Weight, 5)),
    sum(head(holdings$Weight, 10)),
    sum(tail(holdings$Weight, -10))
  ),
  Companies = c(5, 5, nrow(holdings) - 10)
)

fig_concentration <- plot_ly(
  concentration_groups,
  x = ~Group,
  y = ~Weight,
  type = 'bar',
  marker = list(
    color = c(css_colors$oro_lujo, css_colors$oro_premium, css_colors$oro_antiguo),
    line = list(color = css_colors$fondo_negro, width = 2)
  ),
  text = ~paste0(round(Weight, 1), "%"),
  textposition = 'auto',
  hovertemplate = paste(
    "<b>%{x}</b><br>",
    "Peso: <b>%{y:.1f}%</b><br>",
    "Empresas: %{customdata}<br>",
    "<extra></extra>"
  ),
  customdata = concentration_groups$Companies
) %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; font-size:22px; color:#D4AF37;'>
              ✦ CONCENTRACIÓN DEL ÍNDICE ✦</span>",
      x = 0.5
    ),
    
    xaxis = list(
      title = list(
        text = "<b>GRUPO DE EMPRESAS</b>",
        font = list(family = 'Cinzel', color = css_colors$texto_oro)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(color = css_colors$texto_claro)
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PESO ACUMULADO (%)</b>",
        font = list(family = 'Cinzel', color = css_colors$texto_oro)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(color = css_colors$texto_claro),
      ticksuffix = "%"
    ),
    
    paper_bgcolor = css_colors$fondo_negro,
    plot_bgcolor = css_colors$panel_negro,
    
    annotations = list(
      list(
        x = 0.5,
        y = 0.9,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Top 5: ", round(concentration_groups$Weight[1], 1), "% | ",
          "Top 10: ", round(concentration_groups$Weight[2], 1), "% | ",
          "Resto: ", round(concentration_groups$Weight[3], 1), "%"
        ),
        showarrow = FALSE,
        font = list(family = 'Inter', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 5
      )
    ),
    
    margin = list(l = 80, r = 80, t = 100, b = 80)
  )


top_1 <- holdings[which.max(holdings$Weight), ]
top_5_weight <- sum(head(holdings$Weight, 5))
top_10_weight <- sum(head(holdings$Weight, 10))


fig_sector_pie       

Las principales posiciones del fondo incluyen a las empresas más representativas de la economía colombiana. Grupo Cibest Preferencial (anteriormente Bancolombia Pref) lidera con 21.25% de ponderación, seguido por Grupo Cibest S.A. con 14.05%, Interconexión Eléctrica S.A. (ISA) con 9.02%, Ecopetrol S.A. con 8.71%, Grupo Energía Bogotá S.A. con 7.59%, Cementos Argos S.A. con 6.01%, Grupo de Inversiones Suramericana con 5.41%, Inversiones Argos S.A. con 5.23%, Grupo de Inversiones Suramericana Pref con 5.12% e Inversiones Argos Preferencial S.A. con 2.81% (BlackRock, 2025). En conjunto, estas diez posiciones representan aproximadamente el 85.20% del portafolio total, indicando una concentración considerable en valores líderes del mercado.

fig_sector_bars      
fig_top_15          
fig_treemap        

Los rendimientos históricos del iCOLCAP demuestran un desempeño variable pero con tendencia de recuperación en períodos recientes. Hasta agosto de 2025, el fondo reporta un rendimiento anualizado de 46.65% en el último año, 24.97% en tres años, 15.80% en cinco años, 7.40% en diez años y 3.06% desde su creación (BlackRock, 2025). Los retornos por año calendario muestran una recuperación notable: 2024 registró +23.61%, 2023 +4.52%, mientras que 2022 y 2021 presentaron resultados negativos de -2.98% y -0.59% respectivamente, impactados principalmente por la crisis de COVID-19 en 2020 que generó una caída de -13.18% (BlackRock, 2025). Vale destacar que en octubre de 2024 el índice MSCI COLCAP alcanzó un máximo histórico de 1,977 puntos, superando los 2,000 puntos en diciembre de 2024 (Instituto Nacional de Contadores Públicos [INCP], 2024), y al 3 de diciembre de 2025 el iCOLCAP cotiza alrededor de $20,774 COP por unidad (La República, 2025).

Las métricas de valoración del fondo reflejan múltiplos relativamente atractivos, con un Precio/Utilidad (P/E) de 6.33 y un Precio/Valor en Libros (P/B) de 0.97 (BlackRock, 2025). Estos indicadores sugieren que las empresas del índice COLCAP se encuentran valoradas en niveles moderados comparados con mercados desarrollados, lo cual puede representar oportunidades de inversión para perfiles de largo plazo. En cuanto a aspectos de sostenibilidad, el fondo mantiene una calificación ESG de MSCI nivel A, con puntuación de calidad ESG de 6.89 sobre 10, cobertura ESG del 86.36% e intensidad media ponderada de carbono de 631.65 toneladas CO₂ por millón de dólares en ventas (BlackRock, 2025).

Desde la perspectiva de riesgo, el iCOLCAP está clasificado como producto de riesgo por la Superintendencia Financiera de Colombia (Citivalores S.A., 2024). Los principales riesgos incluyen el riesgo de mercado asociado a la volatilidad de precios de acciones subyacentes, influenciado por factores macroeconómicos nacionales e internacionales (Rojas Cuervo & Torres Cháux, 2023; Galarza Melo, 2021); riesgo de liquidez relacionado con la redención de participaciones en mercados con limitada profundidad (Bolsa de Valores de Colombia, s.f.); riesgo de concentración sectorial dado que el sector financiero supera el 54% del portafolio (BlackRock, 2025); y tracking error derivado de costos operacionales (BlackRock, 2025). Estudios académicos han confirmado que el índice COLCAP presenta memoria de largo plazo y alta persistencia de volatilidad, especialmente durante crisis como la pandemia de COVID-19 en 2020 (Rojas Cuervo & Torres Cháux, 2023; Valora Analitik, 2024).

library(gt)
library(dplyr)
library(scales)
library(webshot2)  
library(psych)
precios <- as.numeric(coredata(accion))  
fechas <- index(accion)

retornos <- diff(log(precios))

estadisticas_basicas <- data.frame(
  Estadística = c(
    "Número de observaciones",
    "Período de análisis",
    "Primera fecha",
    "Última fecha",
    "Precio inicial (COP)",
    "Precio final (COP)",
    "Cambio absoluto (COP)",
    "Cambio porcentual total (%)",
    "Media aritmética (COP)",
    "Mediana (COP)",
    "Mínimo histórico (COP)",
    "Máximo histórico (COP)",
    "Rango total (COP)",
    "Desviación estándar (COP)",
    "Varianza (COP²)",
    "Coeficiente de variación",
    "Asimetría (Skewness)",
    "Curtosis (Kurtosis)",
    "Prueba Jarque-Bera (p-value)"
  ),
  Valor = c(
    length(precios),
    paste(format(min(fechas), "%d/%m/%Y"), "al", format(max(fechas), "%d/%m/%Y")),
    format(min(fechas), "%d/%m/%Y"),
    format(max(fechas), "%d/%m/%Y"),
    format(round(precios[1], 2), big.mark = ".", decimal.mark = ","),
    format(round(precios[length(precios)], 2), big.mark = ".", decimal.mark = ","),
    format(round(precios[length(precios)] - precios[1], 2), big.mark = ".", decimal.mark = ","),
    round(((precios[length(precios)] / precios[1]) - 1) * 100, 2),
    format(round(mean(precios, na.rm = TRUE), 2), big.mark = ".", decimal.mark = ","),
    format(round(median(precios, na.rm = TRUE), 2), big.mark = ".", decimal.mark = ","),
    format(round(min(precios, na.rm = TRUE), 2), big.mark = ".", decimal.mark = ","),
    format(round(max(precios, na.rm = TRUE), 2), big.mark = ".", decimal.mark = ","),
    format(round(max(precios) - min(precios), 2), big.mark = ".", decimal.mark = ","),
    format(round(sd(precios, na.rm = TRUE), 2), big.mark = ".", decimal.mark = ","),
    format(round(var(precios, na.rm = TRUE), 2), big.mark = ".", decimal.mark = ","),
    round(sd(precios, na.rm = TRUE) / mean(precios, na.rm = TRUE), 4),
    round(skew(precios, na.rm = TRUE), 4),
    round(kurtosi(precios, na.rm = TRUE), 4),
    round(jarque.bera.test(precios)$p.value, 6)
  )
)

retornos_clean <- retornos[!is.na(retornos)]

estadisticas_retornos <- data.frame(
  Estadística = c(
    "Número de retornos",
    "Media diaria (%)",
    "Mediana diaria (%)",
    "Desviación estándar diaria (%)",
    "Volatilidad anualizada (%)",
    "Retorno anualizado (%)",
    "Ratio de Sharpe (anualizado, rf=0%)",
    "Mínimo retorno diario (%)",
    "Máximo retorno diario (%)",
    "Rango de retornos diarios (%)",
    "Asimetría de retornos",
    "Curtosis de retornos",
    "VaR 95% (1 día, %)",
    "CVaR/ES 95% (1 día, %)",
    "Ratio Sortino (anualizado)",
    "Ratio Calmar (si > 1 año)"
  ),
  Valor = c(
    length(retornos_clean),
    round(mean(retornos_clean) * 100, 4),
    round(median(retornos_clean) * 100, 4),
    round(sd(retornos_clean) * 100, 4),
    round(sd(retornos_clean) * sqrt(252) * 100, 4),
    round((exp(mean(retornos_clean) * 252) - 1) * 100, 4),
    round((mean(retornos_clean) / sd(retornos_clean)) * sqrt(252), 4),
    round(min(retornos_clean) * 100, 4),
    round(max(retornos_clean) * 100, 4),
    round((max(retornos_clean) - min(retornos_clean)) * 100, 4),
    round(skew(retornos_clean), 4),
    round(kurtosi(retornos_clean), 4),
    round(quantile(retornos_clean, 0.05) * 100, 4),
    round(mean(retornos_clean[retornos_clean <= quantile(retornos_clean, 0.05)]) * 100, 4),
    round(mean(retornos_clean) / sd(retornos_clean[retornos_clean < 0]) * sqrt(252), 4),
    ifelse(length(fechas) > 252,
           round((exp(mean(retornos_clean) * 252) - 1) / abs(min(retornos_clean)), 4),
           "Insuficientes datos")
  )
)

t <- 1:length(precios)
modelo_tendencia <- lm(precios ~ t)
summary_tendencia <- summary(modelo_tendencia)

resumen_tendencia <- data.frame(
  Estadística = c(
    "Coeficiente de tendencia (pendiente diaria)",
    "Intercepto",
    "R-cuadrado del modelo",
    "R-cuadrado ajustado",
    "Estadístico F",
    "p-value del modelo",
    "Error estándar del modelo",
    "Valor t de la tendencia",
    "p-value de la tendencia",
    "¿Tendencia significativa? (α=0.05)"
  ),
  Valor = c(
    format(round(coef(modelo_tendencia)[2], 4), big.mark = ".", decimal.mark = ","),
    format(round(coef(modelo_tendencia)[1], 2), big.mark = ".", decimal.mark = ","),
    round(summary_tendencia$r.squared, 4),
    round(summary_tendencia$adj.r.squared, 4),
    round(summary_tendencia$fstatistic[1], 2),
    format(round(pf(summary_tendencia$fstatistic[1], 
                    summary_tendencia$fstatistic[2], 
                    summary_tendencia$fstatistic[3], 
                    lower.tail = FALSE), 6), scientific = FALSE),
    format(round(sigma(modelo_tendencia), 2), big.mark = ".", decimal.mark = ","),
    round(summary_tendencia$coefficients[2, 3], 4),
    round(summary_tendencia$coefficients[2, 4], 6),
    ifelse(summary_tendencia$coefficients[2, 4] < 0.05, "SÍ", "NO")
  )
)


analizar_estacionalidad <- function(precios, fechas) {
  if(length(fechas) < 365) {
    return(data.frame(
      Estadística = c("Análisis estacional"),
      Valor = c("Se requieren al menos 365 días de datos para análisis estacional")
    ))
  }
  datos_mensuales <- data.frame(
    Fecha = as.Date(fechas),
    Precio = precios,
    Mes = month(fechas),
    Año = year(fechas)
  )
  
  estacionalidad_mensual <- datos_mensuales %>%
    group_by(Mes) %>%
    summarise(
      Media_Mensual = mean(Precio, na.rm = TRUE),
      Retorno_Medio = mean(diff(log(Precio)), na.rm = TRUE) * 100,
      Volatilidad_Mensual = sd(Precio, na.rm = TRUE),
      Observaciones = n()
    ) %>%
    mutate(
      Mes_Nombre = month.abb[Mes]
    ) %>%
    arrange(Mes)
  
  amplitud <- max(estacionalidad_mensual$Media_Mensual) - min(estacionalidad_mensual$Media_Mensual)
  
  data.frame(
    Estadística = c(
      "Amplitud estacional mensual (máx - mín)",
      "Mes con mayor precio promedio",
      "Mes con menor precio promedio",
      "Desviación estándar del patrón estacional",
      "Fuerza de la estacionalidad (0-1)",
      "Meses analizados"
    ),
    Valor = c(
      format(round(amplitud, 2), big.mark = ".", decimal.mark = ","),
      month.abb[estacionalidad_mensual$Mes[which.max(estacionalidad_mensual$Media_Mensual)]],
      month.abb[estacionalidad_mensual$Mes[which.min(estacionalidad_mensual$Media_Mensual)]],
      format(round(sd(estacionalidad_mensual$Media_Mensual), 2), big.mark = ".", decimal.mark = ","),
      round(1 - (sd(diff(precios)) / sd(precios)), 4),
      paste(unique(estacionalidad_mensual$Mes_Nombre), collapse = ", ")
    )
  )
}


estadisticas_estacionalidad <- analizar_estacionalidad(precios, fechas)


adf_test <- adf.test(precios)
pp_test <- pp.test(precios)
kpss_test <- tryCatch(
  kpss.test(precios, null = "Level"),
  error = function(e) list(statistic = NA, p.value = NA)
)

pruebas_estacionariedad <- data.frame(
  Prueba = c(
    "Augmented Dickey-Fuller (ADF)",
    "Phillips-Perron (PP)",
    "KPSS"
  ),
  `Hipótesis Nula` = c(
    "La serie tiene raíz unitaria (no estacionaria)",
    "La serie tiene raíz unitaria (no estacionaria)",
    "La serie es estacionaria"
  ),
  Estadístico = c(
    round(adf_test$statistic, 4),
    round(pp_test$statistic, 4),
    ifelse(is.na(kpss_test$statistic), "N/A", round(kpss_test$statistic, 4))
  ),
  `Valor Crítico 5%` = c(
    "-2.86", "-2.86", "0.463"
  ),
  `p-value` = c(
    round(adf_test$p.value, 4),
    round(pp_test$p.value, 4),
    ifelse(is.na(kpss_test$p.value), "N/A", round(kpss_test$p.value, 4))
  ),
  Conclusión = c(
    ifelse(adf_test$p.value < 0.05, "Rechazar H₀: Serie ESTACIONARIA", "No rechazar H₀: Serie NO ESTACIONARIA"),
    ifelse(pp_test$p.value < 0.05, "Rechazar H₀: Serie ESTACIONARIA", "No rechazar H₀: Serie NO ESTACIONARIA"),
    ifelse(is.na(kpss_test$p.value), "Error en prueba",
           ifelse(kpss_test$p.value > 0.05, "No rechazar H₀: Serie ESTACIONARIA", "Rechazar H₀: Serie NO ESTACIONARIA"))
  )
)

percentiles <- quantile(precios, probs = seq(0, 1, 0.05), na.rm = TRUE)
cuartiles <- quantile(precios, probs = c(0, 0.25, 0.5, 0.75, 1), na.rm = TRUE)

tabla_percentiles <- data.frame(
  Percentil = paste0(seq(0, 100, 5), "%"),
  Valor = format(round(percentiles, 2), big.mark = ".", decimal.mark = ",")
)

tabla_cuartiles <- data.frame(
  Medida = c("Mínimo (0%)", "Primer Cuartil (25%)", "Mediana (50%)", "Tercer Cuartil (75%)", "Máximo (100%)"),
  Valor = format(round(cuartiles, 2), big.mark = ".", decimal.mark = ","),
  Descripción = c(
    "Valor más bajo observado",
    "25% de los datos son menores a este valor",
    "Punto medio de la distribución",
    "75% de los datos son menores a este valor",
    "Valor más alto observado"
  )
)


css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37",
  borde_oro = "#D4AF37",
  fondo_titulo = "#1A1A1A"
)


tabla1_gt <- estadisticas_basicas %>%
  gt() %>%
  tab_header(
    title = md("** TABLA 1: ESTADÍSTICAS DESCRIPTIVAS DE LA SERIE DE TIEMPO**"),
    subtitle = paste("Período:", format(min(fechas), "%d/%m/%Y"), "al", format(max(fechas), "%d/%m/%Y"))
  ) %>%
  cols_label(
    Estadística = "ESTADÍSTICA",
    Valor = "VALOR"
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro, size = "medium"),
      cell_fill(color = css_colors$fondo_titulo),
      cell_borders(sides = c("top", "bottom"), color = css_colors$borde_oro, weight = px(2))
    ),
    locations = cells_title()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_lujo, weight = "bold", size = "large"),
      cell_fill(color = css_colors$panel_negro),
      cell_borders(sides = "all", color = css_colors$grilla_oscura)
    ),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro, size = "small"),
      cell_fill(color = css_colors$fondo_negro),
      cell_borders(sides = "bottom", color = css_colors$grilla_oscura)
    ),
    locations = cells_body()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_premium, weight = "bold"),
      cell_fill(color = css_colors$panel_negro)
    ),
    locations = cells_body(columns = Estadística)
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro, align = "right")
    ),
    locations = cells_body(columns = Valor)
  ) %>%
  tab_footnote(
    footnote = md("**Fuente:** Cálculos propios basados en datos de ICOLCAP"),
    locations = cells_title(groups = "subtitle")
  ) %>%
  tab_options(
    table.background.color = css_colors$fondo_negro,
    table.border.top.color = css_colors$oro_premium,
    table.border.bottom.color = css_colors$oro_premium,
    table.border.left.color = css_colors$oro_premium,
    table.border.right.color = css_colors$oro_premium,
    heading.border.bottom.color = css_colors$oro_premium,
    column_labels.border.top.color = css_colors$oro_premium,
    column_labels.border.bottom.color = css_colors$oro_premium,
    footnotes.border.bottom.color = css_colors$oro_premium,
    row_group.border.bottom.color = css_colors$oro_premium,
    table_body.hlines.color = css_colors$grilla_oscura,
    table_body.vlines.color = css_colors$grilla_oscura,
    table.font.size = px(12),
    heading.title.font.size = px(18),
    heading.subtitle.font.size = px(14),
    heading.padding = px(10),
    footnotes.font.size = px(11)
  ) %>%
  opt_table_font(
    font = list(
      google_font(name = "Inter"),
      "Segoe UI", "Arial", "sans-serif"
    )
  )


tabla2_gt <- estadisticas_retornos %>%
  gt() %>%
  tab_header(
    title = md("**TABLA 2: ESTADÍSTICAS DE RETORNOS**"),
    subtitle = paste("Retornos diarios | N =", length(retornos_clean), "observaciones")
  ) %>%
  cols_label(
    Estadística = "ESTADÍSTICA",
    Valor = "VALOR"
  ) %>%
  fmt_number(
    columns = Valor,
    rows = 2:16,  
    decimals = 4,
    sep_mark = ".",
    dec_mark = ","
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro),
      cell_fill(color = css_colors$fondo_titulo),
      cell_borders(sides = c("top", "bottom"), color = css_colors$borde_oro, weight = px(2))
    ),
    locations = cells_title()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_lujo, weight = "bold"),
      cell_fill(color = css_colors$panel_negro),
      cell_borders(sides = "all", color = css_colors$grilla_oscura)
    ),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro),
      cell_fill(color = css_colors$fondo_negro),
      cell_borders(sides = "bottom", color = css_colors$grilla_oscura)
    ),
    locations = cells_body()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_premium, weight = "bold"),
      cell_fill(color = css_colors$panel_negro)
    ),
    locations = cells_body(columns = Estadística)
  ) %>%
  tab_footnote(
    footnote = md("**Nota:** Volatilidad anualizada calculada con 252 días hábiles"),
    locations = cells_title(groups = "subtitle")
  ) %>%
  tab_options(
    table.background.color = css_colors$fondo_negro,
    table.border.top.color = css_colors$oro_premium,
    table.border.bottom.color = css_colors$oro_premium,
    table.border.left.color = css_colors$oro_premium,
    table.border.right.color = css_colors$oro_premium,
    heading.border.bottom.color = css_colors$oro_premium,
    column_labels.border.top.color = css_colors$oro_premium,
    column_labels.border.bottom.color = css_colors$oro_premium,
    footnotes.border.bottom.color = css_colors$oro_premium,
    table.font.size = px(12)
  )

tabla3_gt <- resumen_tendencia %>%
  gt() %>%
  tab_header(
    title = md("**TABLA 3: ANÁLISIS DE TENDENCIA**"),
    subtitle = "Modelo de regresión lineal: Precio ∼ Tiempo"
  ) %>%
  cols_label(
    Estadística = "ESTADÍSTICA",
    Valor = "VALOR"
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro),
      cell_fill(color = css_colors$fondo_titulo),
      cell_borders(sides = c("top", "bottom"), color = css_colors$borde_oro, weight = px(2))
    ),
    locations = cells_title()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_lujo, weight = "bold"),
      cell_fill(color = css_colors$panel_negro),
      cell_borders(sides = "all", color = css_colors$grilla_oscura)
    ),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro),
      cell_fill(color = css_colors$fondo_negro),
      cell_borders(sides = "bottom", color = css_colors$grilla_oscura)
    ),
    locations = cells_body()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_premium, weight = "bold"),
      cell_fill(color = css_colors$panel_negro)
    ),
    locations = cells_body(columns = Estadística)
  ) %>%
  tab_footnote(
    footnote = md("**Interpretación:** p-value < 0.05 indica tendencia estadísticamente significativa"),
    locations = cells_title(groups = "subtitle")
  ) %>%
  tab_options(
    table.background.color = css_colors$fondo_negro,
    table.border.top.color = css_colors$oro_premium,
    table.border.bottom.color = css_colors$oro_premium,
    table.border.left.color = css_colors$oro_premium,
    table.border.right.color = css_colors$oro_premium,
    heading.border.bottom.color = css_colors$oro_premium,
    column_labels.border.top.color = css_colors$oro_premium,
    column_labels.border.bottom.color = css_colors$oro_premium,
    footnotes.border.bottom.color = css_colors$oro_premium,
    table.font.size = px(12)
  )


tabla4_gt <- pruebas_estacionariedad %>%
  gt() %>%
  tab_header(
    title = md("**🔍 TABLA 4: PRUEBAS DE ESTACIONARIEDAD**"),
    subtitle = "Análisis de raíces unitarias y estacionariedad"
  ) %>%
  cols_label(
    Prueba = "PRUEBA",
    Hipótesis.Nula = "HIPÓTESIS NULA",
    Estadístico = "ESTADÍSTICO",
    Valor.Crítico.5. = "VALOR CRÍTICO 5%",
    p.value = "P-VALUE",
    Conclusión = "CONCLUSIÓN"
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro),
      cell_fill(color = css_colors$fondo_titulo),
      cell_borders(sides = c("top", "bottom"), color = css_colors$borde_oro, weight = px(2))
    ),
    locations = cells_title()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_lujo, weight = "bold", size = "small"),
      cell_fill(color = css_colors$panel_negro),
      cell_borders(sides = "all", color = css_colors$grilla_oscura)
    ),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro, size = "small"),
      cell_fill(color = css_colors$fondo_negro),
      cell_borders(sides = "bottom", color = css_colors$grilla_oscura)
    ),
    locations = cells_body()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_premium, weight = "bold", size = "small"),
      cell_fill(color = css_colors$panel_negro)
    ),
    locations = cells_body(columns = c(Prueba, Conclusión))
  ) %>%
  tab_style(
    style = cell_text(color = ifelse(pruebas_estacionariedad$Conclusión == "Rechazar H₀: Serie ESTACIONARIA", 
                                     "#00FF00", "#FF6B6B")),
    locations = cells_body(columns = Conclusión)
  ) %>%
  tab_footnote(
    footnote = md("**Nota:** ADF y PP: p-value < 0.05 rechaza H₀ (serie estacionaria) | KPSS: p-value > 0.05 no rechaza H₀ (serie estacionaria)"),
    locations = cells_title(groups = "subtitle")
  ) %>%
  tab_options(
    table.background.color = css_colors$fondo_negro,
    table.border.top.color = css_colors$oro_premium,
    table.border.bottom.color = css_colors$oro_premium,
    table.border.left.color = css_colors$oro_premium,
    table.border.right.color = css_colors$oro_premium,
    heading.border.bottom.color = css_colors$oro_premium,
    column_labels.border.top.color = css_colors$oro_premium,
    column_labels.border.bottom.color = css_colors$oro_premium,
    footnotes.border.bottom.color = css_colors$oro_premium,
    table.font.size = px(11),
    data_row.padding = px(5)
  )


tabla5_gt <- tabla_cuartiles %>%
  gt() %>%
  tab_header(
    title = md("**TABLA 4: CUARTILES**"),
    subtitle = "Distribución de precios del ICOLCAP"
  ) %>%
  cols_label(
    Medida = "MEDIDA",
    Valor = "VALOR (COP)",
    Descripción = "DESCRIPCIÓN"
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro),
      cell_fill(color = css_colors$fondo_titulo),
      cell_borders(sides = c("top", "bottom"), color = css_colors$borde_oro, weight = px(2))
    ),
    locations = cells_title()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_lujo, weight = "bold"),
      cell_fill(color = css_colors$panel_negro),
      cell_borders(sides = "all", color = css_colors$grilla_oscura)
    ),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro),
      cell_fill(color = css_colors$fondo_negro),
      cell_borders(sides = "bottom", color = css_colors$grilla_oscura)
    ),
    locations = cells_body()
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_premium, weight = "bold"),
      cell_fill(color = css_colors$panel_negro)
    ),
    locations = cells_body(columns = Medida)
  ) %>%
  tab_footnote(
    footnote = md("**Nota:** Los cuartiles dividen la distribución en cuatro partes iguales"),
    locations = cells_title(groups = "subtitle")
  ) %>%
  tab_options(
    table.background.color = css_colors$fondo_negro,
    table.border.top.color = css_colors$oro_premium,
    table.border.bottom.color = css_colors$oro_premium,
    table.border.left.color = css_colors$oro_premium,
    table.border.right.color = css_colors$oro_premium,
    heading.border.bottom.color = css_colors$oro_premium,
    column_labels.border.top.color = css_colors$oro_premium,
    column_labels.border.bottom.color = css_colors$oro_premium,
    footnotes.border.bottom.color = css_colors$oro_premium,
    table.font.size = px(12)
  )


tabla1_gt
** TABLA 1: ESTADÍSTICAS DESCRIPTIVAS DE LA SERIE DE TIEMPO**
Período: 02/01/2015 al 21/11/20251
ESTADÍSTICA VALOR
Número de observaciones 2656
Período de análisis 02/01/2015 al 21/11/2025
Primera fecha 02/01/2015
Última fecha 21/11/2025
Precio inicial (COP) 15.200
Precio final (COP) 19.990
Cambio absoluto (COP) 4.790
Cambio porcentual total (%) 31.51
Media aritmética (COP) 13.987,16
Mediana (COP) 13.789
Mínimo histórico (COP) 9.215
Máximo histórico (COP) 20.533,5
Rango total (COP) 11.318,5
Desviación estándar (COP) 1.750,03
Varianza (COP²) 3.062.612
Coeficiente de variación 0.1251
Asimetría (Skewness) 0.4938
Curtosis (Kurtosis) 0.3228
Prueba Jarque-Bera (p-value) 0
1 Fuente: Cálculos propios basados en datos de ICOLCAP
tabla2_gt
TABLA 2: ESTADÍSTICAS DE RETORNOS
Retornos diarios | N = 2655 observaciones1
ESTADÍSTICA VALOR
Número de retornos 2655
Media diaria (%) 0,0103
Mediana diaria (%) 0,0000
Desviación estándar diaria (%) 1,2132
Volatilidad anualizada (%) 19,2594
Retorno anualizado (%) 2,6342
Ratio de Sharpe (anualizado, rf=0%) 0,1350
Mínimo retorno diario (%) −15,8494
Máximo retorno diario (%) 13,3384
Rango de retornos diarios (%) 29,1877
Asimetría de retornos −0,9535
Curtosis de retornos 28,8960
VaR 95% (1 día, %) −1,7118
CVaR/ES 95% (1 día, %) −2,9637
Ratio Sortino (anualizado) 0,1530
Ratio Calmar (si > 1 año) 0,1662
1 Nota: Volatilidad anualizada calculada con 252 días hábiles
tabla3_gt
TABLA 3: ANÁLISIS DE TENDENCIA
Modelo de regresión lineal: Precio ∼ Tiempo1
ESTADÍSTICA VALOR
Coeficiente de tendencia (pendiente diaria) 0,2306
Intercepto 13.680,84
R-cuadrado del modelo 0.0102
R-cuadrado ajustado 0.0098
Estadístico F 27.37
p-value del modelo 0
Error estándar del modelo 1.741,4
Valor t de la tendencia 5.2319
p-value de la tendencia 0
¿Tendencia significativa? (α=0.05)
1 Interpretación: p-value < 0.05 indica tendencia estadísticamente significativa
tabla5_gt
TABLA 4: CUARTILES
Distribución de precios del ICOLCAP1
MEDIDA VALOR (COP) DESCRIPCIÓN
Mínimo (0%) 9.215,00 Valor más bajo observado
Primer Cuartil (25%) 12.768,75 25% de los datos son menores a este valor
Mediana (50%) 13.789,00 Punto medio de la distribución
Tercer Cuartil (75%) 15.210,25 75% de los datos son menores a este valor
Máximo (100%) 20.533,50 Valor más alto observado
1 Nota: Los cuartiles dividen la distribución en cuatro partes iguales
fig_hist <- plot_ly() %>%
  add_histogram(
    x = precios,
    name = "Frecuencia",
    marker = list(
      color = css_colors$oro_antiguo,
      line = list(color = css_colors$fondo_negro, width = 1)
    ),
    opacity = 0.7,
    nbinsx = 40
  ) %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:22px;'>
              DISTRIBUCIÓN DE PRECIOS - ICOLCAP</span>",
      x = 0.5
    ),
    xaxis = list(
      title = "<b>PRECIO (COP)</b>",
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(color = css_colors$texto_claro)
    ),
    yaxis = list(
      title = "<b>FRECUENCIA</b>",
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(color = css_colors$texto_claro)
    ),
    plot_bgcolor = css_colors$panel_negro,
    paper_bgcolor = css_colors$fondo_negro
  )

fig_box_retornos <- plot_ly(
  y = retornos_clean * 100,
  type = 'box',
  name = 'Retornos Diarios',
  boxpoints = 'outliers',
  marker = list(color = css_colors$oro_premium),
  line = list(color = css_colors$oro_lujo),
  fillcolor = css_colors$panel_negro
) %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:22px;'>
              DISTRIBUCIÓN DE RETORNOS DIARIOS</span>",
      x = 0.5
    ),
    yaxis = list(
      title = "<b>RETORNO (%)</b>",
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(color = css_colors$texto_claro)
    ),
    plot_bgcolor = css_colors$panel_negro,
    paper_bgcolor = css_colors$fondo_negro
  )

fig_hist
fig_box_retornos

El análisis descriptivo del ICOLCAP durante el período comprendido entre el 2 de enero de 2015 y el 21 de noviembre de 2025 revela características fundamentales sobre el comportamiento del índice bursátil colombiano. La muestra comprende 2.656 observaciones de retornos diarios, proporcionando una base de datos robusta para el análisis estadístico y la modelación de series temporales. Durante este período de aproximadamente diez años y diez meses, el ICOLCAP experimentó un cambio porcentual total del 31,51%, pasando de un precio inicial de 15.200 pesos colombianos a un precio final de 19.990 pesos, reflejando una tendencia alcista de largo plazo en la valoración del índice.

Las estadísticas de tendencia central revelan que el índice se ha mantenido en un nivel promedio de 13.987,16 COP con una mediana de 13.789 COP, indicando un comportamiento relativamente simétrico alrededor de la media. El mínimo histórico alcanzado fue de 9.215 COP, mientras que el máximo se ubicó en 20.533,5 COP, generando un rango total de 11.318,5 puntos. La desviación estándar de 1.750,03 COP representa aproximadamente un 12,51% de la media aritmética (coeficiente de variación), sugiriendo una volatilidad moderada en el nivel del índice durante el período analizado. Esta variabilidad es característica de índices bursátiles en economías emergentes, donde factores macroeconómicos, políticos y externos pueden generar fluctuaciones significativas.

El análisis de retornos diarios proporciona una perspectiva complementaria sobre la dinámica de precios. La media diaria de retornos fue de 0,0103%, lo que corresponde a una rentabilidad anualizada del 2,6342%, una cifra considerablemente baja si se considera como referencia la inflación colombiana y el costo de capital. La mediana diaria de retornos fue exactamente 0,0000%, indicando que en aproximadamente la mitad de los días de negociación el ICOLCAP no experimentó cambios de precio. La desviación estándar diaria de 1,2132% refleja la volatilidad diaria característica de mercados accionarios, que fue anualizaba a 19,2594%, una volatilidad moderada para un índice de economía emergente.

La distribución de retornos exhibe asimetría negativa (sesgo = -0,9535), sugiriendo que la cola izquierda de la distribución es más pronunciada que la derecha. Esto implica que el ICOLCAP experimenta con mayor frecuencia movimientos bajistas moderados que movimientos alcistas de similar magnitud. La curtosis de 28,8960 es extremadamente elevada, indicando colas muy pesadas y un exceso de eventos extremos (tanto positivos como negativos) en comparación con una distribución normal. Esta característica es típica de series financieras y justifica la implementación de modelos como ARIMA que capturan la heteroscedasticidad condicional.

Las medidas de riesgo revelan información crítica para inversionistas. El Valor en Riesgo (VaR) al 95% en un horizonte de un día fue de -1,7118%, indicando que existe una probabilidad del 5% de que los retornos diarios caigan por debajo de esta cifra. El Valor en Riesgo condicional (CVaR) al 95% fue de -2,9637%, representando la pérdida esperada en escenarios extremos más severos que el VaR. El Ratio de Sharpe anualizado de 0,1350 indica un retorno por unidad de riesgo considerablemente bajo, sugiriendo que el ICOLCAP podría no haber ofrecido compensación adecuada por el riesgo asumido durante el período analizado, considerando una tasa libre de riesgo del 0%. El Ratio Sortino anualizado de 0,1530 y el Ratio de Calmar de 0,1662 confirman esta evaluación desde perspectivas alternativas de rentabilidad ajustada por riesgo.

El análisis de cuartiles proporciona una visión detallada de la distribución de precios. El primer cuartil (25%) se ubicó en 12.768,75 COP, mientras que el tercer cuartil (75%) fue de 15.210,25 COP, generando un rango intercuartílico de 2.441,5 puntos. El 50% central de las observaciones se concentra en este rango relativamente estrecho, reflejando que la mayoría de los días el ICOLCAP operó dentro de una banda de precios relativamente estable. La mediana de 13.789 COP divide exactamente la distribución en dos mitades, confirmando la simetría aproximada del nivel del índice. Finalmente, la prueba de Jarque-Bera con un p-value de 0 rechaza claramente la hipótesis de normalidad, confirmando que los retornos del ICOLCAP no siguen una distribución normal, característica que fundamenta la necesidad de modelos probabilísticos más sofisticados que la especificación ARIMA tradicional.

3.2 Contexto Histórico de la Serie de Tiempo

El COLCAP (Colombia Capital) es el índice bursátil de referencia de la Bolsa de Valores de Colombia que refleja el comportamiento del mercado accionario colombiano a través del desempeño de las acciones más líquidas y representativas del país. Este indicador fue inaugurado oficialmente el 15 de enero de 2008 con un valor inicial de 1.000 puntos, siendo creado por la Bolsa de Valores de Colombia con el propósito de ofrecer un reflejo fiel del comportamiento de las acciones más significativas que se negocian en su plataforma (Rankia, 2024).

La creación del COLCAP surgió en un contexto de consolidación y modernización del mercado de capitales colombiano. La historia del mercado bursátil en Colombia se remonta al año 1928 con la creación de la Bolsa de Bogotá, la primera de su tipo en el país (Hapi Trade, 2025). Con el tiempo se fundaron otras bolsas importantes como las de Medellín y Occidente, cada una operando de manera independiente durante varias décadas (Hapi Trade, 2025). En 2001 se llevó a cabo la integración de estas tres bolsas regionales para conformar la Bolsa de Valores de Colombia (BVC), proceso que unificó las operaciones y normativas del mercado accionario nacional (Hapi Trade, 2025). En ese mismo año se implementó el Índice General de la Bolsa de Colombia (IGBC) como primer indicador consolidado del mercado (Hapi Trade, 2025).

3.3 El Ciclo de Descenso (2015-2020)

3.3.1 Crisis Petrolera y Caída Inicial (2015-2016)

El periodo 2015-2016 marcó un punto de inflexión negativo para el mercado accionario colombiano. El COLCAP enfrentó presiones significativas debido a la caída abrupta de los precios internacionales del petróleo. El crudo Brent, que es fundamental para la economía colombiana dado el peso de Ecopetrol en el índice, experimentó un descenso dramático desde niveles superiores a $100 por barril en 2014 hasta mínimos cercanos a $30 por barril a principios de 2016 (Valora Analitik, 2020; Ecopetrol, 2020; TradingView, 2025).

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

Brent=getSymbols("BZ=F", from = "2015-01-01", to = "2016-12-31")

Brent <- `BZ=F`
Brent_df <- data.frame(
  Fecha = index(Brent),
  Cierre = as.numeric(Cl(Brent))  
)

Brent_df <- na.omit(Brent_df)

Brent_df$SMA_20 <- SMA(Brent_df$Cierre, n = 20)
Brent_df$SMA_50 <- SMA(Brent_df$Cierre, n = 50)

max_valor <- max(Brent_df$Cierre, na.rm = TRUE)
min_valor <- min(Brent_df$Cierre, na.rm = TRUE)
max_fecha <- Brent_df$Fecha[which.max(Brent_df$Cierre)]
min_fecha <- Brent_df$Fecha[which.min(Brent_df$Cierre)]

fig <- plot_ly(Brent_df, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "BRENT",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>PETRÓLEO BRENT</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}/barril<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              PETRÓLEO BRENT - CRUDO (2015-2016)</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Crisis del petróleo: caída histórica de precios</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO (USD/BARRIL)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.2f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(Brent_df$Fecha), "%d %b %Y"), 
          " - ", format(max(Brent_df$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", round(max_valor, 2), "/bbl | ",
          "Mínimo: $", round(min_valor, 2), "/bbl | ",
          "Variación: ", sprintf("%+.1f%%", (tail(Brent_df$Cierre, 1) - head(Brent_df$Cierre, 1)) / head(Brent_df$Cierre, 1) * 100)
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

Los analistas proyectaban que el índice COLCAP terminaría 2015 en aproximadamente 1.213 unidades, reflejando una caída considerable respecto a años anteriores (La República, 2015). Esta presión se intensificó por el incremento en la probabilidad de alzas en las tasas de interés en Estados Unidos, lo que afectó los flujos de capital hacia mercados emergentes (La República, 2015). El sector energético, especialmente Ecopetrol, sufrió desvalorizaciones pronunciadas con su acción cayendo de $5.930 pesos (máximo histórico en mayo de 2012) hasta mínimos de $880 pesos en enero de 2016 (TradingView, 2025; Ocensa, 2025).

Ecopetrol=read_excel("EC.2015.xlsx") %>% rename(Fecha=1,Cierre=2)
library(plotly)
library(readxl)
library(TTR)
library(xts)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

Ecopetrol$Fecha <- as.Date(Ecopetrol$Fecha)

Ecopetrol$SMA_20 <- SMA(Ecopetrol$Cierre, n = 20)
Ecopetrol$SMA_50 <- SMA(Ecopetrol$Cierre, n = 50)

max_valor <- max(Ecopetrol$Cierre, na.rm = TRUE)
min_valor <- min(Ecopetrol$Cierre, na.rm = TRUE)
max_fecha <- Ecopetrol$Fecha[which.max(Ecopetrol$Cierre)]
min_fecha <- Ecopetrol$Fecha[which.min(Ecopetrol$Cierre)]

fig <- plot_ly(Ecopetrol, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "ECOPETROL",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>ECOPETROL</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Cierre: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo Histórico",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo Histórico",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              ECOPETROL 2015-2016 - Historicos </span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Precio de cierre con medias móviles y puntos extremos</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO DE CIERRE (USD)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.2f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(Ecopetrol$Fecha), "%d %b %Y"), 
          " - ", format(max(Ecopetrol$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", round(max_valor, 2), 
          " | Mínimo: $", round(min_valor, 2)
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

3.3.2 Recuperación Moderada y Volatilidad (2017-2019)

Entre 2017 y 2019, el mercado colombiano experimentó una recuperación moderada pero inconsistente. Los precios del petróleo se estabilizaron gradualmente, con el Brent promediando alrededor de $64 por barril en 2019, aunque esto representaba una caída del 10.5% respecto a 2018 (Ecopetrol, 2020; Corficolombiana, 2020). Ecopetrol logró adaptarse a este nuevo entorno de precios más bajos, alcanzando una utilidad neta récord de $13.3 billones en 2019, la más alta en seis años, con un EBITDA histórico de $31.1 billones (Ecopetrol, 2020; Corficolombiana, 2020).

Sin embargo, el índice COLCAP no logró traducir estos buenos resultados corporativos individuales en un desempeño bursátil consistente. La falta de liquidez en el mercado colombiano y el número limitado de emisores continuaron siendo obstáculos estructurales (Bloomberg Línea, 2022; Emerald Insight, 2017). Durante 2018, el volumen de negociación registró nuevos mínimos, siguiendo la tendencia negativa iniciada en 2017 (Casa de Bolsa, 2018).

El contexto macroeconómico del periodo reflejaba una economía en proceso de recuperación gradual pero frágil. En 2017, la economía colombiana creció apenas 1,8%, la menor cifra desde 2009 y la más baja desde que cayeron los precios del petróleo en 2014 (BBVA Research, 2018; DANE, 2018). El DANE posteriormente revisó esta cifra a la baja hasta 1,4%, evidenciando que la situación había sido incluso más débil de lo estimado inicialmente (SELA, 2019). El crecimiento de 2017 estuvo caracterizado por el débil desempeño de sectores clave: el sector minero-energético cayó 0,8%, mientras que la construcción y la industria aún no lograban recuperarse de las caídas previas (DANE, 2018).

A pesar del comportamiento general negativo del índice, algunas acciones individuales mostraron un desempeño destacado. Tres de las cinco acciones con mejor desempeño en 2019 fueron empresas antioqueñas, evidenciando que existían oportunidades específicas incluso en un mercado adverso (El Colombiano, 2020). Entre las acciones más líquidas y negociadas del periodo se destacaron constantemente Bancolombia (tanto ordinaria como preferencial), Ecopetrol, ISA y Grupo Aval, que representaban la mayor parte del volumen transado en la Bolsa de Valores de Colombia (Banco de la República, 2023).

Los inversionistas extranjeros mostraron un comportamiento selectivo durante este periodo. Aunque sus tenencias se concentraban en las acciones más líquidas como Bancolombia, ISA y Ecopetrol, su participación era diversificada y no presentaba una alta concentración individual (Banco de la República, 2023). Sin embargo, se observó una tendencia preocupante: después de realizar compras netas significativas en años anteriores, los inversionistas extranjeros comenzaron a acumular ventas, reflejando un menor apetito por acciones colombianas (Banco de la República, 2023).

Bancolombia=read_excel("BANCOLOMBIA.xlsx") %>% rename(Fecha=1,Cierre=2)
library(plotly)
library(readxl)
library(TTR)


Bancolombia$Fecha <- as.Date(Bancolombia$Fecha)

Bancolombia$SMA_20 <- SMA(Bancolombia$Cierre, n = 20)
Bancolombia$SMA_50 <- SMA(Bancolombia$Cierre, n = 50)

max_valor <- max(Bancolombia$Cierre, na.rm = TRUE)
min_valor <- min(Bancolombia$Cierre, na.rm = TRUE)
max_fecha <- Bancolombia$Fecha[which.max(Bancolombia$Cierre)]
min_fecha <- Bancolombia$Fecha[which.min(Bancolombia$Cierre)]

fig <- plot_ly(Bancolombia, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "BANCOLOMBIA",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>BANCOLOMBIA</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Cierre: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo Histórico",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo Histórico",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              BANCOLOMBIA - Historicos</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Precio de cierre con medias móviles y puntos extremos</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO DE CIERRE (COP)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.0f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(Bancolombia$Fecha), "%d %b %Y"), 
          " - ", format(max(Bancolombia$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", format(round(max_valor, 0), big.mark = ","), 
          " | Mínimo: $", format(round(min_valor, 0), big.mark = ",")
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

3.3.3 Pandemia COVID-19: El Colapso de 2020

El año 2020 representó uno de los periodos más desafiantes en la historia reciente del mercado accionario colombiano. La pandemia de COVID-19 provocó un desplome histórico del COLCAP, que cerró 2019 en 1.662,42 unidades y terminó 2020 en 1.437,89 puntos, reflejando una pérdida del 13,5% (Valora Analitik, 2020). El mínimo histórico se alcanzó el 18 de marzo de 2020, cuando el índice tocó 880,72 puntos, representando una caída de aproximadamente 47% desde el cierre de 2019 (TradingView, 2025; Repositorio Unicórdoba, 2021).

La crisis que desencadenó esta caída fue de naturaleza dual: por un lado, la pandemia de COVID-19 declarada oficialmente por la Organización Mundial de la Salud el 11 de marzo de 2020, y por otro, la guerra de precios del petróleo entre Rusia y Arabia Saudita iniciada en marzo de 2020. Este doble golpe creó una tormenta perfecta que afectó simultáneamente la oferta y la demanda globales. Los mercados bursátiles mundiales sufrieron la mayor caída desde la Gran Recesión de 2008, con caídas diarias récord: el índice S&P/ASX 200 de Australia cayó 9,7% en un solo día, el Dow Jones cayó 12% el 16 de marzo, y el 18 de marzo cayó más de 9% adicional.

El gobierno colombiano respondió rápidamente a la crisis declarando el estado de emergencia económica el 17 de marzo de 2020 e implementando medidas de confinamiento obligatorio a partir del 25 de marzo (Sierra Muñiz, 2021). Estas medidas fueron entre las más estrictas del continente: restricción de eventos, cierre de bares y discotecas, cierre de fronteras terrestres, aéreas y fluviales, y aislamiento preventivo obligatorio en los hogares. Como resultado, la movilidad de la población se redujo hasta en un 70% en abril de 2020 (ANDI, 2020).

El impacto económico fue devastador. La economía colombiana se contrajo 6,8% en 2020, siendo el segundo año más difícil después de 2009 durante la Gran Recesión (Grupo Aval, 2021). En términos trimestrales, el PIB cayó 15,6% en el segundo trimestre de 2020 respecto al mismo trimestre de 2019, luego se contrajo 8,3% en el tercer trimestre y 3,5% en el cuarto trimestre (Grupo Aval, 2021). La demanda interna se desplomó 7,6%, con una caída en la formación bruta de capital del 21,2% y una contracción del consumo privado del 5,8%, lo que evidenciaba la profundidad de la crisis (Grupo Aval, 2021).

Las acciones más afectadas fueron aquellas del sector financiero y energético. Ecopetrol vio su acción caer de $3.315 pesos en 2019 a $2.245 pesos en 2020, una disminución del 32,27% (Valora Analitik, 2020). Las acciones del Grupo Sura (preferencial y ordinaria) también sufrieron caídas superiores al 24%, cerrando el año en $22.000 (acción preferencial) y $25.280 (acción ordinaria) frente a los $29.300 y $34.000 respectivamente que había tenido al cierre de 2019 (Valora Analitik, 2020).

SURA=read_excel("SURA.xlsx") %>% rename(Fecha=1,Cierre=2)
library(plotly)
library(readxl)
library(TTR)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

SURA$Fecha <- as.Date(SURA$Fecha)

SURA$SMA_20 <- SMA(SURA$Cierre, n = 20)
SURA$SMA_50 <- SMA(SURA$Cierre, n = 50)

max_valor <- max(SURA$Cierre, na.rm = TRUE)
min_valor <- min(SURA$Cierre, na.rm = TRUE)
max_fecha <- SURA$Fecha[which.max(SURA$Cierre)]
min_fecha <- SURA$Fecha[which.min(SURA$Cierre)]

fig <- plot_ly(SURA, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "SURA",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>SURA</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Cierre: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo Histórico",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo Histórico",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              SURA - HISTORICOS</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Precio de cierre con medias móviles y puntos extremos</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO DE CIERRE (COP)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.0f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(SURA$Fecha), "%d %b %Y"), 
          " - ", format(max(SURA$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", format(round(max_valor, 0), big.mark = ","), 
          " | Mínimo: $", format(round(min_valor, 0), big.mark = ",")
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

Un aspecto notable fue la divergencia entre los resultados del primero y el segundo semestre de 2020. Mientras que la primera mitad del año fue catastrófica, la segunda mitad mostró una recuperación progresiva a medida que las cuarentenas se flexibilizaban. De hecho, diciembre de 2020 fue un mes excepcionalmente positivo para las acciones: el COLCAP subió 14,3% durante el mes, recuperándose desde 1.258 puntos en noviembre a niveles más cercanos al cierre del año (Valora Analitik, 2020).

Las acciones que más subieron en diciembre de 2020 fueron Cemex Latam Holdings (+43,3%), Davivienda preferencial (+29,5%), Bancolombia ordinaria (+26,4%) y Bancolombia preferencial (+25,9%), evidenciando que las instituciones financieras mejor posicionadas lideraron la recuperación (Valora Analitik, 2020). En contraste, Canacol Energy fue la única acción del conjunto principal que cayó en diciembre (-6,6%), reflejando la persistente debilidad del sector energético incluso cuando el mercado comenzaba a recuperarse.

Davivienda=read_excel("Davivienda.xlsx") %>% rename(Fecha=1,Cierre=2)
library(plotly)
library(readxl)
library(TTR)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

Davivienda$Fecha <- as.Date(Davivienda$Fecha)

Davivienda$SMA_20 <- SMA(Davivienda$Cierre, n = 20)
Davivienda$SMA_50 <- SMA(Davivienda$Cierre, n = 50)

max_valor <- max(Davivienda$Cierre, na.rm = TRUE)
min_valor <- min(Davivienda$Cierre, na.rm = TRUE)
max_fecha <- Davivienda$Fecha[which.max(Davivienda$Cierre)]
min_fecha <- Davivienda$Fecha[which.min(Davivienda$Cierre)]

fig <- plot_ly(Davivienda, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "Davivienda",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>Davivienda</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Cierre: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo Histórico",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo Histórico",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              Davivienda - HISTORICOS</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Precio de cierre con medias móviles y puntos extremos</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO DE CIERRE (COP)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.0f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(Davivienda$Fecha), "%d %b %Y"), 
          " - ", format(max(Davivienda$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", format(round(max_valor, 0), big.mark = ","), 
          " | Mínimo: $", format(round(min_valor, 0), big.mark = ",")
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

El sector financiero demostró una resiliencia excepcional durante la crisis. Grupo Aval, por ejemplo, implementó exitosamente esquemas de trabajo remoto que permitieron que casi el 95% de su fuerza laboral trabajara desde casa, mientras fortalecía sus canales digitales y ampliaba la capacidad de sus call-centers (Grupo Aval, 2021). Las transacciones digitales aumentaron un 52% durante 2020, y las entidades financieras lograron mantener continuidad operativa, adaptabilidad a la crisis y gestión prudente del riesgo, lo que permitió que sus acciones mantuvieran mayor liquidez y demanda incluso en plena pandemia (Grupo Aval, 2021)

AVAL=read_excel("AVAL.xlsx") %>% rename(Fecha=1,Cierre=2)
library(plotly)
library(readxl)
library(TTR)
library(zoo) 

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

AVAL$Fecha <- as.Date(AVAL$Fecha)

AVAL$SMA_20 <- rollmean(AVAL$Cierre, 20, align = "right", fill = NA)
AVAL$SMA_50 <- rollmean(AVAL$Cierre, 50, align = "right", fill = NA)

max_valor <- max(AVAL$Cierre, na.rm = TRUE)
min_valor <- min(AVAL$Cierre, na.rm = TRUE)
max_fecha <- AVAL$Fecha[which.max(AVAL$Cierre)]
min_fecha <- AVAL$Fecha[which.min(AVAL$Cierre)]

fig <- plot_ly(AVAL, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "AVAL",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>AVAL</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Cierre: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>",
    connectgaps = FALSE  
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>",
    connectgaps = FALSE  
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo Histórico",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo Histórico",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              AVAL - HISTORICOS</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Precio de cierre con medias móviles completas</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO DE CIERRE (COP)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.0f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(AVAL$Fecha), "%d %b %Y"), 
          " - ", format(max(AVAL$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", format(round(max_valor, 0), big.mark = ","), 
          " | Mínimo: $", format(round(min_valor, 0), big.mark = ",")
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

3.4 El Periodo de Recuperación Gradual (2021-2023)

3.4.1 Rebote Post-Pandemia (2021-2022)

El año 2021 marcó el inicio de una recuperación económica significativa para Colombia. El PIB creció un impresionante 10,7%, una cifra históricamente elevada que reflejaba el efecto rebote desde la profunda contracción de 2020, aunque gran parte de este crecimiento provino de la baja base de comparación (Banco de la República, 2021). El crecimiento fue amplio: en el segundo trimestre de 2021, el PIB anual alcanzó un crecimiento de 17,6% gracias a ese efecto de base, aunque en términos trimestrales el crecimiento fue más moderado (BBVA Asset Management, 2021). En el primer semestre de 2022, la economía continuó con una trayectoria ascendente, registrando una expansión del 8,6% (Banco de la República, 2021).

Esta reactivación estuvo impulsada por múltiples factores convergentes. Las políticas monetarias fueron ampliamente expansivas, con el Banco de la República manteniendo tasas históricamente bajas para facilitar la recuperación (Banco de la República, 2021). El avance en los programas de vacunación permitió la reapertura gradual de la economía y la recomposición del empleo, aunque de manera desigual entre sectores y estratos sociales (Banco de la República, 2021). Los mejores términos de intercambio gracias a la recuperación de los precios del petróleo inyectaron liquidez en la economía, especialmente en sectores exportadores y en las arcas fiscales (Banco de la República, 2021).

El petróleo Brent experimentó una fuerte recuperación, pasando de niveles cercanos a $78 por barril a inicios de 2022 hasta alcanzar un máximo de $127 por barril en marzo de 2022, impulsado por la invasión rusa a Ucrania que interrumpió los flujos mundiales de petróleo y energía (Ecopetrol, 2023; CARM, 2022). Este evento geopolítico de magnitud transformó el panorama energético global e impactó directamente a Colombia como productor petrolero. Sin embargo, para finales de 2022, el Brent había moderado su precio a aproximadamente $86 por barril, reflejando la adaptación de los mercados globales a la nueva realidad geopolítica (Ecopetrol, 2023).

library(quantmod)
library(plotly)
library(TTR)
library(zoo)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)


tryCatch({
  getSymbols("BZ=F", from = "2021-01-01", to = "2021-12-31", auto.assign = TRUE)
  brent_data <- `BZ=F`
  Brent_df <- data.frame(
    Fecha = index(brent_data),
    Cierre = as.numeric(Cl(brent_data))  
  )
})

Brent_df <- na.omit(Brent_df)

Brent_df <- Brent_df[order(Brent_df$Fecha), ]

Brent_df$SMA_20 <- rollmean(Brent_df$Cierre, 20, align = "right", fill = NA)
Brent_df$SMA_50 <- rollmean(Brent_df$Cierre, 50, align = "right", fill = NA)

max_valor <- max(Brent_df$Cierre, na.rm = TRUE)
min_valor <- min(Brent_df$Cierre, na.rm = TRUE)
max_fecha <- Brent_df$Fecha[which.max(Brent_df$Cierre)]
min_fecha <- Brent_df$Fecha[which.min(Brent_df$Cierre)]

fig <- plot_ly(Brent_df, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "BRENT",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>PETRÓLEO BRENT</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}/barril<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>",
    connectgaps = FALSE
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>",
    connectgaps = FALSE
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo 2021",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO 2021</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo 2021",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO 2021</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              PETRÓLEO BRENT - AÑO 2021</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Recuperación post-pandemia: tendencia alcista del crudo</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y",
      tickvals = seq.Date(as.Date("2021-01-01"), as.Date("2021-12-31"), by = "month")
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO (USD/BARRIL)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.2f",
      range = c(min_valor * 0.95, max_valor * 1.05)
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: 2021 completo | ",
          "Máximo: $", round(max_valor, 2), "/bbl (", format(max_fecha, "%d %b"), ") | ",
          "Mínimo: $", round(min_valor, 2), "/bbl (", format(min_fecha, "%d %b"), ") | ",
          "Variación anual: ", sprintf("%+.1f%%", (tail(Brent_df$Cierre, 1) - head(Brent_df$Cierre, 1)) / head(Brent_df$Cierre, 1) * 100)
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

A nivel sectorial, el desempeño fue heterogéneo. En agosto de 2021, mientras el COLCAP en general se revalorizaba 6,73%, favorecido por un contexto internacional favorable y por transferencias de cesantías de corto a largo plazo que generaban demanda de renta variable, las acciones más valorizadas fueron Bancolombia (+14,6%), Davivienda (+13,5%) y Grupo Sura (+11%), mientras que BVC retrocedió 6%, Corficolombiana cayó 4% y Grupo Bolívar se desvalorizó 1,3% (BBVA Asset Management, 2021).

Esta divergencia reflejaba que algunos sectores y empresas específicas lograban comunicar mejor su recuperación a los inversionistas, mientras que otros permanecían rezagados. El sector financiero, siendo dominante en el índice, fue crucial para cualquier rally que pudiera experimentar el COLCAP, pero sus perspectivas de crecimiento futuro estaban limitadas por la cartera de crédito débil derivada de la crisis (BBVA Asset Management, 2021).

cibest=read_excel("cibest.xlsx") %>% rename(Fecha=1,Cierre=2)
library(plotly)
library(readxl)
library(TTR)

cibest$Fecha <- as.Date(cibest$Fecha)

cibest$SMA_20 <- SMA(cibest$Cierre, n = 20)
cibest$SMA_50 <- SMA(cibest$Cierre, n = 50)

max_valor <- max(cibest$Cierre, na.rm = TRUE)
min_valor <- min(cibest$Cierre, na.rm = TRUE)
max_fecha <- cibest$Fecha[which.max(cibest$Cierre)]
min_fecha <- cibest$Fecha[which.min(cibest$Cierre)]

fig <- plot_ly(cibest, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "BANCOLOMBIA",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>BANCOLOMBIA</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Cierre: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo Histórico",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo Histórico",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              BANCOLOMBIA 2021 - Historicos</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Precio de cierre con medias móviles y puntos extremos</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO DE CIERRE (COP)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.0f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(Bancolombia$Fecha), "%d %b %Y"), 
          " - ", format(max(Bancolombia$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", format(round(max_valor, 0), big.mark = ","), 
          " | Mínimo: $", format(round(min_valor, 0), big.mark = ",")
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

3.4.2 Incertidumbre Política y Reformas (2022-2023)

La elección de Gustavo Petro en junio de 2022 introdujo una nueva fuente de volatilidad e incertidumbre en los mercados financieros colombianos. El COLCAP, que había repuntado brevemente hasta los 1.500 puntos en 2022, se desplomó nuevamente tras la victoria de Petro en segunda vuelta (El Colombiano, 2025). Para septiembre de 2022, el COLCAP había perdido 15,3% en lo corrido del año, y la caída acumulada de los últimos tres años era del 28%, a pesar de los buenos momentos de la economía durante 2021 y parte de 2022 (Bloomberg Línea, 2022).

La propuesta de transición energética del nuevo gobierno generó preocupaciones significativas sobre el futuro de Ecopetrol, la empresa más importante del índice. El plan de Petro incluía una prohibición al fracking, la suspensión de nuevas licencias para explotación de petróleo, la detención de proyectos piloto de fracking, y la exploración o explotación de yacimientos costa afuera (Crudo Transparente, 2022; Rosa Luxemburgo, 2022). Aunque Ecopetrol anunció inversiones en transición energética para 2023 entre $25,3 y $29,8 billones de pesos, con el 23% destinado a nuevos negocios de bajas emisiones como hidrógeno y autogeneración renovable, los inversionistas permanecieron escépticos sobre si estas iniciativas podrían compensar la caída esperada en la producción de petróleo convencional (Crudo Transparente, 2022; Ecopetrol, 2023).

El gobierno de Petro implementó la Reforma Tributaria 2022 (Ley 2277), que entró en vigor en 2023 y buscaba recaudar inicialmente $25 billones (Jade & Rio, 2024; La República, 2023). Esta reforma introdujo cambios significativos que afectaron directamente al mercado accionario. El aumento del impuesto a dividendos fue especialmente controversial: la tarifa para dividendos de personas naturales residentes se incrementó al 15%, y al 20% para no residentes (Jade & Rio, 2024; Infobae Colombia, 2024). Esta medida disincentivó significativamente la inversión individual en acciones, ya que reducía el retorno neto para los accionistas después de impuestos.

Simultáneamente, se implementó una sobretasa permanente del 5% para el sector financiero, elevando su tarifa efectiva de renta al 40%, la más alta de toda la economía (Jade & Rio, 2024; La República, 2023). Dado que el sector financiero representaba aproximadamente el 46-55% del peso del COLCAP, esta medida tuvo un impacto desproporcionado en las valuaciones del índice. La reforma también incluyó una limitación de deducciones y descuentos tributarios al 3% de la renta líquida ordinaria, lo que aumentó aún más la carga tributaria efectiva de las empresas (Jade & Rio, 2024).

Analistas de mercado como Luis Fernando Velandia de Credicorp Capital advertían en 2022 que la reforma tributaria generaba “miedos importantes a los inversionistas del mercado de capitales” (Bloomberg Línea, 2022). El mercado permanecería bajo presión hasta tener mayor claridad sobre el texto final de la reforma y sus implicaciones reales (Bloomberg Línea, 2022).

La incertidumbre sobre estas medidas tributarias se combinó con presiones inflacionarias globales y locales que mantuvieron al mercado accionario bajo presión continua. La inflación en Colombia alcanzó 9,28% en diciembre de 2023, la más alta en varios años, obligando al Banco de la República a mantener tasas de interés elevadas (BBVA Research, 2024; ANIF, 2025). Esta inflación fue impulsada por múltiples factores: incrementos en precios de alimentos, energía, arriendos, y servicios regulados. El componente de alimentos cayó a 5% al cierre de 2023 desde máximos superiores al 13%, pero la inflación sin alimentos se mantuvo en 10,33% en diciembre de 2023, reflejando persistentes presiones en servicios y energía (BBVA Research, 2024).

En este contexto de inflación elevada y tasas de interés altas, el atractivo relativo de las acciones versus bonos y depósitos se deterioró significativamente. El costo de financiamiento para las empresas se incrementó, presionando márgenes de ganancia, mientras que los rendimientos ofrecidos por instrumentos de renta fija se tornaban más competitivos (Banco de la República, 2021). Los fondos de pensiones y seguros, inversores institucionales clave, redujeron sus compras de acciones ante el mayor retorno disponible en instrumentos de menor riesgo.

En 2023, el índice retornó a niveles similares a los de la crisis pandémica de 2020, con el COLCAP alcanzando mínimos cercanos a 1.300-1.400 puntos, con valoraciones extremadamente bajas (Pluralidad, 2024). Las relaciones Price-to-Earnings del mercado colombiano se encontraban entre las más bajas del mundo, y los dividend yields más altos, reflejando un mercado severamente infravalorado.

Sin duda, la gran protagonista de este periodo fue Grupo Nutresa. Su acción experimentó una valorización histórica impulsada por una sucesión de Ofertas Públicas de Adquisición (OPA) lanzadas inicialmente a finales de 2021 por el Grupo Gilinski y sus socios árabes (IHC), operaciones que continuaron marcando la agenda en 2022 y 2023 (Universidad del Rosario, 2023; Grupo Sura, 2024). La batalla de OPAs entre Gilinski y la administración tradicional de Nutresa fue uno de los mayores eventos corporativos en la historia reciente del mercado accionario colombiano, capturando la atención de inversionistas institucionales globales y generando liquidez anómala en un título que de otra forma habría estado condenado al rezago general.

Nutresa=read_excel("NUT.xlsx") %>% rename(Fecha=1,Cierre=2)
Nutresa$Fecha <- as.Date(Nutresa$Fecha)


Nutresa$SMA_20 <- SMA(Nutresa$Cierre, n = 20)
Nutresa$SMA_50 <- SMA(Nutresa$Cierre, n = 50)

max_valor <- max(Nutresa$Cierre, na.rm = TRUE)
min_valor <- min(Nutresa$Cierre, na.rm = TRUE)
max_fecha <- Nutresa$Fecha[which.max(Nutresa$Cierre)]
min_fecha <- Nutresa$Fecha[which.min(Nutresa$Cierre)]

fig <- plot_ly(Nutresa, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "Nutresa",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>Nutresa</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Cierre: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo Histórico",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo Histórico",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

figN <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              Nutresa 2022-2023 - Historicos</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Precio de cierre con medias móviles y puntos extremos</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO DE CIERRE (COP)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.0f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(Nutresa$Fecha), "%d %b %Y"), 
          " - ", format(max(Nutresa$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", format(round(max_valor, 0), big.mark = ","), 
          " | Mínimo: $", format(round(min_valor, 0), big.mark = ",")
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

figN

La acción de Nutresa experimentó un despegue extraordinario desde niveles cercanos a los $22.000 pesos antes de que las OPAs se anunciaran públicamente, hasta superar los $50.000-$60.000 pesos en varios momentos durante 2022 y 2023 (Investing.com, 2025; La República, 2024). Esta revalorización de más del 150% desafió por completo la caída del mercado general que registraba pérdidas de 12-15% anual, consolidando a Nutresa como una “burbuja de valor” completamente aislada del pesimismo político y económico que dominaba el resto del mercado (Universidad del Rosario, 2023).

El año 2024 representó un punto de inflexión decisivo para el mercado accionario colombiano, marcando el inicio de un ciclo de optimismo renovado y una recuperación estructural que se extendió con fuerza hacia 2025. Contra todos los pronósticos iniciales, estos dos años han sido excepcionales, llevando al índice MSCI COLCAP a romper récords históricos y consolidar una tendencia alcista sin precedentes recientes (La República, 2025; INCP, 2024).

El desempeño del mercado ha sido sobresaliente. En octubre de 2024, el índice MSCI COLCAP alcanzó los 1.987,12 puntos, superando sus máximos históricos anteriores y marcando el fin de una década de estancamiento (La República, 2025). La tendencia no se detuvo ahí: para noviembre de 2025, el índice rompió por primera vez la barrera psicológica de los 2.000 puntos, consolidando un crecimiento acumulado del 45% solo en ese año (RTVC Noticias, 2025; Colombia Chamber, 2025).

El cierre del 2 de diciembre de 2025 fue emblemático: el índice se ubicó en 2.115,75 puntos, con una variación diaria positiva del 1,65% y acumulando un impresionante crecimiento anual del 53,36% (Valora Analitik, 2025a; Yahoo Finanzas, 2025). En una perspectiva de largo plazo, el repunte es aún más evidente: en los últimos cinco años el índice ha crecido 62,98%, y en la última década ha registrado un aumento del 95,68%, cifras que validan la tesis de que el mercado colombiano estaba severamente subvalorado y solo necesitaba un catalizador para liberar ese valor atrapado (La República, 2025a; Valora Analitik, 2025a).

El principal catalizador de este renacimiento bursátil fue la resolución definitiva del conflicto corporativo más complejo y prolongado de la historia empresarial reciente de Colombia: el llamado “desenroque” del Grupo Empresarial Antioqueño (GEA) (Infobae, 2025). Este proceso implicó la eliminación de las participaciones cruzadas entre Grupo Sura, Grupo Argos y Grupo Nutresa, una estructura que durante décadas protegió a estas compañías de tomas hostiles pero que, irónicamente, terminó castigando sus valoraciones de mercado al generar un “descuento de holding” persistente.

La simplificación de esta estructura desbloqueó un valor inmenso para los accionistas. La separación definitiva, sellada en julio de 2025, permitió que cada compañía se enfocara en su core business: Argos en infraestructura, Sura en servicios financieros y Nutresa en alimentos (bajo el control de Gilinski) (Infobae, 2025). Esta claridad estratégica fue recibida con euforia por el mercado.

Grupo Argos fue una de las grandes beneficiadas, con su acción triplicando su valor en dos años para septiembre de 2025. Los accionistas vieron un crecimiento de más del 47% solo en 2025, impulsado por la valorización de la acción y la entrega de dividendos y acciones de Grupo Sura (Grupo Argos, 2025; Valora Analitik, 2025).

ARGOS=read_excel("ARGOS.xlsx") %>% rename(Fecha=1,Cierre=2)
library(plotly)
library(readxl)
library(TTR)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

ARGOS$Fecha <- as.Date(ARGOS$Fecha)

ARGOS$SMA_20 <- SMA(ARGOS$Cierre, n = 20)
ARGOS$SMA_50 <- SMA(ARGOS$Cierre, n = 50)

max_valor <- max(ARGOS$Cierre, na.rm = TRUE)
min_valor <- min(ARGOS$Cierre, na.rm = TRUE)
max_fecha <- ARGOS$Fecha[which.max(ARGOS$Cierre)]
min_fecha <- ARGOS$Fecha[which.min(ARGOS$Cierre)]

fig <- plot_ly(ARGOS, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "ARGOS",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>ARGOS</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Cierre: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo Histórico",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo Histórico",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              GRUPO ARGOS - HISTORICOS</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Precio de cierre con medias móviles y puntos extremos</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO DE CIERRE (COP)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.0f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(ARGOS$Fecha), "%d %b %Y"), 
          " - ", format(max(ARGOS$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", format(round(max_valor, 0), big.mark = ","), 
          " | Mínimo: $", format(round(min_valor, 0), big.mark = ",")
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

Cementos Argos implementó su estrategia “Sprint 2.0”, que incluyó la combinación de activos en EE.UU. con Summit Materials y una conversión masiva de acciones preferenciales a ordinarias. Esto generó valorizaciones superiores al 150% en 2024 y un retorno total en dólares de 480% desde el inicio del programa (Cementos Argos, 2025; Valora Analitik, 2025).

GEB=read_excel("GEB.xlsx") %>% rename(Fecha=1,Cierre=2)
library(plotly)
library(readxl)
library(TTR)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

GEB$Fecha <- as.Date(GEB$Fecha)

GEB$SMA_20 <- SMA(GEB$Cierre, n = 20)
GEB$SMA_50 <- SMA(GEB$Cierre, n = 50)

max_valor <- max(GEB$Cierre, na.rm = TRUE)
min_valor <- min(GEB$Cierre, na.rm = TRUE)
max_fecha <- GEB$Fecha[which.max(GEB$Cierre)]
min_fecha <- GEB$Fecha[which.min(GEB$Cierre)]

fig <- plot_ly(GEB, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "GEB",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>GEB</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Cierre: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo Histórico",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo Histórico",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              GEB - HISTORICOS</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Precio de cierre con medias móviles y puntos extremos</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO DE CIERRE (COP)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.0f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(GEB$Fecha), "%d %b %Y"), 
          " - ", format(max(GEB$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", format(round(max_valor, 0), big.mark = ","), 
          " | Mínimo: $", format(round(min_valor, 0), big.mark = ",")
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

Interconexión Eléctrica S.A. (ISA) y Grupo Energía Bogotá (GEB) fueron piezas clave en la estrategia de muchos portafolios. GEB fue resaltada por su capacidad histórica de generación de caja y su crecimiento en el exterior, siendo una inversión recomendada consistentemente por su perfil defensivo y de crecimiento (La República, 2024). Por su parte, ISA, a pesar de enfrentar retos regulatorios, mantuvo su atractivo por su expansión en energías renovables y proyectos de transmisión eléctrica, asegurando flujos constantes a largo plazo (Casa de Bolsa, 2025). Ambas compañías se beneficiaron del entorno de tasas de interés más bajas, que reduce el costo de su deuda y mejora la valoración de sus flujos de caja futuros.

ISA=read_excel("ISA.xlsx") %>% rename(Fecha=1,Cierre=2)
library(plotly)
library(readxl)
library(TTR)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

ISA$Fecha <- as.Date(ISA$Fecha)

ISA$SMA_20 <- SMA(ISA$Cierre, n = 20)
ISA$SMA_50 <- SMA(ISA$Cierre, n = 50)

max_valor <- max(ISA$Cierre, na.rm = TRUE)
min_valor <- min(ISA$Cierre, na.rm = TRUE)
max_fecha <- ISA$Fecha[which.max(ISA$Cierre)]
min_fecha <- ISA$Fecha[which.min(ISA$Cierre)]

fig <- plot_ly(ISA, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "ISA",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>ISA</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Cierre: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo Histórico",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo Histórico",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              ISA - HISTORICOS</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Precio de cierre con medias móviles y puntos extremos</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO DE CIERRE (COP)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.0f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(ISA$Fecha), "%d %b %Y"), 
          " - ", format(max(ISA$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", format(round(max_valor, 0), big.mark = ","), 
          " | Mínimo: $", format(round(min_valor, 0), big.mark = ",")
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

3.5 Actualidad

El contexto histórico del ICOLCAP entre 2015 y 2025 refleja una montaña rusa de ciclos económicos, crisis y recuperaciones. Desde las profundidades de la crisis petrolera de 2015-2016, pasando por el colapso pandémico de 2020, hasta el rally histórico de 2024-2025, el mercado accionario colombiano ha demostrado tanto vulnerabilidad como resiliencia (Investing.com, 2025; Trading Economics, 2025; Bloomberg Línea, 2022; INCP, 2024; TradingView, 2025; Valora Analitik, 2020; Valora Analitik, 2025a; El Colombiano, 2025).

El momento actual representa un punto de inflexión significativo. Por primera vez en su historia, el MSCI COLCAP ha superado los 2.100 puntos, consolidando un crecimiento de más del 50% en el año (La República, 2025a; Valora Analitik, 2025a; Yahoo Finanzas, 2025). Sin embargo, este éxito debe contextualizarse: es en gran medida una recuperación desde niveles históricamente deprimidos más que un crecimiento sostenido de largo plazo (El Colombiano, 2025).

A pesar del optimismo actual, el mercado colombiano permanece altamente dependiente de variables externas fuera del control de los tomadores de decisión locales. Los precios internacionales de petróleo, oro, y otros commodities continúan siendo determinantes críticos del desempeño de empresas clave como Ecopetrol y Mineros, que representan una porción significativa del índice. Si los precios del petróleo cayeran nuevamente a $60 o $50 por barril (niveles no es inéditos en la historia reciente), el impacto en el mercado sería devastador, deshaciendo los gains de 2024-2025. Esta dependencia de commodities es una limitación estructural que está fuera del alcance de reformas de corto plazo.

ISA=read_excel("MINEROS.xlsx") %>% rename(Fecha=1,Cierre=2) %>% na.omit()
library(plotly)
library(readxl)
library(TTR)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

ISA$Fecha <- as.Date(ISA$Fecha)

ISA$SMA_20 <- SMA(ISA$Cierre, n = 20)
ISA$SMA_50 <- SMA(ISA$Cierre, n = 50)

max_valor <- max(ISA$Cierre, na.rm = TRUE)
min_valor <- min(ISA$Cierre, na.rm = TRUE)
max_fecha <- ISA$Fecha[which.max(ISA$Cierre)]
min_fecha <- ISA$Fecha[which.min(ISA$Cierre)]

fig <- plot_ly(ISA, x = ~Fecha) %>%
  add_lines(
    y = ~Cierre,
    name = "MINEROS",
    line = list(color = css_colors$oro_premium, width = 2.5),
    hovertemplate = paste(
      "<b>MINEROS</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Cierre: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_lines(
    y = ~SMA_20,
    name = "SMA 20",
    line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
    hovertemplate = "SMA 20: $%{y:.2f}<extra></extra>"
  ) %>%
  add_lines(
    y = ~SMA_50,
    name = "SMA 50",
    line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
    hovertemplate = "SMA 50: $%{y:.2f}<extra></extra>"
  ) %>%
  add_markers(
    x = max_fecha,
    y = max_valor,
    name = "Máximo Histórico",
    marker = list(
      color = css_colors$oro_lujo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÁXIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_markers(
    x = min_fecha,
    y = min_valor,
    name = "Mínimo Histórico",
    marker = list(
      color = css_colors$oro_antiguo,
      size = 12,
      symbol = "star",
      line = list(color = css_colors$oro_premium, width = 2)
    ),
    hovertemplate = paste(
      "<b>MÍNIMO HISTÓRICO</b><br>",
      "Fecha: %{x|%d-%b-%Y}<br>",
      "Precio: $%{y:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              MINEROS - HISTORICOS</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Precio de cierre con medias móviles y puntos extremos</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>FECHA</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      type = "date",
      tickformat = "%b %Y"
    ),
    
    yaxis = list(
      title = list(
        text = "<b>PRECIO DE CIERRE (COP)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickprefix = "$",
      tickformat = ",.0f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Período: ", format(min(ISA$Fecha), "%d %b %Y"), 
          " - ", format(max(ISA$Fecha), "%d %b %Y"), " | ",
          "Máximo: $", format(round(max_valor, 0), big.mark = ","), 
          " | Mínimo: $", format(round(min_valor, 0), big.mark = ",")
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    )
  )

fig

La volatilidad política sigue siendo un factor de incertidumbre persistente que ha costado enormemente al mercado en años anteriores. Los próximos cambios de gobierno (2026) podrían introducir nuevos choques regulatorios y fiscales que nuevamente castiguen el mercado. Las políticas del actual gobierno Petro, aunque han moderado su radicalismo inicial respecto a transición energética y tributación, siguen generando incertidumbre. Las potenciales reformas futuras en regulación ambiental, reforma agraria, o regulación laboral podrían afectar el apetito de inversionistas internacionales. El hecho de que el mercado respondió tan dramáticamente a los anuncios de reforma tributaria en 2022-2023 demuestra cuán vulnerable sigue siendo a cambios de política inesperados.

Para que esta tendencia positiva se consolide, Colombia necesitará abordar sus desafíos estructurales: mejorar la profundidad y liquidez del mercado, reducir la carga tributaria sobre las empresas, mantener la estabilidad macroeconómica, y sobre todo, garantizar un entorno institucional predecible y favorable para la inversión privada (Bloomberg Línea, 2022; INCP, 2024; El Colombiano, 2025). El desempeño futuro del ICOLCAP dependerá crucialmente de cómo el país navegue estos desafíos en los años venideros.

4 Resultados del modelo ARIMA

4.1 Ventanas

La primera etapa del análisis consistió en examinar las características de la serie temporal del índice COLCAP para determinar su estacionariedad. La serie fue dividida en dos ventanas temporales: una ventana de entrenamiento que abarca desde el inicio de la serie hasta el 31 de octubre de 2025, y una ventana de prueba que comprende desde el 3 de noviembre de 2025 en adelante. Esta partición permitió reservar observaciones recientes para validar posteriormente la capacidad predictiva del modelo ajustado.

La Figura 1 presenta el gráfico de la serie temporal de la ventana de entrenamiento del índice COLCAP. La visualización revela la presencia de una tendencia creciente sostenida a lo largo del período analizado (Minitab, 2024). Este patrón ascendente en el largo plazo indica que la media de la serie no permanece constante a través del tiempo, sino que exhibe un incremento progresivo en los valores del índice (Minitab, 2024). Adicionalmente, se observan fluctuaciones alrededor de la tendencia que corresponden a la volatilidad característica de los mercados financieros, reflejando movimientos de corto plazo en los precios de las acciones que componen el índice.

ventana<-window(accion, end = ("2025-10-31")) # ventana de entrenamiento
ventana2<-window(accion, start = ("2025-11-03")) # ventana de prueba
library(tidyquant)
library(plotly)
library(xts)
library(scales)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

grafica_icolcap_dinamica <- function(ventana) {
  
  if(inherits(ventana, "xts")) {
    df <- data.frame(
      Fecha = as.Date(index(ventana)), 
      ICOLCAP = as.numeric(coredata(ventana))
    )
  } 
  else if(inherits(ventana, "zoo")) {
    df <- data.frame(
      Fecha = as.Date(time(ventana)),  
      ICOLCAP = as.numeric(coredata(ventana))
    )
  }
  else {
    df <- as.data.frame(ventana)
    if(!inherits(df$Fecha, "Date")) {
      df$Fecha <- as.Date(df$Fecha)
    }
  }
  
  icolcap_xts <- xts::xts(df$ICOLCAP, order.by = df$Fecha)
  
  tryCatch({
    df$SMA_20 <- as.numeric(TTR::SMA(icolcap_xts, n = 20))
    df$SMA_50 <- as.numeric(TTR::SMA(icolcap_xts, n = 50))
    bb <- TTR::BBands(icolcap_xts, n = 20, sd = 2)
    if(!is.null(bb)) {
      df$BB_Lower <- as.numeric(bb$dn)
      df$BB_Middle <- as.numeric(bb$mavg)
      df$BB_Upper <- as.numeric(bb$up)
    } else {
      df$BB_Lower <- df$ICOLCAP
      df$BB_Middle <- df$ICOLCAP
      df$BB_Upper <- df$ICOLCAP
    }
  }, error = function(e) {
    df$SMA_20 <- df$ICOLCAP
    df$SMA_50 <- df$ICOLCAP
    df$BB_Lower <- df$ICOLCAP
    df$BB_Middle <- df$ICOLCAP
    df$BB_Upper <- df$ICOLCAP
  })
  
  df <- na.omit(df)
  
  max_valor <- max(df$ICOLCAP, na.rm = TRUE)
  min_valor <- min(df$ICOLCAP, na.rm = TRUE)
  media_valor <- mean(df$ICOLCAP, na.rm = TRUE)
  ultimo_valor <- tail(df$ICOLCAP, 1)
  primer_valor <- head(df$ICOLCAP, 1)
  variacion_total <- ((ultimo_valor - primer_valor) / primer_valor) * 100
  
  fig <- plot_ly(df, x = ~Fecha)
  
  fig <- fig %>%
    add_lines(
      y = ~ICOLCAP,
      name = "ICOLCAP",
      line = list(color = css_colors$oro_premium, width = 2.5),
      hovertemplate = paste(
        "<b>ICOLCAP</b><br>",
        "Fecha: %{x|%d-%b-%Y}<br>",
        "Precio: %{y:.2f} pts<br>",
        "<extra></extra>"
      )
    ) %>%
    add_lines(
      y = ~SMA_20,
      name = "SMA 20 días",
      line = list(color = css_colors$oro_lujo, width = 1.5, dash = "dash"),
      hovertemplate = "SMA-20: %{y:.2f}<extra></extra>"
    ) %>%
    add_lines(
      y = ~SMA_50,
      name = "SMA 50 días",
      line = list(color = css_colors$texto_claro, width = 1.5, dash = "dot"),
      hovertemplate = "SMA-50: %{y:.2f}<extra></extra>"
    )
  
  fig <- fig %>%
    add_markers(
      data = df[which.max(df$ICOLCAP), ],
      x = ~Fecha,
      y = ~ICOLCAP,
      name = "Máximo",
      marker = list(
        color = css_colors$oro_lujo,
        size = 10,
        symbol = "diamond",
        line = list(color = css_colors$oro_premium, width = 2)
      ),
      hovertemplate = paste(
        "<b>MÁXIMO</b><br>",
        "Fecha: %{x|%d-%b-%Y}<br>",
        "Precio: %{y:.2f} pts<br>",
        "<extra></extra>"
      )
    ) %>%
    add_markers(
      data = df[which.min(df$ICOLCAP), ],
      x = ~Fecha,
      y = ~ICOLCAP,
      name = "Mínimo",
      marker = list(
        color = css_colors$oro_antiguo,
        size = 10,
        symbol = "diamond",
        line = list(color = css_colors$oro_premium, width = 2)
      ),
      hovertemplate = paste(
        "<b>MÍNIMO</b><br>",
        "Fecha: %{x|%d-%b-%Y}<br>",
        "Precio: %{y:.2f} pts<br>",
        "<extra></extra>"
      )
    ) %>%
    add_markers(
      data = tail(df, 1),
      x = ~Fecha,
      y = ~ICOLCAP,
      name = "Actual",
      marker = list(
        color = css_colors$texto_claro,
        size = 8,
        symbol = "circle",
        line = list(color = css_colors$oro_premium, width = 2)
      ),
      hovertemplate = paste(
        "<b>ACTUAL</b><br>",
        "Fecha: %{x|%d-%b-%Y}<br>",
        "Precio: %{y:.2f} pts<br>",
        "<extra></extra>"
      )
    )
  
  fig <- fig %>%
    layout(
      title = list(
        text = paste(
          "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>",
          "Figura 1. - VENTANA DE ENTRENAMIENTO </span><br>",
          "<span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>",
          "Período: ", format(min(df$Fecha), "%d %b %Y"), 
          " - ", format(max(df$Fecha), "%d %b %Y"),
          " | Variación: ", sprintf("%+.2f%%", variacion_total), "</span>"
        ),
        x = 0.5,
        xanchor = 'center'
      ),
      
      xaxis = list(
        title = list(
          text = "<b>FECHA</b>",
          font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
        ),
        gridcolor = css_colors$grilla_oscura,
        linecolor = css_colors$oro_premium,
        tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
        showgrid = TRUE,
        showline = TRUE,
        zeroline = FALSE,
        type = "date",
        tickformat = "%b %Y"
      ),
      
      yaxis = list(
        title = list(
          text = "<b>VALOR DEL ÍNDICE (PUNTOS)</b>",
          font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
        ),
        gridcolor = css_colors$grilla_oscura,
        tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
        linecolor = css_colors$oro_premium,
        zerolinecolor = css_colors$grilla_oscura,
        showline = TRUE,
        zeroline = FALSE,
        tickformat = ",.2f"
      ),
      
      plot_bgcolor = css_colors$fondo_negro,
      paper_bgcolor = css_colors$fondo_negro,
      
      hoverlabel = list(
        bgcolor = 'rgba(26, 26, 26, 0.9)',
        bordercolor = css_colors$oro_premium,
        font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
      ),
      
      margin = list(l = 80, r = 80, t = 120, b = 100),
      
      legend = list(
        orientation = 'h',
        x = 0.5,
        xanchor = 'center',
        y = -0.2,
        bgcolor = 'rgba(0,0,0,0)',
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12)
      ),
      
      annotations = list(
        list(
          x = 0.5,
          y = 1.05,
          xref = "paper",
          yref = "paper",
          text = paste(
            "Máximo: ", round(max_valor, 2), 
            " | Mínimo: ", round(min_valor, 2),
            " | Media: ", round(media_valor, 2)
          ),
          showarrow = FALSE,
          font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
          bgcolor = 'rgba(26, 26, 26, 0.7)',
          bordercolor = css_colors$oro_premium,
          borderwidth = 1,
          borderpad = 8
        )
      ),
      
      shapes = list(
        list(
          type = 'line',
          x0 = min(df$Fecha),
          x1 = max(df$Fecha),
          y0 = media_valor,
          y1 = media_valor,
          line = list(color = 'rgba(212, 175, 55, 0.5)', width = 1, dash = 'dash')
        )
      )
    )
  
  return(fig)
}

grafica_final <- grafica_icolcap_dinamica(ventana)
grafica_final

La presencia de esta tendencia creciente constituye un indicio preliminar de no estacionariedad en la serie, dado que una serie estacionaria debe mantener propiedades estadísticas constantes a lo largo del tiempo, incluyendo una media estable (NumXL, 2024). Este comportamiento es típico en series financieras de precios e índices bursátiles, donde los valores tienden a presentar deriva estocástica o tendencias deterministas que impiden la estacionariedad.

La Figura 2 muestra el correlograma de la función de autocorrelación (ACF) para la serie original. El gráfico ACF exhibe un patrón característico de decaimiento lento en las autocorrelaciones a medida que aumenta el número de rezagos. Las autocorrelaciones permanecen significativamente diferentes de cero y positivas durante un número considerable de rezagos, superando las bandas de confianza y decayendo de manera gradual sin converger rápidamente hacia cero (Miranda, 2021).

library(plotly)
library(forecast)

css_acf <- list(
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  luz_oro = "#FFF8DC",
  puro_blanco = "#FFFFFF"
)


add_alpha <- function(color, alpha) {
  rgb_col <- col2rgb(color)
  rgb(rgb_col[1], rgb_col[2], rgb_col[3], alpha = alpha * 255, maxColorValue = 255)
}

acf_data <- Acf(ventana, lag.max = 80, plot = FALSE)
confianza <- 2/sqrt(length(ventana))

df_acf <- data.frame(
  Lag = 0:80,
  ACF = as.numeric(acf_data$acf),
  Significativo = abs(as.numeric(acf_data$acf)) > confianza,
  Color = ifelse(abs(as.numeric(acf_data$acf)) > confianza, 
                 css_acf$oro_lujo, css_acf$oro_premium)
)

fig <- plot_ly(df_acf, x = ~Lag)

fig <- fig %>%
  add_bars(
    y = ~ACF,
    marker = list(
      color = ~Color,
      line = list(color = css_acf$oro_premium, width = 1.5)
    ),
    name = "ACF",
    hovertemplate = paste(
      "<b>Lag: %{x}</b><br>",
      "ACF: %{y:.3f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  add_trace(
    x = c(-1, 41),
    y = c(confianza, confianza),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$oro_antiguo, dash = 'dash', width = 1.5),
    name = 'Límite Superior',
    hoverinfo = 'none'
  )

fig <- fig %>%
  add_trace(
    x = c(-1, 41),
    y = c(-confianza, -confianza),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$oro_antiguo, dash = 'dash', width = 1.5),
    name = 'Límite Inferior',
    hoverinfo = 'none'
  )

fig <- fig %>%
  add_trace(
    x = c(-1, 41),
    y = c(0, 0),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$luz_oro, width = 1),
    name = 'Línea Cero',
    hoverinfo = 'none'
  )

fig <- fig %>%
  add_markers(
    data = subset(df_acf, Significativo & Lag > 0),
    x = ~Lag,
    y = ~ACF,
    marker = list(
      color = css_acf$oro_lujo,
      size = 10,
      symbol = 'diamond',
      line = list(color = css_acf$oro_premium, width = 2)
    ),
    name = 'Significativo',
    hovertemplate = paste(
      "<b>¡SIGNIFICATIVO!</b><br>",
      "Lag: %{x}<br>",
      "ACF: %{y:.3f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              Figura 2. FUNCIÓN DE AUTOCORRELACIÓN (ACF) - ICOLCAP</span><br>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>REZAGO (LAGS)</b>",
        font = list(family = 'Cinzel', color = css_acf$oro_premium, size = 14)
      ),
      gridcolor = css_acf$grilla_oscura,
      linecolor = css_acf$oro_premium,
      tickfont = list(family = 'Inter', color = css_acf$luz_oro, size = 11),
      zeroline = FALSE,
      showgrid = TRUE,
      range = c(-0.5, 80.5),
      dtick = 5
    ),
    
    yaxis = list(
      title = list(
        text = "<b>COEFICIENTE ACF</b>",
        font = list(family = 'Cinzel', color = css_acf$oro_premium, size = 14)
      ),
      gridcolor = css_acf$grilla_oscura,
      linecolor = css_acf$oro_premium,
      tickfont = list(family = 'Inter', color = css_acf$luz_oro, size = 11),
      range = c(-1.1, 1.1),
      tickvals = seq(-1, 1, 0.2),
      tickformat = '.1f',
      zeroline = FALSE,
      showgrid = TRUE
    ),
    
    plot_bgcolor = css_acf$fondo_negro,
    paper_bgcolor = css_acf$fondo_negro,
    
    hoverlabel = list(
      bgcolor = add_alpha(css_acf$panel_negro, 0.9),
      bordercolor = css_acf$oro_premium,
      font = list(family = 'Inter', color = css_acf$luz_oro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(0,0,0,0)',
      font = list(family = 'Cinzel', color = css_acf$oro_premium, size = 12)
    ),
    
    annotations = list(
      list(
        x = 1,
        y = 1.05,
        xref = 'paper',
        yref = 'paper',
        text = paste(
          "Intervalo de confianza: 95%<br>",
          "Lags analizados: 80<br>",
          "N observaciones:", length(ventana), "<br>",
          "Límite confianza: ±", round(confianza, 3)
        ),
        showarrow = FALSE,
        align = 'right',
        font = list(
          family = 'Playfair Display',
          size = 12,
          color = css_acf$luz_oro
        ),
        bgcolor = add_alpha(css_acf$panel_negro, 0.7),
        bordercolor = css_acf$oro_premium,
        borderwidth = 1,
        borderpad = 10
      )
    ),
    
    shapes = list(
      list(
        type = 'rect',
        x0 = -0.5,
        x1 = 80.5,
        y0 = -confianza,
        y1 = confianza,
        fillcolor = add_alpha(css_acf$oro_premium, 0.05),
        line = list(width = 0)
      )
    )
  )

fig

Este patrón de decaimiento lento en la ACF es un indicador robusto de no estacionariedad en la serie temporal (Miranda, 2021; UCM, 2013). En particular, cuando las autocorrelaciones muestrales decaen de forma lenta y presentan valores elevados en los primeros rezagos, esto sugiere fuertemente la presencia de una raíz unitaria en el proceso generador de datos, lo cual implica que la serie posee una deriva estocástica (Universidad de Vigo, s.f.; NumXL, 2024). Las series con raíz unitaria no revierten a una media constante y tienden a alejarse indefinidamente de cualquier valor inicial, característica incompatible con la estacionariedad (NumXL, 2024).

En contraste, una serie estacionaria mostraría autocorrelaciones que decaen rápidamente hacia cero después de algunos rezagos, permaneciendo dentro de las bandas de confianza. El comportamiento observado en la ACF del COLCAP confirma visualmente la necesidad de aplicar transformaciones a la serie original para alcanzar la estacionariedad requerida por los modelos ARIMA.

Para complementar el análisis gráfico y proporcionar evidencia estadística formal sobre la estacionariedad de la serie podemos verlo en la tabla 1, se aplicó la prueba de Dickey-Fuller Aumentada (ADF). Los resultados de la prueba ADF se interpretan examinando el valor p obtenido. Si el valor p es mayor que el nivel de significancia convencional de 0.05, la decisión estadística es no rechazar la hipótesis nula, concluyendo que existe evidencia insuficiente para afirmar que la serie es estacionaria (Minitab, 2024; NumXL, 2024). En el caso de la serie original del COLCAP, los resultados de la prueba ADF confirman estadísticamente lo observado en los análisis gráficos previos: la serie no es estacionaria (NumXL, 2024).

library(kableExtra)
library(tseries)

resultado_adf <- adf.test(ventana)

df_valor <- round(resultado_adf$statistic, 6)
p_valor <- ifelse(resultado_adf$p.value < 0.001, "< 0.001", 
                  sprintf("%.6f", resultado_adf$p.value))
lags_valor <- resultado_adf$parameter
n_obs <- length(ventana) - lags_valor

es_significativo <- resultado_adf$p.value < 0.05
color_significancia <- ifelse(es_significativo, "#D4AF37", "#FF6B6B")
simbolo_significancia <- ifelse(es_significativo, "✓", "✗")
texto_significancia <- ifelse(es_significativo, "Significativo (rechaza H₀ a α = 0.05)", "No significativo")

df_tabla <- data.frame(
  Estadístico = c(
    "Dickey-Fuller Aumentado",
    "Valor p",
    "Retrasos (Lags)",
    "Observaciones"
  ),
  Valor = c(
    sprintf("%.6f", df_valor),
    p_valor,
    as.character(lags_valor),
    as.character(n_obs)
  ),
  Interpretación = c(
    "Estadístico de prueba para H₀: raíz unitaria",
    ifelse(es_significativo,
           paste0("<span style='color:", color_significancia, "; font-weight:bold;'>", 
                  simbolo_significancia, " ", texto_significancia, "</span>"),
           paste0("<span style='color:", color_significancia, ";'>", 
                  simbolo_significancia, " ", texto_significancia, "</span>")),
    "Número de retardos utilizados",
    "Tamaño efectivo de muestra"
  ),
  stringsAsFactors = FALSE
)

tabla_html <- kable(
  df_tabla,
  align = c("l", "c", "l"),
  col.names = c("**Estadístico**", "**Valor**", "**Interpretación**"),
  escape = FALSE,
  format = "html"
)

tabla_estilizada <- tabla_html %>%
  kable_styling(
    bootstrap_options = c("striped", "hover"),
    full_width = FALSE,
    position = "center",
    font_size = 16,
    html_font = "'Inter', sans-serif"
  )

tabla_estilizada <- tabla_estilizada %>%
  row_spec(0, 
           background = "#D4AF37",
           color = "#0A0A0A",
           bold = TRUE,
           font_size = 18)

for(i in 1:4) {
  color_fondo <- ifelse(i %% 2 == 1, "#1A1A1A", "#2C2C2C")
  
  if(i == 2) {  
    tabla_estilizada <- tabla_estilizada %>%
      row_spec(i, 
               color = "#F5F5F5",
               background = color_fondo,
               bold = FALSE)
  } else {
    tabla_estilizada <- tabla_estilizada %>%
      row_spec(i, 
               color = "#F5F5F5",
               background = color_fondo)
  }
}

tabla_estilizada <- tabla_estilizada %>%
  column_spec(1, 
              color = "#D4AF37",
              bold = TRUE) %>%
  column_spec(2, 
              color = "#FFFFFF",
              bold = TRUE) %>%
  column_spec(3, 
              color = "#E0E0E0",
              bold = FALSE)

conclusion <- ifelse(es_significativo,
                    "La serie es ESTACIONARIA (rechaza H₀)",
                    "La serie NO ES ESTACIONARIA (no rechaza H₀)")

tabla_final <- tabla_estilizada

tabla_final
Estadístico Valor Interpretación
Dickey-Fuller Aumentado -1.847289 Estadístico de prueba para H₀: raíz unitaria
Valor p 0.642979 ✗ No significativo
Retrasos (Lags) 13 Número de retardos utilizados
Observaciones 2630 Tamaño efectivo de muestra

Este hallazgo indica que la serie posee una raíz unitaria y presenta deriva estocástica, manteniendo una tendencia que impide que sus propiedades estadísticas permanezcan constantes a través del tiempo (NumXL, 2024). La conclusión derivada de la prueba ADF es que se requiere aplicar diferenciación a la serie para remover la tendencia y lograr estacionariedad (Minitab, 2024).

4.2 Difenciación de la serie

Como se concluyó en el análisis anterior, la serie original del COLCAP requería diferenciación para alcanzar la estacionariedad. Por lo tanto, se procedió a aplicar una diferenciación de primer orden a la serie, La Figura 3 presenta el gráfico de la serie temporal después de aplicar la diferenciación de primer orden. La visualización muestra un cambio drástico en el comportamiento de la serie comparado con la serie original (Minitab, 2024). La tendencia creciente observada anteriormente ha sido eliminada completamente, y la serie diferenciada ahora fluctúa alrededor de una media constante cercana a cero como vemos en la figura 4 (Minitab, 2024; MathWorks, 2025).

library(plotly)
library(xts)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700", 
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

add_alpha <- function(color, alpha) {
  rgb_col <- col2rgb(color)
  rgb(rgb_col[1], rgb_col[2], rgb_col[3], alpha = alpha * 255, maxColorValue = 255)
}

dif1serie <- diff(ventana) %>% na.omit()

grafica_dif1serie_dinamica <- function(dif1serie) {
  
  if(inherits(dif1serie, "xts")) {
    df <- data.frame(
      Fecha = as.Date(index(dif1serie)), 
      Cambio = as.numeric(coredata(dif1serie))
    )
  } 
  else if(inherits(dif1serie, "zoo")) {
    df <- data.frame(
      Fecha = as.Date(time(dif1serie)),  
      Cambio = as.numeric(coredata(dif1serie))
    )
  }
  else {
    df <- as.data.frame(dif1serie)
    if(!inherits(df$Fecha, "Date")) {
      df$Fecha <- as.Date(df$Fecha)
    }
  }
  
  media_cambio <- mean(df$Cambio, na.rm = TRUE)
  desv_cambio <- sd(df$Cambio, na.rm = TRUE)
  max_cambio <- max(df$Cambio, na.rm = TRUE)
  min_cambio <- min(df$Cambio, na.rm = TRUE)
  positivo <- sum(df$Cambio > 0, na.rm = TRUE)
  negativo <- sum(df$Cambio < 0, na.rm = TRUE)
  neutro <- sum(df$Cambio == 0, na.rm = TRUE)
  
  df$Color <- ifelse(df$Cambio > 0, css_colors$oro_lujo,
                     ifelse(df$Cambio < 0, css_colors$oro_antiguo, css_colors$texto_claro))
  
  fig <- plot_ly(df, x = ~Fecha)
  
  fig <- fig %>%
    add_bars(
      y = ~Cambio,
      marker = list(
        color = ~Color,
        line = list(color = css_colors$oro_premium, width = 1)
      ),
      name = "Cambio Diario",
      hovertemplate = paste(
        "<b>Cambio Diario</b><br>",
        "Fecha: %{x|%d-%b-%Y}<br>",
        "Cambio: %{y:.2f} pts<br>",
        "<extra></extra>"
      )
    )
  
  fig <- fig %>%
    add_lines(
      y = rep(0, nrow(df)),
      x = ~Fecha,
      name = "Línea Cero",
      line = list(color = css_colors$texto_claro, width = 1, dash = "dash"),
      hovertemplate = "Línea Cero<extra></extra>"
    ) %>%
    add_lines(
      y = rep(media_cambio, nrow(df)),
      x = ~Fecha,
      name = "Media Cambios",
      line = list(color = css_colors$oro_premium, width = 1.5, dash = "dot"),
      hovertemplate = paste("Media: ", round(media_cambio, 2), "<extra></extra>")
    ) %>%
    add_lines(
      y = rep(media_cambio + desv_cambio, nrow(df)),
      x = ~Fecha,
      name = "+1 Desviación",
      line = list(color = add_alpha(css_colors$oro_lujo, 0.5), width = 1, dash = "dash"),
      hovertemplate = paste("+1σ: ", round(media_cambio + desv_cambio, 2), "<extra></extra>")
    ) %>%
    add_lines(
      y = rep(media_cambio - desv_cambio, nrow(df)),
      x = ~Fecha,
      name = "-1 Desviación",
      line = list(color = add_alpha(css_colors$oro_antiguo, 0.5), width = 1, dash = "dash"),
      hovertemplate = paste("-1σ: ", round(media_cambio - desv_cambio, 2), "<extra></extra>")
    )
  
  fig <- fig %>%
    add_markers(
      data = df[which.max(df$Cambio), ],
      x = ~Fecha,
      y = ~Cambio,
      name = "Mayor Alza",
      marker = list(
        color = css_colors$oro_lujo,
        size = 12,
        symbol = "star",
        line = list(color = css_colors$oro_premium, width = 2)
      ),
      hovertemplate = paste(
        "<b>MAYOR ALZA</b><br>",
        "Fecha: %{x|%d-%b-%Y}<br>",
        "Cambio: %{y:.2f} pts<br>",
        "<extra></extra>"
      )
    ) %>%
    add_markers(
      data = df[which.min(df$Cambio), ],
      x = ~Fecha,
      y = ~Cambio,
      name = "Mayor Baja",
      marker = list(
        color = css_colors$oro_antiguo,
        size = 12,
        symbol = "star",
        line = list(color = css_colors$oro_premium, width = 2)
      ),
      hovertemplate = paste(
        "<b>MAYOR BAJA</b><br>",
        "Fecha: %{x|%d-%b-%Y}<br>",
        "Cambio: %{y:.2f} pts<br>",
        "<extra></extra>"
      )
    )
  
  fig <- fig %>%
    layout(
      title = list(
        text = paste(
          "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>",
          "Figura 3. - Serie diferenciada (ΔYₜ = Yₜ - Yₜ₋₁)</span><br>",
          "<span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>",
          "Cambios Diarios | Estacionariedad | Volatilidad</span>"
        ),
        x = 0.5,
        xanchor = 'center'
      ),
      
      xaxis = list(
        title = list(
          text = "<b>FECHA</b>",
          font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
        ),
        gridcolor = css_colors$grilla_oscura,
        linecolor = css_colors$oro_premium,
        tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
        showgrid = TRUE,
        showline = TRUE,
        zeroline = FALSE,
        type = "date",
        tickformat = "%b %Y"
      ),
      
      yaxis = list(
        title = list(
          text = "<b>CAMBIO DIARIO (PUNTOS)</b>",
          font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
        ),
        gridcolor = css_colors$grilla_oscura,
        tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
        linecolor = css_colors$oro_premium,
        zerolinecolor = css_colors$grilla_oscura,
        showline = TRUE,
        zeroline = FALSE,
        tickformat = ",.2f"
      ),
      
      plot_bgcolor = css_colors$fondo_negro,
      paper_bgcolor = css_colors$fondo_negro,
      
      hoverlabel = list(
        bgcolor = 'rgba(26, 26, 26, 0.9)',
        bordercolor = css_colors$oro_premium,
        font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
      ),
      
      margin = list(l = 80, r = 80, t = 120, b = 100),
      
      legend = list(
        orientation = 'h',
        x = 0.5,
        xanchor = 'center',
        y = -0.25,
        bgcolor = 'rgba(26, 26, 26, 0.8)',
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
        bordercolor = css_colors$oro_premium,
        borderwidth = 1
      ),
      
      annotations = list(
        list(
          x = 0.5,
          y = 1.05,
          xref = "paper",
          yref = "paper",
          text = paste(
            "Estadísticas: ",
            "Media: ", round(media_cambio, 3), 
            " | Desv: ", round(desv_cambio, 3),
            " | Positivos: ", positivo, 
            " | Negativos: ", negativo,
            " | Neutros: ", neutro
          ),
          showarrow = FALSE,
          font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
          bgcolor = 'rgba(26, 26, 26, 0.7)',
          bordercolor = css_colors$oro_premium,
          borderwidth = 1,
          borderpad = 8
        )
      ),
      
      shapes = list(
        list(
          type = 'rect',
          x0 = min(df$Fecha),
          x1 = max(df$Fecha),
          y0 = media_cambio - desv_cambio,
          y1 = media_cambio + desv_cambio,
          fillcolor = add_alpha(css_colors$oro_premium, 0.1),
          line = list(width = 0)
        )
      )
    )
  
  return(fig)
}

grafica_dif1 <- grafica_dif1serie_dinamica(dif1serie)
grafica_dif1

Este patrón de fluctuaciones alrededor de una media estable es característico de una serie estacionaria (Minitab, 2024). Las oscilaciones representan los cambios diarios en el valor del índice COLCAP, reflejando la volatilidad natural del mercado sin la presencia de una deriva de largo plazo. La amplitud de las fluctuaciones parece mantenerse relativamente constante a lo largo del tiempo, lo que sugiere homocedasticidad o varianza constante, otra propiedad fundamental de la estacionariedad (Minitab, 2024; NumXL, 2024).

La Figura 4 muestra el correlograma de la función de autocorrelación (ACF) para la serie diferenciada. El gráfico presenta un cambio notable respecto al ACF de la serie original. Ahora se observa un decaimiento rápido de las autocorrelaciones hacia cero, con la mayoría de los rezagos cayendo dentro de las bandas de confianza después de los primeros rezagos (Miranda, 2021).

library(plotly)
library(forecast)

css_acf <- list(
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  luz_oro = "#FFF8DC",
  puro_blanco = "#FFFFFF"
)

acf_dif_data <- Acf(dif1serie, lag.max = 40, plot = FALSE)
confianza <- 2/sqrt(length(dif1serie))

df_acf <- data.frame(
  Lag = 0:40,
  ACF = as.numeric(acf_dif_data$acf),
  Significativo = abs(as.numeric(acf_dif_data$acf)) > confianza,
  Color = ifelse(abs(as.numeric(acf_dif_data$acf)) > confianza, 
                 css_acf$oro_lujo, css_acf$oro_premium)
)

df_acf <- df_acf %>% filter(Lag >= 1)

max_acf <- max(abs(df_acf$ACF), na.rm = TRUE)
y_range <- max(0.2, max_acf * 1.2)

fig <- plot_ly(df_acf, x = ~Lag)

fig <- fig %>%
  add_bars(
    y = ~ACF,
    marker = list(
      color = ~Color,
      line = list(color = css_acf$oro_premium, width = 1.5)
    ),
    name = "ACF",
    hovertemplate = paste(
      "<b>Lag: %{x}</b><br>",
      "ACF: %{y:.3f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  add_trace(
    x = c(0.5, 40.5),
    y = c(confianza, confianza),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$oro_antiguo, dash = 'dash', width = 1.5),
    name = 'Límite Superior',
    hoverinfo = 'none'
  ) %>%
  add_trace(
    x = c(0.5, 40.5),
    y = c(-confianza, -confianza),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$oro_antiguo, dash = 'dash', width = 1.5),
    name = 'Límite Inferior',
    hoverinfo = 'none'
  ) %>%
  add_trace(
    x = c(0.5, 40.5),
    y = c(0, 0),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$luz_oro, width = 1),
    name = 'Línea Cero',
    hoverinfo = 'none'
  )

fig <- fig %>%
  add_markers(
    data = subset(df_acf, Significativo),
    x = ~Lag,
    y = ~ACF,
    marker = list(
      color = css_acf$oro_lujo,
      size = 10,
      symbol = 'diamond',
      line = list(color = css_acf$oro_premium, width = 2)
    ),
    name = 'Significativo',
    hovertemplate = paste(
      "<b>¡SIGNIFICATIVO!</b><br>",
      "Lag: %{x}<br>",
      "ACF: %{y:.3f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              Figura 4. FUNCIÓN DE AUTOCORRELACIÓN (ACF) - SERIE DIFERENCIADA ICOLCAP</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Análisis de dependencia temporal de la primera diferencia</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>REZAGO (LAGS)</b>",
        font = list(family = 'Cinzel', color = css_acf$oro_premium, size = 14)
      ),
      gridcolor = css_acf$grilla_oscura,
      linecolor = css_acf$oro_premium,
      tickfont = list(family = 'Inter', color = css_acf$luz_oro, size = 11),
      zeroline = FALSE,
      showgrid = TRUE,
      range = c(0.5, 40.5),
      dtick = 5
    ),
    
    yaxis = list(
      title = list(
        text = "<b>COEFICIENTE ACF</b>",
        font = list(family = 'Cinzel', color = css_acf$oro_premium, size = 14)
      ),
      gridcolor = css_acf$grilla_oscura,
      linecolor = css_acf$oro_premium,
      tickfont = list(family = 'Inter', color = css_acf$luz_oro, size = 11),
      range = c(-y_range, y_range),
      tickformat = '.3f',
      zeroline = FALSE,
      showgrid = TRUE,
      tickvals = seq(-round(y_range, 1), round(y_range, 1), by = 0.05)
    ),
    
    plot_bgcolor = css_acf$fondo_negro,
    paper_bgcolor = css_acf$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_acf$oro_premium,
      font = list(family = 'Inter', color = css_acf$luz_oro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.2,
      bgcolor = 'rgba(0,0,0,0)',
      font = list(family = 'Cinzel', color = css_acf$oro_premium, size = 12)
    ),
    
    annotations = list(
      list(
        x = 1,
        y = 1.05,
        xref = 'paper',
        yref = 'paper',
        text = paste(
          "Intervalo de confianza: 95%<br>",
          "Lags analizados: 1-40<br>",
          "N observaciones:", length(dif1serie), "<br>",
          "Límite confianza: ±", round(confianza, 3)
        ),
        showarrow = FALSE,
        align = 'right',
        font = list(
          family = 'Playfair Display',
          size = 12,
          color = css_acf$luz_oro
        ),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_acf$oro_premium,
        borderwidth = 1,
        borderpad = 10
      )
    ),
    
    shapes = list(
      list(
        type = 'rect',
        x0 = 0.5,
        x1 = 40.5,
        y0 = -confianza,
        y1 = confianza,
        fillcolor = 'rgba(212, 175, 55, 0.05)',
        line = list(width = 0)
      )
    )
  )

fig

Este patrón de decaimiento rápido en la ACF es un indicador fuerte de estacionariedad en la serie diferenciada (Miranda, 2021). A diferencia del decaimiento lento observado en la serie original, la serie diferenciada muestra autocorrelaciones que se aproximan rápidamente a cero, indicando que la dependencia temporal entre observaciones distantes es débil o inexistente (UCM, 2013). Únicamente algunos rezagos iniciales presentan autocorrelaciones estadísticamente significativas (que exceden las bandas de confianza), lo cual es normal en series financieras y proporciona información valiosa para la identificación de los órdenes p y q del modelo ARIMA, en nuestro caso vemos que el rezago 2 es significativo. (Universidad de Vigo, s.f.).

El comportamiento del correlograma ACF sugiere que la diferenciación ha sido exitosa en remover la no estacionariedad presente en la serie original, transformándola en una serie con propiedades estadísticas estables a través del tiempo (Miranda, 2021).Para confirmar formalmente la estacionariedad de la serie diferenciada, se aplicó nuevamente la prueba de Dickey-Fuller Aumentada (ADF). Los resultados de esta prueba en la serie diferenciada contrastan marcadamente con los obtenidos para la serie original, vemos los resultados en la tabla 2 (NumXL, 2024; Minitab, 2024).

library(gt)
library(tseries)

resultado_adf_dif1 <- adf.test(dif1serie)

df_valor <- resultado_adf_dif1$statistic
p_valor <- resultado_adf_dif1$p.value
lags_valor <- resultado_adf_dif1$parameter
n_obs <- length(dif1serie) - lags_valor

es_significativo <- p_valor < 0.05

adf_data <- data.frame(
  Estadístico = c(
    "Dickey-Fuller Aumentado",
    "Valor p",
    "Retrasos (Lags)",
    "Observaciones"
  ),
  Valor = c(
    sprintf("%.6f", df_valor),
    ifelse(p_valor < 0.001, "< 0.001", sprintf("%.6f", p_valor)),
    as.character(lags_valor),
    as.character(n_obs)
  ),
  Interpretación = c(
    "Estadístico de prueba para H₀: raíz unitaria en Δ¹(serie)",
    ifelse(es_significativo, "✓ Significativo (rechaza H₀)", "✗ No significativo"),
    "Número de retardos utilizados en la regresión",
    "Tamaño efectivo de muestra analizada"
  )
)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37",
  verde_exito = "#00FF00",
  rojo_error = "#FF6B6B"
)

adf_gt <- adf_data %>%
  gt() %>%
  tab_header(
    title = md("**Tabla 2. RESULTADOS PRUEBA ADF - SERIE DIFERENCIADA**"),
    subtitle = md("Augmented Dickey-Fuller Test en Δ¹(serie)")
  ) %>%
  cols_label(
    Estadístico = md("**ESTADÍSTICO**"),
    Valor = md("**VALOR**"),
    Interpretación = md("**INTERPRETACIÓN**")
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$oro_premium,
        font = "Cinzel",
        weight = "bold",
        size = "x-large"
      ),
      cell_fill(color = css_colors$panel_negro),
      cell_borders(sides = "all", color = css_colors$oro_premium, weight = px(2))
    ),
    locations = cells_title(groups = "title")
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$texto_claro,
        font = "Inter",
        size = "medium"
      ),
      cell_fill(color = css_colors$panel_negro)
    ),
    locations = cells_title(groups = "subtitle")
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$fondo_negro,
        font = "Cinzel",
        weight = "bold",
        size = "medium",
        align = "center"
      ),
      cell_fill(color = css_colors$oro_premium),
      cell_borders(sides = "all", color = css_colors$oro_antiguo, weight = px(1))
    ),
    locations = cells_column_labels(columns = everything())
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = css_colors$panel_negro),
      cell_text(color = css_colors$texto_claro),
      cell_borders(sides = "bottom", color = css_colors$grilla_oscura)
    ),
    locations = cells_body(
      rows = c(1, 3)
    )
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = css_colors$fondo_negro),
      cell_text(color = css_colors$texto_claro),
      cell_borders(sides = "bottom", color = css_colors$grilla_oscura)
    ),
    locations = cells_body(
      rows = c(2, 4)
    )
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = ifelse(es_significativo, css_colors$verde_exito, css_colors$rojo_error),
        weight = "bold"
      )
    ),
    locations = cells_body(
      columns = c(Valor, Interpretación),
      rows = 2
    )
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$oro_lujo,
        font = "Cinzel",
        weight = "bold"
      )
    ),
    locations = cells_body(
      columns = Estadístico
    )
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$texto_claro,
        weight = "bold",
        align = "center"
      )
    ),
    locations = cells_body(
      columns = Valor
    )
  ) %>%
  tab_footnote(
    footnote = md(paste0(
      "**Hipótesis Nula (H₀):** La primera diferencia de la serie (Δ¹) tiene una raíz unitaria (no es estacionaria)<br>",
      "**Conclusión:** ",
      ifelse(es_significativo, "✓ Δ¹(serie) ES ESTACIONARIA", "✗ Δ¹(serie) NO ES ESTACIONARIA"),
      ifelse(es_significativo, 
             " (Se rechaza H₀ - Primera diferencia estacionaria)", 
             " (No se rechaza H₀ - Se requiere mayor diferenciación)")
    )),
    locations = cells_title(groups = "title")
  ) %>%
  tab_options(
    table.background.color = css_colors$fondo_negro,
    table.border.top.style = "solid",
    table.border.top.color = css_colors$oro_premium,
    table.border.top.width = px(2),
    table.border.bottom.style = "solid",
    table.border.bottom.color = css_colors$oro_premium,
    table.border.bottom.width = px(2),
    table.border.left.style = "solid",
    table.border.left.color = css_colors$oro_premium,
    table.border.left.width = px(2),
    table.border.right.style = "solid",
    table.border.right.color = css_colors$oro_premium,
    table.border.right.width = px(2),
    heading.border.bottom.style = "solid",
    heading.border.bottom.color = css_colors$oro_premium,
    heading.border.bottom.width = px(2),
    heading.padding = px(15),
    footnotes.border.bottom.style = "solid",
    footnotes.border.bottom.color = css_colors$oro_premium,
    footnotes.border.bottom.width = px(1),
    footnotes.padding = px(15),
    footnotes.marks = "standard",
    footnotes.sep = "<br>",
    column_labels.border.top.style = "solid",
    column_labels.border.top.color = css_colors$oro_premium,
    column_labels.border.top.width = px(1),
    column_labels.border.bottom.style = "solid",
    column_labels.border.bottom.color = css_colors$oro_premium,
    column_labels.border.bottom.width = px(1),
    table_body.hlines.style = "solid",
    table_body.hlines.color = css_colors$grilla_oscura,
    table_body.hlines.width = px(1),
    table.font.size = px(14),
    heading.title.font.size = px(20),
    heading.subtitle.font.size = px(16),
    footnotes.font.size = px(12),
    data_row.padding = px(10)
  ) %>%
  opt_table_font(
    font = list(
      google_font(name = "Cinzel"),
      google_font(name = "Inter"),
      "Segoe UI", "Arial", "sans-serif"
    )
  ) %>%
  tab_source_note(
    source_note = md(paste(
      "**Prueba ADF realizada el:**", format(Sys.Date(), "%d/%m/%Y"),
      "| **Nivel de significancia:** α = 0.05",
      "| **Serie analizada:** Primera diferencia de ICOLCAP"
    ))
  )


adf_gt
Tabla 2. RESULTADOS PRUEBA ADF - SERIE DIFERENCIADA*
Augmented Dickey-Fuller Test en Δ¹(serie)
ESTADÍSTICO VALOR INTERPRETACIÓN
Dickey-Fuller Aumentado -13.483452 Estadístico de prueba para H₀: raíz unitaria en Δ¹(serie)
Valor p 0.010000 ✓ Significativo (rechaza H₀)
Retrasos (Lags) 13 Número de retardos utilizados en la regresión
Observaciones 2629 Tamaño efectivo de muestra analizada
* Hipótesis Nula (H₀): La primera diferencia de la serie (Δ¹) tiene una raíz unitaria (no es estacionaria)
Conclusión: ✓ Δ¹(serie) ES ESTACIONARIA (Se rechaza H₀ - Primera diferencia estacionaria)
Prueba ADF realizada el: 05/12/2025 | Nivel de significancia: α = 0.05 | Serie analizada: Primera diferencia de ICOLCAP

En esta ocasión, el valor p de la prueba ADF es menor que el nivel de significancia de 0.05, lo que conduce a rechazar la hipótesis nula de presencia de raíz unitaria (Minitab, 2024; NumXL, 2024). Esta decisión estadística proporciona evidencia robusta de que la serie diferenciada es estacionaria (Minitab, 2024; NumXL, 2024). El estadístico de prueba también resulta significativamente menor que los valores críticos tabulados, reforzando la conclusión de estacionariedad (NumXL, 2024).

Este resultado confirma que la diferenciación de primer orden fue suficiente para transformar la serie no estacionaria del COLCAP en una serie estacionaria adecuada para el modelado ARIMA (Minitab, 2024). La evidencia estadística formal de la prueba ADF, junto con la evidencia visual del gráfico de la serie diferenciada y el comportamiento del correlograma ACF, permiten concluir con confianza que la serie cumple ahora con los requisitos de estacionariedad.

Por lo tanto, el parámetro de integración del modelo ARIMA se establece en d=1, indicando que se requiere una diferenciación para lograr estacionariedad (DataCamp, 2024). Con esta transformación completada satisfactoriamente, se puede proceder a identificar los órdenes de los componentes autorregresivo (p) y de media móvil (q) mediante el análisis conjunto de las funciones de autocorrelación (ACF) y autocorrelación parcial (PACF) de la serie diferenciada (UCM, 2013).

4.3 Identificación de los Órdenes p y q mediante ACF y PACF

Una vez confirmada la estacionariedad de la serie diferenciada, se procedió a identificar los órdenes de los componentes autorregresivo (p) y de media móvil (q) del modelo ARIMA mediante el análisis conjunto de las funciones de autocorrelación (ACF) y autocorrelación parcial (PACF) (Minitab, 2024).

La Figura 5 presenta los correlogramas de ACF y PACF de la serie diferenciada, dispuestos lado a lado para facilitar su comparación y análisis simultáneo. Estos gráficos constituyen herramientas fundamentales en la metodología de Box-Jenkins para determinar la estructura apropiada del modelo ARIMA (DataCamp, 2024).

library(plotly)
library(forecast)

css_acf <- list(
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  luz_oro = "#FFF8DC",
  puro_blanco = "#FFFFFF"
)

acf_dif_data <- Acf(dif1serie, lag.max = 20, plot = FALSE)
pacf_dif_data <- Pacf(dif1serie, lag.max = 20, plot = FALSE)
confianza <- 2/sqrt(length(dif1serie))

df_acf <- data.frame(
  Lag = 0:20,
  ACF = as.numeric(acf_dif_data$acf),
  Significativo = abs(as.numeric(acf_dif_data$acf)) > confianza,
  Tipo = "ACF"
)

df_pacf <- data.frame(
  Lag = 1:20,
  ACF = as.numeric(pacf_dif_data$acf),
  Significativo = abs(as.numeric(pacf_dif_data$acf)) > confianza,
  Tipo = "PACF"
)

df_acf <- df_acf %>% filter(Lag >= 1)

max_abs_acf <- max(abs(df_acf$ACF), na.rm = TRUE)
max_abs_pacf <- max(abs(df_pacf$ACF), na.rm = TRUE)
max_abs <- max(max_abs_acf, max_abs_pacf, 0.2)
y_range <- max_abs * 1.2

fig_acf <- plot_ly(df_acf, x = ~Lag) %>%
  add_bars(
    y = ~ACF,
    marker = list(
      color = ~ifelse(Significativo, css_acf$oro_lujo, css_acf$oro_premium),
      line = list(color = css_acf$oro_premium, width = 1.5)
    ),
    name = "ACF",
    hovertemplate = paste(
      "<b>Lag: %{x}</b><br>",
      "ACF: %{y:.3f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_trace(
    x = c(0.5, 20.5),
    y = c(confianza, confianza),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$oro_antiguo, dash = 'dash', width = 1.5),
    name = 'Límite Superior',
    hoverinfo = 'none'
  ) %>%
  add_trace(
    x = c(0.5, 20.5),
    y = c(-confianza, -confianza),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$oro_antiguo, dash = 'dash', width = 1.5),
    name = 'Límite Inferior',
    hoverinfo = 'none'
  ) %>%
  add_trace(
    x = c(0.5, 20.5),
    y = c(0, 0),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$luz_oro, width = 1),
    name = 'Línea Cero',
    hoverinfo = 'none'
  ) %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:20px;'>ACF - Serie Diferenciada</span>",
      x = 0.5
    ),
    xaxis = list(
      title = "REZAGO (LAGS)",
      gridcolor = css_acf$grilla_oscura,
      linecolor = css_acf$oro_premium,
      tickfont = list(family = 'Inter', color = css_acf$luz_oro, size = 11),
      zeroline = FALSE,
      showgrid = TRUE,
      range = c(0.5, 20.5),
      dtick = 5
    ),
    yaxis = list(
      title = "COEFICIENTE ACF",
      gridcolor = css_acf$grilla_oscura,
      linecolor = css_acf$oro_premium,
      tickfont = list(family = 'Inter', color = css_acf$luz_oro, size = 11),
      range = c(-y_range, y_range),
      tickformat = '.3f',
      zeroline = FALSE,
      showgrid = TRUE
    ),
    plot_bgcolor = css_acf$fondo_negro,
    paper_bgcolor = css_acf$fondo_negro,
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_acf$oro_premium,
      font = list(family = 'Inter', color = css_acf$luz_oro, size = 11)
    ),
    margin = list(l = 60, r = 30, t = 80, b = 60),
    showlegend = FALSE
  )

fig_pacf <- plot_ly(df_pacf, x = ~Lag) %>%
  add_bars(
    y = ~ACF,
    marker = list(
      color = ~ifelse(Significativo, css_acf$oro_lujo, css_acf$oro_premium),
      line = list(color = css_acf$oro_premium, width = 1.5)
    ),
    name = "PACF",
    hovertemplate = paste(
      "<b>Lag: %{x}</b><br>",
      "PACF: %{y:.3f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_trace(
    x = c(0.5, 20.5),
    y = c(confianza, confianza),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$oro_antiguo, dash = 'dash', width = 1.5),
    name = 'Límite Superior',
    hoverinfo = 'none'
  ) %>%
  add_trace(
    x = c(0.5, 20.5),
    y = c(-confianza, -confianza),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$oro_antiguo, dash = 'dash', width = 1.5),
    name = 'Límite Inferior',
    hoverinfo = 'none'
  ) %>%
  add_trace(
    x = c(0.5, 20.5),
    y = c(0, 0),
    type = 'scatter',
    mode = 'lines',
    line = list(color = css_acf$luz_oro, width = 1),
    name = 'Línea Cero',
    hoverinfo = 'none'
  ) %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:20px;'>PACF - Serie Diferenciada</span>",
      x = 0.5
    ),
    xaxis = list(
      title = "REZAGO (LAGS)",
      gridcolor = css_acf$grilla_oscura,
      linecolor = css_acf$oro_premium,
      tickfont = list(family = 'Inter', color = css_acf$luz_oro, size = 11),
      zeroline = FALSE,
      showgrid = TRUE,
      range = c(0.5, 20.5),
      dtick = 5
    ),
    yaxis = list(
      title = "COEFICIENTE PACF",
      gridcolor = css_acf$grilla_oscura,
      linecolor = css_acf$oro_premium,
      tickfont = list(family = 'Inter', color = css_acf$luz_oro, size = 11),
      range = c(-y_range, y_range),
      tickformat = '.3f',
      zeroline = FALSE,
      showgrid = TRUE
    ),
    plot_bgcolor = css_acf$fondo_negro,
    paper_bgcolor = css_acf$fondo_negro,
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_acf$oro_premium,
      font = list(family = 'Inter', color = css_acf$luz_oro, size = 11)
    ),
    margin = list(l = 60, r = 30, t = 80, b = 60),
    showlegend = FALSE
  )

fig_combinado <- subplot(fig_acf, fig_pacf, 
                         nrows = 1, 
                         shareY = TRUE,
                         titleX = TRUE,
                         titleY = TRUE) %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              Grafico 5. ANÁLISIS ACF y PACF - SERIE DIFERENCIADA ICOLCAP</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              Identificación de modelos ARIMA</span>",
      x = 0.5,
      y = 0.95
    ),
    annotations = list(
      list(
        x = 0.5,
        y = 1.02,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Intervalo de confianza: 95% |",
          "Lags analizados: 20 |",
          "N observaciones:", length(dif1serie), "|",
          "Límite confianza: ±", round(confianza, 3)
        ),
        showarrow = FALSE,
        font = list(
          family = 'Playfair Display',
          size = 12,
          color = css_acf$luz_oro
        ),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_acf$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    ),
    plot_bgcolor = css_acf$fondo_negro,
    paper_bgcolor = css_acf$fondo_negro,
    margin = list(l = 60, r = 60, t = 120, b = 80)
  )

fig_combinado

4.3.1 Interpretación del Gráfico ACF

El gráfico de la función de autocorrelación (ACF) muestra el patrón de correlaciones entre la serie y sus valores rezagados (Minitab, 2024). En el correlograma ACF de la serie diferenciada del COLCAP se observa que los dos primeros rezagos (lag 1 y lag 2) presentan autocorrelaciones significativas, evidenciadas por barras que exceden las bandas de confianza (Minitab, 2024; Enrdados, 2020). Específicamente, el rezago 2 muestra una autocorrelación positiva significativa.

A partir del rezago 3 en adelante, las autocorrelaciones caen dentro de las bandas de confianza y fluctúan alrededor de cero, lo que indica que no son estadísticamente diferentes de cero (Minitab, 2024; Enrdados, 2020). Este patrón de corte abrupto después de los primeros dos rezagos sugiere la presencia de un componente de media móvil (MA) en los datos (Minitab, 2024; Ciencia de Datos, 2025).

Según las reglas de identificación de modelos ARIMA, cuando la ACF muestra correlaciones significativas solamente en los primeros q rezagos, seguidas por correlaciones no significativas, esto indica un término de media móvil de orden q (Minitab, 2024; Enrdados, 2020). En este caso, la evidencia sugiere un modelo MA(2), dado que los dos primeros rezagos son significativos antes del corte (DataCamp, 2024; Ciencia de Datos, 2025).

4.3.2 Interpretación del Gráfico PACF

El gráfico de la función de autocorrelación parcial (PACF) mide la correlación entre observaciones separadas por k períodos después de controlar por la influencia de los rezagos intermedios (Minitab, 2024). En el correlograma PACF de la serie diferenciada del COLCAP se observa que los dos primeros rezagos (lag 1 y lag 2) presentan autocorrelaciones parciales significativas, con barras que sobrepasan las bandas de confianza (Minitab, 2024).

El rezago 1 muestra una autocorrelación parcial positiva y significativa, mientras que el rezago 2 también excede los límites de significancia (Minitab, 2024). A partir del rezago 3, las autocorrelaciones parciales caen dentro de las bandas de confianza y oscilan alrededor de cero, indicando que no son estadísticamente significativas (Minitab, 2024).

Este patrón de corte después del segundo rezago es característico de un proceso autorregresivo (AR) (Minitab, 2024; Enrdados, 2020). Según la teoría de identificación ARIMA, cuando la PACF muestra correlaciones significativas en los primeros p rezagos seguidas de correlaciones no significativas, esto indica un término autorregresivo de orden p (Minitab, 2024; Ciencia de Datos, 2025). La evidencia del correlograma PACF sugiere un modelo AR(2), dado que hay dos rezagos significativos antes del corte (DataCamp, 2024; Enrdados, 2020).

4.3.3 Modelos Candidatos Identificados

Basándose en el análisis de los correlogramas ACF y PACF, se identificaron dos modelos candidatos principales para la serie temporal del COLCAP (Ciencia de Datos, 2025):

  1. ARIMA(2,1,0): Este modelo incorpora un componente autorregresivo de orden 2, sugerido por el patrón de corte en el PACF después del segundo rezago (Minitab, 2024; Enrdados, 2020). La notación (2,1,0) indica que el modelo incluye dos términos autorregresivos (p=2), requiere una diferenciación para alcanzar estacionariedad (d=1), y no incluye términos de media móvil (q=0) (DataCamp, 2024).

  2. ARIMA(0,1,2): Este modelo incorpora un componente de media móvil de orden 2, sugerido por el patrón de corte en el ACF después del segundo rezago (Minitab, 2024; Ciencia de Datos, 2025). La notación (0,1,2) indica que el modelo no incluye términos autorregresivos (p=0), requiere una diferenciación (d=1), e incluye dos términos de media móvil (q=2) (DataCamp, 2024).

Ambos modelos son plausibles desde la perspectiva teórica, ya que los patrones observados en ACF y PACF presentan características que justifican tanto la especificación AR como MA (Enrdados, 2020; Ciencia de Datos, 2025). La decisión sobre cuál de estos modelos es más apropiado se determinará en la siguiente etapa de la metodología de Box-Jenkins mediante la estimación de parámetros y la comparación de criterios de información como AIC, AICc y BIC, así como el análisis de residuales (Minitab, 2024).

Es importante señalar que la identificación de modelos ARIMA mediante ACF y PACF constituye un arte tanto como una ciencia, y en ocasiones múltiples modelos pueden parecer razonables basándose en estos correlogramas (Enrdados, 2020; Reddit, 2016). Por ello, la selección final del modelo óptimo requiere la evaluación formal de las métricas de bondad de ajuste y el diagnóstico de residuales, aspectos que se abordarán en las secciones subsiguientes (DataCamp, 2024).

4.4 Segunda etapa

Una vez identificados los órdenes provisionales de los componentes autorregresivo (p) y de media móvil (q) mediante el análisis conjunto de los correlogramas ACF y PACF, se procedió con la segunda etapa de la metodología de Box-Jenkins: la estimación de parámetros (Estadística.net, s.f.). En esta etapa se estimaron 7 modelos alternativos con diferentes especificaciones de p y q, manteniendo constante el parámetro de integración d=1, confirmado en la etapa anterior (DataCamp, 2024).

Los 7 modelos estimados fueron los siguientes (DataCamp, 2024):

Modelo 1: ARIMA(3,1,0) - Resultado del algoritmo auto.arima

Modelo 2: ARIMA(0,1,2) - Especificación de media móvil alternativa

Modelo 3: ARIMA(2,1,0) - Especificación autorregresiva identificada en ACF/PACF

Modelo 4: ARIMA(0,1,3) - Extensión de media móvil

Modelo 5: ARIMA(3,1,2) - Combinaciones de modelos anteriores

Modelo 6: ARIMA(2,1,2) - Combinaciones de modelos anteriores

Modelo 7: ARIMA(2,1,3) - Combinaciones de modelos anteriores

library(gt)
library(dplyr)

modelo1 <- Arima(ventana, order = c(3,1,0)) # Modelo de autoarima

modelo2 <- Arima(ventana, order = c(0,1,2))

modelo3 <- Arima(ventana, order = c(2,1,0))

modelo4 <- Arima(ventana, order = c(0,1,3))

modelo5 <- Arima(ventana, order = c(3,1,2)) 

modelo6 <- Arima(ventana, order = c(2,1,2))

modelo7 <- Arima(ventana, order = c(2,1,3))

css_colors <- list(
  deep_black = "#0A0A0A",
  rich_black = "#1A1A1A",
  dark_charcoal = "#2C2C2C",
  premium_gold = "#D4AF37",
  luxury_gold = "#FFD700",
  antique_gold = "#B8860B",
  warm_brown = "#8B4513",
  elegant_brown = "#A0522D",
  light_gold = "#FFF8DC",
  pure_white = "#FFFFFF",
  text_gold = "#D4AF37",
  text_light = "#F5F5F5",
  text_dark = "#333333"
)

df_modelos <- data.frame(
  Modelo = c("Modelo 1", "Modelo 2", "Modelo 3", "Modelo 4", "Modelo 5", "Modelo 6", "Modelo 7"),
  Orden = c(
    paste0("ARIMA(", modelo1$arma[1], ",", modelo1$arma[6], ",", modelo1$arma[2], ")"),
    paste0("ARIMA(", modelo2$arma[1], ",", modelo2$arma[6], ",", modelo2$arma[2], ")"),
    paste0("ARIMA(", modelo3$arma[1], ",", modelo3$arma[6], ",", modelo3$arma[2], ")"),
    paste0("ARIMA(", modelo4$arma[1], ",", modelo4$arma[6], ",", modelo4$arma[2], ")"),
    paste0("ARIMA(", modelo5$arma[1], ",", modelo5$arma[6], ",", modelo5$arma[2], ")"),
    paste0("ARIMA(", modelo6$arma[1], ",", modelo6$arma[6], ",", modelo6$arma[2], ")"),
    paste0("ARIMA(", modelo7$arma[1], ",", modelo7$arma[6], ",", modelo7$arma[2], ")")
  ),
  AIC = round(c(AIC(modelo1), AIC(modelo2), AIC(modelo3), AIC(modelo4), 
                AIC(modelo5), AIC(modelo6), AIC(modelo7)), 3),
  BIC = round(c(BIC(modelo1), BIC(modelo2), BIC(modelo3), BIC(modelo4), 
                BIC(modelo5), BIC(modelo6), BIC(modelo7)), 3),
  LogLik = round(c(logLik(modelo1), logLik(modelo2), logLik(modelo3), logLik(modelo4), 
                   logLik(modelo5), logLik(modelo6), logLik(modelo7)), 3),
  Sigma2 = round(c(modelo1$sigma2, modelo2$sigma2, modelo3$sigma2, modelo4$sigma2, 
                   modelo5$sigma2, modelo6$sigma2, modelo7$sigma2), 6),
  stringsAsFactors = FALSE
)

mejor_aic <- which.min(df_modelos$AIC)
mejor_bic <- which.min(df_modelos$BIC)
mejor_loglik <- which.max(df_modelos$LogLik)
mejor_sigma2 <- which.min(df_modelos$Sigma2)

add_alpha <- function(color, alpha) {
  rgb_col <- col2rgb(color)
  rgb(rgb_col[1], rgb_col[2], rgb_col[3], alpha = alpha * 255, maxColorValue = 255)
}

tabla_modelos_gt <- df_modelos %>%
  gt() %>%
  tab_header(
    title = md("**Tabla 3. ESPECIFICACIONES Y MÉTRICAS DE MODELOS ARIMA**"),
    subtitle = md("Comparativa de 7 modelos ARIMA ajustados a la serie ICOLCAP")
  ) %>%
  cols_label(
    Modelo = md("**MODELO**"),
    Orden = md("**ORDEN (p,d,q)**"),
    AIC = md("**AIC**"),
    BIC = md("**BIC**"),
    LogLik = md("**LOG-LIK**"),
    Sigma2 = md("**σ²**")
  ) %>%
  tab_footnote(
    footnote = "AIC: Criterio de Información de Akaike (menor es mejor)",
    locations = cells_column_labels(columns = AIC)
  ) %>%
  tab_footnote(
    footnote = "BIC: Criterio de Información Bayesiano (menor es mejor)",
    locations = cells_column_labels(columns = BIC)
  ) %>%
  tab_footnote(
    footnote = "LogLik: Logaritmo de la Verosimilitud (mayor es mejor)",
    locations = cells_column_labels(columns = LogLik)
  ) %>%
  tab_footnote(
    footnote = "σ²: Varianza de los residuos (menor es mejor)",
    locations = cells_column_labels(columns = Sigma2)
  ) %>%
  fmt_number(
    columns = c(AIC, BIC, LogLik),
    decimals = 3,
    sep_mark = ".",
    dec_mark = ","
  ) %>%
  fmt_number(
    columns = Sigma2,
    decimals = 6,
    sep_mark = ".",
    dec_mark = ","
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$premium_gold,
        font = "Cinzel",
        weight = "bold",
        size = "xx-large"
      ),
      cell_fill(color = css_colors$rich_black),
      cell_borders(sides = c("top", "bottom"), color = css_colors$premium_gold, weight = px(3))
    ),
    locations = cells_title(groups = "title")
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$light_gold,
        font = "Inter",
        size = "large"
      ),
      cell_fill(color = css_colors$rich_black)
    ),
    locations = cells_title(groups = "subtitle")
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$deep_black,
        font = "Cinzel",
        weight = "bold",
        size = "medium",
        align = "center"
      ),
      cell_fill(color = css_colors$luxury_gold),
      cell_borders(sides = "all", color = css_colors$antique_gold, weight = px(2))
    ),
    locations = cells_column_labels(columns = everything())
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$text_gold,
        font = "Cinzel",
        weight = "bold",
        align = "center"
      ),
      cell_fill(color = css_colors$rich_black)
    ),
    locations = cells_body(columns = c(Modelo, Orden))
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = css_colors$rich_black)
    ),
    locations = cells_body(
      rows = seq(1, 7, 2)  
    )
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = css_colors$dark_charcoal)
    ),
    locations = cells_body(
      rows = seq(2, 7, 2)  
    )
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = add_alpha(css_colors$luxury_gold, 0.3)),
      cell_text(color = css_colors$luxury_gold, weight = "bold")
    ),
    locations = cells_body(
      columns = AIC,
      rows = mejor_aic
    )
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = add_alpha(css_colors$luxury_gold, 0.3)),
      cell_text(color = css_colors$luxury_gold, weight = "bold")
    ),
    locations = cells_body(
      columns = BIC,
      rows = mejor_bic
    )
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = add_alpha(css_colors$luxury_gold, 0.3)),
      cell_text(color = css_colors$luxury_gold, weight = "bold")
    ),
    locations = cells_body(
      columns = LogLik,
      rows = mejor_loglik
    )
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = add_alpha(css_colors$luxury_gold, 0.3)),
      cell_text(color = css_colors$luxury_gold, weight = "bold")
    ),
    locations = cells_body(
      columns = Sigma2,
      rows = mejor_sigma2
    )
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$text_light,
        align = "center"
      )
    ),
    locations = cells_body(columns = c(AIC, BIC, LogLik, Sigma2))
  ) %>%
  data_color(
    columns = c(AIC, BIC),
    colors = scales::col_numeric(
      palette = c(css_colors$antique_gold, css_colors$luxury_gold, css_colors$warm_brown),
      domain = NULL
    )
  ) %>%
  data_color(
    columns = LogLik,
    colors = scales::col_numeric(
      palette = c(css_colors$warm_brown, css_colors$luxury_gold, css_colors$antique_gold),
      domain = NULL
    )
  ) %>%
  data_color(
    columns = Sigma2,
    colors = scales::col_numeric(
      palette = c(css_colors$antique_gold, css_colors$luxury_gold, css_colors$warm_brown),
      domain = NULL
    )
  ) %>%
  tab_options(
    table.background.color = css_colors$deep_black,
    table.border.top.style = "solid",
    table.border.top.color = css_colors$premium_gold,
    table.border.top.width = px(3),
    table.border.bottom.style = "solid",
    table.border.bottom.color = css_colors$premium_gold,
    table.border.bottom.width = px(3),
    table.border.left.style = "solid",
    table.border.left.color = css_colors$premium_gold,
    table.border.left.width = px(3),
    table.border.right.style = "solid",
    table.border.right.color = css_colors$premium_gold,
    table.border.right.width = px(3),
    heading.border.bottom.style = "solid",
    heading.border.bottom.color = css_colors$premium_gold,
    heading.border.bottom.width = px(2),
    heading.padding = px(20),
    footnotes.border.bottom.style = "solid",
    footnotes.border.bottom.color = css_colors$premium_gold,
    footnotes.border.bottom.width = px(1),
    footnotes.padding = px(10),
    footnotes.marks = "standard",
    footnotes.sep = "<br>",
    column_labels.border.top.style = "solid",
    column_labels.border.top.color = css_colors$premium_gold,
    column_labels.border.top.width = px(2),
    column_labels.border.bottom.style = "solid",
    column_labels.border.bottom.color = css_colors$premium_gold,
    column_labels.border.bottom.width = px(2),
    table_body.hlines.style = "solid",
    table_body.hlines.color = css_colors$dark_charcoal,
    table_body.hlines.width = px(1),
    table_body.vlines.style = "solid",
    table_body.vlines.color = css_colors$dark_charcoal,
    table_body.vlines.width = px(1),
    table.font.size = px(14),
    heading.title.font.size = px(24),
    heading.subtitle.font.size = px(18),
    footnotes.font.size = px(11),
    data_row.padding = px(12),
    column_labels.padding = px(15)
  ) %>%
  opt_table_font(
    font = list(
      google_font(name = "Cinzel"),
      google_font(name = "Inter"),
      "Segoe UI", "Arial", "sans-serif"
    )
  ) %>%
  tab_source_note(
    source_note = md(paste(
      "<span style='color:", css_colors$premium_gold, "; font-family: Cinzel; font-weight: bold;'>RESUMEN DE MEJORES MODELOS:</span><br>",
      "• <span style='color:", css_colors$light_gold, "'>Mejor AIC:</span> Modelo ", mejor_aic, " (", df_modelos$Orden[mejor_aic], ")<br>",
      "• <span style='color:", css_colors$light_gold, "'>Mejor BIC:</span> Modelo ", mejor_bic, " (", df_modelos$Orden[mejor_bic], ")<br>",
      "• <span style='color:", css_colors$light_gold, "'>Mejor Log-Likelihood:</span> Modelo ", mejor_loglik, " (", df_modelos$Orden[mejor_loglik], ")<br>",
      "• <span style='color:", css_colors$light_gold, "'>Menor σ²:</span> Modelo ", mejor_sigma2, " (", df_modelos$Orden[mejor_sigma2], ")"
    ))
  ) %>%
  tab_source_note(
    source_note = md(paste(
      "<span style='color:", css_colors$light_gold, ";'>Nota:</span> p = orden AR, d = grado de diferenciación, q = orden MA<br>",
      "<span style='color:", css_colors$luxury_gold, ";'>Los modelos 1 y 5 corresponden al autoarima sugerido</span>"
    ))
  )

tabla_modelos_gt
Tabla 3. ESPECIFICACIONES Y MÉTRICAS DE MODELOS ARIMA
Comparativa de 7 modelos ARIMA ajustados a la serie ICOLCAP
MODELO ORDEN (p,d,q) AIC* BIC LOG-LIK σ²§
Modelo 1 ARIMA(3,1,0) 34.296,528 34.320,045 −17.144,264 25.374,465791
Modelo 2 ARIMA(0,1,2) 34.298,831 34.316,468 −17.146,415 25.406,247976
Modelo 3 ARIMA(2,1,0) 34.296,894 34.314,532 −17.145,447 25.387,609490
Modelo 4 ARIMA(0,1,3) 34.297,232 34.320,749 −17.144,616 25.381,236463
Modelo 5 ARIMA(3,1,2) 34.300,486 34.335,762 −17.144,243 25.393,312632
Modelo 6 ARIMA(2,1,2) 34.298,369 34.327,765 −17.144,184 25.382,554477
Modelo 7 ARIMA(2,1,3) 34.300,365 34.335,641 −17.144,183 25.392,146612
* AIC: Criterio de Información de Akaike (menor es mejor)
BIC: Criterio de Información Bayesiano (menor es mejor)
LogLik: Logaritmo de la Verosimilitud (mayor es mejor)
§ σ²: Varianza de los residuos (menor es mejor)
RESUMEN DE MEJORES MODELOS:
Mejor AIC: Modelo 1 ( ARIMA(3,1,0) )
Mejor BIC: Modelo 3 ( ARIMA(2,1,0) )
Mejor Log-Likelihood: Modelo 7 ( ARIMA(2,1,3) )
Menor σ²: Modelo 1 ( ARIMA(3,1,0) )
Nota: p = orden AR, d = grado de diferenciación, q = orden MA
Los modelos 1 y 5 corresponden al autoarima sugerido

Para cada uno de estos modelos, se utilizó el método de máxima verosimilitud (MLE) para estimar los coeficientes autorregresivos y de media móvil, así como los errores estándar asociados con cada parámetro (Ciencia de Datos, 2025; Estadística.net, s.f.). La máxima verosimilitud es el método estándar en la modelización ARIMA, ya que busca los valores de los parámetros que maximizan la probabilidad de observar los datos disponibles bajo el modelo propuesto (DataCamp, 2024).

Este procedimiento de estimación produce no solo los coeficientes estimados del modelo, sino también estadísticas de diagnóstico y criterios de información que permiten evaluar la calidad del ajuste relativo y la complejidad de cada especificación (Ciencia de Datos, 2025).

Con los siete modelos estimados, se procedió a comparar su desempeño relativo utilizando criterios de información estadística, que constituyen herramientas fundamentales en la metodología de Box-Jenkins para seleccionar el modelo óptimo (DataCamp, 2024).

El Criterio de Información de Akaike (AIC) es una medida que evalúa la bondad de ajuste del modelo penalizando simultáneamente por la complejidad (Ciencia de Datos, 2025; IBM, s.f.). El AIC combina dos componentes: la verosimilitud del modelo (qué tan bien explica los datos) y una penalización por el número de parámetros estimados (Ciencia de Datos, 2025; IBM, s.f.). La penalización evita el sobreajuste, donde un modelo con demasiados parámetros podría adaptarse excesivamente a los datos de entrenamiento sin generalizar bien a nuevas observaciones (Ciencia de Datos, 2025).

La regla de decisión para AIC es simple: el modelo con el valor de AIC más bajo es preferible (Ciencia de Datos, 2025; IBM, s.f.). Entre los cuatro modelos evaluados, se selecciona aquel que minimiza el AIC, ya que este balanceo entre ajuste y parsimonia lo hace más eficiente (DataCamp, 2024).

El Criterio de Información Bayesiano (BIC), también conocido como Criterio de Schwarz, es similar al AIC pero aplica una penalización más severa por la complejidad del modelo (Ciencia de Datos, 2025; DataCamp, 2024). El BIC es particularmente útil cuando se busca una especificación más parsimoniosa (es decir, con fewer parámetros) en comparación con el AIC (Ciencia de Datos, 2025).

Al igual que con el AIC, los valores más bajos del BIC indican mejor desempeño, pero dado que el BIC penaliza más fuertemente la complejidad, tiende a favorecer modelos con especificaciones más simples que el AIC ( Ciencia de Datos, 2025). En muchas situaciones de modelización ARIMA, los criterios AIC y BIC pueden sugerir modelos diferentes; por ello, es común reportar ambos criterios para proporcionar una visión integral de la comparación (Estadística.net, s.f.; DataCamp, 2024).

4.4.1 Residuales

El análisis de residuos constituye la tercera etapa de la metodología Box-Jenkins y representa un componente crítico en la validación de modelos de series temporales (Box & Jenkins, 1976). Una vez estimados los parámetros del modelo ARIMA, resulta imprescindible verificar que los residuos —definidos como las diferencias entre los valores observados y los valores predichos— se comporten como ruido blanco, es decir, como una secuencia de variables aleatorias no correlacionadas con media cero y varianza constante (Hyndman & Athanasopoulos, 2018). Esta verificación permite confirmar que el modelo ha capturado adecuadamente toda la estructura de autocorrelación presente en la serie original del ICOLCAP.

library(gt)
library(dplyr)

modelos_lista <- list(modelo1, modelo2, modelo3, modelo4, modelo5, modelo6, modelo7)
nombres_modelos <- c("ARIMA(3,1,0)", "ARIMA(0,1,2)", "ARIMA(2,1,0)", "ARIMA(0,1,3)", 
                     "ARIMA(3,1,2)", "ARIMA(2,1,2)", "ARIMA(2,1,3)")

obtener_estadisticos_residuos <- function(modelo, nombre) {
  residuos <- residuals(modelo)
  
  lb_test <- Box.test(residuos, lag = 10, type = "Ljung-Box")

  if(length(residuos) > 5000) {
    set.seed(123)
    muestra <- sample(residuos, 5000)
    sw_test <- shapiro.test(muestra)
    sw_stat <- round(sw_test$statistic, 4)
    sw_pvalue <- ifelse(sw_test$p.value < 0.001, "< 0.001", 
                       sprintf("%.4f", sw_test$p.value))
  } else {
    sw_test <- shapiro.test(residuos)
    sw_stat <- round(sw_test$statistic, 4)
    sw_pvalue <- ifelse(sw_test$p.value < 0.001, "< 0.001", 
                       sprintf("%.4f", sw_test$p.value))
  }
  
  data.frame(
    Modelo = nombre,
    Media = sprintf("%.6f", mean(residuos, na.rm = TRUE)),
    Desviacion = sprintf("%.6f", sd(residuos, na.rm = TRUE)),
    Minimo = sprintf("%.4f", min(residuos, na.rm = TRUE)),
    Maximo = sprintf("%.4f", max(residuos, na.rm = TRUE)),
    Asimetria = sprintf("%.4f", skewness(residuos, na.rm = TRUE)),
    Curtosis = sprintf("%.4f", kurtosis(residuos, na.rm = TRUE)),
    Ljung_Box_p = ifelse(lb_test$p.value < 0.001, "< 0.001", 
                            sprintf("%.4f", lb_test$p.value)),
    Shapiro_Wilk_p = sw_pvalue,
    SW_Estadistico = sw_stat,
    stringsAsFactors = FALSE
  )
}

tabla_residuos <- purrr::map2_dfr(modelos_lista, nombres_modelos, 
                                  obtener_estadisticos_residuos)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37",
  verde_exito = "#00FF00",
  rojo_error = "#FF6B6B"
)

add_alpha <- function(color, alpha) {
  rgb_col <- col2rgb(color)
  rgb(rgb_col[1], rgb_col[2], rgb_col[3], alpha = alpha * 255, maxColorValue = 255)
}

tabla_residuos_gt <- tabla_residuos %>%
  gt() %>%
  tab_header(
    title = md("**Tabla 4. ANÁLISIS DE RESIDUALES - MODELOS ARIMA**"),
    subtitle = md("Estadísticas descriptivas y pruebas de diagnóstico de los residuales")
  ) %>%
  cols_label(
    Modelo = "MODELO",
    Media = "MEDIA",
    Desviacion = "DESVIACIÓN",
    Minimo = "MÍNIMO",
    Maximo = "MÁXIMO",
    Asimetria = "ASIMETRÍA",
    Curtosis = "CURTOSIS",
    Ljung_Box_p = "LJUNG-BOX (p)", 
    Shapiro_Wilk_p = "SHAPIRO-WILK (p)", 
    SW_Estadistico = "SW ESTADÍSTICO" 
  ) %>%
  tab_footnote(
    footnote = "Ljung-Box: p > 0.05 indica residuos no correlacionados (ruido blanco)",
    locations = cells_column_labels(columns = "Ljung_Box_p")  
  ) %>%
  tab_footnote(
    footnote = "Shapiro-Wilk: p > 0.05 indica normalidad de los residuos",
    locations = cells_column_labels(columns = "Shapiro_Wilk_p")  
  ) %>%
  tab_footnote(
    footnote = "Asimetría ideal = 0 | Curtosis ideal = 3 (distribución normal)",
    locations = cells_column_labels(columns = c("Asimetria", "Curtosis"))
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_premium, weight = "bold", size = "xx-large"),
      cell_fill(color = css_colors$panel_negro),
      cell_borders(sides = c("top", "bottom"), color = css_colors$oro_premium, weight = px(3))
    ),
    locations = cells_title(groups = "title")
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$texto_claro, size = "large"),
      cell_fill(color = css_colors$panel_negro)
    ),
    locations = cells_title(groups = "subtitle")
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$fondo_negro, weight = "bold", align = "center"),
      cell_fill(color = css_colors$oro_lujo),
      cell_borders(sides = "all", color = css_colors$oro_antiguo, weight = px(2))
    ),
    locations = cells_column_labels(columns = everything())
  ) %>%
  tab_style(
    style = list(
      cell_text(color = css_colors$oro_premium, weight = "bold", align = "center"),
      cell_fill(color = css_colors$panel_negro)
    ),
    locations = cells_body(columns = "Modelo")
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = css_colors$panel_negro)
    ),
    locations = cells_body(rows = seq(1, 7, 2))
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = css_colors$fondo_negro)
    ),
    locations = cells_body(rows = seq(2, 7, 2))
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = ifelse(as.numeric(gsub("< ", "", tabla_residuos$Ljung_Box_p)) > 0.05, 
                      css_colors$verde_exito, css_colors$rojo_error),
        weight = "bold"
      )
    ),
    locations = cells_body(columns = "Ljung_Box_p")
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = ifelse(as.numeric(gsub("< ", "", tabla_residuos$Shapiro_Wilk_p)) > 0.05, 
                      css_colors$verde_exito, css_colors$rojo_error),
        weight = "bold"
      )
    ),
    locations = cells_body(columns = "Shapiro_Wilk_p")
  ) %>%
  tab_options(
    table.background.color = css_colors$fondo_negro,
    table.border.top.color = css_colors$oro_premium,
    table.border.bottom.color = css_colors$oro_premium,
    table.border.left.color = css_colors$oro_premium,
    table.border.right.color = css_colors$oro_premium,
    heading.border.bottom.color = css_colors$oro_premium,
    column_labels.border.top.color = css_colors$oro_premium,
    column_labels.border.bottom.color = css_colors$oro_premium,
    footnotes.border.bottom.color = css_colors$oro_premium,
    table.font.size = px(12),
    heading.title.font.size = px(20),
    heading.subtitle.font.size = px(16),
    footnotes.font.size = px(11)
  )

tabla_residuos_gt
Tabla 4. ANÁLISIS DE RESIDUALES - MODELOS ARIMA
Estadísticas descriptivas y pruebas de diagnóstico de los residuales
MODELO MEDIA DESVIACIÓN MÍNIMO MÁXIMO ASIMETRÍA1 CURTOSIS1 LJUNG-BOX (p)2 SHAPIRO-WILK (p)3 SW ESTADÍSTICO
ARIMA(3,1,0) 1.413902 159.196900 -1619.9434 1349.1185 -0.7806 17.7849 0.9951 < 0.001 0.8626
ARIMA(0,1,2) 1.495331 159.326013 -1638.1869 1348.7398 -0.8158 18.1611 0.7663 < 0.001 0.8604
ARIMA(2,1,0) 1.454999 159.267929 -1629.9578 1348.7736 -0.8055 18.0687 0.9161 < 0.001 0.8611
ARIMA(0,1,3) 1.445006 159.217861 -1627.1141 1350.8595 -0.7973 17.8554 0.9857 < 0.001 0.8625
ARIMA(3,1,2) 1.409303 159.195693 -1619.9022 1348.5598 -0.7779 17.7879 0.9955 < 0.001 0.8626
ARIMA(2,1,2) 1.413550 159.192105 -1619.5574 1348.6744 -0.7830 17.7704 0.9966 < 0.001 0.8628
ARIMA(2,1,3) 1.413983 159.191996 -1619.4371 1348.6600 -0.7827 17.7736 0.9966 < 0.001 0.8628
1 Asimetría ideal = 0 | Curtosis ideal = 3 (distribución normal)
2 Ljung-Box: p > 0.05 indica residuos no correlacionados (ruido blanco)
3 Shapiro-Wilk: p > 0.05 indica normalidad de los residuos

Los siete modelos ARIMA muestran un comportamiento de residuos muy similar y, en general, adecuado en términos de independencia, pero con claras desviaciones respecto a la normalidad.

En primer lugar, la media de los residuos es cercana a cero para todos los modelos (entre 1,41 y 1,49 en unidades muy pequeñas frente a la escala de la serie), lo que indica que, en promedio, los errores no presentan sesgos sistemáticos y el nivel de la serie está bien capturado. La desviación estándar es prácticamente la misma en todas las especificaciones (alrededor de 159), lo que sugiere que los distintos modelos generan un nivel de dispersión de errores muy parecido y ninguno reduce de forma relevante la volatilidad residual respecto a los demás.

En segundo lugar, las medidas de forma muestran asimetrías negativas moderadas (entre -0,78 y -0,82) y curtosis muy elevadas (entre 17,7 y 18,2), muy por encima del valor 3 de una distribución normal. Esto implica que los residuos presentan colas pesadas y una mayor frecuencia de valores extremos de la esperada bajo normalidad, rasgo típico de series financieras como los índices bursátiles. En coherencia con ello, todas las pruebas de Shapiro–Wilk arrojan p‑values menores a 0,001, lo que lleva a rechazar de forma contundente la hipótesis de residuos normales. En términos prácticos, esto significa que, aunque los modelos capturan bien la dinámica media, los errores siguen mostrando eventos extremos que el modelo lineal ARIMA no logra representar completamente.

En tercer lugar, la prueba de Ljung–Box aplicada a los residuos ofrece p‑values muy altos en todos los modelos (entre 0,7663 y 0,9966), muy por encima del umbral habitual de 0,05. Esto indica que no existe evidencia estadística de autocorrelación remanente en los residuos y, por tanto, que la estructura de dependencia temporal de la serie del ICOLCAP ha sido capturada de forma adecuada por las distintas especificaciones ARIMA (Ljung & Box, 1978). Desde la perspectiva de la metodología Box–Jenkins, este resultado es clave, porque confirma que los residuos se comportan como un proceso aproximadamente de ruido blanco en términos de correlación, condición necesaria para considerar que el modelo está correctamente especificado (Box & Jenkins, 1976; Hyndman & Athanasopoulos, 2018).​

Comparando las distintas especificaciones, las diferencias son marginales: todas presentan medias muy cercanas entre sí, asimetrías y curtosis casi idénticas, y p‑values de Ljung–Box muy elevados. En este contexto, la elección del “mejor” modelo no proviene del diagnóstico de residuos —que es favorable en todos los casos en cuanto a independencia— sino de otros criterios como AIC, BIC o el desempeño predictivo (MAE, MAPE) obtenidos en la fase de pronósticos. No obstante, la fuerte no normalidad de los residuos sugiere que, si el objetivo fuese modelar también la dinámica de la volatilidad (por ejemplo, para riesgo financiero), podría ser pertinente complementar el ARIMA con modelos de varianza condicional como GARCH, tal como recomiendan diversos estudios para series financieras con colas pesadas y eventos extremos (Oladejo et al., 2019).

4.4.2 Análisis de Métricas de Desempeño y Selección del Modelo Óptimo

Complementando el análisis de criterios de información (AIC y BIC) y el diagnóstico de residuales, se procedió a evaluar el desempeño predictivo de los modelos mediante un conjunto integral de métricas de accuracy que cuantifican la magnitud y naturaleza de los errores de pronóstico (Polmartisa, 2021). Estas métricas proporcionan información complementaria desde diferentes perspectivas, permitiendo una evaluación multidimensional de la capacidad de predicción de cada modelo (Polmartisa, 2021).

El Error Medio (ME) representa el promedio de todos los errores de predicción sin aplicar transformaciones (Polmartisa, 2021; SlimStock, 2025). El ME es una métrica fundamental que permite evaluar si existe sesgo sistemático en las predicciones (Polmartisa, 2021). Un valor de ME cercano a cero indica que los errores positivos (sobrestimaciones) y negativos (subestimaciones) se compensan mutuamente, reflejando ausencia de tendencia sistemática en los errores (SlimStock, 2025; Polmartisa, 2021).

El Error Absoluto Medio (MAE) soluciona el problema de cancelación de errores al utilizar los valores absolutos de todos los errores de predicción (SlimStock, 2025; Polmartisa, 2021). El MAE proporciona el promedio de la magnitud absoluta de los errores, independientemente de su dirección (Polmartisa, 2021; Mora, 2023).

El Error Cuadrático Medio (RMSE) es una de las métricas más utilizadas en la predicción de series temporales (Mora, 2023; SlimStock, 2025). El RMSE calcula la raíz cuadrada del promedio de los errores elevados al cuadrado, produciendo una métrica que se interpreta como la desviación estándar de los residuales (Mora, 2023; Qlik, s.f.).

La relación entre MAE y RMSE proporciona información diagnóstica: cuando el RMSE es significativamente mayor que el MAE, esto indica la presencia de errores grandes que generan imprecisión, mientras que una relación cercana entre ambos sugiere mayor estabilidad en la precisión de las predicciones (Polmartisa, 2021; SlimStock, 2025).

El Error Porcentual Medio (MPE) expresa los errores como porcentajes de los valores reales observados (Polmartisa, 2021). El MPE permite evaluar el sesgo direccional en términos porcentuales, indicando si el modelo tiende sistemáticamente a sobrestimar (MPE positivo) o subestimar (MPE negativo) los valores reales (Polmartisa, 2021; SlimStock, 2025).

Similar al ME, el MPE adolece del problema de compensación de errores con signos opuestos, por lo que es más útil para detectar dirección del sesgo que para evaluar magnitud del error (Polmartisa, 2021). Un MPE cercano a cero es deseable e indica ausencia de sesgo sistemático en las predicciones (Polmartisa, 2021).

El Error Porcentual Absoluto Medio (MAPE) es quizás la métrica más interpretable y frecuentemente utilizada en la evaluación de modelos de pronóstico (SlimStock, 2025; Polmartisa, 2021; Mora, 2023). El MAPE calcula el promedio de los errores porcentuales absolutos, expresando la precisión del modelo como un porcentaje fácilmente comprensible (Polmartisa, 2021; SlimStock, 2025).

Una ventaja fundamental del MAPE es que normaliza los errores respecto a la magnitud de los valores reales, permitiendo comparaciones significativas entre series con escalas diferentes (Polmartisa, 2021). Los criterios de interpretación son ampliamente aceptados en la industria (Polmartisa, 2021):

  • MAPE < 10%: Desempeño excelente

  • MAPE 10-20%: Desempeño bueno

  • MAPE 20-50%: Desempeño aceptable

  • MAPE > 50%: Desempeño pobre

Sin embargo, el MAPE presenta limitaciones cuando los valores reales son cercanos a cero o cuando existen valores cero en la serie, ya que esto puede producir divisiones por cero o valores de MAPE arbitrariamente grandes (SlimStock, 2025; Polmartisa, 2021). Además, el MAPE penaliza asimétricamente la sobrestimación versus subestimación, produciendo mayores penalizaciones para sobrestimaciones (SlimStock, 2025).

El Error Absoluto Escalado Medio (MASE) constituye una métrica particularmente reveladora que sitúa el desempeño del modelo ARIMA en perspectiva relativa (Qlik, s.f.; Polmartisa, 2021). El MASE compara el error absoluto medio (MAE) del modelo contra el MAE de un pronóstico ingenuo o “naive” que simplemente utiliza el último valor observado como predicción para todos los períodos futuros (Qlik, s.f.; Polmartisa, 2021).

La interpretación del MASE es directa y poderosa (Qlik, s.f.):

  • MASE < 1: El modelo ARIMA supera al pronóstico naive, constituyendo un modelo valioso

  • MASE = 1: El modelo ARIMA tiene desempeño equivalente al pronóstico naive

  • MASE > 1: El modelo ARIMA es inferior incluso a un simple pronóstico naive, sugiriendo que la especificación requiere revisión

El MASE es particularmente útil en contextos financieros como la predicción de índices bursátiles, donde un benchmark natural es simplemente mantener el último precio observado como predicción del futuro (Polmartisa, 2021). Un MASE consistentemente menor que 1 proporciona justificación convincente para utilizar el modelo ARIMA en lugar de estrategias ingenuas (Qlik, s.f.).

La evaluación integral de los tres modelos (Modelo 1: ARIMA(3,1,0), Modelo 2: ARIMA(0,1,2), Modelo 3: ARIMA(2,1,0)) se realizó examinando simultáneamente todas las métricas de desempeño (Polmartisa, 2021; Mora, 2023). Esta aproximación multimétrica es crítica porque cada indicador proporciona perspectivas complementarias sobre diferentes aspectos de la precisión predictiva:

library(gt)
library(dplyr)
library(scales)

css_colors <- list(
  deep_black = "#0A0A0A",
  rich_black = "#1A1A1A",
  dark_charcoal = "#2C2C2C",
  premium_gold = "#D4AF37",
  luxury_gold = "#FFD700",
  antique_gold = "#B8860B",
  warm_brown = "#8B4513",
  elegant_brown = "#A0522D",
  light_gold = "#FFF8DC",
  pure_white = "#FFFFFF",
  text_gold = "#D4AF37",
  text_light = "#F5F5F5",
  text_dark = "#333333",
  fondo_titulo = "#0A0A0A",       
  fondo_negro = "#1A1A1A",         
  panel_negro = "#2C2C2C",      
  oro_premium = "#D4AF37",        
  oro_lujo = "#FFD700",           
  oro_antiguo = "#B8860B",        
  borde_oro = "#8B4513",           
  grilla_oscura = "#444444",       
  verde_exito = "#228B22",        
  rojo_error = "#B22222"           
)

add_alpha <- function(color, alpha) {
  rgb_col <- col2rgb(color)
  rgb(rgb_col[1], rgb_col[2], rgb_col[3], alpha = alpha * 255, maxColorValue = 255)
}

metrics_modelo1 <- accuracy(modelo1)
metrics_modelo2 <- accuracy(modelo2)
metrics_modelo3 <- accuracy(modelo3)
metrics_modelo4 <- accuracy(modelo4)
metrics_modelo5 <- accuracy(modelo5)
metrics_modelo6 <- accuracy(modelo6)
metrics_modelo7 <- accuracy(modelo7)

procesar_metrics <- function(metrics, modelo_name) {
  if (is.matrix(metrics)) {
    metrics_df <- as.data.frame(metrics)
    if (nrow(metrics_df) > 1) {
      metrics_df <- metrics_df[1, , drop = FALSE]
    }
  } else if (is.vector(metrics)) {
    metrics_df <- as.data.frame(t(metrics))
  } else {
    metrics_df <- as.data.frame(metrics)
  }
  metricas_base <- data.frame(
    Modelo = modelo_name,
    ME = NA,
    RMSE = NA,
    MAE = NA,
    MPE = NA,
    MAPE = NA,
    MASE = NA,
    ACF1 = NA,
    stringsAsFactors = FALSE
  )

  metric_names <- colnames(metrics_df)
  
  if ("ME" %in% metric_names) metricas_base$ME <- metrics_df$ME
  if ("RMSE" %in% metric_names) metricas_base$RMSE <- metrics_df$RMSE
  if ("MAE" %in% metric_names) metricas_base$MAE <- metrics_df$MAE
  if ("MPE" %in% metric_names) metricas_base$MPE <- metrics_df$MPE
  if ("MAPE" %in% metric_names) metricas_base$MAPE <- metrics_df$MAPE
  if ("MASE" %in% metric_names) metricas_base$MASE <- metrics_df$MASE
  if ("ACF1" %in% metric_names) metricas_base$ACF1 <- metrics_df$ACF1
  
  if (all(is.na(metricas_base[-1]))) {
    n_metrics <- min(ncol(metrics_df), 8) - 1
    for (i in 1:n_metrics) {
      metricas_base[i + 1] <- metrics_df[1, i]
    }
  }
  
  return(metricas_base)
}

comparacion_df <- bind_rows(
  procesar_metrics(metrics_modelo1, "Modelo 1"),
  procesar_metrics(metrics_modelo2, "Modelo 2"),
  procesar_metrics(metrics_modelo3, "Modelo 3"),
  procesar_metrics(metrics_modelo4, "Modelo 4"),
  procesar_metrics(metrics_modelo5, "Modelo 5"),
  procesar_metrics(metrics_modelo6, "Modelo 6"),
  procesar_metrics(metrics_modelo7, "Modelo 7")
)


encontrar_mejores <- function(df, metricas) {
  mejores <- list()
  for (metrica in metricas) {
    if (metrica %in% colnames(df)) {
      valores <- df[[metrica]]
      valores_numericos <- suppressWarnings(as.numeric(valores))
      
      valores_validos <- valores_numericos[!is.na(valores_numericos)]
      
      if (length(valores_validos) > 0) {
        if (metrica == "ACF1") {
          mejores_idx <- which.min(abs(valores_numericos))
        } else if (metrica %in% c("ME", "RMSE", "MAE", "MPE", "MAPE", "MASE")) {
          mejores_idx <- which.min(valores_numericos)
        } else {
          mejores_idx <- NULL
        }
        
        if (!is.null(mejores_idx) && !is.na(mejores_idx)) {
          mejores[[metrica]] <- mejores_idx
        }
      }
    }
  }
  return(mejores)
}

metricas_a_evaluar <- c("RMSE", "MAE", "MAPE", "MASE", "ACF1")
mejores_modelos <- encontrar_mejores(comparacion_df, metricas_a_evaluar)

comparacion_gt <- comparacion_df %>%
  gt() %>%
  tab_header(
    title = md("**Tabla 5. COMPARACIÓN DE MODELOS DE PRONÓSTICO**"),
    subtitle = md("Métricas de Accuracy para 7 Modelos de Series Temporales")
  ) %>%
  cols_label(
    Modelo = md("**MODELO**"),
    ME = md("**ME**"),
    RMSE = md("**RMSE**"),
    MAE = md("**MAE**"),
    MPE = md("**MPE**"),
    MAPE = md("**MAPE**"),
    MASE = md("**MASE**"),
    ACF1 = md("**ACF1**")
  ) %>%
  # Formato numérico con 4 decimales
  fmt_number(
    columns = c(ME, RMSE, MAE, MPE, MAPE, MASE, ACF1),
    decimals = 4,
    sep_mark = ".",
    dec_mark = ","
  ) %>%
  tab_footnote(
    footnote = "ME: Error Medio | RMSE: Raíz del Error Cuadrático Medio | MAE: Error Absoluto Medio",
    locations = cells_column_labels(columns = c(ME, RMSE, MAE))
  ) %>%
  tab_footnote(
    footnote = "MPE: Error Porcentual Medio | MAPE: Error Porcentual Absoluto Medio | MASE: Error Absoluto Medio Escalado",
    locations = cells_column_labels(columns = c(MPE, MAPE, MASE))
  ) %>%
  tab_footnote(
    footnote = "ACF1: Autocorrelación de primer orden de los residuos (idealmente cercana a 0)",
    locations = cells_column_labels(columns = ACF1)
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$oro_premium,
        font = "Cinzel",
        weight = "bold",
        size = "xx-large"
      ),
      cell_fill(color = css_colors$fondo_titulo),
      cell_borders(sides = c("top", "bottom"), color = css_colors$borde_oro, weight = px(3))
    ),
    locations = cells_title(groups = "title")
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$text_light,
        font = "Inter",
        size = "large"
      ),
      cell_fill(color = css_colors$fondo_titulo)
    ),
    locations = cells_title(groups = "subtitle")
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$fondo_negro,
        font = "Cinzel",
        weight = "bold",
        size = "medium",
        align = "center"
      ),
      cell_fill(color = css_colors$oro_lujo),
      cell_borders(sides = "all", color = css_colors$oro_antiguo, weight = px(2))
    ),
    locations = cells_column_labels(columns = everything())
  ) %>%
  tab_style(
    style = list(
      cell_text(
        color = css_colors$oro_premium,
        font = "Cinzel",
        weight = "bold",
        align = "center"
      ),
      cell_fill(color = css_colors$panel_negro)
    ),
    locations = cells_body(columns = Modelo)
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = css_colors$panel_negro)
    ),
    locations = cells_body(
      rows = seq(1, nrow(comparacion_df), 2)  
    )
  ) %>%
  tab_style(
    style = list(
      cell_fill(color = css_colors$fondo_negro)
    ),
    locations = cells_body(
      rows = seq(2, nrow(comparacion_df), 2)  
    )
  ) %>%
  {
    gt_table <- .
    for (metrica in names(mejores_modelos)) {
      mejor_idx <- mejores_modelos[[metrica]]
      if (!is.null(mejor_idx) && mejor_idx <= nrow(comparacion_df)) {
        color_con_transparencia <- add_alpha(css_colors$verde_exito, 0.3)
        gt_table <- gt_table %>%
          tab_style(
            style = list(
              cell_fill(color = color_con_transparencia),
              cell_text(color = css_colors$verde_exito, weight = "bold")
            ),
            locations = cells_body(
              columns = metrica,
              rows = mejor_idx
            )
          )
      }
    }
    gt_table
  } %>%
  data_color(
    columns = c(RMSE, MAE, MAPE, MASE),
    colors = scales::col_numeric(
      palette = c(css_colors$verde_exito, css_colors$oro_lujo, css_colors$rojo_error),
      domain = NULL
    )
  ) %>%
  data_color(
    columns = ACF1,
    colors = scales::col_numeric(
      palette = c(css_colors$verde_exito, css_colors$oro_lujo, css_colors$rojo_error),
      domain = c(-1, 1)
    )
  ) %>%
  tab_options(
    table.background.color = css_colors$fondo_negro,
    table.border.top.style = "solid",
    table.border.top.color = css_colors$oro_premium,
    table.border.top.width = px(3),
    table.border.bottom.style = "solid",
    table.border.bottom.color = css_colors$oro_premium,
    table.border.bottom.width = px(3),
    table.border.left.style = "solid",
    table.border.left.color = css_colors$oro_premium,
    table.border.left.width = px(3),
    table.border.right.style = "solid",
    table.border.right.color = css_colors$oro_premium,
    table.border.right.width = px(3),
    heading.border.bottom.style = "solid",
    heading.border.bottom.color = css_colors$oro_premium,
    heading.border.bottom.width = px(2),
    heading.padding = px(20),
    footnotes.border.bottom.style = "solid",
    footnotes.border.bottom.color = css_colors$oro_premium,
    footnotes.border.bottom.width = px(1),
    footnotes.padding = px(10),
    footnotes.marks = "standard",
    footnotes.sep = "<br>",
    column_labels.border.top.style = "solid",
    column_labels.border.top.color = css_colors$oro_premium,
    column_labels.border.top.width = px(2),
    column_labels.border.bottom.style = "solid",
    column_labels.border.bottom.color = css_colors$oro_premium,
    column_labels.border.bottom.width = px(2),
    table_body.hlines.style = "solid",
    table_body.hlines.color = css_colors$grilla_oscura,
    table_body.hlines.width = px(1),
    table_body.vlines.style = "solid",
    table_body.vlines.color = css_colors$grilla_oscura,
    table_body.vlines.width = px(1),
    table.font.size = px(14),
    heading.title.font.size = px(24),
    heading.subtitle.font.size = px(18),
    footnotes.font.size = px(12),
    data_row.padding = px(12),
    column_labels.padding = px(15)
  ) %>%
  opt_table_font(
    font = list(
      google_font(name = "Cinzel"),
      google_font(name = "Inter"),
      "Segoe UI", "Arial", "sans-serif"
    )
  ) %>%
  tab_source_note(
    source_note = md(paste(
      "**Resumen:** ", 
      ifelse(!is.null(mejores_modelos$RMSE) && length(mejores_modelos$RMSE) > 0, 
             paste("Mejor RMSE: Modelo", mejores_modelos$RMSE), ""),
      ifelse(!is.null(mejores_modelos$MAE) && length(mejores_modelos$MAE) > 0, 
             paste(" | Mejor MAE: Modelo", mejores_modelos$MAE), ""),
      ifelse(!is.null(mejores_modelos$MAPE) && length(mejores_modelos$MAPE) > 0, 
             paste(" | Mejor MAPE: Modelo", mejores_modelos$MAPE), ""),
      "<br>",
      "**Interpretación:** Valores más bajos indican mejor ajuste para ME, RMSE, MAE, MPE, MAPE, MASE.",
      " Para ACF1, valores cercanos a 0 indican residuos no correlacionados."
    ))
  )

comparacion_gt
Tabla 5. COMPARACIÓN DE MODELOS DE PRONÓSTICO
Métricas de Accuracy para 7 Modelos de Series Temporales
MODELO ME* RMSE* MAE* MPE MAPE MASE ACF1
Modelo 1 1,4139 159,1731 101,9055 0,0024 0,7467 0,0073 −0,0007
Modelo 2 1,4953 159,3029 101,7419 0,0023 0,7455 0,0073 0,0033
Modelo 3 1,4550 159,2444 101,7986 0,0023 0,7459 0,0073 −0,0038
Modelo 4 1,4450 159,1943 101,9100 0,0024 0,7467 0,0073 0,0002
Modelo 5 1,4093 159,1718 101,8993 0,0024 0,7467 0,0073 −0,0006
Modelo 6 1,4136 159,1683 101,9290 0,0024 0,7469 0,0073 −0,0006
Modelo 7 1,4140 159,1682 101,9241 0,0024 0,7468 0,0073 −0,0005
* ME: Error Medio | RMSE: Raíz del Error Cuadrático Medio | MAE: Error Absoluto Medio
MPE: Error Porcentual Medio | MAPE: Error Porcentual Absoluto Medio | MASE: Error Absoluto Medio Escalado
ACF1: Autocorrelación de primer orden de los residuos (idealmente cercana a 0)
Resumen: Mejor RMSE: Modelo 7 | Mejor MAE: Modelo 2 | Mejor MAPE: Modelo 2
Interpretación: Valores más bajos indican mejor ajuste para ME, RMSE, MAE, MPE, MAPE, MASE. Para ACF1, valores cercanos a 0 indican residuos no correlacionados.

Mediante la comparación de estas métricas de los 7 modelos, se identifica el modelo con mejor balance entre precisión, estabilidad y parsimonia, seleccionando finalmente la especificación más apropiada para realizar los pronósticos del índice COLCAP con máxima confiabilidad (Polmartisa, 2021; Mora, 2023).

4.5 Seleccion del modelo

La selección final del modelo se fundamenta en la convergencia de múltiples criterios: (1) criterios de información AIC y BIC más bajos, (2) residuales que cumplen criterios de ruido blanco con prueba Ljung-Box p-valor > 0.05, y (3) métricas de accuracy superiores respecto a los modelos competidores (Polmartisa, 2021).

La selección del modelo ARIMA(0,1,2) como especificación final se justifica por la convergencia coherente de criterios estadísticos de información, diagnóstico de residuos y desempeño predictivo frente a las alternativas evaluadas. En primer lugar, este modelo presenta uno de los valores de AIC más bajos y el BIC mínimo entre los siete modelos considerados, lo que indica que logra un buen compromiso entre calidad de ajuste y parsimonia, al explicar adecuadamente la dinámica del ICOLCAP sin introducir parámetros innecesarios. De acuerdo con la literatura de Box y Jenkins, los criterios de información más reducidos son indicativos de un modelo que captura la estructura esencial de la serie temporal con la menor complejidad posible, por lo que constituyen un criterio sólido de selección en contextos comparativos.

En segundo lugar, los diagnósticos de residuos muestran que el ARIMA(0,1,2) cumple el requisito de generar residuos que se comportan como ruido blanco, condición clave para validar la adecuación del modelo. El p‑valor elevado de la prueba de Ljung–Box (p > 0,05) indica ausencia de autocorrelación remanente en los residuos, lo que sugiere que la estructura de dependencia temporal del ICOLCAP ha sido capturada de manera adecuada. Además, la autocorrelación de primer orden de los residuos es muy cercana a cero, reforzando la interpretación de que no subsisten patrones sistemáticos explotables en los errores de predicción. Desde la perspectiva de la metodología Box–Jenkins, esta evidencia respalda que el modelo no deja “información en los residuos” que pudiera ser aprovechada por una especificación alternativa.

En tercer lugar, el modelo ARIMA(0,1,2) exhibe las mejores métricas de accuracy entre los modelos comparados, particularmente en términos de MAE y MAPE, que resultan inferiores a los del resto de especificaciones. Esto implica que, en promedio, las desviaciones entre los valores pronosticados y los valores observados del ICOLCAP son menores, tanto en unidades absolutas como en términos porcentuales, lo que se traduce en una mayor precisión predictiva. Dado que el objetivo central del estudio es obtener pronósticos confiables del índice bursátil, la prioridad otorgada a estas métricas es consistente con las recomendaciones metodológicas para la evaluación de modelos de series temporales en contextos financieros.

Finalmente, al integrar estos tres bloques de evidencia —criterios de información favorables, residuos con comportamiento de ruido blanco y desempeño predictivo superior— la elección del ARIMA(0,1,2) resulta consistente con enfoques de selección basados en múltiples criterios propuestos en la literatura aplicada. En el informe puedes explicitar que la decisión no se basó en un único indicador aislado, sino en la convergencia de señales estadísticas que posicionan a este modelo como la opción más robusta y parsimoniosa para describir y pronosticar el comportamiento del ICOLCAP en el horizonte considerado

4.6 Pronosticos

Una vez ajustado el modelo ARIMA(0,1,2) y verificado el cumplimiento de los supuestos mediante el análisis de residuos, se procedió a generar pronósticos para un horizonte de cinco períodos. Este horizonte de pronóstico se seleccionó considerando la naturaleza del índice ICOLCAP y la necesidad de mantener un equilibrio entre la utilidad práctica de las predicciones y la degradación natural de la precisión a medida que se extiende el horizonte temporal. Los pronósticos se construyeron con un intervalo de confianza del 95%, proporcionando límites inferior y superior que delimitan la región probabilística donde se espera que se ubiquen los valores futuros del índice.

La Tabla 6 presenta los resultados de la validación de pronósticos, comparando los valores reales observados durante el período de validación contra las predicciones generadas por el modelo. Para cada uno de los cinco períodos pronosticados (del 4 al 10 de noviembre de 2025), se reportan el valor real del ICOLCAP, el pronóstico puntual, el error absoluto, el error porcentual, los límites del intervalo de confianza y la verificación de si el valor real cayó dentro del intervalo establecido. Esta estructura permite evaluar tanto la precisión de los pronósticos puntuales como la calibración de la incertidumbre estimada por el modelo.

library(gt)
library(forecast)

pronostico <- forecast(modelo2, h = 5, level = 0.95)
valores_reales <- as.numeric(ventana2)[1:5]
fechas <- as.character(time(ventana2)[1:5])

if (is.matrix(pronostico$lower)) {
  limite_inf <- round(pronostico$lower[, 1], 4)
  limite_sup <- round(pronostico$upper[, 1], 4)
} else {
  limite_inf <- round(pronostico$lower, 4)
  limite_sup <- round(pronostico$upper, 4)
}

df_pronosticos <- data.frame(
  Período = 1:5,
  Fecha = fechas,
  Real = round(valores_reales, 4),
  Pronóstico = round(pronostico$mean, 4),
  Error = round(valores_reales - pronostico$mean, 4),
  Error_porcentual = round(abs((valores_reales - pronostico$mean) / valores_reales) * 100, 2),
  Limite_Inferior = limite_inf,
  Limite_Superior = limite_sup,
  Dentro_IC = ifelse(
    valores_reales >= limite_inf & valores_reales <= limite_sup,
    "✓ Sí", "✗ No"
  ),
  stringsAsFactors = FALSE
)

mae <- round(mean(abs(df_pronosticos$Error)), 4)
mape <- round(mean(df_pronosticos$Error_porcentual), 2)
cobertura <- round(sum(df_pronosticos$Dentro_IC == "✓ Sí") / 5 * 100, 1)

nivel_desempeno <- if (mape < 10) {
  "excelente"
} else if (mape < 20) {
  "buen"
} else {
  "moderado"
}

gt_table <- df_pronosticos %>%
  gt() %>%
  tab_header(
    title = "Tabla 6. VALIDACIÓN DE PRONÓSTICOS ARIMA",
    subtitle = paste(
      "Modelo: ARIMA(", modelo2$arma[1], ",", modelo2$arma[6], ",", modelo2$arma[2], 
      ") | Horizonte: 5 períodos | Nivel de confianza: 95%"
    )
  ) %>%
  cols_label(
    Período = "PERÍODO",
    Fecha = "FECHA",
    Real = "VALOR REAL",
    Pronóstico = "PRONÓSTICO",
    Error = "ERROR",
    Error_porcentual = "ERROR %",
    Limite_Inferior = "LÍMITE INF.",
    Limite_Superior = "LÍMITE SUP.",
    Dentro_IC = "EN IC 95%"
  ) %>%
  tab_style(
    style = cell_fill(color = "#D4AF37"),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = cell_text(color = "#0A0A0A", weight = "bold"),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = cell_fill(color = "#1A1A1A"),
    locations = cells_body(rows = c(1, 3, 5))
  ) %>%
  tab_style(
    style = cell_fill(color = "#2C2C2C"),
    locations = cells_body(rows = c(2, 4))
  ) %>%
  tab_style(
    style = cell_text(color = "#F5F5F5"),
    locations = cells_body()
  ) %>%
  tab_style(
    style = cell_text(color = "#D4AF37", weight = "bold"),
    locations = cells_body(columns = c("Período", "Fecha", "Pronóstico"))
  ) %>%
  tab_style(
    style = cell_text(color = "#FFFFFF", weight = "bold"),
    locations = cells_body(columns = "Real")
  ) %>%
  fmt_number(
    columns = c("Real", "Pronóstico", "Error", "Limite_Inferior", "Limite_Superior"),
    decimals = 4
  ) %>%
  fmt_number(
    columns = "Error_porcentual",
    decimals = 2,
    pattern = "{x}%"
  ) %>%
  tab_options(
    table.background.color = "#1A1A1A",
    table.font.names = "Inter",
    table.border.top.color = "#D4AF37",
    table.border.bottom.color = "#D4AF37",
    table.border.left.color = "#D4AF37",
    table.border.right.color = "#D4AF37",
    heading.align = "center"
  )


for (i in 1:5) {
  error_color <- if (abs(df_pronosticos$Error[i]) < sd(valores_reales) * 0.5) {
    "#D4AF37"
  } else if (abs(df_pronosticos$Error[i]) < sd(valores_reales)) {
    "#FFD700"
  } else {
    "#FF6B6B"
  }
  
  gt_table <- gt_table %>%
    tab_style(
      style = cell_text(color = error_color, weight = "bold"),
      locations = cells_body(rows = i, columns = "Error")
    )
  error_pct <- df_pronosticos$Error_porcentual[i]
  error_pct_color <- if (error_pct < 10) {
    "#D4AF37"
  } else if (error_pct < 20) {
    "#FFD700"
  } else {
    "#FF6B6B"
  }
  
  gt_table <- gt_table %>%
    tab_style(
      style = cell_text(color = error_pct_color, weight = "bold"),
      locations = cells_body(rows = i, columns = "Error_porcentual")
    )
  ic_color <- if (df_pronosticos$Dentro_IC[i] == "✓ Sí") {
    "#D4AF37"
  } else {
    "#FF6B6B"
  }
  
  gt_table <- gt_table %>%
    tab_style(
      style = cell_text(color = ic_color, weight = "bold"),
      locations = cells_body(rows = i, columns = "Dentro_IC")
    )
}

gt_table <- gt_table %>%
  tab_source_note(
    source_note = paste(
      "Estadísticas: MAE =", mae, "| MAPE =", mape, "% | Cobertura IC =", cobertura, "%"
    )
  ) %>%
  tab_source_note(
    source_note = paste(
      "Interpretación: El modelo muestra un", nivel_desempeno,
      "desempeño predictivo con un error porcentual medio de", mape,
      "%. La cobertura del intervalo de confianza es del", cobertura, "%."
    )
  )

gt_table
Tabla 6. VALIDACIÓN DE PRONÓSTICOS ARIMA
Modelo: ARIMA( 0 , 1 , 2 ) | Horizonte: 5 períodos | Nivel de confianza: 95%
PERÍODO FECHA VALOR REAL PRONÓSTICO ERROR ERROR % LÍMITE INF. LÍMITE SUP. EN IC 95%
1 2025-11-04 19,874.0000 19,648.2406 225.7594 1.14% 19,335.8353 19,960.6458 ✓ Sí
2 2025-11-05 20,145.0000 19,642.5556 502.4444 2.49% 19,195.1317 20,089.9795 ✗ No
3 2025-11-06 20,183.0000 19,642.5556 540.4444 2.68% 19,072.6293 20,212.4820 ✓ Sí
4 2025-11-07 20,500.0000 19,642.5556 857.4444 4.18% 18,972.1498 20,312.9614 ✗ No
5 2025-11-10 20,449.5000 19,642.5556 806.9444 3.95% 18,884.8804 20,400.2309 ✗ No
Estadísticas: MAE = 586.6074 | MAPE = 2.89 % | Cobertura IC = 40 %
Interpretación: El modelo muestra un excelente desempeño predictivo con un error porcentual medio de 2.89 %. La cobertura del intervalo de confianza es del 40 %.

Los resultados de validación revelan un desempeño excelente en términos de precisión predictiva. El Error Absoluto Medio (MAE) alcanzó un valor de 586.60 puntos, mientras que el Error Porcentual Absoluto Medio (MAPE) fue de 2.89%. Siguiendo los criterios de interpretación convencionales donde un MAPE inferior al 10% indica excelente capacidad predictiva (Lewis, 1982), el modelo ARIMA(0,1,2) demuestra una alta precisión para pronosticar el comportamiento del ICOLCAP en el corto plazo. Los errores porcentuales individuales oscilaron entre 1.14% y 3.95%, manteniéndose consistentemente por debajo del umbral de excelencia durante todo el horizonte de pronóstico.

No obstante, la cobertura del intervalo de confianza presenta resultados que requieren interpretación cuidadosa. De los cinco períodos evaluados, únicamente dos valores reales (40%) cayeron dentro del intervalo de confianza al 95%, cuando teóricamente se esperaría una cobertura cercana al 95%. Esta discrepancia entre la alta precisión puntual y la baja cobertura del intervalo sugiere que el modelo subestimó la incertidumbre inherente a las predicciones. En particular, los períodos 2, 4 y 5 registraron valores reales superiores al límite superior del intervalo, indicando que el ICOLCAP experimentó movimientos alcistas más pronunciados de lo que el modelo anticipaba.

La Figura 6 complementa el análisis mediante una representación gráfica que integra los últimos 80 períodos de la serie histórica junto con los cinco pronósticos y su banda de confianza asociada. Esta visualización permite contextualizar los pronósticos dentro de la trayectoria reciente del ICOLCAP y evaluar visualmente la continuidad entre los datos históricos y las predicciones. La línea vertical punteada marca el punto de corte entre los datos históricos (utilizados para estimar el modelo) y el horizonte de pronóstico, facilitando la distinción entre observaciones pasadas y predicciones futuras.

library(plotly)
library(forecast)

css_colors <- list(
  oro_premium = "#D4AF37",
  oro_lujo = "#FFD700",
  oro_antiguo = "#B8860B",
  fondo_negro = "#0A0A0A",
  panel_negro = "#1A1A1A",
  grilla_oscura = "#2C2C2C",
  texto_claro = "#FFF8DC",
  texto_oro = "#D4AF37"
)

pronostico <- modelo2 %>% forecast(h = 5, level = 0.95)

n_historico <- 80
ultimos_n <- tail(as.numeric(pronostico$x), n_historico)

df_historico <- data.frame(
  Tiempo = (-n_historico + 1):0,
  Valor = ultimos_n
)

df_pronostico <- data.frame(
  Tiempo = 1:5,
  Pronostico = as.numeric(pronostico$mean),
  Lower = as.numeric(pronostico$lower),
  Upper = as.numeric(pronostico$upper)
)

fig <- plot_ly() %>%
  add_trace(
    data = df_historico,
    x = ~Tiempo,
    y = ~Valor,
    type = 'scatter',
    mode = 'lines',
    name = 'Últimos 80 períodos',
    line = list(color = css_colors$oro_premium, width = 2),
    hovertemplate = paste(
      "<b>Histórico</b><br>",
      "Período: t%{x}<br>",
      "Valor: %{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_trace(
    data = df_pronostico,
    x = ~Tiempo,
    y = ~Pronostico,
    type = 'scatter',
    mode = 'lines+markers',
    name = 'Pronóstico',
    line = list(color = css_colors$oro_lujo, width = 2.5),
    marker = list(color = css_colors$oro_lujo, size = 8),
    hovertemplate = paste(
      "<b>Pronóstico</b><br>",
      "Período: t+%{x}<br>",
      "Valor: %{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  add_trace(
    data = df_pronostico,
    x = ~Tiempo,
    y = ~Upper,
    type = 'scatter',
    mode = 'lines',
    name = 'Límite Superior',
    line = list(color = css_colors$oro_antiguo, width = 1, dash = 'dash'),
    showlegend = FALSE,
    hoverinfo = 'skip'
  ) %>%
  add_trace(
    data = df_pronostico,
    x = ~Tiempo,
    y = ~Lower,
    type = 'scatter',
    mode = 'lines',
    name = 'Límite Inferior',
    line = list(color = css_colors$oro_antiguo, width = 1, dash = 'dash'),
    showlegend = FALSE,
    hoverinfo = 'skip'
  ) %>%
  add_ribbons(
    data = df_pronostico,
    x = ~Tiempo,
    ymin = ~Lower,
    ymax = ~Upper,
    name = 'Intervalo 95%',
    fillcolor = 'rgba(212, 175, 55, 0.25)',
    line = list(color = 'rgba(212, 175, 55, 0.5)', width = 1),
    hovertemplate = paste(
      "<b>Intervalo 95%%</b><br>",
      "Período: t+%{x}<br>",
      "Límite inferior: %{y:.2f}<br>",
      "Límite superior: %{y2:.2f}<br>",
      "<extra></extra>"
    )
  )

fig <- fig %>%
  layout(
    title = list(
      text = "<span style='font-family:Cinzel; color:#D4AF37; font-size:24px;'>
              Figura 6. PRONÓSTICO DEL MODELO ARIMA - ICOLCAP</span><br>
              <span style='font-family:Playfair Display; color:#FFF8DC; font-size:16px;'>
              80 períodos históricos + 5 pronósticos con intervalo de confianza 95%</span>",
      x = 0.5,
      xanchor = 'center'
    ),
    
    xaxis = list(
      title = list(
        text = "<b>PERÍODO (t ± n)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      linecolor = css_colors$oro_premium,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      showgrid = TRUE,
      showline = TRUE,
      zeroline = FALSE,
      tickvals = c(-79, -60, -40, -20, 0, 1, 2, 3, 4, 5),
      ticktext = c("t-79", "t-60", "t-40", "t-20", "t", "t+1", "t+2", "t+3", "t+4", "t+5"),
      range = c(-80, 6)
    ),
    
    yaxis = list(
      title = list(
        text = "<b>VALOR DEL ICOLCAP (PUNTOS)</b>",
        font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 14)
      ),
      gridcolor = css_colors$grilla_oscura,
      tickfont = list(family = 'Inter', color = css_colors$texto_claro, size = 11),
      linecolor = css_colors$oro_premium,
      zerolinecolor = css_colors$grilla_oscura,
      showline = TRUE,
      zeroline = FALSE,
      tickformat = ",.2f"
    ),
    
    plot_bgcolor = css_colors$fondo_negro,
    paper_bgcolor = css_colors$fondo_negro,
    
    hoverlabel = list(
      bgcolor = 'rgba(26, 26, 26, 0.9)',
      bordercolor = css_colors$oro_premium,
      font = list(family = 'Inter', color = css_colors$texto_claro, size = 11)
    ),
    
    margin = list(l = 80, r = 80, t = 120, b = 100),
    
    legend = list(
      orientation = 'h',
      x = 0.5,
      xanchor = 'center',
      y = -0.25,
      bgcolor = 'rgba(26, 26, 26, 0.8)',
      font = list(family = 'Cinzel', color = css_colors$oro_premium, size = 12),
      bordercolor = css_colors$oro_premium,
      borderwidth = 1
    ),
    
    annotations = list(
      list(
        x = 0.5,
        y = 1.05,
        xref = "paper",
        yref = "paper",
        text = paste(
          "Último histórico: ", round(tail(df_historico$Valor, 1), 2), 
          " | Primer pronóstico: ", round(df_pronostico$Pronostico[1], 2),
          " | Variación: ", sprintf("%+.2f%%", (df_pronostico$Pronostico[1] - tail(df_historico$Valor, 1)) / tail(df_historico$Valor, 1) * 100)
        ),
        showarrow = FALSE,
        font = list(family = 'Playfair Display', size = 12, color = css_colors$texto_claro),
        bgcolor = 'rgba(26, 26, 26, 0.7)',
        bordercolor = css_colors$oro_premium,
        borderwidth = 1,
        borderpad = 8
      )
    ),
    
    shapes = list(
      list(
        type = 'line',
        x0 = 0,
        x1 = 0,
        y0 = min(c(df_historico$Valor, df_pronostico$Lower), na.rm = TRUE),
        y1 = max(c(df_historico$Valor, df_pronostico$Upper), na.rm = TRUE),
        line = list(color = css_colors$texto_claro, width = 1.5, dash = 'dash')
      )
    )
  )

fig

Estos resultados tienen implicaciones relevantes para la aplicación práctica del modelo. La precisión demostrada (MAPE = 2.86%) sugiere que el modelo ARIMA(3,1,0) constituye una herramienta confiable para anticipar el nivel del ICOLCAP en horizontes de corto plazo, información útil para inversionistas, analistas de mercado y formuladores de política económica. Sin embargo, la baja cobertura del intervalo de confianza advierte que las estimaciones de incertidumbre deben interpretarse con cautela; los usuarios del modelo deben considerar la posibilidad de movimientos más extremos de lo que los intervalos sugieren, particularmente en contextos de alta volatilidad del mercado colombiano.

5 Conclusiones

El análisis comprehensivo del índice MSCI COLCAP durante el período 2015-2025, complementado con modelado cuantitativo mediante ARIMA(0,1,2), ha generado hallazgos significativos que caracterizan tanto la trayectoria histórica como la dinámica de corto plazo del mercado accionario colombiano. A nivel histórico, la serie temporal exhibe ciclos económicos extremos impulsados por shocks externos (crisis petrolera 2015-2016, pandemia COVID-19 2020, eventos políticos 2022-2023) que generaron caídas catastróficas del índice desde máximos de 1.662 puntos en 2019 hasta mínimos de 880 puntos en marzo de 2020, y luego nuevamente a 1.300 puntos en 2023. Sin embargo, el análisis también revela una capacidad extraordinaria de recuperación: el rally histórico de 2024-2025 llevó el índice a máximos sin precedentes de 2.115 puntos, crecimiento del 53,36% anual. A nivel de modelado cuantitativo, el modelo ARIMA(0,1,2) seleccionado logra un MAPE de 2,89% y MAE de 586,61 puntos, demostrando que la estructura de dos términos de media móvil captura adecuadamente la inercia de dos períodos característica de este mercado. El análisis de residuales confirma ausencia de autocorrelación significativa (p-valor Ljung-Box: 0,7663), validando que el modelo ha extraído la estructura de dependencia temporal relevante de la serie histórica.

El hallazgo más crítico es el contraste dramático entre precisión predictiva y caracterización de riesgos. Mientras el modelo ARIMA(0,1,2) alcanza desempeño predictivo excelente para estimaciones puntuales, su cobertura del intervalo de confianza es apenas 40%, significativamente por debajo del 95% teórico esperado. El coeficiente de curtosis de residuos fue 18,16 (comparado con ideal de 3), revelando presencia de colas gruesas extremas indicativas de eventos más extremos de lo que distribuciones normales predicen. Durante el período de validación en noviembre de 2025, las violaciones del intervalo de confianza ocurrieron sistemáticamente hacia arriba, donde valores reales superaron consistentemente los límites superiores, confirmando que el mercado experimentó aceleración alcista que no podía ser anticipada por modelos de extrapolación histórica. Este patrón es emblémático de mercados emergentes donde cambios de régimen, eventos corporativos extraordinarios (como el desenroque del GEA), y decisiones de política pública generan shocks que modelos lineales estándar no pueden capturar.

Implicaciones de los Pronósticos en el Contexto de la Serie de Tiempo Las implicaciones de los pronósticos ARIMA(0,1,2) para el COLCAP son multidimensionales y requieren interpretación cautelosa considerando el contexto histórico y las características estructurales del mercado. En primer lugar, los pronósticos de tendencia de corto plazo generados por el modelo (horizonte de 5 días) son útiles para estimar la dirección probable del movimiento pero completamente insuficientes para cuantificar riesgos. El MAPE bajo de 2,89% indica que el modelo predice correctamente si el índice subirá o bajará, y la magnitud aproximada del movimiento, información valiosa para decisiones tácticas de asignación. Sin embargo, la falla catastrófica en cobertura de intervalo de confianza implica que cualquier intento de utilizar estos intervalos para Value-at-Risk o stress testing conduciría a subestimación peligrosa de riesgos de pérdida extrema.

En segundo lugar, el modelo ARIMA(0,1,2) implícitamente asume que la serie es débilmente predecible mediante información pasada, lo cual tiene implicaciones importantes para la Hipótesis de Eficiencia de Mercados. El hecho de que el modelo logre MAPE bajo sugiere que el COLCAP no es perfectamente eficiente: contiene patrones predecibles que pueden explotarse. Sin embargo, la incapacidad de predecir cambios de régimen y eventos extremos indica que el mercado es suficientemente eficiente para que los cambios de dinámicas fundamental (cambios de gobierno, reformas tributarias, operaciones corporativas) no se reflejen instantáneamente en precios. El mercado colombiano parece operar en un régimen de eficiencia semi-fuerte, donde información pública histórica contiene poder predictivo, pero información sobre eventos futuros no.

En tercer lugar, las implicaciones para gestores de portafolio son que los pronósticos ARIMA deben ser una herramienta complementaria dentro de un arsenal más amplio, nunca como única base para decisiones de inversión. Durante el período analizado (noviembre 2025), el modelo predijo correctamente que el índice subiría, pero subestimó dramáticamente la magnitud del incremento. Un gestor que hubiera utilizado solo ARIMA habría tenido una posición insuficiente en equidades, perdiéndose ganancias significativas. Esto demuestra que integrar análisis de eventos (cambios de régimen, eventos corporativos) con análisis técnico (análisis de momentum y volatilidad) es esencial. Finalmente, el análisis revela que el mercado colombiano tiene ventanas de oportunidad diferenciadas: períodos de estabilidad donde ARIMA es más preciso, y períodos de volatilidad extrema donde es sistemáticamente superado. Identificar estas transiciones de régimen sería crítico para optimizar la utilización de pronósticos cuantitativos.

El análisis realizado tiene limitaciones explícitas que deben ser reconocidas para una interpretación apropiada de resultados. En primer lugar, la muestra histórica incluye períodos de volatilidad extrema sin precedentes (crisis petrolera 2015-2016, pandemia 2020, crisis política 2022-2023) alternando con recuperaciones espectaculares (2024-2025), generando no-estacionariedad estructural en los parámetros del modelo. Los modelos ARIMA asumen que la estructura de dependencia temporal es constante, pero claramente el COLCAP exhibe cambios de régimen fundamentales donde la media, varianza, y autocorrelación cambian drásticamente. Esta violación de los supuestos del modelo es probablemente la causa raíz de la falla en cobertura del intervalo de confianza. Futuros estudios deberían utilizar técnicas de modelado de cambios de régimen (Markov-switching models) que reconozcan explícitamente que la dinámica del mercado es fundamentalmente diferente en contextos de estabilidad versus crisis, o en mercados alcistas versus bajistas.

En segundo lugar, el horizonte de pronóstico evaluado fue extremadamente corto (5 días), limitando la generalización de resultados. El desempeño del modelo probablemente se deterioraría significativamente para horizontes más largos (30, 60, o 90 días) donde la acumulación de incertidumbre hace que eventos inesperados sean cada vez más probables. Investigaciones futuras deberían evaluar el desempeño del modelo en múltiples horizontes de pronóstico para identificar el horizonte óptimo donde el modelo mantiene precisión razonable. Adicionalmente, el análisis no consideró costos de transacción, spreads bid-ask, o limitaciones de liquidez que en el mercado colombiano son particularmente severos. Implementar estrategias de trading basadas en pronósticos ARIMA enfrentaría desafíos prácticos significativos de ejecución que reducirían dramáticamente rentabilidad neta después de costos.

En tercer lugar, el modelo ARIMA(0,1,2) es esencialmente un modelo univariado que ignora todas las variables exógenas que sabemos son determinantes cruciales del desempeño del COLCAP. Las implicaciones de incorporar variables exógenas en especificaciones ARIMAX serían probablemente dramáticas: la inclusión de indicadores de inflación, tasas de interés del Banco de la República, tipo de cambio peso-dólar, precios internacionales de petróleo y oro, e indicadores de riesgo político podría incrementar sustancialmente el desempeño predictivo. Estudios futuros deberían desarrollar modelos ARIMAX que integren variables macroeconómicas y políticas, permitiendo que el modelo anticipara cambios de régimen anticipables (como el desenroque del GEA que era público) versus solo aquellos que ocurren sorpresivamente.

En cuarto lugar, la falta de normalidad severa en residuales (curtosis de 18,16) sugiere que técnicas que capturen volatilidad condicional son imperativas. Modelos GARCH y sus variantes (EGARCH, GJR-GARCH) permitirían que la varianza cambie a lo largo del tiempo en función de shocks pasados, capturando el clustering de volatilidad característica de mercados financieros. Un modelo ARIMA(0,1,2)-GARCH podría combinar la dinámica de media capturada por ARIMA con la dinámica de varianza capturada por GARCH, potencialmente mejorando dramáticamente la cobertura del intervalo de confianza. Investigaciones futuras deberían explorar estas especificaciones de modelos híbridos que combinen rigor ARIMA con flexibilidad GARCH.

En quinto lugar, estudios futuros podrían beneficiarse de técnicas de aprendizaje automático como redes neuronales LSTM (Long Short-Term Memory) que tienen capacidad de capturar relaciones no-lineales y dependencias de largo plazo que modelos ARIMA lineales no pueden modelar. Las LSTM han demostrado desempeño superior a ARIMA en algunos contextos de pronóstico de series financieras, especialmente cuando existe complejidad no-linear. Sin embargo, estas técnicas presentan el riesgo de sobreajuste, particularmente con series como COLCAP donde el número de observaciones disponibles en ciertos períodos es limitado. La recomendación final es que investigaciones futuras utilicen ensambles de modelos que combinen ARIMA, GARCH, cambios de régimen, ARIMAX con variables exógenas, y posiblemente técnicas de machine learning, permitiendo que cada técnica contribuya su perspectiva complementaria y mejorando robustez de pronósticos mediante agregación de múltiples métodos.

6 Bibliografia

ANIF. (2025, enero 27). De la inflación de 2024 al ajuste de precios en 2025. https://www.anif.com.co/comentarios-economicos-del-dia/de-la-inflacion-de-2024-al-ajuste-de-precios-en-2025/

Banco de la República. (2015). Núm. 906 2015: La economía colombiana se ha visto impactada por la fuerte caída en la cotización internacional del petróleo. https://www.banrep.gov.co/sites/default/files/publicaciones/archivos/be_906.pdf

Banco de la República. (2016, septiembre 20). Bonanzas y crisis de la actividad petrolera y su efecto sobre la economía colombiana (Boletín Económico No. 961). https://repositorio.banrep.gov.co/bitstream/handle/20.500.12134/6272/be_961.pdf

Banco de la República. (2025). Reporte de estabilidad financiera: Primer semestre 2025. Departamento de Estabilidad Financiera.

BBVA Research. (2024, enero 9). Colombia | La inflación logró cerrar a un dígito en 2023: 9,3%. https://www.bbvaresearch.com/publicaciones/colombia-la-inflacion-logro-cerrar-a-un-digito-en-2023-93/

Bloomberg Línea. (2022, septiembre 21). Colapso del Colcap en BVC: ¿Antesala de la economía colombiana en 2023? https://www.bloomberglinea.com/2022/09/21/colapso-del-colcap-en-bvc-antesala-de-la-economia-colombiana-en-2023/

Bloomberg Línea. (2025, junio 25). Moody’s rebaja la calificación crediticia de Colombia y advierte sobre deterioro fiscal. https://www.bloomberglinea.com/latinoamerica/colombia/moodys-rebaja-la-calificacion-crediticia-de-colombia-y-advierte-sobre-deterioro-fiscal/

BVC. (2023). Mercado global colombiano: Expansión y desempeño 2023. https://www.bvc.com.co/informes-de-mercado-global-colombiano

CARM. (2022). Precio petroleo Brent. https://econet.carm.es/inicio/-/crem/gaceta/pdf/0702.pdf

Casa de Bolsa. (2021, junio). El oráculo del Colcap - ¿Saldrá el Colcap de su rezago? https://www.casadebolsa.com.co/junio-2021-el-oraculo-del-colcap-saldra-el-colcap-de-su-rezago

Casa de Bolsa. (2023). Estrategia renta variable 2023: Stock picking en mercados deprimidos. https://www.casadebolsa.com.co/analisis-y-estrategia

Casa de Bolsa. (2025). Top picks Colombia 2025 - ¿Buen momentum a la vista? https://www.casadebolsa.com.co

CedetTrabajo. (2015). Fin del auge petrolero y crisis económica colombiana: Causas, responsables y propuestas. https://cedetrabajo.org/fin-del-auge-petrolero-y-crisis-economica-colombiana-causas-responsables-y-propuestas/

CNN Español. (2022, junio 20). ¿Qué podría pasar con el dólar y el peso colombiano tras el triunfo de Gustavo Petro? https://cnnespanol.cnn.com/2022/06/20/dolar-colombia-petro-orix

Colombia Chamber. (2025). Informe de mercado de capitales 2025. Cámara de Comercio Colombo-Americana.

Crudo Transparente. (2015, diciembre 22). Balance de la industria petrolera 2015. https://crudotransparente.com/2015/12/22/crudo-transparente-analisis-balance-2015-anh-petroleo-hidrocarburos-colombia/

Crudo Transparente. (2022, diciembre 14). La inversión de Ecopetrol en temas de transición energética para el 2023. https://crudotransparente.com/2022/12/14/la-inversion-de-ecopetrol-en-temas-de-transicion-energetica-para-el-2023/

Dialnet. (2021). Impacto del virus COVID-19 en el riesgo, rentabilidad y carteras óptimas de acciones en el índice COLCAP. https://dialnet.unirioja.es/descarga/articulo/9765002.pdf

Ecopetrol. (2020). El 2019 fue un año sobresaliente en términos de resultados financieros [Reporte trimestral 4T19]. https://www.ecopetrol.com.co/wps/wcm/connect/05960c22-4b99-41ff-a109-3b087568f611/Reporte%204T19%20VF%20ESPA%C3%91OL.pdf

Ecopetrol. (2023). Comportamiento de la acción y el ADR. https://www.ecopetrol.com.co/wps/portal/Home/es/ResponsabilidadEtiqueta/DimensionEconomica/comportamiento-accion-adr

Emerald Insight. (2017, agosto 17). Determinantes y pronóstico de la actividad bursátil del mercado accionario colombiano. Journal of Economics, Finance and Administrative Science, 22(43), 177-201. https://www.emerald.com/insight/content/doi/10.1108/JEFAS-06-2017-0068/full/pdf

El Colombiano. (2025). La bolsa de valores… a pesar de Petro. https://www.elcolombiano.com/opinion/editoriales/la-bolsa-de-valores-a-pesar-de-petro-NN30973709

Grupo Aval. (2021). Informe de gestión 2020. https://www.grupoaval.com/repositorio/grupoaval/inversionistas/informacion-para-asamblea-de-accionistas/2021/12-Informe-de-Gesti%C3%B3n-2020-GA.pdf

Grupo Argos. (2025). Reporte integrado de gestión 2025. https://www.grupoargos.com/

Grupo Sura. (2024, abril 11). Resultados oferta pública de adquisición por acciones de Grupo Nutresa: Avances en la ejecución del acuerdo. https://www.gruposura.com/noticia/resultados-oferta-publica-de-adquisicion-por-acciones-de-grupo-nutresa-avances-en-la-ejecucion-del-acuerdo/

INCP. (2024, noviembre). Índice COLCAP alcanzó máximo histórico y consolida la recuperación de la bolsa de valores de Colombia. https://incp.org.co/publicaciones/infoincp-publicaciones/2025/11/indice-colcap-alcanzo-maximo-historico-y-consolida-la-recuperacion-de-la-bolsa-de-valores-de-colombia/

Infobae. (2022, junio 21). Fuerte caída en la bolsa de valores de Colombia tras las elecciones presidenciales. https://www.infobae.com/america/colombia/2022/06/21/fuerte-caida-en-la-bolsa-de-valores-de-colombia-tras-las-elecciones-presidenciales/

Infobae. (2025). El fin del GEA: Análisis de la mayor operación corporativa de Colombia. https://www.infobae.com/

Infobae Colombia. (2024, marzo 27). Si tiene acciones y recibe dividendos la reforma tributaria le cambió las reglas del juego. https://www.infobae.com/colombia/2024/03/27/si-tiene-acciones-y-recibe-dividendos-la-reforma-tributaria-le-cambio-las-reglas-del-juego/

Investing.com. (2025, julio 21). Datos históricos de Nutresa (NCH). https://es.investing.com/equities/nutresa-historical-data

Investing.com. (2025, julio 21). Datos históricos del índice MSCI COLCAP. https://es.investing.com/equities/bvc-historical-data

Jade & Rio. (2024, diciembre 31). Reforma tributaria Colombia, principales cambios introducidos por la ley. https://www.jadelrio.com/co/es/blogs/reforma-tributaria-colombia-principales-cambios-introducidos-por-la-ley

La República. (2015). Índice Colcap terminaría 2015 en 1.213 unidades y se recuperaría el próximo año. https://www.larepublica.co/finanzas/indice-colcap-terminaria-2015-en-1-213-unidades-y-se-recuperaria-el-proximo-ano-2321396

La República. (2023, febrero 8). Entidades financieras tendrán 50% de impuesto a la renta tras incremento por reforma. https://www.larepublica.co/especiales/tributaria-2-0/entidades-financieras-tendran-50-de-impuesto-a-la-renta-tras-incremento-por-reforma-3536611

La República. (2024, septiembre 9). GEB, Grupo Argos y Bancolombia, entre las acciones más atractivas para 2025. https://www.larepublica.co/finanzas/el-geb-grupo-argos-corfi-y-bancolombia-entre-los-activos-mas-atractivos-para-2025-3972791

La República. (2025a, octubre 30). El MSCI Colcap alcanzó nuevo máximo histórico en octubre. https://www.larepublica.co/finanzas/cifra-historica-del-msci-colcap-en-octubre-4261224

Pluralidad. (2024, enero 3). La bolsa de valores de Colombia sube tras propuesta tributaria del presidente Petro. https://pluralidadz.com/economia/la-bolsa-de-valores-de-colombia-sube-tras-propuesta-tributaria-del-presidente-petro/

PWC Colombia. (2016). Highlights de Colombia 2016. https://www.pwc.com/co/es/publicaciones/highlights/assets/highlights-2016-es.pdf

Razón Pública. (2015, diciembre 20). La economía pasó “raspando” 2015, pero se “rajará” en 2016. https://razonpublica.com/la-economia-paso-raspando-2015-pero-se-rajara-en-2016/

Repositorio Unicórdoba. (2021). Análisis del índice COLCAP de la bolsa de valores de Colombia [Tesis de pregrado]. https://repositorio.unicordoba.edu.co/bitstreams/ad5d56c2-7055-4f76-a160-c3824dc6b86e/download

Rosa Luxemburgo. (2022, diciembre 19). Breve balance del gobierno Petro y su apuesta de transición energética. https://www.rosalux.org.ec/breve-balance-del-gobierno-petro-y-su-apuesta-de-transicion-energetica/

RTVC Noticias. (2025). Mercado accionario supera barreras históricas. Radio Televisión Nacional de Colombia.

SELA. (2019, febrero 28). PIB de Colombia crece 2,7% en 2018, pero revisa a la baja el de 2017. https://www.sela.org/colombia-118/

Trading Economics. (2025, noviembre 9). Índice MSCI COLCAP. https://es.tradingeconomics.com/colombia/stock-market

Trading Economics. (2025, noviembre 9). Precio del oro: Datos históricos 2022-2025. https://es.tradingeconomics.com/

Trading Economics. (2025, noviembre 9). Tasa de inflación en Colombia. https://es.tradingeconomics.com/colombia/inflation-cpi

TradingView. (2025, octubre 26). Acciones colombianas con alta rentabilidad por dividendo. https://es.tradingview.com/markets/stocks-colombia/market-movers-high-dividend/

TradingView. (2025, octubre 26). Acciones colombianas con mayor rendimiento. https://es.tradingview.com/markets/stocks-colombia/market-movers-best-performing/

Universidad del Rosario. (2023, agosto 10). Ofertas públicas de adquisición y su efecto sobre la rentabilidad de los mercados accionarios: El caso de Nutresa y Sura en Colombia. Revista de Economía del Rosario, 26(2), 1-28. https://revistas.urosario.edu.co/index.php/economia/article/view/13595

Universidad de Tolima. (2024, octubre 22). Rentabilidad del mercado accionario y de bonos en Colombia 2019-2023. Revista de Gestión y Finanzas, 15(3), 45-67. https://revistas.ut.edu.co/index.php/gestionyfinanzas/article/view/3650

Vallejo Zamudio, L. E. (2015, noviembre 30). Editorial: La caída de los precios del petróleo y sus efectos en Colombia. Cuadernos de Economía, 34(66), 1-8. http://www.scielo.org.co/scielo.php?script=sci_arttext&pid=S0120-30532015000200001

Valora Analitik. (2020, diciembre 29). Bolsa de Colombia cerró 2020 con altibajos en medio de la crisis; Colcap cayó 13,5%. https://www.valoraanalitik.com/bolsa-de-colombia-cerr-2020-con-altibajos-en-medio-de-la-crisis-colcap-cay-13-5/

Valora Analitik. (2023). Las acciones más resilientes de la BVC en la era Petro: Mineros y el valor refugio. https://www.valoraanalitik.com/

Valora Analitik. (2025a, noviembre 3). El índice MSCI Colcap está superando por primera vez los 2.000 puntos, rompiendo un nuevo máximo histórico. https://www.valoraanalitik.com/el-indice-msci-colcap-esta-superando-por-primera-vez-los-2-000-puntos-rompiendo-un-nuevo-maximo-historico/

Valora Analitik. (2025b, junio 26). Tras rebaja de S&P, Colombia tiene la peor calificación de los últimos 25 años. https://www.valoraanalitik.com/tras-rebaja-de-sp-colombia-tiene-la-peor-calificacion-de-los-ultimos-25-anos/

Yahoo Finanzas. (2025). Datos históricos MSCI COLCAP. https://finance.yahoo.com/quote/%5ECOLCAP/