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.
Files changed (39) hide show
  1. {pyield-0.27.0 → pyield-0.27.2}/PKG-INFO +1 -1
  2. pyield-0.27.2/pyield/__about__.py +1 -0
  3. {pyield-0.27.0 → pyield-0.27.2}/pyield/__init__.py +8 -0
  4. {pyield-0.27.0 → pyield-0.27.2}/pyield/anbima/imaq.py +12 -7
  5. {pyield-0.27.0 → pyield-0.27.2}/pyield/bc/auction.py +9 -11
  6. pyield-0.27.2/pyield/bc/repos.py +194 -0
  7. {pyield-0.27.0 → pyield-0.27.2}/pyield/indicators.py +118 -106
  8. pyield-0.27.0/pyield/__about__.py +0 -1
  9. {pyield-0.27.0 → pyield-0.27.2}/.gitignore +0 -0
  10. {pyield-0.27.0 → pyield-0.27.2}/LICENSE +0 -0
  11. {pyield-0.27.0 → pyield-0.27.2}/README.md +0 -0
  12. {pyield-0.27.0 → pyield-0.27.2}/pyield/anbima/__init__.py +0 -0
  13. {pyield-0.27.0 → pyield-0.27.2}/pyield/anbima/ima.py +0 -0
  14. {pyield-0.27.0 → pyield-0.27.2}/pyield/anbima/tpf.py +0 -0
  15. {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/__init__.py +0 -0
  16. {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/common.py +0 -0
  17. {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/di.py +0 -0
  18. {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/historical.py +0 -0
  19. {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/intraday.py +0 -0
  20. {pyield-0.27.0 → pyield-0.27.2}/pyield/b3_futures/xml.py +0 -0
  21. {pyield-0.27.0 → pyield-0.27.2}/pyield/bday.py +0 -0
  22. {pyield-0.27.0 → pyield-0.27.2}/pyield/conftest.py +0 -0
  23. {pyield-0.27.0 → pyield-0.27.2}/pyield/data_cache.py +0 -0
  24. {pyield-0.27.0 → pyield-0.27.2}/pyield/date_converter.py +0 -0
  25. {pyield-0.27.0 → pyield-0.27.2}/pyield/holidays/__init__.py +0 -0
  26. {pyield-0.27.0 → pyield-0.27.2}/pyield/holidays/br_holidays_new.txt +0 -0
  27. {pyield-0.27.0 → pyield-0.27.2}/pyield/holidays/br_holidays_old.txt +0 -0
  28. {pyield-0.27.0 → pyield-0.27.2}/pyield/holidays/brholidays.py +0 -0
  29. {pyield-0.27.0 → pyield-0.27.2}/pyield/interpolator.py +0 -0
  30. {pyield-0.27.0 → pyield-0.27.2}/pyield/projections.py +0 -0
  31. {pyield-0.27.0 → pyield-0.27.2}/pyield/py.typed +0 -0
  32. {pyield-0.27.0 → pyield-0.27.2}/pyield/tools.py +0 -0
  33. {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/__init__.py +0 -0
  34. {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/lft.py +0 -0
  35. {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/ltn.py +0 -0
  36. {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/ntnb.py +0 -0
  37. {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/ntnf.py +0 -0
  38. {pyield-0.27.0 → pyield-0.27.2}/pyield/tpf/tools.py +0 -0
  39. {pyield-0.27.0 → pyield-0.27.2}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PYield
3
- Version: 0.27.0
3
+ Version: 0.27.2
4
4
  Summary: A Python library for analysis of fixed income instruments in Brazil
5
5
  Project-URL: Source, https://github.com/crdcj/PYield
6
6
  Author-email: Carlos Carvalho <cr.cj@outlook.com>
@@ -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
- logging.warning("No data found. Returning empty DataFrame.")
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
- logging.warning("No date found. Returning empty DataFrame.")
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
- df = _process_df(df)
137
- return df
138
- except Exception as e:
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
- logging.error(f"Error fetching IMA for {date_str}. Returning empty DataFrame.")
141
- logging.error(f"Error message: {e}", exc_info=True)
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
- df_ltn["MDuration"] = df_ltn["Duration"] / (1 + df_ltn["AvgRate"])
145
- df_ltn["DV01FR"] = 0.0001 * df_ltn["MDuration"] * df_ltn["AvgPrice"]
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
- df_ntnf["MDuration"] = df_ntnf["Duration"] / (1 + df_ntnf["AvgRate"])
155
- df_ntnf["DV01FR"] = 0.0001 * df_ntnf["MDuration"] * df_ntnf["AvgPrice"]
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
- df_ntnb["MDuration"] = df_ntnb["Duration"] / (1 + df_ntnb["AvgRate"])
165
- df_ntnb["DV01FR"] = 0.0001 * df_ntnb["MDuration"] * df_ntnb["AvgPrice"]
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 = 5
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
- # Send a GET request to the API
95
- response = requests.get(api_url, timeout=TIMEOUT)
96
-
97
- # Raises HTTPError, if one occurred
98
- response.raise_for_status()
99
-
100
- # Parse the JSON response
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
- if not data:
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=json&dataInicial={selic_date}&dataFinal={selic_date}"
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
- # Increment attempt count and add backoff delay
145
- attempt += 1
146
- time.sleep(1.5**attempt) # Exponential backoff: 1.5, 2.3, 3.9, 5.1, 7.6 seconds
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
- # After all retries, raise an error
149
- raise ValueError("Failed to fetch SELIC Target rate after maximum attempts")
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=json&dataInicial={formatted_date}&dataFinal={formatted_date}"
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
- attempt = 0
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
- while attempt < MAX_ATTEMPTS:
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
- # Access the value directly, any issue will raise an exception
182
- value = data[0]["valor"]
183
- return round(float(value) / 100, 4)
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
- except (RequestException, KeyError, IndexError, ValueError) as e:
186
- msg = f"Error fetching SELIC Over rate (attempt {attempt + 1}): {e}"
187
- logging.error(msg)
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
- # Increment attempt count and add backoff delay
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 _di(reference_date: pd.Timestamp) -> float:
198
- # https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
199
- di_date = reference_date.strftime("%d/%m/%Y")
200
- api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial={di_date}&dataFinal={di_date}"
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
- if di_date not in response.text:
205
- return float("nan")
206
- data = response.json()
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
- def _extract_vna_value(text: str) -> float:
213
- # Finding the part that contains the table
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
- # Extracting the table
218
- table_text = text[start_of_table:end_of_table].strip()
219
- table_lines = table_text.splitlines()
211
+ vnas = []
212
+ for line in body_lines:
213
+ vna_str = line.split()[-1].replace(",", ".")
214
+ vnas.append(float(vna_str))
220
215
 
221
- # Remove empty lines
222
- table_lines = [line.strip() for line in table_lines if line.strip()]
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
- # Remove first line
225
- body_lines = table_lines[1:]
223
+ return vna_value
226
224
 
227
- vnas = []
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
- return vna_value
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
- def _vna_lft(reference_date: pd.Timestamp) -> float:
242
- # url example: https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/20240418APC238
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 = session.get(url_vna, timeout=TIMEOUT)
252
- response.raise_for_status()
253
- return _extract_vna_value(response.text)
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 (RequestException, KeyError, IndexError, ValueError) as e:
256
- msg = f"Error fetching SELIC Over rate (attempt {attempt + 1}): {e}"
257
- logging.error(msg)
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.5**attempt) # Exponential backoff: 1.5, 2.3, 3.9, 5.1, 7.6 seconds
274
+ time.sleep(1.2**attempt) # Backoff exponencial
262
275
 
263
- # After all retries, raise an error
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