Criando Base de Dados via Webscraping da ANBIMA [R]

Neste post vou ensinar como criar um script de webscraping com R e como automatizar a execução deste script com o task scheduler do Windows. Com isso, teremos um arquivo .csv que recebe novos dados a cada execução semanal.

ÍNDICE

1 - Por que capturar estes dados?

A ANBIMA disponibiliza nesta página a estimação diária da estrutura a termo das taxas de juros (ETTJ), porém o site só permite o acesso a dados dos últimos 5 dias úteis.

Então como nem todos possuem um terminal Bloomberg para ter acesso a estes dados diários, eu resolvi criar uma base própria usando um script que é executado semanalmente.

2 - O que são e para que servem estes dados?

A estimação diária da Estrutura a Termo das Taxas de Juros (ETTJ) é feita com os parâmetros da curva de juros disponibilizada na ANBIMA. Com estes dados (parâmetros do modelo) é possível gerar diferentes curvas de juros para as minhas análises de cenário de renda fixa. Abaixo há um exemplo de análise comparativa das curvas de juros entre dois períodos.

No próximo post irei ensinar como criei uma função que gera esse gráfico comparativo a partir do input de duas datas. Por agora, tenho um post no site com um gif que demonstra o comportamento o histórico dessas curvas de juros que pode ser visto aqui. Os códigos completos para você reproduzir também estão disponíveis no github.

O site permite baixar os dados em diferentes formatos e nesse caso irei utilizar o formato .csv. Quando abrimos o arquivo no Sublime Text, observamos a seguinte estrutura na imagem abaixo, com destaque nos parâmetros que serão coletados.

3 - Criando o script R

3.1 - Extração dos parâmetros pelo site da ANBIMA

Investigando o funcionamento do site quando fazemos o download, verifica-se, através da ferramenta de desenvolvedor do navegador, uma requisição POST que retorna os dados no formato solicitado. Abaixo há uma imagem destacando os pontos de identificação da requisição na aba de redes.

Quando aprofundamos na requisição, encontramos os dados que precisam ser inputados para a requisição POST retornar o que desejamos. Com eles iremos automatizar a requisição via script.

3.2 - Reproduzindo a requisição via script R

Agora iremos executar o passo a passo da requisição e limpeza dos dados. Em seguida, encapsularei tudo em uma função para poder iterar este processo nos 5 dias úteis disponíveis usando o pacote purrr.

Com o pacote httr é possível reproduzir a requisição post passando:

  • A url em texto no argumento url.
  • Um objeto list no argumento body com todos os dados da requisição que vimos anteriormente.
  • A string “form” para o argumento encode para identificar que a requisição tem a estrutura de formulário.
url <- "https://www.anbima.com.br/informacoes/est-termo/CZ-down.asp"

dt = "21/05/2021"

r <- httr::POST(url = url,
                body = list(Idioma = "PT",
                            Dt_Ref = dt,
                            saida = "csv"),
                encode = "form")
