Análitica de Datos Financieros de Mercado

El siguiente documento muestra un ejercicio basado en la Extracción, Transformación y Carga (ETL), el cual tiene como propósito principal obtener información financiera de mercado, realizar procesos de limpieza y transformación, y garantizar la calidad e integridad de los datos antes de proceder al análisis estadístico.

Para ello, se emplean diversas librerías especializadas del ecosistema de R, entre ellas herramientas orientadas al análisis financiero y a la manipulación eficiente de datos. Particularmente, se utiliza el paquete yfR para la descarga de información histórica desde Yahoo Finance, junto con librerías como quantmod, tidyverse, tidyquant, zoo y PerformanceAnalytics, que permiten el tratamiento de series temporales financieras y el cálculo de métricas de rendimiento.

Librerías

En esta fase inicial es necesario instalar las librerías que permitirán realizar la descarga, transformación y análisis de los datos financieros. Los paquetes quantmod, tidyverse, tidyquant, zoo y PerformanceAnalytics se instalan desde el repositorio oficial de CRAN mediante la función install.packages(), ya que forman parte del ecosistema estándar de R para análisis de datos y finanzas.

install.packages(c("quantmod", "tidyverse", "tidyquant", "zoo", "PerformanceAnalytics"))

Por su parte, el paquete yfR, utilizado para descargar información histórica desde Yahoo Finance, puede instalarse desde CRAN; sin embargo, en este ejercicio se opta por instalarlo directamente desde GitHub mediante:

devtools::install_github('ropensci/yfR')

Cargue de librerías

library(yfR)                 # Descarga datos desde Yahoo Finance
library(quantmod)            # Modelado cuantitativo financiero
library(tidyverse)           # Manipulación y visualización de datos
library(tidyquant)           # Integración tidy + finanzas
library(zoo)                 # Series temporales
library(PerformanceAnalytics)# Métricas de rendimiento
library(dplyr)               # Manipulación de datos
library(tidyr)               # Transformación de estructuras

Extracción de datos

Como primer paso del proceso ETL se definen los activos financieros objeto de estudio. En este caso, se seleccionan acciones que componen el índice COLCAP, el cual agrupa a las principales empresas listadas en la Bolsa de Valores de Colombia. La identificación de cada activo se realiza mediante su respectivo ticker, información que debe consultarse directamente en Yahoo Finance.

tickers <- c("CEMARGOS.CL" , "CORFICOLCF.CL", "PFCIBEST.CL",
             
             "BOGOTA.CL", "ECOPETROL.CL", "GEB.CL", "GRUPOARGOS.CL",
             
             "MINEROS.CL", "GRUPOSURA.CL", "NUTRESA.CL","GRUPOAVAL.CL","PFAVAL.CL",
             
             "PFCEMARGOS.CL", "PFDAVVNDA.CL", "PFGRUPOARG.CL", "PFGRUPSURA.CL"
             
             ,"PROMIGAS.CL", "TERPEL.CL", "GRUBOLIVAR.CL")

Posteriormente, se define el horizonte temporal del análisis estableciendo una fecha de inicio y una fecha de finalización bajo el formato día-mes-año (DD-MM-AAAA). Inicialmente, ambas fechas se declaran como cadenas de texto y posteriormente se convierten al tipo de dato Date mediante la función as.Date() especificando el formato correspondiente (“%d-%m-%Y”). Esta transformación es esencial para garantizar una correcta interpretación cronológica, permitir filtrados por rango temporal y facilitar el manejo adecuado de las series de tiempo dentro del entorno de análisis.

fecha_inicio <- "01-01-2019"
fecha_fin    <- "02-01-2025"

fecha_inicio <- as.Date(fecha_inicio, format = "%d-%m-%Y")

fecha_fin    <- as.Date(fecha_fin, format = "%d-%m-%Y")

Una vez definidos los tickers, se procede a la descarga de la información histórica utilizando la función yf_get() del paquete yfR. Para este análisis se establece una frecuencia diaria (daily), adecuada para estudiar la dinámica de corto y mediano plazo de los precios. Asimismo, se emplean retornos logarítmicos (type_return = “log”), preferidos en finanzas por su propiedad de aditividad temporal y su conveniencia en modelaciones econométricas. Finalmente, se activa el parámetro do_complete_data = TRUE con el fin de garantizar la integridad de las series temporales, evitando inconsistencias o saltos injustificados en los datos descargados.

