PYield 0.44.5__tar.gz → 0.45.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 (71) hide show
  1. {pyield-0.44.5 → pyield-0.45.0}/PKG-INFO +1 -2
  2. pyield-0.45.0/pyield/__about__.py +1 -0
  3. pyield-0.45.0/pyield/_internal/cache.py +39 -0
  4. {pyield-0.44.5 → pyield-0.45.0}/pyield/_internal/converters.py +7 -2
  5. {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/ettj_intraday.py +2 -2
  6. {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/ettj_last.py +2 -2
  7. {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/ima.py +2 -2
  8. {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/di1.py +0 -4
  9. {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/futures/__init__.py +1 -1
  10. {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/futures/historical.py +2 -2
  11. {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/futures/intraday.py +1 -1
  12. {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/intraday_derivatives.py +2 -2
  13. {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/ptax_api.py +2 -2
  14. {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/rates.py +2 -2
  15. {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/trades_intraday.py +2 -2
  16. {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/trades_monthly.py +8 -36
  17. {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/vna.py +2 -2
  18. {pyield-0.44.5 → pyield-0.45.0}/pyield/bday/core.py +4 -0
  19. {pyield-0.44.5 → pyield-0.45.0}/pyield/fwd.py +0 -4
  20. pyield-0.45.0/pyield/ipca/projected.py +103 -0
  21. {pyield-0.44.5 → pyield-0.45.0}/pyield/rmd.py +13 -35
  22. {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/lft.py +66 -34
  23. {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ltn.py +63 -37
  24. {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ntnb.py +128 -81
  25. {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ntnb1.py +17 -25
  26. {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ntnbprinc.py +6 -6
  27. {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ntnc.py +63 -28
  28. {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ntnf.py +118 -185
  29. pyield-0.45.0/pyield/tn/utils.py +216 -0
  30. {pyield-0.44.5 → pyield-0.45.0}/pyproject.toml +3 -2
  31. pyield-0.44.5/pyield/__about__.py +0 -1
  32. pyield-0.44.5/pyield/ipca/projected.py +0 -117
  33. pyield-0.44.5/pyield/tn/tools.py +0 -112
  34. {pyield-0.44.5 → pyield-0.45.0}/.gitignore +0 -0
  35. {pyield-0.44.5 → pyield-0.45.0}/LICENSE +0 -0
  36. {pyield-0.44.5 → pyield-0.45.0}/README.md +0 -0
  37. {pyield-0.44.5 → pyield-0.45.0}/pyield/__init__.py +0 -0
  38. {pyield-0.44.5 → pyield-0.45.0}/pyield/_internal/__init__.py +0 -0
  39. {pyield-0.44.5 → pyield-0.45.0}/pyield/_internal/data_cache.py +0 -0
  40. {pyield-0.44.5 → pyield-0.45.0}/pyield/_internal/retry.py +0 -0
  41. {pyield-0.44.5 → pyield-0.45.0}/pyield/_internal/types.py +0 -0
  42. {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/__init__.py +0 -0
  43. {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/difusao.py +0 -0
  44. {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/imaq.py +0 -0
  45. {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/tpf.py +0 -0
  46. {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/__init__.py +0 -0
  47. {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/_contracts.py +0 -0
  48. {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/_validar_pregao.py +0 -0
  49. {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/di_over.py +0 -0
  50. {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/futures/common.py +0 -0
  51. {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/price_report.py +0 -0
  52. {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/__init__.py +0 -0
  53. {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/auction.py +0 -0
  54. {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/copom.py +0 -0
  55. {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/repo.py +0 -0
  56. {pyield-0.44.5 → pyield-0.45.0}/pyield/bday/__init__.py +0 -0
  57. {pyield-0.44.5 → pyield-0.45.0}/pyield/bday/holidays/br_holidays_new.txt +0 -0
  58. {pyield-0.44.5 → pyield-0.45.0}/pyield/bday/holidays/br_holidays_old.txt +0 -0
  59. {pyield-0.44.5 → pyield-0.45.0}/pyield/bday/holidays/brholidays.py +0 -0
  60. {pyield-0.44.5 → pyield-0.45.0}/pyield/clock.py +0 -0
  61. {pyield-0.44.5 → pyield-0.45.0}/pyield/interpolator.py +0 -0
  62. {pyield-0.44.5 → pyield-0.45.0}/pyield/ipca/__init__.py +0 -0
  63. {pyield-0.44.5 → pyield-0.45.0}/pyield/ipca/historical.py +0 -0
  64. {pyield-0.44.5 → pyield-0.45.0}/pyield/py.typed +0 -0
  65. {pyield-0.44.5 → pyield-0.45.0}/pyield/selic/__init__.py +0 -0
  66. {pyield-0.44.5 → pyield-0.45.0}/pyield/selic/cpm.py +0 -0
  67. {pyield-0.44.5 → pyield-0.45.0}/pyield/selic/probabilities.py +0 -0
  68. {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/__init__.py +0 -0
  69. {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/auctions.py +0 -0
  70. {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/benchmark.py +0 -0
  71. {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/pre.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyield
3
- Version: 0.44.5
3
+ Version: 0.45.0
4
4
  Summary: A Python library for analysis of fixed income instruments in Brazil
5
5
  Project-URL: Homepage, https://github.com/crdcj/PYield
6
6
  Project-URL: Documentation, https://crdcj.github.io/PYield
@@ -36,7 +36,6 @@ Classifier: Operating System :: OS Independent
36
36
  Classifier: Programming Language :: Python :: 3
37
37
  Classifier: Topic :: Office/Business :: Financial :: Investment
38
38
  Requires-Python: >=3.12
39
- Requires-Dist: cachetools>=5.0.0
40
39
  Requires-Dist: fastexcel>=0.19.0
41
40
  Requires-Dist: lxml>=6.0.0
42
41
  Requires-Dist: polars>=1.39.0
@@ -0,0 +1 @@
1
+ __version__ = "0.45.0"
@@ -0,0 +1,39 @@
1
+ """Cache com expiração por tempo (TTL) para proteger APIs externas contra
2
+ chamadas repetidas acidentais (ex.: re-execução de célula em notebook).
3
+ """
4
+
5
+ import time
6
+ from functools import wraps
7
+
8
+ _TTL_PADRAO = 60 # segundos
9
+ _TAMANHO_MAXIMO = 16
10
+
11
+
12
+ def ttl_cache(ttl: int = _TTL_PADRAO, maxsize: int = _TAMANHO_MAXIMO):
13
+ """Decorador de cache com expiração por tempo.
14
+
15
+ Args:
16
+ ttl: Tempo de vida de cada entrada em segundos.
17
+ maxsize: Número máximo de entradas no cache.
18
+ """
19
+
20
+ def decorador(func):
21
+ _cache: dict = {}
22
+
23
+ @wraps(func)
24
+ def wrapper(*args):
25
+ agora = time.monotonic()
26
+ if args in _cache:
27
+ resultado, expira_em = _cache[args]
28
+ if agora < expira_em:
29
+ return resultado
30
+ resultado = func(*args)
31
+ _cache[args] = (resultado, agora + ttl)
32
+ # Remove entrada mais antiga quando excede o tamanho máximo
33
+ if len(_cache) > maxsize:
34
+ _cache.pop(next(iter(_cache)))
35
+ return resultado
36
+
37
+ return wrapper
38
+
39
+ return decorador
@@ -121,6 +121,10 @@ def converter_datas(
121
121
  >>> converter_datas("") is None
122
122
  True
123
123
 
124
+ Data inválida retorna None:
125
+ >>> converter_datas("31-02-2024") is None
126
+ True
127
+
124
128
  Lista com valores nulos propaga os nulls:
125
129
  >>> converter_datas(["01-01-2024", None])
126
130
  shape: (2,)
@@ -140,12 +144,13 @@ def converter_datas(
140
144
  ]
141
145
 
142
146
  Formatos mistos por linha:
143
- >>> converter_datas(["25-12-2024", "2024-12-26"])
144
- shape: (2,)
147
+ >>> converter_datas(["25-12-2024", "26/12/2024", "2024-12-27"])
148
+ shape: (3,)
145
149
  Series: 'dates' [date]
146
150
  [
147
151
  2024-12-25
148
152
  2024-12-26
153
+ 2024-12-27
149
154
  ]
150
155
  """
151
156
  if not types.is_collection(dates):
@@ -2,10 +2,10 @@ import datetime as dt
2
2
  import logging
3
3
  from io import StringIO
4
4
 
5
- import cachetools
6
5
  import polars as pl
7
6
  import requests
8
7
 
8
+ from pyield._internal.cache import ttl_cache
9
9
  from pyield._internal.retry import retry_padrao
10
10
 
11
11
  logger = logging.getLogger(__name__)
@@ -19,7 +19,7 @@ URL_ETTJ_INTRADAY = (
19
19
  CASAS_DECIMAIS = 6
20
20
 
21
21
 
22
- @cachetools.cached(cache=cachetools.TTLCache(maxsize=16, ttl=60))
22
+ @ttl_cache()
23
23
  @retry_padrao
24
24
  def _buscar_texto_intraday() -> str:
25
25
  carga_requisicao = {"Dt_Ref": "", "saida": "csv"}
@@ -2,10 +2,10 @@ import datetime as dt
2
2
  import logging
3
3
  from io import StringIO
4
4
 
5
- import cachetools
6
5
  import polars as pl
7
6
  import requests
8
7
 
8
+ from pyield._internal.cache import ttl_cache
9
9
  from pyield._internal.retry import retry_padrao
10
10
 
11
11
  logger = logging.getLogger(__name__)
@@ -16,7 +16,7 @@ URL_ULTIMA_ETTJ = "https://www.anbima.com.br/informacoes/est-termo/CZ-down.asp"
16
16
  CASAS_DECIMAIS = 6
17
17
 
18
18
 
19
- @cachetools.cached(cache=cachetools.TTLCache(maxsize=16, ttl=60))
19
+ @ttl_cache()
20
20
  @retry_padrao
21
21
  def _buscar_texto_ultima_ettj() -> str:
22
22
  """Busca o texto bruto da curva de juros na ANBIMA."""
@@ -2,10 +2,10 @@ import io
2
2
  import logging
3
3
  from typing import Literal
4
4
 
5
- import cachetools
6
5
  import polars as pl
7
6
  import requests
8
7
 
8
+ from pyield._internal.cache import ttl_cache
9
9
  from pyield._internal.retry import retry_padrao
10
10
 
11
11
  logger = logging.getLogger(__name__)
@@ -82,7 +82,7 @@ ORDEM_COLUNAS_FINAL = [
82
82
  URL_ULTIMO_IMA = "https://www.anbima.com.br/informacoes/ima/arqs/ima_completo.txt"
83
83
 
84
84
 
85
- @cachetools.cached(cache=cachetools.TTLCache(maxsize=16, ttl=60))
85
+ @ttl_cache()
86
86
  @retry_padrao
87
87
  def _buscar_texto_ultimo_ima() -> str:
88
88
  resposta = requests.get(URL_ULTIMO_IMA, timeout=3)
@@ -293,10 +293,6 @@ def interpolate_rate(
293
293
  >>> # Extrapola taxa para uma data de vencimento futura
294
294
  >>> di1.interpolate_rate("25-04-2025", "01-01-2050", extrapolate=True)
295
295
  0.13881
296
-
297
- >>> # Entradas nulas retornam NaN
298
- >>> di1.interpolate_rate(None, "01-01-2030")
299
- nan
300
296
  """
301
297
  if any_is_collection(date, expiration):
302
298
  raise ValueError("As entradas 'date' e 'expiration' devem ser datas escalares.")
@@ -118,7 +118,7 @@ def _buscar_varias_datas(
118
118
  # Bulk: carrega todas as datas de uma vez do cache, por contrato
119
119
  resultados = []
120
120
  for codigo in codigos:
121
- df_cache = historical.obter_futuros_pr(datas_validas, codigo)
121
+ df_cache = historical._obter_futuros_pr(datas_validas, codigo)
122
122
  if not df_cache.is_empty():
123
123
  resultados.append(df_cache)
124
124
 
@@ -79,7 +79,7 @@ def historical(
79
79
  codigos_sem_cache: list[str] = []
80
80
 
81
81
  for codigo in codigos:
82
- df_cache = obter_futuros_pr([data], codigo)
82
+ df_cache = _obter_futuros_pr([data], codigo)
83
83
  if df_cache.is_empty():
84
84
  codigos_sem_cache.append(codigo)
85
85
  else:
@@ -116,7 +116,7 @@ def historical(
116
116
  return df_resultado.sort(*colunas_ordenacao)
117
117
 
118
118
 
119
- def obter_futuros_pr(datas: list[dt.date], codigo_contrato: str) -> pl.DataFrame:
119
+ def _obter_futuros_pr(datas: list[dt.date], codigo_contrato: str) -> pl.DataFrame:
120
120
  """Carrega histórico de futuros do dataset PR para uma lista de datas."""
121
121
  if not datas:
122
122
  return pl.DataFrame()
@@ -18,7 +18,7 @@ HORA_INICIO_INTRADAY = dt.time(9, 16)
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
20
 
21
- def data_intraday_valida(data_verificacao: dt.date) -> bool:
21
+ def _data_intraday_valida(data_verificacao: dt.date) -> bool:
22
22
  """Verifica se a data é elegível para consulta intraday."""
23
23
  if not data_negociacao_valida(data_verificacao):
24
24
  return False
@@ -1,9 +1,9 @@
1
1
  import logging
2
2
 
3
- import cachetools
4
3
  import polars as pl
5
4
  import requests
6
5
 
6
+ from pyield._internal.cache import ttl_cache
7
7
  from pyield._internal.retry import retry_padrao
8
8
 
9
9
  URL_BASE_INTRADAY = "https://cotacao.b3.com.br/mds/api/v1/DerivativeQuotation"
@@ -50,7 +50,7 @@ def _mapa_renomeacao_intraday() -> dict[str, str]:
50
50
  return {nome_orig: nome_novo for nome_orig, nome_novo, _ in COLUNAS_INTRADAY}
51
51
 
52
52
 
53
- @cachetools.cached(cache=cachetools.TTLCache(maxsize=16, ttl=15))
53
+ @ttl_cache(ttl=15)
54
54
  @retry_padrao
55
55
  def _buscar_json_intraday(codigo_contrato: str) -> list[dict]:
56
56
  url = f"{URL_BASE_INTRADAY}/{codigo_contrato}"
@@ -18,12 +18,12 @@ cotacaoCompra, cotacaoVenda, dataHoraCotacao
18
18
  import datetime as dt
19
19
  import logging
20
20
 
21
- import cachetools
22
21
  import polars as pl
23
22
  import requests
24
23
 
25
24
  import pyield._internal.converters as cv
26
25
  from pyield import clock
26
+ from pyield._internal.cache import ttl_cache
27
27
  from pyield._internal.retry import retry_padrao
28
28
  from pyield._internal.types import DateLike
29
29
 
@@ -54,7 +54,7 @@ def _montar_url_api(inicio: dt.date, fim: dt.date) -> str:
54
54
  return url
55
55
 
56
56
 
57
- @cachetools.cached(cache=cachetools.TTLCache(maxsize=16, ttl=15))
57
+ @ttl_cache(ttl=15)
58
58
  @retry_padrao
59
59
  def _buscar_texto_api(url: str) -> bytes:
60
60
  resposta = requests.get(url, timeout=10)
@@ -15,11 +15,11 @@ Notas de implementação:
15
15
  import logging
16
16
  from enum import Enum
17
17
 
18
- import cachetools
19
18
  import polars as pl
20
19
  import requests
21
20
 
22
21
  from pyield import clock
22
+ from pyield._internal.cache import ttl_cache
23
23
  from pyield._internal.converters import converter_datas
24
24
  from pyield._internal.retry import retry_padrao
25
25
  from pyield._internal.types import DateLike, any_is_empty
@@ -43,7 +43,7 @@ class SerieBC(Enum):
43
43
  DI_OVER = 11
44
44
 
45
45
 
46
- @cachetools.cached(cache=cachetools.TTLCache(maxsize=16, ttl=15))
46
+ @ttl_cache(ttl=15)
47
47
  @retry_padrao
48
48
  def _chamar_api(url_api: str) -> list[dict[str, str]]:
49
49
  """Executa uma chamada GET na API do BCB e retorna o JSON.
@@ -7,12 +7,12 @@ import datetime as dt
7
7
  import io
8
8
  import logging
9
9
 
10
- import cachetools
11
10
  import polars as pl
12
11
  import polars.selectors as cs
13
12
  import requests
14
13
 
15
14
  from pyield import bday, clock
15
+ from pyield._internal.cache import ttl_cache
16
16
  from pyield._internal.retry import retry_padrao
17
17
 
18
18
  HORA_INICIO_TEMPO_REAL = dt.time(9, 0, 0)
@@ -94,7 +94,7 @@ ORDEM_COLUNAS_FINAL = [
94
94
  ]
95
95
 
96
96
 
97
- @cachetools.cached(cache=cachetools.TTLCache(maxsize=16, ttl=15))
97
+ @ttl_cache(ttl=15)
98
98
  @retry_padrao
99
99
  def _buscar_csv() -> str:
100
100
  """
@@ -18,6 +18,7 @@ import zipfile as zf
18
18
  import polars as pl
19
19
  import requests
20
20
 
21
+ from pyield._internal.cache import ttl_cache
21
22
  from pyield._internal.converters import converter_datas
22
23
  from pyield._internal.retry import retry_padrao
23
24
  from pyield._internal.types import DateLike, any_is_empty
@@ -51,28 +52,6 @@ MAPA_COLUNAS = [
51
52
  ESQUEMA_CSV = {csv: tipo for csv, _, tipo in MAPA_COLUNAS}
52
53
  MAPEAMENTO_COLUNAS = {csv: novo for csv, novo, _ in MAPA_COLUNAS}
53
54
 
54
- ORDEM_COLUNAS_FINAL = [
55
- "SettlementDate",
56
- "BondType",
57
- "SelicCode",
58
- "ISIN",
59
- "IssueDate",
60
- "MaturityDate",
61
- "Trades",
62
- "Quantity",
63
- "Value",
64
- "MinPrice",
65
- "AvgPrice",
66
- "MaxPrice",
67
- "UnderlyingPrice",
68
- "ParValue",
69
- "MinRate",
70
- "AvgRate",
71
- "MaxRate",
72
- "BrokerageTrades",
73
- "BrokerageQuantity",
74
- ]
75
-
76
55
  CHAVES_ORDENACAO = ["SettlementDate", "BondType", "MaturityDate"]
77
56
 
78
57
 
@@ -90,6 +69,7 @@ def _montar_url(data_alvo: dt.date, extragroup: bool) -> str:
90
69
  return f"{URL_BASE}/Neg{sufixo_operacao}{ano_mes}.ZIP"
91
70
 
92
71
 
72
+ @ttl_cache()
93
73
  @retry_padrao
94
74
  def _baixar_zip(url_arquivo: str) -> bytes:
95
75
  """Baixa o conteúdo ZIP e retorna os bytes."""
@@ -116,7 +96,7 @@ def _ler_df_zip(conteudo_csv: bytes) -> pl.DataFrame:
116
96
 
117
97
 
118
98
  def _processar_df(df: pl.DataFrame) -> pl.DataFrame:
119
- """Processa tipos, calcula valor e ordena."""
99
+ """Renomeia colunas, converte datas, calcula valor e ordena."""
120
100
  colunas_data = ["SettlementDate", "IssueDate", "MaturityDate"]
121
101
  return (
122
102
  df.rename(MAPEAMENTO_COLUNAS)
@@ -128,12 +108,6 @@ def _processar_df(df: pl.DataFrame) -> pl.DataFrame:
128
108
  )
129
109
 
130
110
 
131
- def _ordenar_selecionar_colunas(df: pl.DataFrame) -> pl.DataFrame:
132
- """Reordena colunas e ordena linhas para saída consistente e determinística."""
133
- colunas_selecionadas = [col for col in ORDEM_COLUNAS_FINAL if col in df.columns]
134
- return df.select(colunas_selecionadas).sort(by=CHAVES_ORDENACAO)
135
-
136
-
137
111
  def tpf_monthly_trades(target_date: DateLike, extragroup: bool = False) -> pl.DataFrame:
138
112
  """Consulta negociações mensais no mercado secundário de TPF
139
113
  registradas no sistema Selic do BCB.
@@ -164,19 +138,19 @@ def tpf_monthly_trades(target_date: DateLike, extragroup: bool = False) -> pl.Da
164
138
  - ISIN (str): código ISIN (International Securities Identification Number).
165
139
  - IssueDate (Date): data de emissão do título.
166
140
  - MaturityDate (Date): data de vencimento do título.
167
- - Trades (Int64): número de operações realizadas.
168
- - Quantity (Int64): quantidade negociada.
141
+ - Trades (Int64): número total de operações realizadas.
142
+ - Quantity (Int64): quantidade total negociada.
169
143
  - Value (Float64): valor financeiro negociado (Quantity * AvgPrice).
170
144
  - MinPrice (Float64): preço unitário mínimo.
171
145
  - AvgPrice (Float64): preço unitário médio.
172
146
  - MaxPrice (Float64): preço unitário máximo.
173
147
  - UnderlyingPrice (Float64): PU lastro.
174
- - ParValue (Float64): valor par.
148
+ - ParValue (Float64): valor nominal atualizado (VNA) do título.
175
149
  - MinRate (Float64): taxa mínima.
176
150
  - AvgRate (Float64): taxa média.
177
151
  - MaxRate (Float64): taxa máxima.
178
- - BrokerageTrades (Int64): número de operações com corretagem.
179
- - BrokerageQuantity (Int64): quantidade negociada com corretagem.
152
+ - BrokerageTrades (Int64): subconjunto de Trades com corretagem.
153
+ - BrokerageQuantity (Int64): subconjunto de Quantity com corretagem.
180
154
 
181
155
  Notes:
182
156
  - Dados ordenados por: SettlementDate, BondType, MaturityDate.
@@ -187,7 +161,6 @@ def tpf_monthly_trades(target_date: DateLike, extragroup: bool = False) -> pl.Da
187
161
 
188
162
  """
189
163
  if any_is_empty(target_date):
190
- registro.warning("Nenhuma data informada. Retornando DataFrame vazio.")
191
164
  return pl.DataFrame()
192
165
  try:
193
166
  data_alvo = converter_datas(target_date)
@@ -197,7 +170,6 @@ def tpf_monthly_trades(target_date: DateLike, extragroup: bool = False) -> pl.Da
197
170
  arquivo_extraido = _descompactar_zip(conteudo_zip)
198
171
  df = _ler_df_zip(arquivo_extraido)
199
172
  df = _processar_df(df)
200
- df = _ordenar_selecionar_colunas(df)
201
173
  except Exception as e:
202
174
  registro.exception(f"Erro ao coletar dados mensais do BCB: {e}")
203
175
  return pl.DataFrame()
@@ -1,8 +1,8 @@
1
1
  import logging
2
2
 
3
- import cachetools
4
3
  import requests
5
4
 
5
+ from pyield._internal.cache import ttl_cache
6
6
  from pyield._internal.converters import converter_datas
7
7
  from pyield._internal.retry import retry_padrao
8
8
  from pyield._internal.types import DateLike, any_is_empty
@@ -10,7 +10,7 @@ from pyield._internal.types import DateLike, any_is_empty
10
10
  logger = logging.getLogger(__name__)
11
11
 
12
12
 
13
- @cachetools.cached(cache=cachetools.TTLCache(maxsize=16, ttl=15))
13
+ @ttl_cache(ttl=15)
14
14
  @retry_padrao
15
15
  def _baixar_texto(date: DateLike) -> str:
16
16
  """Baixa o arquivo diário do SELIC no site do BCB."""
@@ -143,6 +143,10 @@ def count(
143
143
  >>> bday.count("15-12-2023", "01-01-2024")
144
144
  10
145
145
 
146
+ Contagem negativa quando ``start`` é posterior a ``end``:
147
+ >>> bday.count("08-01-2023", "01-01-2023")
148
+ -5
149
+
146
150
  Total de dias úteis em janeiro e fevereiro desde o início do ano:
147
151
  >>> bday.count(start="01-01-2024", end=["01-02-2024", "01-03-2024"])
148
152
  shape: (2,)
@@ -266,10 +266,6 @@ def forward(
266
266
  >>> print(yd.forward(20, 10, 0.06, 0.05))
267
267
  nan
268
268
 
269
- >>> # Argumentos nulos retornam nan
270
- >>> print(yd.forward(10, 20, 0.05, None))
271
- nan
272
-
273
269
  Notes:
274
270
  `du₂` precisa ser necessariamente maior que `du₁` para que
275
271
  o cálculo da taxa a termo seja matematicamente válido.
@@ -0,0 +1,103 @@
1
+ import datetime as dt
2
+ from dataclasses import dataclass
3
+
4
+ import polars as pl
5
+ import requests
6
+
7
+ from pyield._internal.cache import ttl_cache
8
+ from pyield._internal.retry import retry_padrao
9
+
10
+ _URL_XLS = "https://www.anbima.com.br/informacoes/indicadores/arqs/indicadores.xls"
11
+
12
+
13
+ @dataclass
14
+ class ProjecaoIndicador:
15
+ ultima_atualizacao: dt.datetime # Data e hora da última atualização
16
+ periodo_referencia: str # Mês de referência no formato "MMM/YY"
17
+ valor_projetado: float # Valor projetado
18
+
19
+
20
+ @ttl_cache()
21
+ @retry_padrao
22
+ def _baixar_planilha() -> bytes:
23
+ """Baixa o arquivo XLS de indicadores da ANBIMA e retorna os bytes."""
24
+ try:
25
+ r = requests.get(_URL_XLS, timeout=10)
26
+ r.raise_for_status()
27
+ return r.content
28
+ except requests.exceptions.RequestException as e:
29
+ raise ConnectionError(f"Erro ao acessar a planilha da ANBIMA: {e}")
30
+
31
+
32
+ def _extrair_datetime(texto: str) -> dt.datetime:
33
+ """Extrai datetime do texto da primeira célula do XLS.
34
+
35
+ Formato esperado:
36
+ "Data e Hora da Última Atualização: 13/03/2026 - 15:20 h"
37
+ """
38
+ # Prefixo fixo → restante é " 13/03/2026 - 15:20 h"
39
+ prefixo = "Data e Hora da Última Atualização:"
40
+ parte_data = texto.split(prefixo, maxsplit=1)[1]
41
+ parte_data = parte_data.strip().removesuffix("h").strip()
42
+ return dt.datetime.strptime(parte_data, "%d/%m/%Y - %H:%M")
43
+
44
+
45
+ def _extrair_periodo(texto: str) -> str:
46
+ """Extrai o período de referência de 'Projeção (mar/26)' → 'mar/26'."""
47
+ inicio = texto.index("(") + 1
48
+ fim = texto.index(")")
49
+ return texto[inicio:fim]
50
+
51
+
52
+ def projected_rate() -> ProjecaoIndicador:
53
+ """
54
+ Obtém a projeção atual do IPCA no site da ANBIMA.
55
+
56
+ A função baixa a planilha XLS de indicadores da ANBIMA e extrai os
57
+ dados da projeção do IPCA.
58
+
59
+ Returns:
60
+ ProjecaoIndicador: Objeto contendo:
61
+ - ultima_atualizacao (dt.datetime): Data e hora da última
62
+ atualização.
63
+ - periodo_referencia (str): Período de referência no formato
64
+ "MMM/YY" (ex.: "mar/26").
65
+ - valor_projetado (float): Valor projetado do IPCA (decimal).
66
+
67
+ Raises:
68
+ ConnectionError: Se houver erro de conexão com o site da ANBIMA.
69
+ ValueError: Se os dados esperados não forem encontrados na
70
+ planilha.
71
+
72
+ Notes:
73
+ Requer conexão com a internet para acessar o site da ANBIMA.
74
+
75
+ Examples:
76
+ >>> from pyield import ipca
77
+ >>> # Obter a projeção atual do IPCA na ANBIMA
78
+ >>> ipca.projected_rate()
79
+ ProjecaoIndicador(ultima_atualizacao=..., periodo_referencia=..., valor_projetado=...)
80
+ """
81
+ conteudo = _baixar_planilha()
82
+ df = pl.read_excel(conteudo, has_header=False)
83
+
84
+ # Linha 0: "Data e Hora da Última Atualização: DD/MM/YYYY - HH:MM h"
85
+ texto_atualizacao = df.item(0, "column_1")
86
+ ultima_atualizacao = _extrair_datetime(texto_atualizacao)
87
+
88
+ # Linha do IPCA: column_1 começa com "IPCA" e column_2 com "Projeção"
89
+ linha_ipca = df.filter(
90
+ pl.col("column_1").str.starts_with("IPCA")
91
+ & pl.col("column_2").str.starts_with("Projeção")
92
+ )
93
+ if linha_ipca.is_empty():
94
+ raise ValueError("Não foi possível encontrar a projeção do IPCA na planilha.")
95
+
96
+ periodo_referencia = _extrair_periodo(linha_ipca.item(0, "column_2"))
97
+ valor_projetado = round(float(linha_ipca.item(0, "column_3")) / 100, 4)
98
+
99
+ return ProjecaoIndicador(
100
+ ultima_atualizacao=ultima_atualizacao,
101
+ periodo_referencia=periodo_referencia,
102
+ valor_projetado=valor_projetado,
103
+ )
@@ -17,6 +17,7 @@ URL_BASE = (
17
17
  "https://www.tesourotransparente.gov.br/publicacoes/relatorio-mensal-da-divida-rmd"
18
18
  )
19
19
  _ABAS_DISPONIVEIS = ("1.3",)
20
+ _TIMEOUT_SEGUNDOS = 60
20
21
 
21
22
  # Índices de linha (0-based) na planilha após leitura com fastexcel (sem cabeçalho)
22
23
  # O fastexcel compacta linhas totalmente vazias, resultando em 81 linhas ao invés das
@@ -89,17 +90,9 @@ def _parsear_periodo(periodo: str) -> datetime.date | None:
89
90
 
90
91
 
91
92
  @retry_padrao
92
- def _buscar_pagina(url: str) -> bytes:
93
+ def _buscar_conteudo(url: str) -> bytes:
93
94
  """Busca o conteúdo de uma URL, seguindo redirects, com retry."""
94
- resposta = requests.get(url, timeout=15)
95
- resposta.raise_for_status()
96
- return resposta.content
97
-
98
-
99
- @retry_padrao
100
- def _baixar_zip(url: str) -> bytes:
101
- """Baixa o arquivo ZIP com retry."""
102
- resposta = requests.get(url, timeout=60)
95
+ resposta = requests.get(url, timeout=_TIMEOUT_SEGUNDOS)
103
96
  resposta.raise_for_status()
104
97
  return resposta.content
105
98
 
@@ -110,7 +103,7 @@ def _buscar_url_anexo() -> str:
110
103
  A URL base redireciona automaticamente para a página do mês atual.
111
104
  O lxml localiza o link do anexo ZIP nessa página.
112
105
  """
113
- conteudo_pagina = _buscar_pagina(URL_BASE)
106
+ conteudo_pagina = _buscar_conteudo(URL_BASE)
114
107
  arvore = html.fromstring(conteudo_pagina)
115
108
  resultado = arvore.xpath("//a[contains(@href, 'publicacao-anexo')]/@href")
116
109
  if not isinstance(resultado, list) or not resultado:
@@ -169,25 +162,15 @@ def _classificar_categorias(
169
162
  return eventos_titulos, eventos_diretos
170
163
 
171
164
 
172
- def _construir_grupos(
173
- eventos: list[tuple[int, str, str, str]],
174
- ) -> dict[tuple[str, str], dict[str, int]]:
175
- """Agrupa eventos por (grupo, subgrupo): {chave → {titulo → índice na matriz}}."""
176
- grupos: dict[tuple[str, str], dict[str, int]] = {}
177
- for idx, grupo, subgrupo, titulo in eventos:
178
- chave = (grupo, subgrupo)
179
- if chave not in grupos:
180
- grupos[chave] = {}
181
- grupos[chave][titulo] = idx
182
- return grupos
183
-
184
-
185
165
  def _montar_df_titulos(
186
- grupos: dict[tuple[str, str], dict[str, int]],
166
+ eventos: list[tuple[int, str, str, str]],
187
167
  datas_mensais: list[datetime.date],
188
168
  matriz: pl.DataFrame,
189
169
  ) -> pl.DataFrame:
190
170
  """Monta DataFrame de detalhamento por título na ordem canônica, após unpivot."""
171
+ grupos: dict[tuple[str, str], dict[str, int]] = {}
172
+ for idx, grupo, subgrupo, titulo in eventos:
173
+ grupos.setdefault((grupo, subgrupo), {})[titulo] = idx
191
174
  n_periodos = len(datas_mensais)
192
175
  partes: list[pl.DataFrame] = []
193
176
  for grupo, subgrupo in _ORDEM_SUBGRUPOS:
@@ -267,8 +250,7 @@ def _estruturar_dados(conteudo_excel: bytes) -> pl.DataFrame:
267
250
  # Matriz de valores (apenas períodos mensais), cast para Float64
268
251
  matriz = df_dados[:, 1:].cast(pl.Float64, strict=False)[:, indices_mensais]
269
252
 
270
- grupos = _construir_grupos(eventos_titulos)
271
- df_titulos = _montar_df_titulos(grupos, datas_mensais, matriz)
253
+ df_titulos = _montar_df_titulos(eventos_titulos, datas_mensais, matriz)
272
254
  partes_diretas = _montar_partes_diretos(eventos_diretos, datas_mensais, matriz)
273
255
  return (
274
256
  pl.concat([df_titulos] + partes_diretas)
@@ -277,9 +259,6 @@ def _estruturar_dados(conteudo_excel: bytes) -> pl.DataFrame:
277
259
  )
278
260
 
279
261
 
280
- _PROCESSADORES: dict[str, object] = {"1.3": _estruturar_dados}
281
-
282
-
283
262
  def rmd(tab: str) -> pl.DataFrame:
284
263
  """Retorna dados do Relatório Mensal da Dívida (RMD) do Tesouro Nacional.
285
264
 
@@ -353,18 +332,17 @@ def rmd(tab: str) -> pl.DataFrame:
353
332
  >>> round(resgates_2025, 2)
354
333
  1395109062272.45
355
334
  """
356
- if tab not in _PROCESSADORES:
357
- disponiveis = ", ".join(f'"{t}"' for t in sorted(_PROCESSADORES))
335
+ if tab not in _ABAS_DISPONIVEIS:
336
+ disponiveis = ", ".join(f'"{t}"' for t in sorted(_ABAS_DISPONIVEIS))
358
337
  raise ValueError(
359
338
  f"Aba '{tab}' não disponível. Abas implementadas: {disponiveis}."
360
339
  )
361
- processador = _PROCESSADORES[tab]
362
340
  try:
363
341
  url_anexo = _buscar_url_anexo()
364
342
  registro.debug(f"URL do anexo RMD: {url_anexo}")
365
- conteudo_zip = _baixar_zip(url_anexo)
343
+ conteudo_zip = _buscar_conteudo(url_anexo)
366
344
  conteudo_excel = _extrair_excel(conteudo_zip)
367
- df = processador(conteudo_excel) # type: ignore[operator]
345
+ df = _estruturar_dados(conteudo_excel)
368
346
  except Exception as e:
369
347
  registro.exception(f"Erro ao coletar dados do RMD (aba {tab!r}): {e}")
370
348
  return pl.DataFrame()