19/01/2023

Curva de Juros em Python


A curva de juros mostra o rendimento que um investidor espera ganhar se emprestar seu dinheiro por um determinado período de tempo.

Com a curva de juros é possível projetar visualmente a tendência da evolução dos juros com o passar dos anos, conforme as condições atuais de mercado.

Desse modo é possível visualizar as diferentes taxas para os múltiplos vencimentos em um único gráfico.

Quem é responsável por calcular a curva de juros?

No Brasil, a instituição responsável por calcular a curva de juros à vista é a Associação Brasileira das Entidades dos Mercados Financeiro e de Capitais ANBIMA.

Qual é a finalidade da curva de juros?

Cabe frisar que a curva de juros considera o comportamento dos juros em relação ao período. Isto é, ela realiza previsões dos rendimentos em investimentos, empréstimos, etc. Isso permite mitigar os riscos envolvidos em cada aplicação financeira.

Por exemplo, uma curva de juros normal se inclina para cima e para a direita à medida que os rendimentos aumentam com o vencimento. Isso indica que as condições de mercado e a economia estão saudáveis e funcionando normalmente, o que permite que os investidores escolham ativos cuja rentabilidade esteja atrelada a juros maiores.

Por outro lado, também é possível que a previsão relacionada à curva de juros não seja otimista. Quando as taxas para prazos mais curtos são mais altas do que aquelas para prazos mais longos,temos uma curva de juros invertida.

Nesse caso, a curva de juros se inclina para baixo e para a direita. Isso pode indicar uma recessão ou um mercado em baixa, podendo sofrer quedas prolongadas nos preços e nos rendimentos dos títulos.

Exemplo usando Python

# Instalando e importando as bibliotecas necessárias

!pip install --upgrade pip
!pip install holidays
!pip install workalendar 

from datetime import datetime, date, timedelta
from workalendar.america import Brazil

import requests
from time import sleep
from requests import ConnectTimeout, ReadTimeout
import pandas as pd
import plotly.graph_objects as go

import numpy as np
from io import StringIO

import holidays
import urllib3
urllib3.disable_warnings()
# Busca os feriados no Brasil
data_atual = datetime.today()
ano_atual = data_atual.year
dias_semana = {"SEGUNDA": 0, "TERCA": 1, "QUARTA": 2, "QUINTA": 3, "SEXTA": 4, "SABADO": 5, "DOMINGO": 6}

cal = Brazil()
feriados_brasil = []
for i in range(ano_atual, ano_atual+15):
    feriados_brasil_raw = cal.holidays(i)
    for j in feriados_brasil_raw:
        feriados_brasil.append(j)

feriados_brasil = (list(zip(*feriados_brasil))[0])
feriados_brasil
# Métodos auxiliares

def iterdates(date1, date2):
    one_day = timedelta(days = 1)
    current = date1
    
    while current < date2:
        yield current
        current += one_day

def ajustar_data(df):
    meses = {'Jan': '01', 'Fev': '02', 'Mar': '03', 'Abr': '04', 'Mai': '05', 'Jun': '06',
             'Jul': '07', 'Ago': '08', 'Set': '09', 'Out': '10', 'Nov': '11', 'Dez': '12'}

    for mes, numero in meses.items():
        df = df.str.replace(mes, numero)

    df = df.str.replace(" ", "/")
    df = pd.to_datetime(df, format="%d/%m/%Y")
    return df

def buscar_vencimento_titulo(titulo):
    meses = {'01': 'F', '02': 'G', '03': 'H', '04': 'J', '05': 'K', '06': 'M',
             '07': 'N', '08': 'Q', '09': 'U', '10': 'V', '11': 'X', '12': 'Z'}
    meses = {y: x for x, y in meses.items()}
    ano = "20" + titulo[-2:]
    mes = meses.get(titulo[3])

    dias = pd.date_range(start= mes + '/1/' + ano, periods=7, freq='BMS')
    dias = filter(lambda dia: datetime.fromtimestamp(dia.timestamp()) not in feriados_brasil, dias)
    dia = list(dias)[0]
    return dia

def buscar_dias_uteis_ate_vencimento_titulo(titulo):
    dia_vencimento = buscar_vencimento_titulo(titulo)
    start = date.today()
    end = dia_vencimento.date()

    dias_uteis = sum(1 for day in iterdates(start, end) if day.weekday() not in (dias_semana.get("SABADO"),dias_semana.get("DOMINGO")) and day not in feriados_brasil)
    dias_de_semana = sum(1 for day in iterdates(start, end) if day.weekday() not in (dias_semana.get("SABADO"),dias_semana.get("DOMINGO")))
    feriados_na_semana = dias_de_semana - dias_uteis
    #print(f"Dias úteis entre {start} e {end}: {dias_uteis}") 
    #print(f"Dias de semana entre {start} e {end}: {dias_de_semana}") 
    # print(f"Feriados entre {start} e {end}: {feriados_na_semana}") 
    return dias_uteis

def buscar_proximas_series(anos_futuros=2, anos_anteriores=0):
    nomes_series = []
    data_atual = datetime.today()
    mes_atual = data_atual.month
    ano_atual = data_atual.year
    meses = {'1': 'F', '2': 'G', '3': 'H', '4': 'J', '5': 'K', '6': 'M',
             '7': 'N', '8': 'Q', '9': 'U', '10': 'V', '11': 'X', '12': 'Z'}

    # Atualmente não é possível buscar séries passadas
    #
    # print("Busca dados anos anteriores")
    # for i in range(ano_atual - anos_anteriores - 1, ano_atual - 1):
    #     ano = str(i)
    #     nomes_series.append("DI1F" + ano[2:4])
    #     print(nomes_series)

    print("Busca dados ano atual")
    for i, letra in meses.items():
        if int(i) >= int(mes_atual):
            nomes_series.append("DI1" + letra + str(ano_atual)[2:4])
        #print(nomes_series)

    print("Busca dados anos futuros")
    for i in range(ano_atual + 1, ano_atual + anos_futuros + 1):
        ano = str(i)
        nomes_series.append("DI1F" + ano[2:4])
        #print(nomes_series)

    return nomes_series

def buscar_dados_futuros(titulo):
    sleep(0.1)
    headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'}
    url = "https://br.advfn.com/bolsa-de-valores/bmf/{}/historico/mais-dados-historicos".format(titulo)
    print("Abrindo " + url)
    html = requests.get(url, headers=headers, verify=False, timeout=5)
    dif = pd.read_html(html.text, decimal=',', thousands='.')[1]
    dif = dif[["Data", "Fechamento"]]
    dif = dif.rename(columns={"Fechamento": titulo})
    dif[['Data']] = dif[['Data']].apply(lambda x: ajustar_data(x))
    dif = dif.set_index("Data")
    return dif
# Definindo parâmetros iniciais

anos = 10 #@param {type:"integer"}
semanas = 7 #@param {type:"integer"}
# Buscando dados ADVFN

series_advfn = []
nomes_series = buscar_proximas_series(anos, anos_anteriores)
print(nomes_series)

for nome_serie in nomes_series:
    try:
        s = buscar_dados_futuros(nome_serie)
        s = s.loc[~s.index.duplicated(keep='first')]
        series_advfn.append(s)
    except ConnectTimeout:
        print("Timeout ao buscar dados de " + nome_serie + ". Buscando próxima série...")
    except ReadTimeout:
        print("Timeout ao buscar dados de " + nome_serie + ". Buscando próxima série...")
# Buscando dados ANBIMA

hoje = date.today().strftime('%d/%m/%Y')
ontem = date.today() - timedelta(days = 1)
ontem = ontem.strftime('%d/%m/%Y')

url = 'https://www.anbima.com.br/informacoes/est-termo/CZ-down.asp'
payload = {'Idioma': 'US', 'Dt_Ref': ontem, 'saida': 'xml'}

from urllib import request, parse
data = parse.urlencode(payload).encode()
req =  request.Request(url, data=data) # this will make the method "POST"

with request.urlopen(req) as response:
   xml = response.read()

vertices = pd.read_xml(xml, xpath="//TERM_STRUCTURE")
vertices.drop('Indexed', axis=1, inplace=True)                                   
vertices.drop('BEI', axis=1, inplace=True)
vertices.dropna(inplace=True)
vertices = vertices.replace({',': ''}, regex=True)
vertices['Prefixed'] = vertices['Prefixed'].astype(float)
vertices['Business_Day'] = vertices['Business_Day'].astype(int)
vertices.rename({'Prefixed': 'Taxa'}, axis=1, inplace=True)
vertices.rename({'Business_Day': 'DiasUteis'}, axis=1, inplace=True)
# vertices

circular = pd.read_xml(xml, xpath="//CIRCULAR ")
circular = circular.replace({',': ''}, regex=True)
circular['Rate'] = circular['Rate'].astype(float)
circular['Business_Day'] = circular['Business_Day'].astype(int)
circular.rename({'Rate': 'Taxa'}, axis=1, inplace=True)
circular.rename({'Business_Day': 'DiasUteis'}, axis=1, inplace=True)
circular.dropna(inplace=True)
# circular

anbima_df = pd.concat([vertices, circular]).sort_values(by="DiasUteis").drop_duplicates().reset_index(drop=True)
anbima_df.set_index('DiasUteis', inplace=True)
# anbima_df
# Já buscamos os dados, agora é trabalhar neles...

difuturo_por_titulo = pd.concat(series_advfn, axis=1)
difuturo_por_titulo.index = pd.to_datetime(difuturo_por_titulo.index)
# difuturo_por_titulo

dayofweek = difuturo_por_titulo.index.dayofweek
semanal_por_titulo = difuturo_por_titulo.iloc[(dayofweek == 0) | (difuturo_por_titulo.index==difuturo_por_titulo.index.max())].copy()
semanal_por_titulo.sort_index(ascending=False, inplace=True)
# semanal_por_titulo

difuturo_por_titulo = semanal_por_titulo.head(semanas+1)
difuturo_por_titulo_transposto = difuturo_por_titulo.transpose(copy=True)
# difuturo_por_titulo_transposto

difuturo_por_vencimento_transposto = difuturo_por_titulo_transposto.copy(deep=True)
difuturo_por_vencimento_transposto['Vencimento'] = difuturo_por_vencimento_transposto.index
difuturo_por_vencimento_transposto['Vencimento'] = difuturo_por_vencimento_transposto['Vencimento'].apply(lambda x: buscar_vencimento_titulo(x))
difuturo_por_vencimento_transposto = difuturo_por_vencimento_transposto.set_index('Vencimento')
difuturo_por_vencimento_transposto.sort_index(ascending=False, inplace=True)
# difuturo_por_vencimento_transposto

difuturo_por_dias_uteis_transposto = difuturo_por_titulo_transposto.copy(deep=True)
difuturo_por_dias_uteis_transposto['Vencimento'] = difuturo_por_dias_uteis_transposto.index
difuturo_por_dias_uteis_transposto['Vencimento'] = difuturo_por_dias_uteis_transposto['Vencimento'].apply(lambda x: buscar_dias_uteis_ate_vencimento_titulo(x))
difuturo_por_dias_uteis_transposto = difuturo_por_dias_uteis_transposto.loc[(difuturo_por_dias_uteis_transposto['Vencimento'] > 0)].copy()

difuturo_por_dias_uteis_transposto = difuturo_por_dias_uteis_transposto.set_index('Vencimento')
difuturo_por_dias_uteis_transposto.sort_index(ascending=False, inplace=True)
# difuturo_por_dias_uteis_transposto
# Criando os gráficos... Gráfico por título usando Plotly

hoje = date.today().strftime('%d/%m/%Y')

layout = go.Layout(
    annotations=[
        dict(x=1.12, y=1.05, align="right", valign="top", text='Semanas:', showarrow=False, xref="paper", yref="paper", 
             xanchor="center", yanchor="top"),
        dict(text = f"Fonte dos dados: ADVFN - https://br.advfn.com/ 
Data: {hoje}", showarrow=False, x = 0, y = -0.15, xref='paper', yref='paper', xanchor='left', yanchor='bottom', xshift=-10, yshift=-150, font=dict(size=10, color="grey"), align="left") ] ) fig = go.Figure(layout=layout) fig.update_layout(title_text="Curva de Juros", title_font_size=20) fig.update_layout(autosize=False, width=900, height=700) fig.update_xaxes(title_text="Títulos") fig.update_xaxes(tickangle=45) fig.update_xaxes(rangeslider_visible=True) fig.update_yaxes(title_text="Taxas (em %)") for numero, i in enumerate(difuturo_por_titulo_transposto): #suavizado, conectando gaps https://plotly.com/python/line-charts/ ontem = date.today() - timedelta(days = 1) if i.date() == ontem: texto_legenda = "(ADVFN) Ontem: " + i.strftime('%d/%m/%Y') else: texto_legenda = "(ADVFN) Semana " + str(numero) + ": " + i.strftime('%d/%m/%Y') fig.add_trace(go.Scatter(x=difuturo_por_titulo_transposto.index, y=difuturo_por_titulo_transposto[i], mode='lines', name=texto_legenda, line_shape='spline', connectgaps=True)) fig.update_layout() fig.show()


O gráfico ficou legal, mas tem um problema: a distorção gerada no eixo X pela falta de proporcionalidade entre os vencimentos dos títulos... Vamos trabalhar nisso, usando a data de vencimento dos títulos.

# Criando os gráficos... Gráfico por data de vencimento usando Plotly

hoje = date.today().strftime('%d/%m/%Y')

layout = go.Layout(
    annotations=[
        dict(x=1.12, y=1.05, align="right", valign="top", text='Semanas:', showarrow=False, xref="paper", yref="paper", 
             xanchor="center", yanchor="top"),
        dict(text = f"Fonte dos dados: ADVFN - https://br.advfn.com/ 
Data: {hoje}", showarrow=False, x = 0, y = -0.15, xref='paper', yref='paper', xanchor='left', yanchor='bottom', xshift=-10, yshift=-150, font=dict(size=10, color="grey"), align="left") ] ) fig = go.Figure(layout=layout) fig.update_layout(title_text="Curva de Juros", title_font_size=20) fig.update_layout(autosize=False, width=900, height=700) fig.update_xaxes(title_text="Data de vencimento do título") fig.update_xaxes(tickangle=45) fig.update_xaxes(rangeslider_visible=True) fig.update_yaxes(title_text="Taxas (em %)") for numero, i in enumerate(difuturo_por_vencimento_transposto): ontem = date.today() - timedelta(days = 1) if i.date() == ontem: texto_legenda = "(ADVFN) Ontem: " + i.strftime('%d/%m/%Y') else: texto_legenda = "(ADVFN) Semana " + str(numero) + ": " + i.strftime('%d/%m/%Y') #suavizado, conectando gaps https://plotly.com/python/line-charts/ fig.add_trace(go.Scatter(x=difuturo_por_vencimento_transposto.index, y=difuturo_por_vencimento_transposto[i], mode='lines', name=texto_legenda, line_shape='spline', connectgaps=True)) fig.update_layout() fig.show()


Agora o gráfico não está distorcendo o eixo X, mas quero trabalhar com dias úteis até o vencimento, para poder comparar com os dados da ANBIMA.

# Criando os gráficos... Gráfico por dias úteis (ANBIMA e ADVFN) usando Plotly

hoje = date.today().strftime('%d/%m/%Y')

layout = go.Layout(
    annotations=[
        dict(x=1.12, y=1.05, align="right", valign="top", text='Semanas:', showarrow=False, xref="paper", yref="paper", 
             xanchor="center", yanchor="top"),
        dict(text = f"Fonte dos dados: ADVFN - https://br.advfn.com/, ANBIMA - https://www.anbima.com.br/ 
Data: {hoje}", showarrow=False, x = 0, y = -0.15, xref='paper', yref='paper', xanchor='left', yanchor='bottom', xshift=-10, yshift=-150, font=dict(size=10, color="grey"), align="left") ] ) fig = go.Figure(layout=layout) fig.update_layout(title_text="Curva de Juros", title_font_size=20) fig.update_layout(autosize=False, width=900, height=700) fig.update_xaxes(title_text="Dias úteis até o vencimento do título") fig.update_xaxes(tickangle=45) fig.update_xaxes(rangeslider_visible=True) fig.update_yaxes(title_text="Taxas (em %)") fig.add_trace(go.Scatter(x=anbima_df.index, y=anbima_df['Taxa'], name="(ANBIMA) Ontem: " + ontem.strftime('%d/%m/%Y'), line_shape='spline', mode='lines+markers', connectgaps=True, marker_size=10)) for numero, i in enumerate(difuturo_por_dias_uteis_transposto): #suavizado, conectando gaps https://plotly.com/python/line-charts/ ontem = date.today() - timedelta(days = 1) if i.date() == ontem: texto_legenda = "(ADVFN) Ontem: " + i.strftime('%d/%m/%Y') else: texto_legenda = "(ADVFN) Semana " + str(numero) + ": " + i.strftime('%d/%m/%Y') fig.add_trace(go.Scatter(x=difuturo_por_dias_uteis_transposto.index, y=difuturo_por_dias_uteis_transposto[i], mode='lines+markers', name=texto_legenda, line_shape='spline', connectgaps=True, marker_size=10)) fig.update_layout() fig.show()


Agora sim, o gráfico permite uma comparação entre os dados da ANBIMA e ADVFN. Podem ter pequenos problemas na exatidão do cálculo dos dias úteis...

Links relacionados

Problemas com o código?

Em caso de problema com o código, por favor, me avise no comentários.

Nenhum comentário:

Postar um comentário