PYield 0.32.0__tar.gz → 0.32.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.32.0 → pyield-0.32.2}/PKG-INFO +2 -2
- pyield-0.32.2/pyield/__about__.py +1 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/anbima/tpf.py +2 -2
- {pyield-0.32.0 → pyield-0.32.2}/pyield/b3/di.py +21 -18
- {pyield-0.32.0 → pyield-0.32.2}/pyield/bc/bcdata.py +17 -7
- {pyield-0.32.0 → pyield-0.32.2}/pyield/bday.py +59 -34
- pyield-0.32.2/pyield/data_cache.py +70 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/holidays/br_holidays_new.txt +105 -0
- pyield-0.32.2/pyield/retry.py +154 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyproject.toml +1 -1
- pyield-0.32.0/pyield/__about__.py +0 -1
- pyield-0.32.0/pyield/data_cache.py +0 -184
- pyield-0.32.0/pyield/retry.py +0 -56
- {pyield-0.32.0 → pyield-0.32.2}/.gitignore +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/LICENSE +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/README.md +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/__init__.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/anbima/__init__.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/anbima/ettj.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/anbima/ima.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/anbima/imaq.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/anbima/ipca.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/b3/__init__.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/b3/di_over.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/b3/futures/__init__.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/b3/futures/common.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/b3/futures/historical.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/b3/futures/intraday.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/b3/futures/xml.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/bc/__init__.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/bc/auction.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/bc/ptax.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/bc/repo.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/bc/trade.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/bc/vna.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/conftest.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/date_converter.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/forward.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/holidays/__init__.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/holidays/br_holidays_old.txt +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/holidays/brholidays.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/ibge/__init__.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/ibge/ipca.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/interpolator.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/py.typed +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/tpf/__init__.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/tpf/lft.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/tpf/ltn.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/tpf/ntnb.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/tpf/ntnc.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/tpf/ntnf.py +0 -0
- {pyield-0.32.0 → pyield-0.32.2}/pyield/tpf/tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PYield
|
|
3
|
-
Version: 0.32.
|
|
3
|
+
Version: 0.32.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>
|
|
@@ -34,7 +34,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
34
34
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
35
35
|
Requires-Python: >=3.11
|
|
36
36
|
Requires-Dist: numpy>=2.0.0
|
|
37
|
-
Requires-Dist: pandas[html]>=2.0.0
|
|
37
|
+
Requires-Dist: pandas[html,parquet]>=2.0.0
|
|
38
38
|
Requires-Dist: requests>=2.31.0
|
|
39
39
|
Requires-Dist: tenacity>=9.0.0
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.32.2"
|
|
@@ -5,7 +5,7 @@ from urllib.error import HTTPError
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
|
|
7
7
|
from pyield import bday
|
|
8
|
-
from pyield.data_cache import
|
|
8
|
+
from pyield.data_cache import get_cached_dataset
|
|
9
9
|
from pyield.date_converter import DateScalar, convert_input_dates
|
|
10
10
|
from pyield.retry import default_retry
|
|
11
11
|
|
|
@@ -154,7 +154,7 @@ def tpf_data(
|
|
|
154
154
|
- Price: The price (PU) of the bond.
|
|
155
155
|
"""
|
|
156
156
|
|
|
157
|
-
df =
|
|
157
|
+
df = get_cached_dataset("TPF")
|
|
158
158
|
date = convert_input_dates(date)
|
|
159
159
|
df = df.query("ReferenceDate == @date").reset_index(drop=True)
|
|
160
160
|
|
|
@@ -7,7 +7,7 @@ import pandas as pd
|
|
|
7
7
|
import pyield.date_converter as dc
|
|
8
8
|
import pyield.forward as fw
|
|
9
9
|
from pyield import b3, bday, interpolator
|
|
10
|
-
from pyield.data_cache import
|
|
10
|
+
from pyield.data_cache import get_cached_dataset
|
|
11
11
|
from pyield.date_converter import DateArray, DateScalar
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
@@ -78,7 +78,7 @@ class DIFutures:
|
|
|
78
78
|
Returns a sorted series of unique trade dates available in the DI dataset.
|
|
79
79
|
"""
|
|
80
80
|
return (
|
|
81
|
-
|
|
81
|
+
get_cached_dataset("DI")
|
|
82
82
|
.drop_duplicates(subset=["TradeDate"])["TradeDate"]
|
|
83
83
|
.sort_values(ascending=True)
|
|
84
84
|
.reset_index(drop=True)
|
|
@@ -110,21 +110,25 @@ class DIFutures:
|
|
|
110
110
|
|
|
111
111
|
def _update_df(self) -> pd.DataFrame:
|
|
112
112
|
"""Retrieve DI contract DataFrame for the initialized trade date."""
|
|
113
|
-
if self.
|
|
113
|
+
if self.date is None:
|
|
114
114
|
logger.info("No date specified. Returning empty DataFrame.")
|
|
115
115
|
return pd.DataFrame()
|
|
116
116
|
# Return an empty DataFrame if the trade date is a holiday
|
|
117
|
-
if not bday.is_business_day(self.
|
|
117
|
+
if not bday.is_business_day(self.date):
|
|
118
118
|
logger.warning("Specified date is not a business day.")
|
|
119
119
|
logger.warning("Returning empty DataFrame.")
|
|
120
120
|
return pd.DataFrame()
|
|
121
121
|
|
|
122
122
|
# Get historical data
|
|
123
|
-
df =
|
|
123
|
+
df = (
|
|
124
|
+
get_cached_dataset("DI")
|
|
125
|
+
.query("TradeDate == @self.date")
|
|
126
|
+
.reset_index(drop=True)
|
|
127
|
+
)
|
|
124
128
|
|
|
125
129
|
if df.empty:
|
|
126
130
|
logger.info("No historical data found. Trying real-time data.")
|
|
127
|
-
df = b3.futures(contract_code="DI1", date=self.
|
|
131
|
+
df = b3.futures(contract_code="DI1", date=self.date)
|
|
128
132
|
|
|
129
133
|
if df.empty:
|
|
130
134
|
logger.warning("No DI Futures data found for the specified date.")
|
|
@@ -134,13 +138,11 @@ class DIFutures:
|
|
|
134
138
|
if "DaysToExpiration" in df.columns:
|
|
135
139
|
df.drop(columns=["DaysToExpiration"], inplace=True)
|
|
136
140
|
|
|
137
|
-
if self.
|
|
141
|
+
if self.pre_filter:
|
|
138
142
|
df_pre = (
|
|
139
|
-
|
|
143
|
+
get_cached_dataset("TPF")
|
|
140
144
|
.query("BondType in ['LTN', 'NTN-F']")
|
|
141
|
-
.query("ReferenceDate == @self.
|
|
142
|
-
["ReferenceDate", "MaturityDate"]
|
|
143
|
-
]
|
|
145
|
+
.query("ReferenceDate == @self.date")[["ReferenceDate", "MaturityDate"]]
|
|
144
146
|
.drop_duplicates()
|
|
145
147
|
.reset_index(drop=True)
|
|
146
148
|
)
|
|
@@ -155,12 +157,12 @@ class DIFutures:
|
|
|
155
157
|
|
|
156
158
|
df = df.merge(df_pre, how="inner")
|
|
157
159
|
|
|
158
|
-
if self.
|
|
160
|
+
if self.month_start:
|
|
159
161
|
df["ExpirationDate"] = (
|
|
160
162
|
df["ExpirationDate"].dt.to_period("M").dt.to_timestamp()
|
|
161
163
|
)
|
|
162
164
|
|
|
163
|
-
if not self.
|
|
165
|
+
if not self.all_columns:
|
|
164
166
|
cols = [
|
|
165
167
|
"TradeDate",
|
|
166
168
|
"TickerSymbol",
|
|
@@ -273,7 +275,7 @@ class DIFutures:
|
|
|
273
275
|
|
|
274
276
|
# Load DI rates dataset filtered by the provided reference dates
|
|
275
277
|
dfr = (
|
|
276
|
-
|
|
278
|
+
get_cached_dataset("DI")
|
|
277
279
|
.query("TradeDate in @dfi['tdate'].unique()")
|
|
278
280
|
.reset_index(drop=True)
|
|
279
281
|
)
|
|
@@ -316,8 +318,10 @@ class DIFutures:
|
|
|
316
318
|
) -> float:
|
|
317
319
|
"""Retrieve the DI rate for a specified expiration date."""
|
|
318
320
|
expiration = dc.convert_input_dates(expiration)
|
|
321
|
+
if not self.date:
|
|
322
|
+
return float("NaN")
|
|
319
323
|
|
|
320
|
-
if self.
|
|
324
|
+
if self.month_start:
|
|
321
325
|
# Force the expiration date to be the start of the month
|
|
322
326
|
expiration = expiration.to_period("M").to_timestamp()
|
|
323
327
|
else:
|
|
@@ -349,11 +353,11 @@ class DIFutures:
|
|
|
349
353
|
known_rates=df["SettlementRate"],
|
|
350
354
|
extrapolate=extrapolate,
|
|
351
355
|
)
|
|
352
|
-
bd = bday.count(self.
|
|
356
|
+
bd = bday.count(self.date, expiration)
|
|
353
357
|
return ff_interp(bd)
|
|
354
358
|
|
|
355
359
|
@property
|
|
356
|
-
def date(self) -> pd.Timestamp:
|
|
360
|
+
def date(self) -> pd.Timestamp | None:
|
|
357
361
|
"""
|
|
358
362
|
The trade date to retrieve the DI contract data.
|
|
359
363
|
|
|
@@ -363,7 +367,6 @@ class DIFutures:
|
|
|
363
367
|
Returns:
|
|
364
368
|
pd.Timestamp: The trade date set for this instance.
|
|
365
369
|
"""
|
|
366
|
-
|
|
367
370
|
return self._date
|
|
368
371
|
|
|
369
372
|
@date.setter
|
|
@@ -19,6 +19,7 @@ from enum import Enum
|
|
|
19
19
|
from urllib.error import HTTPError
|
|
20
20
|
|
|
21
21
|
import pandas as pd
|
|
22
|
+
import requests
|
|
22
23
|
|
|
23
24
|
from pyield.date_converter import DateScalar, convert_input_dates
|
|
24
25
|
from pyield.retry import default_retry
|
|
@@ -63,7 +64,7 @@ def _build_download_url(
|
|
|
63
64
|
end_str = end.strftime("%d/%m/%Y")
|
|
64
65
|
|
|
65
66
|
api_url = BASE_URL
|
|
66
|
-
api_url += f"{serie.value}/dados?formato=
|
|
67
|
+
api_url += f"{serie.value}/dados?formato=json"
|
|
67
68
|
api_url += f"&dataInicial={start_str}&dataFinal={end_str}"
|
|
68
69
|
|
|
69
70
|
return api_url
|
|
@@ -91,16 +92,29 @@ def _fetch_data_from_url(
|
|
|
91
92
|
api_url = _build_download_url(serie, start, end)
|
|
92
93
|
|
|
93
94
|
try:
|
|
94
|
-
|
|
95
|
+
response = requests.get(api_url, timeout=30)
|
|
96
|
+
response.raise_for_status() # Raise exception for 4XX/5XX status codes
|
|
97
|
+
data = response.json()
|
|
98
|
+
|
|
99
|
+
if not data:
|
|
100
|
+
logger.warning(f"No data available for the requested period: {api_url}")
|
|
101
|
+
return pd.DataFrame()
|
|
102
|
+
|
|
103
|
+
# Convert JSON to DataFrame
|
|
104
|
+
df = pd.DataFrame(data)
|
|
105
|
+
|
|
95
106
|
if df.empty or "valor" not in df.columns:
|
|
96
107
|
logger.warning(f"No data available for the requested period: {api_url}")
|
|
97
108
|
return pd.DataFrame()
|
|
98
109
|
|
|
99
110
|
# Process the dataframe
|
|
111
|
+
df = pd.DataFrame(data, dtype="string")
|
|
100
112
|
df = df.rename(columns={"data": "Date", "valor": "Value"})
|
|
113
|
+
|
|
101
114
|
df["Date"] = pd.to_datetime(df["Date"], format="%d/%m/%Y")
|
|
102
115
|
# Value is in percentage format, so divide by 100
|
|
103
|
-
df["Value"]
|
|
116
|
+
df["Value"] = (df["Value"].astype("Float64")) / 100
|
|
117
|
+
|
|
104
118
|
return df
|
|
105
119
|
except HTTPError as e:
|
|
106
120
|
if e.code == 404: # noqa
|
|
@@ -142,10 +156,6 @@ def selic_over_series(
|
|
|
142
156
|
|
|
143
157
|
Examples:
|
|
144
158
|
>>> from pyield import bc
|
|
145
|
-
>>> bc.selic_over_series("31-05-2024", "31-05-2024")
|
|
146
|
-
Date Value
|
|
147
|
-
0 2024-05-31 0.104
|
|
148
|
-
|
|
149
159
|
>>> # No data on 26-01-2025 (sunday). Rate changed due to Copom meeting.
|
|
150
160
|
>>> bc.selic_over_series("26-01-2025") # Returns all data since 26-01-2025
|
|
151
161
|
Date Value
|
|
@@ -8,7 +8,6 @@ import pyield.date_converter as dc
|
|
|
8
8
|
import pyield.holidays as hl
|
|
9
9
|
from pyield.date_converter import DateArray, DateScalar
|
|
10
10
|
|
|
11
|
-
# Timezone for Brazil
|
|
12
11
|
TIMEZONE_BZ = ZoneInfo("America/Sao_Paulo")
|
|
13
12
|
|
|
14
13
|
IntegerScalar = int | np.integer
|
|
@@ -62,7 +61,12 @@ def offset(
|
|
|
62
61
|
|
|
63
62
|
Args:
|
|
64
63
|
dates (DateScalar | DateArray): The date(s) to offset. Can be a scalar date type
|
|
65
|
-
or a collection of dates.
|
|
64
|
+
or a collection of dates. **Transition Handling:** Due to a change in
|
|
65
|
+
Brazilian national holidays effective from 2023-12-26 (`TRANSITION_DATE`),
|
|
66
|
+
this function automatically selects the appropriate holiday list
|
|
67
|
+
(old or new) based on **each individual date** in the `dates` input.
|
|
68
|
+
Dates before 2023-12-26 use the old list for their offset calculation,
|
|
69
|
+
while dates on or after 2023-12-26 use the new list.
|
|
66
70
|
offset (int | Series | np.ndarray | list[int] | tuple[int], optional):
|
|
67
71
|
The number of business days to offset the dates. Positive for future dates,
|
|
68
72
|
negative for past dates. Zero will return the same date if it's a business
|
|
@@ -142,37 +146,40 @@ def offset(
|
|
|
142
146
|
|
|
143
147
|
if isinstance(dates_pd, pd.Series):
|
|
144
148
|
# Divide the input in order to apply the correct holiday list
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
old_dates = dates_pd[dates_pd < br_holidays.TRANSITION_DATE]
|
|
150
|
+
new_dates = dates_pd[dates_pd >= br_holidays.TRANSITION_DATE]
|
|
151
|
+
# nat = NaT = Not a Time = missing value
|
|
152
|
+
nat_dates = dates_pd[dates_pd.isna()]
|
|
148
153
|
|
|
149
|
-
|
|
150
|
-
dc.to_numpy_date_type(
|
|
154
|
+
offsetted_old_dates = np.busday_offset(
|
|
155
|
+
dc.to_numpy_date_type(old_dates),
|
|
151
156
|
offsets=offset,
|
|
152
157
|
roll=roll,
|
|
153
158
|
holidays=OLD_HOLIDAYS_ARRAY,
|
|
154
159
|
)
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
dc.to_numpy_date_type(
|
|
161
|
+
offsetted_new_dates = np.busday_offset(
|
|
162
|
+
dc.to_numpy_date_type(new_dates),
|
|
158
163
|
offsets=offset,
|
|
159
164
|
roll=roll,
|
|
160
165
|
holidays=NEW_HOLIDAYS_ARRAY,
|
|
161
166
|
)
|
|
162
167
|
|
|
163
168
|
# Convert from numpy.datetime64 to pandas.Timestamp
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
offsetted_old_dates = pd.to_datetime(offsetted_old_dates)
|
|
170
|
+
offsetted_new_dates = pd.to_datetime(offsetted_new_dates)
|
|
166
171
|
|
|
167
172
|
# 'pd.to_datetime' does not necessarily return datetime64[ns] Series
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
offsetted_old_dates = pd.Series(offsetted_old_dates).astype("datetime64[ns]")
|
|
174
|
+
offsetted_new_dates = pd.Series(offsetted_new_dates).astype("datetime64[ns]")
|
|
170
175
|
|
|
171
176
|
# Use old index to rejoin the results
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
offsetted_old_dates.index = old_dates.index
|
|
178
|
+
offsetted_new_dates.index = new_dates.index
|
|
174
179
|
|
|
175
|
-
offsetted_dates = pd.concat(
|
|
180
|
+
offsetted_dates = pd.concat(
|
|
181
|
+
[offsetted_old_dates, offsetted_new_dates, nat_dates]
|
|
182
|
+
)
|
|
176
183
|
|
|
177
184
|
# Reorder the result to match the original input order
|
|
178
185
|
return offsetted_dates.sort_index()
|
|
@@ -224,10 +231,13 @@ def count(
|
|
|
224
231
|
applies to the calculation.
|
|
225
232
|
|
|
226
233
|
Args:
|
|
227
|
-
start (DateScalar | DateArray): The start date(s) for counting.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
234
|
+
start (DateScalar | DateArray): The start date(s) for counting (inclusive).
|
|
235
|
+
**Transition Handling:** The holiday list used for the *entire* counting
|
|
236
|
+
period between `start` and `end` is determined solely by the `start` date's
|
|
237
|
+
relation to the holiday transition date (2023-12-26). If `start` is before
|
|
238
|
+
this date, the old holiday list is used for the whole count, even if `end`
|
|
239
|
+
is after it. If `start` is on or after this date, new holiday list is used.
|
|
240
|
+
end (DateScalar | DateArray): The end date(s) for counting (exclusive).
|
|
231
241
|
|
|
232
242
|
Returns:
|
|
233
243
|
int | pd.Series: Returns an integer if `start` and `end` are single dates,
|
|
@@ -235,20 +245,19 @@ def count(
|
|
|
235
245
|
|
|
236
246
|
Notes:
|
|
237
247
|
- This function is a wrapper around `numpy.busday_count`, adapted to work
|
|
238
|
-
|
|
248
|
+
directly with various Pandas and Numpy date formats.
|
|
239
249
|
- It supports flexible date inputs, including single dates, lists, Series, and
|
|
240
|
-
|
|
250
|
+
more, for both `start` and `end` parameters.
|
|
241
251
|
- The return type depends on the input types: single dates return an int, while
|
|
242
|
-
|
|
252
|
+
arrays of dates return a pd.Series with the count for each date range.
|
|
243
253
|
- The `start` date determines the holiday list, ensuring consistency with the
|
|
244
|
-
|
|
254
|
+
applicable calendar at the time.
|
|
245
255
|
- See `numpy.busday_count` documentation for more details on how holidays are
|
|
246
|
-
|
|
247
|
-
|
|
256
|
+
handled and how business day counts are calculated:
|
|
257
|
+
https://numpy.org/doc/stable/reference/generated/numpy.busday_count.html.
|
|
248
258
|
|
|
249
259
|
Examples:
|
|
250
260
|
>>> from pyield import bday
|
|
251
|
-
|
|
252
261
|
>>> bday.count("15-12-2023", "01-01-2024")
|
|
253
262
|
10
|
|
254
263
|
|
|
@@ -334,7 +343,7 @@ def generate(
|
|
|
334
343
|
start: DateScalar | None = None,
|
|
335
344
|
end: DateScalar | None = None,
|
|
336
345
|
inclusive: Literal["both", "neither", "left", "right"] = "both",
|
|
337
|
-
holiday_option: Literal["old", "new", "infer"] = "
|
|
346
|
+
holiday_option: Literal["old", "new", "infer"] = "new",
|
|
338
347
|
) -> pd.Series:
|
|
339
348
|
"""
|
|
340
349
|
Generates a Series of business days between a `start` and `end` date, considering
|
|
@@ -350,9 +359,14 @@ def generate(
|
|
|
350
359
|
Determines which of the start and end dates are included in the result.
|
|
351
360
|
Valid options are 'both', 'neither', 'left', 'right'. Defaults to 'both'.
|
|
352
361
|
holiday_option (Literal["old", "new", "infer"], optional):
|
|
353
|
-
Specifies the list of holidays to consider.
|
|
354
|
-
|
|
355
|
-
|
|
362
|
+
Specifies the list of holidays to consider. Defaults to "new".
|
|
363
|
+
- **'old'**: Uses the holiday list effective *before* the transition date
|
|
364
|
+
of 2023-12-26.
|
|
365
|
+
- **'new'**: Uses the holiday list effective *on and after* the transition
|
|
366
|
+
date of 2023-12-26.
|
|
367
|
+
- **'infer'**: Automatically selects the holiday list ('old' or 'new') based
|
|
368
|
+
on the `start` date relative to the transition date (2023-12-26). If `start`
|
|
369
|
+
is before the transition, 'old' is used; otherwise, 'new' is used.
|
|
356
370
|
|
|
357
371
|
Returns:
|
|
358
372
|
pd.Series: A Series representing a range of business days between the specified
|
|
@@ -384,7 +398,7 @@ def generate(
|
|
|
384
398
|
else:
|
|
385
399
|
end_pd = pd.Timestamp.today()
|
|
386
400
|
|
|
387
|
-
|
|
401
|
+
applicable_holidays = br_holidays.get_holiday_series(
|
|
388
402
|
dates=start_pd, holiday_option=holiday_option
|
|
389
403
|
).to_list()
|
|
390
404
|
|
|
@@ -394,8 +408,9 @@ def generate(
|
|
|
394
408
|
end=end_pd,
|
|
395
409
|
freq="C",
|
|
396
410
|
inclusive=inclusive,
|
|
397
|
-
holidays=
|
|
411
|
+
holidays=applicable_holidays,
|
|
398
412
|
)
|
|
413
|
+
|
|
399
414
|
return pd.Series(result_dti.values)
|
|
400
415
|
|
|
401
416
|
|
|
@@ -413,9 +428,14 @@ def is_business_day(date: DateScalar) -> bool:
|
|
|
413
428
|
>>> from pyield import bday
|
|
414
429
|
>>> bday.is_business_day("25-12-2023") # Christmas
|
|
415
430
|
False
|
|
431
|
+
|
|
432
|
+
Notes:
|
|
433
|
+
- This function correctly identifies business days according to the applicable
|
|
434
|
+
Brazilian holiday list (before or after the 2023-12-26 transition), based
|
|
435
|
+
on the input `date`.
|
|
416
436
|
"""
|
|
417
437
|
date_pd = dc.convert_input_dates(date)
|
|
418
|
-
shifted_date = offset(date_pd, 0) # Shift the date if it is not a
|
|
438
|
+
shifted_date = offset(date_pd, 0) # Shift the date if it is not a business day
|
|
419
439
|
return date_pd == shifted_date
|
|
420
440
|
|
|
421
441
|
|
|
@@ -428,6 +448,11 @@ def last_business_day() -> pd.Timestamp:
|
|
|
428
448
|
Returns:
|
|
429
449
|
pd.Timestamp: The last business day in Brazil.
|
|
430
450
|
|
|
451
|
+
Notes:
|
|
452
|
+
- The determination of the last business day considers the correct Brazilian
|
|
453
|
+
holiday list (before or after the 2023-12-26 transition) applicable to
|
|
454
|
+
the current date.
|
|
455
|
+
|
|
431
456
|
"""
|
|
432
457
|
# Get the current date in Brazil without timezone information
|
|
433
458
|
bz_today = pd.Timestamp.now(TIMEZONE_BZ).normalize().tz_localize(None)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from zoneinfo import ZoneInfo
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
# --- Configurações Centralizadas ---
|
|
9
|
+
TIMEZONE_BZ = ZoneInfo("America/Sao_Paulo")
|
|
10
|
+
|
|
11
|
+
GIT_URL = "https://raw.githubusercontent.com/crdcj/pyield-data/main"
|
|
12
|
+
# Diretório base onde os arquivos parquet estão localizados
|
|
13
|
+
BASE_DATA_URL = f"{GIT_URL}/data"
|
|
14
|
+
|
|
15
|
+
# Dicionário global para configurações dos datasets usando 'filename'
|
|
16
|
+
DATASET_CONFIGS = {
|
|
17
|
+
"DI": {
|
|
18
|
+
"filename": "b3_di.parquet", # Apenas o nome do arquivo
|
|
19
|
+
"date_column": "TradeDate",
|
|
20
|
+
"name": "Futuro de DI (B3)",
|
|
21
|
+
},
|
|
22
|
+
"TPF": {
|
|
23
|
+
"filename": "anbima_tpf.parquet", # Apenas o nome do arquivo
|
|
24
|
+
"date_column": "ReferenceDate",
|
|
25
|
+
"name": "TPF (ANBIMA)",
|
|
26
|
+
},
|
|
27
|
+
# --- Adicionar futuros datasets aqui ---
|
|
28
|
+
# "NOVODATASET": {
|
|
29
|
+
# "filename": "novo_dataset.parquet",
|
|
30
|
+
# "date_column": "DataReferencia",
|
|
31
|
+
# "name": "Novo Dataset Exemplo"
|
|
32
|
+
# },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_today_date_key():
|
|
39
|
+
"""Retorna a data atual como string no formato YYYY-MM-DD."""
|
|
40
|
+
return datetime.now(TIMEZONE_BZ).strftime("%Y-%m-%d")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@functools.lru_cache(maxsize=len(DATASET_CONFIGS))
|
|
44
|
+
def _get_dataset_with_ttl(dataset_id: str, date_key: str) -> pd.DataFrame:
|
|
45
|
+
"""Carrega o dataset a partir do ID e da chave de data. Cache expira diariamente."""
|
|
46
|
+
if dataset_id not in DATASET_CONFIGS:
|
|
47
|
+
raise ValueError(f"Dataset com ID '{dataset_id}' não encontrado.")
|
|
48
|
+
|
|
49
|
+
config = DATASET_CONFIGS[dataset_id]
|
|
50
|
+
filename = config["filename"]
|
|
51
|
+
date_column = config["date_column"]
|
|
52
|
+
dataset_name = config.get("name", dataset_id)
|
|
53
|
+
|
|
54
|
+
# Constrói a URL completa
|
|
55
|
+
full_url = f"{BASE_DATA_URL}/{filename}"
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
df = pd.read_parquet(full_url)
|
|
59
|
+
last_date = df[date_column].max().date()
|
|
60
|
+
logger.info(f"Dataset {dataset_name} carregado. Última data: {last_date}")
|
|
61
|
+
return df.copy()
|
|
62
|
+
|
|
63
|
+
except Exception:
|
|
64
|
+
logger.exception(f"Erro ao carregar dataset {dataset_name} ({dataset_id})")
|
|
65
|
+
raise # Re-raise a exceção para que o chamador saiba que falhou
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_cached_dataset(dataset_id: str) -> pd.DataFrame:
|
|
69
|
+
"""Obtém um dataset configurado pelo seu ID. O cache expira diariamente."""
|
|
70
|
+
return _get_dataset_with_ttl(dataset_id, _get_today_date_key())
|
|
@@ -3,6 +3,111 @@
|
|
|
3
3
|
# Direct link to the ANBIMA file: https://www.anbima.com.br/feriados/arqs/feriados_nacionais.xls
|
|
4
4
|
# In december 2023, a new holiday was added: "Dia Nacional de Zumbi"
|
|
5
5
|
# Note: dates until 22-12-2023 (inclusive) must use "old_br_holidays.txt" list
|
|
6
|
+
01/01/1990
|
|
7
|
+
26/02/1990
|
|
8
|
+
27/02/1990
|
|
9
|
+
01/05/1990
|
|
10
|
+
14/06/1990
|
|
11
|
+
07/09/1990
|
|
12
|
+
12/10/1990
|
|
13
|
+
02/11/1990
|
|
14
|
+
15/11/1990
|
|
15
|
+
25/12/1990
|
|
16
|
+
01/01/1991
|
|
17
|
+
11/02/1991
|
|
18
|
+
12/02/1991
|
|
19
|
+
29/03/1991
|
|
20
|
+
01/05/1991
|
|
21
|
+
30/05/1991
|
|
22
|
+
15/11/1991
|
|
23
|
+
25/12/1991
|
|
24
|
+
01/01/1992
|
|
25
|
+
02/03/1992
|
|
26
|
+
03/03/1992
|
|
27
|
+
17/04/1992
|
|
28
|
+
21/04/1992
|
|
29
|
+
01/05/1992
|
|
30
|
+
18/06/1992
|
|
31
|
+
07/09/1992
|
|
32
|
+
12/10/1992
|
|
33
|
+
02/11/1992
|
|
34
|
+
25/12/1992
|
|
35
|
+
01/01/1993
|
|
36
|
+
22/02/1993
|
|
37
|
+
23/02/1993
|
|
38
|
+
09/04/1993
|
|
39
|
+
21/04/1993
|
|
40
|
+
10/06/1993
|
|
41
|
+
07/09/1993
|
|
42
|
+
12/10/1993
|
|
43
|
+
02/11/1993
|
|
44
|
+
15/11/1993
|
|
45
|
+
14/02/1994
|
|
46
|
+
15/02/1994
|
|
47
|
+
01/04/1994
|
|
48
|
+
21/04/1994
|
|
49
|
+
02/06/1994
|
|
50
|
+
07/09/1994
|
|
51
|
+
12/10/1994
|
|
52
|
+
02/11/1994
|
|
53
|
+
15/11/1994
|
|
54
|
+
27/02/1995
|
|
55
|
+
28/02/1995
|
|
56
|
+
14/04/1995
|
|
57
|
+
21/04/1995
|
|
58
|
+
01/05/1995
|
|
59
|
+
15/06/1995
|
|
60
|
+
07/09/1995
|
|
61
|
+
12/10/1995
|
|
62
|
+
02/11/1995
|
|
63
|
+
15/11/1995
|
|
64
|
+
25/12/1995
|
|
65
|
+
01/01/1996
|
|
66
|
+
19/02/1996
|
|
67
|
+
20/02/1996
|
|
68
|
+
05/04/1996
|
|
69
|
+
01/05/1996
|
|
70
|
+
06/06/1996
|
|
71
|
+
15/11/1996
|
|
72
|
+
25/12/1996
|
|
73
|
+
01/01/1997
|
|
74
|
+
10/02/1997
|
|
75
|
+
11/02/1997
|
|
76
|
+
28/03/1997
|
|
77
|
+
21/04/1997
|
|
78
|
+
01/05/1997
|
|
79
|
+
29/05/1997
|
|
80
|
+
25/12/1997
|
|
81
|
+
01/01/1998
|
|
82
|
+
23/02/1998
|
|
83
|
+
24/02/1998
|
|
84
|
+
10/04/1998
|
|
85
|
+
21/04/1998
|
|
86
|
+
01/05/1998
|
|
87
|
+
11/06/1998
|
|
88
|
+
07/09/1998
|
|
89
|
+
12/10/1998
|
|
90
|
+
02/11/1998
|
|
91
|
+
25/12/1998
|
|
92
|
+
01/01/1999
|
|
93
|
+
15/02/1999
|
|
94
|
+
16/02/1999
|
|
95
|
+
02/04/1999
|
|
96
|
+
21/04/1999
|
|
97
|
+
03/06/1999
|
|
98
|
+
07/09/1999
|
|
99
|
+
12/10/1999
|
|
100
|
+
02/11/1999
|
|
101
|
+
15/11/1999
|
|
102
|
+
06/03/2000
|
|
103
|
+
07/03/2000
|
|
104
|
+
01/05/2000
|
|
105
|
+
22/06/2000
|
|
106
|
+
07/09/2000
|
|
107
|
+
12/10/2000
|
|
108
|
+
02/11/2000
|
|
109
|
+
15/11/2000
|
|
110
|
+
25/12/2000
|
|
6
111
|
01/01/2001
|
|
7
112
|
26/02/2001
|
|
8
113
|
27/02/2001
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo de retry configurável para chamadas de API e processamento de dados.
|
|
3
|
+
|
|
4
|
+
Este módulo fornece um decorador `tenacity.retry` pré-configurado (`default_retry`)
|
|
5
|
+
para lidar com erros transitórios comuns ao interagir com APIs web e ao
|
|
6
|
+
processar os dados resultantes (ex: parsing de CSV/JSON).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from urllib.error import HTTPError, URLError
|
|
11
|
+
|
|
12
|
+
# Dependências de terceiros
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import tenacity
|
|
15
|
+
|
|
16
|
+
# Importar tipos específicos necessários
|
|
17
|
+
from tenacity import RetryCallState
|
|
18
|
+
|
|
19
|
+
# --- Configuração do Logger ---
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# --- Constantes ---
|
|
23
|
+
DEFAULT_TIMEOUT = (2, 10)
|
|
24
|
+
RETRYABLE_NETWORK_ERRORS = (URLError,)
|
|
25
|
+
RETRYABLE_PARSING_ERRORS = (pd.errors.EmptyDataError, pd.errors.ParserError)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --- Funções Auxiliares para Tenacity ---
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _log_before_sleep(retry_state: RetryCallState):
|
|
32
|
+
"""
|
|
33
|
+
Loga uma mensagem ANTES de o Tenacity entrar em espera (sleep) entre tentativas.
|
|
34
|
+
|
|
35
|
+
Simplificada para confiar no contrato do Tenacity e usar asserções
|
|
36
|
+
para verificar o estado esperado.
|
|
37
|
+
"""
|
|
38
|
+
# 1. Verificar o resultado (outcome)
|
|
39
|
+
outcome = retry_state.outcome
|
|
40
|
+
assert outcome is not None, "before_sleep chamada sem 'outcome'"
|
|
41
|
+
# Adicionado: Garantir que realmente falhou antes de logar como falha
|
|
42
|
+
assert outcome.failed, "before_sleep chamada para 'outcome' bem-sucedido"
|
|
43
|
+
|
|
44
|
+
exception = outcome.exception
|
|
45
|
+
# Adicionado: Garantir que a exceção existe quando falhou
|
|
46
|
+
assert exception is not None, "before_sleep chamada sem 'exception' no outcome"
|
|
47
|
+
|
|
48
|
+
# 2. Verificar a próxima ação (next_action)
|
|
49
|
+
next_action = retry_state.next_action
|
|
50
|
+
assert next_action is not None, "before_sleep chamada sem 'next_action'"
|
|
51
|
+
|
|
52
|
+
# 3. Acessar a duração do sono (sleep)
|
|
53
|
+
# Confiamos que 'next_action' terá '.sleep' neste contexto, pois é antes de dormir.
|
|
54
|
+
sleep_duration = next_action.sleep
|
|
55
|
+
|
|
56
|
+
# Log principal
|
|
57
|
+
logger.warning(
|
|
58
|
+
f"Tentativa {retry_state.attempt_number} falhou com "
|
|
59
|
+
f"{type(exception).__name__}: {exception}. Tentando novamente em "
|
|
60
|
+
f"{sleep_duration:.2f} segundos..."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def should_retry_exception(retry_state: RetryCallState) -> bool:
|
|
65
|
+
"""Determina se uma exceção capturada justifica uma nova tentativa."""
|
|
66
|
+
if not retry_state.outcome or not retry_state.outcome.failed:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# Usar o atributo .exception
|
|
70
|
+
exception = retry_state.outcome.exception
|
|
71
|
+
assert exception is not None, (
|
|
72
|
+
"should_retry_exception processando um outcome sem exceção?"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
attempt_number = retry_state.attempt_number
|
|
76
|
+
url = getattr(exception, "url", "N/A")
|
|
77
|
+
|
|
78
|
+
retry_decision = False
|
|
79
|
+
log_message = ""
|
|
80
|
+
log_level = logging.ERROR
|
|
81
|
+
|
|
82
|
+
match exception:
|
|
83
|
+
case HTTPError(code=404):
|
|
84
|
+
log_message = (
|
|
85
|
+
f"Tentativa {attempt_number}: Recurso não encontrado (404) em {url}. "
|
|
86
|
+
f"Não tentará novamente."
|
|
87
|
+
)
|
|
88
|
+
retry_decision = False
|
|
89
|
+
log_level = logging.INFO
|
|
90
|
+
|
|
91
|
+
case HTTPError(code=504) if attempt_number == 1:
|
|
92
|
+
log_message = (
|
|
93
|
+
f"Tentativa {attempt_number}: Gateway Timeout (504) da API em {url}. "
|
|
94
|
+
f"Tentará novamente uma vez."
|
|
95
|
+
)
|
|
96
|
+
retry_decision = True
|
|
97
|
+
log_level = logging.WARNING
|
|
98
|
+
|
|
99
|
+
case HTTPError(code=504):
|
|
100
|
+
log_message = (
|
|
101
|
+
f"Tentativa {attempt_number}: Gateway Timeout (504) persistiu em {url}."
|
|
102
|
+
f"API parece indisponível. Desistindo."
|
|
103
|
+
)
|
|
104
|
+
retry_decision = False
|
|
105
|
+
log_level = logging.ERROR
|
|
106
|
+
|
|
107
|
+
case HTTPError() as http_err:
|
|
108
|
+
log_message = (
|
|
109
|
+
f"Tentativa {attempt_number}: Erro HTTP {http_err.code} encontrado "
|
|
110
|
+
f"para {url}. Pode ser transitório. Tentando novamente."
|
|
111
|
+
)
|
|
112
|
+
retry_decision = True
|
|
113
|
+
log_level = logging.WARNING
|
|
114
|
+
|
|
115
|
+
case URLError():
|
|
116
|
+
log_message = (
|
|
117
|
+
f"Tentativa {attempt_number}: Erro de rede recuperável "
|
|
118
|
+
f"({type(exception).__name__}). Tentando novamente."
|
|
119
|
+
)
|
|
120
|
+
retry_decision = True
|
|
121
|
+
log_level = logging.WARNING
|
|
122
|
+
|
|
123
|
+
case pd.errors.EmptyDataError() | pd.errors.ParserError():
|
|
124
|
+
log_message = (
|
|
125
|
+
f"Tentativa {attempt_number}: Erro de parsing "
|
|
126
|
+
f"({type(exception).__name__}). Tentando novamente."
|
|
127
|
+
)
|
|
128
|
+
retry_decision = True
|
|
129
|
+
log_level = logging.WARNING
|
|
130
|
+
|
|
131
|
+
case _:
|
|
132
|
+
log_message_exc = (
|
|
133
|
+
f"Tentativa {attempt_number}: Erro não recuperável/inesperado "
|
|
134
|
+
f"({type(exception).__name__}). Desistindo."
|
|
135
|
+
)
|
|
136
|
+
retry_decision = False
|
|
137
|
+
log_level = logging.ERROR
|
|
138
|
+
logger.exception(log_message_exc)
|
|
139
|
+
log_message = None
|
|
140
|
+
|
|
141
|
+
if log_message:
|
|
142
|
+
logger.log(log_level, log_message)
|
|
143
|
+
|
|
144
|
+
return retry_decision
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# --- Decorador Tenacity Principal ---
|
|
148
|
+
default_retry = tenacity.retry(
|
|
149
|
+
retry=should_retry_exception,
|
|
150
|
+
wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
|
|
151
|
+
stop=(tenacity.stop_after_attempt(3) | tenacity.stop_after_delay(15)),
|
|
152
|
+
before_sleep=_log_before_sleep,
|
|
153
|
+
reraise=True,
|
|
154
|
+
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.32.0"
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Módulo para carregamento e cache de dados financeiros.
|
|
3
|
-
Fornece acesso a dados DI e TPF com atualizações automáticas.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import logging
|
|
7
|
-
from functools import wraps
|
|
8
|
-
from typing import Any, Dict
|
|
9
|
-
from zoneinfo import ZoneInfo
|
|
10
|
-
|
|
11
|
-
import pandas as pd
|
|
12
|
-
import requests
|
|
13
|
-
|
|
14
|
-
from pyield import bday
|
|
15
|
-
|
|
16
|
-
# Configuração de logging
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
# Configurações
|
|
20
|
-
TIMEZONE_BZ = ZoneInfo("America/Sao_Paulo")
|
|
21
|
-
|
|
22
|
-
GIT_URL = "https://raw.githubusercontent.com/crdcj/pyield-data/main"
|
|
23
|
-
DI_URL = f"{GIT_URL}/data/b3_di.pkl.gz"
|
|
24
|
-
TPF_URL = f"{GIT_URL}/data/anbima_tpf.pkl.gz"
|
|
25
|
-
|
|
26
|
-
# Configuração de datasets
|
|
27
|
-
DATASETS_CONFIG = {
|
|
28
|
-
"di": {"url": f"{DI_URL}", "date_column": "TradeDate"},
|
|
29
|
-
"tpf": {"url": f"{TPF_URL}", "date_column": "ReferenceDate"},
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def singleton(cls):
|
|
34
|
-
"""Decorator para implementar o padrão Singleton."""
|
|
35
|
-
instances = {}
|
|
36
|
-
|
|
37
|
-
@wraps(cls)
|
|
38
|
-
def get_instance(*args, **kwargs):
|
|
39
|
-
if cls not in instances:
|
|
40
|
-
instances[cls] = cls(*args, **kwargs)
|
|
41
|
-
return instances[cls]
|
|
42
|
-
|
|
43
|
-
return get_instance
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@singleton
|
|
47
|
-
class DataCache:
|
|
48
|
-
"""
|
|
49
|
-
Classe para gerenciar o cache de datasets financeiros.
|
|
50
|
-
Implementa o padrão Singleton para garantir uma única instância.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
def __init__(self):
|
|
54
|
-
"""Inicializa o cache de dados vazio."""
|
|
55
|
-
self._datasets: Dict[str, Dict[str, Any]] = {}
|
|
56
|
-
|
|
57
|
-
# Inicializar configurações de datasets
|
|
58
|
-
for key, config in DATASETS_CONFIG.items():
|
|
59
|
-
self._datasets[key] = {
|
|
60
|
-
"url": config["url"],
|
|
61
|
-
"date_column": config["date_column"],
|
|
62
|
-
"df": None,
|
|
63
|
-
"last_date": None,
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
def _should_update_dataset(self, key: str) -> bool:
|
|
67
|
-
"""
|
|
68
|
-
Verifica se o dataset precisa ser atualizado.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
key: Identificador do dataset
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
True se o dataset precisar ser atualizado, False caso contrário
|
|
75
|
-
"""
|
|
76
|
-
data_info = self._datasets[key]
|
|
77
|
-
last_date_in_dataset = data_info["last_date"]
|
|
78
|
-
|
|
79
|
-
# Se nunca foi carregado, deve atualizar
|
|
80
|
-
if last_date_in_dataset is None:
|
|
81
|
-
return True
|
|
82
|
-
|
|
83
|
-
bz_last_bday = bday.last_business_day().date()
|
|
84
|
-
n_bdays = bday.count(last_date_in_dataset, bz_last_bday)
|
|
85
|
-
|
|
86
|
-
# Atualizar se houver mais de 1 dia útil de diferença
|
|
87
|
-
return n_bdays > 1 # noqa
|
|
88
|
-
|
|
89
|
-
def _load_dataset(self, key: str) -> None:
|
|
90
|
-
"""
|
|
91
|
-
Carrega ou atualiza um dataset.
|
|
92
|
-
|
|
93
|
-
Args:
|
|
94
|
-
key: Identificador do dataset
|
|
95
|
-
|
|
96
|
-
Raises:
|
|
97
|
-
requests.RequestException: Se ocorrer erro ao fazer download dos dados
|
|
98
|
-
ValueError: Se o formato dos dados for inválido
|
|
99
|
-
"""
|
|
100
|
-
dataset_info = self._datasets[key]
|
|
101
|
-
|
|
102
|
-
try:
|
|
103
|
-
logger.info(f"Carregando dataset {key} de {dataset_info['url']}")
|
|
104
|
-
df = pd.read_pickle(dataset_info["url"])
|
|
105
|
-
|
|
106
|
-
# Validar se o DataFrame tem a coluna de data esperada
|
|
107
|
-
date_column = dataset_info["date_column"]
|
|
108
|
-
if date_column not in df.columns:
|
|
109
|
-
raise ValueError(
|
|
110
|
-
f"Coluna de data '{date_column}' não encontrada no dataset '{key}'"
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
dataset_info["df"] = df
|
|
114
|
-
dataset_info["last_date"] = df[date_column].max().date()
|
|
115
|
-
|
|
116
|
-
logger.info(
|
|
117
|
-
f"Dataset {key} atualizado. Última data: {dataset_info['last_date']}"
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
except requests.RequestException as e:
|
|
121
|
-
logger.error(f"Erro ao fazer download do dataset {key}: {str(e)}")
|
|
122
|
-
raise
|
|
123
|
-
except Exception as e:
|
|
124
|
-
logger.error(f"Erro ao processar dataset {key}: {str(e)}")
|
|
125
|
-
raise
|
|
126
|
-
|
|
127
|
-
def get_dataframe(self, key: str) -> pd.DataFrame:
|
|
128
|
-
"""
|
|
129
|
-
Obtém um DataFrame, carregando-o ou atualizando-o se necessário.
|
|
130
|
-
|
|
131
|
-
Args:
|
|
132
|
-
key: Identificador do dataset
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
DataFrame com os dados solicitados
|
|
136
|
-
|
|
137
|
-
Raises:
|
|
138
|
-
ValueError: Se o dataset não estiver configurado
|
|
139
|
-
Exception: Se ocorrer erro ao carregar os dados
|
|
140
|
-
"""
|
|
141
|
-
dataset_info = self._datasets.get(key)
|
|
142
|
-
|
|
143
|
-
if dataset_info is None:
|
|
144
|
-
logger.error(f"Dataset '{key}' não está configurado")
|
|
145
|
-
raise ValueError(f"Dataset '{key}' não está configurado")
|
|
146
|
-
|
|
147
|
-
try:
|
|
148
|
-
if dataset_info["df"] is None or self._should_update_dataset(key):
|
|
149
|
-
self._load_dataset(key)
|
|
150
|
-
|
|
151
|
-
return dataset_info["df"].copy()
|
|
152
|
-
except Exception as e:
|
|
153
|
-
logger.error(f"Erro ao obter dataset {key}: {str(e)}")
|
|
154
|
-
raise e
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# Instância única da classe para uso interno da biblioteca
|
|
158
|
-
_data_cache = DataCache()
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def get_di_dataset() -> pd.DataFrame:
|
|
162
|
-
"""
|
|
163
|
-
Obtém o dataset de DI (Depósito Interfinanceiro).
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
DataFrame com os dados de DI
|
|
167
|
-
|
|
168
|
-
Raises:
|
|
169
|
-
Exception: Se ocorrer erro ao carregar os dados
|
|
170
|
-
"""
|
|
171
|
-
return _data_cache.get_dataframe("di")
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def get_tpf_dataset() -> pd.DataFrame:
|
|
175
|
-
"""
|
|
176
|
-
Obtém o dataset de TPF (Títulos Públicos Federais).
|
|
177
|
-
|
|
178
|
-
Returns:
|
|
179
|
-
DataFrame com os dados de TPF
|
|
180
|
-
|
|
181
|
-
Raises:
|
|
182
|
-
Exception: Se ocorrer erro ao carregar os dados
|
|
183
|
-
"""
|
|
184
|
-
return _data_cache.get_dataframe("tpf")
|
pyield-0.32.0/pyield/retry.py
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import time
|
|
3
|
-
from urllib.error import HTTPError, URLError
|
|
4
|
-
|
|
5
|
-
import pandas as pd
|
|
6
|
-
import tenacity
|
|
7
|
-
|
|
8
|
-
logger = logging.getLogger(__name__)
|
|
9
|
-
|
|
10
|
-
# Default timeout values for HTTP requests (connect_timeout, read_timeout)
|
|
11
|
-
DEFAULT_TIMEOUT = (2, 10)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def retry_if_recoverable(exception):
|
|
15
|
-
"""
|
|
16
|
-
Determine if the exception is worth retrying.
|
|
17
|
-
"""
|
|
18
|
-
if isinstance(exception, HTTPError):
|
|
19
|
-
# Don't retry for 404 errors
|
|
20
|
-
if exception.code == 404: # noqa
|
|
21
|
-
logger.info("Resource not found (404), not retrying.")
|
|
22
|
-
return False
|
|
23
|
-
|
|
24
|
-
# Don't retry for 504 Gateway Timeout after first attempt
|
|
25
|
-
# These often indicate longer-term issues with API
|
|
26
|
-
if exception.code == 504: # noqa
|
|
27
|
-
logger.warning(
|
|
28
|
-
"Gateway Timeout (504) from API - likely service degradation."
|
|
29
|
-
)
|
|
30
|
-
# Check if we've already retried for this 504
|
|
31
|
-
retry_state = getattr(retry_if_recoverable, "retry_for_504", 0)
|
|
32
|
-
if retry_state >= 1:
|
|
33
|
-
logger.error("Multiple 504 errors - BC API appears to be unavailable.")
|
|
34
|
-
return False
|
|
35
|
-
# Allow just one retry for 504 with a longer wait
|
|
36
|
-
retry_if_recoverable.retry_for_504 = retry_state + 1
|
|
37
|
-
time.sleep(2) # Immediate wait before retry
|
|
38
|
-
return True
|
|
39
|
-
|
|
40
|
-
# Reset the 504 counter for other types of errors
|
|
41
|
-
retry_if_recoverable.retry_for_504 = 0
|
|
42
|
-
|
|
43
|
-
# Retry for other HTTP errors, URLErrors, and parsing errors
|
|
44
|
-
return isinstance(
|
|
45
|
-
exception,
|
|
46
|
-
(HTTPError, URLError, pd.errors.EmptyDataError, pd.errors.ParserError),
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# Custom retry configuration that handles parsing errors
|
|
51
|
-
default_retry = tenacity.retry(
|
|
52
|
-
retry=tenacity.retry_if_exception(retry_if_recoverable),
|
|
53
|
-
stop=(tenacity.stop_after_attempt(3) | tenacity.stop_after_delay(15)),
|
|
54
|
-
wait=tenacity.wait_exponential(multiplier=0.5, min=0.5, max=2),
|
|
55
|
-
before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
|
|
56
|
-
)
|
|
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
|