PYield 0.34.2__tar.gz → 0.35.1__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.34.2 → pyield-0.35.1}/PKG-INFO +1 -1
- pyield-0.35.1/pyield/__about__.py +1 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/__init__.py +2 -2
- {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/__init__.py +2 -2
- pyield-0.34.2/pyield/b3/dif.py → pyield-0.35.1/pyield/b3/di1.py +52 -80
- {pyield-0.34.2 → pyield-0.35.1}/pyield/data_cache.py +9 -1
- {pyield-0.34.2 → pyield-0.35.1}/pyield/retry.py +64 -44
- {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/ltn.py +2 -2
- {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/ntnb.py +1 -1
- {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/ntnf.py +2 -2
- {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/tools.py +2 -2
- pyield-0.34.2/pyield/__about__.py +0 -1
- {pyield-0.34.2 → pyield-0.35.1}/.gitignore +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/LICENSE +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/README.md +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/__init__.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/ettj.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/ima.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/imaq.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/ipca.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/anbima/tpf.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/di_over.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/futures/__init__.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/futures/common.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/futures/historical.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/futures/intraday.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/b3/futures/xml.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/__init__.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/auction.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/bcdata.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/ptax.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/repo.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/trades_intraday.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/trades_monthly.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/bc/vna.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/bday.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/conftest.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/date_converter.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/fwd.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/holidays/__init__.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/holidays/br_holidays_new.txt +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/holidays/br_holidays_old.txt +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/holidays/brholidays.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/ibge/__init__.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/ibge/ipca.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/interpolator.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/py.typed +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/__init__.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/lft.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyield/tpf/ntnc.py +0 -0
- {pyield-0.34.2 → pyield-0.35.1}/pyproject.toml +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.35.1"
|
|
@@ -2,7 +2,7 @@ import logging
|
|
|
2
2
|
|
|
3
3
|
from pyield import anbima, b3, bc, bday, ibge
|
|
4
4
|
from pyield.__about__ import __version__
|
|
5
|
-
from pyield.b3 import
|
|
5
|
+
from pyield.b3 import di1, futures
|
|
6
6
|
from pyield.fwd import forward, forwards
|
|
7
7
|
from pyield.interpolator import Interpolator
|
|
8
8
|
from pyield.tpf import lft, ltn, ntnb, ntnc, ntnf
|
|
@@ -11,7 +11,7 @@ __all__ = [
|
|
|
11
11
|
"__version__",
|
|
12
12
|
"anbima",
|
|
13
13
|
"bday",
|
|
14
|
-
"
|
|
14
|
+
"di1",
|
|
15
15
|
"forwards",
|
|
16
16
|
"forward",
|
|
17
17
|
"futures",
|
|
@@ -15,7 +15,7 @@ TIMEZONE_BZ = ZoneInfo("America/Sao_Paulo")
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def data(
|
|
18
|
-
|
|
18
|
+
dates: DateScalar | DateArray,
|
|
19
19
|
month_start: bool = False,
|
|
20
20
|
pre_filter: bool = False,
|
|
21
21
|
all_columns: bool = True,
|
|
@@ -27,9 +27,7 @@ def data(
|
|
|
27
27
|
(to month start) and optional filtering based on LTN and NTN-F bond maturities.
|
|
28
28
|
|
|
29
29
|
Args:
|
|
30
|
-
|
|
31
|
-
If the date is invalid, a holiday, in the future, or None, an empty
|
|
32
|
-
DataFrame is returned with appropriate logging.
|
|
30
|
+
dates (DateScalar): The trade dates for which to retrieve DI contract data.
|
|
33
31
|
month_start (bool, optional): If True, adjusts all expiration dates to the
|
|
34
32
|
first day of their respective month (e.g., 2025-02-01 becomes
|
|
35
33
|
2025-01-01). Defaults to False.
|
|
@@ -43,12 +41,12 @@ def data(
|
|
|
43
41
|
|
|
44
42
|
Returns:
|
|
45
43
|
pd.DataFrame: A DataFrame containing the DI futures contract data for the
|
|
46
|
-
specified
|
|
47
|
-
if no data is found
|
|
44
|
+
specified dates, sorted by trade dates and expiration dates.
|
|
45
|
+
Returns an empty DataFrame if no data is found
|
|
48
46
|
|
|
49
47
|
Examples:
|
|
50
|
-
>>> from pyield import
|
|
51
|
-
>>> df =
|
|
48
|
+
>>> from pyield import di1
|
|
49
|
+
>>> df = di1.data(dates="16-10-2024", month_start=True)
|
|
52
50
|
>>> df.iloc[:5, :5] # Show the first five rows and columns
|
|
53
51
|
TradeDate ExpirationDate TickerSymbol BDaysToExp OpenContracts
|
|
54
52
|
0 2024-10-16 2024-11-01 DI1X24 12 1744269
|
|
@@ -57,31 +55,25 @@ def data(
|
|
|
57
55
|
3 2024-10-16 2025-02-01 DI1G25 74 279491
|
|
58
56
|
4 2024-10-16 2025-03-01 DI1H25 94 344056
|
|
59
57
|
"""
|
|
60
|
-
if date:
|
|
61
|
-
date = dc.convert_input_dates(date)
|
|
62
|
-
bz_today = dt.now(TIMEZONE_BZ).date()
|
|
63
|
-
if date.date() > bz_today:
|
|
64
|
-
logger.warning(f"DI date ({date}) after current date {bz_today}")
|
|
65
|
-
logger.warning("Returning empty DataFrame.")
|
|
66
|
-
return pd.DataFrame()
|
|
67
|
-
else:
|
|
68
|
-
logger.info("No date specified. Returning empty DataFrame.")
|
|
69
|
-
return pd.DataFrame()
|
|
70
|
-
# Return an empty DataFrame if the trade date is a holiday
|
|
71
|
-
if not bday.is_business_day(date):
|
|
72
|
-
logger.warning("Specified date is not a business day.")
|
|
73
|
-
logger.warning("Returning empty DataFrame.")
|
|
74
|
-
return pd.DataFrame()
|
|
75
58
|
|
|
76
|
-
|
|
77
|
-
|
|
59
|
+
dates = dc.convert_input_dates(dates)
|
|
60
|
+
# Force dates to be a Series for consistency
|
|
61
|
+
dates = pd.Series(dates) if isinstance(dates, pd.Timestamp) else dates
|
|
62
|
+
# Remove duplicates and sort dates
|
|
63
|
+
dates = dates.drop_duplicates().sort_values().reset_index(drop=True)
|
|
78
64
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
65
|
+
df = get_cached_dataset("DI").query("TradeDate in @dates").reset_index(drop=True)
|
|
66
|
+
|
|
67
|
+
today = dt.now(TIMEZONE_BZ)
|
|
68
|
+
last_bday = bday.last_business_day()
|
|
69
|
+
is_trading_day = today.date() == last_bday.date()
|
|
70
|
+
if is_trading_day and last_bday in df["TradeDate"]:
|
|
71
|
+
logger.info("Trying to get DI Futures data for today.")
|
|
72
|
+
df_intraday = b3.futures(contract_code="DI1", date=last_bday)
|
|
73
|
+
df = pd.concat([df, df_intraday]).reset_index(drop=True)
|
|
82
74
|
|
|
83
75
|
if df.empty:
|
|
84
|
-
logger.warning("No DI Futures data found for the specified
|
|
76
|
+
logger.warning("No DI Futures data found for the specified dates.")
|
|
85
77
|
logger.warning("Returning empty DataFrame.")
|
|
86
78
|
return pd.DataFrame()
|
|
87
79
|
|
|
@@ -92,24 +84,35 @@ def data(
|
|
|
92
84
|
df_pre = (
|
|
93
85
|
get_cached_dataset("TPF")
|
|
94
86
|
.query("BondType in ['LTN', 'NTN-F']")
|
|
95
|
-
.
|
|
87
|
+
.drop_duplicates(subset=["ReferenceDate", "MaturityDate"])
|
|
88
|
+
.sort_values(["ReferenceDate", "MaturityDate"])
|
|
89
|
+
.reset_index(drop=True)[["ReferenceDate", "MaturityDate", "BondType"]]
|
|
96
90
|
)
|
|
91
|
+
# Assure that the MaturityDate is a business day
|
|
92
|
+
df_pre["MaturityDate"] = bday.offset(df_pre["MaturityDate"], 0)
|
|
97
93
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
# Rename columns for merging
|
|
95
|
+
df_pre = df_pre.rename(
|
|
96
|
+
columns={"ReferenceDate": "TradeDate", "MaturityDate": "ExpirationDate"}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
df = pd.merge_asof(
|
|
100
|
+
df, df_pre, on="TradeDate", by="ExpirationDate", direction="backward"
|
|
101
|
+
)
|
|
102
102
|
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.
|
|
103
|
+
# BondType was used as flag to filter the maturities
|
|
104
|
+
df = (
|
|
105
|
+
df.query("BondType.notna()")
|
|
106
|
+
.drop(columns=["BondType"])
|
|
107
107
|
.reset_index(drop=True)
|
|
108
108
|
)
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
if df.empty:
|
|
111
|
+
logger.warning(
|
|
112
|
+
"No DI Futures data found for the specified dates after pre-filtering."
|
|
113
|
+
"Returning empty DataFrame."
|
|
114
|
+
)
|
|
115
|
+
return pd.DataFrame()
|
|
113
116
|
|
|
114
117
|
if month_start:
|
|
115
118
|
df["ExpirationDate"] = df["ExpirationDate"].dt.to_period("M").dt.to_timestamp()
|
|
@@ -139,33 +142,6 @@ def data(
|
|
|
139
142
|
return df.sort_values(by=["TradeDate", "ExpirationDate"]).reset_index(drop=True)
|
|
140
143
|
|
|
141
144
|
|
|
142
|
-
def _find_nearest_date(
|
|
143
|
-
target_date: pd.Timestamp, date_series: pd.Series
|
|
144
|
-
) -> pd.Timestamp:
|
|
145
|
-
"""Finds the date in a Series closest to the target date.
|
|
146
|
-
|
|
147
|
-
Args:
|
|
148
|
-
target_date (pd.Timestamp): The reference date.
|
|
149
|
-
date_series (pd.Series): A Series of dates to search within.
|
|
150
|
-
|
|
151
|
-
Returns:
|
|
152
|
-
pd.Timestamp: The date from `date_series` that is closest in time
|
|
153
|
-
to `target_date`. Returns pd.NaT if the series is empty or
|
|
154
|
-
target_date is None.
|
|
155
|
-
"""
|
|
156
|
-
if date_series.empty or target_date is None:
|
|
157
|
-
return pd.NaT
|
|
158
|
-
# The result will be a Series of positive Timedeltas (durations)
|
|
159
|
-
abs_differences = (date_series - target_date).abs()
|
|
160
|
-
|
|
161
|
-
# Find the index of the minimum difference in the differences Series
|
|
162
|
-
# idxmin() returns the index label where the minimum value occurs
|
|
163
|
-
closest_index = abs_differences.idxmin()
|
|
164
|
-
|
|
165
|
-
# Use the found index to get the corresponding date from the original Series
|
|
166
|
-
return date_series.loc[closest_index] # or date_series[closest_index]
|
|
167
|
-
|
|
168
|
-
|
|
169
145
|
def interpolate_rates(
|
|
170
146
|
dates: DateScalar | DateArray,
|
|
171
147
|
expirations: DateScalar | DateArray,
|
|
@@ -230,11 +206,7 @@ def interpolate_rates(
|
|
|
230
206
|
dfi["irate"] = dfi["irate"].astype("Float64")
|
|
231
207
|
|
|
232
208
|
# Load DI rates dataset filtered by the provided reference dates
|
|
233
|
-
dfr = (
|
|
234
|
-
get_cached_dataset("DI")
|
|
235
|
-
.query("TradeDate in @dfi['tdate'].unique()")
|
|
236
|
-
.reset_index(drop=True)
|
|
237
|
-
)
|
|
209
|
+
dfr = data(dates=dates)
|
|
238
210
|
|
|
239
211
|
# Return an empty DataFrame if no rates are found
|
|
240
212
|
if dfr.empty:
|
|
@@ -295,17 +267,17 @@ def interpolate_rate(
|
|
|
295
267
|
- An interpolation calculation fails.
|
|
296
268
|
|
|
297
269
|
Examples:
|
|
298
|
-
>>> from pyield import
|
|
270
|
+
>>> from pyield import di1
|
|
299
271
|
>>> # Get rate for an existing contract expiration
|
|
300
|
-
>>>
|
|
272
|
+
>>> di1.interpolate_rate("25-04-2025", "01-01-2027")
|
|
301
273
|
0.13901
|
|
302
274
|
|
|
303
275
|
>>> # Get rate for a non-existing contract expiration
|
|
304
|
-
>>>
|
|
276
|
+
>>> di1.interpolate_rate("25-04-2025", "01-11-2027")
|
|
305
277
|
0.13576348733268917
|
|
306
278
|
|
|
307
279
|
>>> # Extrapolate rate for a future expiration date
|
|
308
|
-
>>>
|
|
280
|
+
>>> di1.interpolate_rate("25-04-2025", "01-01-2050", extrapolate=True)
|
|
309
281
|
0.13881
|
|
310
282
|
"""
|
|
311
283
|
|
|
@@ -314,7 +286,7 @@ def interpolate_rate(
|
|
|
314
286
|
return float("NaN")
|
|
315
287
|
|
|
316
288
|
# Get the DI contract DataFrame
|
|
317
|
-
df = data(
|
|
289
|
+
df = data(dates=date)
|
|
318
290
|
|
|
319
291
|
if df.empty:
|
|
320
292
|
return float("NaN")
|
|
@@ -354,9 +326,9 @@ def eod_dates() -> pd.Series:
|
|
|
354
326
|
for which DI data is available.
|
|
355
327
|
|
|
356
328
|
Examples:
|
|
357
|
-
>>> from pyield import
|
|
329
|
+
>>> from pyield import di1
|
|
358
330
|
>>> # DI Futures series starts from 1995-01-02
|
|
359
|
-
>>>
|
|
331
|
+
>>> di1.eod_dates().head(5)
|
|
360
332
|
0 1995-01-02
|
|
361
333
|
1 1995-01-03
|
|
362
334
|
2 1995-01-04
|
|
@@ -5,6 +5,8 @@ from zoneinfo import ZoneInfo
|
|
|
5
5
|
|
|
6
6
|
import pandas as pd
|
|
7
7
|
|
|
8
|
+
from pyield.retry import default_retry
|
|
9
|
+
|
|
8
10
|
# --- Configurações Centralizadas ---
|
|
9
11
|
TIMEZONE_BZ = ZoneInfo("America/Sao_Paulo")
|
|
10
12
|
|
|
@@ -40,6 +42,12 @@ def _get_today_date_key():
|
|
|
40
42
|
return datetime.now(TIMEZONE_BZ).strftime("%Y-%m-%d")
|
|
41
43
|
|
|
42
44
|
|
|
45
|
+
@default_retry
|
|
46
|
+
def _load_github_file(file_url: str) -> pd.DataFrame:
|
|
47
|
+
"""Carrega um arquivo do GitHub e retorna um DataFrame."""
|
|
48
|
+
return pd.read_parquet(file_url)
|
|
49
|
+
|
|
50
|
+
|
|
43
51
|
@functools.lru_cache(maxsize=len(DATASET_CONFIGS))
|
|
44
52
|
def _get_dataset_with_ttl(dataset_id: str, date_key: str) -> pd.DataFrame:
|
|
45
53
|
"""Carrega o dataset a partir do ID e da chave de data. Cache expira diariamente."""
|
|
@@ -55,7 +63,7 @@ def _get_dataset_with_ttl(dataset_id: str, date_key: str) -> pd.DataFrame:
|
|
|
55
63
|
full_url = f"{BASE_DATA_URL}/{filename}"
|
|
56
64
|
|
|
57
65
|
try:
|
|
58
|
-
df =
|
|
66
|
+
df = _load_github_file(full_url)
|
|
59
67
|
last_date = df[date_column].max().date()
|
|
60
68
|
logger.info(f"Dataset {dataset_name} carregado. Última data: {last_date}")
|
|
61
69
|
return df.copy()
|
|
@@ -19,66 +19,65 @@ from tenacity import RetryCallState
|
|
|
19
19
|
# --- Configuração do Logger ---
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
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
22
|
|
|
28
23
|
# --- Funções Auxiliares para Tenacity ---
|
|
29
|
-
|
|
30
|
-
|
|
31
24
|
def _log_before_sleep(retry_state: RetryCallState):
|
|
32
25
|
"""
|
|
33
26
|
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
27
|
"""
|
|
38
|
-
# 1. Verificar o resultado (outcome)
|
|
39
28
|
outcome = retry_state.outcome
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
if not outcome or not outcome.failed:
|
|
30
|
+
logger.debug(
|
|
31
|
+
"before_sleep chamada sem 'outcome' ou para 'outcome' bem-sucedido."
|
|
32
|
+
)
|
|
33
|
+
return
|
|
43
34
|
|
|
44
|
-
exception = outcome.exception
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
exception = outcome.exception()
|
|
36
|
+
if not exception:
|
|
37
|
+
logger.debug("before_sleep chamada sem 'exception' real no outcome.")
|
|
38
|
+
return
|
|
47
39
|
|
|
48
|
-
# 2. Verificar a próxima ação (next_action)
|
|
49
40
|
next_action = retry_state.next_action
|
|
50
|
-
|
|
41
|
+
if not next_action or not hasattr(next_action, "sleep"):
|
|
42
|
+
logger.debug(
|
|
43
|
+
"before_sleep chamada sem 'next_action' ou 'sleep' em next_action."
|
|
44
|
+
)
|
|
45
|
+
return
|
|
51
46
|
|
|
52
|
-
# 3. Acessar a duração do sono (sleep)
|
|
53
|
-
# Confiamos que 'next_action' terá '.sleep' neste contexto, pois é antes de dormir.
|
|
54
47
|
sleep_duration = next_action.sleep
|
|
55
48
|
|
|
56
|
-
# Log principal
|
|
57
49
|
logger.warning(
|
|
58
50
|
f"Tentativa {retry_state.attempt_number} falhou com "
|
|
59
|
-
f"{type(exception).__name__}: {exception}. Tentando novamente em "
|
|
51
|
+
f"{type(exception).__name__}: {str(exception)[:150]}. Tentando novamente em "
|
|
60
52
|
f"{sleep_duration:.2f} segundos..."
|
|
61
53
|
)
|
|
62
54
|
|
|
63
55
|
|
|
64
56
|
def should_retry_exception(retry_state: RetryCallState) -> bool:
|
|
65
|
-
"""Determina se uma exceção capturada justifica uma nova tentativa
|
|
57
|
+
"""Determina se uma exceção capturada justifica uma nova tentativa"""
|
|
66
58
|
if not retry_state.outcome or not retry_state.outcome.failed:
|
|
67
59
|
return False
|
|
68
60
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
exception = retry_state.outcome.exception()
|
|
62
|
+
|
|
63
|
+
if exception is None:
|
|
64
|
+
# Isso não deveria acontecer se outcome.failed é True.
|
|
65
|
+
logger.error(
|
|
66
|
+
"should_retry_exception: outcome.failed é True,"
|
|
67
|
+
"mas exception() retornou None."
|
|
68
|
+
)
|
|
69
|
+
return False
|
|
74
70
|
|
|
75
71
|
attempt_number = retry_state.attempt_number
|
|
72
|
+
# getattr é mais seguro caso a exceção não tenha 'url'
|
|
76
73
|
url = getattr(exception, "url", "N/A")
|
|
77
74
|
|
|
78
75
|
retry_decision = False
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
# Inicializar como None para evitar log default se nenhum case corresponder
|
|
77
|
+
log_message = None
|
|
78
|
+
log_level = logging.ERROR # Default para não retentativa
|
|
81
79
|
|
|
80
|
+
# Usando match/case como originalmente preferido
|
|
82
81
|
match exception:
|
|
83
82
|
case HTTPError(code=404):
|
|
84
83
|
log_message = (
|
|
@@ -88,6 +87,15 @@ def should_retry_exception(retry_state: RetryCallState) -> bool:
|
|
|
88
87
|
retry_decision = False
|
|
89
88
|
log_level = logging.INFO
|
|
90
89
|
|
|
90
|
+
case HTTPError(code=429): # ADICIONADO: Tratamento para Too Many Requests
|
|
91
|
+
log_message = (
|
|
92
|
+
f"Tentativa {attempt_number}: Too Many Requests (429) da API em {url}. "
|
|
93
|
+
f"API pediu para aguardar. Tentando novamente."
|
|
94
|
+
)
|
|
95
|
+
retry_decision = True
|
|
96
|
+
log_level = logging.WARNING
|
|
97
|
+
|
|
98
|
+
# Gateway Timeout, primeira tentativa
|
|
91
99
|
case HTTPError(code=504) if attempt_number == 1:
|
|
92
100
|
log_message = (
|
|
93
101
|
f"Tentativa {attempt_number}: Gateway Timeout (504) da API em {url}. "
|
|
@@ -96,7 +104,7 @@ def should_retry_exception(retry_state: RetryCallState) -> bool:
|
|
|
96
104
|
retry_decision = True
|
|
97
105
|
log_level = logging.WARNING
|
|
98
106
|
|
|
99
|
-
case HTTPError(code=504):
|
|
107
|
+
case HTTPError(code=504): # Gateway Timeout, tentativas subsequentes
|
|
100
108
|
log_message = (
|
|
101
109
|
f"Tentativa {attempt_number}: Gateway Timeout (504) persistiu em {url}."
|
|
102
110
|
f"API parece indisponível. Desistindo."
|
|
@@ -104,41 +112,52 @@ def should_retry_exception(retry_state: RetryCallState) -> bool:
|
|
|
104
112
|
retry_decision = False
|
|
105
113
|
log_level = logging.ERROR
|
|
106
114
|
|
|
107
|
-
case HTTPError() as http_err:
|
|
115
|
+
case HTTPError() as http_err: # Outros erros HTTP
|
|
116
|
+
# Esta regra genérica para HTTPError retentará outros códigos HTTP.
|
|
117
|
+
# Por exemplo, 500, 502, 503 (geralmente bom para retry)
|
|
118
|
+
# mas também 400, 401, 403 (geralmente não bom para retry).
|
|
119
|
+
# Se precisar de mais granularidade, adicione cases específicos acima deste.
|
|
108
120
|
log_message = (
|
|
109
|
-
f"Tentativa {attempt_number}: Erro HTTP {http_err.code}
|
|
110
|
-
f"para {url}.
|
|
121
|
+
f"Tentativa {attempt_number}: Erro HTTP {http_err.code} "
|
|
122
|
+
f"({http_err.reason}) encontrado para {url}. "
|
|
123
|
+
"Pode ser transitório. Tentando novamente."
|
|
111
124
|
)
|
|
112
125
|
retry_decision = True
|
|
113
126
|
log_level = logging.WARNING
|
|
114
127
|
|
|
128
|
+
# Erros de rede mais genéricos (e.g., DNS falhou, conexão recusada)
|
|
115
129
|
case URLError():
|
|
116
130
|
log_message = (
|
|
117
131
|
f"Tentativa {attempt_number}: Erro de rede recuperável "
|
|
118
|
-
f"({type(exception).__name__})
|
|
132
|
+
f"({type(exception).__name__}: {str(exception)[:100]}) para {url}. "
|
|
133
|
+
f"Tentando novamente."
|
|
119
134
|
)
|
|
120
135
|
retry_decision = True
|
|
121
136
|
log_level = logging.WARNING
|
|
122
137
|
|
|
138
|
+
# Usando o operador | para combinar tipos de exceção no case
|
|
123
139
|
case pd.errors.EmptyDataError() | pd.errors.ParserError():
|
|
124
140
|
log_message = (
|
|
125
|
-
f"Tentativa {attempt_number}: Erro de parsing "
|
|
141
|
+
f"Tentativa {attempt_number}: Erro de parsing de dados "
|
|
126
142
|
f"({type(exception).__name__}). Tentando novamente."
|
|
127
143
|
)
|
|
128
144
|
retry_decision = True
|
|
129
145
|
log_level = logging.WARNING
|
|
130
146
|
|
|
131
|
-
case _:
|
|
147
|
+
case _: # Exceção não mapeada/inesperada
|
|
148
|
+
# Mensagem para o log de exceção
|
|
132
149
|
log_message_exc = (
|
|
133
150
|
f"Tentativa {attempt_number}: Erro não recuperável/inesperado "
|
|
134
|
-
f"({type(exception).__name__})
|
|
151
|
+
f"({type(exception).__name__}: {str(exception)[:100]}) "
|
|
152
|
+
f"ao processar {url}. Desistindo."
|
|
135
153
|
)
|
|
136
|
-
|
|
137
|
-
log_level = logging.ERROR
|
|
154
|
+
# Sempre incluir traceback para erros inesperados
|
|
138
155
|
logger.exception(log_message_exc)
|
|
139
|
-
log_message = None
|
|
156
|
+
log_message = None # Evita log duplicado pela seção logger.log abaixo
|
|
157
|
+
retry_decision = False
|
|
158
|
+
log_level = logging.ERROR # Já é o default, mas explícito aqui
|
|
140
159
|
|
|
141
|
-
if log_message:
|
|
160
|
+
if log_message: # Loga a mensagem formatada se uma foi definida
|
|
142
161
|
logger.log(log_level, log_message)
|
|
143
162
|
|
|
144
163
|
return retry_decision
|
|
@@ -147,8 +166,9 @@ def should_retry_exception(retry_state: RetryCallState) -> bool:
|
|
|
147
166
|
# --- Decorador Tenacity Principal ---
|
|
148
167
|
default_retry = tenacity.retry(
|
|
149
168
|
retry=should_retry_exception,
|
|
169
|
+
# Backoff exponencial é bom para 429
|
|
150
170
|
wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
|
|
151
171
|
stop=(tenacity.stop_after_attempt(3) | tenacity.stop_after_delay(15)),
|
|
152
172
|
before_sleep=_log_before_sleep,
|
|
153
|
-
reraise=True,
|
|
173
|
+
reraise=True, # Importante para propagar o erro final se as tentativas falharem
|
|
154
174
|
)
|
|
@@ -2,7 +2,7 @@ import pandas as pd
|
|
|
2
2
|
|
|
3
3
|
from pyield import anbima, bday
|
|
4
4
|
from pyield import date_converter as dc
|
|
5
|
-
from pyield.b3 import
|
|
5
|
+
from pyield.b3 import di1
|
|
6
6
|
from pyield.date_converter import DateScalar
|
|
7
7
|
from pyield.tpf import tools
|
|
8
8
|
|
|
@@ -223,7 +223,7 @@ def historical_premium(
|
|
|
223
223
|
ltn_rate = float(ltn_rates.iloc[0])
|
|
224
224
|
|
|
225
225
|
# Retrieve DI rate for the reference date and maturity
|
|
226
|
-
di_rate =
|
|
226
|
+
di_rate = di1.interpolate_rate(date=date, expiration=maturity, extrapolate=True)
|
|
227
227
|
if pd.isnull(di_rate): # Check if the DI rate is NaN
|
|
228
228
|
return float("NaN")
|
|
229
229
|
|
|
@@ -463,7 +463,7 @@ def bei_rates(
|
|
|
463
463
|
|
|
464
464
|
Get the DI Futures settlement rates for the same reference date to be used as
|
|
465
465
|
reference for the nominal rates:
|
|
466
|
-
>>> df_di = yd.
|
|
466
|
+
>>> df_di = yd.di1.data("05-09-2024")
|
|
467
467
|
|
|
468
468
|
Calculate the BEI rates considering the settlement at the reference date:
|
|
469
469
|
>>> yd.ntnb.bei_rates(
|
|
@@ -6,7 +6,7 @@ import pandas as pd
|
|
|
6
6
|
from pyield import anbima, bday
|
|
7
7
|
from pyield import date_converter as dc
|
|
8
8
|
from pyield import interpolator as ip
|
|
9
|
-
from pyield.b3 import
|
|
9
|
+
from pyield.b3 import di1
|
|
10
10
|
from pyield.date_converter import DateScalar
|
|
11
11
|
from pyield.tpf import tools
|
|
12
12
|
|
|
@@ -596,7 +596,7 @@ def historical_premium(
|
|
|
596
596
|
df["BYears"] = df["BDToMat"] / 252
|
|
597
597
|
df["ReferenceDate"] = date
|
|
598
598
|
|
|
599
|
-
df["DIRate"] =
|
|
599
|
+
df["DIRate"] = di1.interpolate_rates(
|
|
600
600
|
dates=df["ReferenceDate"],
|
|
601
601
|
expirations=df["PaymentDate"],
|
|
602
602
|
extrapolate=True,
|
|
@@ -5,7 +5,7 @@ import pandas as pd
|
|
|
5
5
|
|
|
6
6
|
from pyield import date_converter as dc
|
|
7
7
|
from pyield.anbima import tpf
|
|
8
|
-
from pyield.b3 import
|
|
8
|
+
from pyield.b3 import di1
|
|
9
9
|
from pyield.date_converter import DateScalar
|
|
10
10
|
|
|
11
11
|
|
|
@@ -77,7 +77,7 @@ def pre_spreads(date: DateScalar) -> pd.DataFrame:
|
|
|
77
77
|
"""
|
|
78
78
|
# Fetch DI rates for the reference date
|
|
79
79
|
converted_date = dc.convert_input_dates(date)
|
|
80
|
-
df_di =
|
|
80
|
+
df_di = di1.data(dates=converted_date, month_start=True)
|
|
81
81
|
if "SettlementRate" not in df_di.columns or df_di.empty:
|
|
82
82
|
raise ValueError("DI data is missing the 'SettlementRate' column or is empty.")
|
|
83
83
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.34.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
|