PYield 0.34.2__tar.gz → 0.35.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {pyield-0.34.2 → pyield-0.35.0}/PKG-INFO +1 -1
  2. pyield-0.35.0/pyield/__about__.py +1 -0
  3. {pyield-0.34.2 → pyield-0.35.0}/pyield/__init__.py +2 -2
  4. {pyield-0.34.2 → pyield-0.35.0}/pyield/b3/__init__.py +2 -2
  5. pyield-0.34.2/pyield/b3/dif.py → pyield-0.35.0/pyield/b3/di1.py +8 -8
  6. {pyield-0.34.2 → pyield-0.35.0}/pyield/data_cache.py +9 -1
  7. {pyield-0.34.2 → pyield-0.35.0}/pyield/retry.py +64 -44
  8. {pyield-0.34.2 → pyield-0.35.0}/pyield/tpf/ltn.py +2 -2
  9. {pyield-0.34.2 → pyield-0.35.0}/pyield/tpf/ntnb.py +1 -1
  10. {pyield-0.34.2 → pyield-0.35.0}/pyield/tpf/ntnf.py +2 -2
  11. {pyield-0.34.2 → pyield-0.35.0}/pyield/tpf/tools.py +2 -2
  12. pyield-0.34.2/pyield/__about__.py +0 -1
  13. {pyield-0.34.2 → pyield-0.35.0}/.gitignore +0 -0
  14. {pyield-0.34.2 → pyield-0.35.0}/LICENSE +0 -0
  15. {pyield-0.34.2 → pyield-0.35.0}/README.md +0 -0
  16. {pyield-0.34.2 → pyield-0.35.0}/pyield/anbima/__init__.py +0 -0
  17. {pyield-0.34.2 → pyield-0.35.0}/pyield/anbima/ettj.py +0 -0
  18. {pyield-0.34.2 → pyield-0.35.0}/pyield/anbima/ima.py +0 -0
  19. {pyield-0.34.2 → pyield-0.35.0}/pyield/anbima/imaq.py +0 -0
  20. {pyield-0.34.2 → pyield-0.35.0}/pyield/anbima/ipca.py +0 -0
  21. {pyield-0.34.2 → pyield-0.35.0}/pyield/anbima/tpf.py +0 -0
  22. {pyield-0.34.2 → pyield-0.35.0}/pyield/b3/di_over.py +0 -0
  23. {pyield-0.34.2 → pyield-0.35.0}/pyield/b3/futures/__init__.py +0 -0
  24. {pyield-0.34.2 → pyield-0.35.0}/pyield/b3/futures/common.py +0 -0
  25. {pyield-0.34.2 → pyield-0.35.0}/pyield/b3/futures/historical.py +0 -0
  26. {pyield-0.34.2 → pyield-0.35.0}/pyield/b3/futures/intraday.py +0 -0
  27. {pyield-0.34.2 → pyield-0.35.0}/pyield/b3/futures/xml.py +0 -0
  28. {pyield-0.34.2 → pyield-0.35.0}/pyield/bc/__init__.py +0 -0
  29. {pyield-0.34.2 → pyield-0.35.0}/pyield/bc/auction.py +0 -0
  30. {pyield-0.34.2 → pyield-0.35.0}/pyield/bc/bcdata.py +0 -0
  31. {pyield-0.34.2 → pyield-0.35.0}/pyield/bc/ptax.py +0 -0
  32. {pyield-0.34.2 → pyield-0.35.0}/pyield/bc/repo.py +0 -0
  33. {pyield-0.34.2 → pyield-0.35.0}/pyield/bc/trades_intraday.py +0 -0
  34. {pyield-0.34.2 → pyield-0.35.0}/pyield/bc/trades_monthly.py +0 -0
  35. {pyield-0.34.2 → pyield-0.35.0}/pyield/bc/vna.py +0 -0
  36. {pyield-0.34.2 → pyield-0.35.0}/pyield/bday.py +0 -0
  37. {pyield-0.34.2 → pyield-0.35.0}/pyield/conftest.py +0 -0
  38. {pyield-0.34.2 → pyield-0.35.0}/pyield/date_converter.py +0 -0
  39. {pyield-0.34.2 → pyield-0.35.0}/pyield/fwd.py +0 -0
  40. {pyield-0.34.2 → pyield-0.35.0}/pyield/holidays/__init__.py +0 -0
  41. {pyield-0.34.2 → pyield-0.35.0}/pyield/holidays/br_holidays_new.txt +0 -0
  42. {pyield-0.34.2 → pyield-0.35.0}/pyield/holidays/br_holidays_old.txt +0 -0
  43. {pyield-0.34.2 → pyield-0.35.0}/pyield/holidays/brholidays.py +0 -0
  44. {pyield-0.34.2 → pyield-0.35.0}/pyield/ibge/__init__.py +0 -0
  45. {pyield-0.34.2 → pyield-0.35.0}/pyield/ibge/ipca.py +0 -0
  46. {pyield-0.34.2 → pyield-0.35.0}/pyield/interpolator.py +0 -0
  47. {pyield-0.34.2 → pyield-0.35.0}/pyield/py.typed +0 -0
  48. {pyield-0.34.2 → pyield-0.35.0}/pyield/tpf/__init__.py +0 -0
  49. {pyield-0.34.2 → pyield-0.35.0}/pyield/tpf/lft.py +0 -0
  50. {pyield-0.34.2 → pyield-0.35.0}/pyield/tpf/ntnc.py +0 -0
  51. {pyield-0.34.2 → pyield-0.35.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PYield
3
- Version: 0.34.2
3
+ Version: 0.35.0
4
4
  Summary: A Python library for analysis of fixed income instruments in Brazil
5
5
  Project-URL: Source, https://github.com/crdcj/PYield
6
6
  Author-email: Carlos Carvalho <cr.cj@outlook.com>
@@ -0,0 +1 @@
1
+ __version__ = "0.35.0"
@@ -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 dif, futures
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
- "dif",
14
+ "di1",
15
15
  "forwards",
16
16
  "forward",
17
17
  "futures",
@@ -1,9 +1,9 @@
1
- from pyield.b3 import dif
1
+ from pyield.b3 import di1
2
2
  from pyield.b3.di_over import di_over
3
3
  from pyield.b3.futures import futures
4
4
 
5
5
  __all__ = [
6
6
  "di_over",
7
7
  "futures",
8
- "dif",
8
+ "di1",
9
9
  ]
@@ -47,8 +47,8 @@ def data(
47
47
  if no data is found, the date is invalid, a holiday, or in the future.
48
48
 
49
49
  Examples:
50
- >>> from pyield import dif
51
- >>> df = dif.data(date="16-10-2024", month_start=True)
50
+ >>> from pyield import di1
51
+ >>> df = di1.data(date="16-10-2024", month_start=True)
52
52
  >>> df.iloc[:5, :5] # Show the first five rows and columns
53
53
  TradeDate ExpirationDate TickerSymbol BDaysToExp OpenContracts
54
54
  0 2024-10-16 2024-11-01 DI1X24 12 1744269
@@ -295,17 +295,17 @@ def interpolate_rate(
295
295
  - An interpolation calculation fails.
296
296
 
297
297
  Examples:
298
- >>> from pyield import dif
298
+ >>> from pyield import di1
299
299
  >>> # Get rate for an existing contract expiration
300
- >>> dif.interpolate_rate("25-04-2025", "01-01-2027")
300
+ >>> di1.interpolate_rate("25-04-2025", "01-01-2027")
301
301
  0.13901
302
302
 
303
303
  >>> # Get rate for a non-existing contract expiration
304
- >>> dif.interpolate_rate("25-04-2025", "01-11-2027")
304
+ >>> di1.interpolate_rate("25-04-2025", "01-11-2027")
305
305
  0.13576348733268917
306
306
 
307
307
  >>> # Extrapolate rate for a future expiration date
308
- >>> dif.interpolate_rate("25-04-2025", "01-01-2050", extrapolate=True)
308
+ >>> di1.interpolate_rate("25-04-2025", "01-01-2050", extrapolate=True)
309
309
  0.13881
310
310
  """
311
311
 
@@ -354,9 +354,9 @@ def eod_dates() -> pd.Series:
354
354
  for which DI data is available.
355
355
 
356
356
  Examples:
357
- >>> from pyield import dif
357
+ >>> from pyield import di1
358
358
  >>> # DI Futures series starts from 1995-01-02
359
- >>> dif.eod_dates().head(5)
359
+ >>> di1.eod_dates().head(5)
360
360
  0 1995-01-02
361
361
  1 1995-01-03
362
362
  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 = pd.read_parquet(full_url)
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
- 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"
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
- # Adicionado: Garantir que a exceção existe quando falhou
46
- assert exception is not None, "before_sleep chamada sem 'exception' no outcome"
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
- assert next_action is not None, "before_sleep chamada sem 'next_action'"
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
- # 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
- )
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
- log_message = ""
80
- log_level = logging.ERROR
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} encontrado "
110
- f"para {url}. Pode ser transitório. Tentando novamente."
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__}). Tentando novamente."
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__}). Desistindo."
151
+ f"({type(exception).__name__}: {str(exception)[:100]}) "
152
+ f"ao processar {url}. Desistindo."
135
153
  )
136
- retry_decision = False
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 dif
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 = dif.interpolate_rate(date=date, expiration=maturity, extrapolate=True)
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.dif.data("05-09-2024")
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 dif
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"] = dif.interpolate_rates(
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 dif
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 = dif.data(date=converted_date, month_start=True)
80
+ df_di = di1.data(date=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