datos_financieros <- yf_get(
  tickers = tickers, 
  first_date = fecha_inicio, 
  last_date = fecha_fin,
  freq_data = "daily",           
  type_return = "log",
  do_complete_data = TRUE        
  
)

Visualizamos nuestra base de datos incial

datos_financieros 
## # A tibble: 27,994 × 11
##    ticker    ref_date   price_open price_high price_low price_close volume
##  * <chr>     <date>          <dbl>      <dbl>     <dbl>       <dbl>  <dbl>
##  1 BOGOTA.CL 2019-01-01     51887.     51887.    51887.      51887.      0
##  2 BOGOTA.CL 2019-01-02     50269.     53932.    50232.      50269.   1478
##  3 BOGOTA.CL 2019-01-03     53003.     53077.    50269.      53003.   2358
##  4 BOGOTA.CL 2019-01-04     52110.     54844.    52073.      52110.   7626
##  5 BOGOTA.CL 2019-01-07     52110.     52110.    52110.      52110.      0
##  6 BOGOTA.CL 2019-01-08     52538.     52538.    52166.      52538.  12387
##  7 BOGOTA.CL 2019-01-09     53932.     53932.    53003.      53932.   3540
##  8 BOGOTA.CL 2019-01-10     53932.     54825.    53932.      53932.   2222
##  9 BOGOTA.CL 2019-01-11     53932.     54769.    53932.      53932.   9576
## 10 BOGOTA.CL 2019-01-14     53932.     53932.    53932.      53932.   6291
## # ℹ 27,984 more rows
## # ℹ 4 more variables: price_adjusted <dbl>, ret_adjusted_prices <dbl>,
## #   ret_closing_prices <dbl>, cumret_adjusted_prices <dbl>

En este momento es pertienente hacer un filtro de verificación en la obtención de los datos.Se establecio un número de activos financieros anteriormente, entonces podemos revisar si todos los datos de los activos financieros fueron cargados correctamente.

El resultado del siguiente fragmento es un vector vacío si todos los activos financieros fueron correctamente descargados. En caso contrario, el resultado mostrará cuáles no se cargaron, permitiendo tomar acciones correctivas.

# Número de tickers definidos
length(tickers)
## [1] 19
# Número de tickers efectivamente cargados
length(unique(datos_financieros$ticker))
## [1] 18
# Ver cuáles tickers fueron cargados
unique(datos_financieros$ticker)
##  [1] "BOGOTA.CL"     "CEMARGOS.CL"   "CORFICOLCF.CL" "ECOPETROL.CL" 
##  [5] "GEB.CL"        "GRUBOLIVAR.CL" "GRUPOARGOS.CL" "GRUPOAVAL.CL" 
##  [9] "GRUPOSURA.CL"  "MINEROS.CL"    "NUTRESA.CL"    "PFAVAL.CL"    
## [13] "PFCIBEST.CL"   "PFDAVVNDA.CL"  "PFGRUPOARG.CL" "PFGRUPSURA.CL"
## [17] "PROMIGAS.CL"   "TERPEL.CL"
# Verificar si falta alguno
setdiff(tickers, unique(datos_financieros$ticker))
## [1] "PFCEMARGOS.CL"

Por ejemplo, la acción PFCEMARGOS.CL fue eliminada automáticamente por la función yf_get dado que el paquete exige que al menos el 75% de los datos del período solicitado sean válidos, pero como solo había 4%, el activo fue descartado automáticamente. Las razones pueden ser:

  1. El activo dejó de cotizar en el período solicitado
  2. El ticker cambió de nombre
  3. Problemas de disponibilidad en Yahoo Finance
  4. Muy baja liquidez o información incompleta
  5. El período solicitado incluye fechas anteriores a su emisión

Selección de variables relevantes

En esta etapa se filtran únicamente las variables necesarias para el análisis, optimizando la estructura de la base de datos y asegurando que se trabaje exclusivamente con información pertinente para la evaluación financiera. Es fundamental ser cuidadoso con los nombres de las columnas, ya que deben coincidir exactamente con los reconocidos por la función de descarga.

