pyield 0.58.0__tar.gz → 0.58.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 (83) hide show
  1. {pyield-0.58.0 → pyield-0.58.2}/PKG-INFO +3 -3
  2. {pyield-0.58.0 → pyield-0.58.2}/README.md +2 -2
  3. {pyield-0.58.0 → pyield-0.58.2}/pyield/b3/boletim.py +0 -4
  4. {pyield-0.58.0 → pyield-0.58.2}/pyield/bc/sgs.py +3 -1
  5. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/_utils.py +15 -0
  6. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/lft.py +7 -12
  7. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/ltn.py +2 -0
  8. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/ntnb.py +8 -6
  9. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/ntnb1.py +7 -19
  10. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/ntnbp.py +52 -7
  11. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/ntnc.py +162 -19
  12. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/ntnf.py +4 -4
  13. {pyield-0.58.0 → pyield-0.58.2}/pyield/vna/__init__.py +9 -1
  14. {pyield-0.58.0 → pyield-0.58.2}/pyield/vna/_consulta.py +58 -10
  15. {pyield-0.58.0 → pyield-0.58.2}/pyield/vna/_lft.py +84 -1
  16. {pyield-0.58.0 → pyield-0.58.2}/pyproject.toml +1 -1
  17. {pyield-0.58.0 → pyield-0.58.2}/pyproject.toml.orig +1 -1
  18. {pyield-0.58.0 → pyield-0.58.2}/LICENSE +0 -0
  19. {pyield-0.58.0 → pyield-0.58.2}/pyield/__init__.py +0 -0
  20. {pyield-0.58.0 → pyield-0.58.2}/pyield/_internal/__init__.py +0 -0
  21. {pyield-0.58.0 → pyield-0.58.2}/pyield/_internal/br_numbers.py +0 -0
  22. {pyield-0.58.0 → pyield-0.58.2}/pyield/_internal/cache.py +0 -0
  23. {pyield-0.58.0 → pyield-0.58.2}/pyield/_internal/converters.py +0 -0
  24. {pyield-0.58.0 → pyield-0.58.2}/pyield/_internal/data_cache.py +0 -0
  25. {pyield-0.58.0 → pyield-0.58.2}/pyield/_internal/excel.py +0 -0
  26. {pyield-0.58.0 → pyield-0.58.2}/pyield/_internal/numbers.py +0 -0
  27. {pyield-0.58.0 → pyield-0.58.2}/pyield/_internal/retry.py +0 -0
  28. {pyield-0.58.0 → pyield-0.58.2}/pyield/_internal/types.py +0 -0
  29. {pyield-0.58.0 → pyield-0.58.2}/pyield/anbima/__init__.py +0 -0
  30. {pyield-0.58.0 → pyield-0.58.2}/pyield/anbima/imaq.py +0 -0
  31. {pyield-0.58.0 → pyield-0.58.2}/pyield/anbima/taxas.py +0 -0
  32. {pyield-0.58.0 → pyield-0.58.2}/pyield/b3/__init__.py +0 -0
  33. {pyield-0.58.0 → pyield-0.58.2}/pyield/b3/_contratos.py +0 -0
  34. {pyield-0.58.0 → pyield-0.58.2}/pyield/b3/_validar_pregao.py +0 -0
  35. {pyield-0.58.0 → pyield-0.58.2}/pyield/b3/derivativos_intradia.py +0 -0
  36. {pyield-0.58.0 → pyield-0.58.2}/pyield/b3/di_over.py +0 -0
  37. {pyield-0.58.0 → pyield-0.58.2}/pyield/bc/__init__.py +0 -0
  38. {pyield-0.58.0 → pyield-0.58.2}/pyield/bc/_olinda.py +0 -0
  39. {pyield-0.58.0 → pyield-0.58.2}/pyield/bc/leiloes.py +0 -0
  40. {pyield-0.58.0 → pyield-0.58.2}/pyield/compromissada.py +0 -0
  41. {pyield-0.58.0 → pyield-0.58.2}/pyield/copom/__init__.py +0 -0
  42. {pyield-0.58.0 → pyield-0.58.2}/pyield/copom/_calendario.py +0 -0
  43. {pyield-0.58.0 → pyield-0.58.2}/pyield/cpm/__init__.py +0 -0
  44. {pyield-0.58.0 → pyield-0.58.2}/pyield/cpm/probabilidades.py +0 -0
  45. {pyield-0.58.0 → pyield-0.58.2}/pyield/du/__init__.py +0 -0
  46. {pyield-0.58.0 → pyield-0.58.2}/pyield/du/core.py +0 -0
  47. {pyield-0.58.0 → pyield-0.58.2}/pyield/du/feriados_br/__init__.py +0 -0
  48. {pyield-0.58.0 → pyield-0.58.2}/pyield/du/feriados_br/anteriores.txt +0 -0
  49. {pyield-0.58.0 → pyield-0.58.2}/pyield/du/feriados_br/atuais.txt +0 -0
  50. {pyield-0.58.0 → pyield-0.58.2}/pyield/futuro/__init__.py +0 -0
  51. {pyield-0.58.0 → pyield-0.58.2}/pyield/futuro/contratos.py +0 -0
  52. {pyield-0.58.0 → pyield-0.58.2}/pyield/futuro/di1.py +0 -0
  53. {pyield-0.58.0 → pyield-0.58.2}/pyield/futuro/historico.py +0 -0
  54. {pyield-0.58.0 → pyield-0.58.2}/pyield/futuro/intradia.py +0 -0
  55. {pyield-0.58.0 → pyield-0.58.2}/pyield/fwd.py +0 -0
  56. {pyield-0.58.0 → pyield-0.58.2}/pyield/interpolador.py +0 -0
  57. {pyield-0.58.0 → pyield-0.58.2}/pyield/ipca/__init__.py +0 -0
  58. {pyield-0.58.0 → pyield-0.58.2}/pyield/ipca/historico.py +0 -0
  59. {pyield-0.58.0 → pyield-0.58.2}/pyield/ipca/projetado.py +0 -0
  60. {pyield-0.58.0 → pyield-0.58.2}/pyield/py.typed +0 -0
  61. {pyield-0.58.0 → pyield-0.58.2}/pyield/relogio.py +0 -0
  62. {pyield-0.58.0 → pyield-0.58.2}/pyield/rmd/__init__.py +0 -0
  63. {pyield-0.58.0 → pyield-0.58.2}/pyield/rmd/_aba_1_3.py +0 -0
  64. {pyield-0.58.0 → pyield-0.58.2}/pyield/rmd/_aba_2_1.py +0 -0
  65. {pyield-0.58.0 → pyield-0.58.2}/pyield/rmd/_common.py +0 -0
  66. {pyield-0.58.0 → pyield-0.58.2}/pyield/rmd/_download.py +0 -0
  67. {pyield-0.58.0 → pyield-0.58.2}/pyield/selic/__init__.py +0 -0
  68. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/__init__.py +0 -0
  69. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/_taxas.py +0 -0
  70. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/benchmark.py +0 -0
  71. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/dealers.py +0 -0
  72. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/leiloes.py +0 -0
  73. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/secundario/__init__.py +0 -0
  74. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/secundario/_intradia.py +0 -0
  75. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/secundario/_mensal.py +0 -0
  76. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/__init__.py +0 -0
  77. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/_ntnb_bootstrap.py +0 -0
  78. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/_ntnf_bootstrap.py +0 -0
  79. {pyield-0.58.0 → pyield-0.58.2}/pyield/tpf/titulos/pre.py +0 -0
  80. {pyield-0.58.0 → pyield-0.58.2}/pyield/vna/_calculo.py +0 -0
  81. {pyield-0.58.0 → pyield-0.58.2}/pyield/vna/_download.py +0 -0
  82. {pyield-0.58.0 → pyield-0.58.2}/pyield/vna/_ntnb.py +0 -0
  83. {pyield-0.58.0 → pyield-0.58.2}/pyield/vna/_ntnc.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyield
