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.
- {pyield-0.43.3 → pyield-0.43.4}/PKG-INFO +3 -1
- {pyield-0.43.3 → pyield-0.43.4}/README.md +1 -0
- pyield-0.43.4/pyield/__about__.py +1 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/__init__.py +2 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/_internal/__init__.py +0 -1
- pyield-0.43.4/pyield/rmd.py +373 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyproject.toml +19 -2
- pyield-0.43.3/pyield/__about__.py +0 -1
- {pyield-0.43.3 → pyield-0.43.4}/.gitignore +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/LICENSE +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/_internal/converters.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/_internal/data_cache.py +1 -1
- {pyield-0.43.3 → pyield-0.43.4}/pyield/_internal/retry.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/_internal/types.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/__init__.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/difusao.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/ettj_intraday.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/ettj_last.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/ima.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/imaq.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/anbima/tpf.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/__init__.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/common.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/di1.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/di_over.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/__init__.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/core.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/historical/__init__.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/historical/core.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/historical/historical_b3.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/historical/historical_bmf.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/futures/intraday.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/b3/price_report.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/__init__.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/auction.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/ptax_api.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/rates.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/repo.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/trades_intraday.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/trades_monthly.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bc/vna.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bday/__init__.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bday/core.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bday/holidays/br_holidays_new.txt +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bday/holidays/br_holidays_old.txt +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/bday/holidays/brholidays.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/clock.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/fwd.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/interpolator.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/ipca/__init__.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/ipca/historical.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/ipca/projected.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/py.typed +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/__init__.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/auctions.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/benchmark.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/lft.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ltn.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ntnb.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ntnb1.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ntnbprinc.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ntnc.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/ntnf.py +0 -0
- {pyield-0.43.3 → pyield-0.43.4}/pyield/tn/pre.py +0 -0
- {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
|
+
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
|
|
|
@@ -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
|
|
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
|
|
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
|