Las variables seleccionadas son:

  • ticker → Identificador o símbolo del activo financiero.
  • ref_date → Fecha de referencia de cada observación.
  • price_open → Precio de apertura del activo en la jornada.
  • price_high → Precio máximo alcanzado durante el día.
  • price_close → Precio de cierre de la jornada.
  • volume → Cantidad de acciones negociadas, indicador de liquidez.
  • price_adjusted → Precio ajustado por dividendos y splits, variable crucial para análisis de largo plazo.
  • ret_adjusted_prices → Retorno diario calculado con base en el precio ajustado.
  • ret_closing_prices → Retorno diario basado en el precio de cierre.
  • cumret_adjusted_prices → Retorno acumulado calculado sobre precios ajustados.
datos_finales <- datos_financieros %>%
  select(
    ticker, 
    ref_date, 
    price_open, 
    price_high, 
    price_close, 
    volume,                
    price_adjusted, 
    ret_adjusted_prices, 
    ret_closing_prices,    
    cumret_adjusted_prices
  )

datos_finales
## # A tibble: 27,994 × 10
##    ticker    ref_date   price_open price_high price_close volume price_adjusted
##    <chr>     <date>          <dbl>      <dbl>       <dbl>  <dbl>          <dbl>
##  1 BOGOTA.CL 2019-01-01     51887.     51887.      51887.      0         33350.
##  2 BOGOTA.CL 2019-01-02     50269.     53932.      50269.   1478         32467.
##  3 BOGOTA.CL 2019-01-03     53003.     53077.      53003.   2358         34233.
##  4 BOGOTA.CL 2019-01-04     52110.     54844.      52110.   7626         33656.
##  5 BOGOTA.CL 2019-01-07     52110.     52110.      52110.      0         33656.
##  6 BOGOTA.CL 2019-01-08     52538.     52538.      52538.  12387         33933.
##  7 BOGOTA.CL 2019-01-09     53932.     53932.      53932.   3540         34833.
##  8 BOGOTA.CL 2019-01-10     53932.     54825.      53932.   2222         34833.
##  9 BOGOTA.CL 2019-01-11     53932.     54769.      53932.   9576         34833.
## 10 BOGOTA.CL 2019-01-14     53932.     53932.      53932.   6291         34833.
## # ℹ 27,984 more rows
## # ℹ 3 more variables: ret_adjusted_prices <dbl>, ret_closing_prices <dbl>,
## #   cumret_adjusted_prices <dbl>

Transformación de datos

Como segundo paso del proceso ETL se transforman los datos para garantizar la consistencia estructural, facilitar la indexación de la base de datos y asegurar la compatibilidad técnica con los modelos de análisis financiero.

Siguiendo las buenas prácticas de bases de datos argumentadas por las formas normales, se genera una Llave Primaria Sintética combinando el Ticker y la Fecha. Esto garantiza que cada registro sea único e identificable.

datos_finales <- datos_finales %>%
  mutate(
    # Llave: Ticker + Fecha formateada (Ej: ECOPETROL_20240101)
    llave_id = paste0(ticker, "_", format(ref_date, "%Y%m%d"))
  ) %>%
  select(llave_id, everything())

