pyield 0.52.2__tar.gz → 0.53.1__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.52.2 → pyield-0.53.1}/PKG-INFO +3 -3
  2. {pyield-0.52.2 → pyield-0.53.1}/README.md +2 -2
  3. {pyield-0.52.2 → pyield-0.53.1}/pyield/_internal/converters.py +3 -2
  4. {pyield-0.52.2 → pyield-0.53.1}/pyield/b3/boletim.py +1 -0
  5. {pyield-0.52.2 → pyield-0.53.1}/pyield/b3/derivativos_intradia.py +7 -0
  6. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/__init__.py +2 -4
  7. pyield-0.53.1/pyield/tpf/secundario/__init__.py +19 -0
  8. pyield-0.52.2/pyield/bc/tpf_intradia.py → pyield-0.53.1/pyield/tpf/secundario/_intradia.py +15 -24
  9. pyield-0.53.1/pyield/tpf/secundario/_mensal.py +310 -0
  10. {pyield-0.52.2 → pyield-0.53.1}/pyproject.toml +1 -2
  11. pyield-0.52.2/pyield/bc/tpf_mensal.py +0 -146
  12. {pyield-0.52.2 → pyield-0.53.1}/LICENSE +0 -0
  13. {pyield-0.52.2 → pyield-0.53.1}/pyield/__init__.py +0 -0
  14. {pyield-0.52.2 → pyield-0.53.1}/pyield/_internal/__init__.py +0 -0
  15. {pyield-0.52.2 → pyield-0.53.1}/pyield/_internal/br_numbers.py +0 -0
  16. {pyield-0.52.2 → pyield-0.53.1}/pyield/_internal/cache.py +0 -0
  17. {pyield-0.52.2 → pyield-0.53.1}/pyield/_internal/data_cache.py +0 -0
  18. {pyield-0.52.2 → pyield-0.53.1}/pyield/_internal/retry.py +0 -0
  19. {pyield-0.52.2 → pyield-0.53.1}/pyield/_internal/types.py +0 -0
  20. {pyield-0.52.2 → pyield-0.53.1}/pyield/anbima/__init__.py +0 -0
  21. {pyield-0.52.2 → pyield-0.53.1}/pyield/anbima/imaq.py +0 -0
  22. {pyield-0.52.2 → pyield-0.53.1}/pyield/anbima/mercado_secundario.py +0 -0
  23. {pyield-0.52.2 → pyield-0.53.1}/pyield/b3/__init__.py +0 -0
  24. {pyield-0.52.2 → pyield-0.53.1}/pyield/b3/_contratos.py +0 -0
  25. {pyield-0.52.2 → pyield-0.53.1}/pyield/b3/_validar_pregao.py +0 -0
  26. {pyield-0.52.2 → pyield-0.53.1}/pyield/b3/di_over.py +0 -0
  27. {pyield-0.52.2 → pyield-0.53.1}/pyield/bc/__init__.py +0 -0
  28. {pyield-0.52.2 → pyield-0.53.1}/pyield/bc/_olinda.py +0 -0
  29. {pyield-0.52.2 → pyield-0.53.1}/pyield/bc/leiloes.py +0 -0
  30. {pyield-0.52.2 → pyield-0.53.1}/pyield/bc/lft.py +0 -0
  31. {pyield-0.52.2 → pyield-0.53.1}/pyield/bc/sgs.py +0 -0
  32. {pyield-0.52.2 → pyield-0.53.1}/pyield/du/__init__.py +0 -0
  33. {pyield-0.52.2 → pyield-0.53.1}/pyield/du/core.py +0 -0
  34. {pyield-0.52.2 → pyield-0.53.1}/pyield/du/feriados/__init__.py +0 -0
  35. {pyield-0.52.2 → pyield-0.53.1}/pyield/du/feriados/feriados_antigos_br.txt +0 -0
  36. {pyield-0.52.2 → pyield-0.53.1}/pyield/du/feriados/feriados_br.py +0 -0
  37. {pyield-0.52.2 → pyield-0.53.1}/pyield/du/feriados/feriados_novos_br.txt +0 -0
  38. {pyield-0.52.2 → pyield-0.53.1}/pyield/futuro/__init__.py +0 -0
  39. {pyield-0.52.2 → pyield-0.53.1}/pyield/futuro/contratos.py +0 -0
  40. {pyield-0.52.2 → pyield-0.53.1}/pyield/futuro/di1.py +0 -0
  41. {pyield-0.52.2 → pyield-0.53.1}/pyield/futuro/historico.py +0 -0
  42. {pyield-0.52.2 → pyield-0.53.1}/pyield/futuro/intradia.py +0 -0
  43. {pyield-0.52.2 → pyield-0.53.1}/pyield/fwd.py +0 -0
  44. {pyield-0.52.2 → pyield-0.53.1}/pyield/interpolador.py +0 -0
  45. {pyield-0.52.2 → pyield-0.53.1}/pyield/ipca/__init__.py +0 -0
  46. {pyield-0.52.2 → pyield-0.53.1}/pyield/ipca/historico.py +0 -0
  47. {pyield-0.52.2 → pyield-0.53.1}/pyield/ipca/projetado.py +0 -0
  48. {pyield-0.52.2 → pyield-0.53.1}/pyield/py.typed +0 -0
  49. {pyield-0.52.2 → pyield-0.53.1}/pyield/relogio.py +0 -0
  50. {pyield-0.52.2 → pyield-0.53.1}/pyield/selic/__init__.py +0 -0
  51. {pyield-0.52.2 → pyield-0.53.1}/pyield/selic/compromissada.py +0 -0
  52. {pyield-0.52.2 → pyield-0.53.1}/pyield/selic/copom.py +0 -0
  53. {pyield-0.52.2 → pyield-0.53.1}/pyield/selic/cpm.py +0 -0
  54. {pyield-0.52.2 → pyield-0.53.1}/pyield/selic/probabilities.py +0 -0
  55. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/_titulos/__init__.py +0 -0
  56. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/_titulos/lft.py +0 -0
  57. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/_titulos/ltn.py +0 -0
  58. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/_titulos/ntnb.py +0 -0
  59. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/_titulos/ntnb1.py +0 -0
  60. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/_titulos/ntnbprinc.py +0 -0
  61. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/_titulos/ntnc.py +0 -0
  62. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/_titulos/ntnf.py +0 -0
  63. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/benchmark.py +0 -0
  64. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/leiloes.py +0 -0
  65. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/pre.py +0 -0
  66. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/rmd/__init__.py +0 -0
  67. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/rmd/_aba_1_3.py +0 -0
  68. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/rmd/_aba_2_1.py +0 -0
  69. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/rmd/_common.py +0 -0
  70. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/rmd/_download.py +0 -0
  71. {pyield-0.52.2 → pyield-0.53.1}/pyield/tpf/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyield
3
- Version: 0.52.2
3
+ Version: 0.53.1
4
4
  Summary: Polars-powered toolkit for Brazilian fixed income analysis
5
5
  Keywords: fixed-income,brazil,finance,analysis,bonds
6
6
  Author: Carlos Carvalho
@@ -113,7 +113,7 @@ documentação.
113
113
  | `yd.forwards(...)` | função | Curva de taxas a termo | |
114
114
  | `yd.futuro` | módulo | Contratos futuros da B3 | `di1`, `historico`, `intradia`, `datas_disponiveis`, `vencimento`, `enriquecer`, `vencimento_expr` |
115
115
  | `yd.di1` | módulo | Curva DI1 e interpolação | `dados`, `interpolar_taxas`, `interpolar_taxa`, `datas_disponiveis` |
116
- | `yd.tpf` | módulo | Títulos públicos federais | `taxas`, `vencimentos`, `estoque`, `leiloes`, `benchmarks`, `curva_pre`, `premio_pre`, `rmd`, `secundario_mensal`, `secundario_intradia` |
116
+ | `yd.tpf` | módulo | Títulos públicos federais | `taxas`, `vencimentos`, `estoque`, `leiloes`, `benchmarks`, `curva_pre`, `premio_pre`, `rmd`, `secundario` |
117
117
  | `yd.lft` | módulo | LFT | `dados`, `vencimentos`, `cotacao`, `pu`, `taxa`, `vna`, `rentabilidade`, `rentabilidade_expr` |
118
118
  | `yd.ltn` | módulo | LTN | `dados`, `vencimentos`, `pu`, `taxa`, `duration_expr`, `dv01`, `dv01_expr`, `rentabilidade`, `rentabilidade_expr`, `taxas_forward` |
119
119
  | `yd.ntnb` | módulo | NTN-B | `dados`, `vencimentos`, `datas_pagamento`, `fluxos_caixa`, `cotacao`, `pu`, `taxa`, `taxas_zero`, `duration`, `duration_expr`, `dv01`, `dv01_expr`, `implicitas`, `curva` |
@@ -305,7 +305,7 @@ fonte indisponível) retornam DataFrame vazio ou `nan`, sem lançar exceção:
305
305
  import pyield as yd
306
306
 
307
307
  yd.futuro.historico("01-01-2030", "DI1").is_empty() # -> True
308
- yd.tpf.secundario_mensal("01-01-2030").is_empty() # -> True
308
+ yd.tpf.secundario.mensal("01-01-2030").is_empty() # -> True
309
309
  yd.ptax("25-12-2025") # -> nan
310
310
  ```
311
311
 
@@ -69,7 +69,7 @@ documentação.
69
69
  | `yd.forwards(...)` | função | Curva de taxas a termo | |
70
70
  | `yd.futuro` | módulo | Contratos futuros da B3 | `di1`, `historico`, `intradia`, `datas_disponiveis`, `vencimento`, `enriquecer`, `vencimento_expr` |
71
71
  | `yd.di1` | módulo | Curva DI1 e interpolação | `dados`, `interpolar_taxas`, `interpolar_taxa`, `datas_disponiveis` |
72
- | `yd.tpf` | módulo | Títulos públicos federais | `taxas`, `vencimentos`, `estoque`, `leiloes`, `benchmarks`, `curva_pre`, `premio_pre`, `rmd`, `secundario_mensal`, `secundario_intradia` |
72
+ | `yd.tpf` | módulo | Títulos públicos federais | `taxas`, `vencimentos`, `estoque`, `leiloes`, `benchmarks`, `curva_pre`, `premio_pre`, `rmd`, `secundario` |
73
73
  | `yd.lft` | módulo | LFT | `dados`, `vencimentos`, `cotacao`, `pu`, `taxa`, `vna`, `rentabilidade`, `rentabilidade_expr` |
74
74
  | `yd.ltn` | módulo | LTN | `dados`, `vencimentos`, `pu`, `taxa`, `duration_expr`, `dv01`, `dv01_expr`, `rentabilidade`, `rentabilidade_expr`, `taxas_forward` |
75
75
  | `yd.ntnb` | módulo | NTN-B | `dados`, `vencimentos`, `datas_pagamento`, `fluxos_caixa`, `cotacao`, `pu`, `taxa`, `taxas_zero`, `duration`, `duration_expr`, `dv01`, `dv01_expr`, `implicitas`, `curva` |
@@ -261,7 +261,7 @@ fonte indisponível) retornam DataFrame vazio ou `nan`, sem lançar exceção:
261
261
  import pyield as yd
262
262
 
263
263
  yd.futuro.historico("01-01-2030", "DI1").is_empty() # -> True
264
- yd.tpf.secundario_mensal("01-01-2030").is_empty() # -> True
264
+ yd.tpf.secundario.mensal("01-01-2030").is_empty() # -> True
265
265
  yd.ptax("25-12-2025") # -> nan
266
266
  ```
