Análisis Dinámico de Acciones del Índice Financiero de la Bolsa de Valores de Caracas: 2023-2025

Autor/a

Javier Melendez

Fecha de publicación

12 de febrero de 2025


Resumen

Este proyecto analizará el comportamiento de las acciones del índice financiero de la Bolsa de Valores de Caracas (BVC) entre enero de 2023 y febrero de 2024. El objetivo es ofrecer una herramienta útil para inversionistas y entender mejor las dinámicas recientes del mercado de valores venezolano

Introducción

El mercado de valores venezolano ha experimentado fluctuaciones significativas en los últimos años, influenciadas por factores macroeconómicos y eventos globales. Este proyecto se enfoca en el análisis del comportamiento de las acciones del índice financiero de la BVC durante un período específico(2023-01 y 2025-02) excluyendo el impacto directo de la pandemia de COVID-19 y la recuperación posterior (2021), para centrarse en la dinámica más reciente del mercado

Plantiamiento del problema

¿Cuáles son las tendencias y patrones en el comportamiento de las acciones del índice financiero de la BVC durante el período comprendido entre el 2 de enero de 2023 y el 5 de febrero de 2025?

Objetivo general

Analizar el comportamiento de las acciones del índice financiero de la BVCaracas durante el período 2023-2024 y desarrollar un modelo ARIMA para predecir su valor futuro a corto plazo.

Objetivos específicos

1. Recopilar y procesar datos históricos del índice financiero de la BVCaracas y de las acciones que lo componen.

2. Identificar tendencias, patrones y relaciones entre las variables relevantes utilizando técnicas de análisis estadístico y econométrico.

Metodologia

Recoleccion de datos: Para obtener los datos

Las acciones analizadas son: BPV, BNC, BVCC, ABC.A, MVZ.A, MVZ.B.

1. Librerías Necesarias

Código
library(httr)
library(jsonlite)
library(lubridate)

Adjuntando el paquete: 'lubridate'
The following objects are masked from 'package:base':

    date, intersect, setdiff, union
Código
library(dplyr)

Adjuntando el paquete: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
Código
library(purrr)

Adjuntando el paquete: 'purrr'
The following object is masked from 'package:jsonlite':

    flatten
Código
library(DT)

Extraccion datos desnudos de la BVC

La siguiente función se encarga de realizar una solicitud POST a la Bolsa de Valores de Caracas y extraer los datos históricos correspondientes a cada acción. Se incluyen controles de error y mensajes informativo

Código
obtener_datos_desnudos <- function(simbolo) {
  tryCatch({
    # Configurar solicitud HTTP
    headers <- add_headers(
      "User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
      "Referer" = "https://www.bolsadecaracas.com/historicos/"
    )
    
    # Realizar solicitud POST
    response <- POST(
      "https://www.bolsadecaracas.com/wp-admin/admin-ajax.php",
      headers,
      body = list(action = "getHistoricoSimbolo", simbolo = simbolo),
      encode = "form",
      timeout(15)
    )
    
    # Verificar que la solicitud fue exitosa
    if (status_code(response) != 200) {
      message("Error en la respuesta para ", simbolo, ": ", status_code(response))
      return(NULL)
    }
    
    # Procesar JSON y devolver el contenido completo
    contenido <- content(response, "text", encoding = "UTF-8")
    datos_json <- fromJSON(contenido)
    return(datos_json)
    
  }, error = function(e) {
    message("Error procesando ", simbolo, ": ", e$message)
    return(NULL)
  })
}

Definimos la lista de símbolos a analizar y ejecutamos la extracción de datos para cada uno, almacenando los resultados en una lista. Se introduce una pausa entre solicitudes para evitar saturar el servidor.

Código
# Lista de símbolos
simbolos <- c("BPV", "BNC", "BVCC", "ABC.A", "MVZ.A", "MVZ.B")

# Ejecución principal: recoger todos los datos sin limpieza
datos_finales <- list()

