PYield 0.43.3__tar.gz → 0.43.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {pyield-0.43.3 → pyield-0.43.4}/PKG-INFO +3 -1
  2. {pyield-0.43.3 → pyield-0.43.4}/README.md +1 -0
  3. pyield-0.43.4/pyield/__about__.py +1 -0
  4. {pyield-0.43.3 → pyield-0.43.4}/pyield/__init__.py +2 -0
  5. {pyield-0.43.3 → pyield-0.43.4}/pyield/_internal/__init__.py +0 -1
  6. pyield-0.43.4/pyield/rmd.py +373 -0
  7. {pyield-0.43.3 → pyield-0.43.4}/pyproject.toml +19 -2
  8. pyield-0.43.3/pyield/__about__.py +0 -1
  9. {pyield-0.43.3 → pyield-0.43.4}/.gitignore +0 -0
  10. {pyield-0.43.3 → pyield-0.43.4}/LICENSE +0 -0
  11. {pyield-0.43.3 → pyield-0.43.4}/pyield/_internal/converters.py +0 -0
  12. {pyield-0.43.3 → pyield-0.43.4}/pyield/_internal/data_cache.py +1 -1
  13. {pyield-0.43.3 → pyield-0.43.4}/pyield/_internal/retry.py +0 -0
  14. {pyield-0.43.3 → pyield-0.43.4}/pyield/_internal/types.py +0 -0
  15. {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/__init__.py +0 -0
  16. {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/difusao.py +0 -0
  17. {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/ettj_intraday.py +0 -0
  18. {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/ettj_last.py +0 -0
  19. {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/ima.py +0 -0
  20. {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/imaq.py +0 -0
  21. {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/tpf.py +0 -0
  22. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/__init__.py +0 -0
  23. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/common.py +0 -0
  24. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/di1.py +0 -0
  25. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/di_over.py +0 -0
  26. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/__init__.py +0 -0
  27. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/core.py +0 -0
  28. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/historical/__init__.py +0 -0
  29. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/historical/core.py +0 -0
  30. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/historical/historical_b3.py +0 -0
  31. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/historical/historical_bmf.py +0 -0
  32. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/intraday.py +0 -0
  33. {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/price_report.py +0 -0
  34. {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/__init__.py +0 -0
  35. {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/auction.py +0 -0
  36. {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/ptax_api.py +0 -0
  37. {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/rates.py +0 -0
  38. {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/repo.py +0 -0
  39. {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/trades_intraday.py +0 -0
  40. {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/trades_monthly.py +0 -0
  41. {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/vna.py +0 -0
  42. {pyield-0.43.3 → pyield-0.43.4}/pyield/bday/__init__.py +0 -0
  43. {pyield-0.43.3 → pyield-0.43.4}/pyield/bday/core.py +0 -0
  44. {pyield-0.43.3 → pyield-0.43.4}/pyield/bday/holidays/br_holidays_new.txt +0 -0
  45. {pyield-0.43.3 → pyield-0.43.4}/pyield/bday/holidays/br_holidays_old.txt +0 -0
  46. {pyield-0.43.3 → pyield-0.43.4}/pyield/bday/holidays/brholidays.py +0 -0
  47. {pyield-0.43.3 → pyield-0.43.4}/pyield/clock.py +0 -0
  48. {pyield-0.43.3 → pyield-0.43.4}/pyield/fwd.py +0 -0
  49. {pyield-0.43.3 → pyield-0.43.4}/pyield/interpolator.py +0 -0
  50. {pyield-0.43.3 → pyield-0.43.4}/pyield/ipca/__init__.py +0 -0
  51. {pyield-0.43.3 → pyield-0.43.4}/pyield/ipca/historical.py +0 -0
  52. {pyield-0.43.3 → pyield-0.43.4}/pyield/ipca/projected.py +0 -0
  53. {pyield-0.43.3 → pyield-0.43.4}/pyield/py.typed +0 -0
  54. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/__init__.py +0 -0
  55. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/auctions.py +0 -0
  56. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/benchmark.py +0 -0
  57. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/lft.py +0 -0
  58. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ltn.py +0 -0
  59. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ntnb.py +0 -0
  60. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ntnb1.py +0 -0
  61. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ntnbprinc.py +0 -0
  62. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ntnc.py +0 -0
  63. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ntnf.py +0 -0
  64. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/pre.py +0 -0
  65. {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyield
3
- Version: 0.43.3
3
+ Version: 0.43.4
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,6 +36,7 @@ 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: fastexcel>=0.19.0
39
40
  Requires-Dist: lxml>=6.0.0
40
41
  Requires-Dist: polars>=1.33.0
41
42
  Requires-Dist: python-dateutil>=2.8.0
@@ -178,6 +179,7 @@ forwards(bdays, rates) # -> Series: [0.05, 0.070095, 0.090284]
178
179
  | `anbima` | Dados da ANBIMA (preços de TPF, curvas de juros, índices IMA) |
179
180
  | `bc` | Indicadores do BCB (SELIC, PTAX, repos, VNA) |
180
181
  | `ipca` | Dados de inflação (histórico e projeções) |
182
+ | `rmd` | Relatório Mensal da Dívida do Tesouro Nacional |
181
183
  | `today` / `now` | Data/hora atual no Brasil (America/Sao_Paulo) |
182
184
 
183
185
  ## Títulos Públicos
@@ -133,6 +133,7 @@ forwards(bdays, rates) # -> Series: [0.05, 0.070095, 0.090284]
133
133
  | `anbima` | Dados da ANBIMA (preços de TPF, curvas de juros, índices IMA) |
134
134
  | `bc` | Indicadores do BCB (SELIC, PTAX, repos, VNA) |
135
135
  | `ipca` | Dados de inflação (histórico e projeções) |
136
+ | `rmd` | Relatório Mensal da Dívida do Tesouro Nacional |
136
137
  | `today` / `now` | Data/hora atual no Brasil (America/Sao_Paulo) |
137
138
 
138
139
  ## Títulos Públicos
@@ -0,0 +1 @@
1
+ __version__ = "0.43.4"
@@ -6,6 +6,7 @@ from pyield.b3 import di1, futures
6
6
  from pyield.clock import now, today
7
7
  from pyield.fwd import forward, forwards
8
8
  from pyield.interpolator import Interpolator
9
+ from pyield.rmd import rmd
9
10
  from pyield.tn import lft, ltn, ntnb, ntnb1, ntnbprinc, ntnc, ntnf, pre
10
11
 
11
12
  __all__ = [
@@ -31,6 +32,7 @@ __all__ = [
31
32
  "ntnc",
32
33
  "ntnf",
33
34
  "pre",
35
+ "rmd",
34
36
  "bday",
35
37
  ]
36
38
 
@@ -2,4 +2,3 @@
2
2
 
3
3
  Este módulo não faz parte da API pública estável do PYield.
4
4
  """
5
-
@@ -0,0 +1,373 @@
1
+ """Módulo para buscar dados do Relatório Mensal da Dívida (RMD) do Tesouro Nacional."""
2
+
3
+ import datetime
4
+ import io
5
+ import logging
6
+ import zipfile as zf
7
+
8
+ import polars as pl
9
+ import requests
10
+ from lxml import html
11
+
12
+ from pyield._internal.retry import retry_padrao
13
+
14
+ registro = logging.getLogger(__name__)
15
+
16
+ URL_BASE = (
17
+ "https://www.tesourotransparente.gov.br/publicacoes/relatorio-mensal-da-divida-rmd"
18
+ )
19
+ _ABAS_DISPONIVEIS = ("1.3",)
20
+
21
+ # Índices de linha (0-based) na planilha após leitura com fastexcel (sem cabeçalho)
22
+ # O fastexcel compacta linhas totalmente vazias, resultando em 81 linhas ao invés das
23
+ # 101 do Excel bruto. Os índices abaixo refletem o layout observado no arquivo atual.
24
+ _LINHA_PERIODOS = 2 # Rótulos de período: "Nov/06", "Dez/06", ..., "2025"
25
+ _LINHA_INICIO_DADOS = 3 # Primeira linha de dados: "I - EMISSÕES"
26
+ _LINHA_FIM_DADOS = 67 # Exclusivo: notas de rodapé a partir desta linha
27
+
28
+ # Tipos de título que viram colunas (em ordem)
29
+ _TITULOS = ("LFT", "LTN", "NTN-B", "NTN-B1", "NTN-F", "NTN-C", "NTN-D", "Demais")
30
+
31
+ # Mapeamento de rótulo de seção → nome limpo
32
+ _SECOES = {"I - EMISSÕES": "Emissões", "II - RESGATES": "Resgates"}
33
+
34
+ # Rótulos de subgrupo conhecidos e prefixo do Tesouro Direto
35
+ _SUBGRUPOS = {"Vendas", "Trocas", "Vencimentos", "Compras"}
36
+ _SUBGRUPO_TD = "Tesouro Direto"
37
+
38
+ # Subgrupos sem detalhamento por tipo de título (valor direto na linha)
39
+ # Tuple para ordem determinística; correspondência por prefixo (ignora notas de rodapé)
40
+ _SUBGRUPOS_DIRETOS = (
41
+ "Transferência de Carteira",
42
+ "Emissão Direta com Financeiro",
43
+ "Emissão Direta sem Financeiro",
44
+ "Pagamento de Dividendos",
45
+ "Cancelamentos",
46
+ )
47
+
48
+ # Prefixos que sinalizam fim da área de interesse (seções a ignorar)
49
+ _PREFIXOS_IGNORAR = ("IMPACTO", "OPERAÇÕES", "III -", "RESGATE")
50
+
51
+ _MESES_PT = {
52
+ "Jan": 1,
53
+ "Fev": 2,
54
+ "Mar": 3,
55
+ "Abr": 4,
56
+ "Mai": 5,
57
+ "Jun": 6,
58
+ "Jul": 7,
59
+ "Ago": 8,
60
+ "Set": 9,
61
+ "Out": 10,
62
+ "Nov": 11,
63
+ "Dez": 12,
64
+ }
65
+
66
+ # Ordem canônica das combinações (Section, Subgroup)
67
+ _ORDEM_SUBGRUPOS = [
68
+ ("Emissões", "Vendas"),
69
+ ("Emissões", "Trocas"),
70
+ ("Emissões", "Tesouro Direto"),
71
+ ("Resgates", "Vencimentos"),
72
+ ("Resgates", "Compras"),
73
+ ("Resgates", "Trocas"),
74
+ ("Resgates", "Tesouro Direto"),
75
+ ]
76
+
77
+
78
+ def _parsear_periodo(periodo: str) -> datetime.date | None:
79
+ """Converte string de período para datetime.date ou None para totais anuais."""
80
+ try:
81
+ mes_str, ano_str = periodo.split("/")
82
+ except ValueError:
83
+ return None # ex: "2025" (total anual) → descartado
84
+ mes = _MESES_PT.get(mes_str)
85
+ if mes is None:
86
+ return None
87
+ ano = 2000 + int(ano_str)
88
+ return datetime.date(ano, mes, 1)
89
+
90
+
91
+ @retry_padrao
92
+ def _buscar_pagina(url: str) -> bytes:
93
+ """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)
103
+ resposta.raise_for_status()
104
+ return resposta.content
105
+
106
+
107
+ def _buscar_url_anexo() -> str:
108
+ """Encontra a URL do arquivo ZIP do anexo mais recente do RMD.
109
+
110
+ A URL base redireciona automaticamente para a página do mês atual.
111
+ O lxml localiza o link do anexo ZIP nessa página.
112
+ """
113
+ conteudo_pagina = _buscar_pagina(URL_BASE)
114
+ arvore = html.fromstring(conteudo_pagina)
115
+ resultado = arvore.xpath("//a[contains(@href, 'publicacao-anexo')]/@href")
116
+ if not isinstance(resultado, list) or not resultado:
117
+ raise ValueError("Link do anexo ZIP não encontrado na página do RMD.")
118
+ return str(resultado[0])
119
+
120
+
121
+ def _extrair_excel(conteudo_zip: bytes) -> bytes:
122
+ """Extrai o arquivo Excel do ZIP."""
123
+ with zf.ZipFile(io.BytesIO(conteudo_zip), "r") as arquivo_zip:
124
+ nomes_excel = [
125
+ n for n in arquivo_zip.namelist() if n.lower().endswith((".xlsx", ".xls"))
126
+ ]
127
+ if not nomes_excel:
128
+ raise ValueError("Nenhum arquivo Excel encontrado no ZIP do RMD.")
129
+ return arquivo_zip.read(nomes_excel[0])
130
+
131
+
132
+ def _classificar_categorias(
133
+ categorias: list[str],
134
+ ) -> tuple[list[tuple[int, str, str, str]], list[tuple[int, str, str]]]:
135
+ """Percorre rótulos de categoria e emite eventos de título e de subgrupo direto.
136
+
137
+ Implementa uma máquina de estados que rastreia o grupo (Emissões/Resgates) e o
138
+ subgrupo corrente. Emite dois tipos de evento:
139
+ - eventos_titulos: linhas de detalhe por título (idx, grupo, subgrupo, titulo)
140
+ - eventos_diretos: subgrupos sem detalhamento por título (idx, grupo, subgrupo)
141
+
142
+ Args:
143
+ categorias: Lista de rótulos de categoria lidos da coluna 0 do Excel.
144
+
145
+ Returns:
146
+ Par (eventos_titulos, eventos_diretos) com as listas de eventos detectados.
147
+ """
148
+ grupo = ""
149
+ subgrupo = ""
150
+ eventos_titulos: list[tuple[int, str, str, str]] = []
151
+ eventos_diretos: list[tuple[int, str, str]] = []
152
+ for i, cat in enumerate(categorias):
153
+ c = cat.strip()
154
+ if c in _SECOES:
155
+ grupo, subgrupo = _SECOES[c], ""
156
+ elif any(c.startswith(p) for p in _PREFIXOS_IGNORAR):
157
+ grupo = ""
158
+ elif grupo:
159
+ if c in _SUBGRUPOS:
160
+ subgrupo = c
161
+ elif c.startswith(_SUBGRUPO_TD):
162
+ subgrupo = _SUBGRUPO_TD
163
+ elif c in _TITULOS:
164
+ eventos_titulos.append((i, grupo, subgrupo, c))
165
+ else:
166
+ prefixo = next((p for p in _SUBGRUPOS_DIRETOS if c.startswith(p)), None)
167
+ if prefixo:
168
+ eventos_diretos.append((i, grupo, prefixo))
169
+ return eventos_titulos, eventos_diretos
170
+
171
+
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
+ def _montar_df_titulos(
186
+ grupos: dict[tuple[str, str], dict[str, int]],
187
+ datas_mensais: list[datetime.date],
188
+ matriz: pl.DataFrame,
189
+ ) -> pl.DataFrame:
190
+ """Monta DataFrame de detalhamento por título na ordem canônica, após unpivot."""
191
+ n_periodos = len(datas_mensais)
192
+ partes: list[pl.DataFrame] = []
193
+ for grupo, subgrupo in _ORDEM_SUBGRUPOS:
194
+ chave = (grupo, subgrupo)
195
+ if chave not in grupos:
196
+ continue
197
+ titulos_presentes = grupos[chave]
198
+ colunas: dict[str, pl.Series] = {
199
+ "periodo": pl.Series("periodo", datas_mensais, dtype=pl.Date),
200
+ "grupo": pl.Series("grupo", [grupo] * n_periodos),
201
+ "subgrupo": pl.Series("subgrupo", [subgrupo] * n_periodos),
202
+ }
203
+ for titulo in _TITULOS:
204
+ if titulo in titulos_presentes:
205
+ valores = list(matriz.row(titulos_presentes[titulo]))
206
+ else:
207
+ valores = [None] * n_periodos
208
+ colunas[titulo] = pl.Series(titulo, valores, dtype=pl.Float64)
209
+ partes.append(pl.DataFrame(colunas))
210
+ return pl.concat(partes).unpivot(
211
+ on=list(_TITULOS),
212
+ index=["periodo", "grupo", "subgrupo"],
213
+ variable_name="titulo",
214
+ value_name="valor",
215
+ )
216
+
217
+
218
+ def _montar_partes_diretos(
219
+ eventos: list[tuple[int, str, str]],
220
+ datas_mensais: list[datetime.date],
221
+ matriz: pl.DataFrame,
222
+ ) -> list[pl.DataFrame]:
223
+ """Monta lista de DataFrames para subgrupos sem detalhamento por título."""
224
+ n_periodos = len(datas_mensais)
225
+ return [
226
+ pl.DataFrame(
227
+ {
228
+ "periodo": pl.Series("periodo", datas_mensais, dtype=pl.Date),
229
+ "grupo": pl.Series("grupo", [grupo] * n_periodos),
230
+ "subgrupo": pl.Series("subgrupo", [subgrupo] * n_periodos),
231
+ "titulo": pl.Series("titulo", [None] * n_periodos, dtype=pl.String),
232
+ "valor": pl.Series("valor", list(matriz.row(idx)), dtype=pl.Float64),
233
+ }
234
+ )
235
+ for idx, grupo, subgrupo in eventos
236
+ ]
237
+
238
+
239
+ def _estruturar_dados(conteudo_excel: bytes) -> pl.DataFrame:
240
+ """Lê a aba '1.3' do Excel e retorna DataFrame longo com emissões e resgates."""
241
+ df_bruto = pl.read_excel(
242
+ conteudo_excel,
243
+ sheet_name="1.3",
244
+ has_header=False,
245
+ )
246
+
247
+ # Rótulos de período: "Nov/06", "Dez/06", ..., "Dez/25", "2025"
248
+ periodos_raw = [str(p) for p in df_bruto.row(_LINHA_PERIODOS)[1:] if p is not None]
249
+
250
+ # Filtra totais anuais e converte para datetime.date
251
+ datas_e_indices = [
252
+ (i, d)
253
+ for i, periodo in enumerate(periodos_raw)
254
+ if (d := _parsear_periodo(periodo)) is not None
255
+ ]
256
+ indices_mensais = [i for i, _ in datas_e_indices]
257
+ datas_mensais = [d for _, d in datas_e_indices]
258
+
259
+ # Linhas de dados, excluindo linhas sem rótulo de categoria
260
+ df_dados = df_bruto[_LINHA_INICIO_DADOS:_LINHA_FIM_DADOS]
261
+ df_dados = df_dados.filter(df_dados[:, 0].is_not_null())
262
+
263
+ eventos_titulos, eventos_diretos = _classificar_categorias(
264
+ [str(c) for c in df_dados[:, 0].to_list()]
265
+ )
266
+
267
+ # Matriz de valores (apenas períodos mensais), cast para Float64
268
+ matriz = df_dados[:, 1:].cast(pl.Float64, strict=False)[:, indices_mensais]
269
+
270
+ grupos = _construir_grupos(eventos_titulos)
271
+ df_titulos = _montar_df_titulos(grupos, datas_mensais, matriz)
272
+ partes_diretas = _montar_partes_diretos(eventos_diretos, datas_mensais, matriz)
273
+ return (
274
+ pl.concat([df_titulos] + partes_diretas)
275
+ .with_columns(valor=pl.col("valor").mul(1_000_000).round(2))
276
+ .filter(pl.col("valor").is_not_null() & (pl.col("valor") != 0))
277
+ )
278
+
279
+
280
+ _PROCESSADORES: dict[str, object] = {"1.3": _estruturar_dados}
281
+
282
+
283
+ def rmd(tab: str) -> pl.DataFrame:
284
+ """Retorna dados do Relatório Mensal da Dívida (RMD) do Tesouro Nacional.
285
+
286
+ Baixa e processa a planilha do RMD, extraindo dados de emissões e resgates
287
+ de Títulos Públicos Federais da Dívida Pública Mobiliária Federal interna
288
+ (DPMFi). A publicação mais recente é descoberta automaticamente via parse
289
+ HTML da página oficial.
290
+
291
+ Args:
292
+ tab: Número da aba a processar (ex: "1.3"). Abas implementadas: "1.3".
293
+
294
+ Returns:
295
+ DataFrame longo com dados de emissões e resgates por período, seção,
296
+ subgrupo e tipo de título. Registros com valor nulo ou zero são excluídos.
297
+ Em caso de erro, retorna DataFrame vazio e registra log da exceção.
298
+
299
+ Output Columns (para tab='1.3'):
300
+ * periodo (Date): primeiro dia do mês de referência (ex: date(2006, 11, 1)).
301
+ * grupo (String): seção principal — "Emissões" ou "Resgates".
302
+ * subgrupo (String): categoria dentro do grupo:
303
+ - Emissões:
304
+ "Vendas"
305
+ "Trocas"
306
+ "Tesouro Direto"
307
+ "Transferência de Carteira"
308
+ "Emissão Direta com Financeiro"
309
+ "Emissão Direta sem Financeiro".
310
+ - Resgates:
311
+ "Vencimentos"
312
+ "Compras"
313
+ "Trocas"
314
+ "Tesouro Direto"
315
+ "Pagamento de Dividendos"
316
+ "Cancelamentos".
317
+ * titulo (String): tipo de título:
318
+ - "LFT"
319
+ - "LTN"
320
+ - "NTN-B"
321
+ - "NTN-B1"
322
+ - "NTN-F"
323
+ - "NTN-C"
324
+ - "NTN-D"
325
+ - "Demais"
326
+ - null para subgrupos sem detalhamento por título
327
+ (ex: "Transferência de Carteira").
328
+ * valor (Float64): valor em R$.
329
+
330
+ Raises:
331
+ ValueError: Se `tab` não estiver entre as abas implementadas.
332
+
333
+ Notes:
334
+ - A função sempre busca a publicação mais recente disponível.
335
+ - Totais anuais são excluídos; podem ser recalculados via group_by.
336
+ - Operações do Banco Central e totais agregados são excluídos; podem ser
337
+ recalculados por agrupamento quando necessário.
338
+ - Registros com valor nulo ou zero são omitidos.
339
+ - Totais de referência para 2025 (validados contra o Excel do Tesouro Nacional):
340
+ Emissões = R$ 1.840.946.621.648,18
341
+ Resgates = R$ 1.395.109.062.272,45.
342
+
343
+ Examples:
344
+ >>> import polars as pl
345
+ >>> from pyield import rmd
346
+ >>> df = rmd("1.3")
347
+ >>> df_2025 = df.filter(pl.col("periodo").dt.year() == 2025)
348
+ >>> # Totais de 2025 — ver valores de referência do Tesouro Nacional nas Notes
349
+ >>> emissoes_2025 = df_2025.filter(pl.col("grupo") == "Emissões")["valor"].sum()
350
+ >>> round(emissoes_2025, 2)
351
+ 1840946621648.18
352
+ >>> resgates_2025 = df_2025.filter(pl.col("grupo") == "Resgates")["valor"].sum()
353
+ >>> round(resgates_2025, 2)
354
+ 1395109062272.45
355
+ """
356
+ if tab not in _PROCESSADORES:
357
+ disponiveis = ", ".join(f'"{t}"' for t in sorted(_PROCESSADORES))
358
+ raise ValueError(
359
+ f"Aba '{tab}' não disponível. Abas implementadas: {disponiveis}."
360
+ )
361
+ processador = _PROCESSADORES[tab]
362
+ try:
363
+ url_anexo = _buscar_url_anexo()
364
+ registro.debug(f"URL do anexo RMD: {url_anexo}")
365
+ conteudo_zip = _baixar_zip(url_anexo)
366
+ conteudo_excel = _extrair_excel(conteudo_zip)
367
+ df = processador(conteudo_excel) # type: ignore[operator]
368
+ except Exception as e:
369
+ registro.exception(f"Erro ao coletar dados do RMD (aba {tab!r}): {e}")
370
+ return pl.DataFrame()
371
+
372
+ registro.info(f"Dados do RMD (aba {tab!r}) processados. Shape: {df.shape}.")
373
+ return df
@@ -15,10 +15,11 @@ classifiers = [
15
15
  ]
16
16
  dependencies = [
17
17
  "polars>=1.33.0",
18
- "lxml>=6.0.0",
18
+ "lxml>=6.0.0",
19
19
  "python-dateutil>=2.8.0",
20
20
  "requests>=2.31.0",
21
21
  "tenacity>=9.0.0",
22
+ "fastexcel>=0.19.0",
22
23
  ]
23
24
  dynamic = ["version"]
24
25
 
@@ -63,4 +64,20 @@ testpaths = ["tests", "pyield"]
63
64
  addopts = "--doctest-modules"
64
65
  python_files = "test_*.py"
65
66
  python_classes = "Test*"
66
- python_functions = "test_*"
67
+ python_functions = "test_*"
68
+
69
+ [tool.ruff]
70
+ line-length = 88
71
+ indent-width = 4
72
+ target-version = "py312"
73
+ extend-include = ["*.ipynb"]
74
+
75
+ [tool.ruff.lint]
76
+ preview = true
77
+ select = ["I", "F", "E", "W", "PL", "PT"]
78
+
79
+ [tool.ruff.lint.pycodestyle]
80
+ max-doc-length = 120
81
+
82
+ [tool.ruff.format]
83
+ docstring-code-format = true
@@ -1 +0,0 @@
1
- __version__ = "0.43.3"
File without changes
File without changes
@@ -7,8 +7,8 @@ from typing import Literal
7
7
  import polars as pl
8
8
  import requests
9
9
 
10
- from pyield.clock import now
11
10
  from pyield._internal.retry import retry_padrao
11
+ from pyield.clock import now
12
12
 
13
13
  URL_BASE = "https://github.com/crdcj/pyield-data/releases/latest/download"
14
14
  registro = logging.getLogger(__name__)
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