PYield 0.27.0__tar.gz → 0.27.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyield-0.27.0 → pyield-0.27.2}/PKG-INFO +1 -1
- pyield-0.27.2/pyield/__about__.py +1 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/__init__.py +8 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/anbima/imaq.py +12 -7
- {pyield-0.27.0 → pyield-0.27.2}/pyield/bc/auction.py +9 -11
- pyield-0.27.2/pyield/bc/repos.py +194 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/indicators.py +118 -106
- pyield-0.27.0/pyield/__about__.py +0 -1
- {pyield-0.27.0 → pyield-0.27.2}/.gitignore +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/LICENSE +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/README.md +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/anbima/__init__.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/anbima/ima.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/anbima/tpf.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/__init__.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/common.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/di.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/historical.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/intraday.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/xml.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/bday.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/conftest.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/data_cache.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/date_converter.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/holidays/__init__.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/holidays/br_holidays_new.txt +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/holidays/br_holidays_old.txt +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/holidays/brholidays.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/interpolator.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/projections.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/py.typed +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/tools.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/__init__.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/lft.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/ltn.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/ntnb.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/ntnf.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/tools.py +0 -0
- {pyield-0.27.0 → pyield-0.27.2}/pyproject.toml +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.27.2"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
from pyield import bday
|
|
2
4
|
from pyield.__about__ import __version__
|
|
3
5
|
from pyield.anbima.ima import ima
|
|
@@ -6,6 +8,7 @@ from pyield.anbima.tpf import anbima_tpf_data, anbima_tpf_rates, tpf_pre_maturit
|
|
|
6
8
|
from pyield.b3_futures import futures
|
|
7
9
|
from pyield.b3_futures.di import DIFutures
|
|
8
10
|
from pyield.bc.auction import auctions
|
|
11
|
+
from pyield.bc.repos import repos
|
|
9
12
|
from pyield.indicators import indicator
|
|
10
13
|
from pyield.interpolator import Interpolator
|
|
11
14
|
from pyield.projections import projection
|
|
@@ -18,6 +21,7 @@ __all__ = [
|
|
|
18
21
|
"anbima_tpf_rates",
|
|
19
22
|
"tpf_pre_maturities",
|
|
20
23
|
"auctions",
|
|
24
|
+
"repos",
|
|
21
25
|
"bday",
|
|
22
26
|
"DIFutures",
|
|
23
27
|
"forward_rates",
|
|
@@ -32,3 +36,7 @@ __all__ = [
|
|
|
32
36
|
"ntnf",
|
|
33
37
|
"projection",
|
|
34
38
|
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Configura o logger do pacote principal com um NullHandler
|
|
42
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
@@ -10,6 +10,10 @@ from bs4 import XMLParsedAsHTMLWarning
|
|
|
10
10
|
from pyield import date_converter as dc
|
|
11
11
|
from pyield.date_converter import DateScalar
|
|
12
12
|
|
|
13
|
+
# Configura o logger do módulo
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
13
17
|
IMA_URL = "https://www.anbima.com.br/informacoes/ima/ima-quantidade-mercado.asp"
|
|
14
18
|
COLUMN_MAPPING = {
|
|
15
19
|
"Data Referência": "Date",
|
|
@@ -42,7 +46,7 @@ def _fetch_url_tables(target_date: pd.Timestamp) -> pd.DataFrame:
|
|
|
42
46
|
r.raise_for_status()
|
|
43
47
|
r.encoding = "iso-8859-1"
|
|
44
48
|
if "Não há dados disponíveis" in r.text:
|
|
45
|
-
|
|
49
|
+
logger.warning(f"No data for {target_date_str}. Returning empty DataFrame.")
|
|
46
50
|
return pd.DataFrame()
|
|
47
51
|
|
|
48
52
|
string_io_buffer = io.StringIO(r.text)
|
|
@@ -80,7 +84,7 @@ def _fetch_url_tables(target_date: pd.Timestamp) -> pd.DataFrame:
|
|
|
80
84
|
anbima_date_str = match.group(1)
|
|
81
85
|
anbima_date = pd.to_datetime(anbima_date_str, format="%d/%m/%Y")
|
|
82
86
|
else:
|
|
83
|
-
|
|
87
|
+
logger.warning(f"No data for {target_date_str}. Returning empty DataFrame.")
|
|
84
88
|
return pd.DataFrame()
|
|
85
89
|
|
|
86
90
|
df["Data Referência"] = anbima_date
|
|
@@ -133,10 +137,11 @@ def imaq(date: DateScalar) -> pd.DataFrame:
|
|
|
133
137
|
try:
|
|
134
138
|
date = dc.convert_input_dates(date)
|
|
135
139
|
df = _fetch_url_tables(date)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
if df.empty:
|
|
141
|
+
return df
|
|
142
|
+
return _process_df(df)
|
|
143
|
+
except Exception: # Erro inesperado
|
|
139
144
|
date_str = date.strftime("%d/%m/%Y")
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
msg = f"Error fetching IMA for {date_str}. Returning empty DataFrame."
|
|
146
|
+
logger.exception(msg)
|
|
142
147
|
return pd.DataFrame()
|
|
@@ -141,8 +141,8 @@ def _add_dv01(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
141
141
|
df_ltn = df_is_accepted.query("BondType == 'LTN'").reset_index(drop=True)
|
|
142
142
|
if not df_ltn.empty:
|
|
143
143
|
df_ltn["Duration"] = df_ltn["BDToMat"] / 252
|
|
144
|
-
|
|
145
|
-
df_ltn["DV01FR"] = 0.0001 *
|
|
144
|
+
mduration = df_ltn["Duration"] / (1 + df_ltn["AvgRate"])
|
|
145
|
+
df_ltn["DV01FR"] = 0.0001 * mduration * df_ltn["AvgPrice"]
|
|
146
146
|
|
|
147
147
|
def compute_f_duration(row):
|
|
148
148
|
return duration_f(row["Date"], row["Maturity"], row["CutRate"])
|
|
@@ -151,8 +151,8 @@ def _add_dv01(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
151
151
|
if not df_ntnf.empty:
|
|
152
152
|
df_ntnf["Duration"] = df_ntnf.apply(compute_f_duration, axis=1)
|
|
153
153
|
df_ntnf["Duration"] = df_ntnf["Duration"].astype("Float64")
|
|
154
|
-
|
|
155
|
-
df_ntnf["DV01FR"] = 0.0001 *
|
|
154
|
+
mduration = df_ntnf["Duration"] / (1 + df_ntnf["AvgRate"])
|
|
155
|
+
df_ntnf["DV01FR"] = 0.0001 * mduration * df_ntnf["AvgPrice"]
|
|
156
156
|
|
|
157
157
|
def compute_b_duration(row):
|
|
158
158
|
return duration_b(row["Date"], row["Maturity"], row["CutRate"])
|
|
@@ -161,8 +161,8 @@ def _add_dv01(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
161
161
|
if not df_ntnb.empty:
|
|
162
162
|
df_ntnb["Duration"] = df_ntnb.apply(compute_b_duration, axis=1)
|
|
163
163
|
df_ntnb["Duration"] = df_ntnb["Duration"].astype("Float64")
|
|
164
|
-
|
|
165
|
-
df_ntnb["DV01FR"] = 0.0001 *
|
|
164
|
+
mduration = df_ntnb["Duration"] / (1 + df_ntnb["AvgRate"])
|
|
165
|
+
df_ntnb["DV01FR"] = 0.0001 * mduration * df_ntnb["AvgPrice"]
|
|
166
166
|
|
|
167
167
|
df = pd.concat([df_not_accepted, df_lft, df_ltn, df_ntnf, df_ntnb])
|
|
168
168
|
|
|
@@ -181,6 +181,8 @@ def _sort_and_reorder_columns(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
181
181
|
"Buyer",
|
|
182
182
|
"BondType",
|
|
183
183
|
"Maturity",
|
|
184
|
+
"BDToMat",
|
|
185
|
+
"Duration",
|
|
184
186
|
"SelicCode",
|
|
185
187
|
"Value",
|
|
186
188
|
"ValueFR",
|
|
@@ -191,14 +193,11 @@ def _sort_and_reorder_columns(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
191
193
|
"AcceptedQuantity",
|
|
192
194
|
"AcceptedQuantityFR",
|
|
193
195
|
"AcceptedQuantitySR",
|
|
196
|
+
"DV01FR",
|
|
194
197
|
"AvgPrice",
|
|
195
198
|
"CutPrice",
|
|
196
199
|
"AvgRate",
|
|
197
200
|
"CutRate",
|
|
198
|
-
"BDToMat",
|
|
199
|
-
"Duration",
|
|
200
|
-
"MDuration",
|
|
201
|
-
"DV01FR",
|
|
202
201
|
]
|
|
203
202
|
|
|
204
203
|
primary_sort_keys = ["Date", "AuctionType", "BondType", "Maturity"]
|
|
@@ -304,7 +303,6 @@ def auctions(
|
|
|
304
303
|
- CutRate: Taxa de corte.
|
|
305
304
|
- BDToMat: Dias úteis até o vencimento.
|
|
306
305
|
- Duration: Duration (Duração) do título.
|
|
307
|
-
- MDuration: Modified Duration (Duração Modificada) do título.
|
|
308
306
|
- DV01FR: Dollar Value of 1 bp change in yield - Primeira Rodada (FR) em R$.
|
|
309
307
|
"""
|
|
310
308
|
url = BASE_API_URL
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Documentação da API do BC
|
|
3
|
+
https://olinda.bcb.gov.br/olinda/servico/leiloes_selic/versao/v1/aplicacao#!/recursos/leiloes_compromissadas
|
|
4
|
+
Exemplo de chamada:
|
|
5
|
+
https://olinda.bcb.gov.br/olinda/servico/leiloes_selic/versao/v1/odata/leiloes_compromissadas(dataLancamentoInicio=@dataLancamentoInicio,dataLancamentoFim=@dataLancamentoFim,horaInicio=@horaInicio,dataLiquidacao=@dataLiquidacao,dataRetorno=@dataRetorno,publicoPermitidoLeilao=@publicoPermitidoLeilao,nomeTipoOferta=@nomeTipoOferta)?@dataLancamentoInicio='2025-02-13'&@dataLancamentoFim='2025-02-13'&$top=100&$format=text/csv
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from pyield import bday
|
|
15
|
+
from pyield import date_converter as dc
|
|
16
|
+
from pyield.date_converter import DateScalar
|
|
17
|
+
|
|
18
|
+
"""Dicionário com o mapeamento das colunas da API do BC para o DataFrame final
|
|
19
|
+
Chaves com comentário serão descartadas ao final do processamento
|
|
20
|
+
A ordem das chaves será a ordem das colunas no DataFrame final"""
|
|
21
|
+
COLUMN_MAPPING = {
|
|
22
|
+
# "id": "ID",
|
|
23
|
+
"dataMovimento": "Date",
|
|
24
|
+
"horaInicio": "StartTime",
|
|
25
|
+
"publicoPermitidoLeilao": "AllowedParticipants", # ['SomenteDealer', 'TodoMercado']
|
|
26
|
+
"numeroComunicado": "CommunicationNumber",
|
|
27
|
+
"nomeTipoOferta": "OfferType",
|
|
28
|
+
# "ofertante": "Offerer", # Only Banco Central can offer
|
|
29
|
+
"prazoDiasCorridos": "CDToMat",
|
|
30
|
+
"dataLiquidacao": "Settlement",
|
|
31
|
+
"dataRetorno": "Maturity",
|
|
32
|
+
"volumeAceito": "AcceptedVolume",
|
|
33
|
+
"taxaCorte": "CutRate",
|
|
34
|
+
"percentualCorte": "CutPct",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
BASE_API_URL = "https://olinda.bcb.gov.br/olinda/servico/leiloes_selic/versao/v1/odata/leiloes_compromissadas(dataLancamentoInicio=@dataLancamentoInicio,dataLancamentoFim=@dataLancamentoFim,horaInicio=@horaInicio,dataLiquidacao=@dataLiquidacao,dataRetorno=@dataRetorno,publicoPermitidoLeilao=@publicoPermitidoLeilao,nomeTipoOferta=@nomeTipoOferta)?"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_from_url(url: str) -> pd.DataFrame:
|
|
41
|
+
response = requests.get(url, timeout=10)
|
|
42
|
+
response.raise_for_status()
|
|
43
|
+
return pd.read_csv(
|
|
44
|
+
io.StringIO(response.text),
|
|
45
|
+
dtype_backend="numpy_nullable",
|
|
46
|
+
decimal=",",
|
|
47
|
+
date_format="%Y-%m-%d",
|
|
48
|
+
parse_dates=["dataMovimento", "dataLiquidacao", "dataRetorno"],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _pre_process_df(df: pd.DataFrame) -> pd.DataFrame:
|
|
53
|
+
# Remover colunas que não serão utilizadas
|
|
54
|
+
keep_columns = [col for col in COLUMN_MAPPING.keys() if col in df.columns]
|
|
55
|
+
return df[keep_columns].rename(columns=COLUMN_MAPPING)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _process_df(df: pd.DataFrame) -> pd.DataFrame:
|
|
59
|
+
df["StartTime"] = pd.to_datetime(df["StartTime"], format="%H:%M").dt.time
|
|
60
|
+
# AcceptedVolume é em milhares de reais -> converter para reais
|
|
61
|
+
df["AcceptedVolume"] = (1_000 * df["AcceptedVolume"]).round(0).astype("Int64")
|
|
62
|
+
|
|
63
|
+
# Remove the percentage sign and round to 6 decimal places (4 decimal places in %)
|
|
64
|
+
df["CutRate"] = (df["CutRate"] / 100).round(6)
|
|
65
|
+
# df["BDtoReturn"] = bday.count(df["Date"], df["ReturnDate"])
|
|
66
|
+
df["BDtoMat"] = bday.count(df["Date"], df["Maturity"])
|
|
67
|
+
return df
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _adjust_null_values(df: pd.DataFrame) -> pd.DataFrame:
|
|
71
|
+
# Onde não há volume aceito, os valores de corte devem ser nulos
|
|
72
|
+
is_accepted = df["AcceptedVolume"] != 0
|
|
73
|
+
df["CutRate"] = df["CutRate"].where(is_accepted, pd.NA)
|
|
74
|
+
df["CutPct"] = df["CutPct"].where(is_accepted, pd.NA)
|
|
75
|
+
return df
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _sort_and_reorder_columns(df: pd.DataFrame) -> pd.DataFrame:
|
|
79
|
+
column_sequence = [
|
|
80
|
+
"Date",
|
|
81
|
+
"Settlement",
|
|
82
|
+
"Maturity",
|
|
83
|
+
"CDToMat",
|
|
84
|
+
"BDtoMat",
|
|
85
|
+
"StartTime",
|
|
86
|
+
"AllowedParticipants",
|
|
87
|
+
"CommunicationNumber",
|
|
88
|
+
"OfferType",
|
|
89
|
+
"AcceptedVolume",
|
|
90
|
+
"CutRate",
|
|
91
|
+
"CutPct",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
primary_sort_keys = ["Date", "StartTime", "OfferType"]
|
|
95
|
+
return df[column_sequence].sort_values(by=primary_sort_keys).reset_index(drop=True)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _fetch_df_from_url(url: str) -> pd.DataFrame:
|
|
99
|
+
try:
|
|
100
|
+
df = _load_from_url(url)
|
|
101
|
+
if df.empty:
|
|
102
|
+
logging.warning("No auction data found for the specified period.")
|
|
103
|
+
return pd.DataFrame()
|
|
104
|
+
df = _pre_process_df(df)
|
|
105
|
+
df = _adjust_null_values(df)
|
|
106
|
+
df = _process_df(df)
|
|
107
|
+
df = _sort_and_reorder_columns(df)
|
|
108
|
+
return df
|
|
109
|
+
except Exception:
|
|
110
|
+
logging.exception("Error fetching auction data from BC API.")
|
|
111
|
+
return pd.DataFrame()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def repos(
|
|
115
|
+
start: DateScalar | None = None,
|
|
116
|
+
end: DateScalar | None = None,
|
|
117
|
+
) -> pd.DataFrame:
|
|
118
|
+
"""
|
|
119
|
+
Recupera dados de leilões para um determinado período e tipo de leilão da API do BC.
|
|
120
|
+
|
|
121
|
+
**Consultas de Período:**
|
|
122
|
+
- Para consultar dados de um intervalo, forneça as datas de `start` e `end`.
|
|
123
|
+
Exemplo: `auctions(start='2024-10-20', end='2024-10-27')`
|
|
124
|
+
- Se apenas `start` for fornecido, a API do BC retornará dados de leilão a partir
|
|
125
|
+
da data de `start` **até a data mais recente disponível**.
|
|
126
|
+
Exemplo: `auctions(start='2024-10-20')`
|
|
127
|
+
- Se apenas `end` for fornecido, a API do BC retornará dados de leilão **desde a
|
|
128
|
+
data mais antiga disponível até a data de `end`**.
|
|
129
|
+
Exemplo: `auctions(end='2024-10-27')`
|
|
130
|
+
|
|
131
|
+
**Série Histórica Completa:**
|
|
132
|
+
- Para recuperar a série histórica completa de leilões (desde 12/11/2012 até o
|
|
133
|
+
último dia útil), chame a função sem fornecer os parâmetros `start` e `end`.
|
|
134
|
+
Exemplo: `auctions()`
|
|
135
|
+
|
|
136
|
+
Busca dados de leilões da API do BC para as datas de início e fim especificadas,
|
|
137
|
+
filtrando os resultados diretamente na API pelo tipo de leilão, se especificado.
|
|
138
|
+
O comportamento da função em relação aos parâmetros `start` e `end` segue o padrão
|
|
139
|
+
da API do Banco Central:
|
|
140
|
+
- Se `start` for fornecido e `end` não, a função retorna dados de `start` até o fim.
|
|
141
|
+
- Se `end` for fornecido e `start` não, a API retorna dados do início até `end`.
|
|
142
|
+
- Se ambos `start` e `end` forem omitidos, a API retorna a série histórica completa.
|
|
143
|
+
|
|
144
|
+
Os dados podem ser filtrados pelo tipo de leilão especificado ("Sell" ou "Buy").
|
|
145
|
+
Leilões de "Sell" são aqueles em que o Tesouro Nacional vende títulos ao mercado.
|
|
146
|
+
Leilões de "Buy" são aqueles em que o Tesouro Nacional compra títulos do mercado.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
start (DateScalar, opcional): A data de início para a consulta dos leilões.
|
|
150
|
+
Se `start` for fornecido e `end` for `None`, a API retornará dados de
|
|
151
|
+
leilão a partir de `start` até a data mais recente disponível.
|
|
152
|
+
Se `start` e `end` forem `None`, a série histórica completa será retornada.
|
|
153
|
+
Padrão é `None`.
|
|
154
|
+
end (DateScalar, opcional): A data de fim para a consulta de dados de leilão.
|
|
155
|
+
Se `end` for fornecido e `start` for `None`, a API retornará dados de
|
|
156
|
+
leilão desde a data mais antiga disponível até a data de `end`.
|
|
157
|
+
Se `start` e `end` forem `None`, a série histórica completa será retornada.
|
|
158
|
+
Padrão é `None`.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
pd.DataFrame: Um DataFrame contendo dados de leilões para o período e tipo
|
|
162
|
+
especificados. Em caso de erro ao buscar os dados, um DataFrame vazio
|
|
163
|
+
é retornado e uma mensagem de erro é registrada no log.
|
|
164
|
+
|
|
165
|
+
Notes:
|
|
166
|
+
O DataFrame possui as seguintes colunas:
|
|
167
|
+
- Date: Data do leilão.
|
|
168
|
+
- Settlement: Data de liquidação do leilão.
|
|
169
|
+
- Maturity: Data de retorno do leilão.
|
|
170
|
+
- CDToMat: Prazo em dias corridos até o vencimento.
|
|
171
|
+
- BDtoMat: Prazo em dias úteis até o vencimento.
|
|
172
|
+
- StartTime: Hora de início do leilão.
|
|
173
|
+
- AllowedParticipants: Participantes permitidos no leilão.
|
|
174
|
+
- CommunicationNumber: Número do comunicado.
|
|
175
|
+
- OfferType: Tipo de oferta do leilão.
|
|
176
|
+
- AcceptedVolume: Volume aceito no leilão (em R$).
|
|
177
|
+
- CutRate: Taxa de corte do leilão.
|
|
178
|
+
- CutPct: Percentual de corte do leilão.
|
|
179
|
+
|
|
180
|
+
"""
|
|
181
|
+
url = BASE_API_URL
|
|
182
|
+
if start:
|
|
183
|
+
start = dc.convert_input_dates(start)
|
|
184
|
+
start_str = start.strftime("%Y-%m-%d")
|
|
185
|
+
url += f"@dataLancamentoInicio='{start_str}'"
|
|
186
|
+
|
|
187
|
+
if end:
|
|
188
|
+
end = dc.convert_input_dates(end)
|
|
189
|
+
end_str = end.strftime("%Y-%m-%d")
|
|
190
|
+
url += f"&@dataLancamentoFim='{end_str}'"
|
|
191
|
+
|
|
192
|
+
url += "&$format=text/csv" # Adiciona o formato CSV ao final
|
|
193
|
+
|
|
194
|
+
return _fetch_df_from_url(url)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import io
|
|
1
2
|
import logging
|
|
2
3
|
import time
|
|
3
|
-
from typing import Literal
|
|
4
|
+
from typing import Callable, Literal
|
|
4
5
|
|
|
5
6
|
import pandas as pd
|
|
6
7
|
import requests
|
|
@@ -9,8 +10,11 @@ from requests.exceptions import RequestException
|
|
|
9
10
|
from pyield import date_converter as dc
|
|
10
11
|
from pyield.date_converter import DateScalar
|
|
11
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Timeout para as requisições HTTP (conexão e leitura)
|
|
12
16
|
TIMEOUT = (5, 20)
|
|
13
|
-
MAX_ATTEMPTS =
|
|
17
|
+
MAX_ATTEMPTS = 10
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
def indicator(
|
|
@@ -91,20 +95,15 @@ def ipca_monthly_rate(reference_date: pd.Timestamp) -> float:
|
|
|
91
95
|
# Construct the API URL using the formatted date
|
|
92
96
|
api_url = f"https://servicodados.ibge.gov.br/api/v3/agregados/6691/periodos/{ipca_date}/variaveis/63?localidades=N1[all]"
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
data = response.json()
|
|
98
|
+
def process_ipca_response(data):
|
|
99
|
+
if not data:
|
|
100
|
+
msg = f"No data available for IPCA Monthly Rate on {reference_date}"
|
|
101
|
+
logger.warning(msg)
|
|
102
|
+
return float("nan")
|
|
103
|
+
ipca_str = data[0]["resultados"][0]["series"][0]["serie"][ipca_date]
|
|
104
|
+
return round(float(ipca_str) / 100, 4)
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
raise ValueError("No data available for the specified date")
|
|
105
|
-
# Extract and return the IPCA monthly growth rate if data is available
|
|
106
|
-
ipca_str = data[0]["resultados"][0]["series"][0]["serie"][ipca_date]
|
|
107
|
-
return round(float(ipca_str) / 100, 4)
|
|
106
|
+
return _fetch_with_retry(api_url, "IPCA Monthly Rate", process_ipca_response, True)
|
|
108
107
|
|
|
109
108
|
|
|
110
109
|
def _selic_target(reference_date: pd.Timestamp) -> float:
|
|
@@ -122,31 +121,21 @@ def _selic_target(reference_date: pd.Timestamp) -> float:
|
|
|
122
121
|
"""
|
|
123
122
|
# https://api.bcb.gov.br/dados/serie/bcdata.sgs.432/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
|
|
124
123
|
selic_date = reference_date.strftime("%d/%m/%Y")
|
|
125
|
-
api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.432/dados?formato=
|
|
126
|
-
|
|
127
|
-
attempt = 0
|
|
128
|
-
while attempt < MAX_ATTEMPTS:
|
|
129
|
-
try:
|
|
130
|
-
response = requests.get(api_url, timeout=TIMEOUT)
|
|
131
|
-
response.raise_for_status() # Raise an error for bad HTTP response
|
|
132
|
-
|
|
133
|
-
# Try parsing JSON, raises an error if response is not valid JSON
|
|
134
|
-
data = response.json()
|
|
135
|
-
|
|
136
|
-
# Access the value directly, any issue will raise an exception
|
|
137
|
-
value = data[0]["valor"]
|
|
138
|
-
return round(float(value) / 100, 4)
|
|
139
|
-
|
|
140
|
-
except (RequestException, KeyError, IndexError, ValueError) as e:
|
|
141
|
-
msg = f"Error fetching SELIC Target rate (attempt {attempt + 1}): {e}"
|
|
142
|
-
logging.error(msg)
|
|
124
|
+
api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.432/dados?formato=csv&dataInicial={selic_date}&dataFinal={selic_date}"
|
|
143
125
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
126
|
+
def process_selic_target_response(data):
|
|
127
|
+
csv_data = io.StringIO(data)
|
|
128
|
+
df = pd.read_csv(csv_data, sep=";", decimal=",")
|
|
129
|
+
if df.empty or "valor" not in df.columns:
|
|
130
|
+
msg = f"No data available for SELIC Target rate on {reference_date}"
|
|
131
|
+
logger.warning(msg)
|
|
132
|
+
return float("nan")
|
|
133
|
+
value = float(df["valor"].iloc[0] / 100) # SELIC Target rate
|
|
134
|
+
return round(float(value), 4)
|
|
147
135
|
|
|
148
|
-
|
|
149
|
-
|
|
136
|
+
return _fetch_with_retry(
|
|
137
|
+
api_url, "SELIC Target", process_selic_target_response, False
|
|
138
|
+
)
|
|
150
139
|
|
|
151
140
|
|
|
152
141
|
def _selic_over(reference_date: pd.Timestamp) -> float:
|
|
@@ -166,99 +155,122 @@ def _selic_over(reference_date: pd.Timestamp) -> float:
|
|
|
166
155
|
|
|
167
156
|
formatted_date = reference_date.strftime("%d/%m/%Y")
|
|
168
157
|
# https://api.bcb.gov.br/dados/serie/bcdata.sgs.1178/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
|
|
169
|
-
api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.1178/dados?formato=
|
|
158
|
+
api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.1178/dados?formato=csv&dataInicial={formatted_date}&dataFinal={formatted_date}"
|
|
170
159
|
|
|
171
|
-
|
|
160
|
+
def process_selic_over_response(data):
|
|
161
|
+
csv_data = io.StringIO(data)
|
|
162
|
+
df = pd.read_csv(csv_data, sep=";", decimal=",")
|
|
163
|
+
if df.empty or "valor" not in df.columns:
|
|
164
|
+
msg = f"No data available for SELIC Over rate on {reference_date}"
|
|
165
|
+
logger.warning(msg)
|
|
166
|
+
return float("nan")
|
|
167
|
+
value = float(df["valor"].iloc[0] / 100) # SELIC Over daily rate
|
|
168
|
+
return round(value, 4)
|
|
172
169
|
|
|
173
|
-
|
|
174
|
-
try:
|
|
175
|
-
response = requests.get(api_url, timeout=TIMEOUT)
|
|
176
|
-
response.raise_for_status() # Handle 4xx/5xx HTTP errors
|
|
170
|
+
return _fetch_with_retry(api_url, "SELIC Over", process_selic_over_response, False)
|
|
177
171
|
|
|
178
|
-
# Try parsing JSON, raises an error if response is not valid JSON
|
|
179
|
-
data = response.json()
|
|
180
172
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
173
|
+
def _di(reference_date: pd.Timestamp) -> float:
|
|
174
|
+
# https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
|
|
175
|
+
di_date = reference_date.strftime("%d/%m/%Y")
|
|
176
|
+
api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=csv&dataInicial={di_date}&dataFinal={di_date}"
|
|
184
177
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
178
|
+
def process_di_response(data):
|
|
179
|
+
csv_data = io.StringIO(data)
|
|
180
|
+
df = pd.read_csv(csv_data, sep=";", decimal=",")
|
|
181
|
+
if df.empty or "valor" not in df.columns:
|
|
182
|
+
logger.warning(f"No data available for DI rate on {reference_date}")
|
|
183
|
+
return float("nan")
|
|
184
|
+
value = float(df["valor"].iloc[0] / 100) # DI daily rate
|
|
185
|
+
return round((1 + value) ** 252 - 1, 4) # Annualize the daily rate
|
|
188
186
|
|
|
189
|
-
|
|
190
|
-
attempt += 1
|
|
191
|
-
time.sleep(1.5**attempt) # Exponential backoff: 1.5, 2.3, 3.9, 5.1, 7.6 seconds
|
|
187
|
+
return _fetch_with_retry(api_url, "DI Over", process_di_response, False)
|
|
192
188
|
|
|
193
|
-
# After all retries, raise an error
|
|
194
|
-
raise ValueError("Failed to fetch SELIC Over rate after maximum attempts")
|
|
195
189
|
|
|
190
|
+
def _vna_lft(reference_date: pd.Timestamp) -> float:
|
|
191
|
+
# url example: https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/20240418APC238
|
|
192
|
+
url_base = "https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/"
|
|
193
|
+
url_file = f"{reference_date.strftime('%Y%m%d')}APC238"
|
|
194
|
+
url_vna = url_base + url_file
|
|
196
195
|
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
response = requests.get(api_url, timeout=TIMEOUT)
|
|
202
|
-
response.raise_for_status()
|
|
196
|
+
def _process_vna_response(text: str) -> float:
|
|
197
|
+
# Finding the part that contains the table
|
|
198
|
+
start_of_table = text.find("EMISSAO")
|
|
199
|
+
end_of_table = text.find("99999999*")
|
|
203
200
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
value = float(data[0]["valor"]) / 100 # DI daily rate
|
|
208
|
-
# Annualize the daily rate
|
|
209
|
-
return round((1 + value) ** 252 - 1, 4)
|
|
201
|
+
# Extracting the table
|
|
202
|
+
table_text = text[start_of_table:end_of_table].strip()
|
|
203
|
+
table_lines = table_text.splitlines()
|
|
210
204
|
|
|
205
|
+
# Remove empty lines
|
|
206
|
+
table_lines = [line.strip() for line in table_lines if line.strip()]
|
|
211
207
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
start_of_table = text.find("EMISSAO")
|
|
215
|
-
end_of_table = text.find("99999999*")
|
|
208
|
+
# Remove first line
|
|
209
|
+
body_lines = table_lines[1:]
|
|
216
210
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
211
|
+
vnas = []
|
|
212
|
+
for line in body_lines:
|
|
213
|
+
vna_str = line.split()[-1].replace(",", ".")
|
|
214
|
+
vnas.append(float(vna_str))
|
|
220
215
|
|
|
221
|
-
|
|
222
|
-
|
|
216
|
+
# Raise error if all values are not the same
|
|
217
|
+
vna_value = vnas[0]
|
|
218
|
+
if any(vna_value != vna for vna in vnas):
|
|
219
|
+
bcb_url = "https://www.bcb.gov.br/estabilidadefinanceira/selicbaixar"
|
|
220
|
+
msg = f"VNA values are not the same. Please check data at {bcb_url}"
|
|
221
|
+
raise ValueError(msg)
|
|
223
222
|
|
|
224
|
-
|
|
225
|
-
body_lines = table_lines[1:]
|
|
223
|
+
return vna_value
|
|
226
224
|
|
|
227
|
-
|
|
228
|
-
for line in body_lines:
|
|
229
|
-
vna_str = line.split()[-1].replace(",", ".")
|
|
230
|
-
vnas.append(float(vna_str))
|
|
225
|
+
return _fetch_with_retry(url_vna, "VNA LFT", _process_vna_response, False)
|
|
231
226
|
|
|
232
|
-
# Raise error if all values are not the same
|
|
233
|
-
vna_value = vnas[0]
|
|
234
|
-
if any(vna_value != vna for vna in vnas):
|
|
235
|
-
bcb_url = "https://www.bcb.gov.br/estabilidadefinanceira/selicbaixar"
|
|
236
|
-
raise ValueError(f"VNA values are not the same. Please check data at {bcb_url}")
|
|
237
227
|
|
|
238
|
-
|
|
228
|
+
def _fetch_with_retry(
|
|
229
|
+
api_url: str,
|
|
230
|
+
error_message: str,
|
|
231
|
+
process_response: Callable[[requests.Response], float],
|
|
232
|
+
use_json: bool = True,
|
|
233
|
+
) -> float:
|
|
234
|
+
"""
|
|
235
|
+
Função genérica para fazer requisições HTTP com retentativas e tratamento de erro.
|
|
239
236
|
|
|
237
|
+
Args:
|
|
238
|
+
api_url (str): URL da API a ser chamada.
|
|
239
|
+
error_message (str): Mensagem de erro específica para a função que está usando.
|
|
240
|
+
process_response (Callable[[str], float]): Função para processar os dados da
|
|
241
|
+
resposta (JSON decodificado ou texto, dependendo de 'use_json').
|
|
242
|
+
Deve retornar um float.
|
|
243
|
+
use_json (bool): Se True, tenta decodificar a resposta como JSON antes de passar
|
|
244
|
+
para `process_response`. Se False, passa a resposta como texto.
|
|
240
245
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
url_base = "https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/"
|
|
244
|
-
url_file = f"{reference_date.strftime('%Y%m%d')}APC238"
|
|
245
|
-
url_vna = url_base + url_file
|
|
246
|
-
session = requests.Session()
|
|
246
|
+
Returns:
|
|
247
|
+
float: O resultado do processamento da resposta.
|
|
247
248
|
|
|
249
|
+
Raises:
|
|
250
|
+
ValueError: Se a requisição falhar após o número máximo de tentativas.
|
|
251
|
+
"""
|
|
248
252
|
attempt = 0
|
|
249
253
|
while attempt < MAX_ATTEMPTS:
|
|
250
254
|
try:
|
|
251
|
-
response =
|
|
252
|
-
response.raise_for_status()
|
|
253
|
-
|
|
255
|
+
response = requests.get(api_url, timeout=TIMEOUT)
|
|
256
|
+
response.raise_for_status() # Levanta uma exceção para códigos de erro HTTP
|
|
257
|
+
|
|
258
|
+
if use_json:
|
|
259
|
+
data = response.json()
|
|
260
|
+
else:
|
|
261
|
+
data = response.text
|
|
262
|
+
|
|
263
|
+
return process_response(data)
|
|
264
|
+
|
|
265
|
+
except RequestException as e:
|
|
266
|
+
msg = f"Erro ao buscar {error_message} (tentativa {attempt + 1}): {e}"
|
|
267
|
+
logger.error(msg)
|
|
254
268
|
|
|
255
|
-
except
|
|
256
|
-
msg = f"
|
|
257
|
-
|
|
269
|
+
except Exception as unhandled_error: # Outros erros - LÓGICA INESPERADA!
|
|
270
|
+
msg = f"Erro INESPERADO ao buscar {error_message}: {unhandled_error}"
|
|
271
|
+
logger.exception(msg) # Loga stack trace completo para debug
|
|
258
272
|
|
|
259
|
-
# Increment attempt count and add backoff delay
|
|
260
273
|
attempt += 1
|
|
261
|
-
time.sleep(1.
|
|
274
|
+
time.sleep(1.2**attempt) # Backoff exponencial
|
|
262
275
|
|
|
263
|
-
|
|
264
|
-
raise ValueError("Failed to fetch VNA LFT value after maximum attempts")
|
|
276
|
+
raise ValueError(f"Falha ao buscar {error_message} após {MAX_ATTEMPTS} tentativas")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.27.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|