pyield 0.51.0__tar.gz → 0.51.2__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.51.0 → pyield-0.51.2}/PKG-INFO +1 -1
- {pyield-0.51.0 → pyield-0.51.2}/pyield/__init__.py +2 -1
- {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/converters.py +4 -4
- {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/types.py +9 -9
- {pyield-0.51.0 → pyield-0.51.2}/pyield/anbima/imaq.py +18 -6
- {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/boletim.py +44 -28
- {pyield-0.51.0 → pyield-0.51.2}/pyield/du/core.py +3 -3
- {pyield-0.51.0 → pyield-0.51.2}/pyield/futuro/contratos.py +3 -3
- {pyield-0.51.0 → pyield-0.51.2}/pyield/futuro/di1.py +37 -7
- {pyield-0.51.0 → pyield-0.51.2}/pyield/futuro/historico.py +5 -5
- {pyield-0.51.0 → pyield-0.51.2}/pyield/futuro/intradia.py +5 -3
- pyield-0.51.2/pyield/fwd.py +460 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/interpolador.py +7 -3
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ltn.py +5 -8
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ntnb.py +11 -16
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ntnf.py +4 -4
- {pyield-0.51.0 → pyield-0.51.2}/pyproject.toml +5 -1
- pyield-0.51.0/pyield/fwd.py +0 -292
- {pyield-0.51.0 → pyield-0.51.2}/LICENSE +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/README.md +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/br_numbers.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/cache.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/data_cache.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/retry.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/anbima/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/anbima/mercado_secundario.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/_contratos.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/_validar_pregao.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/derivativos_intradia.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/di_over.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/_olinda.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/leiloes.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/lft.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/sgs.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/tpf_intradia.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/tpf_mensal.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/du/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/du/feriados/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/du/feriados/feriados_antigos_br.txt +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/du/feriados/feriados_br.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/du/feriados/feriados_novos_br.txt +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/futuro/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/ipca/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/ipca/historico.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/ipca/projetado.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/py.typed +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/relogio.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/selic/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/selic/compromissada.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/selic/copom.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/selic/cpm.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/selic/probabilities.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/lft.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ntnb1.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ntnbprinc.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ntnc.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/benchmark.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/leiloes.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/pre.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/rmd/__init__.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/rmd/_aba_1_3.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/rmd/_aba_2_1.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/rmd/_common.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/rmd/_download.py +0 -0
- {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/utils.py +0 -0
|
@@ -12,7 +12,7 @@ from pyield.futuro import di1
|
|
|
12
12
|
# durante a inicialização do pacote.
|
|
13
13
|
from pyield import selic, tpf
|
|
14
14
|
from pyield.bc.sgs import ptax, ptax_serie
|
|
15
|
-
from pyield.fwd import forward, forwards
|
|
15
|
+
from pyield.fwd import forward, forwards, forwards_expr
|
|
16
16
|
from pyield.interpolador import Interpolador
|
|
17
17
|
from pyield.relogio import agora, hoje
|
|
18
18
|
from pyield.tpf import lft, ltn, ntnb, ntnb1, ntnbprinc, ntnc, ntnf
|
|
@@ -31,6 +31,7 @@ __all__ = [
|
|
|
31
31
|
"du",
|
|
32
32
|
"forward",
|
|
33
33
|
"forwards",
|
|
34
|
+
"forwards_expr",
|
|
34
35
|
"futuro",
|
|
35
36
|
"hoje",
|
|
36
37
|
"Interpolador",
|
|
@@ -162,12 +162,12 @@ def converter_datas(
|
|
|
162
162
|
2024-12-27
|
|
163
163
|
]
|
|
164
164
|
"""
|
|
165
|
-
if
|
|
166
|
-
eh_escalar = True
|
|
167
|
-
serie = pl.Series(values=[dates])
|
|
168
|
-
else:
|
|
165
|
+
if types.is_array_like(dates):
|
|
169
166
|
eh_escalar = False
|
|
170
167
|
serie = pl.Series(values=dates)
|
|
168
|
+
else:
|
|
169
|
+
eh_escalar = True
|
|
170
|
+
serie = pl.Series(values=[dates])
|
|
171
171
|
|
|
172
172
|
serie = (
|
|
173
173
|
pl.DataFrame({"dates": serie}, nan_to_null=True)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import datetime as dt
|
|
2
2
|
import math
|
|
3
|
-
from collections.abc import
|
|
4
|
-
from typing import Any, TypeAlias
|
|
3
|
+
from collections.abc import Sized
|
|
4
|
+
from typing import Any, TypeAlias, TypeGuard
|
|
5
5
|
|
|
6
6
|
import polars as pl
|
|
7
7
|
|
|
@@ -32,18 +32,18 @@ def any_is_empty(*args: Any) -> bool:
|
|
|
32
32
|
return any(_is_empty(arg) for arg in args)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def
|
|
36
|
-
"""Verifica se o argumento
|
|
37
|
-
return isinstance(arg,
|
|
35
|
+
def is_array_like(arg: Any) -> TypeGuard[ArrayLike]:
|
|
36
|
+
"""Verifica se o argumento segue o contrato interno de ``ArrayLike``."""
|
|
37
|
+
return isinstance(arg, (list, tuple, pl.Series))
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def
|
|
41
|
-
"""Verifica se algum dos argumentos fornecidos é
|
|
40
|
+
def any_is_array_like(*args: Any) -> bool:
|
|
41
|
+
"""Verifica se algum dos argumentos fornecidos é ``ArrayLike``.
|
|
42
42
|
|
|
43
43
|
Args:
|
|
44
44
|
*args: Uma lista variável de argumentos de qualquer tipo.
|
|
45
45
|
|
|
46
46
|
Returns:
|
|
47
|
-
bool: True se algum argumento for
|
|
47
|
+
bool: True se algum argumento for ``ArrayLike``, caso contrário False.
|
|
48
48
|
"""
|
|
49
|
-
return any(
|
|
49
|
+
return any(is_array_like(arg) for arg in args)
|
|
@@ -11,7 +11,7 @@ import datetime as dt
|
|
|
11
11
|
import polars as pl
|
|
12
12
|
import polars.selectors as ps
|
|
13
13
|
import requests
|
|
14
|
-
from lxml.html import HTMLParser
|
|
14
|
+
from lxml.html import HtmlElement, HTMLParser
|
|
15
15
|
from lxml.html import fromstring as html_fromstring
|
|
16
16
|
|
|
17
17
|
import pyield._internal.converters as cv
|
|
@@ -47,6 +47,15 @@ def _normalizar_nome_coluna(texto: str) -> str:
|
|
|
47
47
|
return " ".join(texto.strip().split())
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def _xpath_elementos(elemento: HtmlElement, expressao: str) -> list[HtmlElement]:
|
|
51
|
+
resultado = elemento.xpath(expressao)
|
|
52
|
+
if not isinstance(resultado, list) or not all(
|
|
53
|
+
isinstance(item, HtmlElement) for item in resultado
|
|
54
|
+
):
|
|
55
|
+
raise TypeError("XPath deveria retornar uma lista de elementos HTML.")
|
|
56
|
+
return resultado
|
|
57
|
+
|
|
58
|
+
|
|
50
59
|
def _parsear_tabelas_html(html_content: bytes) -> pl.DataFrame:
|
|
51
60
|
"""Parseia tabelas HTML com lxml e retorna DataFrame via read_csv.
|
|
52
61
|
|
|
@@ -58,19 +67,22 @@ def _parsear_tabelas_html(html_content: bytes) -> pl.DataFrame:
|
|
|
58
67
|
parser = HTMLParser(encoding="iso-8859-1")
|
|
59
68
|
tree = html_fromstring(html_content, parser=parser)
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
if not isinstance(tree, HtmlElement):
|
|
71
|
+
raise TypeError("HTML deveria ser parseado como elemento HTML.")
|
|
72
|
+
|
|
73
|
+
nested_tables = _xpath_elementos(tree, "//table[@width='100%'][parent::td]")
|
|
62
74
|
|
|
63
75
|
linhas = []
|
|
64
76
|
nomes_colunas = None
|
|
65
77
|
|
|
66
|
-
for table in nested_tables:
|
|
67
|
-
headers = table
|
|
78
|
+
for table in nested_tables:
|
|
79
|
+
headers = _xpath_elementos(table, ".//thead//th")
|
|
68
80
|
if not nomes_colunas:
|
|
69
81
|
nomes_colunas = [_normalizar_nome_coluna(h.text_content()) for h in headers]
|
|
70
82
|
|
|
71
|
-
data_rows = table
|
|
83
|
+
data_rows = _xpath_elementos(table, ".//tbody//tr[td]")
|
|
72
84
|
for row in data_rows:
|
|
73
|
-
cells = row
|
|
85
|
+
cells = _xpath_elementos(row, ".//td")
|
|
74
86
|
if len(cells) != len(nomes_colunas):
|
|
75
87
|
continue
|
|
76
88
|
linhas.append("\t".join(c.text_content().strip() for c in cells))
|
|
@@ -97,7 +97,7 @@ COLUNAS_PRICE_REPORT: list[tuple[str, str, type[pl.DataType]]] = [
|
|
|
97
97
|
]
|
|
98
98
|
|
|
99
99
|
# Schema completo: nome_xml → tipo_polars. Garante ordem e tipagem constante.
|
|
100
|
-
SCHEMA_PRICE_REPORT = {nome: tipo for _, nome, tipo in COLUNAS_PRICE_REPORT}
|
|
100
|
+
SCHEMA_PRICE_REPORT = pl.Schema({nome: tipo for _, nome, tipo in COLUNAS_PRICE_REPORT})
|
|
101
101
|
|
|
102
102
|
logger = logging.getLogger(__name__)
|
|
103
103
|
_SESSAO = requests.Session()
|
|
@@ -146,7 +146,32 @@ def baixar_zip(data: DateLike, boletim_completo: bool = False) -> bytes:
|
|
|
146
146
|
return resposta.content
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
def
|
|
149
|
+
def _zip_tem_xml_valido(conteudo_zip: bytes) -> bool: # noqa: PLR0911
|
|
150
|
+
with zipfile.ZipFile(io.BytesIO(conteudo_zip), "r") as zip_externo:
|
|
151
|
+
nomes = zip_externo.namelist()
|
|
152
|
+
if not nomes:
|
|
153
|
+
logger.debug("ZIP externo do boletim está vazio.")
|
|
154
|
+
return False
|
|
155
|
+
if zip_externo.testzip() is not None:
|
|
156
|
+
logger.debug("ZIP externo do boletim contém arquivo corrompido.")
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
conteudo_interno = zip_externo.read(nomes[0])
|
|
160
|
+
with zipfile.ZipFile(io.BytesIO(conteudo_interno), "r") as zip_interno:
|
|
161
|
+
nomes_xml = [
|
|
162
|
+
nome for nome in zip_interno.namelist() if nome.endswith(".xml")
|
|
163
|
+
]
|
|
164
|
+
if not nomes_xml:
|
|
165
|
+
logger.debug("ZIP interno do boletim não contém XML.")
|
|
166
|
+
return False
|
|
167
|
+
if zip_interno.testzip() is not None:
|
|
168
|
+
logger.debug("ZIP interno do boletim contém arquivo corrompido.")
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _zip_valido(conteudo_zip: bytes) -> bool:
|
|
150
175
|
"""Verifica se o ZIP bruto do boletim contém um XML legível."""
|
|
151
176
|
tamanho_minimo = 1024 # ZIP válido ~2KB; 1KB detecta arquivos "sem dados"
|
|
152
177
|
if len(conteudo_zip) < tamanho_minimo:
|
|
@@ -154,33 +179,11 @@ def _zip_valido(conteudo_zip: bytes) -> bool: # noqa: PLR0911
|
|
|
154
179
|
return False
|
|
155
180
|
|
|
156
181
|
try:
|
|
157
|
-
|
|
158
|
-
nomes = zip_externo.namelist()
|
|
159
|
-
if not nomes:
|
|
160
|
-
logger.debug("ZIP externo do boletim está vazio.")
|
|
161
|
-
return False
|
|
162
|
-
if zip_externo.testzip() is not None:
|
|
163
|
-
logger.debug("ZIP externo do boletim contém arquivo corrompido.")
|
|
164
|
-
return False
|
|
165
|
-
|
|
166
|
-
conteudo_interno = zip_externo.read(nomes[0])
|
|
167
|
-
with zipfile.ZipFile(io.BytesIO(conteudo_interno), "r") as zip_interno:
|
|
168
|
-
nomes_xml = [
|
|
169
|
-
nome for nome in zip_interno.namelist() if nome.endswith(".xml")
|
|
170
|
-
]
|
|
171
|
-
if not nomes_xml:
|
|
172
|
-
logger.debug("ZIP interno do boletim não contém XML.")
|
|
173
|
-
return False
|
|
174
|
-
if zip_interno.testzip() is not None:
|
|
175
|
-
logger.debug("ZIP interno do boletim contém arquivo corrompido.")
|
|
176
|
-
return False
|
|
177
|
-
|
|
182
|
+
return _zip_tem_xml_valido(conteudo_zip)
|
|
178
183
|
except (zipfile.BadZipFile, KeyError, OSError, RuntimeError):
|
|
179
184
|
logger.debug("ZIP do boletim inválido ou ilegível.")
|
|
180
185
|
return False
|
|
181
186
|
|
|
182
|
-
return True
|
|
183
|
-
|
|
184
187
|
|
|
185
188
|
def _extrair(conteudo_zip: bytes) -> bytes:
|
|
186
189
|
"""Extrai o XML válido do ZIP aninhado do Price Report da B3.
|
|
@@ -210,6 +213,20 @@ def _extrair(conteudo_zip: bytes) -> bytes:
|
|
|
210
213
|
return zip_interno.read(nomes_xml[-1])
|
|
211
214
|
|
|
212
215
|
|
|
216
|
+
def _xpath_elementos_xml(
|
|
217
|
+
elemento: etree._ElementTree | etree._Element,
|
|
218
|
+
expressao: str,
|
|
219
|
+
*,
|
|
220
|
+
namespaces: dict[str, str] | None = None,
|
|
221
|
+
) -> list[etree._Element]:
|
|
222
|
+
resultado = elemento.xpath(expressao, namespaces=namespaces)
|
|
223
|
+
if not isinstance(resultado, list) or not all(
|
|
224
|
+
isinstance(item, etree._Element) for item in resultado
|
|
225
|
+
):
|
|
226
|
+
raise TypeError("XPath deveria retornar uma lista de elementos XML.")
|
|
227
|
+
return [item for item in resultado if isinstance(item, etree._Element)]
|
|
228
|
+
|
|
229
|
+
|
|
213
230
|
def _extrair_dados_contrato(pric_rpt: etree._Element) -> dict | None:
|
|
214
231
|
dados = {}
|
|
215
232
|
tem_ticker = False
|
|
@@ -266,8 +283,7 @@ def _parsear_xml_registros(xml_bytes: bytes) -> list[dict]:
|
|
|
266
283
|
load_dtd=False,
|
|
267
284
|
)
|
|
268
285
|
arvore = etree.parse(io.BytesIO(xml_bytes), parser=analisador)
|
|
269
|
-
|
|
270
|
-
elementos: list[etree._Element] = resultado # type: ignore[assignment]
|
|
286
|
+
elementos = _xpath_elementos_xml(arvore, "//ns:PricRpt", namespaces=namespaces)
|
|
271
287
|
registros = [
|
|
272
288
|
dados
|
|
273
289
|
for pric_rpt in elementos
|
|
@@ -282,7 +298,7 @@ def _converter_para_df(registros: list[dict]) -> pl.DataFrame:
|
|
|
282
298
|
# (pl.DataFrame infere colunas de ~50 primeiras linhas por padrão.)
|
|
283
299
|
schema_str = {nome: pl.String for nome in SCHEMA_PRICE_REPORT}
|
|
284
300
|
df = pl.DataFrame(registros, schema=schema_str)
|
|
285
|
-
return df.cast(SCHEMA_PRICE_REPORT, strict=False)
|
|
301
|
+
return df.cast(SCHEMA_PRICE_REPORT, strict=False)
|
|
286
302
|
|
|
287
303
|
|
|
288
304
|
def _processar_xml_extraido(xml_bytes: bytes) -> pl.DataFrame:
|
|
@@ -214,7 +214,7 @@ def contar(
|
|
|
214
214
|
.get_column("dias_uteis")
|
|
215
215
|
)
|
|
216
216
|
|
|
217
|
-
if not tp.
|
|
217
|
+
if not tp.any_is_array_like(inicio, fim):
|
|
218
218
|
return s.item()
|
|
219
219
|
|
|
220
220
|
return s
|
|
@@ -490,7 +490,7 @@ def deslocar(
|
|
|
490
490
|
.get_column("data_ajustada")
|
|
491
491
|
)
|
|
492
492
|
|
|
493
|
-
if not tp.
|
|
493
|
+
if not tp.any_is_array_like(datas, deslocamento):
|
|
494
494
|
return s.item()
|
|
495
495
|
|
|
496
496
|
return s
|
|
@@ -672,7 +672,7 @@ def eh_dia_util(datas: None | DateLike | ArrayLike) -> None | bool | pl.Series:
|
|
|
672
672
|
.get_column("eh_dia_util")
|
|
673
673
|
)
|
|
674
674
|
|
|
675
|
-
if not tp.
|
|
675
|
+
if not tp.any_is_array_like(datas):
|
|
676
676
|
return s.item()
|
|
677
677
|
|
|
678
678
|
return s
|
|
@@ -4,7 +4,7 @@ from typing import overload
|
|
|
4
4
|
import polars as pl
|
|
5
5
|
|
|
6
6
|
from pyield import du
|
|
7
|
-
from pyield._internal.types import ArrayLike,
|
|
7
|
+
from pyield._internal.types import ArrayLike, any_is_array_like
|
|
8
8
|
|
|
9
9
|
# Lista de contratos que negociam por taxa (juros/cupom).
|
|
10
10
|
# Nestes contratos, as colunas OHLC são taxas e precisam ser divididas por 100.
|
|
@@ -80,14 +80,14 @@ def vencimento(
|
|
|
80
80
|
null
|
|
81
81
|
]
|
|
82
82
|
"""
|
|
83
|
-
dados = codigo if
|
|
83
|
+
dados = codigo if any_is_array_like(codigo) else [codigo]
|
|
84
84
|
serie = (
|
|
85
85
|
pl.DataFrame({"codigo": dados})
|
|
86
86
|
.select(vencimento=vencimento_expr("codigo", contrato))
|
|
87
87
|
.get_column("vencimento")
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
if
|
|
90
|
+
if any_is_array_like(codigo):
|
|
91
91
|
return serie
|
|
92
92
|
|
|
93
93
|
return serie.item()
|
|
@@ -5,16 +5,25 @@ Fonte:
|
|
|
5
5
|
Report e usados como curva de ajuste do DI1.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
8
10
|
import polars as pl
|
|
9
11
|
|
|
10
12
|
import pyield._internal.converters as cv
|
|
11
13
|
from pyield import du
|
|
12
14
|
from pyield._internal.data_cache import obter_dataset_cacheado
|
|
13
|
-
from pyield._internal.types import ArrayLike, DateLike,
|
|
15
|
+
from pyield._internal.types import ArrayLike, DateLike, any_is_array_like, any_is_empty
|
|
14
16
|
from pyield.futuro.historico import buscar_historico_cacheado
|
|
15
17
|
from pyield.futuro.historico import datas_disponiveis as _datas_futuro
|
|
16
18
|
from pyield.interpolador import Interpolador
|
|
17
19
|
|
|
20
|
+
TipoTaxaDI1 = Literal["ajuste", "fechamento"]
|
|
21
|
+
|
|
22
|
+
_COLUNAS_TIPO_TAXA: dict[TipoTaxaDI1, str] = {
|
|
23
|
+
"ajuste": "taxa_ajuste",
|
|
24
|
+
"fechamento": "taxa_fechamento",
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
|
|
19
28
|
def dados(
|
|
20
29
|
datas: DateLike | ArrayLike,
|
|
@@ -111,6 +120,7 @@ def interpolar_taxas(
|
|
|
111
120
|
datas_referencia: DateLike | ArrayLike,
|
|
112
121
|
datas_vencimento: DateLike | ArrayLike,
|
|
113
122
|
extrapolar: bool = True,
|
|
123
|
+
tipo_taxa: TipoTaxaDI1 = "ajuste",
|
|
114
124
|
) -> pl.Series:
|
|
115
125
|
"""Interpola taxas de DI para datas de negociação e vencimentos especificados.
|
|
116
126
|
|
|
@@ -130,6 +140,9 @@ def interpolar_taxas(
|
|
|
130
140
|
compatível em tamanho com ``datas_referencia`` se ambos forem arrays.
|
|
131
141
|
extrapolar: Se permite extrapolação além do intervalo de taxas DI
|
|
132
142
|
conhecidas para uma data de negociação. Padrão: True.
|
|
143
|
+
tipo_taxa: Tipo de taxa usada como curva de referência. Use
|
|
144
|
+
``"ajuste"`` para a taxa de ajuste ou ``"fechamento"`` para a
|
|
145
|
+
última taxa negociada. Padrão: ``"ajuste"``.
|
|
133
146
|
|
|
134
147
|
Returns:
|
|
135
148
|
Series contendo as taxas DI interpoladas (como floats). Valores serão
|
|
@@ -139,10 +152,13 @@ def interpolar_taxas(
|
|
|
139
152
|
Raises:
|
|
140
153
|
ValueError: Se ``datas_referencia`` e ``datas_vencimento`` forem ambos
|
|
141
154
|
array-like mas tiverem tamanhos diferentes.
|
|
155
|
+
ValueError: Se ``tipo_taxa`` não for ``"ajuste"`` ou ``"fechamento"``.
|
|
142
156
|
|
|
143
157
|
Notes:
|
|
144
|
-
- Todas as taxas
|
|
145
|
-
flat-forward.
|
|
158
|
+
- Todas as taxas disponíveis do tipo escolhido são usadas para
|
|
159
|
+
interpolação flat-forward.
|
|
160
|
+
- ``tipo_taxa="fechamento"`` usa a última taxa negociada (last trade),
|
|
161
|
+
que pode ter menos vértices disponíveis que a curva de ajuste.
|
|
146
162
|
- A função trata broadcasting de entradas escalares e array-like.
|
|
147
163
|
|
|
148
164
|
Examples:
|
|
@@ -191,6 +207,10 @@ def interpolar_taxas(
|
|
|
191
207
|
null
|
|
192
208
|
]
|
|
193
209
|
"""
|
|
210
|
+
coluna_taxa = _COLUNAS_TIPO_TAXA.get(tipo_taxa)
|
|
211
|
+
if coluna_taxa is None:
|
|
212
|
+
raise ValueError("tipo_taxa deve ser 'ajuste' ou 'fechamento'.")
|
|
213
|
+
|
|
194
214
|
if any_is_empty(datas_referencia, datas_vencimento):
|
|
195
215
|
return pl.Series(dtype=pl.Float64)
|
|
196
216
|
|
|
@@ -217,7 +237,7 @@ def interpolar_taxas(
|
|
|
217
237
|
# Inicializa taxa_interpolada como None
|
|
218
238
|
df_entrada = df_entrada.with_columns(
|
|
219
239
|
dias_uteis=du.contar_expr("data_referencia", "data_vencimento"),
|
|
220
|
-
taxa_interpolada=None,
|
|
240
|
+
taxa_interpolada=pl.lit(None, dtype=pl.Float64),
|
|
221
241
|
)
|
|
222
242
|
|
|
223
243
|
# Lista para armazenar os blocos processados
|
|
@@ -237,10 +257,15 @@ def interpolar_taxas(
|
|
|
237
257
|
blocos_processados.append(df_parcial)
|
|
238
258
|
continue
|
|
239
259
|
|
|
260
|
+
df_curva = df_referencia.select("dias_uteis", coluna_taxa).drop_nulls()
|
|
261
|
+
if df_curva.is_empty():
|
|
262
|
+
blocos_processados.append(df_parcial)
|
|
263
|
+
continue
|
|
264
|
+
|
|
240
265
|
# Inicializa o interpolador com taxas e dias úteis conhecidos
|
|
241
266
|
interpolador_du = Interpolador(
|
|
242
|
-
dias_uteis=
|
|
243
|
-
taxas=
|
|
267
|
+
dias_uteis=df_curva["dias_uteis"],
|
|
268
|
+
taxas=df_curva[coluna_taxa],
|
|
244
269
|
metodo="flat_forward",
|
|
245
270
|
extrapolar=extrapolar,
|
|
246
271
|
)
|
|
@@ -267,6 +292,7 @@ def interpolar_taxa(
|
|
|
267
292
|
data_referencia: DateLike,
|
|
268
293
|
data_vencimento: DateLike,
|
|
269
294
|
extrapolar: bool = False,
|
|
295
|
+
tipo_taxa: TipoTaxaDI1 = "ajuste",
|
|
270
296
|
) -> float:
|
|
271
297
|
"""Interpola ou obtém a taxa DI para uma única data de vencimento.
|
|
272
298
|
|
|
@@ -282,6 +308,9 @@ def interpolar_taxa(
|
|
|
282
308
|
extrapolar: Se True, permite extrapolação se o ``data_vencimento`` estiver
|
|
283
309
|
fora do intervalo de vencimentos de contratos disponíveis para a
|
|
284
310
|
``data_referencia``. Padrão: False.
|
|
311
|
+
tipo_taxa: Tipo de taxa usada como curva de referência. Use
|
|
312
|
+
``"ajuste"`` para a taxa de ajuste ou ``"fechamento"`` para a
|
|
313
|
+
última taxa negociada. Padrão: ``"ajuste"``.
|
|
285
314
|
|
|
286
315
|
Returns:
|
|
287
316
|
Taxa de liquidação DI exata ou interpolada para a data e vencimento
|
|
@@ -305,7 +334,7 @@ def interpolar_taxa(
|
|
|
305
334
|
>>> di1.interpolar_taxa("25-04-2025", "01-01-2050", extrapolar=True)
|
|
306
335
|
0.13881
|
|
307
336
|
"""
|
|
308
|
-
if
|
|
337
|
+
if any_is_array_like(data_referencia, data_vencimento):
|
|
309
338
|
raise ValueError(
|
|
310
339
|
"As entradas 'data_referencia' e 'data_vencimento' devem ser datas escalares."
|
|
311
340
|
)
|
|
@@ -314,6 +343,7 @@ def interpolar_taxa(
|
|
|
314
343
|
datas_referencia=data_referencia,
|
|
315
344
|
datas_vencimento=data_vencimento,
|
|
316
345
|
extrapolar=extrapolar,
|
|
346
|
+
tipo_taxa=tipo_taxa,
|
|
317
347
|
)
|
|
318
348
|
if taxa.is_empty():
|
|
319
349
|
return float("nan")
|
|
@@ -10,7 +10,7 @@ from pyield._internal.data_cache import obter_dataset_cacheado
|
|
|
10
10
|
from pyield._internal.types import ArrayLike, DateLike, any_is_empty
|
|
11
11
|
from pyield.b3._validar_pregao import data_negociacao_valida
|
|
12
12
|
from pyield.futuro import contratos as ct
|
|
13
|
-
from pyield.fwd import
|
|
13
|
+
from pyield.fwd import forwards_expr
|
|
14
14
|
|
|
15
15
|
# Renomeação preco_* → taxa_* para contratos cotados por taxa.
|
|
16
16
|
# Bid/Ask são invertidos: BestBidPric (bid em PU) = menor taxa = venda de taxa;
|
|
@@ -128,10 +128,10 @@ def _enriquecer_dados(df: pl.DataFrame, contrato: str) -> pl.DataFrame:
|
|
|
128
128
|
|
|
129
129
|
if contrato in {"DI1", "DAP"}:
|
|
130
130
|
df = df.with_columns(
|
|
131
|
-
taxa_forward=
|
|
132
|
-
dias_uteis=
|
|
133
|
-
taxas=
|
|
134
|
-
agrupar_por=
|
|
131
|
+
taxa_forward=forwards_expr(
|
|
132
|
+
dias_uteis="dias_uteis",
|
|
133
|
+
taxas="taxa_ajuste",
|
|
134
|
+
agrupar_por="data_referencia",
|
|
135
135
|
)
|
|
136
136
|
)
|
|
137
137
|
|
|
@@ -6,7 +6,7 @@ from pyield._internal.br_numbers import pct_para_decimal
|
|
|
6
6
|
from pyield.b3._validar_pregao import intradia_disponivel
|
|
7
7
|
from pyield.b3.derivativos_intradia import derivativo_intradia
|
|
8
8
|
from pyield.futuro.contratos import CONTRATOS_TAXA, dv01_expr
|
|
9
|
-
from pyield.fwd import
|
|
9
|
+
from pyield.fwd import forwards_expr
|
|
10
10
|
|
|
11
11
|
# Renomeação preco_* → taxa_* para contratos cotados por taxa.
|
|
12
12
|
_PRECO_PARA_TAXA_INTRADIA = {
|
|
@@ -157,10 +157,12 @@ def _processar_intradia(df: pl.DataFrame, contrato: str) -> pl.DataFrame:
|
|
|
157
157
|
df = df.with_columns(pct_para_decimal(cs.starts_with("taxa_")))
|
|
158
158
|
|
|
159
159
|
if contrato in {"DI1", "DAP"}:
|
|
160
|
-
taxa_fwd = forwards(dias_uteis=df["dias_uteis"], taxas=df["taxa_ultima"])
|
|
161
160
|
anos_uteis = pl.col("dias_uteis") / 252
|
|
162
161
|
preco_ultimo = 100_000 / ((1 + pl.col("taxa_ultima")) ** anos_uteis)
|
|
163
|
-
df = df.with_columns(
|
|
162
|
+
df = df.with_columns(
|
|
163
|
+
preco_ultimo=preco_ultimo.round(2),
|
|
164
|
+
taxa_forward=forwards_expr("dias_uteis", "taxa_ultima"),
|
|
165
|
+
)
|
|
164
166
|
|
|
165
167
|
if contrato == "DI1":
|
|
166
168
|
df = df.with_columns(
|