R/bcb.R

Defines functions bcb_error_body bcb_olinda bcb_ptax bcb parse_bcb_fx_rates bcb_odata_filter parse_bcb_selic bcb_selic parse_bcb_inflation bcb_inflation parse_bcb_top5 bcb_top5 parse_bcb_expectations bcb_expectations parse_bcb_currencies bcb_currencies bcb_fx_rates parse_bcb_data bcb_data

Documented in bcb_currencies bcb_data bcb_expectations bcb_fx_rates bcb_inflation bcb_selic bcb_top5

#' Fetch Banco Central do Brasil (BCB) data
#'
#' Retrieve time series data from the Banco Central do Brasil SGS (Sistema Gerenciador de Séries
#' Temporais) API.
#'
#' Daily series require a date range and the API limits the query window to at most 10 years; supply
#' `start_date` (and optionally `end_date`) when querying such series.
#'
#' @param series (`integer(1)`)\cr
#'   The SGS series code to query (e.g., `1` for the USD/BRL exchange rate). Series codes can be
#'   found on the SGS website.
#' @param start_date (`NULL` | `Date(1)` | `character(1)`)\cr
#'   Start date of the data (e.g., `"2024-01-01"`). If `NULL`, all available data is returned.
#'   Default `NULL`.
#' @param end_date (`NULL` | `Date(1)` | `character(1)`)\cr
#'   End date of the data, in the same format as start_date. If `NULL`, data up to the latest
#'   available date is returned. Default `NULL`.
#' @returns A [data.table::data.table()] with the requested data.
#' @source <https://dadosabertos.bcb.gov.br/>
#' @family data
#' @export
#' @examplesIf curl::has_internet()
#' \donttest{
#' # fetch USD/BRL exchange rate
#' bcb_data(1, start_date = "2024-01-01", end_date = "2024-01-31")
#'
#' # fetch the Selic target rate
#' bcb_data(432, start_date = "2024-01-01", end_date = "2024-01-31")
#' }
bcb_data = function(series, start_date = NULL, end_date = NULL) {
  series = assert_count(series, positive = TRUE, coerce = TRUE)
  start_date = assert_dateish(start_date, null.ok = TRUE)
  end_date = assert_dateish(end_date, null.ok = TRUE)

  json = bcb(
    series,
    dataInicial = start_date %&&% format(start_date, "%d/%m/%Y"),
    dataFinal = end_date %&&% format(end_date, "%d/%m/%Y")
  )
  parse_bcb_data(json, series)
}

parse_bcb_data = function(json, series) {
  if (length(json) == 0L) {
    dt = data.table(date = as.Date(character()), id = character(), value = numeric())
    setnames(dt, "id", "key")
    return(dt[])
  }
  data = valor = NULL
  dt = rbindlist(json, fill = TRUE)
  dt[, let(
    date = as.Date(data, "%d/%m/%Y"),
    key = as.character(series),
    value = as.numeric(valor)
  )]
  dt[, c("data", "valor") := NULL]
  setcolorder(dt, c("date", "key", "value"))
  dt[]
}

#' Fetch Banco Central do Brasil (BCB) exchange rates
#'
#' Retrieve PTAX foreign exchange reference rates from the Banco Central do Brasil Olinda API. The
#' rates are the closing (Fechamento) bid and ask quotations expressed in Brazilian real (BRL) per
#' unit of the foreign currency.
#'
#' Rates are published only on business days, so weekends and holidays return no rows.
#'
#' @param currency (`character()`)\cr
#'   One or more ISO 4217 currency codes (e.g. `"USD"`, `c("USD", "EUR")`). See [bcb_currencies()]
#'   for the available currencies.
#' @param start_date (`Date(1)` | `character(1)`)\cr
#'   Start date of the data (e.g., `"2024-01-01"`).
#' @param end_date (`NULL` | `Date(1)` | `character(1)`)\cr
#'   End date of the data, in the same format as start_date. If `NULL`, the `start_date` is used.
#'   Default `NULL`.
#' @returns A [data.table::data.table()] with columns `date`, `currency`, `bid`, and `ask`.
#' @source <https://dadosabertos.bcb.gov.br/>
#' @family data
#' @export
#' @examplesIf curl::has_internet()
#' \donttest{
#' # fetch USD/BRL closing rates
#' bcb_fx_rates("USD", start_date = "2024-01-01", end_date = "2024-01-31")
#'
#' # fetch multiple currencies
#' bcb_fx_rates(c("USD", "EUR"), start_date = "2024-01-01", end_date = "2024-01-31")
#' }
bcb_fx_rates = function(currency, start_date, end_date = NULL) {
  assert_character(currency, min.len = 1L, n.chars = 3L, any.missing = FALSE)
  start_date = assert_dateish(start_date)
  end_date = assert_dateish(end_date, null.ok = TRUE)
  end_date = end_date %||% start_date

  currency = toupper(currency)
  rbindlist(map(currency, function(cur) {
    json = bcb_ptax(cur, start_date, end_date)
    parse_bcb_fx_rates(json, cur)
  }))
}