for (simbolo in simbolos) {
  message("Procesando: ", simbolo)
  datos <- obtener_datos_desnudos(simbolo)
  
  if (!is.null(datos)) {
    datos_finales[[simbolo]] <- datos
    Sys.sleep(1.5)  # Pausa para evitar saturar el servidor
  }
}
Procesando: BPV
Procesando: BNC
Procesando: BVCC
Procesando: ABC.A
Procesando: MVZ.A
Procesando: MVZ.B

Optimización y Limpieza del DataFrame ####En esta sección se combinan los datos extraídos, se ajustan los nombres de las columnas y se realiza la limpieza de datos:

Se convierte la columna de fechas al formato Date (usando lubridate::as_date). Se agrega la columna ACCION para identificar cada símbolo. Se limpian las columnas numéricas, sustituyendo comas por puntos para su correcta conversión a valores numéricos.

Código
datos_totales <- map_df(names(datos_finales), function(simbolo) {
  # Extraer el componente relevante
  df <- datos_finales[[simbolo]]$cur_hist_mov_emisora
  if (is.null(df)) {
    return(data.frame(ACCION = simbolo))
  }
  
  df <- as.data.frame(df)
  
  # Renombrar columnas según el formato requerido
  colnames(df) <- c("FECHA", "PRECIO_APERT", "PRECIO_CIE", "VAR_ABS", "VAR_REL", 
                    "PRECIO_MAX", "PRECIO_MIN", "N_OPERACIONES", 
                    "TITULOS_NEGOCIADOS", "MONTO_EFECTIVO")[1:ncol(df)]
  
  # Convertir la columna FECHA al formato Date (formato original: 'dd-mm-yy')
  df$FECHA <- as_date(df$FECHA, format = '%d-%m-%y')
  
  # Agregar la columna ACCION
  df$ACCION <- simbolo
  
  return(df)
}) %>%
  # Limpiar columnas numéricas: reemplazar puntos y comas para que sean interpretadas como números
  mutate(across(
    c(PRECIO_APERT, PRECIO_CIE, VAR_ABS, VAR_REL, 
      PRECIO_MAX, PRECIO_MIN, N_OPERACIONES, 
      TITULOS_NEGOCIADOS, MONTO_EFECTIVO),
    ~ as.numeric(gsub(",", ".", gsub("\\.", "", .)))
  ))

# Verificar la estructura final del DataFrame
str(datos_totales)
'data.frame':   6564 obs. of  11 variables:
 $ FECHA             : Date, format: "2025-02-11" "2025-02-10" ...
 $ PRECIO_APERT      : num  3.8 3.9 3.9 4.35 4 3.55 3.52 5.4 5.25 5.27 ...
 $ PRECIO_CIE        : num  3.85 3.8 3.9 3.9 4.35 4 3.55 5.55 5.4 5.25 ...
 $ VAR_ABS           : num  0.05 -0.1 0 -0.45 0.35 0.45 0.03 0.15 0.15 -0.02 ...
 $ VAR_REL           : num  1.3 -2.63 0 -11.54 8.05 ...
 $ PRECIO_MAX        : num  3.88 3.89 4 4.34 4.35 4 3.75 5.6 5.49 5.3 ...
 $ PRECIO_MIN        : num  3.8 3.79 3.8 3.9 4.05 3.55 3.35 5.4 5.3 5.25 ...
 $ N_OPERACIONES     : num  60 54 59 39 66 84 53 82 59 39 ...
 $ TITULOS_NEGOCIADOS: num  54700 18586 36261 11980 31491 ...
 $ MONTO_EFECTIVO    : num  208731 70787 141295 49895 133725 ...
 $ ACCION            : chr  "BPV" "BPV" "BPV" "BPV" ...

Visualisacion de tabla de datos_totales

