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.
Files changed (70) hide show
  1. {pyield-0.51.0 → pyield-0.51.2}/PKG-INFO +1 -1
  2. {pyield-0.51.0 → pyield-0.51.2}/pyield/__init__.py +2 -1
  3. {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/converters.py +4 -4
  4. {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/types.py +9 -9
  5. {pyield-0.51.0 → pyield-0.51.2}/pyield/anbima/imaq.py +18 -6
  6. {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/boletim.py +44 -28
  7. {pyield-0.51.0 → pyield-0.51.2}/pyield/du/core.py +3 -3
  8. {pyield-0.51.0 → pyield-0.51.2}/pyield/futuro/contratos.py +3 -3
  9. {pyield-0.51.0 → pyield-0.51.2}/pyield/futuro/di1.py +37 -7
  10. {pyield-0.51.0 → pyield-0.51.2}/pyield/futuro/historico.py +5 -5
  11. {pyield-0.51.0 → pyield-0.51.2}/pyield/futuro/intradia.py +5 -3
  12. pyield-0.51.2/pyield/fwd.py +460 -0
  13. {pyield-0.51.0 → pyield-0.51.2}/pyield/interpolador.py +7 -3
  14. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ltn.py +5 -8
  15. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ntnb.py +11 -16
  16. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ntnf.py +4 -4
  17. {pyield-0.51.0 → pyield-0.51.2}/pyproject.toml +5 -1
  18. pyield-0.51.0/pyield/fwd.py +0 -292
  19. {pyield-0.51.0 → pyield-0.51.2}/LICENSE +0 -0
  20. {pyield-0.51.0 → pyield-0.51.2}/README.md +0 -0
  21. {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/__init__.py +0 -0
  22. {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/br_numbers.py +0 -0
  23. {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/cache.py +0 -0
  24. {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/data_cache.py +0 -0
  25. {pyield-0.51.0 → pyield-0.51.2}/pyield/_internal/retry.py +0 -0
  26. {pyield-0.51.0 → pyield-0.51.2}/pyield/anbima/__init__.py +0 -0
  27. {pyield-0.51.0 → pyield-0.51.2}/pyield/anbima/mercado_secundario.py +0 -0
  28. {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/__init__.py +0 -0
  29. {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/_contratos.py +0 -0
  30. {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/_validar_pregao.py +0 -0
  31. {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/derivativos_intradia.py +0 -0
  32. {pyield-0.51.0 → pyield-0.51.2}/pyield/b3/di_over.py +0 -0
  33. {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/__init__.py +0 -0
  34. {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/_olinda.py +0 -0
  35. {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/leiloes.py +0 -0
  36. {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/lft.py +0 -0
  37. {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/sgs.py +0 -0
  38. {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/tpf_intradia.py +0 -0
  39. {pyield-0.51.0 → pyield-0.51.2}/pyield/bc/tpf_mensal.py +0 -0
  40. {pyield-0.51.0 → pyield-0.51.2}/pyield/du/__init__.py +0 -0
  41. {pyield-0.51.0 → pyield-0.51.2}/pyield/du/feriados/__init__.py +0 -0
  42. {pyield-0.51.0 → pyield-0.51.2}/pyield/du/feriados/feriados_antigos_br.txt +0 -0
  43. {pyield-0.51.0 → pyield-0.51.2}/pyield/du/feriados/feriados_br.py +0 -0
  44. {pyield-0.51.0 → pyield-0.51.2}/pyield/du/feriados/feriados_novos_br.txt +0 -0
  45. {pyield-0.51.0 → pyield-0.51.2}/pyield/futuro/__init__.py +0 -0
  46. {pyield-0.51.0 → pyield-0.51.2}/pyield/ipca/__init__.py +0 -0
  47. {pyield-0.51.0 → pyield-0.51.2}/pyield/ipca/historico.py +0 -0
  48. {pyield-0.51.0 → pyield-0.51.2}/pyield/ipca/projetado.py +0 -0
  49. {pyield-0.51.0 → pyield-0.51.2}/pyield/py.typed +0 -0
  50. {pyield-0.51.0 → pyield-0.51.2}/pyield/relogio.py +0 -0
  51. {pyield-0.51.0 → pyield-0.51.2}/pyield/selic/__init__.py +0 -0
  52. {pyield-0.51.0 → pyield-0.51.2}/pyield/selic/compromissada.py +0 -0
  53. {pyield-0.51.0 → pyield-0.51.2}/pyield/selic/copom.py +0 -0
  54. {pyield-0.51.0 → pyield-0.51.2}/pyield/selic/cpm.py +0 -0
  55. {pyield-0.51.0 → pyield-0.51.2}/pyield/selic/probabilities.py +0 -0
  56. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/__init__.py +0 -0
  57. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/__init__.py +0 -0
  58. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/lft.py +0 -0
  59. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ntnb1.py +0 -0
  60. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ntnbprinc.py +0 -0
  61. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/_titulos/ntnc.py +0 -0
  62. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/benchmark.py +0 -0
  63. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/leiloes.py +0 -0
  64. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/pre.py +0 -0
  65. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/rmd/__init__.py +0 -0
  66. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/rmd/_aba_1_3.py +0 -0
  67. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/rmd/_aba_2_1.py +0 -0
  68. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/rmd/_common.py +0 -0
  69. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/rmd/_download.py +0 -0
  70. {pyield-0.51.0 → pyield-0.51.2}/pyield/tpf/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyield
3
- Version: 0.51.0
3
+ Version: 0.51.2
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
@@ -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 not types.is_collection(dates):
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 Collection, Sized
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 is_collection(arg: Any) -> bool:
36
- """Verifica se o argumento é uma coleção (array-like), excluindo strings e bytes."""
37
- return isinstance(arg, Collection) and not isinstance(arg, (str, bytes))
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 any_is_collection(*args: Any) -> bool:
41
- """Verifica se algum dos argumentos fornecidos é uma coleção (array-like).
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 uma coleção, caso contrário False.
47
+ bool: True se algum argumento for ``ArrayLike``, caso contrário False.
48
48
  """
49
- return any(is_collection(arg) for arg in args)
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
- nested_tables = tree.xpath("//table[@width='100%'][parent::td]")
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: # type: ignore[misc]
67
- headers = table.xpath(".//thead//th")
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.xpath(".//tbody//tr[td]")
83
+ data_rows = _xpath_elementos(table, ".//tbody//tr[td]")
72
84
  for row in data_rows:
73
- cells = row.xpath(".//td")
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 _zip_valido(conteudo_zip: bytes) -> bool: # noqa: PLR0911
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
- with zipfile.ZipFile(io.BytesIO(conteudo_zip), "r") as zip_externo:
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
- resultado = arvore.xpath("//ns:PricRpt", namespaces=namespaces)
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) # type: ignore
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.any_is_collection(inicio, fim):
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.any_is_collection(datas, deslocamento):
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.any_is_collection(datas):
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, any_is_collection
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 any_is_collection(codigo) else [codigo]
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 any_is_collection(codigo):
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, any_is_collection, any_is_empty
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 de liquidação disponíveis são usadas para interpolação
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=df_referencia["dias_uteis"],
243
- taxas=df_referencia["taxa_ajuste"],
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 any_is_collection(data_referencia, data_vencimento):
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 forwards
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=forwards(
132
- dias_uteis=df["dias_uteis"],
133
- taxas=df["taxa_ajuste"],
134
- agrupar_por=df["data_referencia"],
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 forwards
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(preco_ultimo=preco_ultimo.round(2), taxa_forward=taxa_fwd)
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(