import os
"BRASA_DATA_PATH"] = "D:\\brasa"
os.environ[import brasa
Vamos construir as curvas de cupom cambial, limpo e sujo, da B3 utilizando a biblioteca QuantLib.
Primeiramente vou fazer o setup do projeto brasa
definindo o repositório que contém os dados. Lembrando que o projeto brasa
é um projeto em release alpha que venho trabalhando para organizar dados de fontes públicas.
Os demais pacotes importados são o setup básico que inclui: pandas
, numpy
, datetime
, matplotlib
e pyarrow
.
from datetime import datetime
import QuantLib as ql
import pandas as pd
import numpy as np
import pyarrow.compute as pc
import matplotlib.ticker as mticker
Vou trabalhar com a data de referência 2023-07-14. Para esta data vamos começar carregando os contratos futuros de dólar (DOL) e taxa DI (DI1) negociados na B3. Utilizo o pacote brasa
para obter os dados de preços de ajuste dos contratos futuros da B3.
= datetime(2023, 7, 14)
refdate
= (brasa
fut_dol "b3-futures-dol")
.get_dataset(filter(pc.field("refdate") == refdate)
.
.to_table()
.to_pandas())
= (brasa
fut_di1 "b3-futures-di1")
.get_dataset(filter(pc.field("refdate") == refdate)
.
.to_table() .to_pandas())
Agora vamos pegar a taxa DI para a data de referência utilizando o dataset de indicadores econômicos divulgado pela B3. Isso é importante porque estes dados são utilizados pela B3 na construção das curvas.
= (brasa
cdi "b3-economic-indicators-price")
.get_dataset(filter(pc.field("refdate") == refdate)
.filter(pc.field("commodity") == "DI1")
.filter(pc.field("symbol") == "RTDI1")
.=["price"])
.to_table(columns0,0]) / 100 .to_pandas().iloc[
Preciso ainda obter as cotações de dólar, e acredite, temos diversas cotações de dólar. As cotações de dólar são referentes à data de referência e ao dia anterior. Para obter estes pontos de forma higiênica, vou fazer o setup da QuantLib e utilizar o calendário brasileiro para filtrar os dados nas datas de interesse, a data de referência e o dia anterior.
= ql.Date().from_date(refdate)
today = today
ql.Settings.instance().evaluationDate = ql.Brazil(ql.Brazil.Settlement)
calendar = ql.NullCalendar() calendar_act
= (brasa
df_econ_ind "b3-economic-indicators-price")
.get_dataset(filter(pc.field("refdate") >= calendar.advance(today, ql.Period(-1, ql.Days)).to_date())
.filter(pc.field("refdate") <= refdate)
.filter(pc.field("commodity") == "DOL")
.
.to_table()
.to_pandas()) df_econ_ind
refdate | commodity | symbol | description | price | |
---|---|---|---|---|---|
0 | 2023-07-13 | DOL | RTDOLCL | DÓLAR CUPOM LIMPO - CÁLCULADO PELA B3 | 4.7922 |
1 | 2023-07-13 | DOL | RTDOLD1 | DÓLAR B3 SPOT - 1 DIA | 4.7949 |
2 | 2023-07-13 | DOL | RTDOLD2 | DÓLAR B3 SPOT - 2 DIAS | 4.7967 |
3 | 2023-07-13 | DOL | RTDOLT1 | PTAX800 VENDA | 4.8038 |
4 | 2023-07-14 | DOL | RTDOLCL | DÓLAR CUPOM LIMPO - CÁLCULADO PELA B3 | 4.7896 |
5 | 2023-07-14 | DOL | RTDOLD1 | DÓLAR B3 SPOT - 1 DIA | 4.7901 |
6 | 2023-07-14 | DOL | RTDOLD2 | DÓLAR B3 SPOT - 2 DIAS | 4.7905 |
7 | 2023-07-14 | DOL | RTDOLT1 | PTAX800 VENDA | 4.7957 |
Como podemos observar, para cada data de referência temos 4 cotações de dólar:
- Dólar cupom limpo cálculado pela B3 a partir do casado
- Dólar spot para liquidação em D+1
- Dólar spot para liquidação em D+2
- PTAX800 cotação de venda
Precisamos pegar a cotação da PTAX800 (apenas PTAX) do dia anterior para calcular o cupom sujo. O symbol
da PTAX, de acordo com a tabela anterior, é RTDOLT1
. Assim vamos filtrar por este símbolo na data do dia anterior. Para calcular o cupom limpo utilizamos o dólar cupom limpo, filtrando pelo símbolo RTDOLCL
.
Antes de filtrar vou criar uma função para converter ql.Date
para datetime.datetime
. Com o pandas é mais fácil trabalhar com datetime
.
def ql_to_datetime(d):
# reference: https://stackoverflow.com/questions/45087828/python-quantlib-convert-quantlib-date-to-datetime
# datetime.datetime has methods fromordinal() and toordinal() to convert between datetime.datetime and an integer
# that represents the date.
# ql.Date also has the method serialNumber() to convert ql.Date to integer and its constructor accepts the integer
# as well.
# datetime.datetime uses 0001-01-01 as 1 with increments in days
# ql.Date uses 1899-12-31 as 0
return datetime.fromordinal(d.serialNumber() + datetime(1899, 12, 31).toordinal() - 1)
Para seguir com o cálculo do cupom cambial vamos juntar os dados de futuros de taxa DI com os futuros de dólar.
= fut_dol.merge(fut_di1, on=["refdate", "maturity_date", "business_days"], suffixes=("_dol", "_di1")) fut
Agora sim, procedemos com o cálculo do cupom sujo. A curva de cupom cambial tem os vértices nas datas da coluna fixing, que são as data onde o fluxo de caixa, transferência de recursos aconte. Aqui o fixing é no dia útil após o vencimento. Essa abordagem é diferente do que é implementado pela B3, que utiliza as data de vencimento como vértices da curva. O cálculo do cupom cambial sujo é dado pela fórmula.
\[ \left( 1 + c_{v}(t) \cdot \frac{DC_{v}(t)}{360} \right) = \frac{ \frac{100000}{PUDI1_{v}(t)} }{ \frac{PUDOL_{v}(t)}{PTAX_{t-1} \cdot 1000} } \]
\(DC_{v}(t)\) são os dias corridos da data de referência até a data de fixing.
= calendar.advance(today, -1, ql.Days)
yesterday = df_econ_ind.loc[(df_econ_ind["refdate"] == ql_to_datetime(yesterday)) & (df_econ_ind["symbol"] == "RTDOLT1"), "price"].item()
ptax_1 = 100000 / fut["settlement_price_di1"]
di1_factor = fut["settlement_price_dol"] / (ptax_1 * 1000)
dol_factor = [calendar.advance(d, 1, ql.Days)
fixing for d in [calendar.adjust(ql.Date.from_date(d), ql.Following)
for d in fut["maturity_date"]]]
= [calendar_act.businessDaysBetween(today, d) for d in fixing]
dc "fixing"] = [ql_to_datetime(d) for d in fixing]
fut["cupom_sujo"] = (di1_factor / dol_factor - 1) * 360 / dc fut[
O cálculo do cupom cambial limpo é dado pela fórmula.
\[ \left( 1 + c_{v}(t) \cdot \frac{DC_{v}^{CL}(t)}{360} \right) = \frac{ \frac{100000}{PUDI1_{v}(t)}\frac{1}{(1 + CDI_t)^{1/252}} }{ \frac{PUDOL_{v}(t)}{DOLCL_{t-1} \cdot 1000} } \]
\(DC_{v}^{CL}(t)\) são os dias corridos da data de referência, mais 2 dias úteis, até a data de fixing. A motivação para essa mudança é que o dólar cupom limpo é liquidado em 2 dias úteis, portanto, a curva deve começar nesta data. Por este motivo, também é necessário retirar 1 dia de CDI do fator da taxa DI.
= df_econ_ind.loc[(df_econ_ind["refdate"] == refdate) & (df_econ_ind["symbol"] == "RTDOLCL"), "price"].item()
spot = (100000 / fut["settlement_price_di1"]) / ((1 + cdi) ** (1/252))
di1_factor = fut["settlement_price_dol"] / (spot * 1000)
dol_factor = [calendar_act.businessDaysBetween(calendar.advance(today, 2, ql.Days), ql.Date.from_date(d)) for d in fut["fixing"]]
dc "cupom_limpo"] = (di1_factor / dol_factor - 1) * 360 / dc
fut["dc_limpo"] = dc fut[
Para termos de comparação do shape da estrutura a termos vamos fazer um gráfico com a curva americana de Treasuries.
= pd.DataFrame({
us_curve "dc": pd.Series([1, 2, 3, 4, 6, 12, 24, 36, 60, 72, 120, 240, 360]) * 30,
"rate": pd.Series([5.37, 5.49, 5.49, 5.53, 5.52, 5.34, 4.74, 4.35, 4.04, 3.94, 3.83, 4.11, 3.93]) / 100,
})"date"] = [ql_to_datetime(calendar_act.advance(today, d, ql.Days)) for d in us_curve["dc"]] us_curve[
= fut[["fixing", "cupom_limpo", "cupom_sujo"]].set_index("fixing").plot()
ax "date", "rate"]].query("date <= '2027-01-01'").set_index("date").plot(ax=ax) us_curve[[
<Axes: xlabel='date'>