Código
datatable(datos_totales,
          rownames = FALSE,
          # No incluimos la extensión 'Buttons' ni configuramos botones de descarga
          options = list(
            dom = 'frtip',  # 'f' = filtro, 'r' = procesamiento, 't' = tabla, 'i' = info, 'p' = paginación
            lengthChange = FALSE,
            language = list(url = '//cdn.datatables.net/plug-ins/1.10.11/i18n/Spanish.json'),
            pageLength = 5,
            initComplete = JS(
              "function(settings, json) {",
              "$(this.api().table().header()).css({'background-color': '#224eb2','color': '#fff'});",
              "}"
            ),
            Filter = 0
          ),
          escape = FALSE
)# %>% 
Código
 # formatStyle(columns = colnames(.), fontSize = '50%')

Para realizar un análisis más enfocado, se filtran los datos para el período comprendido entre enero 2023 y febrero 2025.

Código
# Definir el rango de fechas
inicio <- as.Date("2023-01-01")
fin    <- as.Date("2025-02-11")  # Incluye febrero 2025

# Filtrar el DataFrame para el rango deseado
datos_filtrados <- datos_totales %>% 
  filter(FECHA >= inicio & FECHA <= fin)

# Visualizar las primeras filas del conjunto filtrado
head(datos_filtrados)
       FECHA PRECIO_APERT PRECIO_CIE VAR_ABS VAR_REL PRECIO_MAX PRECIO_MIN
1 2025-02-11         3.80       3.85    0.05    1.30       3.88       3.80
2 2025-02-10         3.90       3.80   -0.10   -2.63       3.89       3.79
3 2025-02-07         3.90       3.90    0.00    0.00       4.00       3.80
4 2025-02-06         4.35       3.90   -0.45  -11.54       4.34       3.90
5 2025-02-05         4.00       4.35    0.35    8.05       4.35       4.05
6 2025-02-04         3.55       4.00    0.45   11.25       4.00       3.55
  N_OPERACIONES TITULOS_NEGOCIADOS MONTO_EFECTIVO ACCION
1            60              54700      208731.09    BPV
2            54              18586       70787.24    BPV
3            59              36261      141295.01    BPV
4            39              11980       49895.35    BPV
5            66              31491      133724.62    BPV
6            84              67060      261178.24    BPV

tabla de datos_filtrados

Código
datatable(datos_filtrados,
          rownames = FALSE,
          #
          options = list(
            dom = 'frtip',  # 'f' = filtro, 'r' = procesamiento, 't' = tabla, 'i' = info, 'p' = paginación
            lengthChange = FALSE,
            language = list(url = '//cdn.datatables.net/plug-ins/1.10.11/i18n/Spanish.json'),
            pageLength = 5,
            initComplete = JS(
              "function(settings, json) {",
              "$(this.api().table().header()).css({'background-color': '#224eb2','color': '#fff'});",
              "}"
            ),
            Filter = 0
          ),
          escape = FALSE
) %>% 
  formatStyle(columns = colnames(.), fontSize = '50%')

Acontinuacion se presente dos tipos de visualizaciones:

Gráficas independientes por acción: Cada acción se analiza de forma individual para apreciar su evolución en el tiempo.

Gráfica combinada: Se muestra la evolución del precio de cierre para todas las acciones en un mismo gráfico (usando facets), lo que permite compararlas manteniendo una escala adecuada para cada una y asegurando que el precio se lea bien.

Donde la data frame datos_filtrados que contiene, entre otras columnas, las variables:
FECHA (de tipo Date)
PRECIO_CIE (precio de cierre)
ACCION (símbolo de la acción)

1. Gráficas Independientes por Acción

Código
# Cargar librerías necesarias
library(plotly)
Cargando paquete requerido: ggplot2

Adjuntando el paquete: 'plotly'
The following object is masked from 'package:ggplot2':

    last_plot
The following object is masked from 'package:httr':

    config
The following object is masked from 'package:stats':

    filter
The following object is masked from 'package:graphics':

    layout
Código
library(dplyr)
library(scales)

Adjuntando el paquete: 'scales'
The following object is masked from 'package:purrr':

    discard
Código
# Lista única de acciones
acciones <- unique(datos_filtrados$ACCION)

