PYield 0.36.2__tar.gz → 0.36.4__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 (53) hide show
  1. {pyield-0.36.2 → pyield-0.36.4}/PKG-INFO +1 -1
  2. pyield-0.36.4/pyield/__about__.py +1 -0
  3. {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/imaq.py +1 -1
  4. {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/auction.py +12 -2
  5. {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/bcdata.py +102 -32
  6. pyield-0.36.2/pyield/__about__.py +0 -1
  7. {pyield-0.36.2 → pyield-0.36.4}/.gitignore +0 -0
  8. {pyield-0.36.2 → pyield-0.36.4}/LICENSE +0 -0
  9. {pyield-0.36.2 → pyield-0.36.4}/README.md +0 -0
  10. {pyield-0.36.2 → pyield-0.36.4}/pyield/__init__.py +0 -0
  11. {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/__init__.py +0 -0
  12. {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/ettj.py +0 -0
  13. {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/ima.py +0 -0
  14. {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/ipca.py +0 -0
  15. {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/tpf.py +0 -0
  16. {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/__init__.py +0 -0
  17. {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/di1.py +0 -0
  18. {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/di_over.py +0 -0
  19. {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/futures/__init__.py +0 -0
  20. {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/futures/common.py +0 -0
  21. {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/futures/historical.py +0 -0
  22. {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/futures/intraday.py +0 -0
  23. {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/futures/xml.py +0 -0
  24. {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/__init__.py +0 -0
  25. {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/ptax.py +0 -0
  26. {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/repo.py +0 -0
  27. {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/trades_intraday.py +0 -0
  28. {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/trades_monthly.py +0 -0
  29. {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/vna.py +0 -0
  30. {pyield-0.36.2 → pyield-0.36.4}/pyield/bday.py +0 -0
  31. {pyield-0.36.2 → pyield-0.36.4}/pyield/conftest.py +0 -0
  32. {pyield-0.36.2 → pyield-0.36.4}/pyield/data_cache.py +0 -0
  33. {pyield-0.36.2 → pyield-0.36.4}/pyield/date_converter.py +0 -0
  34. {pyield-0.36.2 → pyield-0.36.4}/pyield/fwd.py +0 -0
  35. {pyield-0.36.2 → pyield-0.36.4}/pyield/holidays/__init__.py +0 -0
  36. {pyield-0.36.2 → pyield-0.36.4}/pyield/holidays/br_holidays_new.txt +0 -0
  37. {pyield-0.36.2 → pyield-0.36.4}/pyield/holidays/br_holidays_old.txt +0 -0
  38. {pyield-0.36.2 → pyield-0.36.4}/pyield/holidays/brholidays.py +0 -0
  39. {pyield-0.36.2 → pyield-0.36.4}/pyield/ibge/__init__.py +0 -0
  40. {pyield-0.36.2 → pyield-0.36.4}/pyield/ibge/ipca.py +0 -0
  41. {pyield-0.36.2 → pyield-0.36.4}/pyield/interpolator.py +0 -0
  42. {pyield-0.36.2 → pyield-0.36.4}/pyield/py.typed +0 -0
  43. {pyield-0.36.2 → pyield-0.36.4}/pyield/retry.py +0 -0
  44. {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/__init__.py +0 -0
  45. {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/benchmark.py +0 -0
  46. {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/lft.py +0 -0
  47. {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/ltn.py +0 -0
  48. {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/ntnb.py +0 -0
  49. {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/ntnc.py +0 -0
  50. {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/ntnf.py +0 -0
  51. {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/pre.py +0 -0
  52. {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/tools.py +0 -0
  53. {pyield-0.36.2 → pyield-0.36.4}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PYield
3
- Version: 0.36.2
3
+ Version: 0.36.4
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.36.4"
@@ -104,7 +104,7 @@ def _process_df(df: pd.DataFrame) -> pd.DataFrame:
104
104
  df[col] = pd.to_numeric(df[col], errors="coerce")
105
105
 
106
106
  # Remove the thousands unit from numeric columns
107
- df[col] *= (1000 * df[col]).astype("Int64")
107
+ df[col] = (1000 * df[col]).astype("Int64")
108
108
 
109
109
  return df
110
110
 
@@ -195,8 +195,18 @@ def _get_ptax_df(start_date: pd.Timestamp | None = None) -> pd.DataFrame:
195
195
 
196
196
  def _add_usd_dv01(df: pd.DataFrame) -> pd.DataFrame:
197
197
  ptax_start_date = df["Date"].min()
198
- df_ptax = _get_ptax_df(start_date=ptax_start_date)
199
- df = pd.merge_asof(left=df, right=df_ptax)
198
+
199
+ # 1. Garanta que o DataFrame 'right' esteja ordenado pela chave de merge.
200
+ df_ptax = (
201
+ _get_ptax_df(start_date=ptax_start_date)
202
+ .sort_values(by="Date")
203
+ .reset_index(drop=True)
204
+ )
205
+
206
+ # 2. Garanta que o DataFrame 'left' esteja ordenado pela chave de merge.
207
+ df = df.sort_values(by="Date").reset_index(drop=True)
208
+ df = pd.merge_asof(left=df, right=df_ptax, on="Date", direction="backward")
209
+
200
210
  dv01_cols = [c for c in df.columns if c.startswith("DV01")]
201
211
  for col in dv01_cols:
202
212
  df[f"{col}USD"] = df[col] / df["PTAX"]
@@ -15,7 +15,9 @@ Implementation Notes:
15
15
  """
16
16
 
17
17
  import logging
18
+ from datetime import datetime
18
19
  from enum import Enum
20
+ from functools import lru_cache
19
21
  from urllib.error import HTTPError
20
22
 
21
23
  import pandas as pd
@@ -30,6 +32,12 @@ BASE_URL = "https://api.bcb.gov.br/dados/serie/bcdata.sgs."
30
32
  DECIMAL_PLACES_ANNUALIZED = 4 # 2 decimal places in percentage format
31
33
  DECIMAL_PLACES_DAILY = 8 # 6 decimal places in percentage format
32
34
 
35
+ # 404 Not Found error code for resource not found in the API
36
+ ERROR_CODE_NOT_FOUND = 404
37
+
38
+ # 400 Bad Request error code for invalid requests
39
+ ERROR_CODE_BAD_REQUEST = 400
40
+
33
41
 
34
42
  class BCSerie(Enum):
35
43
  """Enum para as séries disponíveis no Banco Central."""
@@ -70,68 +78,130 @@ def _build_download_url(
70
78
  return api_url
71
79
 
72
80
 
81
+ @lru_cache(maxsize=128)
73
82
  @default_retry
74
- def _fetch_data_from_url(
75
- serie: BCSerie, start: DateScalar | None = None, end: DateScalar | None = None
83
+ def _cached_fetch_worker(
84
+ serie: BCSerie,
85
+ start: DateScalar | None,
86
+ end: DateScalar | None,
87
+ hourly_timestamp_hash: str,
76
88
  ) -> pd.DataFrame:
77
89
  """
78
- Fetches data from the Central Bank API with retry mechanism.
79
-
80
- Args:
81
- serie: The series enum to fetch
82
- start: The start date for the data to fetch
83
- end: The end date for the data
84
-
85
- Returns:
86
- DataFrame with the requested data
87
-
88
- Raises:
89
- HTTPError: If the resource is not found (404) or other HTTP error
90
- Various exceptions: For other errors after retry attempts are exhausted
90
+ Worker function that fetches data from the API.
91
+ This function is decorated with lru_cache. The hourly_timestamp_hash argument
92
+ is a string in 'yymmddHH' format, used to invalidate the cache hourly.
91
93
  """
92
94
  api_url = _build_download_url(serie, start, end)
95
+ logger.debug(
96
+ f"CACHE MISS for hash {hourly_timestamp_hash}. Fetching from URL: {api_url}"
97
+ )
93
98
 
94
99
  try:
95
100
  response = requests.get(api_url, timeout=30)
96
- response.raise_for_status() # Raise exception for 4XX/5XX status codes
101
+ response.raise_for_status()
97
102
  data = response.json()
98
103
 
99
104
  if not data:
100
105
  logger.warning(f"No data available for the requested period: {api_url}")
101
106
  return pd.DataFrame()
102
107
 
103
- # Convert JSON to DataFrame
104
- df = pd.DataFrame(data)
105
-
106
- if df.empty or "valor" not in df.columns:
107
- logger.warning(f"No data available for the requested period: {api_url}")
108
- return pd.DataFrame()
109
-
110
- # Process the dataframe
111
108
  df = pd.DataFrame(data, dtype="string")
112
109
  df = df.rename(columns={"data": "Date", "valor": "Value"})
113
-
114
110
  df["Date"] = pd.to_datetime(df["Date"], format="%d/%m/%Y")
115
- # Value is in percentage format, so divide by 100
116
111
  df["Value"] = (df["Value"].astype("Float64")) / 100
117
112
 
118
- return df
113
+ # Return a copy to ensure the cached object is not mutated externally
114
+ return df.copy()
115
+
119
116
  except HTTPError as e:
120
- if e.code == 404: # noqa
117
+ if e.response.status_code == ERROR_CODE_NOT_FOUND:
121
118
  logger.warning(f"Resource not found (404): {api_url}")
122
119
  return pd.DataFrame()
120
+ if (
121
+ e.response.status_code == ERROR_CODE_BAD_REQUEST
122
+ and "janela de consulta" in e.response.text
123
+ ):
124
+ logger.error(
125
+ "API request exceeded the 10-year limit. "
126
+ "This should have been handled by the chunking logic. URL: %s",
127
+ api_url,
128
+ )
123
129
  else:
124
130
  logger.error(f"HTTP error accessing Central Bank API: {e}")
125
- raise
126
- except pd.errors.ParserError as e:
127
- # For parsing errors, log and re-raise to allow retry
128
- logger.warning(f"CSV parsing error (possibly HTML or invalid format): {e}")
129
131
  raise
130
132
  except Exception as e:
131
133
  logger.error(f"Error fetching data from Central Bank API: {e}")
132
134
  raise
133
135
 
134
136
 
137
+ def _fetch_single_request(
138
+ serie: BCSerie, start: DateScalar | None = None, end: DateScalar | None = None
139
+ ) -> pd.DataFrame:
140
+ """
141
+ Wrapper that provides an hourly time-based cache invalidation for the worker.
142
+ It computes a readable hash based on the current hour ('yymmddHH') and
143
+ passes it to the cached worker function.
144
+ """
145
+ hourly_timestamp_hash = datetime.now().strftime("%y%m%d%H")
146
+ return _cached_fetch_worker(serie, start, end, hourly_timestamp_hash)
147
+
148
+
149
+ def _fetch_data_from_url(
150
+ serie: BCSerie, start: DateScalar | None = None, end: DateScalar | None = None
151
+ ) -> pd.DataFrame:
152
+ """
153
+ Orchestrates fetching data from the Central Bank API, handling requests longer
154
+ than 10 years by splitting them into smaller chunks using pandas DateOffset.
155
+
156
+ Args:
157
+ serie: The series enum to fetch
158
+ start: The start date for the data to fetch
159
+ end: The end date for the data
160
+
161
+ Returns:
162
+ DataFrame with the requested data
163
+ """
164
+ # 1. Converter datas usando a função auxiliar existente e o pandas
165
+ start_date = convert_input_dates(start) if start else None
166
+ # Se a data final não for fornecida, usar a data de hoje para o cálculo do período
167
+ end_date = convert_input_dates(end) if end else pd.to_datetime("today").normalize()
168
+
169
+ # Se não houver data de início, a API já limita a 10 anos. Chamada direta é segura.
170
+ if not start_date:
171
+ return _fetch_single_request(serie, start, end)
172
+
173
+ # 2. Verificar se o período é maior que 10 anos usando pd.DateOffset
174
+ if end_date < start_date + pd.DateOffset(years=10):
175
+ return _fetch_single_request(serie, start_date, end_date)
176
+
177
+ # 3. Se for maior, quebrar em pedaços (chunking)
178
+ logger.info("Date range exceeds 10 years. Fetching data in chunks.")
179
+ all_dfs = []
180
+ current_start = start_date
181
+
182
+ while current_start < end_date:
183
+ # Fim do chunk é 10 anos à frente, menos 1 dia.
184
+ chunk_end = current_start + pd.DateOffset(years=10) - pd.DateOffset(days=1)
185
+
186
+ # Garantir que o fim do chunk não ultrapasse a data final solicitada
187
+ chunk_end = min(chunk_end, end_date)
188
+
189
+ # Buscar os dados para este chunk
190
+ df_chunk = _fetch_single_request(serie, current_start, chunk_end)
191
+ if not df_chunk.empty:
192
+ all_dfs.append(df_chunk)
193
+
194
+ # Preparar o início do próximo chunk
195
+ current_start = chunk_end + pd.DateOffset(days=1)
196
+
197
+ if not all_dfs:
198
+ return pd.DataFrame()
199
+
200
+ # 4. Concatenar todos os DataFrames e retornar o resultado
201
+ final_df = pd.concat(all_dfs, ignore_index=True)
202
+ return final_df
203
+
204
+
135
205
  def selic_over_series(
136
206
  start: DateScalar | None = None, end: DateScalar | None = None
137
207
  ) -> pd.DataFrame:
@@ -1 +0,0 @@
1
- __version__ = "0.36.2"
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
File without changes
File without changes
File without changes