267
267
 
@@ -52,14 +52,15 @@ def converter_datas_expr(expr: pl.Expr | str) -> pl.Expr:
52
52
  expr = pl.col(expr)
53
53
 
54
54
  # Fallback por linha: o primeiro valor não-nulo vence.
55
- # Se o elemento for Date, o cast resolve no 1o termo.
55
+ # Datas e datetimes Python chegam ao Polars como strings ISO.
56
56
  # Se o elemento for null, ele permanece null em todos os termos.
57
57
  expr_str = expr.cast(pl.String, strict=False).str.strip_chars()
58
58
  return pl.coalesce(
59
- expr.cast(pl.Date, strict=False),
60
59
  expr_str.str.to_date(format="%d-%m-%Y", strict=False),
61
60
  expr_str.str.to_date(format="%d/%m/%Y", strict=False),
62
61
  expr_str.str.to_date(format="%Y-%m-%d", strict=False),
62
+ expr_str.str.to_date(format="%Y-%m-%d %H:%M:%S", strict=False),
63
+ expr_str.str.to_date(format="%Y-%m-%d %H:%M:%S%.f", strict=False),
63
64
  )
64
65
 
65
66
 
@@ -298,6 +298,7 @@ def _converter_para_df(registros: list[dict]) -> pl.DataFrame:
298
298
  # (pl.DataFrame infere colunas de ~50 primeiras linhas por padrão.)
299
299
  schema_str = {nome: pl.String for nome in SCHEMA_PRICE_REPORT}
300
300
  df = pl.DataFrame(registros, schema=schema_str)
301
+ df = df.with_columns(TradDt=pl.col("TradDt").str.to_date("%Y-%m-%d"))
301
302
  return df.cast(SCHEMA_PRICE_REPORT, strict=False)
302
303
 
303
304
 
@@ -103,8 +103,15 @@ def _converter_json_intradia(dados_json: list[dict]) -> pl.DataFrame:
103
103
  def _processar_colunas_intradia(df: pl.DataFrame) -> pl.DataFrame:
104
104
  colunas_disponiveis = [col for col in MAPEAMENTO if col in df.columns]
105
105
  tipos_disponiveis = pl.Schema({col: TIPOS[col] for col in colunas_disponiveis})
106
+ coluna_vencimento = "asset.AsstSummry.mtrtyCode"
107
+ exprs_data = []
108
+ if coluna_vencimento in colunas_disponiveis:
109
+ exprs_data.append(
110
+ pl.col(coluna_vencimento).str.to_date("%Y-%m-%d", strict=False)
111
+ )
106
112
  return (
107
113
  df.select(colunas_disponiveis)
114
+ .with_columns(exprs_data)
108
115
  .cast(tipos_disponiveis, strict=False)
109
116
  .rename(MAPEAMENTO, strict=False)
110
117
  )
@@ -2,8 +2,7 @@
2
2
 
3
3
  from pyield.anbima.imaq import estoque
4
4
  from pyield.anbima.mercado_secundario import TipoTPF, taxas, vencimentos
5
- from pyield.bc.tpf_intradia import secundario_intradia
6
- from pyield.bc.tpf_mensal import secundario_mensal
5
+ from pyield.tpf import secundario
7
6
  from pyield.tpf._titulos import lft, ltn, ntnb, ntnb1, ntnbprinc, ntnc, ntnf
8
7
  from pyield.tpf.benchmark import benchmarks
9
8
  from pyield.tpf.leiloes import leiloes
@@ -26,8 +25,7 @@ __all__ = [
26
25
  "ntnf",
27
26
  "premio_pre",
28
27
  "rmd",
29
- "secundario_intradia",
30
- "secundario_mensal",
28
+ "secundario",
31
29
  "taxas",
32
30
  "vencimentos",
33
31
  ]
@@ -0,0 +1,19 @@
1
+ """Negociações do mercado secundário de TPFs no sistema Selic do BCB."""
2
+
3
+ from pyield.tpf.secundario._intradia import intradia as intradia
4
+ from pyield.tpf.secundario._mensal import baixar_zip as baixar_zip
5
+ from pyield.tpf.secundario._mensal import ler_zip as ler_zip
6
+ from pyield.tpf.secundario._mensal import mensal as mensal
7
+ from pyield.tpf.secundario._mensal import (
8
+ nome_arquivo_mensal as nome_arquivo_mensal,
9
+ )
10
+ from pyield.tpf.secundario._mensal import zip_para_silver as zip_para_silver
11
+
12
+ __all__ = [
13
+ "baixar_zip",
14
+ "intradia",
15
+ "ler_zip",
16
+ "mensal",
17
+ "nome_arquivo_mensal",
18
+ "zip_para_silver",
19
+ ]
@@ -1,7 +1,4 @@
1
- """
2
- Busca dados intradiários de negociações secundárias da dívida pública federal.
3
- https://www.bcb.gov.br/htms/selic/selicprecos.asp?frame=1
4
- """
1
+ """Dados intradia do mercado secundário de TPFs no sistema Selic do BCB."""
5
2
 
6
3
  import datetime as dt
7
4
 
@@ -13,30 +10,26 @@ from pyield._internal.br_numbers import float_br, inteiro_br, taxa_br
13
10
  from pyield._internal.cache import ttl_cache