# Crear una lista para almacenar la información de cada traza
trazas <- list()
for (accion in acciones) {
  datos_accion <- datos_filtrados %>% filter(ACCION == accion)
  
  # Definir el texto a mostrar en el hover (tooltip)
  hover_text <- paste("Fecha:", datos_accion$FECHA,
                      "<br>Precio Cierre:", dollar(datos_accion$PRECIO_CIE, prefix = "bs"),
                      "<br>N_OPERACIONES:", datos_accion$N_OPERACIONES,
                      "<br>TITULOS_NEGOCIADOS:", datos_accion$TITULOS_NEGOCIADOS,
                      "<br>MONTO_EFECTIVO:", datos_accion$MONTO_EFECTIVO)
  
  trazas[[accion]] <- list(
    x = datos_accion$FECHA,
    y = datos_accion$PRECIO_CIE,
    type = 'scatter',
    mode = 'lines',
    name = accion,
    line = list(width = 1.5),
    text = hover_text,
    hoverinfo = "text"  # Se indica que se usará el texto personalizado en el hover
  )
}

# Definir la visibilidad inicial: solo se muestra la primera acción
visibilidad_inicial <- sapply(acciones, function(x) x == acciones[1])

# Crear los botones del menú superior
botones <- lapply(seq_along(acciones), function(i) {
  # Vector de visibilidad: solo la traza i es visible
  vis <- rep(FALSE, length(acciones))
  vis[i] <- TRUE
  list(
    method = "update",
    args = list(
      list(visible = vis),  # Actualiza la visibilidad de las trazas
      list(title = paste("Evolución del Precio de Cierre de", acciones[i]))
    ),
    label = acciones[i]
  )
})

# Construir el gráfico
fig <- plot_ly()
for (i in seq_along(acciones)) {
  accion <- acciones[i]
  fig <- fig %>% add_trace(
    x = trazas[[accion]]$x,
    y = trazas[[accion]]$y,
    type = trazas[[accion]]$type,
    mode = trazas[[accion]]$mode,
    name = trazas[[accion]]$name,
    line = trazas[[accion]]$line,
    text = trazas[[accion]]$text,
    hoverinfo = trazas[[accion]]$hoverinfo,
    visible = visibilidad_inicial[i]
  )
}

# Configurar el layout del gráfico (incluye menú de actualización y rangos)
fig <- fig %>% layout(
  title = paste("Evolución del Precio de Cierre de", acciones[1]),
  updatemenus = list(
    list(
      type = "buttons",
      direction = "right",
      x = 0.5,  # Ajusta la posición horizontal según convenga
      y = 1.15, # Ajusta la posición vertical (por encima del gráfico)
      showactive = TRUE,
      buttons = botones
    )
  ),
  xaxis = list(
    title = "Fecha",
    rangeselector = list(
      buttons = list(
        list(count = 7, label = "1 Semana", step = "day", stepmode = "backward"),
        list(count = 1, label = "1 Mes", step = "month", stepmode = "backward"),
        list(count = 6, label = "6 Meses", step = "month", stepmode = "backward"),
        list(count = 1, label = "1 Año", step = "year", stepmode = "backward"),
        list(step = "all", label = "Todo")
      )
    ),
    rangeslider = list(visible = TRUE)
  ),
  yaxis = list(title = "Precio de Cierre (Bs)")
)

# Mostrar el gráfico
fig

####. Gráfica Combinada: En este gráfico se agrupa la evolución de todas las acciones. Se utiliza facet_wrap para crear paneles independientes para cada acción, permitiendo que la escala y se ajuste de manera individual para que el precio se lea de forma óptima.

Código
# Cargar librerías necesarias
library(plotly)
library(dplyr)
library(scales)

# Supongamos que 'datos_filtrados' es tu data frame con las columnas mencionadas.

# Crear el gráfico compartido
fig <- plot_ly()

