[Python] Curvas PRÉ-DI e Cupom Limpo usando web scraping

Hoje trago um post que mistura conceitos de finanças com programação para que o usuário consiga analisar estas duas curvas tão importantes no mercado financeiro brasileiro e para que possa, também, utilizá-las como insumo para outras análises, como a construção de NDFs para uma data, por exemplo.

Como devemos buscar o site para pegar os dados desejados?

  • Buscar o site que contém os dados
  • Analisar no seu código fonte quais são os caminhos de tais dados
  • Construir um bot que simule um usuário comum entrando e copiando os dados, para alguns sites esta pode ser a etapa mais complexa

No caso da B3, o site é bem simples de se extrair dados, com uma breve análise podemos notar que os dados que são mostrados partem do seguinte site base – Base Curvas – e que este pode ser usado de dois modos:

  1. Analisar como as tabelas de dados são construídas no código HTML (mais simples)
  2. Analisar o envio de um comando POST e sua consequente resposta para pegar os dados da tabela (mais avançado*)

*Adendo:

Post Request site BMF Bovespa curvas – exemplo de PRÉ-DI 08/04/2020

Neste post abordarei o método 1 que, por mais que seja mais simples, não demonstra grande perda de performance relativa ao método 2.

Começando pela curva mais simples de fazer scraping, a de Cupom Limpo, que só possui 2 colunas, a de dias do vértice e a de taxas360:

Com uma breve inspeção do código fonte da página na região da tabela descobrimos que cada uma das células é um objeto “td” e que possui uma classe que varia de acordo com a linha, sendo ou “[‘tabelaConteudo1’]” ou “[‘tabelaConteudo2’]”, portanto, devemos focar nos objetos que possuam essas classes:


Para este trabalho utilizaremos, essencialmente, 4 bibliotecas:

  • Para scraping:
    • requests
    • bs4
  • Para estruturar os dados:
    • pandas
  • Para utilizar datas:
    • datetime

Primeiramente importo as bibliotecas para o código:

#===================================================================#
# Desativa o aviso de request não seguro
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
#===================================================================#

import pandas as pd
from datetime import date
import requests
from bs4 import BeautifulSoup

Posteriormente crio a classe que utilizarei para baixar as curvas de juros e a inicío com algumas variáveis:

class bmf:
    def __init__(self, val_date):
        """
        Gera o objeto da bmf a partir de val_date em formato de datetime.date
        """
        self.val_date = val_date
        mes = self.val_date.month
        dia = self.val_date.day
        if self.val_date.month<10: mes='0'+str(self.val_date.month)
        if self.val_date.day<10: dia = '0'+str(self.val_date.day)
        self.dt_barra = f'{dia}/{mes}/{val_date.year}'
        self.dt_corrida = f'{val_date.year}{mes}{dia}'
        self.headers = {"User-Agent":'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'}

Um usuário atento já deve ter notado qual a lógica do link da BMF:

http://www2.bmf.com.br/pages/portal/bmfbovespa/boletim1/TxRef1.asp?Data=13/04/2020&Data1=20200413&slcTaxa=DOC

=

http://www2.bmf.com.br/pages/portal/bmfbovespa/boletim1/TxRef1.asp?Data={dd}/{mm}/{yyyy}&Data1={yyyymmdd}&slcTaxa=DOC

‘slcTaxa=DOC’ -> Curva de cupom limpo

Deste modo, inicio as variáveis que serão utilizadas para a construção do link dada uma data, sendo estas ‘dia’, ‘mes’ e ‘ano’. Com atenção para as variáveis dia e ano, pois devem ser observadas como strings, que utilizam o 0 caso dia ou mês seja inferior a 10, i.e. 09 (Setembro).

A variável ‘headers’ se encarrega de demonstrar para o site que um navegador “legítimo” está solicitando a conexão, e não um bot.

Adiciono, agora, a função que se encarrega de baixar a curva de cupom do dia inicializado no objeto ‘bmf’:

def _baixa_cupom(self):
    """
    Dada a val_date baixa a curva de cupom limpo
    """
    link = f'https://www2.bmf.com.br/pages/portal/bmfbovespa/boletim1/TxRef1.asp?Data={self.dt_barra}&Data1={self.dt_corrida}&slcTaxa=DOC'
    page = requests.get(link, headers=self.headers, verify=False)
    soup = BeautifulSoup(page.content, 'html.parser')
    texto = soup.find_all('td')
    dias, taxas = [], []
    tabelas = ["['tabelaConteudo1']", "['tabelaConteudo2']"]
    for i in range(len(texto)):
        try:
            if str(texto[i]['class']) in tabelas:
                tratado = texto[i].text.replace('\r\n','').replace(',','.').replace(' ','')
                if i==0 or i%2==0:
                    dias.append(int(tratado))
                else:
                    taxas.append(float(tratado)/100)
        except:
            pass
    return pd.DataFrame(data=taxas, index=dias, columns={'taxas360'})

Primeiramente construo o link que será utilizado e o coloco na requisição da biblioteca requests, posteriormente utilizo o objeto de resposta fornecido e coloco na função BeautifulSoup da biblioteca bs4 para que seja gerado um texto – somente com o que me interessa, os td’s neste caso – “parseável” de modo mais amigável.

Após gerar o texto geral do documento passo por cada um de seus itens para descobrir qual a sua classe, se esta estiver na lista definida como as que contém dados que desejo (tabelaConteudo1 e 2), farei um tratamento do texto removendo strings desnecessárias e substituindo a vírgula (‘,’) do número por ponto (‘.’) para colocá-los em padrão americano.

Para os casos no qual o número de “parseamento” seja 0 ou par, este texto corresponde aos dias e deve ser adicionado no vetor de dias como um integer, já no caso de números ímpares o texto correspondente deve ser adicionado como um float no vetor de taxas.

Depois de gerar os dois vetores crio um Pandas DataFrame com os dias e taxas no seguinte padrão:

Os índices do DataFrame são os dias e as taxas360 são as taxas observadas no site divididas por 100. Uma praticidade que este formato de resposta trás é a possibilidade de exportá-lo como um csv.

Já a tabela da Curva PRÉ, se caracteriza por possuir 3 colunas principais, dias do vértice, taxas252 e taxas360, o que irá gerar uma etapa a mais na hora de filtrar os dados, visto que não tem identificação de que nos permita definir a qual coluna estes pertencem:

def _baixa_pre(self):
    """
    Dada a val_date baixa a curva pré-di
    """
    link = f'https://www2.bmf.com.br/pages/portal/bmfbovespa/boletim1/TxRef1.asp?Data={self.dt_barra}&Data1={self.dt_corrida}&slcTaxa=PRE'
    page = requests.get(link, headers=self.headers, verify=False)
    soup = BeautifulSoup(page.content, 'html.parser')
    texto = soup.find_all('td')
    dias, taxas252, taxas360 = [], [], []
    tabelas = ["['tabelaConteudo1']", "['tabelaConteudo2']"]
    for i in range(0,len(texto),3):
        try:
            if str(texto[i]['class']) in tabelas:
                if i<=len(texto)-2:
                    dias.append(int(texto[i].text.replace('\r\n','').replace(',','.').replace(' ','')))
                    taxas252.append(float(texto[i+1].text.replace('\r\n','').replace(',','.').replace(' ',''))/100)
                    taxas360.append(float(texto[i+2].text.replace('\r\n','').replace(',','.').replace(' ',''))/100)
        except:
            pass
    return pd.DataFrame({'taxas252':taxas252,'taxas360':taxas360}, index=dias)   

A lógica do código é idêntica, necessitando apenas de um adendo, sabemos que os dados vêm sempre na sequência de índices [0,1,2], [3,4,5], [6,7,8], …, [n,n+1,n+2], portanto me aproveito desta lógica para determinar quais dados serão adicionados em cada um dos 3 vetores, de dias, de taxas252 e de taxas360, retornando estes em um DataFrame.

Breve plot em HTML das curvas:

*Curva plotada a partir do 4º vértice disponível por possuir grande distorção do curtíssimo prazo causada pela variação do dólar

-M.R.

Deixe um comentário

Crie um site como este com o WordPress.com
Comece agora