14
11
  from pyield._internal.retry import retry_padrao
15
12
 
16
- HORA_INICIO_TEMPO_REAL = dt.time(9, 0, 0)
17
- HORA_FIM_TEMPO_REAL = dt.time(22, 0, 0)
18
13
  URL_BASE_TEMPO_REAL = (
19
14
  "https://www3.bcb.gov.br/novoselic/rest/precosNegociacao/pub/download/estatisticas/"
20
15
  )
21
16
 
17
+ HORA_INICIO_TEMPO_REAL = dt.time(9, 0, 0)
18
+ HORA_FIM_TEMPO_REAL = dt.time(22, 0, 0)
19
+
22
20
 
23
21
  @ttl_cache()
24
22
  @retry_padrao
25
- def _buscar_csv() -> bytes:
26
- """
27
- Exemplo de URL do CSV com dados intradiários:
28
- https://www3.bcb.gov.br/novoselic/rest/precosNegociacao/pub/download/estatisticas/02-06-2025
29
- """
23
+ def _buscar_csv_intradia() -> bytes:
30
24
  hoje = relogio.hoje()
31
25
  data_formatada = hoje.strftime("%d-%m-%Y")
32
26
  url = f"{URL_BASE_TEMPO_REAL}{data_formatada}"
33
- r = requests.get(url, timeout=30) # API costuma levar ~10s
34
- r.raise_for_status()
35
- return r.content
27
+ resposta = requests.get(url, timeout=30) # API costuma levar ~10s
28
+ resposta.raise_for_status()
29
+ return resposta.content
36
30
 
37
31
 
38
- def _parsear_df(dados: bytes) -> pl.DataFrame:
39
- """Lê CSV como strings."""
32
+ def _parsear_csv_intradia(dados: bytes) -> pl.DataFrame:
40
33
  return pl.read_csv(
41
34
  dados,
42
35
  separator=";",
@@ -45,8 +38,7 @@ def _parsear_df(dados: bytes) -> pl.DataFrame:
45
38
  ).rename(lambda c: c.strip())
46
39
 
47
40
 
48
- def _processar_df(df: pl.DataFrame) -> pl.DataFrame:
49
- """Filtra registros, converte tipos e reordena colunas."""
41
+ def _processar_df_intradia(df: pl.DataFrame) -> pl.DataFrame:
50
42
  agora = relogio.agora()