# Validamos que sea una llave primaria (Valores únicos = Número de registros)
n_distinct(datos_finales$llave_id) == nrow(datos_finales)
## [1] TRUE
datos_finales
## # A tibble: 27,994 × 11
##    llave_id           ticker ref_date   price_open price_high price_close volume
##    <chr>              <chr>  <date>          <dbl>      <dbl>       <dbl>  <dbl>
##  1 BOGOTA.CL_20190101 BOGOT… 2019-01-01     51887.     51887.      51887.      0
##  2 BOGOTA.CL_20190102 BOGOT… 2019-01-02     50269.     53932.      50269.   1478
##  3 BOGOTA.CL_20190103 BOGOT… 2019-01-03     53003.     53077.      53003.   2358
##  4 BOGOTA.CL_20190104 BOGOT… 2019-01-04     52110.     54844.      52110.   7626
##  5 BOGOTA.CL_20190107 BOGOT… 2019-01-07     52110.     52110.      52110.      0
##  6 BOGOTA.CL_20190108 BOGOT… 2019-01-08     52538.     52538.      52538.  12387
##  7 BOGOTA.CL_20190109 BOGOT… 2019-01-09     53932.     53932.      53932.   3540
##  8 BOGOTA.CL_20190110 BOGOT… 2019-01-10     53932.     54825.      53932.   2222
##  9 BOGOTA.CL_20190111 BOGOT… 2019-01-11     53932.     54769.      53932.   9576
## 10 BOGOTA.CL_20190114 BOGOT… 2019-01-14     53932.     53932.      53932.   6291
## # ℹ 27,984 more rows
## # ℹ 4 more variables: price_adjusted <dbl>, ret_adjusted_prices <dbl>,
## #   ret_closing_prices <dbl>, cumret_adjusted_prices <dbl>

Una forma de verificar la correcta creación de la llave es comparar si el número de registros únicos de la llave es igual al número de registros del dataset. En el siguiente fragmento, si el valor es TRUE significa que la llave sintética efectivamente toma el papel de ID.

# Validamos que sea una llave primaria (Valores únicos = Número de registros)
n_distinct(datos_finales$llave_id) == nrow(datos_finales)
## [1] TRUE

A continuación, se realiza un proceso de limpieza y estandarización de tipos de datos. Se asegura que la variable de fecha sea de tipo Date, mientras que todas las variables de precios, volumen y retornos se convierten explícitamente a formato numérico. Este paso es crítico, ya que un tipo de dato incorrecto puede generar errores en cálculos estadísticos, visualizaciones o modelos econométricos.

datos_finales <- datos_finales %>%
  mutate(
    # 1. Asegurar que la fecha sea objeto tipo Date
    ref_date = as.Date(ref_date),
    
    # 2. Convertir columnas de precios y retornos a numérico
    # (Usamos as.numeric para forzar por si vienen como caracteres)
    price_open = as.numeric(price_open),
    price_high = as.numeric(price_high),
    price_close = as.numeric(price_close),
    price_adjusted = as.numeric(price_adjusted),
    volume = as.numeric(volume),
    
    # 3. Asegurar que los retornos sean numéricos (muy importante para EDA)
    ret_adjusted_prices = as.numeric(ret_adjusted_prices),
    ret_closing_prices = as.numeric(ret_closing_prices),
    cumret_adjusted_prices = as.numeric(cumret_adjusted_prices))

datos_finales
## # A tibble: 27,994 × 11
##    llave_id           ticker ref_date   price_open price_high price_close volume
##    <chr>              <chr>  <date>          <dbl>      <dbl>       <dbl>  <dbl>
##  1 BOGOTA.CL_20190101 BOGOT… 2019-01-01     51887.     51887.      51887.      0
##  2 BOGOTA.CL_20190102 BOGOT… 2019-01-02     50269.     53932.      50269.   1478
##  3 BOGOTA.CL_20190103 BOGOT… 2019-01-03     53003.     53077.      53003.   2358
##  4 BOGOTA.CL_20190104 BOGOT… 2019-01-04     52110.     54844.      52110.   7626
##  5 BOGOTA.CL_20190107 BOGOT… 2019-01-07     52110.     52110.      52110.      0
##  6 BOGOTA.CL_20190108 BOGOT… 2019-01-08     52538.     52538.      52538.  12387
##  7 BOGOTA.CL_20190109 BOGOT… 2019-01-09     53932.     53932.      53932.   3540
##  8 BOGOTA.CL_20190110 BOGOT… 2019-01-10     53932.     54825.      53932.   2222
##  9 BOGOTA.CL_20190111 BOGOT… 2019-01-11     53932.     54769.      53932.   9576
## 10 BOGOTA.CL_20190114 BOGOT… 2019-01-14     53932.     53932.      53932.   6291
## # ℹ 27,984 more rows
## # ℹ 4 more variables: price_adjusted <dbl>, ret_adjusted_prices <dbl>,
## #   ret_closing_prices <dbl>, cumret_adjusted_prices <dbl>