3
- Version: 0.58.0
3
+ Version: 0.58.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
@@ -78,7 +78,7 @@ uv add pyield
78
78
  | `yd.di1` | módulo | Curva DI1 e interpolação | `dados`, `interpolar_taxas`, `interpolar_taxa`, `datas_disponiveis` |
79
79
  | `yd.tpf` | módulo | Títulos públicos federais | `taxas`, `taxas_historicas`, `vencimentos`, `estoque`, `leiloes`, `benchmarks`, `curva_pre`, `premios_pre`, `secundario` |
80
80
  | `yd.rmd(aba)` | função | Relatório Mensal da Dívida do Tesouro Nacional | |
81
- | `yd.lft` | módulo | LFT | `dados`, `vencimentos`, `cotacao`, `pu`, `taxa`, `vna`, `rentabilidade`, `rentabilidade_expr` |
81
+ | `yd.lft` | módulo | LFT | `dados`, `vencimentos`, `cotacao`, `pu`, `taxa`, `rentabilidade`, `rentabilidade_expr` |
82
82
  | `yd.ltn` | módulo | LTN | `dados`, `vencimentos`, `pu`, `taxa`, `duration_expr`, `dv01`, `dv01_expr`, `rentabilidade`, `rentabilidade_expr`, `taxas_forward` |
83
83
  | `yd.ntnb` | módulo | NTN-B | `dados`, `vencimentos`, `datas_pagamento`, `fluxos_caixa`, `cotacao`, `pu`, `taxa`, `taxas_zero`, `duration`, `dv01`, `dv01_expr`, `implicitas`, `curva` |
84
84
  | `yd.ntnb1` | módulo | NTN-B1 (Educa+ e Renda+) | `NomeComercial`, `datas_pagamento`, `fluxos_caixa`, `cotacao`, `cotacao_curva_zero`, `taxa_curva_zero`, `pu`, `duration`, `dv01` |
@@ -142,7 +142,7 @@ ou de versões anteriores estão no histórico das
142
142
  | `pyield.tpf.vna.calcular_vna(...)` | `yd.vna.calcular_vna(...)` |
143
143
  | `yd.ntnb.vna(data)` | `yd.vna.valor("NTN-B", data)` |
144
144
  | `yd.ntnb.vnas()` | `yd.vna.historico("NTN-B")` |
145
- | `vna_projetado(...)` nos módulos de títulos | `yd.vna.projetado(titulo, data, vna_base, inflacao)` |
145
+ | `vna_projetado(...)` nos módulos de títulos | `yd.vna.projetado(titulo, data, vna_base, inflacao)`; para LFT, use `selic=` |
146
146
  | `vigencia(data)` nos módulos de títulos | `yd.vna.vigencia(titulo, data)` |
147
147
 
148
148
  O namespace público segue a autonomia semântica do conceito que o usuário
@@ -53,7 +53,7 @@ uv add pyield
53
53
  | `yd.di1` | módulo | Curva DI1 e interpolação | `dados`, `interpolar_taxas`, `interpolar_taxa`, `datas_disponiveis` |
54
54
  | `yd.tpf` | módulo | Títulos públicos federais | `taxas`, `taxas_historicas`, `vencimentos`, `estoque`, `leiloes`, `benchmarks`, `curva_pre`, `premios_pre`, `secundario` |
55
55
  | `yd.rmd(aba)` | função | Relatório Mensal da Dívida do Tesouro Nacional | |
56
- | `yd.lft` | módulo | LFT | `dados`, `vencimentos`, `cotacao`, `pu`, `taxa`, `vna`, `rentabilidade`, `rentabilidade_expr` |
56
+ | `yd.lft` | módulo | LFT | `dados`, `vencimentos`, `cotacao`, `pu`, `taxa`, `rentabilidade`, `rentabilidade_expr` |
57
57
  | `yd.ltn` | módulo | LTN | `dados`, `vencimentos`, `pu`, `taxa`, `duration_expr`, `dv01`, `dv01_expr`, `rentabilidade`, `rentabilidade_expr`, `taxas_forward` |
58
58
  | `yd.ntnb` | módulo | NTN-B | `dados`, `vencimentos`, `datas_pagamento`, `fluxos_caixa`, `cotacao`, `pu`, `taxa`, `taxas_zero`, `duration`, `dv01`, `dv01_expr`, `implicitas`, `curva` |
59
59
  | `yd.ntnb1` | módulo | NTN-B1 (Educa+ e Renda+) | `NomeComercial`, `datas_pagamento`, `fluxos_caixa`, `cotacao`, `cotacao_curva_zero`, `taxa_curva_zero`, `pu`, `duration`, `dv01` |
@@ -117,7 +117,7 @@ ou de versões anteriores estão no histórico das
117
117
  | `pyield.tpf.vna.calcular_vna(...)` | `yd.vna.calcular_vna(...)` |
118
118
  | `yd.ntnb.vna(data)` | `yd.vna.valor("NTN-B", data)` |
119
119
  | `yd.ntnb.vnas()` | `yd.vna.historico("NTN-B")` |
120
- | `vna_projetado(...)` nos módulos de títulos | `yd.vna.projetado(titulo, data, vna_base, inflacao)` |
120
+ | `vna_projetado(...)` nos módulos de títulos | `yd.vna.projetado(titulo, data, vna_base, inflacao)`; para LFT, use `selic=` |
121
121
  | `vigencia(data)` nos módulos de títulos | `yd.vna.vigencia(titulo, data)` |
122
122
 
123
123
  O namespace público segue a autonomia semântica do conceito que o usuário
