PYield 0.45.1__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.
- {pyield-0.45.1 → pyield-0.46.0}/PKG-INFO +1 -3
- pyield-0.46.0/pyield/__about__.py +1 -0
- pyield-0.46.0/pyield/_internal/br_numbers.py +40 -0
- pyield-0.46.0/pyield/_internal/retry.py +98 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/anbima/difusao.py +42 -101
- pyield-0.46.0/pyield/anbima/ettj_intraday.py +83 -0
- pyield-0.46.0/pyield/anbima/ettj_last.py +76 -0
- pyield-0.46.0/pyield/anbima/ima.py +129 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/anbima/imaq.py +22 -36
- pyield-0.46.0/pyield/b3/di_over.py +77 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/b3/intraday_derivatives.py +5 -12
- {pyield-0.45.1 → pyield-0.46.0}/pyield/b3/price_report.py +19 -15
- pyield-0.46.0/pyield/bc/auction.py +395 -0
- pyield-0.46.0/pyield/bc/ptax_api.py +173 -0
- pyield-0.46.0/pyield/bc/rates.py +315 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/bc/repo.py +31 -104
- pyield-0.46.0/pyield/bc/trades_intraday.py +153 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/bc/trades_monthly.py +61 -84
- {pyield-0.45.1 → pyield-0.46.0}/pyield/bc/vna.py +12 -4
- pyield-0.46.0/pyield/ipca/historical.py +214 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/ipca/projected.py +1 -1
- {pyield-0.45.1 → pyield-0.46.0}/pyield/rmd.py +33 -79
- {pyield-0.45.1 → pyield-0.46.0}/pyield/selic/cpm.py +2 -6
- {pyield-0.45.1 → pyield-0.46.0}/pyield/tn/auctions.py +34 -29
- pyield-0.46.0/pyield/tn/benchmark.py +137 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/tn/lft.py +18 -9
- {pyield-0.45.1 → pyield-0.46.0}/pyield/tn/ltn.py +20 -2
- {pyield-0.45.1 → pyield-0.46.0}/pyield/tn/ntnb.py +22 -4
- {pyield-0.45.1 → pyield-0.46.0}/pyield/tn/ntnb1.py +1 -2
- {pyield-0.45.1 → pyield-0.46.0}/pyield/tn/ntnc.py +19 -4
- {pyield-0.45.1 → pyield-0.46.0}/pyield/tn/ntnf.py +23 -4
- {pyield-0.45.1 → pyield-0.46.0}/pyield/tn/utils.py +8 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyproject.toml +1 -3
- pyield-0.45.1/pyield/__about__.py +0 -1
- pyield-0.45.1/pyield/_internal/retry.py +0 -75
- pyield-0.45.1/pyield/anbima/ettj_intraday.py +0 -148
- pyield-0.45.1/pyield/anbima/ettj_last.py +0 -117
- pyield-0.45.1/pyield/anbima/ima.py +0 -193
- pyield-0.45.1/pyield/b3/di_over.py +0 -78
- pyield-0.45.1/pyield/bc/auction.py +0 -464
- pyield-0.45.1/pyield/bc/ptax_api.py +0 -258
- pyield-0.45.1/pyield/bc/rates.py +0 -408
- pyield-0.45.1/pyield/bc/trades_intraday.py +0 -239
- pyield-0.45.1/pyield/ipca/historical.py +0 -197
- pyield-0.45.1/pyield/tn/benchmark.py +0 -158
- {pyield-0.45.1 → pyield-0.46.0}/.gitignore +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/LICENSE +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/README.md +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/__init__.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/_internal/__init__.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/_internal/cache.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/_internal/converters.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/_internal/data_cache.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/_internal/types.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/anbima/__init__.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/anbima/tpf.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/b3/__init__.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/b3/_contracts.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/b3/_validar_pregao.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/b3/di1.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/b3/futures/__init__.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/b3/futures/common.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/b3/futures/historical.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/b3/futures/intraday.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/bc/__init__.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/bc/copom.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/bday/__init__.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/bday/core.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/bday/holidays/br_holidays_new.txt +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/bday/holidays/br_holidays_old.txt +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/bday/holidays/brholidays.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/clock.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/fwd.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/interpolator.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/ipca/__init__.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/py.typed +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/selic/__init__.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/selic/probabilities.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/tn/__init__.py +0 -0
- {pyield-0.45.1 → pyield-0.46.0}/pyield/tn/ntnbprinc.py +0 -0
- {pyield-0.45.1 → 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.
|
|
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
|
[](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
|
-
|
|
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
|
|
125
|
-
"""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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("
|
|
165
|
-
|
|
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
|
-
.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 (
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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")
|