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.
- {pyield-0.36.2 → pyield-0.36.4}/PKG-INFO +1 -1
- pyield-0.36.4/pyield/__about__.py +1 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/imaq.py +1 -1
- {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/auction.py +12 -2
- {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/bcdata.py +102 -32
- pyield-0.36.2/pyield/__about__.py +0 -1
- {pyield-0.36.2 → pyield-0.36.4}/.gitignore +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/LICENSE +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/README.md +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/__init__.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/__init__.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/ettj.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/ima.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/ipca.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/anbima/tpf.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/__init__.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/di1.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/di_over.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/futures/__init__.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/futures/common.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/futures/historical.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/futures/intraday.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/b3/futures/xml.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/__init__.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/ptax.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/repo.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/trades_intraday.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/trades_monthly.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/bc/vna.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/bday.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/conftest.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/data_cache.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/date_converter.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/fwd.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/holidays/__init__.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/holidays/br_holidays_new.txt +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/holidays/br_holidays_old.txt +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/holidays/brholidays.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/ibge/__init__.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/ibge/ipca.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/interpolator.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/py.typed +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/retry.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/__init__.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/benchmark.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/lft.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/ltn.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/ntnb.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/ntnc.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/ntnf.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/pre.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyield/tn/tools.py +0 -0
- {pyield-0.36.2 → pyield-0.36.4}/pyproject.toml +0 -0
|
@@ -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]
|
|
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
|
-
|
|
199
|
-
|
|
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
|
|
75
|
-
serie: BCSerie,
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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()
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|