pyield 0.49.4__tar.gz → 0.49.5__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.49.4 → pyield-0.49.5}/PKG-INFO +2 -1
- {pyield-0.49.4 → pyield-0.49.5}/README.md +1 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/b3/di_over.py +29 -26
- pyield-0.49.5/pyield/tpf/rmd/__init__.py +81 -0
- pyield-0.49.5/pyield/tpf/rmd/_aba_1_3.py +107 -0
- pyield-0.49.5/pyield/tpf/rmd/_aba_2_1.py +97 -0
- pyield-0.49.5/pyield/tpf/rmd/_common.py +41 -0
- pyield-0.49.5/pyield/tpf/rmd/_download.py +55 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyproject.toml +1 -1
- pyield-0.49.4/pyield/tpf/rmd.py +0 -262
- {pyield-0.49.4 → pyield-0.49.5}/LICENSE +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/__init__.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/_internal/__init__.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/_internal/br_numbers.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/_internal/cache.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/_internal/converters.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/_internal/data_cache.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/_internal/retry.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/_internal/types.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/anbima/__init__.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/anbima/imaq.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/anbima/mercado_secundario.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/b3/__init__.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/b3/_contratos.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/b3/_validar_pregao.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/b3/boletim.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/b3/derivativos_intradia.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/bc/__init__.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/bc/_olinda.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/bc/leiloes.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/bc/sgs.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/bc/tpf_intradia.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/bc/tpf_mensal.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/bc/vna.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/di1.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/du/__init__.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/du/core.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/du/feriados/__init__.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/du/feriados/feriados_antigos_br.txt +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/du/feriados/feriados_br.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/du/feriados/feriados_novos_br.txt +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/futuro/__init__.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/futuro/contratos.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/futuro/historico.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/futuro/intradia.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/fwd.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/interpolador.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/ipca/__init__.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/ipca/historico.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/ipca/projetado.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/lft.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/ltn.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/ntnb.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/ntnb1.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/ntnbprinc.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/ntnc.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/ntnf.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/py.typed +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/relogio.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/selic/__init__.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/selic/compromissada.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/selic/copom.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/selic/cpm.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/selic/probabilities.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/tpf/__init__.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/tpf/benchmark.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/tpf/leiloes.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/tpf/pre.py +0 -0
- {pyield-0.49.4 → pyield-0.49.5}/pyield/tpf/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pyield
|
|
3
|
-
Version: 0.49.
|
|
3
|
+
Version: 0.49.5
|
|
4
4
|
Summary: A Python library for analysis of fixed income instruments in Brazil
|
|
5
5
|
Keywords: fixed-income,brazil,finance,analysis,bonds
|
|
6
6
|
Author: Carlos Carvalho
|
|
@@ -46,6 +46,7 @@ Description-Content-Type: text/markdown
|
|
|
46
46
|
[](https://python.org "Go to Python homepage")
|
|
47
47
|
[](https://github.com/crdcj/PYield/blob/main/LICENSE)
|
|
48
48
|
[](https://crdcj.github.io/PYield/)
|
|
49
|
+
[](https://colab.research.google.com/github/crdcj/PYield/blob/main/examples/pyield_quickstart.ipynb)
|
|
49
50
|
|
|
50
51
|
# PYield: Toolkit de Renda Fixa Brasileira
|
|
51
52
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
[](https://python.org "Go to Python homepage")
|
|
3
3
|
[](https://github.com/crdcj/PYield/blob/main/LICENSE)
|
|
4
4
|
[](https://crdcj.github.io/PYield/)
|
|
5
|
+
[](https://colab.research.google.com/github/crdcj/PYield/blob/main/examples/pyield_quickstart.ipynb)
|
|
5
6
|
|
|
6
7
|
# PYield: Toolkit de Renda Fixa Brasileira
|
|
7
8
|
|
|
@@ -13,9 +13,12 @@ Notas de implementação:
|
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
import datetime as dt
|
|
16
|
-
import ftplib
|
|
17
16
|
import logging
|
|
17
|
+
import time
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
18
20
|
|
|
21
|
+
from pyield import du
|
|
19
22
|
from pyield._internal.cache import ttl_cache
|
|
20
23
|
from pyield._internal.converters import converter_datas
|
|
21
24
|
from pyield._internal.types import DateLike, any_is_empty
|
|
@@ -28,36 +31,36 @@ DATA_INICIO = dt.date(2012, 8, 20)
|
|
|
28
31
|
# 4 casas decimais na taxa = 2 casas decimais em percentual
|
|
29
32
|
CASAS_DECIMAIS_DI_OVER = 4
|
|
30
33
|
|
|
34
|
+
_URL_BASE = "ftp://ftp.cetip.com.br/MediaCDI/"
|
|
35
|
+
_MAX_TENTATIVAS = 3
|
|
36
|
+
_ESPERA = 2.0 # segundos entre tentativas (erro 421 é transitório)
|
|
37
|
+
|
|
31
38
|
|
|
32
39
|
@ttl_cache()
|
|
33
40
|
def _buscar_taxa(nome_arquivo: str) -> float:
|
|
34
41
|
"""Busca a taxa DI no FTP da CETIP para o arquivo informado."""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
linhas = []
|
|
41
|
-
try:
|
|
42
|
-
ftp.retrlines(f"RETR {nome_arquivo}", linhas.append)
|
|
43
|
-
except ftplib.error_perm as e:
|
|
44
|
-
# Código 550 = arquivo não encontrado (feriado/fim de semana)
|
|
45
|
-
if str(e).startswith("550"):
|
|
46
|
-
return float("nan")
|
|
47
|
-
raise
|
|
48
|
-
|
|
49
|
-
if not linhas:
|
|
50
|
-
registro.error("Arquivo %s está vazio.", nome_arquivo)
|
|
51
|
-
return float("nan")
|
|
52
|
-
|
|
53
|
-
# Formato usual: "00001315" -> 13.15% -> 0.1315
|
|
54
|
-
taxa_bruta = linhas[0].strip()
|
|
55
|
-
taxa = int(taxa_bruta) / 10**CASAS_DECIMAIS_DI_OVER
|
|
42
|
+
for tentativa in range(1, _MAX_TENTATIVAS + 1):
|
|
43
|
+
try:
|
|
44
|
+
with urllib.request.urlopen(_URL_BASE + nome_arquivo, timeout=10) as r:
|
|
45
|
+
conteudo = r.read().decode().strip()
|
|
46
|
+
taxa = int(conteudo) / 10**CASAS_DECIMAIS_DI_OVER
|
|
56
47
|
return round(taxa, CASAS_DECIMAIS_DI_OVER)
|
|
48
|
+
except urllib.error.URLError as e:
|
|
49
|
+
motivo = str(e.reason)
|
|
50
|
+
# Código 550 = arquivo não encontrado (feriado/fim de semana)
|
|
51
|
+
if "550" in motivo:
|
|
52
|
+
return float("nan")
|
|
53
|
+
# Código 421 = muitas conexões simultâneas; erro transitório
|
|
54
|
+
if "421" in motivo and tentativa < _MAX_TENTATIVAS:
|
|
55
|
+
registro.warning(
|
|
56
|
+
"Erro FTP transitório (tentativa %s): %s", tentativa, e.reason
|
|
57
|
+
)
|
|
58
|
+
time.sleep(_ESPERA)
|
|
59
|
+
continue
|
|
60
|
+
raise ConnectionError(f"Falha ao buscar taxa DI via FTP: {e.reason}") from e
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
raise ConnectionError(f"Falha ao buscar taxa DI via FTP: {e}") from e
|
|
62
|
+
msg = "Fluxo de retry inválido."
|
|
63
|
+
raise RuntimeError(msg)
|
|
61
64
|
|
|
62
65
|
|
|
63
66
|
def di_over(data: DateLike) -> float:
|
|
@@ -85,7 +88,7 @@ def di_over(data: DateLike) -> float:
|
|
|
85
88
|
return float("nan")
|
|
86
89
|
|
|
87
90
|
data_ref = converter_datas(data)
|
|
88
|
-
if data_ref < DATA_INICIO:
|
|
91
|
+
if data_ref < DATA_INICIO or not du.eh_dia_util(data_ref):
|
|
89
92
|
return float("nan")
|
|
90
93
|
|
|
91
94
|
return _buscar_taxa(data_ref.strftime("%Y%m%d.txt"))
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Relatório Mensal da Dívida (RMD) do Tesouro Nacional."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
from . import _aba_1_3, _aba_2_1
|
|
8
|
+
from ._download import baixar_planilha_rmd as _carregar_planilha_rmd
|
|
9
|
+
|
|
10
|
+
registro = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
_IMPLEMENTACOES = {
|
|
13
|
+
"1.3": _aba_1_3.estruturar_dados,
|
|
14
|
+
"2.1": _aba_2_1.estruturar_dados,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def rmd(aba: str) -> pl.DataFrame:
|
|
19
|
+
"""Retorna dados do Relatório Mensal da Dívida (RMD) do Tesouro Nacional.
|
|
20
|
+
|
|
21
|
+
Baixa e processa a planilha do RMD, extraindo dados da aba solicitada. A
|
|
22
|
+
publicação mais recente é descoberta automaticamente via parse HTML da página
|
|
23
|
+
oficial do Tesouro Transparente.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
aba: Número da aba a processar. Abas implementadas: ``"1.3"`` e ``"2.1"``.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
DataFrame Polars no schema específico da aba solicitada. Em caso de erro,
|
|
30
|
+
retorna DataFrame vazio e registra o erro em log.
|
|
31
|
+
|
|
32
|
+
Output Columns:
|
|
33
|
+
Aba ``"1.3"``:
|
|
34
|
+
* periodo (Date): primeiro dia do mês de referência.
|
|
35
|
+
* grupo (String): seção principal — ``"Emissões"`` ou ``"Resgates"``.
|
|
36
|
+
* subgrupo (String): categoria dentro do grupo.
|
|
37
|
+
* titulo (String): tipo de título ou ``null`` para subgrupos sem
|
|
38
|
+
detalhamento por título.
|
|
39
|
+
* valor (Float64): valor em R$.
|
|
40
|
+
Aba ``"2.1"``:
|
|
41
|
+
* periodo (Date): primeiro dia do mês de referência.
|
|
42
|
+
* detentor (String): quem detém o estoque — ``"Público"`` ou
|
|
43
|
+
``"Banco Central"``.
|
|
44
|
+
* tipo (String): classificação da dívida — ``"DPMFi"`` (interna) ou
|
|
45
|
+
``"DPFe"`` (externa).
|
|
46
|
+
* categoria (String): subdivisão dentro do tipo, quando houver —
|
|
47
|
+
``"Tesouro Nacional"``, ``"Banco Central"`` (emitente dentro da
|
|
48
|
+
DPMFi pública), ``"Mobiliária"``, ``"Contratual"``; ``null``
|
|
49
|
+
quando não há subdivisão (ex.: DPMFi em poder do Banco Central).
|
|
50
|
+
* titulo (String): título ou instrumento de dívida.
|
|
51
|
+
* valor (Float64): valor em R$. Somente registros folha; subtotais
|
|
52
|
+
devem ser calculados pelo usuário via agregação.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: Se ``aba`` não estiver entre as abas implementadas.
|
|
56
|
+
|
|
57
|
+
Notes:
|
|
58
|
+
- A publicação mais recente é descoberta automaticamente via parse HTML
|
|
59
|
+
do Tesouro Transparente.
|
|
60
|
+
- A aba ``"1.3"`` traz emissões e resgates da DPMFi.
|
|
61
|
+
- A aba ``"2.1"`` traz a série histórica de estoque da DPF.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
>>> df = yd.tpf.rmd(aba="1.3") # doctest: +SKIP
|
|
65
|
+
>>> df = yd.tpf.rmd(aba="2.1") # doctest: +SKIP
|
|
66
|
+
"""
|
|
67
|
+
if aba not in _IMPLEMENTACOES:
|
|
68
|
+
disponiveis = ", ".join(f'"{t}"' for t in sorted(_IMPLEMENTACOES))
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Aba '{aba}' não disponível. Abas implementadas: {disponiveis}."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
conteudo_excel = _carregar_planilha_rmd()
|
|
75
|
+
df = _IMPLEMENTACOES[aba](conteudo_excel)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
registro.exception(f"Erro ao coletar dados do RMD (aba {aba!r}): {e}")
|
|
78
|
+
return pl.DataFrame()
|
|
79
|
+
|
|
80
|
+
registro.info(f"Dados do RMD (aba {aba!r}) processados. Shape: {df.shape}.")
|
|
81
|
+
return df
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Parser da aba 1.3 do RMD."""
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
from ._common import parsear_periodo
|
|
8
|
+
|
|
9
|
+
_LINHA_PERIODOS = 2
|
|
10
|
+
_LINHA_INICIO_DADOS = _LINHA_PERIODOS + 1
|
|
11
|
+
|
|
12
|
+
_TITULOS = ("LFT", "LTN", "NTN-B", "NTN-B1", "NTN-F", "NTN-C", "NTN-D", "Demais")
|
|
13
|
+
_SECOES = {"I - EMISSÕES": "Emissões", "II - RESGATES": "Resgates"}
|
|
14
|
+
_SUBGRUPOS = {"Vendas", "Trocas", "Vencimentos", "Compras"}
|
|
15
|
+
_SUBGRUPO_TD = "Tesouro Direto"
|
|
16
|
+
_SUBGRUPOS_DIRETOS = (
|
|
17
|
+
"Transferência de Carteira",
|
|
18
|
+
"Emissão Direta com Financeiro",
|
|
19
|
+
"Emissão Direta sem Financeiro",
|
|
20
|
+
"Pagamento de Dividendos",
|
|
21
|
+
"Cancelamentos",
|
|
22
|
+
)
|
|
23
|
+
_PREFIXOS_IGNORAR = ("IMPACTO", "OPERAÇÕES", "III -", "RESGATE")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _classificar_categorias(
|
|
27
|
+
categorias: list[str],
|
|
28
|
+
) -> list[tuple[int, str, str, str | None]]:
|
|
29
|
+
"""Percorre rótulos de categoria e classifica linhas de dados."""
|
|
30
|
+
grupo = ""
|
|
31
|
+
subgrupo = ""
|
|
32
|
+
eventos: list[tuple[int, str, str, str | None]] = []
|
|
33
|
+
|
|
34
|
+
for i, cat in enumerate(categorias):
|
|
35
|
+
c = cat.strip()
|
|
36
|
+
if c in _SECOES:
|
|
37
|
+
grupo, subgrupo = _SECOES[c], ""
|
|
38
|
+
elif any(c.startswith(p) for p in _PREFIXOS_IGNORAR):
|
|
39
|
+
grupo = ""
|
|
40
|
+
elif grupo:
|
|
41
|
+
if c in _SUBGRUPOS:
|
|
42
|
+
subgrupo = c
|
|
43
|
+
elif c.startswith(_SUBGRUPO_TD):
|
|
44
|
+
subgrupo = _SUBGRUPO_TD
|
|
45
|
+
elif c in _TITULOS:
|
|
46
|
+
eventos.append((i, grupo, subgrupo, c))
|
|
47
|
+
else:
|
|
48
|
+
prefixo = next((p for p in _SUBGRUPOS_DIRETOS if c.startswith(p)), None)
|
|
49
|
+
if prefixo:
|
|
50
|
+
eventos.append((i, grupo, prefixo, None))
|
|
51
|
+
|
|
52
|
+
return eventos
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _montar_registros(
|
|
56
|
+
eventos: list[tuple[int, str, str, str | None]],
|
|
57
|
+
datas_mensais: list[dt.date],
|
|
58
|
+
matriz: pl.DataFrame,
|
|
59
|
+
) -> pl.DataFrame:
|
|
60
|
+
"""Monta DataFrame longo com todos os registros de emissões e resgates."""
|
|
61
|
+
linhas = [
|
|
62
|
+
(data, grupo, subgrupo, titulo, valor)
|
|
63
|
+
for idx, grupo, subgrupo, titulo in eventos
|
|
64
|
+
for data, valor in zip(datas_mensais, matriz.row(idx))
|
|
65
|
+
]
|
|
66
|
+
return pl.DataFrame(
|
|
67
|
+
linhas,
|
|
68
|
+
schema={
|
|
69
|
+
"periodo": pl.Date,
|
|
70
|
+
"grupo": pl.String,
|
|
71
|
+
"subgrupo": pl.String,
|
|
72
|
+
"titulo": pl.String,
|
|
73
|
+
"valor": pl.Float64,
|
|
74
|
+
},
|
|
75
|
+
orient="row",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def estruturar_dados(conteudo_excel: bytes) -> pl.DataFrame:
|
|
80
|
+
"""Lê a aba ``1.3`` do Excel e retorna DataFrame longo."""
|
|
81
|
+
df_bruto = pl.read_excel(
|
|
82
|
+
conteudo_excel,
|
|
83
|
+
sheet_name="1.3",
|
|
84
|
+
has_header=False,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
periodos_raw = [str(p) for p in df_bruto.row(_LINHA_PERIODOS)[1:] if p is not None]
|
|
88
|
+
|
|
89
|
+
datas_e_indices = [
|
|
90
|
+
(i, data)
|
|
91
|
+
for i, periodo in enumerate(periodos_raw)
|
|
92
|
+
if (data := parsear_periodo(periodo)) is not None
|
|
93
|
+
]
|
|
94
|
+
indices_mensais = [i for i, _ in datas_e_indices]
|
|
95
|
+
datas_mensais = [data for _, data in datas_e_indices]
|
|
96
|
+
|
|
97
|
+
df_dados = df_bruto[_LINHA_INICIO_DADOS:]
|
|
98
|
+
df_dados = df_dados.filter(df_dados[:, 0].is_not_null())
|
|
99
|
+
|
|
100
|
+
eventos = _classificar_categorias([str(c) for c in df_dados[:, 0].to_list()])
|
|
101
|
+
matriz = df_dados[:, 1:].cast(pl.Float64, strict=False)[:, indices_mensais]
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
_montar_registros(eventos, datas_mensais, matriz)
|
|
105
|
+
.with_columns(valor=pl.col("valor").mul(1_000_000).round(2))
|
|
106
|
+
.filter(pl.col("valor").is_not_null() & (pl.col("valor") != 0))
|
|
107
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Parser da aba 2.1 do RMD."""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
from ._common import limpar_rotulo, parsear_periodo
|
|
8
|
+
|
|
9
|
+
_LINHA_PERIODOS = 2
|
|
10
|
+
_LINHA_INICIO_DADOS = _LINHA_PERIODOS + 1
|
|
11
|
+
|
|
12
|
+
# Rótulos (uppercase) que definem transições de estado hierárquico.
|
|
13
|
+
# Valor: (detentor, tipo, categoria, pode_emitir)
|
|
14
|
+
# pode_emitir=False indica estado intermediário; linhas folha são ignoradas até
|
|
15
|
+
# a próxima transição com pode_emitir=True.
|
|
16
|
+
_TRANSICOES: dict[str, tuple[str | None, str | None, str | None, bool]] = {
|
|
17
|
+
"DPF EM PODER DO PÚBLICO": (None, None, None, False),
|
|
18
|
+
"DPMFI": ("Público", "DPMFi", None, False),
|
|
19
|
+
"TESOURO NACIONAL": ("Público", "DPMFi", "Tesouro Nacional", True),
|
|
20
|
+
"BANCO CENTRAL": ("Público", "DPMFi", "Banco Central", True),
|
|
21
|
+
"DPFE": ("Público", "DPFe", None, False),
|
|
22
|
+
"DÍVIDA MOBILIÁRIA": ("Público", "DPFe", "Mobiliária", True),
|
|
23
|
+
"DÍVIDA CONTRATUAL": ("Público", "DPFe", "Contratual", True),
|
|
24
|
+
"DPMFI EM PODER DO BANCO CENTRAL": ("Banco Central", "DPMFi", None, True),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _obter_periodos_mensais(
|
|
29
|
+
df_bruto: pl.DataFrame,
|
|
30
|
+
) -> list[tuple[int, date]]:
|
|
31
|
+
"""Extrai os pares (índice_coluna, data) dos períodos mensais válidos."""
|
|
32
|
+
periodos_raw = [str(p) for p in df_bruto.row(_LINHA_PERIODOS)[1:] if p is not None]
|
|
33
|
+
return [
|
|
34
|
+
(i, data)
|
|
35
|
+
for i, periodo in enumerate(periodos_raw)
|
|
36
|
+
if (data := parsear_periodo(periodo)) is not None
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _montar_registros(df_bruto: pl.DataFrame) -> list[tuple[object, ...]]:
|
|
41
|
+
"""Converte o bloco hierárquico da aba em registros longos (somente folhas)."""
|
|
42
|
+
periodos = _obter_periodos_mensais(df_bruto)
|
|
43
|
+
linhas = df_bruto[_LINHA_INICIO_DADOS:]
|
|
44
|
+
detentor: str | None = None
|
|
45
|
+
tipo: str | None = None
|
|
46
|
+
categoria: str | None = None
|
|
47
|
+
pode_emitir: bool = False
|
|
48
|
+
registros: list[tuple[object, ...]] = []
|
|
49
|
+
|
|
50
|
+
for linha in linhas.iter_rows():
|
|
51
|
+
bruto = linha[0]
|
|
52
|
+
if bruto is None:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
rotulo = limpar_rotulo(bruto)
|
|
56
|
+
if not rotulo:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
transicao = _TRANSICOES.get(rotulo.upper())
|
|
60
|
+
if transicao is not None:
|
|
61
|
+
detentor, tipo, categoria, pode_emitir = transicao
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if not pode_emitir:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
valores = linha[1:]
|
|
68
|
+
for indice, data in periodos:
|
|
69
|
+
registros.append((data, detentor, tipo, categoria, rotulo, valores[indice]))
|
|
70
|
+
|
|
71
|
+
return registros
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def estruturar_dados(conteudo_excel: bytes) -> pl.DataFrame:
|
|
75
|
+
"""Lê a aba ``2.1`` do Excel e retorna DataFrame longo."""
|
|
76
|
+
df_bruto = pl.read_excel(
|
|
77
|
+
conteudo_excel,
|
|
78
|
+
sheet_name="2.1",
|
|
79
|
+
has_header=False,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
pl.DataFrame(
|
|
84
|
+
_montar_registros(df_bruto),
|
|
85
|
+
schema={
|
|
86
|
+
"periodo": pl.Date,
|
|
87
|
+
"detentor": pl.String,
|
|
88
|
+
"tipo": pl.String,
|
|
89
|
+
"categoria": pl.String,
|
|
90
|
+
"titulo": pl.String,
|
|
91
|
+
"valor": pl.Float64,
|
|
92
|
+
},
|
|
93
|
+
orient="row",
|
|
94
|
+
)
|
|
95
|
+
.with_columns(valor=pl.col("valor").mul(1_000_000_000).round(2))
|
|
96
|
+
.filter(pl.col("valor").is_not_null())
|
|
97
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Helpers compartilhados pelos parsers do RMD."""
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
_MESES_PT = {
|
|
7
|
+
"Jan": 1,
|
|
8
|
+
"Fev": 2,
|
|
9
|
+
"Mar": 3,
|
|
10
|
+
"Abr": 4,
|
|
11
|
+
"Mai": 5,
|
|
12
|
+
"Jun": 6,
|
|
13
|
+
"Jul": 7,
|
|
14
|
+
"Ago": 8,
|
|
15
|
+
"Set": 9,
|
|
16
|
+
"Out": 10,
|
|
17
|
+
"Nov": 11,
|
|
18
|
+
"Dez": 12,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_PADRAO_ESPACOS = re.compile(r"\s+")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parsear_periodo(periodo: str) -> dt.date | None:
|
|
25
|
+
"""Converte string de período para ``datetime.date`` ou ``None``."""
|
|
26
|
+
try:
|
|
27
|
+
mes_str, ano_str = periodo.split("/")
|
|
28
|
+
except ValueError:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
mes = _MESES_PT.get(mes_str)
|
|
32
|
+
if mes is None:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
return dt.date(2000 + int(ano_str), mes, 1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def limpar_rotulo(valor: object) -> str:
|
|
39
|
+
"""Remove espaços e notas de rodapé do rótulo lido do Excel."""
|
|
40
|
+
texto = str(valor).replace("¹", "").replace("²", "").strip()
|
|
41
|
+
return _PADRAO_ESPACOS.sub(" ", texto)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Download e extração da planilha do RMD."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import zipfile as zf
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from lxml import html
|
|
8
|
+
|
|
9
|
+
from pyield._internal.cache import ttl_cache
|
|
10
|
+
from pyield._internal.retry import retry_padrao
|
|
11
|
+
|
|
12
|
+
URL_BASE = (
|
|
13
|
+
"https://www.tesourotransparente.gov.br/publicacoes/relatorio-mensal-da-divida-rmd"
|
|
14
|
+
)
|
|
15
|
+
_TIMEOUT_SEGUNDOS = 60
|
|
16
|
+
_TTL_UM_DIA = 86_400 # segundos
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@retry_padrao
|
|
20
|
+
def _buscar_conteudo(url: str) -> bytes:
|
|
21
|
+
"""Busca o conteúdo de uma URL, seguindo redirects, com retry."""
|
|
22
|
+
resposta = requests.get(url, timeout=_TIMEOUT_SEGUNDOS)
|
|
23
|
+
resposta.raise_for_status()
|
|
24
|
+
return resposta.content
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _buscar_url_anexo() -> str:
|
|
28
|
+
"""Encontra a URL do arquivo ZIP do anexo mais recente do RMD."""
|
|
29
|
+
conteudo_pagina = _buscar_conteudo(URL_BASE)
|
|
30
|
+
arvore = html.fromstring(conteudo_pagina)
|
|
31
|
+
resultado = arvore.xpath("//a[contains(@href, 'publicacao-anexo')]/@href")
|
|
32
|
+
if not isinstance(resultado, list) or not resultado:
|
|
33
|
+
raise ValueError("Link do anexo ZIP não encontrado na página do RMD.")
|
|
34
|
+
return str(resultado[0])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _extrair_excel(conteudo_zip: bytes) -> bytes:
|
|
38
|
+
"""Extrai o arquivo Excel do ZIP."""
|
|
39
|
+
with zf.ZipFile(io.BytesIO(conteudo_zip), "r") as arquivo_zip:
|
|
40
|
+
nomes_excel = [
|
|
41
|
+
nome
|
|
42
|
+
for nome in arquivo_zip.namelist()
|
|
43
|
+
if nome.lower().endswith((".xlsx", ".xls"))
|
|
44
|
+
]
|
|
45
|
+
if not nomes_excel:
|
|
46
|
+
raise ValueError("Nenhum arquivo Excel encontrado no ZIP do RMD.")
|
|
47
|
+
return arquivo_zip.read(nomes_excel[0])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@ttl_cache(ttl=_TTL_UM_DIA)
|
|
51
|
+
def baixar_planilha_rmd() -> bytes:
|
|
52
|
+
"""Baixa e extrai a planilha Excel do anexo mais recente do RMD."""
|
|
53
|
+
url_anexo = _buscar_url_anexo()
|
|
54
|
+
conteudo_zip = _buscar_conteudo(url_anexo)
|
|
55
|
+
return _extrair_excel(conteudo_zip)
|
pyield-0.49.4/pyield/tpf/rmd.py
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
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
|
-
_TIMEOUT_SEGUNDOS = 60
|
|
21
|
-
|
|
22
|
-
# Índices de linha (0-based) na planilha após leitura com fastexcel (sem cabeçalho)
|
|
23
|
-
# O fastexcel compacta linhas totalmente vazias, resultando em 81 linhas ao invés das
|
|
24
|
-
# 101 do Excel bruto. Os índices abaixo refletem o layout observado no arquivo atual.
|
|
25
|
-
_LINHA_PERIODOS = 2 # Rótulos de período: "Nov/06", "Dez/06", ..., "2025"
|
|
26
|
-
_LINHA_INICIO_DADOS = 3 # Primeira linha de dados: "I - EMISSÕES"
|
|
27
|
-
_LINHA_FIM_DADOS = 67 # Exclusivo: notas de rodapé a partir desta linha
|
|
28
|
-
|
|
29
|
-
# Tipos de título que viram colunas (em ordem)
|
|
30
|
-
_TITULOS = ("LFT", "LTN", "NTN-B", "NTN-B1", "NTN-F", "NTN-C", "NTN-D", "Demais")
|
|
31
|
-
|
|
32
|
-
# Mapeamento de rótulo de seção → nome limpo
|
|
33
|
-
_SECOES = {"I - EMISSÕES": "Emissões", "II - RESGATES": "Resgates"}
|
|
34
|
-
|
|
35
|
-
# Rótulos de subgrupo conhecidos e prefixo do Tesouro Direto
|
|
36
|
-
_SUBGRUPOS = {"Vendas", "Trocas", "Vencimentos", "Compras"}
|
|
37
|
-
_SUBGRUPO_TD = "Tesouro Direto"
|
|
38
|
-
|
|
39
|
-
# Subgrupos sem detalhamento por tipo de título (valor direto na linha)
|
|
40
|
-
# Tuple para ordem determinística; correspondência por prefixo (ignora notas de rodapé)
|
|
41
|
-
_SUBGRUPOS_DIRETOS = (
|
|
42
|
-
"Transferência de Carteira",
|
|
43
|
-
"Emissão Direta com Financeiro",
|
|
44
|
-
"Emissão Direta sem Financeiro",
|
|
45
|
-
"Pagamento de Dividendos",
|
|
46
|
-
"Cancelamentos",
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
# Prefixos que sinalizam fim da área de interesse (seções a ignorar)
|
|
50
|
-
_PREFIXOS_IGNORAR = ("IMPACTO", "OPERAÇÕES", "III -", "RESGATE")
|
|
51
|
-
|
|
52
|
-
_MESES_PT = {
|
|
53
|
-
"Jan": 1,
|
|
54
|
-
"Fev": 2,
|
|
55
|
-
"Mar": 3,
|
|
56
|
-
"Abr": 4,
|
|
57
|
-
"Mai": 5,
|
|
58
|
-
"Jun": 6,
|
|
59
|
-
"Jul": 7,
|
|
60
|
-
"Ago": 8,
|
|
61
|
-
"Set": 9,
|
|
62
|
-
"Out": 10,
|
|
63
|
-
"Nov": 11,
|
|
64
|
-
"Dez": 12,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _parsear_periodo(periodo: str) -> datetime.date | None:
|
|
69
|
-
"""Converte string de período para datetime.date ou None para totais anuais."""
|
|
70
|
-
try:
|
|
71
|
-
mes_str, ano_str = periodo.split("/")
|
|
72
|
-
except ValueError:
|
|
73
|
-
return None # ex: "2025" (total anual) → descartado
|
|
74
|
-
mes = _MESES_PT.get(mes_str)
|
|
75
|
-
if mes is None:
|
|
76
|
-
return None
|
|
77
|
-
ano = 2000 + int(ano_str)
|
|
78
|
-
return datetime.date(ano, mes, 1)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@retry_padrao
|
|
82
|
-
def _buscar_conteudo(url: str) -> bytes:
|
|
83
|
-
"""Busca o conteúdo de uma URL, seguindo redirects, com retry."""
|
|
84
|
-
resposta = requests.get(url, timeout=_TIMEOUT_SEGUNDOS)
|
|
85
|
-
resposta.raise_for_status()
|
|
86
|
-
return resposta.content
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _buscar_url_anexo() -> str:
|
|
90
|
-
"""Encontra a URL do arquivo ZIP do anexo mais recente do RMD.
|
|
91
|
-
|
|
92
|
-
A URL base redireciona automaticamente para a página do mês atual.
|
|
93
|
-
O lxml localiza o link do anexo ZIP nessa página.
|
|
94
|
-
"""
|
|
95
|
-
conteudo_pagina = _buscar_conteudo(URL_BASE)
|
|
96
|
-
arvore = html.fromstring(conteudo_pagina)
|
|
97
|
-
resultado = arvore.xpath("//a[contains(@href, 'publicacao-anexo')]/@href")
|
|
98
|
-
if not isinstance(resultado, list) or not resultado:
|
|
99
|
-
raise ValueError("Link do anexo ZIP não encontrado na página do RMD.")
|
|
100
|
-
return str(resultado[0])
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _extrair_excel(conteudo_zip: bytes) -> bytes:
|
|
104
|
-
"""Extrai o arquivo Excel do ZIP."""
|
|
105
|
-
with zf.ZipFile(io.BytesIO(conteudo_zip), "r") as arquivo_zip:
|
|
106
|
-
nomes_excel = [
|
|
107
|
-
n for n in arquivo_zip.namelist() if n.lower().endswith((".xlsx", ".xls"))
|
|
108
|
-
]
|
|
109
|
-
if not nomes_excel:
|
|
110
|
-
raise ValueError("Nenhum arquivo Excel encontrado no ZIP do RMD.")
|
|
111
|
-
return arquivo_zip.read(nomes_excel[0])
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _classificar_categorias(
|
|
115
|
-
categorias: list[str],
|
|
116
|
-
) -> list[tuple[int, str, str, str | None]]:
|
|
117
|
-
"""Percorre rótulos de categoria e classifica linhas de dados.
|
|
118
|
-
|
|
119
|
-
Máquina de estados que rastreia grupo (Emissões/Resgates) e subgrupo.
|
|
120
|
-
Retorna lista de eventos (idx, grupo, subgrupo, titulo). Para subgrupos
|
|
121
|
-
sem detalhamento por título, titulo é None.
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
categorias: Lista de rótulos de categoria lidos da coluna 0 do Excel.
|
|
125
|
-
|
|
126
|
-
Returns:
|
|
127
|
-
Lista de eventos (idx, grupo, subgrupo, titulo) detectados.
|
|
128
|
-
"""
|
|
129
|
-
grupo = ""
|
|
130
|
-
subgrupo = ""
|
|
131
|
-
eventos: list[tuple[int, str, str, str | None]] = []
|
|
132
|
-
for i, cat in enumerate(categorias):
|
|
133
|
-
c = cat.strip()
|
|
134
|
-
if c in _SECOES:
|
|
135
|
-
grupo, subgrupo = _SECOES[c], ""
|
|
136
|
-
elif any(c.startswith(p) for p in _PREFIXOS_IGNORAR):
|
|
137
|
-
grupo = ""
|
|
138
|
-
elif grupo:
|
|
139
|
-
if c in _SUBGRUPOS:
|
|
140
|
-
subgrupo = c
|
|
141
|
-
elif c.startswith(_SUBGRUPO_TD):
|
|
142
|
-
subgrupo = _SUBGRUPO_TD
|
|
143
|
-
elif c in _TITULOS:
|
|
144
|
-
eventos.append((i, grupo, subgrupo, c))
|
|
145
|
-
else:
|
|
146
|
-
prefixo = next((p for p in _SUBGRUPOS_DIRETOS if c.startswith(p)), None)
|
|
147
|
-
if prefixo:
|
|
148
|
-
eventos.append((i, grupo, prefixo, None))
|
|
149
|
-
return eventos
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def _montar_registros(
|
|
153
|
-
eventos: list[tuple[int, str, str, str | None]],
|
|
154
|
-
datas_mensais: list[datetime.date],
|
|
155
|
-
matriz: pl.DataFrame,
|
|
156
|
-
) -> pl.DataFrame:
|
|
157
|
-
"""Monta DataFrame longo com todos os registros de emissões e resgates."""
|
|
158
|
-
linhas = [
|
|
159
|
-
(data, grupo, subgrupo, titulo, val)
|
|
160
|
-
for idx, grupo, subgrupo, titulo in eventos
|
|
161
|
-
for data, val in zip(datas_mensais, matriz.row(idx))
|
|
162
|
-
]
|
|
163
|
-
return pl.DataFrame(
|
|
164
|
-
linhas,
|
|
165
|
-
schema={
|
|
166
|
-
"periodo": pl.Date,
|
|
167
|
-
"grupo": pl.String,
|
|
168
|
-
"subgrupo": pl.String,
|
|
169
|
-
"titulo": pl.String,
|
|
170
|
-
"valor": pl.Float64,
|
|
171
|
-
},
|
|
172
|
-
orient="row",
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _estruturar_dados(conteudo_excel: bytes) -> pl.DataFrame:
|
|
177
|
-
"""Lê a aba '1.3' do Excel e retorna DataFrame longo com emissões e resgates."""
|
|
178
|
-
df_bruto = pl.read_excel(
|
|
179
|
-
conteudo_excel,
|
|
180
|
-
sheet_name="1.3",
|
|
181
|
-
has_header=False,
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
periodos_raw = [str(p) for p in df_bruto.row(_LINHA_PERIODOS)[1:] if p is not None]
|
|
185
|
-
|
|
186
|
-
datas_e_indices = [
|
|
187
|
-
(i, d)
|
|
188
|
-
for i, periodo in enumerate(periodos_raw)
|
|
189
|
-
if (d := _parsear_periodo(periodo)) is not None
|
|
190
|
-
]
|
|
191
|
-
indices_mensais = [i for i, _ in datas_e_indices]
|
|
192
|
-
datas_mensais = [d for _, d in datas_e_indices]
|
|
193
|
-
|
|
194
|
-
df_dados = df_bruto[_LINHA_INICIO_DADOS:_LINHA_FIM_DADOS]
|
|
195
|
-
df_dados = df_dados.filter(df_dados[:, 0].is_not_null())
|
|
196
|
-
|
|
197
|
-
eventos = _classificar_categorias([str(c) for c in df_dados[:, 0].to_list()])
|
|
198
|
-
|
|
199
|
-
matriz = df_dados[:, 1:].cast(pl.Float64, strict=False)[:, indices_mensais]
|
|
200
|
-
|
|
201
|
-
return (
|
|
202
|
-
_montar_registros(eventos, datas_mensais, matriz)
|
|
203
|
-
.with_columns(valor=pl.col("valor").mul(1_000_000).round(2))
|
|
204
|
-
.filter(pl.col("valor").is_not_null() & (pl.col("valor") != 0))
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def rmd(aba: str) -> pl.DataFrame:
|
|
209
|
-
"""Retorna dados do Relatório Mensal da Dívida (RMD) do Tesouro Nacional.
|
|
210
|
-
|
|
211
|
-
Baixa e processa a planilha do RMD, extraindo dados de emissões e resgates
|
|
212
|
-
de Títulos Públicos Federais da Dívida Pública Mobiliária Federal interna
|
|
213
|
-
(DPMFi). A publicação mais recente é descoberta automaticamente via parse
|
|
214
|
-
HTML da página oficial.
|
|
215
|
-
|
|
216
|
-
Args:
|
|
217
|
-
aba: Número da aba a processar (ex: ``"1.3"``). Abas implementadas: ``"1.3"``.
|
|
218
|
-
|
|
219
|
-
Returns:
|
|
220
|
-
DataFrame longo com dados de emissões e resgates por período, seção,
|
|
221
|
-
subgrupo e tipo de título. Registros com valor nulo ou zero são excluídos.
|
|
222
|
-
Em caso de erro, retorna DataFrame vazio e registra log da excessão.
|
|
223
|
-
|
|
224
|
-
Output Columns:
|
|
225
|
-
* periodo (Date): primeiro dia do mês de referência.
|
|
226
|
-
* grupo (String): seção principal — ``"Emissões"`` ou ``"Resgates"``.
|
|
227
|
-
* subgrupo (String): categoria dentro do grupo.
|
|
228
|
-
* titulo (String): tipo de título (``"LFT"``, ``"LTN"``, ``"NTN-B"``,
|
|
229
|
-
``"NTN-B1"``, ``"NTN-F"``, ``"NTN-C"``, ``"NTN-D"``, ``"Demais"``,
|
|
230
|
-
ou ``null`` para subgrupos sem detalhamento por título).
|
|
231
|
-
* valor (Float64): valor em R$.
|
|
232
|
-
|
|
233
|
-
Raises:
|
|
234
|
-
ValueError: Se ``aba`` não estiver entre as abas implementadas.
|
|
235
|
-
|
|
236
|
-
Notes:
|
|
237
|
-
- A função sempre busca a publicação mais recente disponível.
|
|
238
|
-
- Totais anuais são excluídos; podem ser recalculados via group_by.
|
|
239
|
-
- Totais de referência para 2025:
|
|
240
|
-
Emissões = R$ 1.840.946.621.648,18
|
|
241
|
-
Resgates = R$ 1.395.109.062.272,45.
|
|
242
|
-
|
|
243
|
-
Examples:
|
|
244
|
-
>>> df = yd.tpf.rmd(aba="1.3") # doctest: +SKIP
|
|
245
|
-
"""
|
|
246
|
-
if aba not in _ABAS_DISPONIVEIS:
|
|
247
|
-
disponiveis = ", ".join(f'"{t}"' for t in sorted(_ABAS_DISPONIVEIS))
|
|
248
|
-
raise ValueError(
|
|
249
|
-
f"Aba '{aba}' não disponível. Abas implementadas: {disponiveis}."
|
|
250
|
-
)
|
|
251
|
-
try:
|
|
252
|
-
url_anexo = _buscar_url_anexo()
|
|
253
|
-
registro.debug(f"URL do anexo RMD: {url_anexo}")
|
|
254
|
-
conteudo_zip = _buscar_conteudo(url_anexo)
|
|
255
|
-
conteudo_excel = _extrair_excel(conteudo_zip)
|
|
256
|
-
df = _estruturar_dados(conteudo_excel)
|
|
257
|
-
except Exception as e:
|
|
258
|
-
registro.exception(f"Erro ao coletar dados do RMD (aba {aba!r}): {e}")
|
|
259
|
-
return pl.DataFrame()
|
|
260
|
-
|
|
261
|
-
registro.info(f"Dados do RMD (aba {aba!r}) processados. Shape: {df.shape}.")
|
|
262
|
-
return df
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|