Posteriormente, se efectúa un análisis de valores faltantes (NA). Los valores NA deben ser gestionados cuidadosamente, ya que pueden generar inconsistencias en cálculos posteriores. En el caso específico de los retornos financieros, es esperable que el primer registro de cada activo contenga un valor NA, dado que el cálculo del retorno requiere un valor del período inmediatamente anterior. Por lo tanto, el número de valores faltantes iniciales debería coincidir con la cantidad de activos analizados, lo cual constituye una validación lógica del proceso.

resumen_nas <- datos_finales %>%
  summarise(across(everything(), ~ sum(is.na(.)))) %>%
  pivot_longer(cols = everything(), names_to = "Variable", values_to = "Cantidad_NAs") %>%
  mutate(Porcentaje = (Cantidad_NAs / nrow(datos_finales)) * 100)

print(resumen_nas)
## # A tibble: 11 × 3
##    Variable               Cantidad_NAs Porcentaje
##    <chr>                         <int>      <dbl>
##  1 llave_id                          0     0     
##  2 ticker                            0     0     
##  3 ref_date                          0     0     
##  4 price_open                        0     0     
##  5 price_high                        0     0     
##  6 price_close                       0     0     
##  7 volume                            0     0     
##  8 price_adjusted                    0     0     
##  9 ret_adjusted_prices              18     0.0643
## 10 ret_closing_prices               18     0.0643
## 11 cumret_adjusted_prices            0     0

Para garantizar que los modelos de volatilidad y rendimientos acumulados funcionen correctamente, se procede a la eliminación de registros NA. Se prioriza la limpieza sobre las variables ret_adjusted_prices y price_close, ya que son los pilares de la fase de modelado.

datos_finales_clean <- datos_finales %>%
  drop_na(ret_adjusted_prices, price_close)

Cargue de datos

La fase final del proceso ETL consiste en consolidar la información transformada en una estructura de datos definitiva. Esta “Base de Datos de Trabajo” representa el insumo refinado que garantiza la integridad de los resultados en las fases posteriores de Análisis Exploratorio (EDA) y Optimización de Portafolios.

BD <- datos_finales_clean

glimpse(BD)
## Rows: 27,976
## Columns: 11
## $ llave_id               <chr> "BOGOTA.CL_20190102", "BOGOTA.CL_20190103", "BO…
## $ ticker                 <chr> "BOGOTA.CL", "BOGOTA.CL", "BOGOTA.CL", "BOGOTA.…
## $ ref_date               <date> 2019-01-02, 2019-01-03, 2019-01-04, 2019-01-07…
## $ price_open             <dbl> 50268.70, 53002.51, 52109.83, 52109.83, 52537.5…
## $ price_high             <dbl> 53932.38, 53076.90, 54843.65, 52109.83, 52537.5…
## $ price_close            <dbl> 50268.70, 53002.51, 52109.83, 52109.83, 52537.5…
## $ volume                 <dbl> 1478, 2358, 7626, 0, 12387, 3540, 2222, 9576, 6…
## $ price_adjusted         <dbl> 32467.20, 34232.91, 33656.34, 33656.34, 33932.6…
## $ ret_adjusted_prices    <dbl> -0.0268288530, 0.0529570276, -0.0169860248, 0.0…
## $ ret_closing_prices     <dbl> -0.0316792789, 0.0529567059, -0.0169855841, 0.0…
## $ cumret_adjusted_prices <dbl> 0.9731711, 1.0261282, 1.0091421, 1.0091421, 1.0…

Conclusión del Proceso ETL

La correcta ejecución de las fases de Extracción, Transformación y Carga permite concluir que los datos se encuentran en un estado óptimo para el análisis avanzado. Es importante resaltar que, aunque los precios nominales están denominados en Pesos Colombianos (COP), la estandarización hacia retornos logarítmicos elimina las disparidades de escala entre activos, permitiendo comparaciones directas de volatilidad y rentabilidad.

Con la creación de la base de datos BD, se ha mitigado el riesgo de errores por valores nulos o tipos de datos inconsistentes, asegurando que cualquier hallazgo estadístico posterior sea reflejo fiel de la dinámica del mercado financiero colombiano y no un artefacto de la calidad de la información.