PYield 0.45.2__tar.gz → 0.46.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {pyield-0.45.2 → pyield-0.46.0}/PKG-INFO +1 -3
  2. pyield-0.46.0/pyield/__about__.py +1 -0
  3. pyield-0.46.0/pyield/_internal/br_numbers.py +40 -0
  4. pyield-0.46.0/pyield/_internal/retry.py +98 -0
  5. {pyield-0.45.2 → pyield-0.46.0}/pyield/anbima/difusao.py +42 -101
  6. pyield-0.46.0/pyield/anbima/ettj_intraday.py +83 -0
  7. pyield-0.46.0/pyield/anbima/ettj_last.py +76 -0
  8. pyield-0.46.0/pyield/anbima/ima.py +129 -0
  9. {pyield-0.45.2 → pyield-0.46.0}/pyield/anbima/imaq.py +22 -36
  10. pyield-0.46.0/pyield/b3/di_over.py +77 -0
  11. {pyield-0.45.2 → pyield-0.46.0}/pyield/b3/intraday_derivatives.py +5 -12
  12. {pyield-0.45.2 → pyield-0.46.0}/pyield/b3/price_report.py +19 -15
  13. pyield-0.46.0/pyield/bc/auction.py +395 -0
  14. pyield-0.46.0/pyield/bc/ptax_api.py +173 -0
  15. pyield-0.46.0/pyield/bc/rates.py +315 -0
  16. {pyield-0.45.2 → pyield-0.46.0}/pyield/bc/repo.py +31 -104
  17. pyield-0.46.0/pyield/bc/trades_intraday.py +153 -0
  18. {pyield-0.45.2 → pyield-0.46.0}/pyield/bc/trades_monthly.py +61 -84
  19. {pyield-0.45.2 → pyield-0.46.0}/pyield/bc/vna.py +12 -4
  20. pyield-0.46.0/pyield/ipca/historical.py +214 -0
  21. {pyield-0.45.2 → pyield-0.46.0}/pyield/ipca/projected.py +1 -1
  22. {pyield-0.45.2 → pyield-0.46.0}/pyield/rmd.py +33 -79
  23. {pyield-0.45.2 → pyield-0.46.0}/pyield/selic/cpm.py +2 -6
  24. {pyield-0.45.2 → pyield-0.46.0}/pyield/tn/auctions.py +2 -2
  25. pyield-0.46.0/pyield/tn/benchmark.py +137 -0
  26. {pyield-0.45.2 → pyield-0.46.0}/pyield/tn/ntnb.py +1 -2
  27. {pyield-0.45.2 → pyield-0.46.0}/pyield/tn/ntnb1.py +1 -2
  28. {pyield-0.45.2 → pyield-0.46.0}/pyield/tn/ntnc.py +1 -2
  29. {pyield-0.45.2 → pyield-0.46.0}/pyield/tn/ntnf.py +1 -2
  30. {pyield-0.45.2 → pyield-0.46.0}/pyield/tn/utils.py +8 -0
  31. {pyield-0.45.2 → pyield-0.46.0}/pyproject.toml +1 -3
  32. pyield-0.45.2/pyield/__about__.py +0 -1
  33. pyield-0.45.2/pyield/_internal/retry.py +0 -75
  34. pyield-0.45.2/pyield/anbima/ettj_intraday.py +0 -148
  35. pyield-0.45.2/pyield/anbima/ettj_last.py +0 -117
  36. pyield-0.45.2/pyield/anbima/ima.py +0 -193
  37. pyield-0.45.2/pyield/b3/di_over.py +0 -78
  38. pyield-0.45.2/pyield/bc/auction.py +0 -464
  39. pyield-0.45.2/pyield/bc/ptax_api.py +0 -258
  40. pyield-0.45.2/pyield/bc/rates.py +0 -408
  41. pyield-0.45.2/pyield/bc/trades_intraday.py +0 -239
  42. pyield-0.45.2/pyield/ipca/historical.py +0 -197
  43. pyield-0.45.2/pyield/tn/benchmark.py +0 -158
  44. {pyield-0.45.2 → pyield-0.46.0}/.gitignore +0 -0
  45. {pyield-0.45.2 → pyield-0.46.0}/LICENSE +0 -0
  46. {pyield-0.45.2 → pyield-0.46.0}/README.md +0 -0
  47. {pyield-0.45.2 → pyield-0.46.0}/pyield/__init__.py +0 -0
  48. {pyield-0.45.2 → pyield-0.46.0}/pyield/_internal/__init__.py +0 -0
  49. {pyield-0.45.2 → pyield-0.46.0}/pyield/_internal/cache.py +0 -0
  50. {pyield-0.45.2 → pyield-0.46.0}/pyield/_internal/converters.py +0 -0
  51. {pyield-0.45.2 → pyield-0.46.0}/pyield/_internal/data_cache.py +0 -0
  52. {pyield-0.45.2 → pyield-0.46.0}/pyield/_internal/types.py +0 -0
  53. {pyield-0.45.2 → pyield-0.46.0}/pyield/anbima/__init__.py +0 -0
  54. {pyield-0.45.2 → pyield-0.46.0}/pyield/anbima/tpf.py +0 -0
  55. {pyield-0.45.2 → pyield-0.46.0}/pyield/b3/__init__.py +0 -0
  56. {pyield-0.45.2 → pyield-0.46.0}/pyield/b3/_contracts.py +0 -0
  57. {pyield-0.45.2 → pyield-0.46.0}/pyield/b3/_validar_pregao.py +0 -0
  58. {pyield-0.45.2 → pyield-0.46.0}/pyield/b3/di1.py +0 -0
  59. {pyield-0.45.2 → pyield-0.46.0}/pyield/b3/futures/__init__.py +0 -0
  60. {pyield-0.45.2 → pyield-0.46.0}/pyield/b3/futures/common.py +0 -0
  61. {pyield-0.45.2 → pyield-0.46.0}/pyield/b3/futures/historical.py +0 -0
  62. {pyield-0.45.2 → pyield-0.46.0}/pyield/b3/futures/intraday.py +0 -0
  63. {pyield-0.45.2 → pyield-0.46.0}/pyield/bc/__init__.py +0 -0
  64. {pyield-0.45.2 → pyield-0.46.0}/pyield/bc/copom.py +0 -0
  65. {pyield-0.45.2 → pyield-0.46.0}/pyield/bday/__init__.py +0 -0
  66. {pyield-0.45.2 → pyield-0.46.0}/pyield/bday/core.py +0 -0
  67. {pyield-0.45.2 → pyield-0.46.0}/pyield/bday/holidays/br_holidays_new.txt +0 -0
  68. {pyield-0.45.2 → pyield-0.46.0}/pyield/bday/holidays/br_holidays_old.txt +0 -0
  69. {pyield-0.45.2 → pyield-0.46.0}/pyield/bday/holidays/brholidays.py +0 -0
  70. {pyield-0.45.2 → pyield-0.46.0}/pyield/clock.py +0 -0
  71. {pyield-0.45.2 → pyield-0.46.0}/pyield/fwd.py +0 -0
  72. {pyield-0.45.2 → pyield-0.46.0}/pyield/interpolator.py +0 -0
  73. {pyield-0.45.2 → pyield-0.46.0}/pyield/ipca/__init__.py +0 -0
  74. {pyield-0.45.2 → pyield-0.46.0}/pyield/py.typed +0 -0
  75. {pyield-0.45.2 → pyield-0.46.0}/pyield/selic/__init__.py +0 -0
  76. {pyield-0.45.2 → pyield-0.46.0}/pyield/selic/probabilities.py +0 -0
  77. {pyield-0.45.2 → pyield-0.46.0}/pyield/tn/__init__.py +0 -0
  78. {pyield-0.45.2 → pyield-0.46.0}/pyield/tn/lft.py +0 -0
  79. {pyield-0.45.2 → pyield-0.46.0}/pyield/tn/ltn.py +0 -0
  80. {pyield-0.45.2 → pyield-0.46.0}/pyield/tn/ntnbprinc.py +0 -0
  81. {pyield-0.45.2 → pyield-0.46.0}/pyield/tn/pre.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyield
3
- Version: 0.45.2
3
+ Version: 0.46.0
4
4
  Summary: A Python library for analysis of fixed income instruments in Brazil
5
5
  Project-URL: Homepage, https://github.com/crdcj/PYield
6
6
  Project-URL: Documentation, https://crdcj.github.io/PYield
@@ -39,9 +39,7 @@ Requires-Python: >=3.12
39
39
  Requires-Dist: fastexcel>=0.19.0
40
40
  Requires-Dist: lxml>=6.0.0
41
41
  Requires-Dist: polars>=1.39.0
42
- Requires-Dist: python-dateutil>=2.8.0
43
42
  Requires-Dist: requests>=2.31.0
44
- Requires-Dist: tenacity>=9.0.0
45
43
  Description-Content-Type: text/markdown
46
44
 
47
45
  [![PyPI version](https://img.shields.io/pypi/v/pyield.svg)](https://pypi.python.org/pypi/pyield)
@@ -0,0 +1 @@
1
+ __version__ = "0.46.0"
@@ -0,0 +1,40 @@
1
+ import polars as pl
2
+
3
+
4
+ def float_br(coluna: str) -> pl.Expr:
5
+ """Converte coluna string no formato numérico brasileiro para Float64.
6
+
7
+ Remove separadores de milhar (.) e troca vírgula decimal (,) por ponto.
8
+ """
9
+ return (
10
+ pl.col(coluna)
11
+ .str.strip_chars()
12
+ .str.replace_all(".", "", literal=True)
13
+ .str.replace(",", ".", literal=True)
14
+ .cast(pl.Float64)
15
+ )
16
+
17
+
18
+ def taxa_br(coluna: str, casas_pct: int = 4) -> pl.Expr:
19
+ """Converte taxa percentual BR (string) para decimal Float64.
20
+
21
+ Args:
22
+ coluna: Nome da coluna com taxa em formato BR (ex.: "12,3456").
23
+ casas_pct: Casas decimais da taxa percentual de entrada. Default 4,
24
+ padrão do mercado brasileiro de renda fixa.
25
+ """
26
+ return (float_br(coluna) / 100).round(casas_pct + 2)
27
+
28
+
29
+ def inteiro_br(coluna: str) -> pl.Expr:
30
+ """Converte coluna string no formato numérico brasileiro para inteiro.
31
+
32
+ Remove separadores de milhar (.) e troca vírgula decimal (,) por ponto,
33
+ depois converte para Int64.
34
+ """
35
+ return float_br(coluna).round(0).cast(pl.Int64)
36
+
37
+
38
+ def inteiro_m(coluna: str) -> pl.Expr:
39
+ """Converte coluna numérica BR em milhares para inteiro (unidades)."""
40
+ return (float_br(coluna) * 1000).round(0).cast(pl.Int64)
@@ -0,0 +1,98 @@
1
+ import functools
2
+ import logging
3
+ import random
4
+ import time
5
+ from collections.abc import Callable
6
+ from typing import ParamSpec, TypeVar
7
+
8
+ from requests import exceptions as rex
9
+
10
+ registro = logging.getLogger(__name__)
11
+
12
+ # Constantes para valores de retry
13
+ _TAMANHO_MAXIMO_EXCECAO = 150
14
+ _HTTP_MUITAS_REQUISICOES = 429
15
+ _HTTP_ERRO_SERVIDOR_MINIMO = 500
16
+ _MAX_TENTATIVAS = 3
17
+ _ESPERA_MINIMA = 1.0
18
+ _ESPERA_MAXIMA = 10.0
19
+ _MULTIPLICADOR_BACKOFF = 2.0
20
+
21
+ _EXCECOES_TRANSITORIAS = (
22
+ rex.Timeout,
23
+ rex.ConnectionError,
24
+ rex.SSLError,
25
+ rex.ChunkedEncodingError,
26
+ )
27
+
28
+ P = ParamSpec("P")
29
+ R = TypeVar("R")
30
+
31
+
32
+ def _logar_antes_espera(
33
+ tentativa: int, excecao: Exception, tempo_espera: float
34
+ ) -> None:
35
+ """Loga uma mensagem antes da espera entre tentativas."""
36
+ texto_excecao = str(excecao).replace("\n", " ")
37
+ texto_excecao_truncado = texto_excecao[:_TAMANHO_MAXIMO_EXCECAO]
38
+ if len(texto_excecao) > _TAMANHO_MAXIMO_EXCECAO:
39
+ texto_excecao_truncado += "..."
40
+
41
+ registro.warning(
42
+ "Tentativa %s falhou com %s: %s Tentando novamente em %.2f segundos...",
43
+ tentativa,
44
+ type(excecao).__name__,
45
+ texto_excecao_truncado,
46
+ tempo_espera,
47
+ )
48
+
49
+
50
+ def _deve_tentar_novamente_por_excecao(excecao: Exception) -> bool:
51
+ """Determina se uma exceção capturada justifica uma nova tentativa."""
52
+ # Erros de rede genéricos são sempre transitórios
53
+ if isinstance(excecao, _EXCECOES_TRANSITORIAS):
54
+ return True
55
+
56
+ # HTTPError: apenas 429 e 5xx são transitórios
57
+ if isinstance(excecao, rex.HTTPError) and excecao.response is not None:
58
+ codigo_status = excecao.response.status_code
59
+ return (
60
+ codigo_status == _HTTP_MUITAS_REQUISICOES
61
+ or codigo_status >= _HTTP_ERRO_SERVIDOR_MINIMO
62
+ )
63
+
64
+ return False
65
+
66
+
67
+ def _calcular_tempo_espera(tentativa: int) -> float:
68
+ """Calcula o tempo de espera com backoff exponencial e jitter."""
69
+ limite_superior = min(
70
+ _ESPERA_MAXIMA,
71
+ max(_ESPERA_MINIMA, _MULTIPLICADOR_BACKOFF * (2 ** (tentativa - 1))),
72
+ )
73
+ return random.uniform(_ESPERA_MINIMA, limite_superior)
74
+
75
+
76
+ def retry_padrao(func: Callable[P, R]) -> Callable[P, R]:
77
+ """Aplica retry com backoff exponencial e jitter para falhas transitórias."""
78
+
79
+ @functools.wraps(func)
80
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
81
+ for tentativa in range(1, _MAX_TENTATIVAS + 1):
82
+ try:
83
+ return func(*args, **kwargs)
84
+ except Exception as excecao:
85
+ if (
86
+ tentativa == _MAX_TENTATIVAS
87
+ or not _deve_tentar_novamente_por_excecao(excecao)
88
+ ):
89
+ raise
90
+
91
+ tempo_espera = _calcular_tempo_espera(tentativa)
92
+ _logar_antes_espera(tentativa, excecao, tempo_espera)
93
+ time.sleep(tempo_espera)
94
+
95
+ msg = "Fluxo de retry inválido: laço encerrado sem retorno nem exceção."
96
+ raise RuntimeError(msg)
97
+
98
+ return wrapper
@@ -2,11 +2,12 @@ import datetime as dt
2
2
  import logging
3
3
 
4
4
  import polars as pl
5
- import polars.selectors as cs
6
5
  import requests
7
6
 
8
7
  import pyield._internal.converters as cv
9
8
  from pyield import bday
9
+ from pyield._internal.br_numbers import taxa_br
10
+ from pyield._internal.cache import ttl_cache
10
11
  from pyield._internal.types import DateLike, any_is_empty
11
12
 
12
13
  VERSAO_API = "1.0018"
@@ -18,51 +19,11 @@ URL_PAGINA_INICIAL = f"{URL_BASE}/taxasOnline.asp"
18
19
  URL_CONSULTA_DADOS = f"{URL_BASE}/exibedados.asp"
19
20
  URL_DOWNLOAD = f"{URL_BASE}/download_dados.asp?extensao=csv"
20
21
 
21
- MAPA_COLUNAS = [
22
- ("Título", "titulo", pl.String),
23
- ("Vencimento", "data_vencimento", pl.String),
24
- ("Código ISIN", "codigo_isin", pl.String),
25
- ("Provedor", "provedor", pl.String),
26
- ("Edital", "edital", pl.String),
27
- ("Horário", "horario", pl.String),
28
- ("Prazo", "prazo", pl.Int64),
29
- ("Lote", "lote", pl.String),
30
- ("Fech D-1", "taxa_indicativa_anterior", pl.Float64),
31
- ("Indicativo Superior", "taxa_limite_superior", pl.Float64),
32
- ("Máxima", "taxa_maxima", pl.Float64),
33
- ("Média", "taxa_media", pl.Float64),
34
- ("Mínima", "taxa_minima", pl.Float64),
35
- ("Indicativo Inferior", "taxa_limite_inferior", pl.Float64),
36
- ("Última", "taxa_ultima", pl.Float64),
37
- ("Oferta Compra", "taxa_compra", pl.Float64),
38
- ("Oferta Venda", "taxa_venda", pl.Float64),
39
- ("Nº de Negócios", "num_negocios", pl.Int64),
40
- ("Quantidade Negociada", "quantidade_negociada", pl.Int64),
41
- ("Volume Negociado (R$)", "volume_negociado", pl.Float64),
42
- ]
43
-
44
- ESQUEMA_API = {csv: tipo for csv, _, tipo in MAPA_COLUNAS}
45
- ALIAS_COLUNAS = {csv: novo for csv, novo, _ in MAPA_COLUNAS}
46
-
47
- # Colunas não selecionadas estão vazias na API.
48
- ORDEM_COLUNAS_FINAL = [
49
- "data_hora_referencia",
50
- "provedor",
51
- "titulo",
52
- "data_vencimento",
53
- "codigo_isin",
54
- "dias_uteis",
55
- "taxa_indicativa_anterior",
56
- "taxa_venda",
57
- "taxa_compra",
58
- "taxa_media",
59
- "taxa_ultima",
60
- ]
61
-
62
22
  logger = logging.getLogger(__name__)
63
23
 
64
24
 
65
- def _buscar_dados_url(data_referencia: str) -> str:
25
+ @ttl_cache()
26
+ def _buscar_dados_url(data_referencia: str) -> bytes:
66
27
  cabecalhos = {
67
28
  "Referer": URL_PAGINA_INICIAL,
68
29
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", # noqa
@@ -115,72 +76,52 @@ def _buscar_dados_url(data_referencia: str) -> str:
115
76
  f"{data_referencia}."
116
77
  )
117
78
  response_download.encoding = "iso-8859-1"
118
- return response_download.text
79
+ return response_download.text.encode("utf-8")
119
80
  except requests.exceptions.RequestException as e:
120
81
  logger.error(f"Erro ao buscar dados para a data '{data_referencia}': {e}")
121
82
  raise
122
83
 
123
84
 
124
- def _extrair_data_referencia_e_csv_limpo(csv_data: str) -> tuple[dt.date, bytes]:
125
- """Extrai a data de referência do CSV e devolve o conteúdo pronto para leitura."""
126
- linhas_csv = csv_data.strip().splitlines()
127
- if not linhas_csv:
128
- raise ValueError("CSV vazio ou inválido.")
129
-
130
- if ":" not in linhas_csv[0]:
131
- raise ValueError("Cabeçalho do CSV inválido: data de referência ausente.")
132
-
133
- data_ref_str = linhas_csv[0].split(":")[1].strip()
134
- data_ref = dt.datetime.strptime(data_ref_str, "%d/%m/%Y").date()
135
-
136
- # O CSV da Anbima contém ';' extra no fim do cabeçalho.
137
- # Remove apenas esse caractere para garantir a leitura correta.
138
- csv_limpo = (
139
- "\n".join(linhas_csv[1:])
140
- .replace("Volume Negociado (R$);", "Volume Negociado (R$)")
141
- .encode()
142
- )
85
+ def _ler_csv_bruto(csv_bruto: bytes) -> pl.DataFrame:
86
+ """Lê o CSV bruto da Anbima e devolve DataFrame com todas as colunas string."""
87
+ return pl.read_csv(
88
+ csv_bruto,
89
+ separator=";",
90
+ infer_schema=False,
91
+ skip_rows=1,
92
+ ).with_columns(pl.all().str.strip_chars())
143
93
 
144
- return data_ref, csv_limpo
145
94
 
95
+ def _processar_csv(data_ref: dt.date, df: pl.DataFrame) -> pl.DataFrame:
96
+ """Converte o DataFrame bruto em DataFrame Polars estruturado."""
146
97
 
147
- def _processar_csv(data_ref: dt.date, csv_limpo: bytes) -> pl.DataFrame:
148
- """Converte o CSV limpo em um DataFrame Polars estruturado."""
149
- df = (
150
- pl.read_csv(
151
- csv_limpo,
152
- separator=";",
153
- decimal_comma=True,
154
- schema=ESQUEMA_API,
155
- )
156
- .rename(ALIAS_COLUNAS)
157
- .with_columns(cs.string().str.strip_chars()) # Remove espaços em branco
158
- .with_columns(
159
- pl.col("data_vencimento").str.to_date(format="%d/%m/%Y"),
160
- data_referencia=data_ref, # Adiciona a coluna de data de referência
161
- # Ajusta o horário para 12:00:00 quando o provedor for "ANBIMA 12H"
162
- horario=pl.when(pl.col("provedor") == "ANBIMA 12H")
98
+ return (
99
+ df.with_columns(
100
+ data_vencimento=pl.col("Vencimento").str.to_date(format="%d/%m/%Y"),
101
+ horario=pl.when(pl.col("Provedor") == "ANBIMA 12H")
163
102
  .then(dt.time(12, 0))
164
- .otherwise(pl.col("horario").str.to_time(format="%H:%M:%S")),
165
- taxa_media=pl.mean_horizontal("taxa_compra", "taxa_venda"),
103
+ .otherwise(pl.col("Horário").str.to_time(format="%H:%M:%S")),
104
+ taxa_venda=taxa_br("Oferta Venda"),
105
+ taxa_compra=taxa_br("Oferta Compra"),
166
106
  )
167
- .with_columns(
168
- bday.count_expr("data_referencia", "data_vencimento").alias("dias_uteis"),
169
- # Converte as colunas de taxa de percentual para decimal
170
- # São 6 casas decimais no máximo.
171
- # Arredondar na 8a para minimizar erros de ponto flutuante.
172
- cs.starts_with("taxa_").truediv(100).round(8),
173
- pl.col("data_referencia")
174
- .dt.combine(pl.col("horario"))
175
- .dt.replace_time_zone("America/Sao_Paulo")
176
- .alias("data_hora_referencia"),
107
+ .select(
108
+ data_hora_referencia=pl.lit(data_ref).dt.combine(pl.col("horario")),
109
+ provedor=pl.col("Provedor"),
110
+ titulo=pl.col("Título"),
111
+ data_vencimento=pl.col("data_vencimento"),
112
+ codigo_isin=pl.col("Código ISIN"),
113
+ dias_uteis=bday.count_expr(data_ref, "data_vencimento"),
114
+ taxa_indicativa_anterior=taxa_br("Fech D-1"),
115
+ taxa_indicativa_superior=taxa_br("Indicativo Superior"),
116
+ taxa_indicativa_inferior=taxa_br("Indicativo Inferior"),
117
+ taxa_venda=pl.col("taxa_venda"),
118
+ taxa_compra=pl.col("taxa_compra"),
119
+ taxa_media=pl.mean_horizontal("taxa_compra", "taxa_venda"),
120
+ taxa_ultima=taxa_br("Última"),
177
121
  )
178
- .select(ORDEM_COLUNAS_FINAL)
179
122
  .sort("titulo", "data_vencimento", "data_hora_referencia")
180
123
  )
181
124
 
182
- return df
183
-
184
125
 
185
126
  def tpf_difusao(data_referencia: DateLike) -> pl.DataFrame:
186
127
  """
@@ -197,14 +138,15 @@ def tpf_difusao(data_referencia: DateLike) -> pl.DataFrame:
197
138
  ValueError: Em resposta inválida da API ou erro de parsing do CSV.
198
139
 
199
140
  Output Columns:
200
- - data_hora_referencia (datetime[America/Sao_Paulo]): Data e hora de
201
- referência da taxa no fuso horário de São Paulo.
141
+ - data_hora_referencia (Datetime): Data e hora de referência da taxa.
202
142
  - provedor (string): Provedor dos dados.
203
143
  - titulo (string): Nome do título (ex: LFT, LTN).
204
144
  - data_vencimento (date): Data de vencimento do título.
205
145
  - codigo_isin (string): Código ISIN do título.
206
146
  - dias_uteis (int): Dias úteis entre a data de referência e o vencimento.
207
147
  - taxa_indicativa_anterior (float): Taxa indicativa de fechamento D-1 (decimal).
148
+ - taxa_indicativa_superior (float): Limite superior da banda indicativa (decimal).
149
+ - taxa_indicativa_inferior (float): Limite inferior da banda indicativa (decimal).
208
150
  - taxa_venda (float): Taxa de oferta de venda (Ask rate) (decimal).
209
151
  - taxa_compra (float): Taxa de oferta de compra (Bid rate) (decimal).
210
152
  - taxa_media (float): Média entre a taxa de compra e venda (decimal).
@@ -216,7 +158,6 @@ def tpf_difusao(data_referencia: DateLike) -> pl.DataFrame:
216
158
  if not bday.is_business_day(data):
217
159
  return pl.DataFrame()
218
160
  data_str = data.strftime("%d/%m/%Y")
219
- csv_data = _buscar_dados_url(data_str)
220
-
221
- data_ref, csv_limpo = _extrair_data_referencia_e_csv_limpo(csv_data)
222
- return _processar_csv(data_ref, csv_limpo)
161
+ csv_bruto = _buscar_dados_url(data_str)
162
+ df = _ler_csv_bruto(csv_bruto)
163
+ return _processar_csv(data, df)
@@ -0,0 +1,83 @@
1
+ import datetime as dt
2
+
3
+ import polars as pl
4
+ import requests
5
+
6
+ from pyield._internal.br_numbers import float_br, taxa_br
7
+ from pyield._internal.cache import ttl_cache
8
+ from pyield._internal.retry import retry_padrao
9
+
10
+ URL_ETTJ_INTRADAY = (
11
+ "https://www.anbima.com.br/informacoes/curvas-intradiarias/cIntra-down.asp"
12
+ )
13
+
14
+ # Dados ETTJ: taxas com 4 casas decimais em percentual (ex.: 14,2644%).
15
+
16
+
17
+ @ttl_cache()
18
+ @retry_padrao
19
+ def _buscar_texto_intraday() -> str:
20
+ carga_requisicao = {"Dt_Ref": "", "saida": "csv"}
21
+ resposta = requests.post(URL_ETTJ_INTRADAY, data=carga_requisicao, timeout=10)
22
+ resposta.raise_for_status()
23
+ resposta.encoding = "latin1"
24
+ return resposta.text
25
+
26
+
27
+ def _extrair_data_e_tabelas(texto: str) -> tuple[dt.date, str, str]:
28
+ """Separa o texto bruto em data de referência e as duas tabelas CSV."""
29
+ # Cada seção (PREFIXADOS e IPCA) é separada por linha em branco
30
+ secao_pre, secao_ipca = texto.strip().replace("\r\n", "\n").split("\n\n")
31
+
32
+ # Estrutura de cada seção: título, data, header+dados
33
+ linhas_pre = secao_pre.splitlines()
34
+ data_ref = dt.datetime.strptime(linhas_pre[1], "%d/%m/%Y").date()
35
+ tabela_pre = "\n".join(linhas_pre[2:])
36
+
37
+ linhas_ipca = secao_ipca.splitlines()
38
+ tabela_ipca = "\n".join(linhas_ipca[2:])
39
+
40
+ return data_ref, tabela_pre, tabela_ipca
41
+
42
+
43
+ def _parsear_tabela_intraday(texto: str, nome_taxa: str) -> pl.DataFrame:
44
+ return pl.read_csv(texto.encode(), separator=";", infer_schema=False).select(
45
+ vertice=float_br("Vertices").cast(pl.Int64),
46
+ **{nome_taxa: taxa_br("D0")},
47
+ )
48
+
49
+
50
+ def intraday_ettj() -> pl.DataFrame:
51
+ """Obtém e processa a curva de juros intradiária da ANBIMA.
52
+
53
+ Busca os dados mais recentes da curva de juros intradiária publicada pela
54
+ ANBIMA, contendo taxas reais (indexadas ao IPCA), taxas nominais e inflação
55
+ implícita em diversos vértices. A curva é publicada por volta das 12h30 BRT.
56
+
57
+ Returns:
58
+ pl.DataFrame: DataFrame com os dados intradiários da ETTJ.
59
+
60
+ Output Columns:
61
+ - data_referencia (Date): data de referência da curva de juros.
62
+ - vertice (Int64): vértice em dias úteis.
63
+ - taxa_nominal (Float64): taxa de juros nominal zero-cupom.
64
+ - taxa_real (Float64): taxa de juros real zero-cupom (indexada ao IPCA).
65
+ - inflacao_implicita (Float64): taxa de inflação implícita (breakeven).
66
+
67
+ Notes:
68
+ Todas as taxas são expressas em formato decimal (ex: 0.12 para 12%).
69
+ """
70
+ texto_api = _buscar_texto_intraday()
71
+
72
+ data_ref, tabela_pre, tabela_ipca = _extrair_data_e_tabelas(texto_api)
73
+
74
+ df_pre = _parsear_tabela_intraday(tabela_pre, "taxa_nominal")
75
+ df_ipca = _parsear_tabela_intraday(tabela_ipca, "taxa_real")
76
+ expr_inflacao_impl = (pl.col("taxa_nominal") + 1) / (pl.col("taxa_real") + 1) - 1
77
+ df = df_pre.join(df_ipca, on="vertice", how="right").with_columns(
78
+ data_referencia=data_ref,
79
+ inflacao_implicita=expr_inflacao_impl.round(6),
80
+ )
81
+ return df.select(
82
+ "data_referencia", "vertice", "taxa_nominal", "taxa_real", "inflacao_implicita"
83
+ )
@@ -0,0 +1,76 @@
1
+ import datetime as dt
2
+
3
+ import polars as pl
4
+ import requests
5
+
6
+ from pyield._internal.br_numbers import float_br, taxa_br
7
+ from pyield._internal.cache import ttl_cache
8
+ from pyield._internal.retry import retry_padrao
9
+
10
+ URL_ULTIMA_ETTJ = "https://www.anbima.com.br/informacoes/est-termo/CZ-down.asp"
11
+
12
+ # Dados ETTJ: taxas com 4 casas decimais em percentual (ex.: 14,2644%).
13
+
14
+
15
+ @ttl_cache()
16
+ @retry_padrao
17
+ def _buscar_texto_ultima_ettj() -> str:
18
+ """Busca o texto bruto da curva de juros na ANBIMA."""
19
+ carga_requisicao = {
20
+ "Idioma": "PT",
21
+ "Dt_Ref": "",
22
+ "saida": "csv",
23
+ }
24
+ resposta = requests.post(URL_ULTIMA_ETTJ, data=carga_requisicao, timeout=10)
25
+ resposta.raise_for_status()
26
+ resposta.encoding = "latin1"
27
+ return resposta.text
28
+
29
+
30
+ def _extrair_data_e_tabela(texto: str) -> tuple[dt.date, str]:
31
+ """Separa o texto bruto em data de referência e a tabela CSV principal."""
32
+ # Seções separadas por linha em branco: betas, ETTJ, circular, erros
33
+ secoes = texto.strip().replace("\r\n", "\n").split("\n\n")
34
+ # A data está na primeira linha da primeira seção (ex.: "20/03/2026;Beta 1;...")
35
+ data_str = secoes[0].splitlines()[0][:10]
36
+ data_ref = dt.datetime.strptime(data_str, "%d/%m/%Y").date()
37
+ # A tabela ETTJ é a segunda seção; pular o título (primeira linha)
38
+ linhas_ettj = secoes[1].splitlines()
39
+ tabela = "\n".join(linhas_ettj[1:])
40
+ return data_ref, tabela
41
+
42
+
43
+ def _processar_tabela(texto: str, data_referencia: dt.date) -> pl.DataFrame:
44
+ """Lê o CSV e converte para DataFrame com taxas decimais."""
45
+ return pl.read_csv(texto.encode(), separator=";", infer_schema=False).select(
46
+ data_referencia=data_referencia,
47
+ vertice=float_br("Vertices").cast(pl.Int64),
48
+ taxa_nominal=taxa_br("ETTJ PREF"),
49
+ taxa_real=taxa_br("ETTJ IPCA"),
50
+ inflacao_implicita=taxa_br("Inflação Implícita"),
51
+ )
52
+
53
+
54
+ def last_ettj() -> pl.DataFrame:
55
+ """Obtém e processa a última curva de juros (ETTJ) publicada pela ANBIMA.
56
+
57
+ Busca os dados mais recentes da curva de juros de fechamento publicada pela
58
+ ANBIMA, contendo taxas reais (indexadas ao IPCA), taxas nominais e inflação
59
+ implícita em diversos vértices.
60
+
61
+ Returns:
62
+ pl.DataFrame: DataFrame com os dados da ETTJ de fechamento.
63
+
64
+ Output Columns:
65
+ - data_referencia (Date): data de referência da curva de juros.
66
+ - vertice (Int64): vértice em dias úteis.
67
+ - taxa_nominal (Float64): taxa de juros nominal zero-cupom.
68
+ - taxa_real (Float64): taxa de juros real zero-cupom (indexada ao IPCA).
69
+ - inflacao_implicita (Float64): taxa de inflação implícita (breakeven).
70
+
71
+ Notes:
72
+ Todas as taxas são expressas em formato decimal (ex: 0.12 para 12%).
73
+ """
74
+ texto = _buscar_texto_ultima_ettj()
75
+ data_ref, tabela = _extrair_data_e_tabela(texto)
76
+ return _processar_tabela(tabela, data_ref)
@@ -0,0 +1,129 @@
1
+ from typing import Literal
2
+
3
+ import polars as pl
4
+ import requests
5
+
6
+ from pyield._internal.br_numbers import float_br, inteiro_m, taxa_br
7
+ from pyield._internal.cache import ttl_cache
8
+ from pyield._internal.retry import retry_padrao
9
+
10
+ TiposIMA = Literal[
11
+ "IRF-M 1",
12
+ "IRF-M 1+",
13
+ "IRF-M",
14
+ "IMA-B 5",
15
+ "IMA-B 5+",
16
+ "IMA-B",
17
+ "IMA-S",
18
+ "IMA-GERAL-EX-C",
19
+ "IMA-GERAL",
20
+ ]
21
+
22
+
23
+ URL_ULTIMO_IMA = "https://www.anbima.com.br/informacoes/ima/arqs/ima_completo.txt"
24
+
25
+
26
+ @ttl_cache()
27
+ @retry_padrao
28
+ def _buscar_texto_ultimo_ima() -> str:
29
+ resposta = requests.get(URL_ULTIMO_IMA, timeout=3)
30
+ resposta.raise_for_status()
31
+ resposta.encoding = "latin1"
32
+ return resposta.text
33
+
34
+
35
+ def _parsear_df(texto: str) -> pl.DataFrame:
36
+ texto_csv = texto.split("2@COMPOSIÇÃO DE CARTEIRA")[1].strip()
37
+ return pl.read_csv(
38
+ texto_csv.encode(),
39
+ separator="@",
40
+ infer_schema=False,
41
+ null_values="--",
42
+ )
43
+
44
+
45
+ def _processar_df(df: pl.DataFrame) -> pl.DataFrame:
46
+ mduration_expr = pl.col("duration") / (1 + pl.col("taxa_indicativa"))
47
+ dv01_mercado_expr = pl.col("dv01") * pl.col("quantidade_mercado")
48
+ df = (
49
+ df.with_columns(
50
+ pu=float_br("PU (R$)"),
51
+ duration=pl.col("Duration (d.u.)").cast(pl.Int64).truediv(252),
52
+ taxa_indicativa=taxa_br("Taxa Indicativa (% a.a.)", 4),
53
+ quantidade_mercado=inteiro_m("Quantidade (1.000 títulos)"),
54
+ )
55
+ .with_columns(dv01=0.0001 * pl.col("pu") * mduration_expr)
56
+ .select(
57
+ data_referencia=pl.col("Data de Referência").str.to_date("%d/%m/%Y"),
58
+ indice=pl.col("INDICE"),
59
+ titulo=pl.col("Títulos"),
60
+ data_vencimento=pl.col("Data de Vencimento").str.to_date("%d/%m/%Y"),
61
+ codigo_selic=pl.col("Código SELIC").cast(pl.Int64),
62
+ codigo_isin=pl.col("Código ISIN"),
63
+ dias_uteis=pl.col("Prazo (d.u.)").cast(pl.Int64),
64
+ duration=pl.col("duration"),
65
+ taxa_indicativa=pl.col("taxa_indicativa"),
66
+ pu=pl.col("pu"),
67
+ pu_juros=float_br("PU de Juros (R$)"),
68
+ dv01=pl.col("dv01"),
69
+ pmr=float_br("PMR"),
70
+ peso=float_br("Peso (%)"),
71
+ convexidade=float_br("Convexidade"),
72
+ quantidade_teorica=float_br("Quantidade Teórica (1.000 títulos)"),
73
+ operacoes=pl.col("Número de Operações *").cast(pl.Int64),
74
+ quantidade_negociada=inteiro_m("Quant. Negociada (1.000 títulos) *"),
75
+ valor_negociado=inteiro_m("Valor Negociado (R$ mil) *"),
76
+ dv01_mercado=dv01_mercado_expr.round(0).cast(pl.Int64),
77
+ quantidade_mercado=pl.col("quantidade_mercado"),
78
+ valor_mercado=inteiro_m("Carteira a Mercado (R$ mil)"),
79
+ )
80
+ )
81
+ return df
82
+
83
+
84
+ def last_ima(ima_type: TiposIMA | None = None) -> pl.DataFrame:
85
+ """Obtém os últimos dados de composição de carteira IMA disponíveis na ANBIMA.
86
+
87
+ Busca e processa os dados do arquivo IMA completo publicado pela ANBIMA,
88
+ retornando um DataFrame estruturado.
89
+
90
+ Args:
91
+ ima_type (str, optional): Tipo de índice IMA para filtrar os dados.
92
+ Se None, retorna todos os índices. Padrão é None.
93
+
94
+ Returns:
95
+ pl.DataFrame: DataFrame com os dados do IMA.
96
+
97
+ Output Columns:
98
+ - data_referencia (Date): data de referência.
99
+ - indice (String): índice IMA (ex: 'IMA-B', 'IRF-M').
100
+ - titulo (String): título (ex: 'LTN', 'NTN-B').
101
+ - data_vencimento (Date): data de vencimento do título.
102
+ - codigo_selic (Int64): código do título no sistema SELIC.
103
+ - codigo_isin (String): código ISIN.
104
+ - dias_uteis (Int64): dias úteis até o vencimento.
105
+ - duration (Float64): duration do título em anos úteis (252 d.u./ano).
106
+ - taxa_indicativa (Float64): taxa indicativa em decimal (ex: 0.10 para 10%).
107
+ - pu (Float64): preço unitário (PU) em R$.
108
+ - pu_juros (Float64): PU de juros em R$.
109
+ - dv01 (Float64): DV01 em R$.
110
+ - pmr (Float64): prazo médio de repactuação.
111
+ - peso (Float64): peso do título no índice (%).
112
+ - convexidade (Float64): convexidade do título.
113
+ - quantidade_teorica (Float64): quantidade teórica (em 1.000 títulos).
114
+ - operacoes (Int64): número de operações.
115
+ - quantidade_negociada (Int64): quantidade negociada (unidades).
116
+ - valor_negociado (Int64): valor negociado em R$.
117
+ - dv01_mercado (Int64): DV01 de mercado em R$.
118
+ - quantidade_mercado (Int64): quantidade em carteira (unidades).
119
+ - valor_mercado (Int64): valor de mercado em R$.
120
+
121
+ Examples:
122
+ >>> yd.anbima.last_ima("IMA-B") # doctest: +SKIP
123
+ """
124
+ texto_ima = _buscar_texto_ultimo_ima()
125
+ df = _parsear_df(texto_ima)
126
+ df = _processar_df(df)
127
+ if ima_type:
128
+ df = df.filter(pl.col("indice") == ima_type)
129
+ return df.sort("indice", "titulo", "data_vencimento")