51
43
  return df.filter(pl.col("//1") == "1").select(
52
44
  data_hora_consulta=agora,
@@ -84,7 +76,6 @@ def _processar_df(df: pl.DataFrame) -> pl.DataFrame:
84
76
 
85
77
 
86
78
  def _mercado_selic_aberto() -> bool:
87
- """Verifica se o mercado SELIC está aberto no momento."""
88
79
  agora = relogio.agora()
89
80
  hoje = agora.date()
90
81
  hora = agora.time()
@@ -94,7 +85,7 @@ def _mercado_selic_aberto() -> bool:
94
85
  return eh_dia_util and eh_horario
95
86
 
96
87
 
97
- def secundario_intradia() -> pl.DataFrame:
88
+ def intradia() -> pl.DataFrame:
98
89
  """Busca dados intradia do mercado secundário de TPFs.
99
90
 
100
91
  Fonte: Banco Central do Brasil, sistema SELIC. Os dados ficam disponíveis
@@ -138,11 +129,11 @@ def secundario_intradia() -> pl.DataFrame:
138
129
  * termo_quantidade_corretagem (Int64): títulos a termo via corretagem.
139
130
 
140
131
  Examples:
141
- >>> df = yd.tpf.secundario_intradia() # doctest: +SKIP
132
+ >>> df = yd.tpf.secundario.intradia() # doctest: +SKIP
142
133
  """
143
134
  if not _mercado_selic_aberto():
144
135
  return pl.DataFrame()
145
136
 
146
- texto_bruto = _buscar_csv()
147
- df = _parsear_df(texto_bruto)
148
- return _processar_df(df)
137
+ texto_bruto = _buscar_csv_intradia()
138
+ df = _parsear_csv_intradia(texto_bruto)
139
+ return _processar_df_intradia(df)
@@ -0,0 +1,310 @@
1
+ """Dados mensais do mercado secundário de TPFs no sistema Selic do BCB."""
2
+
3
+ import datetime as dt
4
+ import io
5
+ import os
6
+ import zipfile as zf
7
+ from pathlib import Path
8
+
9
+ import polars as pl
10
+ import polars.selectors as ps
11
+ import requests
12
+
13
+ from pyield import relogio
14
+ from pyield._internal.br_numbers import float_br
15
+ from pyield._internal.cache import ttl_cache
16
+ from pyield._internal.converters import converter_datas
17
+ from pyield._internal.retry import retry_padrao
18
+ from pyield._internal.types import DateLike, any_is_empty
19
+
20
+ URL_BASE_MENSAL = "https://www4.bcb.gov.br/pom/demab/negociacoes/download"
21
+ CHAVES_ORDENACAO = ["data_liquidacao", "titulo", "data_vencimento"]
22
+ COLUNAS_MINIMAS_CSV = 2
23
+
24
+ type CaminhoArquivo = str | os.PathLike[str]
25
+
26
+
27
+ def _tipo_arquivo(extragrupo: bool) -> str:
28
+ return "E" if extragrupo else "T"
29
+
30
+
31
+ def _data_mensal(data: DateLike) -> dt.date:
32
+ data_alvo = converter_datas(data)
33
+ if not isinstance(data_alvo, dt.date):
34
+ msg = "data deve ser escalar para consultas mensais do secundário de TPFs"
35
+ raise ValueError(msg)
36
+ return data_alvo
37
+
38
+
39
+ def nome_arquivo_mensal(data: DateLike, extragrupo: bool = False) -> str:
40
+ """Retorna o nome do arquivo ZIP mensal do secundário no BCB/SELIC.
41
+
42
+ Args:
43
+ data: Data de referência. Apenas ano e mês definem o arquivo.
44
+ extragrupo: Se verdadeiro, retorna o nome do arquivo extragrupo.
45
+
46
+ Returns:
47
+ Nome do arquivo ZIP mensal publicado pelo BCB.
48
+
49
+ Examples:
50
+ >>> yd.tpf.secundario.nome_arquivo_mensal("07-06-2026")
51
+ 'NegT202606.ZIP'
52
+ >>> yd.tpf.secundario.nome_arquivo_mensal("07-01-2025", extragrupo=True)
53
+ 'NegE202501.ZIP'
54
+ """
55
+ data_alvo = _data_mensal(data)
56
+ return f"Neg{_tipo_arquivo(extragrupo)}{data_alvo:%Y%m}.ZIP"
57
+
58
+
59
+ @ttl_cache()
60
+ @retry_padrao
61
+ def _baixar_url_zip(url_arquivo: str) -> bytes:
62
+ resposta = requests.get(url_arquivo, allow_redirects=True, timeout=60)
63
+ resposta.raise_for_status()
64
+ return resposta.content
65
+
66
+
67
+ def baixar_zip(data: DateLike, extragrupo: bool = False) -> bytes:
68
+ """Baixa o ZIP bruto mensal de negociações secundárias de TPFs.
69
+
70
+ Fonte: Banco Central do Brasil, sistema SELIC. A fonte publica um arquivo
71
+ por mês; por isso, apenas o ano e o mês de ``data`` são usados.
72
+
73
+ A função valida a estrutura mínima do ZIP antes de retornar os bytes, para
74
+ evitar que pipelines de ingestão salvem bronze vazio, corrompido ou sem CSV
75
+ plausível.
76
+
77
+ Args:
78
+ data: Data de referência. Apenas ano e mês definem o arquivo.
79
+ extragrupo: Se verdadeiro, baixa o arquivo extragrupo.
80
+
81
+ Returns:
82
+ Bytes validados do arquivo ZIP mensal publicado pelo BCB.
83
+
84
+ Raises:
85
+ requests.HTTPError: Se o arquivo não estiver disponível no BCB ou a
86
+ resposta HTTP indicar erro.
87
+ ValueError: Se o conteúdo baixado não for um ZIP bruto plausível.
88
+
89
+ Examples:
90
+ >>> conteudo = yd.tpf.secundario.baixar_zip("07-01-2025") # doctest: +SKIP
91
+ """
92
+ arquivo = nome_arquivo_mensal(data, extragrupo)
93
+ conteudo_zip = _baixar_url_zip(f"{URL_BASE_MENSAL}/{arquivo}")
94
+ _validar_zip(conteudo_zip, arquivo)
95
+ return conteudo_zip
96
+
97
+
98
+ def _extrair_csv_zip(conteudo_zip: bytes) -> bytes:
99
+ try:
100
+ arquivo_zip = zf.ZipFile(io.BytesIO(conteudo_zip), "r")
101
+ except zf.BadZipFile as exc:
102
+ msg = "ZIP inválido ou ilegível"
103
+ raise ValueError(msg) from exc
104
+
105
+ with arquivo_zip:
106
+ nomes = arquivo_zip.namelist()
107
+ if not nomes:
108
+ raise ValueError("ZIP vazio")
109
+
110
+ arquivo_corrompido = arquivo_zip.testzip()
111
+ if arquivo_corrompido is not None:
112
+ msg = f"ZIP contém arquivo corrompido: {arquivo_corrompido}"
113
+ raise ValueError(msg)
114
+
115
+ return arquivo_zip.read(nomes[0])
116
+
117
+
118
+ def _validar_zip(conteudo_zip: bytes, nome: str | None = None) -> None:
119
+ rotulo = f"{nome}: " if nome else ""
120
+ try:
121
+ conteudo_csv = _extrair_csv_zip(conteudo_zip)
122
+ except ValueError as exc:
123
+ msg = f"{rotulo}{exc}"
124
+ raise ValueError(msg) from exc
125
+
126
+ df_amostra = pl.read_csv(
127
+ conteudo_csv,
128
+ encoding="latin1",
129
+ separator=";",
130
+ infer_schema=False,
131
+ null_values="",
132
+ n_rows=1,
133
+ )
134
+
135
+ if len(df_amostra.columns) < COLUNAS_MINIMAS_CSV:
136
+ msg = f"{rotulo}CSV não parece estar separado por ponto e vírgula"
137
+ raise ValueError(msg)
138
+
139
+
140
+ def _parsear_csv_mensal(conteudo_csv: bytes) -> pl.DataFrame:
141
+ return pl.read_csv(
142
+ conteudo_csv,
143
+ encoding="latin1",
144
+ separator=";",
145
+ infer_schema=False,
146
+ null_values="",
147
+ )
148
+
149
+
150
+ def _processar_df_mensal(df: pl.DataFrame) -> pl.DataFrame:
151
+ operacoes_corretagem = (
152
+ pl.col("NUM OPER COM CORRETAGEM").cast(pl.Int64)
153
+ if "NUM OPER COM CORRETAGEM" in df.columns
154
+ else pl.lit(None, dtype=pl.Int64)
155
+ )
156
+ quantidade_corretagem = (
157
+ pl.col("QUANT NEG COM CORRETAGEM").cast(pl.Int64)
158
+ if "QUANT NEG COM CORRETAGEM" in df.columns
159
+ else pl.lit(None, dtype=pl.Int64)
160
+ )
161
+
162
+ return (
163
+ df.with_columns(ps.string().str.strip_chars())
164
+ .with_columns(
165
+ quantidade=pl.col("QUANT NEGOCIADA").cast(pl.Int64),
166
+ pu_medio=float_br("PU MED"),
167
+ )
168
+ .select(
169
+ data_liquidacao=pl.col("DATA MOV").str.to_date("%d/%m/%Y", strict=False),
170
+ titulo=pl.col("SIGLA"),
171
+ codigo_selic=pl.col("CODIGO").cast(pl.Int64),
172
+ isin=pl.col("CODIGO ISIN"),
173
+ data_emissao=pl.col("EMISSAO").str.to_date("%d/%m/%Y", strict=False),
174
+ data_vencimento=pl.col("VENCIMENTO").str.to_date("%d/%m/%Y", strict=False),
175
+ operacoes=pl.col("NUM DE OPER").cast(pl.Int64),
176
+ quantidade=pl.col("quantidade"),
177
+ pu_minimo=float_br("PU MIN"),
178
+ pu_medio=pl.col("pu_medio"),
179
+ pu_maximo=float_br("PU MAX"),
180
+ pu_lastro=float_br("PU LASTRO"),
181
+ valor_par=float_br("VALOR PAR"),
182
+ taxa_minima=float_br("TAXA MIN"),
183
+ taxa_media=float_br("TAXA MED"),
184
+ taxa_maxima=float_br("TAXA MAX"),
185
+ operacoes_corretagem=operacoes_corretagem,
186
+ quantidade_corretagem=quantidade_corretagem,
187
+ )
188
+ .sort(CHAVES_ORDENACAO)
189
+ )
190
+
191
+
192
+ def zip_para_silver(conteudo_zip: bytes) -> pl.DataFrame:
193
+ """Converte o ZIP mensal bruto do secundário de TPFs em silver.
194
+
195
+ Fonte: Banco Central do Brasil, sistema SELIC. Esta função representa a
196
+ etapa bronze -> silver: extrai o CSV interno, limpa os campos, converte
197
+ tipos e retorna o schema Polars canônico usado pela PYield. Ela não faz
198
+ enriquecimento para camada ouro.
199
+
200
+ Args:
201
+ conteudo_zip: Bytes do arquivo ZIP bruto mensal.
202
+
203
+ Returns:
204
+ DataFrame Polars com dados mensais do mercado secundário.
205
+
206
+ Output Columns:
207
+ * data_liquidacao (Date): data de liquidação da negociação.
208
+ * titulo (String): sigla do título público.
209
+ * codigo_selic (Int64): código único no sistema SELIC.
210
+ * isin (String): código ISIN.
211
+ * data_emissao (Date): data de emissão do título.
212
+ * data_vencimento (Date): data de vencimento do título.
213
+ * operacoes (Int64): número total de operações.
214
+ * quantidade (Int64): quantidade total negociada.
215
+ * pu_minimo (Float64): preço unitário mínimo.
216
+ * pu_medio (Float64): preço unitário médio.
217
+ * pu_maximo (Float64): preço unitário máximo.
218
+ * pu_lastro (Float64): preço unitário de lastro.
219
+ * valor_par (Float64): valor par do título.
220
+ * taxa_minima (Float64): taxa mínima.
221
+ * taxa_media (Float64): taxa média.
222
+ * taxa_maxima (Float64): taxa máxima.
223
+ * operacoes_corretagem (Int64): operações com corretagem.
224
+ * quantidade_corretagem (Int64): quantidade com corretagem.
225
+
226
+ Notes:
227
+ O schema silver é estável para concatenação entre meses. Em layouts
228
+ antigos da fonte que não trazem corretagem, ``operacoes_corretagem`` e
229
+ ``quantidade_corretagem`` são retornadas como nulas.
230
+
231
+ Examples:
232
+ >>> conteudo = yd.tpf.secundario.baixar_zip("07-01-2025") # doctest: +SKIP
233
+ >>> df = yd.tpf.secundario.zip_para_silver(conteudo) # doctest: +SKIP
234
+ """
235
+ conteudo_csv = _extrair_csv_zip(conteudo_zip)
236
+ return _processar_df_mensal(_parsear_csv_mensal(conteudo_csv))
237
+
238
+
239
+ def ler_zip(caminho: CaminhoArquivo) -> pl.DataFrame:
240
+ """Lê um ZIP mensal local do secundário de TPFs e converte para silver.
241
+
242
+ Fonte: Banco Central do Brasil, sistema SELIC. Esta função é um atalho para
243
+ pipelines que salvam o bronze bruto e depois processam o arquivo local com o
244
+ mesmo schema de ``zip_para_silver``.
245
+
246
+ Args:
247
+ caminho: Caminho do arquivo ZIP bruto.
248
+
249
+ Returns:
250
+ DataFrame Polars com dados mensais do mercado secundário.
251
+
252
+ Examples:
253
+ >>> df = yd.tpf.secundario.ler_zip("NegT202501.ZIP") # doctest: +SKIP
254
+ """
255
+ return zip_para_silver(Path(caminho).read_bytes())
256
+
257
+
258
+ def mensal(data: DateLike, extragrupo: bool = False) -> pl.DataFrame:
259
+ """Busca dados mensais do mercado secundário de TPFs.
260
+
261
+ Fonte: Banco Central do Brasil, sistema SELIC. Baixa o ZIP mensal de
262
+ negociações secundárias, valida o bronze bruto e retorna o DataFrame ouro.
263
+ Apenas o ano e o mês de ``data`` são usados para identificar o arquivo.
264
+
265
+ Args:
266
+ data: Data de referência. Apenas ano e mês definem o arquivo.
267
+ extragrupo: Se verdadeiro, busca apenas negociações extragrupo.
268
+
269
+ Returns:
270
+ DataFrame Polars com dados mensais do mercado secundário.
271
+
272
+ Output Columns:
273
+ * data_liquidacao (Date): data de liquidação da negociação.
274
+ * titulo (String): sigla do título público.
275
+ * codigo_selic (Int64): código único no sistema SELIC.
276
+ * isin (String): código ISIN.
277
+ * data_emissao (Date): data de emissão do título.
278
+ * data_vencimento (Date): data de vencimento do título.
279
+ * operacoes (Int64): número total de operações.
280
+ * quantidade (Int64): quantidade total negociada.
281
+ * pu_minimo (Float64): preço unitário mínimo.
282
+ * pu_medio (Float64): preço unitário médio.
283
+ * pu_maximo (Float64): preço unitário máximo.
284
+ * pu_lastro (Float64): preço unitário de lastro.
285
+ * valor_par (Float64): valor par do título.
286
+ * taxa_minima (Float64): taxa mínima.
287
+ * taxa_media (Float64): taxa média.
288
+ * taxa_maxima (Float64): taxa máxima.
289
+ * operacoes_corretagem (Int64): operações com corretagem.
290
+ * quantidade_corretagem (Int64): quantidade com corretagem.
291
+ * financeiro (Float64): valor financeiro negociado.
292
+
293
+ Notes:
294
+ Esta é a camada ouro mensal: retorna o schema de ``zip_para_silver``
295
+ acrescido de ``financeiro = quantidade * pu_medio``.
296
+
297
+ Examples:
298
+ >>> df = yd.tpf.secundario.mensal("07-01-2025", extragrupo=True)
299
+ """
300
+ if any_is_empty(data):
301
+ return pl.DataFrame()
302
+
303
+ data_alvo = _data_mensal(data)
304
+ hoje = relogio.hoje()
305
+ if (data_alvo.year, data_alvo.month) > (hoje.year, hoje.month):
306
+ return pl.DataFrame()
307
+
308
+ return zip_para_silver(baixar_zip(data_alvo, extragrupo)).with_columns(
309
+ financeiro=(pl.col("quantidade") * pl.col("pu_medio")).round(2),
310
+ )
@@ -19,7 +19,7 @@ dependencies = [
19
19
  "fastexcel>=0.19.0",
20
20
  "tzdata>=2024.1; platform_system == 'Windows'",
21
21
  ]
22
- version = "0.52.2"
22
+ version = "0.53.1"
23
23
 
24
24
  [project.urls]
25
25
  Homepage = "https://github.com/crdcj/PYield"
@@ -32,7 +32,6 @@ dev = [
32
32
  "python-dotenv[cli]",
33
33
  "ipykernel",
34
34
  "pytest",
35
- "pyright",
36
35
  "ruff",
37
36
  "mkdocs",
38
37
  "mkdocs-material",
@@ -1,146 +0,0 @@
1
- """
2
- Módulo para buscar dados mensais de negociações secundárias da Dívida Pública
3
- Federal (TPF) registradas no sistema Selic do Banco Central do Brasil (BCB).
4
- Os dados são baixados em ZIP, extraídos e carregados em um DataFrame Polars.
5
- Exemplo do formato dos dados (3 primeiras linhas):
6
- DATA MOV ; SIGLA; CODIGO; CODIGO ISIN ; EMISSAO ; VENCIMENTO; NUM DE OPER; QUANT NEGOCIADA; VALOR NEGOCIADO; PU MIN ; PU MED ; PU MAX ; PU LASTRO ; VALOR PAR ; TAXA MIN; TAXA MED; TAXA MAX; NUM OPER COM CORRETAGEM; QUANT NEG COM CORRETAGEM
7
- 02/09/2024; LFT ; 210100; BRSTNCLF1RC4; 26/10/2018; 01/03/2025; 48; 100221; ; 15288,00898200; 15292,57098100; 15302,77742100; 15285,54813387; 15288,23830700; -0,1897 ; -0,0565 ; 0,0032 ; 20; 16155
8
- 02/09/2024; LFT ; 210100; BRSTNCLF1RD2; 08/03/2019; 01/09/2025; 101; 230120; ; 15288,23830700; 15294,25937800; 15311,01778200; 15279,49187722; 15288,23830700; -0,1498 ; -0,0395 ; 0,0000 ; 21; 19059
9
- 02/09/2024; LFT ; 210100; BRSTNCLF1RE0; 06/09/2019; 01/03/2026; 88; 512642; ; 15286,63304100; 15288,20025100; 15292,77891300; 15268,60295396; 15288,23830700; -0,0198 ; 0,0002 ; 0,0071 ; 27; 121742
10
- ...
11
- """
12
-
13
- import datetime as dt
14
- import io
15
- import zipfile as zf
16
-
17
- import polars as pl
18
- import polars.selectors as ps
19
- import requests
20
-
21
- from pyield._internal.br_numbers import float_br
22
- from pyield._internal.cache import ttl_cache
23
- from pyield._internal.converters import converter_datas
24
- from pyield._internal.retry import retry_padrao
25
- from pyield._internal.types import DateLike, any_is_empty
26
- from pyield.relogio import hoje
27
-
28
- URL_BASE = "https://www4.bcb.gov.br/pom/demab/negociacoes/download"
29
-
30
- CHAVES_ORDENACAO = ["data_liquidacao", "titulo", "data_vencimento"]
31
-
32
-
33
- def _montar_url(data_alvo: dt.date, extragrupo: bool) -> str:
34
- ano_mes = data_alvo.strftime("%Y%m")
35
- sufixo = "E" if extragrupo else "T"
36
- return f"{URL_BASE}/Neg{sufixo}{ano_mes}.ZIP"
37
-
38
-
39
- @ttl_cache()
40
- @retry_padrao
41
- def _baixar_zip(url_arquivo: str) -> bytes:
42
- """Baixa o conteúdo ZIP e retorna os bytes."""
43
- resposta = requests.get(url_arquivo, timeout=10)
44
- resposta.raise_for_status()
45
- return resposta.content
46
-
47
-
48
- def _descompactar_zip(conteudo_zip: bytes) -> bytes:
49
- """Descompacta o ZIP e retorna o conteúdo do primeiro arquivo."""
50
- with zf.ZipFile(io.BytesIO(conteudo_zip), "r") as arquivo_zip:
51
- return arquivo_zip.read(arquivo_zip.namelist()[0])
52
-
53
-
54
- def _parsear_df(conteudo_csv: bytes) -> pl.DataFrame:
55
- """Lê o CSV extraído do ZIP em DataFrame com todas as colunas como string."""
56
- return pl.read_csv(
57
- conteudo_csv,
58
- encoding="latin1",
59
- separator=";",
60
- infer_schema=False,
61
- null_values="",
62
- )
63
-
64
-
65
- def _processar_df(df: pl.DataFrame) -> pl.DataFrame:
66
- return (
67
- df.with_columns(ps.string().str.strip_chars())
68
- .with_columns(
69
- quantidade=pl.col("QUANT NEGOCIADA").cast(pl.Int64),
70
- pu_medio=float_br("PU MED"),
71
- )
72
- .select(
73
- data_liquidacao=pl.col("DATA MOV").str.to_date("%d/%m/%Y", strict=False),
74
- titulo=pl.col("SIGLA"),
75
- codigo_selic=pl.col("CODIGO").cast(pl.Int64),
76
- isin=pl.col("CODIGO ISIN"),
77
- data_emissao=pl.col("EMISSAO").str.to_date("%d/%m/%Y", strict=False),
78
- data_vencimento=pl.col("VENCIMENTO").str.to_date("%d/%m/%Y", strict=False),
79
- operacoes=pl.col("NUM DE OPER").cast(pl.Int64),
80
- quantidade=pl.col("quantidade"),
81
- financeiro=(pl.col("quantidade") * pl.col("pu_medio")).round(2),
82
- pu_minimo=float_br("PU MIN"),
83
- pu_medio=pl.col("pu_medio"),
84
- pu_maximo=float_br("PU MAX"),
85
- pu_lastro=float_br("PU LASTRO"),
86
- valor_par=float_br("VALOR PAR"),
87
- taxa_minima=float_br("TAXA MIN"),
88
- taxa_media=float_br("TAXA MED"),
89
- taxa_maxima=float_br("TAXA MAX"),
90
- operacoes_corretagem=pl.col("NUM OPER COM CORRETAGEM").cast(pl.Int64),
91
- quantidade_corretagem=pl.col("QUANT NEG COM CORRETAGEM").cast(pl.Int64),
92
- )
93
- .sort(CHAVES_ORDENACAO)
94
- )
95
-
96
-
97
- def secundario_mensal(
98
- data: DateLike,
99
- extragrupo: bool = False,
100
- ) -> pl.DataFrame:
101
- """Busca dados mensais do mercado secundário de TPFs.
102
-
103
- Fonte: Banco Central do Brasil, sistema SELIC. Baixa o arquivo mensal de
104
- negociações secundárias para o mês correspondente à data informada.
105
-
106
- Args:
107
- data: Data de referência.
108
- extragrupo: Se verdadeiro, busca apenas negociações extragrupo.
109
-
110
- Returns:
111
- DataFrame Polars com dados mensais do mercado secundário.
112
-
113
- Output Columns:
114
- * data_liquidacao (Date): data de liquidação da negociação.
115
- * titulo (String): sigla do título público.
116
- * codigo_selic (Int64): código único no sistema SELIC.
117
- * isin (String): código ISIN.
118
- * data_emissao (Date): data de emissão do título.
119
- * data_vencimento (Date): data de vencimento do título.
120
- * operacoes (Int64): número total de operações.
121
- * quantidade (Int64): quantidade total negociada.
122
- * financeiro (Float64): valor financeiro negociado.
123
- * pu_minimo (Float64): preço unitário mínimo.
124
- * pu_medio (Float64): preço unitário médio.
125
- * pu_maximo (Float64): preço unitário máximo.
126
- * pu_lastro (Float64): preço unitário de lastro.
127
- * valor_par (Float64): valor par do título.
128
- * taxa_minima (Float64): taxa mínima.
129
- * taxa_media (Float64): taxa média.
130
- * taxa_maxima (Float64): taxa máxima.
131
- * operacoes_corretagem (Int64): operações com corretagem.
132
- * quantidade_corretagem (Int64): quantidade com corretagem.
133
-
134
- Examples:
135
- >>> df = yd.tpf.secundario_mensal("07-01-2025", extragrupo=True)
136
- """
137
- if any_is_empty(data):
138
- return pl.DataFrame()
139
- data_alvo = converter_datas(data)
140
- if (data_alvo.year, data_alvo.month) > (hoje().year, hoje().month):
141
- return pl.DataFrame()
142
- url = _montar_url(data_alvo, extragrupo)
143
- conteudo_zip = _baixar_zip(url)
144
- arquivo_extraido = _descompactar_zip(conteudo_zip)
145
- df = _parsear_df(arquivo_extraido)
146
- return _processar_df(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