El Banco Central del Paraguay (BCP) publica diariamente las cotizaciones referenciales del dólar americano (USD), real brasileño (BRL) y peso argentino (ARS), entre otras divisas. Este documento muestra cómo automatizar la descarga histórica, limpiar y consolidar los datos para el período 2018‑2025 y luego proyectar los próximos 90 días utilizando un modelo Holt‑Winters con tendencia amortiguada. Finalmente, se presenta una figura interactiva que permite explorar las series históricas y los pronósticos.
library(httr) # manejo de sesiones y POST/GET
library(rvest) # parseo HTML
library(xml2) # backend XML
library(dplyr) # manipulación de datos
library(tidyr) # pivot_longer / complete
library(lubridate) # fechas
library(readr) # lectura y escritura CSV
library(forecast) # HoltWinters + forecast
library(plotly) # figura interactiva
library(purrr) # set_names, map_dfr
library(stringr) # regex util
Las cotizaciones se extraen del formulario público del BCP (https://www.bcp.gov.py/webapps/web/cotizacion/monedas-historica).
Para cada año (anho
) y moneda (moneda
) se
envía una solicitud POST junto con los tokens
ocultos generados por la página ASP.NET.
base_url <- "https://www.bcp.gov.py/webapps/web/cotizacion/monedas-historica"
output_dir <- "G:/Mi unidad/BCP_COTIZACIONES"
years <- 2018:2025
currencies <- c("USD", "BRL", "ARS")
dir.create(output_dir, showWarnings = FALSE)
# Sesión inicial
session <- session(base_url)
form <- session %>% html_node("form#form-fecha")
hidden_vals <- form %>% html_nodes("input[type=hidden]") %>%
{purrr::set_names(html_attr(., "value"), html_attr(., "name"))}
# Descarga de un (año, moneda)
download_year_currency <- function(year, curr) {
csv_path <- file.path(output_dir, sprintf("cotizacion_%s_%d.csv", curr, year))
if (file.exists(csv_path)) return(invisible())
res <- session %>% session_submit(
form,
body = c(hidden_vals, anho = as.character(year), moneda = curr),
submit = "CONSULTAR"
)
# refrescar tokens para el siguiente envío
hidden_vals <<- res %>% html_nodes("form#form-fecha input[type=hidden]") %>%
{purrr::set_names(html_attr(., "value"), html_attr(., "name"))}
tbls <- res %>% html_nodes("table#cotizacion-interbancaria")
if (length(tbls) < 2) return(invisible())
tbls[[2]] %>% html_table(fill = TRUE) %>% write_csv(csv_path)
}
# Bucle de descarga
for (y in years) for (c in currencies) download_year_currency(y, c)
csv_files <- list.files(output_dir, pattern = "cotizacion_.*\\.csv$", full.names = TRUE)
read_clean <- function(path) {
meta <- stringr::str_match(basename(path), "cotizacion_(.*)_([0-9]{4})\\.csv")
tibble_arch <- read_csv(path, show_col_types = FALSE) %>%
rename_with(~ gsub("^#", "DIA", .x)) %>%
mutate(ANHO = as.integer(meta[3]),
MONEDA = meta[2])
tibble_arch
}
df_all <- map_dfr(csv_files, read_clean) %>%
pivot_longer(cols = ENE:DIC, names_to = "MES_ABR", values_to = "COTIZACION") %>%
mutate(
MES = recode(MES_ABR,
"ENE" = 1, "FEB" = 2, "MAR" = 3, "ABR" = 4, "MAY" = 5, "JUN" = 6,
"JUL" = 7, "AGO" = 8, "SEP" = 9, "OCT" = 10, "NOV" = 11, "DIC" = 12,
.default = NA_real_),
FECHA = ymd(sprintf("%s-%02d-%02d", ANHO, MES, DIA)),
COTIZACION = parse_number(COTIZACION, locale = locale(decimal_mark = ","))
) %>%
filter(!is.na(FECHA) & !is.na(COTIZACION))
## Warning: There were 2 warnings in `mutate()`.
## The first warning was:
## ℹ In argument: `FECHA = ymd(sprintf("%s-%02d-%02d", ANHO, MES, DIA))`.
## Caused by warning:
## ! 488 failed to parse.
## ℹ Run `dplyr::last_dplyr_warnings()` to see the 1 remaining warning.
forecast_horizon <- 15
fc_list <- list()
for (curr in unique(df_all$MONEDA)) {
ts_full <- df_all %>% filter(MONEDA == curr) %>%
arrange(FECHA) %>%
complete(FECHA = seq(min(FECHA), max(FECHA), by = "day")) %>%
fill(COTIZACION)
last_date <- max(ts_full$FECHA)
train <- ts_full %>% filter(FECHA >= last_date - years(1)) %>% pull(COTIZACION)
names(train) <- seq.Date(last_date - years(1), last_date, by = "day")
hw_fit <- HoltWinters(train, beta = TRUE, gamma = FALSE)
fc <- forecast(hw_fit, h = forecast_horizon)
fc_list[[curr]] <- tibble(
FECHA = seq(last_date + days(1), by = "day", length.out = forecast_horizon),
COTIZAPROY = as.numeric(fc$mean),
MONEDA = curr
) %>% mutate(
ANHO = year(FECHA), MES = month(FECHA), DIA = day(FECHA), COTIZACION = NA_real_
)
}
df_proj <- bind_rows(fc_list)
df_final <- bind_rows(
df_all %>% mutate(COTIZAPROY = NA_real_),
df_proj
) %>%
arrange(MONEDA, FECHA) %>%
group_by(MONEDA) %>%
mutate(
last_obs = max(FECHA[!is.na(COTIZACION)]),
FLAG = if_else(FECHA >= last_obs - years(1) | !is.na(COTIZAPROY), 1, 0)
) %>%
ungroup()
df_final
## # A tibble: 17,492 × 10
## DIA ANHO MONEDA MES_ABR COTIZACION MES FECHA COTIZAPROY last_obs
## <dbl> <dbl> <chr> <chr> <dbl> <dbl> <date> <dbl> <date>
## 1 2 2006 ARS ENE 2012. 1 2006-01-02 NA 2025-05-12
## 2 3 2006 ARS ENE 2020. 1 2006-01-03 NA 2025-05-12
## 3 4 2006 ARS ENE 2016. 1 2006-01-04 NA 2025-05-12
## 4 5 2006 ARS ENE 2018. 1 2006-01-05 NA 2025-05-12
## 5 6 2006 ARS ENE 2018. 1 2006-01-06 NA 2025-05-12
## 6 9 2006 ARS ENE 2011. 1 2006-01-09 NA 2025-05-12
## 7 10 2006 ARS ENE 2002. 1 2006-01-10 NA 2025-05-12
## 8 11 2006 ARS ENE 2002. 1 2006-01-11 NA 2025-05-12
## 9 12 2006 ARS ENE 2008. 1 2006-01-12 NA 2025-05-12
## 10 13 2006 ARS ENE 2015. 1 2006-01-13 NA 2025-05-12
## # ℹ 17,482 more rows
## # ℹ 1 more variable: FLAG <dbl>
La figura siguiente permite comparar el histórico del último año con las proyecciones para los próximos 90 días. Selecciona/deselecciona cada moneda en la leyenda.