for (accion in unique(datos_filtrados$ACCION)) {
  datos_accion <- datos_filtrados %>% filter(ACCION == accion)
  
  # Crear el texto para el tooltip
  hover_text <- paste("Fecha:", datos_accion$FECHA,
                      "<br>Precio Cierre:", dollar(datos_accion$PRECIO_CIE, prefix = "$"),
                      "<br>N_OPERACIONES:", datos_accion$N_OPERACIONES,
                      "<br>TITULOS_NEGOCIADOS:", datos_accion$TITULOS_NEGOCIADOS,
                      "<br>MONTO_EFECTIVO:", datos_accion$MONTO_EFECTIVO)
  
  fig <- fig %>% add_trace(
    x = datos_accion$FECHA,
    y = datos_accion$PRECIO_CIE,
    type = 'scatter',
    mode = 'lines',
    name = accion,
    line = list(width = 2),
    text = hover_text,
    hoverinfo = "text"
  )
}

# Configurar el layout del gráfico
fig <- fig %>% layout(
  title = "Evolución del Precio de Cierre - Acciones del Índice Financiero de Caracas",
  xaxis = list(
    title = "Fecha",
    rangeselector = list(
      buttons = list(
        list(count = 7, label = "1 Semana", step = "day", stepmode = "backward"),
        list(count = 1, label = "1 Mes", step = "month", stepmode = "backward"),
        list(count = 6, label = "6 Meses", step = "month", stepmode = "backward"),
        list(count = 1, label = "1 Año", step = "year", stepmode = "backward"),
        list(step = "all", label = "Todo")
      )
    ),
    rangeslider = list(visible = TRUE)
  ),
  yaxis = list(title = "Precio de Cierre (Bs)"),
  legend = list(
    orientation = 'h',  # Leyenda horizontal
    x = 0.5,
    xanchor = 'center',
    y = 1.15
  )
)

# Mostrar el gráfico
fig

Analis de volumen de accion por Año

Código
library(dplyr)
library(lubridate)

# Agregar la columna 'Año' a partir de 'FECHA'
datos_filtrados <- datos_filtrados %>%
  mutate(Año = year(FECHA))

# Resumen anual: total negociado por cada acción en cada año
volumen_anual <- datos_filtrados %>%
  group_by(ACCION, Año) %>%
  summarise(total_vol = sum(TITULOS_NEGOCIADOS, na.rm = TRUE)) %>%
  ungroup()
`summarise()` has grouped output by 'ACCION'. You can override using the
`.groups` argument.
Código
accion_favorita_anual <- volumen_anual %>%
  group_by(Año) %>%
  filter(total_vol == max(total_vol)) %>%  # Selecciona la acción con el mayor volumen en cada año
  arrange(Año) %>%
  ungroup()

# Mostrar la acción favorita por año
print(accion_favorita_anual)
# A tibble: 3 × 3
  ACCION   Año total_vol
  <chr>  <dbl>     <dbl>
1 BNC     2023 480023500
2 BNC     2024 184172700
3 BNC     2025  11071104

Analisis grafico

Código
library(plotly)

fig_bar <- plot_ly(volumen_anual, 
                   x = ~Año, 
                   y = ~total_vol, 
                   color = ~ACCION, 
                   type = 'bar') %>%
  layout(title = "Comparativa del Volumen Negociado por Año y Acción",
         xaxis = list(title = "Año"),
         yaxis = list(title = "Total de Títulos Negociados"),
         barmode = 'group',
         legend = list(title = list(text = "<b>Acción</b>")))
fig_bar

Cálculo de Indicadores de Desempeño Con el paquete PerformanceAnalytics podemos calcular métricas importantes. Para cada acción creamos un objeto xts a partir del rendimiento diario y luego calculamos:

Rendimiento Anualizado: El promedio geométrico anualizado.

Volatilidad Anualizada: La desviación estándar anualizada.

Máximo Drawdown: La mayor caída desde un pico hasta un mínimo durante el período.

Código
library(PerformanceAnalytics)
Cargando paquete requerido: xts
Cargando paquete requerido: zoo

Adjuntando el paquete: 'zoo'
The following objects are masked from 'package:base':

    as.Date, as.Date.numeric