#' Fetch Banco Central do Brasil (BCB) currencies
#'
#' Retrieve the list of currencies available from the Banco Central do Brasil PTAX API.
#'
#' @returns A [data.table::data.table()] with columns `code`, `name`, and `type`.
#' @source <https://dadosabertos.bcb.gov.br/>
#' @family metadata
#' @export
#' @examplesIf curl::has_internet()
#' \donttest{
#' bcb_currencies()
#' }
bcb_currencies = function() {
  json = bcb_olinda("PTAX", "Moedas")
  parse_bcb_currencies(json)
}

parse_bcb_currencies = function(json) {
  value = json$value
  data.table(
    code = map_chr(value, "simbolo"),
    name = map_chr(value, "nomeFormatado"),
    type = map_chr(value, "tipoMoeda")
  )
}

#' Fetch Banco Central do Brasil (BCB) market expectations
#'
#' Retrieve market expectations from the Banco Central do Brasil Focus survey (Relatório Focus) via
#' the Olinda API. Each row is the summary of survey responses collected on a given date for a future
#' reference period.
#'
#' Querying without a filter returns the full history, which is large; supplying `indicator` and/or a
#' date range is recommended.
#'
#' @param type (`character(1)`)\cr
#'   The forecast horizon. One of `"annual"`, `"monthly"`, or `"quarterly"`. Default `"annual"`.
#' @param indicator (`NULL` | `character(1)`)\cr
#'   The economic indicator to filter on (e.g. `"IPCA"`, `"Selic"`, `"Câmbio"`). If `NULL`, all
#'   indicators are returned. Default `NULL`.
#' @param start_date (`NULL` | `Date(1)` | `character(1)`)\cr
#'   Start of the survey date range (e.g., `"2024-01-01"`). If `NULL`, no lower bound is applied.
#'   Default `NULL`.
#' @param end_date (`NULL` | `Date(1)` | `character(1)`)\cr
#'   End of the survey date range, in the same format as start_date. If `NULL`, no upper bound is
#'   applied. Default `NULL`.
#' @returns A [data.table::data.table()] with columns `date`, `indicator`, `detail`, `reference`,
#'   `mean`, `median`, `sd`, `min`, `max`, `respondents`, and `base`.
#' @source <https://dadosabertos.bcb.gov.br/>
#' @family data
#' @export
#' @examplesIf curl::has_internet()
#' \donttest{
#' # annual IPCA inflation expectations
#' bcb_expectations("annual", "IPCA", start_date = "2024-01-01", end_date = "2024-01-31")
#' }
bcb_expectations = function(type = "annual", indicator = NULL, start_date = NULL, end_date = NULL) {
  assert_choice(type, c("annual", "monthly", "quarterly"))
  assert_string(indicator, min.chars = 1L, null.ok = TRUE)
  start_date = assert_dateish(start_date, null.ok = TRUE)
  end_date = assert_dateish(end_date, null.ok = TRUE)

  resource = switch(
    type,
    annual = "ExpectativasMercadoAnuais",
    monthly = "ExpectativaMercadoMensais",
    quarterly = "ExpectativasMercadoTrimestrais"
  )
  filter = bcb_odata_filter(
    indicator %&&% sprintf("Indicador eq '%s'", indicator),
    start_date %&&% sprintf("Data ge '%s'", format(start_date)),
    end_date %&&% sprintf("Data le '%s'", format(end_date))
  )
  json = bcb_olinda("Expectativas", resource, `$filter` = filter, `$orderby` = "Data")
  parse_bcb_expectations(json)
}

parse_bcb_expectations = function(json) {
  value = json$value
  if (length(value) == 0L) {
    return(data.table(
      date = as.Date(character()),
      indicator = character(),
      detail = character(),
      reference = character(),
      mean = numeric(),
      median = numeric(),
      sd = numeric(),
      min = numeric(),
      max = numeric(),
      respondents = integer(),
      base = integer()
    ))
  }
  data.table(
    date = as.Date(map_chr(value, "Data")),
    indicator = map_chr(value, "Indicador"),
    detail = map_chr(value, \(x) x$IndicadorDetalhe %||% NA_character_),
    reference = map_chr(value, "DataReferencia"),
    mean = map_dbl(value, \(x) as.numeric(x$Media)),
    median = map_dbl(value, \(x) as.numeric(x$Mediana)),
    sd = map_dbl(value, \(x) as.numeric(x$DesvioPadrao %||% NA_real_)),
    min = map_dbl(value, \(x) as.numeric(x$Minimo %||% NA_real_)),
    max = map_dbl(value, \(x) as.numeric(x$Maximo %||% NA_real_)),
    respondents = map_int(value, \(x) as.integer(x$numeroRespondentes %||% NA_integer_)),
    base = map_int(value, \(x) as.integer(x$baseCalculo %||% NA_integer_))
  )
}

#' Fetch Banco Central do Brasil (BCB) Top-5 market expectations
#'
#' Retrieve the Top-5 market expectations from the Banco Central do Brasil Focus survey (Relatório
#' Focus) via the Olinda API. The Top-5 ranking summarises the forecasts of the institutions with
#' the most accurate projections for a given indicator.
#'
#' @param type (`character(1)`)\cr
#'   The forecast horizon. One of `"annual"`, `"monthly"`, `"quarterly"`, or `"selic"`. Default
#'   `"annual"`.
#' @inheritParams bcb_expectations
#' @returns A [data.table::data.table()] with columns `date`, `indicator`, `type_calc`, `reference`,
#'   `mean`, `median`, `sd`, `min`, and `max`. The `type_calc` column holds the ranking horizon
#'   (`"C"` short, `"M"` medium, or `"L"` long term). For `type = "selic"` the `reference` column
#'   holds the COPOM meeting (e.g. `"R4/2026"`).
#' @source <https://dadosabertos.bcb.gov.br/>
#' @family data
#' @export
#' @examplesIf curl::has_internet()
#' \donttest{
#' # annual Top-5 IPCA inflation expectations
#' bcb_top5("annual", "IPCA", start_date = "2024-01-01", end_date = "2024-01-31")
#'
#' # Top-5 Selic target rate expectations
#' bcb_top5("selic", start_date = "2024-01-01", end_date = "2024-01-31")
#' }
bcb_top5 = function(type = "annual", indicator = NULL, start_date = NULL, end_date = NULL) {
  assert_choice(type, c("annual", "monthly", "quarterly", "selic"))
  assert_string(indicator, min.chars = 1L, null.ok = TRUE)
  start_date = assert_dateish(start_date, null.ok = TRUE)
  end_date = assert_dateish(end_date, null.ok = TRUE)

  resource = switch(
    type,
    annual = "ExpectativasMercadoTop5Anuais",
    monthly = "ExpectativasMercadoTop5Mensais",
    quarterly = "ExpectativaMercadoTop5Trimestral",
    selic = "ExpectativasMercadoTop5Selic"
  )
  # the Selic Top-5 resource names its indicator property in lower case
  indic_field = if (type == "selic") "indicador" else "Indicador"
  filter = bcb_odata_filter(
    indicator %&&% sprintf("%s eq '%s'", indic_field, indicator),
    start_date %&&% sprintf("Data ge '%s'", format(start_date)),
    end_date %&&% sprintf("Data le '%s'", format(end_date))
  )
  json = bcb_olinda("Expectativas", resource, `$filter` = filter, `$orderby` = "Data")
  parse_bcb_top5(json)
}

parse_bcb_top5 = function(json) {
  value = json$value
  if (length(value) == 0L) {
    return(data.table(
      date = as.Date(character()),
      indicator = character(),
      type_calc = character(),
      reference = character(),
      mean = numeric(),
      median = numeric(),
      sd = numeric(),
      min = numeric(),
      max = numeric()
    ))
  }
  # the Selic Top-5 resource uses lower-case field names and `reuniao` for the reference
  data.table(
    date = as.Date(map_chr(value, "Data")),
    indicator = map_chr(value, \(x) x$Indicador %||% x$indicador),
    type_calc = map_chr(value, \(x) x$tipoCalculo %||% NA_character_),
    reference = map_chr(value, \(x) x$DataReferencia %||% x$reuniao %||% NA_character_),
    mean = map_dbl(value, \(x) as.numeric(x$Media %||% x$media)),
    median = map_dbl(value, \(x) as.numeric(x$Mediana %||% x$mediana)),
    sd = map_dbl(value, \(x) as.numeric(x$DesvioPadrao %||% x$desvioPadrao %||% NA_real_)),
    min = map_dbl(value, \(x) as.numeric(x$Minimo %||% x$minimo %||% NA_real_)),
    max = map_dbl(value, \(x) as.numeric(x$Maximo %||% x$maximo %||% NA_real_))
  )
}

#' Fetch Banco Central do Brasil (BCB) inflation expectations
#'
#' Retrieve the rolling 12- or 24-month inflation expectations from the Banco Central do Brasil
#' Focus survey (Relatório Focus) via the Olinda API.
#'
#' @param horizon (`character(1)`)\cr
#'   The forecast horizon, either `"12m"` (next 12 months) or `"24m"` (next 24 months). Default
#'   `"12m"`.
#' @inheritParams bcb_expectations
#' @returns A [data.table::data.table()] with columns `date`, `indicator`, `smoothed`, `mean`,
#'   `median`, `sd`, `min`, `max`, `respondents`, and `base`. The `smoothed` column indicates
#'   whether the forecast is the smoothed (suavizada) series.
#' @source <https://dadosabertos.bcb.gov.br/>
#' @family data
#' @export
#' @examplesIf curl::has_internet()
#' \donttest{
#' # next-12-months IPCA inflation expectations
#' bcb_inflation("12m", "IPCA", start_date = "2024-01-01", end_date = "2024-01-31")
#' }
bcb_inflation = function(horizon = "12m", indicator = NULL, start_date = NULL, end_date = NULL) {
  assert_choice(horizon, c("12m", "24m"))
  assert_string(indicator, min.chars = 1L, null.ok = TRUE)
  start_date = assert_dateish(start_date, null.ok = TRUE)
  end_date = assert_dateish(end_date, null.ok = TRUE)

  resource = switch(
    horizon,
    `12m` = "ExpectativasMercadoInflacao12Meses",
    `24m` = "ExpectativasMercadoInflacao24Meses"
  )
  filter = bcb_odata_filter(
    indicator %&&% sprintf("Indicador eq '%s'", indicator),
    start_date %&&% sprintf("Data ge '%s'", format(start_date)),
    end_date %&&% sprintf("Data le '%s'", format(end_date))
  )
  json = bcb_olinda("Expectativas", resource, `$filter` = filter, `$orderby` = "Data")
  parse_bcb_inflation(json)
}

parse_bcb_inflation = function(json) {
  value = json$value
  if (length(value) == 0L) {
    return(data.table(
      date = as.Date(character()),
      indicator = character(),
      smoothed = logical(),
      mean = numeric(),
      median = numeric(),
      sd = numeric(),
      min = numeric(),
      max = numeric(),
      respondents = integer(),
      base = integer()
    ))
  }
  data.table(
    date = as.Date(map_chr(value, "Data")),
    indicator = map_chr(value, "Indicador"),
    smoothed = map_chr(value, \(x) x$Suavizada %||% NA_character_) == "S",
    mean = map_dbl(value, \(x) as.numeric(x$Media)),
    median = map_dbl(value, \(x) as.numeric(x$Mediana)),
    sd = map_dbl(value, \(x) as.numeric(x$DesvioPadrao %||% NA_real_)),
    min = map_dbl(value, \(x) as.numeric(x$Minimo %||% NA_real_)),
    max = map_dbl(value, \(x) as.numeric(x$Maximo %||% NA_real_)),
    respondents = map_int(value, \(x) as.integer(x$numeroRespondentes %||% NA_integer_)),
    base = map_int(value, \(x) as.integer(x$baseCalculo %||% NA_integer_))
  )
}

#' Fetch Banco Central do Brasil (BCB) Selic expectations
#'
#' Retrieve market expectations for the Selic target rate from the Banco Central do Brasil Focus
#' survey (Relatório Focus) via the Olinda API. Each row summarises the forecasts collected on a
#' given survey date for a future COPOM meeting.
#'
#' @param start_date (`NULL` | `Date(1)` | `character(1)`)\cr
#'   Start of the survey date range (e.g., `"2024-01-01"`). If `NULL`, no lower bound is applied.
#'   Default `NULL`.
#' @param end_date (`NULL` | `Date(1)` | `character(1)`)\cr
#'   End of the survey date range, in the same format as start_date. If `NULL`, no upper bound is
#'   applied. Default `NULL`.
#' @returns A [data.table::data.table()] with columns `date`, `meeting`, `mean`, `median`, `sd`,
#'   `min`, `max`, `respondents`, and `base`. The `meeting` column identifies the COPOM meeting the
#'   forecast refers to (e.g. `"R3/2028"`).
#' @source <https://dadosabertos.bcb.gov.br/>
#' @family data
#' @export
#' @examplesIf curl::has_internet()
#' \donttest{
#' bcb_selic(start_date = "2024-01-01", end_date = "2024-01-31")
#' }
bcb_selic = function(start_date = NULL, end_date = NULL) {
  start_date = assert_dateish(start_date, null.ok = TRUE)
  end_date = assert_dateish(end_date, null.ok = TRUE)

  filter = bcb_odata_filter(
    start_date %&&% sprintf("Data ge '%s'", format(start_date)),
    end_date %&&% sprintf("Data le '%s'", format(end_date))
  )
  json = bcb_olinda(
    "Expectativas",
    "ExpectativasMercadoSelic",
    `$filter` = filter,
    `$orderby` = "Data"
  )
  parse_bcb_selic(json)
}

parse_bcb_selic = function(json) {
  value = json$value
  if (length(value) == 0L) {
    return(data.table(
      date = as.Date(character()),
      meeting = character(),
      mean = numeric(),
      median = numeric(),
      sd = numeric(),
      min = numeric(),
      max = numeric(),
      respondents = integer(),
      base = integer()
    ))
  }
  data.table(
    date = as.Date(map_chr(value, "Data")),
    meeting = map_chr(value, \(x) x$Reuniao %||% NA_character_),
    mean = map_dbl(value, \(x) as.numeric(x$Media)),
    median = map_dbl(value, \(x) as.numeric(x$Mediana)),
    sd = map_dbl(value, \(x) as.numeric(x$DesvioPadrao %||% NA_real_)),
    min = map_dbl(value, \(x) as.numeric(x$Minimo %||% NA_real_)),
    max = map_dbl(value, \(x) as.numeric(x$Maximo %||% NA_real_)),
    respondents = map_int(value, \(x) as.integer(x$numeroRespondentes %||% NA_integer_)),
    base = map_int(value, \(x) as.integer(x$baseCalculo %||% NA_integer_))
  )
}

bcb_odata_filter = function(...) {
  clauses = c(...)
  if (length(clauses) > 0L) paste(clauses, collapse = " and ") else NULL
}

parse_bcb_fx_rates = function(json, currency) {
  value = json$value
  if (length(value) == 0L) {
    return(data.table(
      date = as.Date(character()),
      currency = character(),
      bid = numeric(),
      ask = numeric()
    ))
  }
  data.table(
    date = as.Date(map_chr(value, "dataHoraCotacao")),
    currency = currency,
    bid = map_dbl(value, "cotacaoCompra"),
    ask = map_dbl(value, "cotacaoVenda")
  )
}

bcb = function(series, ...) {
  base_request("https://api.bcb.gov.br/dados/serie") |>
    req_url_path_append(sprintf("bcdata.sgs.%s", series), "dados") |>
    req_url_query(formato = "json", ...) |>
    req_error(body = bcb_error_body) |>
    req_perform() |>
    resp_body_json()
}

bcb_ptax = function(currency, start_date, end_date) {
  resource = paste0(
    "CotacaoMoedaPeriodo(moeda=@moeda,dataInicial=@dataInicial,",
    "dataFinalCotacao=@dataFinalCotacao)"
  )
  bcb_olinda(
    "PTAX",
    resource,
    `@moeda` = sprintf("'%s'", currency),
    `@dataInicial` = sprintf("'%s'", format(start_date, "%m-%d-%Y")),
    `@dataFinalCotacao` = sprintf("'%s'", format(end_date, "%m-%d-%Y")),
    `$filter` = "tipoBoletim eq 'Fechamento'",
    `$select` = "cotacaoCompra,cotacaoVenda,dataHoraCotacao"
  )
}

bcb_olinda = function(service, resource, ...) {
  url = sprintf("https://olinda.bcb.gov.br/olinda/servico/%s/versao/v1/odata", service)
  base_request(url) |>
    req_url_path_append(resource) |>
    req_url_query(..., `$format` = "json") |>
    req_error(body = bcb_error_body) |>
    req_perform() |>
    resp_body_json()
}

bcb_error_body = function(resp) {
  resp_body_string(resp, "UTF-8")
}

Try the bbk package in your browser

Any scripts or data that you put into this service are public.

bbk documentation built on Aug. 28, 2026, 5:07 p.m.