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:
- Analisar como as tabelas de dados são construídas no código HTML (mais simples)
- Analisar o envio de um comando POST e sua consequente resposta para pegar os dados da tabela (mais avançado*)
*Adendo:

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:
=
‘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:


-M.R.