r
Response [https://www.anbima.com.br/informacoes/est-termo/CZ-down.asp]
  Date: 2021-05-30 02:53
  Status: 200
  Content-Type: text/csv
  Size: 2.78 kB
NA

3.3 - Faxinando o dado

Como a requisição possui status 200, sabemos que foi bem sucedida. Agora podemos conferir o conteúdo com a função content(). Com o argumento as a função permite interpretar o resultado como texto puro ‘text’ ou binário ‘raw’. Com os teste que fiz, o mais indicado é ler o output como binário e então converter o binário em texto usando a função RawToChar().

texto_puro <- rawToChar(content(r, as = "raw"))

Com isso, podemos passar o texto puro para a função read_csv2() que irá gerar o dataframe com os dados desejados. Como o texto já está configurado com notaçao científica, a função já entende que os dados são numéricos e interpreta eles corretamente.

Mas antes de passar na função, lembrem-se da imagem do arquivo .csv no sublime text, com ela vemos que é necessário lermos somente as 3 primeiras linhas do texto (1 cabeçalho + 2 observações). Então passaremos essa condição no argumento n_max, iremos ajustar o nome das colunas e adicionar uma columa com a data. Tudo em uma sequência de pipe.

dados <- 
        read_csv2(texto_puro, n_max = 2) %>% 
        `colnames<-`(c("Grupo","B1","B2","B3","B4","L1","L2")) %>% 
        mutate(data = dt)

glimpse(dados)
Rows: 2
Columns: 8
$ Grupo <chr> "PREFIXADOS", "IPCA"
$ B1    <dbl> 0.11373832, 0.05791674
$ B2    <dbl> -0.07779597, -0.04401601
$ B3    <dbl> -0.05762846, -0.07163773
$ B4    <dbl> -0.04368595, -0.02921150
$ L1    <dbl> 2.296147, 1.280478
$ L2    <dbl> 0.43812693, 0.09180126
$ data  <chr> "21/05/2021", "21/05/2021"

3.4 - Encapsulando em uma função

Agora podemos criar uma função que recebe a data e retorna o nosso datafame desejado. Nesse caso precisamos generalizar o argumento de data no nosso código usando a função format(dt, "%d/%m/%Y") no argumento da requisição POST e a função mutate(data = dt) na coluna de data que adicionamos no dataframe. A função final fica dessa forma:

get_ettj_param <- function(dt){
    
    url <- "https://www.anbima.com.br/informacoes/est-termo/CZ-down.asp"
    
    r <- httr::POST(url = url,
                    body = list(Idioma = "PT",
                                Dt_Ref = format(dt, "%d/%m/%Y"),
                                saida = "csv"),
                    encode = "form")
    
    texto_puro <- rawToChar(content(r,as = "raw"))
    
    dados <- 
        read_csv2(texto_puro, n_max = 2) %>% 
        `colnames<-`(c("Grupo","B1","B2","B3","B4","L1","L2")) %>% 
        mutate(data = dt)
    
    return(dados)    
}

3.5 - Obtendo o vetor de dias úteis com o bizdays

Para iterar as datas precisamos criar um vetor com os últimos 5 dias úteis. Felizmente o pacote bizdays nos permite selecionar os últimos 5 dias úteis independente do dia que você executar o script e considerando o calendário oficial da ANBIMA. É um pacote excelente.

# load the working days by calendar of anbima
data(holidaysANBIMA, 
     package = 'bizdays')
cal <- create.calendar(holidaysANBIMA, 
                       weekdays=c('saturday', 'sunday'),
                       name='ANBIMA')
d2 = Sys.Date()
d1 = add.bizdays(d2, -6, cal = cal)
data_seq <- bizseq(d1, d2, cal)
data_seq
[1] "2021-05-21" "2021-05-24" "2021-05-25" "2021-05-26" "2021-05-27"
[6] "2021-05-28"

Primeiro carregamos o calendário da ANBIMA, já disponível no pacote, na variável cal. Depois obtemos o dia atual com a função Sys.Date() e o sexto dia útil passado com a função add.bizdays(., -6) e por fim criamos o vetor dos últimos 5 dias úteis com a função bizseq().

3.6 - Iterando a função em múltiplas datas com o purrr

O purrr permite adotarmos o paradigma de programação funcional permitindo que eliminemos for loops do nosso código utilizando funções como o map(). Além disso ele possui variações como o map_dfr() que além de iterar o vetor de input na função desejada, ele já executa o empilhamento dos dados, retornando um data frame completo.

deal_error <- 
    purrr::possibly(get_ettj_param, 
                    otherwise = NA_real_) # deal with error
result <- 
    purrr::map_dfr(data_seq[1:6], deal_error) %>% 
    select(data, everything())

glimpse(result)
Rows: 12
Columns: 8
$ data  <date> 2021-05-21, 2021-05-21, 2021-05-24, 2021-05-24, 2021-05-25, 202~
$ Grupo <chr> "PREFIXADOS", "IPCA", "PREFIXADOS", "IPCA", "PREFIXADOS", "IPCA"~
$ B1    <dbl> 0.11373832, 0.05791674, 0.11197249, 0.05878552, 0.11159105, 0.05~
$ B2    <dbl> -0.07779597, -0.04401601, -0.07555360, -0.04795247, -0.07494723,~
$ B3    <dbl> -0.05762846, -0.07163773, -0.05615549, -0.07276023, -0.05571391,~
$ B4    <dbl> -0.04368595, -0.02921150, -0.03490220, -0.03105072, -0.03731779,~
$ L1    <dbl> 2.296147, 1.280478, 2.252240, 1.326245, 2.241934, 1.393420, 2.24~
$ L2    <dbl> 0.43812693, 0.09180126, 0.44694263, 0.08407592, 0.45680824, 0.08~

Nesse caso eu também utilizo o possibily() para lidar com possíveis erros em alguma iteração do map. Com ela, caso ocorra um erro em uma iteração, ele irá inputar NA nos dados que falharam e continuará a execuçao do próximo item, sem quebrar a iteração no meio.

3.7 - Salvando os dados em um .csv

Para finalizar o script, precisamos criar uma condição que verifica se o arquivo .csv já existe. Caso exista, ele irá adicionar os novos dados ao arquivo, caso contrário, irá criar um novo arquivo com os dados capturados.

name_db <- "hist_coef_pre_ipca.csv"

if (file.exists(name_db)){
    datas_unicas <- unique(as.Date(read.csv2(name_db,header = 1)$data, "%Y-%m-%d"))
    
    result <- result %>% filter(!data %in% datas_unicas)
    
    write.table(result,
                file =  name_db,
                append = TRUE,
                row.names = FALSE,
                col.names = FALSE,
                sep = ";", 
                fileEncoding = "UTF-8")
} else {
    write.table(result, 
                file =  name_db,
                append = FALSE,
                row.names = FALSE,
                sep = ";", 
                fileEncoding = "UTF-8")
}

4 - Agendando a execução do script

Com o script pronto, podemos agendar a execução do scraper usando um arquivo .bat que executa via linha de comando o script R.

O arquivo .bat fica dessa forma

"C:\Program Files\R\R-4.0.2\bin\R.exe" CMD BATCH C:\Users\augus\Documents\dev_R\schedule_R\coef_curv_anbima_scheduler.R

Nele é passado o executável do R na string inicial, depois o comando CMD BATCH chama a linha de comando que será executado o script R que vem logo em seguida.

A partir desse simples código o TaskScheduler do Windows irá agendar a execução do script e assim toda semana sua base irá receber os dados da semana anterior.

Para não prolongar ainda mais este post, deixo aqui um tutorial de como agendar tarefas com essa ferramenta. Basta seguir estes passos e selecionar o arquivo .bat como programa/script a ser executado.

Para os usuários de Linux é possível usar o crontab para fazer o mesmo agendamento. Essa estrutura permite executar scripts de diferentes linguagens. Por exemplo, caso queira agendar um script python, basta usar o caminho do executável do python e referenciar o script .py que deseja.

No meu caso, agendo o script para que seja executado toda segunda-feira para capturar. Então como eu criei o script em Julho/2020 eu tenho dados desde esse período no meu arquivo .csv.

Por fim, o script R que utilizo se encontra no seguinte gist do github. No próximo post irei detalhar o script R que executa o plot apresentado no início do artigo.