@@ -436,10 +436,6 @@ def buscar(
436
436
  >>> # Múltiplos contratos de uma vez
437
437
  >>> df = yd.b3.boletim.buscar("26-04-2024", ["DI1", "DAP"])
438
438
 
439
- >>> # Feriado ou fim de semana (retorna DataFrame vazio)
440
- >>> df = yd.b3.boletim.buscar("25-12-2023", "DI1")
441
- >>> df.is_empty()
442
- True
443
439
  """
444
440
  if any_is_empty(data):
445
441
  return pl.DataFrame()
@@ -2,6 +2,7 @@
2
2
 
3
3
  Séries disponíveis:
4
4
  - PTAX Venda (SGS 1)
5
+ - SELIC Diária (SGS 11)
5
6
  - SELIC Meta (SGS 432)
6
7
  - SELIC Over (SGS 1178)
7
8
 
@@ -16,7 +17,7 @@ Exemplo de resposta JSON da API do BCB:
16
17
 
17
18
  Notas de implementação:
18
19
  - Intervalos longos são divididos automaticamente em blocos seguros.
19
- - SELIC Over e Meta: valores percentuais convertidos para decimal
20
+ - SELIC Diária, Over e Meta: valores percentuais convertidos para decimal
20
21
  (divididos por 100) e arredondados para 10 casas decimais.
21
22
  - PTAX Venda: valor absoluto em R$ arredondado para 4 casas.
22
23
  """
@@ -54,6 +55,7 @@ class SerieSGS(Enum):
54
55
  """Enum para as séries disponíveis no SGS do Banco Central."""
55
56
 
56
57
  PTAX_VENDA = 1
58
+ SELIC_DIARIA = 11
57
59
  SELIC_META = 432
58
60
  SELIC_OVER = 1178
59
61
 
@@ -302,3 +302,18 @@ def encontrar_raiz(
302
302
  if not math.isfinite(a) or not math.isfinite(b) or a > b:
303
303
  raise ValueError("Os limites do intervalo devem ser finitos e ordenados.")
304
304
  return _metodo_bissecao(func_diferenca_preco, a, b)
305
+
306
+
307
+ def cotacao_por_taxas(pagamentos: pl.DataFrame) -> float:
308
+ """
309
+ Soma os valores presentes dos fluxos, arredondados na 10ª casa decimal.
310
+
311
+ Args:
312
+ pagamentos: DataFrame com uma linha por fluxo e as colunas
313
+ ``valor_pagamento`` (Float64), ``dias_uteis`` (Int64) e
314
+ ``taxa`` (Float64) alinhadas por linha.
315
+ """
316
+ anos_uteis = truncar(pagamentos["dias_uteis"] / 252, 14)
317
+ fatores = (1 + pagamentos["taxa"]) ** anos_uteis
318
+ valores_presentes = pagamentos["valor_pagamento"] / fatores
319
+ return float(valores_presentes.round(10).sum())
@@ -129,11 +129,13 @@ def cotacao(
129
129
  data_vencimento: Data de vencimento do título.
130
130
  taxa: Taxa anualizada do título em formato decimal.
131
131
  Aceita também percentual explícito: "5.75%" ou "5,75%".
132
+ Antes do cálculo, é truncada em oito casas decimais (seis na
133
+ forma percentual), descartando as casas excedentes sem arredondar.
132
134
 
133
135
  Returns:
134
136
  Decimal: Cotação em base 100, truncada em 4 casas decimais.
135
- Retorna ``Decimal("NaN")`` quando o prazo até o vencimento não é
136
- positivo.
137
+ Retorna ``Decimal("NaN")`` para entradas nulas ou quando o prazo
138
+ até o vencimento não é positivo.
137
139
 
138
140
  Notes:
139
141
  A cotação é calculada e retornada na escala percentual (base 100),
@@ -149,14 +151,7 @@ def cotacao(
149
151
  ... taxa="0.1717%",
150
152
  ... )
151
153
  Decimal('98.9645')
152
- >>> lft.cotacao("21-05-2008", "07-03-2014", "-0.0200009%")
153
- Decimal('100.1158')
154
154
 
155
- Entradas nulas retornam Decimal('NaN'):
156
- >>> lft.cotacao(
157
- ... data_liquidacao=None, data_vencimento="01-09-2030", taxa="0.1717%"
158
- ... )
159
- Decimal('NaN')
160
155
  """
161
156
  taxa = _utils.converter_taxa(taxa)
162
157
  if any_is_empty(data_liquidacao, data_vencimento, taxa):
@@ -306,8 +301,10 @@ def pu(
306
301
  Calcula o PU da LFT pela metodologia da STN para leilões primários.
307
302
 
308
303
  Args:
309
- vna: Valor nominal atualizado (VNA).
304
+ vna: Valor nominal atualizado (VNA), truncado em seis casas decimais
305
+ antes do cálculo, sem arredondar.
310
306
  cotacao: Cotação da LFT em base 100.
307
+ Truncada em quatro casas decimais antes do cálculo, sem arredondar.
311
308
 
312
309
  Returns:
313
310
  Decimal: Preço da LFT truncado em 6 casas decimais.
@@ -321,7 +318,5 @@ def pu(
321
318
  >>> from pyield import lft
322
319
  >>> lft.pu(15785.324502, 99.9291)
323
320
  Decimal('15774.132706')
324
- >>> lft.pu(3451.2153459, 100.11589)
325
- Decimal('3455.211852')
326
321
  """
327
322
  return _calcular_pu(vna, cotacao)
@@ -138,6 +138,8 @@ def pu(
138
138
  data_vencimento: Data de vencimento.
139
139
  taxa: Taxa de desconto (YTM) do título em formato decimal.
140
140
  Aceita também percentual explícito: "5.75%" ou "5,75%".
141
+ Antes do cálculo, é truncada em oito casas decimais (seis na
142
+ forma percentual), descartando as casas excedentes sem arredondar.
141
143
 
142
144
  Returns:
143
145
  Decimal: PU da LTN truncado em seis casas decimais. Retorna
@@ -9,10 +9,10 @@ Convenções de precificação (STN, tabela 3):
9
9
  equivalente a 6 casas em termos percentuais.
10
10
  - Cada fluxo descontado: arredondado a 10 casas.
11
11
 
12
- Constantes calculadas conforme regras da STN, exibidas em base 100.
12
+ Valores de referência derivados conforme as regras da STN, exibidos em base 100:
13
13
  TAXA_CUPOM = ((0.06 + 1) ** 0.5 - 1) * 100 # 6% a.a. com capitalização semestral
14
14
  VALOR_CUPOM = round(TAXA_CUPOM, 6) -> 2.956301
15
- VALOR_FINAL = principal + último cupom = 100 + 2.956301
15
+ VALOR_FINAL = 102.956301
16
16
  DIA_CUPOM = 15
17
17
  MESES_CUPOM = {2, 5, 8, 11}
18
18
  """
@@ -35,7 +35,7 @@ from . import _utils
35
35
 
36
36
  BASE_COTACAO = 100
37
37
  VALOR_CUPOM = 2.956301
38
- VALOR_FINAL = BASE_COTACAO + VALOR_CUPOM
38
+ VALOR_FINAL = 102.956301
39
39
 
40
40
  logger = logging.getLogger(__name__)
41
41
 
@@ -291,6 +291,8 @@ def cotacao(
291
291
  data_vencimento (DateLike): Data de vencimento da NTN-B.
292
292
  taxa: Taxa de desconto (TIR) em formato decimal.
293
293
  Aceita também percentual explícito: "5.75%" ou "5,75%".
294
+ Antes do cálculo, é truncada em oito casas decimais (seis na
295
+ forma percentual), descartando as casas excedentes sem arredondar.
294
296
 
295
297
  Returns:
296
298
  Decimal: Cotação em base 100, truncada em 4 casas. Retorna
@@ -364,8 +366,10 @@ def pu(
364
366
  Calcula o PU da NTN-B pela metodologia da STN para leilões primários.
365
367
 
366
368
  Args:
367
- vna: Valor nominal atualizado (VNA).
369
+ vna: Valor nominal atualizado (VNA), truncado em seis casas decimais
370
+ antes do cálculo, sem arredondar.
368
371
  cotacao: Cotação da NTN-B em base 100.
372
+ Truncada em quatro casas decimais antes do cálculo, sem arredondar.
369
373
 
370
374
  Returns:
371
375
  Decimal: Preço da NTN-B truncado em 6 casas decimais.
@@ -381,8 +385,6 @@ def pu(
381
385
  Decimal('4271.864805')
382
386
  >>> ntnb.pu(4315.498383, 100.6409)
383
387
  Decimal('4343.156412')
384
- >>> ntnb.pu(1728.4611369, 97.08139)
385
- Decimal('1678.012540')
386
388
  """
387
389
  return _calcular_pu(vna, cotacao)
388
390
 
@@ -227,6 +227,7 @@ def cotacao(
227
227
  data_vencimento: Data de vencimento da NTN-B1.
228
228
  taxa: Taxa de desconto (YTM) usada no valor presente.
229
229
  Aceita também percentual explícito: "5.75%" ou "5,75%".
230
+ A taxa de entrada não é truncada antes do cálculo.
230
231
  nome_comercial: Nome comercial (Renda+ ou Educa+).
231
232
 
232
233
  Returns:
@@ -285,21 +286,6 @@ def _validar_curva_zero(curva_zero: pl.DataFrame) -> pl.DataFrame:
285
286
  )
286
287
 
287
288
 
288
- def _cotacao_por_taxas(pagamentos: pl.DataFrame) -> float:
289
- """
290
- Soma os valores presentes dos fluxos, arredondados na 10ª casa decimal.
291
-
292
- Args:
293
- pagamentos: DataFrame com uma linha por fluxo e as colunas
294
- ``valor_pagamento`` (Float64), ``dias_uteis`` (Int64) e
295
- ``taxa`` (Float64) alinhadas por linha.
296
- """
297
- anos_uteis = _utils.truncar(pagamentos["dias_uteis"] / 252, 14)
298
- fatores = (1 + pagamentos["taxa"]) ** anos_uteis
299
- valores_presentes = pagamentos["valor_pagamento"] / fatores
300
- return float(valores_presentes.round(10).sum())
301
-
302
-
303
289
  def cotacao_curva_zero(
304
290
  data_liquidacao: DateLike,
305
291
  data_vencimento: DateLike,
@@ -347,7 +333,7 @@ def cotacao_curva_zero(
347
333
  extrapolar=True,
348
334
  )
349
335
  pagamentos = fluxos.with_columns(dias_uteis=dias_fluxos, taxa=taxas_fluxos)
350
- return _cotacao_por_taxas(pagamentos)
336
+ return _utils.cotacao_por_taxas(pagamentos)
351
337
 
352
338
 
353
339
  def _resolver_taxa_equivalente(
@@ -368,7 +354,7 @@ def _resolver_taxa_equivalente(
368
354
 
369
355
  def erro(taxa: float) -> float:
370
356
  pagamentos = pagamentos_base.with_columns(taxa=pl.lit(taxa, dtype=pl.Float64))
371
- return _cotacao_por_taxas(pagamentos) - cotacao_alvo
357
+ return _utils.cotacao_por_taxas(pagamentos) - cotacao_alvo
372
358
 
373
359
  limite_inferior = -0.99
374
360
  limite_superior = max(1.0, 2 * taxa_inicial + 0.01)
@@ -435,7 +421,7 @@ def taxa_curva_zero(
435
421
  extrapolar=True,
436
422
  )
437
423
  pagamentos_base = fluxos.with_columns(dias_uteis=dias_fluxos)
438
- cotacao_alvo = _cotacao_por_taxas(pagamentos_base.with_columns(taxa=taxas_zero))
424
+ cotacao_alvo = _utils.cotacao_por_taxas(pagamentos_base.with_columns(taxa=taxas_zero))
439
425
  return _resolver_taxa_equivalente(
440
426
  cotacao_alvo,
441
427
  pagamentos_base,
@@ -451,8 +437,10 @@ def pu(
451
437
  Calcula o preço (PU) da NTN-B1 pelas regras do Tesouro Nacional.
452
438
 
453
439
  Args:
454
- vna: Valor nominal atualizado (VNA).
440
+ vna: Valor nominal atualizado (VNA), truncado em seis casas decimais
441
+ antes do cálculo, sem arredondar.
455
442
  cotacao: Cotação da NTN-B1 em base 100.
443
+ Truncada em quatro casas decimais antes do cálculo, sem arredondar.
456
444
 
457
445
  Returns:
458
446
  Decimal: Preço da NTN-B1 truncado em 6 casas decimais.
@@ -1,4 +1,22 @@
1
- """Precificação de NTN-B Principal."""
1
+ """Precificação de NTN-B Principal (Tesouro IPCA+ sem cupons).
2
+
3
+ Convenções de cálculo:
4
+ - Fluxo único no vencimento, atualizado pelo IPCA via VNA.
5
+ - Prazo de desconto: dias úteis / 252, truncado a 14 casas.
6
+ - Cotação: base 100, truncada a 4 casas.
7
+ - VNA de entrada e PU retornado: truncados a 6 casas.
8
+ - Taxa da curva: float interpolado por flat-forward, sem arredondamento.
9
+
10
+ O exemplo de precificação do Tesouro Direto (páginas 2 a 4) usa cotação com
11
+ 4 casas, embora o texto da página 4 mencione 2. O PU da biblioteca mantém
12
+ 6 casas; o preço em centavos do documento exige truncamento adicional.
13
+ O documento parte de uma taxa pactuada e não especifica a regra de
14
+ arredondamento da taxa obtida por curva zero.
15
+
16
+ References:
17
+ - Tesouro Direto — Cálculo da Rentabilidade: Tesouro IPCA+.
18
+ <https://www.tesourodireto.com.br/documents/d/guest/tesouro_ipca_juros_semestrais>
19
+ """
2
20
 
3
21
  from decimal import Decimal
4
22
 
@@ -24,6 +42,7 @@ def cotacao(
24
42
  data_vencimento: Data de vencimento.
25
43
  taxa_tir: Taxa interna de retorno anualizada do título.
26
44
  Aceita também percentual explícito: "5.75%" ou "5,75%".
45
+ A taxa de entrada não é truncada antes do cálculo.
27
46
 
28
47
  Returns:
29
48
  Decimal: Cotação em base 100, truncada em 4 casas decimais. Retorna
@@ -33,6 +52,16 @@ def cotacao(
33
52
  >>> from pyield import ntnbp
34
53
  >>> ntnbp.cotacao("02-12-2025", "15-05-2029", "7.77%")
35
54
  Decimal('77.4630')
55
+
56
+ Exemplo do Tesouro Direto, páginas 2 a 4: liquidação em 03/01/2012,
57
+ vencimento em 15/05/2015, prazo de 846 dias úteis e taxa de 5,17% a.a.
58
+
59
+ >>> ntnbp.cotacao("03-01-2012", "15-05-2015", "5.17%")
60
+ Decimal('84.4317')
61
+
62
+ References:
63
+ - Tesouro Direto — Tesouro IPCA+, páginas 2 a 4.
64
+ <https://www.tesourodireto.com.br/documents/d/guest/tesouro_ipca_juros_semestrais>
36
65
  """
37
66
  if isinstance(taxa_tir, str):
38
67
  taxa_tir = float(_utils.converter_taxa(taxa_tir))
@@ -52,8 +81,10 @@ def pu(vna: float | Decimal, cotacao: float | Decimal) -> Decimal:
52
81
  Calcula o preço (PU) da NTN-B Principal.
53
82
 
54
83
  Args:
55
- vna: Valor nominal atualizado (VNA).
84
+ vna: Valor nominal atualizado (VNA), truncado em seis casas decimais
85
+ antes do cálculo, sem arredondar.
56
86
  cotacao: Cotação da NTN-B Principal em base 100.
87
+ Truncada em quatro casas decimais antes do cálculo, sem arredondar.
57
88
 
58
89
  Returns:
59
90
  Decimal: Preço da NTN-B Principal truncado em 6 casas decimais.
@@ -63,6 +94,18 @@ def pu(vna: float | Decimal, cotacao: float | Decimal) -> Decimal:
63
94
  >>> cot = ntnbp.cotacao("02-12-2025", "15-05-2029", "7.77%")
64
95
  >>> ntnbp.pu(4567.033825, cot)
65
96
  Decimal('3537.761411')
97
+
98
+ Exemplo do Tesouro Direto, página 4, com o VNA projetado publicado.
99
+ O retorno preserva seis casas; truncado em centavos, corresponde
100
+ ao preço de R$ 1.776,77 apresentado no documento.
101
+
102
+ >>> cot = ntnbp.cotacao("03-01-2012", "15-05-2015", "5.17%")
103
+ >>> ntnbp.pu(Decimal("2104.390122"), cot)
104
+ Decimal('1776.772354')
105
+
106
+ References:
107
+ - Tesouro Direto — Tesouro IPCA+, página 4.
108
+ <https://www.tesourodireto.com.br/documents/d/guest/tesouro_ipca_juros_semestrais>
66
109
  """
67
110
  if any_is_empty(vna, cotacao):
68
111
  return Decimal("NaN")
@@ -97,10 +140,13 @@ def taxa(
97
140
  """
98
141
  Obtém a TIR da NTN-B Principal a partir da curva zero informada.
99
142
 
100
- A taxa zero correspondente ao vencimento é interpolada por flat-forward e
101
- arredondada em quatro casas decimais. Como a NTN-B Principal possui um único
143
+ A taxa zero correspondente ao vencimento é interpolada por flat-forward,
144
+ sem arredondamento. Como a NTN-B Principal possui um único
102
145
  fluxo no vencimento, essa taxa zero também é a TIR do título.
103
146
 
147
+ Arredondamentos comerciais e spreads de compra ou venda ficam a cargo
148
+ do consumidor.
149
+
104
150
  A curva pode ser produzida por :func:`pyield.ntnb.taxas_zero`. Para
105
151
  cálculos em lote, ela deve ser construída uma única vez e reutilizada entre
106
152
  os títulos.
@@ -111,7 +157,7 @@ def taxa(
111
157
  curva_zero: DataFrame com as colunas ``dias_uteis`` e ``taxa_zero``.
112
158
 
113
159
  Returns:
114
- float: TIR anualizada, arredondada em quatro casas decimais.
160
+ float: TIR anualizada em formato decimal, sem arredondamento.
115
161
  Retorna ``NaN`` se a liquidação for igual ou posterior ao vencimento.
116
162
  """
117
163
  if any_is_empty(data_liquidacao, data_vencimento):
@@ -122,12 +168,11 @@ def taxa(
122
168
  return float("nan")
123
169
 
124
170
  curva = _normalizar_curva_zero(curva_zero)
125
- taxa_zero = interpolador.Interpolador(
171
+ return interpolador.Interpolador(
126
172
  curva["dias_uteis"],
127
173
  curva["taxa_zero"],
128
174
  metodo="flat_forward",
129
175
  ).interpolar(dias_uteis)
130
- return round(taxa_zero, 4)
131
176
 
132
177
 
133
178
  def dv01(
@@ -1,28 +1,40 @@
1
+ """Cálculos e dados de NTN-C.
2
+
3
+ Convenções de precificação (STN, tabela 3):
4
+ - Cotação em base 100, truncada a 4 casas.
5
+ - VNA recebido no cálculo do PU: truncado a 6 casas.
6
+ - Prazo de desconto: dias úteis / 252, truncado a 14 casas.
7
+ - PU: truncado a 6 casas.
8
+ - Taxa implícita retornada: decimal, truncada a 8 casas,
9
+ equivalente a 6 casas em termos percentuais.
10
+ - Cada fluxo descontado: arredondado a 10 casas.
11
+
12
+ Valores de referência derivados conforme as regras da STN, exibidos em base 100.
13
+ Para NTN-C com vencimento em 01-01-2031:
14
+ PRINCIPAL = 100
15
+ TAXA_CUPOM = ((0.12 + 1) ** 0.5 - 1) * 100 # 12% a.a. com capitalização semestral
16
+ VALOR_CUPOM_2031 = round(TAXA_CUPOM, 6) -> 5.830052
17
+ VALOR_FINAL_2031 = 105.830052
18
+
19
+ Para as demais NTN-C:
20
+ TAXA_CUPOM = ((0.06 + 1) ** 0.5 - 1) * 100 # 6% a.a. com capitalização semestral
21
+ VALOR_CUPOM = round(TAXA_CUPOM, 6) -> 2.956301
22
+ VALOR_FINAL = 102.956301
23
+ """
24
+
1
25
  import datetime as dt
26
+ import math
2
27
  from decimal import Decimal
3
28
 
4
29
  import polars as pl
5
30
 
6
31
  import pyield._internal.converters as conversores
7
- from pyield import du
32
+ from pyield import du, interpolador
8
33
  from pyield._internal.numbers import truncar_decimal
9
34
  from pyield._internal.types import DateLike, any_is_empty
10
35
 
11
36
  from . import _utils
12
37
 
13
- """
14
- Constantes calculadas conforme regras da STN, exibidas em base 100.
15
- Válido para NTN-C com vencimento 01-01-2031:
16
- PRINCIPAL = 100
17
- TAXA_CUPOM = ((0.12 + 1) ** 0.5 - 1) * 100 # 12% a.a. com capitalização semestral
18
- VALOR_CUPOM_2031 = round(TAXA_CUPOM, 6) -> 5.830052
19
- VALOR_FINAL_2031 = principal + último cupom = 100 + 5.830052
20
-
21
- Para as demais NTN-C:
22
- TAXA_CUPOM = ((0.06 + 1) ** 0.5 - 1) * 100 # 6% a.a. com capitalização semestral
23
- VALOR_CUPOM = round(TAXA_CUPOM, 6) -> 2.956301
24
- VALOR_FINAL = principal + último cupom = 100 + 2.956301
25
- """
26
38
  # Valores usados nos cálculos, em base 100, com 6 casas decimais
27
39
  VALOR_CUPOM_2031 = 5.830052
28
40
  VALOR_FINAL_2031 = 105.830052
@@ -242,6 +254,8 @@ def cotacao(
242
254
  data_vencimento: Data de vencimento da NTN-C.
243
255
  taxa: Taxa de desconto (YTM) em formato decimal.
244
256
  Aceita também percentual explícito: "5.75%" ou "5,75%".
257
+ Antes do cálculo, é truncada em oito casas decimais (seis na
258
+ forma percentual), descartando as casas excedentes sem arredondar.
245
259
 
246
260
  Returns:
247
261
  Decimal: Cotação em base 100, truncada em 4 casas decimais.
@@ -263,8 +277,6 @@ def cotacao(
263
277
  >>> from pyield import ntnc
264
278
  >>> ntnc.cotacao("21-03-2025", "01-01-2031", "6.7626%")
265
279
  Decimal('126.4958')
266
- >>> ntnc.cotacao("21-05-2008", "01-03-2011", "6.9000009%")
267
- Decimal('99.0981')
268
280
  """
269
281
  taxa = _utils.converter_taxa(taxa)
270
282
  if any_is_empty(data_liquidacao, data_vencimento, taxa):
@@ -287,6 +299,137 @@ def cotacao(
287
299
  return truncar_decimal(cotacao_total, 4)
288
300
 
289
301
 
302
+ def _pagamentos_curva_zero(
303
+ data_liquidacao: DateLike,
304
+ data_vencimento: DateLike,
305
+ curva_zero: pl.DataFrame,
306
+ ) -> pl.DataFrame:
307
+ """Prepara fluxos e taxas da curva fornecida, sem escolher seu indexador."""
308
+ if not {"dias_uteis", "taxa_zero"}.issubset(curva_zero.columns):
309
+ raise ValueError(
310
+ "Curva zero deve conter as colunas 'dias_uteis' e 'taxa_zero'."
311
+ )
312
+ curva = curva_zero.select(
313
+ pl.col("dias_uteis").cast(pl.Float64),
314
+ pl.col("taxa_zero").cast(pl.Float64),
315
+ )
316
+ if (
317
+ curva.is_empty()
318
+ or curva.select(
319
+ (
320
+ pl.col("dias_uteis").is_null()
321
+ | ~pl.col("dias_uteis").is_finite()
322
+ | (pl.col("dias_uteis") <= 0)
323
+ | (pl.col("dias_uteis") != pl.col("dias_uteis").floor())
324
+ | pl.col("taxa_zero").is_null()
325
+ | ~pl.col("taxa_zero").is_finite()
326
+ | (pl.col("taxa_zero") <= -1)
327
+ ).any()
328
+ ).item()
329
+ ):
330
+ raise ValueError(
331
+ "Curva deve ter prazos inteiros positivos e taxas finitas > -1."
332
+ )
333
+ if curva["dias_uteis"].n_unique() != curva.height:
334
+ raise ValueError("Curva zero não pode conter prazos duplicados.")
335
+
336
+ fluxos = fluxos_caixa(data_liquidacao, data_vencimento)
337
+ dias = du.contar(data_liquidacao, fluxos["data_pagamento"])
338
+ taxas = interpolador.interpolar(
339
+ dias, curva["dias_uteis"].cast(pl.Int64), curva["taxa_zero"], extrapolar=True
340
+ )
341
+ return fluxos.with_columns(dias_uteis=dias, taxa=taxas)
342
+
343
+
344
+ def cotacao_curva_zero(
345
+ data_liquidacao: DateLike,
346
+ data_vencimento: DateLike,
347
+ curva_zero: pl.DataFrame,
348
+ ) -> float:
349
+ """Calcula a cotação da NTN-C descontando seus fluxos pela curva informada.
350
+
351
+ Args:
352
+ data_liquidacao: Data de liquidação, exclusiva para seleção dos fluxos.
353
+ data_vencimento: Data de vencimento da NTN-C.
354
+ curva_zero: DataFrame com ``dias_uteis`` (prazos inteiros positivos,
355
+ únicos) e ``taxa_zero`` (taxas anuais decimais finitas maiores que -1).
356
+ A curva deve conter ao menos um vértice; a ordem é indiferente.
357
+
358
+ Returns:
359
+ float: Cotação em base 100, sem truncamento final. Retorna NaN para
360
+ datas ausentes, ausência de fluxos ou resultado não finito.
361
+
362
+ Raises:
363
+ ValueError: Curva vazia, colunas ausentes ou vértices inválidos.
364
+
365
+ Notes:
366
+ Usa os fluxos contratuais de ``fluxos_caixa``, sem deslocar suas datas.
367
+ O prazo em dias úteis / 252 é truncado em 14 casas. A interpolação é
368
+ flat-forward, com taxa constante nas duas pontas da curva. Cada valor
369
+ presente é arredondado em 10 casas, como em ``cotacao``; a soma não é
370
+ truncada, pois serve de alvo para a TIR equivalente.
371
+
372
+ A curva é uma hipótese do consumidor. A biblioteca não escolhe uma
373
+ curva de IPCA como aproximação para IGP-M nem aplica piso ANBIMA.
374
+ Esta operação não representa uma cotação consultada em fonte externa.
375
+ """
376
+ if any_is_empty(data_liquidacao, data_vencimento):
377
+ return float("nan")
378
+ pagamentos = _pagamentos_curva_zero(data_liquidacao, data_vencimento, curva_zero)
379
+ if pagamentos.is_empty():
380
+ return float("nan")
381
+ cotacao = _utils.cotacao_por_taxas(pagamentos)
382
+ return cotacao if math.isfinite(cotacao) else float("nan")
383
+
384
+
385
+ def taxa_curva_zero(
386
+ data_liquidacao: DateLike,
387
+ data_vencimento: DateLike,
388
+ curva_zero: pl.DataFrame,
389
+ ) -> float:
390
+ """Calcula a TIR equivalente à cotação da NTN-C pela curva zero informada.
391
+
392
+ Args:
393
+ data_liquidacao: Data de liquidação.
394
+ data_vencimento: Data de vencimento da NTN-C.
395
+ curva_zero: DataFrame com ``dias_uteis`` e ``taxa_zero``, com os mesmos
396
+ requisitos de ``cotacao_curva_zero``.
397
+
398
+ Returns:
399
+ float: TIR anualizada decimal, sem arredondamento comercial. Retorna
400
+ NaN para datas ausentes, ausência de fluxos com prazo positivo,
401
+ resultado não finito ou falha de convergência.
402
+
403
+ Raises:
404
+ ValueError: Curva vazia, colunas ausentes ou vértices inválidos.
405
+
406
+ Notes:
407
+ Usa datas, interpolação e precisão de ``cotacao_curva_zero``. Resolve
408
+ por bisseção a taxa única que reproduz sua cotação, sem truncar a taxa
409
+ durante a busca. Não inverte um PU nem a cotação truncada de ``cotacao``.
410
+ O intervalo é delimitado pelas menores e maiores taxas interpoladas
411
+ dos fluxos. A busca para por erro de cotação zero ou meia largura do
412
+ intervalo inferior a 1e-12, com limite de 100 iterações.
413
+ A escolha da curva e eventuais spreads ou pisos ficam com o consumidor.
414
+ """
415
+ if any_is_empty(data_liquidacao, data_vencimento):
416
+ return float("nan")
417
+ pagamentos = _pagamentos_curva_zero(data_liquidacao, data_vencimento, curva_zero)
418
+ if pagamentos.is_empty() or not (pagamentos["dias_uteis"] > 0).any():
419
+ return float("nan")
420
+ alvo = _utils.cotacao_por_taxas(pagamentos)
421
+ if not math.isfinite(alvo):
422
+ return float("nan")
423
+
424
+ def erro(taxa: float) -> float:
425
+ return (
426
+ _utils.cotacao_por_taxas(pagamentos.with_columns(taxa=pl.lit(taxa))) - alvo
427
+ )
428
+
429
+ taxas = pagamentos["taxa"].sort()
430
+ return _utils.encontrar_raiz(erro, intervalo=(float(taxas[0]), float(taxas[-1])))
431
+
432
+
290
433
  def _calcular_pu(
291
434
  vna: float | Decimal,
292
435
  cotacao: float | Decimal,
@@ -309,8 +452,10 @@ def pu(
309
452
  pu = VNA * cotacao / 100
310
453
 
311
454
  Args:
312
- vna: Valor nominal atualizado (VNA).
455
+ vna: Valor nominal atualizado (VNA), truncado em seis casas decimais
456
+ antes do cálculo, sem arredondar.
313
457
  cotacao: Cotação da NTN-C em base 100.
458
+ Truncada em quatro casas decimais antes do cálculo, sem arredondar.
314
459
 
315
460
  Returns:
316
461
  Decimal: Preço da NTN-C truncado em 6 casas decimais.
@@ -324,8 +469,6 @@ def pu(
324
469
  >>> from pyield import ntnc
325
470
  >>> ntnc.pu(6598.913723, 126.4958)
326
471
  Decimal('8347.348705')
327
- >>> ntnc.pu(2126.4737349, 99.09819)
328
- Decimal('2107.295067')
329
472
  """
330
473
  return _calcular_pu(vna, cotacao)
331
474
 
@@ -7,7 +7,7 @@ Convenções de precificação (STN, tabela 3):
7
7
  equivalente a 6 casas em termos percentuais.
8
8
  - Cada fluxo descontado: arredondado a 9 casas.
9
9
 
10
- Constantes calculadas conforme regras da STN
10
+ Valores de referência derivados conforme as regras da STN:
11
11
  TAXA_CUPOM = (0.10 + 1) ** 0.5 - 1 -> 10% a.a. com capitalização semestral
12
12
  VALOR_FACE = 1000
13
13
  VALOR_CUPOM = round(VALOR_FACE * TAXA_CUPOM, 5)
@@ -40,7 +40,7 @@ from .pre import premios_pre as _premios_pre
40
40
 
41
41
  VALOR_FACE = 1000
42
42
  VALOR_CUPOM = 48.80885
43
- VALOR_FINAL = VALOR_FACE + VALOR_CUPOM
43
+ VALOR_FINAL = 1048.80885
44
44
 
45
45
  logger = logging.getLogger(__name__)
46
46
 
@@ -340,6 +340,8 @@ def pu(
340
340
  data_vencimento (DateLike): Data de vencimento do título.
341
341
  taxa: Taxa de desconto (TIR) em formato decimal.
342
342
  Aceita também percentual explícito: "5.75%" ou "5,75%".
343
+ Antes do cálculo, é truncada em oito casas decimais (seis na
344
+ forma percentual), descartando as casas excedentes sem arredondar.
343
345
 
344
346
  Returns:
345
347
  Decimal: Preço da NTN-F truncado em seis casas decimais.
@@ -357,8 +359,6 @@ def pu(
357
359
  Decimal('895.359254')
358
360
  >>> ntnf.pu("01-07-2024", "01-01-2027", "10%")
359
361
  Decimal('999.931303')
360
- >>> ntnf.pu("21-05-2008", "01-01-2014", "13.6600009%")
361
- Decimal('903.075616')
362
362
  """
363
363
  return truncar_decimal(_calcular_pu(data_liquidacao, data_vencimento, taxa), 6)
364
364
 
@@ -10,4 +10,12 @@ from pyield.vna._consulta import (
10
10
  vigencia,
11
11
  )
12
12
 
13
- __all__ = ["calcular_vna", "TipoTitulo", "historico", "projetado", "ultimo", "valor", "vigencia"]
13
+ __all__ = [
14
+ "calcular_vna",
15
+ "TipoTitulo",
16
+ "historico",
17
+ "projetado",
18
+ "ultimo",
19
+ "valor",
20
+ "vigencia",
21
+ ]
@@ -6,6 +6,7 @@ from typing import Literal
6
6
 
7
7
  import polars as pl
8
8
 
9
+ from pyield import du
9
10
  from pyield._internal.converters import converter_datas
10
11
  from pyield._internal.types import DateLike, any_is_empty
11
12
  from pyield.vna import _lft, _ntnb, _ntnc
@@ -171,26 +172,73 @@ def projetado(
171
172
  titulo: TipoTitulo,
172
173
  data: DateLike,
173
174
  vna_base: float | Decimal,
174
- inflacao: float | Decimal,
175
+ inflacao: float | Decimal | None = None,
176
+ *,
177
+ selic: float | Decimal | str | None = None,
175
178
  ) -> Decimal:
176
- """Projeta o VNA mensal com base e inflação fornecidas pelo consumidor.
179
+ """Projeta o VNA do título conforme sua metodologia.
177
180
 
178
181
  Args:
179
- titulo: NTN-B ou NTN-C. Projeção de LFT não está disponível.
182
+ titulo: LFT, NTN-B ou NTN-C.
180
183
  data: Data para a qual projetar o VNA.
181
- vna_base: VNA no início da vigência que contém a data.
182
- inflacao: Variação mensal percentual; 0.45 representa 0,45%.
184
+ vna_base: VNA-base correspondente ao início da vigência ou ao último
185
+ dia útil anterior à data, conforme o título.
186
+ inflacao: Para NTN-B e NTN-C, variação mensal percentual; ``0.45``
187
+ representa 0,45%. Para LFT, taxa anual decimal, finita e maior
188
+ que -1; informe-a em ``selic``.
189
+ selic: Obrigatória apenas para LFT. Aceita taxa decimal ou
190
+ percentual explícito, como ``"13.65%"``. Para reproduzir o
191
+ acruamento oficial da LFT, use a taxa Selic diária publicada pelo
192
+ SGS 11. Como ela é publicada em percentual, ``0,050788%`` equivale
193
+ à taxa decimal ``0,00050788`` e ao fator derivado ``1,00050788``.
194
+ Converta essa taxa diária para uma taxa anual equivalente. A Selic
195
+ Over do SGS 1178 é anualizada e serve apenas como aproximação. Para
196
+ NTN-B e NTN-C, deve ser omitida.
183
197
 
184
198
  Returns:
185
199
  Decimal: VNA projetado com seis casas; NaN para entradas nulas.
186
200
 
187
201
  Notes:
188
- Segue a metodologia STN das funções de projeção de NTN-B e NTN-C:
189
- base truncada em seis casas, inflação arredondada em duas e expoente
190
- em dias corridos truncado em catorze. Não busca projeções externas.
202
+ NTN-B e NTN-C seguem a metodologia STN: base truncada em seis casas,
203
+ inflação arredondada em duas e expoente em dias corridos truncado em
204
+ catorze. LFT usa dias úteis, base 252 e capitalização pela taxa anual
205
+ informada. A taxa Selic diária publicada pelo SGS 11 é a referência
206
+ exata para o acruamento; o fator usado no cálculo é ``1 + taxa`` em
207
+ formato decimal. A função não busca a taxa automaticamente nem faz
208
+ projeções externas.
191
209
 
192
210
  Raises:
193
- ValueError: Se o título não for NTN-B ou NTN-C, a base não for positiva
194
- ou a inflação for menor ou igual a -100%.
211
+ ValueError: Se o título for inválido, a taxa for informada para o
212
+ título errado, ou houver violação dos limites da metodologia
213
+ específica.
214
+
215
+ Examples:
216
+ >>> import pyield as yd
217
+ >>> yd.vna.projetado("NTN-B", "30-06-2026", 4731.856412, 0.45)
218
+ Decimal('4742.491138')
219
+ >>> yd.vna.projetado(
220
+ ... "LFT",
221
+ ... "18-09-2026",
222
+ ... 19905.773236,
223
+ ... selic="13.65%",
224
+ ... )
225
+ Decimal('19915.882987')
195
226
  """
227
+ if titulo not in {"LFT", "NTN-B", "NTN-C"}:
228
+ raise ValueError("Título deve ser LFT, NTN-B ou NTN-C.")
229
+ if titulo == "LFT":
230
+ if inflacao is not None:
231
+ raise ValueError("Use selic para a projeção da LFT.")
232
+ if selic is None:
233
+ return Decimal("NaN")
234
+ if any_is_empty(selic):
235
+ return Decimal("NaN")
236
+ data_convertida = converter_datas(data)
237
+ if du.eh_dia_util(data_convertida):
238
+ data_base = du.deslocar(data_convertida, -1, ajuste="anterior")
239
+ else:
240
+ data_base = du.deslocar(data_convertida, 0, ajuste="anterior")
241
+ return _lft.projetado_lft(data_base, data, vna_base, selic)
242
+ if selic is not None:
243
+ raise ValueError("selic aplica-se apenas à projeção da LFT.")
196
244
  return _modulo_mensal(titulo).vna_projetado(data, vna_base, inflacao)
@@ -16,16 +16,99 @@ valores e retorna o VNA único.
16
16
  """
17
17
 
18
18
  import datetime as dt
19
- from decimal import Decimal
19
+ import logging
20
+ from decimal import Decimal, localcontext
20
21
 
21
22
  import requests
22
23
 
24
+ from pyield import du
23
25
  from pyield._internal.cache import ttl_cache
24
26
  from pyield._internal.converters import converter_datas, data_referencia_valida
27
+ from pyield._internal.numbers import truncar_decimal
25
28
  from pyield._internal.retry import retry_padrao
26
29
  from pyield._internal.types import DateLike, any_is_empty
30
+ from pyield.tpf.titulos._utils import converter_taxa
27
31
 
28
32
  CODIGO_LFT = "210100"
33
+ _LOGGER = logging.getLogger(__name__)
34
+
35
+
36
+ def projetado_lft(
37
+ data_base: DateLike,
38
+ data: DateLike,
39
+ vna_base: float | Decimal,
40
+ taxa_anual: float | Decimal | str,
41
+ ) -> Decimal:
42
+ """Projeta o VNA da LFT com uma taxa anual constante fornecida pelo consumidor.
43
+
44
+ Args:
45
+ data_base: Data à qual corresponde o VNA-base.
46
+ data: Data do próximo dia útil de acruamento após a data-base.
47
+ vna_base: VNA-base em reais, positivo e finito.
48
+ taxa_anual: Taxa anual decimal, finita e maior que -1. Aceita também
49
+ percentual explícito, como ``"13.65%"`` ou ``"13,65%"``.
50
+
51
+ Returns:
52
+ Decimal: VNA projetado, truncado em seis casas decimais. Retorna
53
+ ``Decimal('NaN')`` para entradas ausentes ou NaN.
54
+
55
+ Notes:
56
+ Calcula ``vna_base * (1 + taxa_anual) ** (dias_uteis / 252)``.
57
+ Usa ``yd.du.contar`` com calendário automático de feriados brasileiros:
58
+ data-base inclusiva e data final exclusiva, sem deslocar as datas.
59
+ Com zero dias úteis, retorna o VNA-base truncado em seis casas.
60
+
61
+ O cálculo usa Decimal com 28 dígitos de precisão. Entradas float são
62
+ convertidas pela representação decimal textual. Percentuais explícitos
63
+ são convertidos para decimal sem truncamento. Não há truncamentos
64
+ intermediários nem arredondamento comercial da taxa.
65
+
66
+ Não consulta fontes externas nem escolhe automaticamente uma taxa. Para
67
+ reproduzir o acruamento oficial da LFT, use a taxa Selic diária
68
+ publicada pela série SGS 11. Como ela é publicada em percentual,
69
+ ``0,050788%`` corresponde à taxa decimal ``0,00050788`` e ao fator
70
+ derivado ``1,00050788``. Converta essa taxa diária para uma taxa anual
71
+ equivalente antes de informar ``taxa_anual``. A Selic Over da série
72
+ SGS 1178 é anualizada e serve apenas como aproximação. O resultado é
73
+ uma projeção sob a taxa informada, não um VNA oficial realizado. A
74
+ taxa pode ser negativa, desde que maior que -100%.
75
+
76
+ Raises:
77
+ ValueError: Data final anterior à base, VNA não positivo ou não finito,
78
+ taxa não finita ou menor ou igual a -100%, ou data malformada.
79
+
80
+ Examples:
81
+ >>> from decimal import Decimal
82
+ >>> projetado_lft(
83
+ ... "17-09-2026",
84
+ ... "18-09-2026",
85
+ ... Decimal("19905.773236"),
86
+ ... "13.65%",
87
+ ... )
88
+ Decimal('19915.882987')
89
+ """
90
+ if any_is_empty(data_base, data, vna_base, taxa_anual):
91
+ return Decimal("NaN")
92
+ inicio = converter_datas(data_base)
93
+ fim = converter_datas(data)
94
+ if fim < inicio:
95
+ raise ValueError("A data de projeção não pode ser anterior à data-base.")
96
+ dias_uteis = du.contar(inicio, fim)
97
+ if dias_uteis != 1:
98
+ _LOGGER.warning(
99
+ "A projeção da LFT avançou uma quantidade diferente de um dia útil.",
100
+ )
101
+ base = vna_base if isinstance(vna_base, Decimal) else Decimal(str(vna_base))
102
+ taxa_anual = converter_taxa(taxa_anual)
103
+ taxa = taxa_anual if isinstance(taxa_anual, Decimal) else Decimal(str(taxa_anual))
104
+ if not base.is_finite() or base <= 0:
105
+ raise ValueError("O VNA-base deve ser positivo e finito.")
106
+ if not taxa.is_finite() or taxa <= -1:
107
+ raise ValueError("A taxa anual deve ser finita e maior que -100%.")
108
+ with localcontext() as contexto:
109
+ contexto.prec = 28
110
+ expoente = Decimal(dias_uteis) / 252
111
+ return truncar_decimal(base * (1 + taxa) ** expoente, 6)
29
112
 
30
113
 
31
114
  @ttl_cache()
@@ -25,7 +25,7 @@ dependencies = [
25
25
  "fastexcel>=0.19.0",
26
26
  "tzdata>=2024.1; platform_system == 'Windows'",
27
27
  ]
28
- version = "0.58.0"
28
+ version = "0.58.2"
29
29
 
30
30
  [[project.authors]]
31
31
  name = "Carlos Carvalho"
@@ -20,7 +20,7 @@ dependencies = [
20
20
  "fastexcel>=0.19.0",
21
21
  "tzdata>=2024.1; platform_system == 'Windows'",
22
22
  ]
23
- version = "0.58.0"
23
+ version = "0.58.2"
24
24
 
25
25
  [project.urls]
26
26
  Homepage = "https://github.com/crdcj/PYield"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes