PYield 0.28.2__tar.gz → 0.29.0__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 (51) hide show
  1. {pyield-0.28.2 → pyield-0.29.0}/PKG-INFO +2 -1
  2. pyield-0.29.0/pyield/__about__.py +1 -0
  3. pyield-0.29.0/pyield/__init__.py +28 -0
  4. pyield-0.29.0/pyield/anbima/__init__.py +13 -0
  5. {pyield-0.28.2 → pyield-0.29.0}/pyield/anbima/imaq.py +1 -1
  6. pyield-0.29.0/pyield/anbima/ipca.py +52 -0
  7. {pyield-0.28.2 → pyield-0.29.0}/pyield/anbima/tpf.py +4 -4
  8. {pyield-0.28.2 → pyield-0.29.0}/pyield/b3_futures/di.py +1 -1
  9. pyield-0.29.0/pyield/bc/__init__.py +15 -0
  10. {pyield-0.28.2 → pyield-0.29.0}/pyield/bc/auction.py +4 -2
  11. pyield-0.29.0/pyield/bc/bcdata.py +207 -0
  12. {pyield-0.28.2 → pyield-0.29.0}/pyield/bc/repos.py +2 -0
  13. pyield-0.29.0/pyield/bc/sec.py +85 -0
  14. pyield-0.29.0/pyield/bc/vna.py +92 -0
  15. pyield-0.29.0/pyield/config.py +36 -0
  16. pyield-0.29.0/pyield/conftest.py +13 -0
  17. pyield-0.29.0/pyield/ibge/__init__.py +5 -0
  18. pyield-0.29.0/pyield/ibge/ipca.py +38 -0
  19. {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/lft.py +1 -1
  20. {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/ltn.py +1 -1
  21. {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/ntnb.py +1 -1
  22. {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/ntnf.py +1 -1
  23. {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/tools.py +2 -2
  24. {pyield-0.28.2 → pyield-0.29.0}/pyproject.toml +1 -0
  25. pyield-0.28.2/pyield/__about__.py +0 -1
  26. pyield-0.28.2/pyield/__init__.py +0 -44
  27. pyield-0.28.2/pyield/b3_futures/__init__.py +0 -74
  28. pyield-0.28.2/pyield/conftest.py +0 -8
  29. pyield-0.28.2/pyield/indicators.py +0 -295
  30. pyield-0.28.2/pyield/projections.py +0 -81
  31. {pyield-0.28.2 → pyield-0.29.0}/.gitignore +0 -0
  32. {pyield-0.28.2 → pyield-0.29.0}/LICENSE +0 -0
  33. {pyield-0.28.2 → pyield-0.29.0}/README.md +0 -0
  34. {pyield-0.28.2 → pyield-0.29.0}/pyield/anbima/ima.py +0 -0
  35. {pyield-0.28.2/pyield/anbima → pyield-0.29.0/pyield/b3_futures}/__init__.py +0 -0
  36. {pyield-0.28.2 → pyield-0.29.0}/pyield/b3_futures/common.py +0 -0
  37. {pyield-0.28.2 → pyield-0.29.0}/pyield/b3_futures/historical.py +0 -0
  38. {pyield-0.28.2 → pyield-0.29.0}/pyield/b3_futures/intraday.py +0 -0
  39. {pyield-0.28.2 → pyield-0.29.0}/pyield/b3_futures/xml.py +0 -0
  40. {pyield-0.28.2 → pyield-0.29.0}/pyield/bc/ptax.py +0 -0
  41. {pyield-0.28.2 → pyield-0.29.0}/pyield/bday.py +0 -0
  42. {pyield-0.28.2 → pyield-0.29.0}/pyield/data_cache.py +0 -0
  43. {pyield-0.28.2 → pyield-0.29.0}/pyield/date_converter.py +0 -0
  44. /pyield-0.28.2/pyield/tools.py → /pyield-0.29.0/pyield/forwards.py +0 -0
  45. {pyield-0.28.2 → pyield-0.29.0}/pyield/holidays/__init__.py +0 -0
  46. {pyield-0.28.2 → pyield-0.29.0}/pyield/holidays/br_holidays_new.txt +0 -0
  47. {pyield-0.28.2 → pyield-0.29.0}/pyield/holidays/br_holidays_old.txt +0 -0
  48. {pyield-0.28.2 → pyield-0.29.0}/pyield/holidays/brholidays.py +0 -0
  49. {pyield-0.28.2 → pyield-0.29.0}/pyield/interpolator.py +0 -0
  50. {pyield-0.28.2 → pyield-0.29.0}/pyield/py.typed +0 -0
  51. {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PYield
3
- Version: 0.28.2
3
+ Version: 0.29.0
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>
@@ -36,6 +36,7 @@ Requires-Python: >=3.11
36
36
  Requires-Dist: numpy>=2.0.0
37
37
  Requires-Dist: pandas[excel,html,parquet]>=2.0.0
38
38
  Requires-Dist: requests>=2.31.0
39
+ Requires-Dist: tenacity>=9.0.0
39
40
  Description-Content-Type: text/markdown
40
41
 
41
42
  [![PyPI version](https://img.shields.io/pypi/v/pyield.svg)](https://pypi.python.org/pypi/pyield)
@@ -0,0 +1 @@
1
+ __version__ = "0.29.0"
@@ -0,0 +1,28 @@
1
+ import logging
2
+
3
+ from pyield import anbima, bc, bday, ibge
4
+ from pyield.__about__ import __version__
5
+ from pyield.b3_futures import futures
6
+ from pyield.b3_futures.di import DIFutures
7
+ from pyield.forwards import forward_rates
8
+ from pyield.interpolator import Interpolator
9
+ from pyield.tpf import lft, ltn, ntnb, ntnf
10
+
11
+ __all__ = [
12
+ "__version__",
13
+ "anbima",
14
+ "bday",
15
+ "DIFutures",
16
+ "forward_rates",
17
+ "futures",
18
+ "ibge",
19
+ "Interpolator",
20
+ "lft",
21
+ "ltn",
22
+ "ntnb",
23
+ "ntnf",
24
+ "bc",
25
+ ]
26
+
27
+ # Configura o logger do pacote principal com um NullHandler
28
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -0,0 +1,13 @@
1
+ from pyield.anbima.ima import last_ima
2
+ from pyield.anbima.imaq import imaq
3
+ from pyield.anbima.ipca import ipca_projection
4
+ from pyield.anbima.tpf import tpf_data, tpf_pre_maturities, tpf_rates
5
+
6
+ __all__ = [
7
+ "last_ima",
8
+ "imaq",
9
+ "ipca_projection",
10
+ "tpf_data",
11
+ "tpf_rates",
12
+ "tpf_pre_maturities",
13
+ ]
@@ -134,8 +134,8 @@ def imaq(date: DateScalar) -> pd.DataFrame:
134
134
  fetching or processing.
135
135
 
136
136
  """
137
+ date = dc.convert_input_dates(date)
137
138
  try:
138
- date = dc.convert_input_dates(date)
139
139
  df = _fetch_url_tables(date)
140
140
  if df.empty:
141
141
  return df
@@ -0,0 +1,52 @@
1
+ import locale
2
+ from dataclasses import dataclass
3
+
4
+ import pandas as pd
5
+
6
+
7
+ @dataclass
8
+ class IndicatorProjection:
9
+ reference_period: pd.Period # Reference month as a pd.Period object
10
+ projected_value: float # Projected value
11
+ last_updated: pd.Timestamp # Date and time of the last update
12
+
13
+
14
+ def ipca_projection() -> IndicatorProjection:
15
+ """
16
+ This function retrieves and parses the Excel file that contains economic indicators,
17
+ specifically looking for the IPCA projection. It extracts the date of the last
18
+ update and the IPCA projection for the reference month.
19
+
20
+ Data file format example after parsing:
21
+ - ['Data e Hora da Última Atualização: 19/04/2024 - 18:55 h', '', '']
22
+ - ...
23
+ - ['IPCA1', 'Projeção (abr/24)', 0.35]
24
+ - ...
25
+ """
26
+ # Define the URL and get the data
27
+ url = "https://www.anbima.com.br/informacoes/indicadores/arqs/indicadores.xls"
28
+ df = pd.read_excel(url, dtype_backend="numpy_nullable")
29
+
30
+ last_update_str = df.columns[0].split("Atualização:")[-1].strip()
31
+ last_update = pd.to_datetime(last_update_str, format="%d/%m/%Y - %H:%M h")
32
+
33
+ ipca_row = df[df.iloc[:, 0] == "IPCA1"]
34
+
35
+ ipca_value = ipca_row.iloc[0, 2]
36
+ ipca_value = pd.to_numeric(ipca_value, errors="coerce")
37
+ ipca_value = round(float(ipca_value) / 100, 4)
38
+
39
+ # Extract and format the reference month
40
+ ipca_date = ipca_row.iloc[0, 1]
41
+ ipca_date = str(ipca_date)
42
+ ipca_date = ipca_date.split("(")[-1].split(")")[0]
43
+ locale.setlocale(locale.LC_TIME, "pt_BR.UTF-8")
44
+ ipca_date = pd.to_datetime(ipca_date, format="%b/%y")
45
+ reference_period = ipca_date.to_period("M")
46
+ locale.setlocale(locale.LC_TIME, "") # Reset locale to default
47
+
48
+ return IndicatorProjection(
49
+ last_updated=last_update,
50
+ reference_period=reference_period,
51
+ projected_value=ipca_value,
52
+ )
@@ -106,7 +106,7 @@ def _process_raw_df(df_raw: pd.DataFrame) -> pd.DataFrame:
106
106
  return df.sort_values(["BondType", "MaturityDate"], ignore_index=True)
107
107
 
108
108
 
109
- def anbima_tpf_data(date: DateScalar, bond_type: str | None = None) -> pd.DataFrame:
109
+ def tpf_data(date: DateScalar, bond_type: str | None = None) -> pd.DataFrame:
110
110
  """Fetch and process TPF market data from ANBIMA.
111
111
 
112
112
  This function retrieves bond market data from the ANBIMA website for a
@@ -141,7 +141,7 @@ def anbima_tpf_data(date: DateScalar, bond_type: str | None = None) -> pd.DataFr
141
141
  return df.reset_index(drop=True)
142
142
 
143
143
 
144
- def anbima_tpf_rates(
144
+ def tpf_rates(
145
145
  date: DateScalar,
146
146
  bond_type: str | None = None,
147
147
  adj_maturities: bool = False,
@@ -172,7 +172,7 @@ def anbima_tpf_rates(
172
172
 
173
173
  if df.empty:
174
174
  # Try to fetch the data from the Anbima website
175
- df = anbima_tpf_data(date)
175
+ df = tpf_data(date)
176
176
 
177
177
  if df.empty:
178
178
  # If the data is still empty, return an empty DataFrame
@@ -203,7 +203,7 @@ def tpf_pre_maturities(date: DateScalar) -> pd.Series:
203
203
  'NTN-F' bonds, sorted in ascending order.
204
204
  """
205
205
  maturity_dates = (
206
- anbima_tpf_rates(date)
206
+ tpf_rates(date)
207
207
  .query("BondType in ['LTN', 'NTN-F']")["MaturityDate"]
208
208
  .drop_duplicates()
209
209
  .sort_values(ignore_index=True)
@@ -2,7 +2,7 @@ import pandas as pd
2
2
 
3
3
  import pyield.b3_futures as b3
4
4
  import pyield.date_converter as dc
5
- import pyield.tools as tl
5
+ import pyield.forwards as tl
6
6
  from pyield import bday, interpolator
7
7
  from pyield.data_cache import get_anbima_dataset, get_di_dataset
8
8
  from pyield.date_converter import DateArray, DateScalar
@@ -0,0 +1,15 @@
1
+ from pyield.bc.auction import auctions
2
+ from pyield.bc.bcdata import di_over, selic_over, selic_target
3
+ from pyield.bc.ptax import ptax
4
+ from pyield.bc.repos import repos
5
+ from pyield.bc.vna import vna_lft
6
+
7
+ __all__ = [
8
+ "auctions",
9
+ "repos",
10
+ "ptax",
11
+ "vna_lft",
12
+ "selic_over",
13
+ "selic_target",
14
+ "di_over",
15
+ ]
@@ -14,6 +14,7 @@ import requests
14
14
 
15
15
  from pyield import bday
16
16
  from pyield import date_converter as dc
17
+ from pyield.config import default_retry
17
18
  from pyield.date_converter import DateScalar
18
19
  from pyield.tpf.ntnb import duration as duration_b
19
20
  from pyield.tpf.ntnf import duration as duration_f
@@ -48,6 +49,7 @@ COLUMN_MAPPING = {
48
49
  BASE_API_URL = "https://olinda.bcb.gov.br/olinda/servico/leiloes_selic/versao/v1/odata/leiloesTitulosPublicos(dataMovimentoInicio=@dataMovimentoInicio,dataMovimentoFim=@dataMovimentoFim,dataLiquidacao=@dataLiquidacao,codigoTitulo=@codigoTitulo,dataVencimento=@dataVencimento,edital=@edital,tipoPublico=@tipoPublico,tipoOferta=@tipoOferta)?"
49
50
 
50
51
 
52
+ @default_retry
51
53
  def _load_from_url(url: str) -> pd.DataFrame:
52
54
  response = requests.get(url, timeout=10)
53
55
  response.raise_for_status()
@@ -224,7 +226,7 @@ def _fetch_df_from_url(url: str) -> pd.DataFrame:
224
226
  def auctions(
225
227
  start: DateScalar | None = None,
226
228
  end: DateScalar | None = None,
227
- auction_type: Literal["Sell", "Buy"] = None,
229
+ auction_type: Literal["Sell", "Buy"] | None = None,
228
230
  ) -> pd.DataFrame:
229
231
  """
230
232
  Recupera dados de leilões para um determinado período e tipo de leilão da API do BC.
@@ -318,7 +320,7 @@ def auctions(
318
320
 
319
321
  # Mapeamento do auction_type para o valor esperado pela API
320
322
  auction_type_mapping = {"Sell": "Venda", "Buy": "Compra"}
321
- auction_type_api_value = auction_type_mapping.get(auction_type)
323
+ auction_type_api_value = auction_type_mapping.get(str(auction_type))
322
324
  # Adiciona o parâmetro tipoOferta à URL se auction_type for fornecido
323
325
  if auction_type_api_value:
324
326
  url += f"&@tipoOferta='{auction_type_api_value}'"
@@ -0,0 +1,207 @@
1
+ import logging
2
+ from enum import Enum
3
+ from functools import lru_cache
4
+ from urllib.error import HTTPError
5
+
6
+ import pandas as pd
7
+
8
+ from pyield.config import default_retry
9
+ from pyield.date_converter import DateScalar, convert_input_dates
10
+
11
+ logger = logging.getLogger(__name__)
12
+ BASE_URL = "https://api.bcb.gov.br/dados/serie/bcdata.sgs."
13
+
14
+
15
+ class BCSerie(Enum):
16
+ """Enum para as séries disponíveis no Banco Central."""
17
+
18
+ SELIC_OVER = 1178
19
+ SELIC_TARGET = 432
20
+ DI_OVER = 11
21
+
22
+
23
+ def _build_download_url(
24
+ serie: BCSerie, start: DateScalar | None = None, end: DateScalar | None = None
25
+ ) -> str:
26
+ """
27
+ Builds the URL for downloading data from the Central Bank series.
28
+
29
+ Args:
30
+ serie: The series enum value to fetch
31
+ start: The start date for the data to fetch
32
+ end: The end date for the data
33
+
34
+ Returns:
35
+ The formatted URL for the API request
36
+ """
37
+ start_str = ""
38
+ if start:
39
+ start = convert_input_dates(start)
40
+ start_str = start.strftime("%d/%m/%Y")
41
+
42
+ end_str = ""
43
+ if end:
44
+ end = convert_input_dates(end)
45
+ end_str = end.strftime("%d/%m/%Y")
46
+
47
+ api_url = BASE_URL
48
+ api_url += f"{serie.value}/dados?formato=csv"
49
+ api_url += f"&dataInicial={start_str}&dataFinal={end_str}"
50
+
51
+ return api_url
52
+
53
+
54
+ @default_retry
55
+ def _fetch_data_from_url(
56
+ serie: BCSerie, start: DateScalar | None = None, end: DateScalar | None = None
57
+ ) -> pd.DataFrame:
58
+ """
59
+ Fetches data from the Central Bank API with retry mechanism.
60
+
61
+ Args:
62
+ serie: The series enum to fetch
63
+ start: The start date for the data to fetch
64
+ end: The end date for the data
65
+
66
+ Returns:
67
+ DataFrame with the requested data
68
+
69
+ Raises:
70
+ HTTPError: If the resource is not found (404) or other HTTP error
71
+ Various exceptions: For other errors after retry attempts are exhausted
72
+ """
73
+ api_url = _build_download_url(serie, start, end)
74
+
75
+ try:
76
+ df = pd.read_csv(api_url, sep=";", decimal=",", dtype_backend="numpy_nullable")
77
+ if df.empty or "valor" not in df.columns:
78
+ logger.warning(f"No data available for the requested period: {api_url}")
79
+ return pd.DataFrame()
80
+
81
+ # Process the dataframe
82
+ df = df.rename(columns={"data": "Date", "valor": "Value"})
83
+ df["Date"] = pd.to_datetime(df["Date"], format="%d/%m/%Y")
84
+ # Value is in percentage format, so divide by 100
85
+ df["Value"] /= 100
86
+ return df
87
+ except HTTPError as e:
88
+ if e.code == 404: # noqa
89
+ logger.warning(f"Resource not found (404): {api_url}")
90
+ return pd.DataFrame()
91
+ else:
92
+ logger.error(f"HTTP error accessing Central Bank API: {e}")
93
+ raise
94
+ except pd.errors.ParserError as e:
95
+ # For parsing errors, log and re-raise to allow retry
96
+ logger.warning(f"CSV parsing error (possibly HTML or invalid format): {e}")
97
+ raise
98
+ except Exception as e:
99
+ logger.error(f"Error fetching data from Central Bank API: {e}")
100
+ raise
101
+
102
+
103
+ @lru_cache(maxsize=128)
104
+ def selic_over(
105
+ start: DateScalar | None = None, end: DateScalar | None = None
106
+ ) -> pd.DataFrame:
107
+ """
108
+ Fetches the SELIC Over rate for a specific reference date.
109
+
110
+ The SELIC Over rate is the daily average interest rate effectively practiced
111
+ between banks in the interbank market, using public securities as collateral.
112
+
113
+ API URL Example:
114
+ https://api.bcb.gov.br/dados/serie/bcdata.sgs.1178/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
115
+
116
+ Args:
117
+ start: The start date for the data to fetch
118
+ end: The end date for the data to fetch
119
+
120
+ Returns:
121
+ DataFrame containing Date and Value columns with the SELIC Over rate,
122
+ or empty DataFrame if data is not available.
123
+
124
+ Examples:
125
+ >>> yd.bc.selic_over("31-05-2024", "31-05-2024")
126
+ Date Value
127
+ 0 2024-05-31 0.104
128
+
129
+ # Rate changed due to Copom meeting
130
+ >>> yd.bc.selic_over("26-01-2025") # No data on 26-01-2025 (sunday)
131
+ Date Value
132
+ 0 2025-01-27 0.1215
133
+ 1 2025-01-28 0.1215
134
+ 2 2025-01-29 0.1215
135
+ 3 2025-01-30 0.1315
136
+ 4 2025-01-31 0.1315
137
+ ...
138
+
139
+ """
140
+ df = _fetch_data_from_url(BCSerie.SELIC_OVER, start, end)
141
+ df["Value"] = df["Value"].round(4)
142
+ return df
143
+
144
+
145
+ @lru_cache(maxsize=128)
146
+ def selic_target(
147
+ start: DateScalar | None = None, end: DateScalar | None = None
148
+ ) -> pd.DataFrame:
149
+ """
150
+ Fetches the SELIC Target rate for a specific reference date.
151
+
152
+ The SELIC Target rate is the official rate set by the Central Bank of Brazil.
153
+
154
+ API URL Example:
155
+ https://api.bcb.gov.br/dados/serie/bcdata.sgs.432/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
156
+
157
+ Args:
158
+ start: The start date for the data to fetch
159
+ end: The end date for the data to fetch
160
+
161
+ Returns:
162
+ DataFrame containing Date and Value columns with the SELIC Target rate,
163
+ or empty DataFrame if data is not available.
164
+
165
+ Examples:
166
+ >>> yd.bc.selic_target("31-05-2024", "31-05-2024")
167
+ Date Value
168
+ 0 2024-05-31 0.105
169
+ """
170
+ df = _fetch_data_from_url(BCSerie.SELIC_TARGET, start, end)
171
+ df["Value"] = df["Value"].round(4)
172
+ return df
173
+
174
+
175
+ @lru_cache(maxsize=128)
176
+ def di_over(
177
+ start: DateScalar | None = None,
178
+ end: DateScalar | None = None,
179
+ annualized: bool = True,
180
+ ) -> pd.DataFrame:
181
+ """
182
+ Fetches the DI (Interbank Deposit) rate for a specific reference date.
183
+
184
+ API URL Example:
185
+ https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
186
+ https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=csv&dataInicial=12/04/2024&dataFinal=12/04/2024
187
+
188
+ Args:
189
+ start: The start date for the data to fetch
190
+ end: The end date for the data to fetch
191
+ annualized: If True, returns the annualized rate (252 trading
192
+ days per year), otherwise returns the daily rate.
193
+
194
+ Returns:
195
+ DataFrame containing Date and Value columns with the DI rate,
196
+ or empty DataFrame if data is not available.
197
+
198
+ Examples:
199
+ >>> yd.bc.di_over("31-05-2024", "31-05-2024")
200
+ Date Value
201
+ 0 2024-05-31 0.104
202
+ """
203
+ df = _fetch_data_from_url(BCSerie.DI_OVER, start, end)
204
+ if annualized:
205
+ df["Value"] = (df["Value"] + 1) ** 252 - 1
206
+ df["Value"] = df["Value"].round(4)
207
+ return df
@@ -13,6 +13,7 @@ import requests
13
13
 
14
14
  from pyield import bday
15
15
  from pyield import date_converter as dc
16
+ from pyield.config import default_retry
16
17
  from pyield.date_converter import DateScalar
17
18
 
18
19
  """Dicionário com o mapeamento das colunas da API do BC para o DataFrame final
@@ -37,6 +38,7 @@ COLUMN_MAPPING = {
37
38
  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
 
39
40
 
41
+ @default_retry
40
42
  def _load_from_url(url: str) -> pd.DataFrame:
41
43
  response = requests.get(url, timeout=10)
42
44
  response.raise_for_status()
@@ -0,0 +1,85 @@
1
+ import logging
2
+
3
+ import pandas as pd
4
+
5
+ from pyield.config import default_retry
6
+ from pyield.date_converter import DateScalar, convert_input_dates
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ COLUMN_MAPPING = {
11
+ "DATA MOV": "SettlementDate",
12
+ "SIGLA": "BondType",
13
+ "CODIGO": "SelicCode",
14
+ "CODIGO ISIN": "ISIN",
15
+ "EMISSAO": "IssueDate",
16
+ "VENCIMENTO": "Maturity",
17
+ "NUM DE OPER": "TradeCount",
18
+ "QUANT NEGOCIADA": "TradeQuantity",
19
+ "VALOR NEGOCIADO": "TradeValue",
20
+ "PU MIN": "MinPrice",
21
+ "PU MED": "AvgPrice",
22
+ "PU MAX": "MaxPrice",
23
+ "PU LASTRO": "UnderlyingPrice",
24
+ "VALOR PAR": "ParValue",
25
+ "TAXA MIN": "MinRate",
26
+ "TAXA MED": "AvgRate",
27
+ "TAXA MAX": "MaxRate",
28
+ "NUM OPER COM CORRETAGEM": "BrokerageTradeCount",
29
+ "QUANT NEG COM CORRETAGEM": "BrokerageTradeQuantity",
30
+ }
31
+
32
+
33
+ def _build_download_url(target_date: DateScalar) -> str:
34
+ """
35
+ URL com todos os arquivos disponíveis:
36
+ https://www4.bcb.gov.br/pom/demab/negociacoes/apresentacao.asp?frame=1
37
+
38
+ Exemplo de URL para download:
39
+ https://www4.bcb.gov.br/pom/demab/negociacoes/download/NegE202409.ZIP
40
+
41
+ File format: NegEYYYYMM.ZIP
42
+ """
43
+ target_date = convert_input_dates(target_date)
44
+ file_date = target_date.strftime("%Y%m")
45
+ file_name = f"NegE{file_date}.ZIP"
46
+ base_url = "https://www4.bcb.gov.br/pom/demab/negociacoes/download"
47
+ return f"{base_url}/{file_name}"
48
+
49
+
50
+ @default_retry
51
+ def _fetch_data_from_url(file_url: str) -> pd.DataFrame:
52
+ df = pd.read_csv(
53
+ file_url,
54
+ sep=";",
55
+ decimal=",",
56
+ dtype_backend="numpy_nullable",
57
+ )
58
+ for col in ["DATA MOV", "EMISSAO", "VENCIMENTO"]:
59
+ df[col] = pd.to_datetime(df[col], format="%d/%m/%Y", errors="coerce")
60
+
61
+ return df
62
+
63
+
64
+ def sec(target_date: DateScalar) -> pd.DataFrame:
65
+ """Fetches bond trading data for a specific date from the Brazilian Central Bank.
66
+
67
+ Downloads the daily bond trading data from the Brazilian Central Bank
68
+ website for the given target date. The data is downloaded as a ZIP file,
69
+ extracted, and read into a Pandas DataFrame. The columns are then
70
+ renamed to English names for easier use.
71
+
72
+ Args:
73
+ target_date (DateScalar): The date for which to fetch the bond
74
+ trading data. This can be a date-like object understood by
75
+ `pyield.date_converter.convert_input_dates`.
76
+
77
+ Returns:
78
+ pd.DataFrame: A DataFrame containing the bond trading data for the
79
+ specified date. Columns are renamed according to `COLUMN_MAPPING`
80
+ for English readability.
81
+ """
82
+ url = _build_download_url(target_date)
83
+ df = _fetch_data_from_url(url)
84
+ df = df.rename(columns=COLUMN_MAPPING)
85
+ return df
@@ -0,0 +1,92 @@
1
+ import requests
2
+
3
+ from pyield.config import default_retry
4
+ from pyield.date_converter import DateScalar, convert_input_dates
5
+
6
+
7
+ @default_retry
8
+ def _get_text(date: DateScalar) -> str:
9
+ # url example: https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/20240418APC238
10
+ url_base = "https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/"
11
+ date = convert_input_dates(date)
12
+ url_file = f"{date.strftime('%Y%m%d')}APC238"
13
+ url_vna = url_base + url_file
14
+
15
+ response = requests.get(url_vna)
16
+ response.raise_for_status()
17
+ return response.text
18
+
19
+
20
+ def _extract_vna_table_text(text: str) -> str:
21
+ """Extrai o texto contendo a tabela VNA do texto bruto."""
22
+ start_of_table = text.find("EMISSAO")
23
+ end_of_table = text.find("99999999*")
24
+ table_text = text[start_of_table:end_of_table].strip()
25
+ return table_text
26
+
27
+
28
+ def _parse_vna_table_lines(table_text: str) -> list[str]:
29
+ """Processa o texto da tabela VNA para retornar uma lista de linhas."""
30
+ table_lines = table_text.splitlines()
31
+ table_lines = [
32
+ line.strip() for line in table_lines if line.strip()
33
+ ] # Remove empty lines
34
+ body_lines = table_lines[1:] # Remove first line (header)
35
+ return body_lines
36
+
37
+
38
+ def _extract_vna_values_from_lines(body_lines: list[str]) -> list[float]:
39
+ """Extrai valores VNA numéricos de uma lista de linhas de texto."""
40
+ vnas = []
41
+ for line in body_lines:
42
+ vna_str = line.split()[-1].replace(",", ".")
43
+ vnas.append(float(vna_str))
44
+ return vnas
45
+
46
+
47
+ def _validate_vna_values(vnas: list[float]) -> float:
48
+ """Valida se todos os valores VNA são iguais e retorna o valor único."""
49
+ vna_value = vnas[0]
50
+ if any(vna_value != vna for vna in vnas):
51
+ bcb_url = "https://www.bcb.gov.br/estabilidadefinanceira/selicbaixar"
52
+ msg = f"VNA values are not the same. Please check data at {bcb_url}"
53
+ raise ValueError(msg)
54
+ return vna_value
55
+
56
+
57
+ def vna_lft(date: DateScalar) -> float:
58
+ """Retrieves the VNA (Valor Nominal Atualizado) from the BCB for a given date.
59
+
60
+ This function fetches daily data from the BCB website, extracts the
61
+ VNA value from a specific table within the downloaded content, and
62
+ returns this value.
63
+
64
+ Args:
65
+ date (DateScalar): The date for which to retrieve the VNA value.
66
+ This argument accepts various date formats, including string and
67
+ datetime objects, which are then standardized using the
68
+ `convert_input_dates` function.
69
+
70
+ Returns:
71
+ float: The VNA (Valor Nominal Atualizado) value for the specified date.
72
+
73
+ Examples:
74
+ >>> yd.bc.vna_lft("31-05-2024")
75
+ 14903.01148
76
+
77
+ Raises:
78
+ ValueError: If the extracted VNA values from the BCB website are
79
+ inconsistent (i.e., not all extracted values are identical),
80
+ suggesting potential data discrepancies on the source website.
81
+ The error message includes a link to the BCB website for manual
82
+ verification.
83
+ requests.exceptions.HTTPError: If the HTTP request to the BCB website
84
+ fails. This could be due to network issues, website unavailability,
85
+ or the requested data not being found for the given date.
86
+ """
87
+ text = _get_text(date)
88
+ table_text = _extract_vna_table_text(text)
89
+ table_lines = _parse_vna_table_lines(table_text)
90
+ vnas = _extract_vna_values_from_lines(table_lines)
91
+ vna_value = _validate_vna_values(vnas)
92
+ return vna_value
@@ -0,0 +1,36 @@
1
+ import logging
2
+ from urllib.error import HTTPError, URLError
3
+
4
+ import pandas as pd
5
+ import tenacity
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def retry_if_not_404(exception):
11
+ """
12
+ Retry if the exception is not a 404 error.
13
+
14
+ Args:
15
+ exception: The exception that was raised
16
+
17
+ Returns:
18
+ Boolean indicating whether to retry
19
+ """
20
+ # Don't retry for 404 errors (resource doesn't exist)
21
+ if isinstance(exception, HTTPError) and exception.code == 404: # noqa
22
+ logger.info("Resource not found (404), not retrying.")
23
+ return False
24
+ # Retry for parsing errors and other connection errors
25
+ return isinstance(
26
+ exception, (URLError, pd.errors.EmptyDataError, pd.errors.ParserError)
27
+ )
28
+
29
+
30
+ # Custom retry configuration that handles parsing errors
31
+ default_retry = tenacity.retry(
32
+ retry=tenacity.retry_if_exception(retry_if_not_404),
33
+ stop=tenacity.stop_after_attempt(5),
34
+ wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
35
+ before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
36
+ )
@@ -0,0 +1,13 @@
1
+ import pytest
2
+
3
+ import pyield
4
+
5
+
6
+ @pytest.fixture(autouse=True)
7
+ def add_np(doctest_namespace):
8
+ doctest_namespace["yd"] = pyield
9
+
10
+
11
+ def pytest_configure(config):
12
+ config.addinivalue_line("doctest_optionflags", "ELLIPSIS")
13
+ config.addinivalue_line("doctest_optionflags", "NORMALIZE_WHITESPACE")
@@ -0,0 +1,5 @@
1
+ from pyield.ibge.ipca import ipca_monthly_rate
2
+
3
+ __all__ = [
4
+ "ipca_monthly_rate",
5
+ ]