######################### Warning from 'xts' package ##########################
#                                                                             #
# The dplyr lag() function breaks how base R's lag() function is supposed to  #
# work, which breaks lag(my_xts). Calls to lag(my_xts) that you type or       #
# source() into this session won't work correctly.                            #
#                                                                             #
# Use stats::lag() to make sure you're not using dplyr::lag(), or you can add #
# conflictRules('dplyr', exclude = 'lag') to your .Rprofile to stop           #
# dplyr from breaking base R's lag() function.                                #
#                                                                             #
# Code in packages is not affected. It's protected by R's namespace mechanism #
# Set `options(xts.warn_dplyr_breaks_lag = FALSE)` to suppress this warning.  #
#                                                                             #
###############################################################################

Adjuntando el paquete: 'xts'
The following objects are masked from 'package:dplyr':

    first, last

Adjuntando el paquete: 'PerformanceAnalytics'
The following object is masked from 'package:graphics':

    legend
Código
library(xts)
library(dplyr)

# Asegurar que FECHA es tipo Date
datos_filtrados <- datos_filtrados %>%
  mutate(FECHA = as.Date(FECHA))

# Calcular rendimientos diarios si no están en el dataset
datos_filtrados <- datos_filtrados %>%
  group_by(ACCION) %>%
  arrange(FECHA) %>%
  mutate(rend_diario = (PRECIO_CIE / lag(PRECIO_CIE)) - 1) %>%
  ungroup()

# Función para calcular métricas financieras
calcular_metricas <- function(df) {
  serie_xts <- xts(df$rend_diario, order.by = df$FECHA)

  # Si la serie está vacía o tiene solo un dato, evitar errores
  if (nrow(serie_xts) < 2) {
    return(data.frame(
      annualized_return = NA,
      annualized_volatility = NA,
      max_drawdown = NA
    ))
  }

  data.frame(
    annualized_return = as.numeric(Return.annualized(na.omit(serie_xts), scale = 252)),
    annualized_volatility = as.numeric(StdDev.annualized(na.omit(serie_xts), scale = 252)),
    max_drawdown = as.numeric(maxDrawdown(na.omit(serie_xts)))
  )
}

# Aplicar la función para cada acción usando group_modify()
performance_metrics <- datos_filtrados %>%
  group_by(ACCION) %>%
  group_modify(~ calcular_metricas(.)) %>%
  ungroup()

# Mostrar resultados
print(performance_metrics)
# A tibble: 6 × 4
  ACCION annualized_return annualized_volatility max_drawdown
  <chr>              <dbl>                 <dbl>        <dbl>
1 ABC.A              1.65                  0.855        0.597
2 BNC               20.3                 380.           0.978
3 BPV               -0.261                 0.903        0.846
4 BVCC               2.48                  0.853        0.412
5 MVZ.A              2.22                  0.652        0.428
6 MVZ.B              3.17                  0.753        0.322

Rendimineto acumulado

Código
library(plotly)

# Crear el gráfico interactivo del rendimiento acumulado
fig <- plot_ly()

# Obtener la lista de acciones
acciones <- unique(datos_filtrados$ACCION)

for (accion in acciones) {
  datos_accion <- datos_filtrados %>% filter(ACCION == accion)
  
  fig <- fig %>% add_trace(
    x = datos_accion$FECHA,
    y = datos_accion$rend_acumulado,
    type = 'scatter',
    mode = 'lines',
    name = accion,
    line = list(width = 2)
  )
}
Warning: Unknown or uninitialised column: `rend_acumulado`.
Unknown or uninitialised column: `rend_acumulado`.
Unknown or uninitialised column: `rend_acumulado`.
Unknown or uninitialised column: `rend_acumulado`.
Unknown or uninitialised column: `rend_acumulado`.
Unknown or uninitialised column: `rend_acumulado`.
Código
fig <- fig %>% layout(
  title = "Evolución del Rendimiento Acumulado de las Acciones",
  xaxis = list(title = "Fecha"),
  yaxis = list(title = "Rendimiento Acumulado"),
  legend = list(title = list(text = "<b>Acción</b>"))
)

fig