pyield 0.57.1__tar.gz → 0.58.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {pyield-0.57.1 → pyield-0.58.0}/PKG-INFO +33 -4
  2. {pyield-0.57.1 → pyield-0.58.0}/README.md +32 -3
  3. {pyield-0.57.1 → pyield-0.58.0}/pyield/__init__.py +1 -2
  4. {pyield-0.57.1 → pyield-0.58.0}/pyield/_internal/cache.py +7 -1
  5. {pyield-0.57.1 → pyield-0.58.0}/pyield/bc/leiloes.py +2 -2
  6. {pyield-0.57.1 → pyield-0.58.0}/pyield/cpm/__init__.py +4 -0
  7. pyield-0.58.0/pyield/cpm/probabilidades.py +347 -0
  8. {pyield-0.57.1 → pyield-0.58.0}/pyield/fwd.py +20 -68
  9. {pyield-0.57.1 → pyield-0.58.0}/pyield/interpolador.py +50 -81
  10. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/__init__.py +1 -2
  11. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/leiloes.py +2 -2
  12. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/_ntnb_bootstrap.py +114 -147
  13. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/_ntnf_bootstrap.py +3 -2
  14. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/_utils.py +85 -122
  15. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/lft.py +65 -48
  16. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/ltn.py +64 -54
  17. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/ntnb.py +100 -79
  18. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/ntnb1.py +121 -57
  19. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/ntnbp.py +24 -17
  20. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/ntnc.py +88 -79
  21. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/ntnf.py +122 -120
  22. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/pre.py +71 -5
  23. {pyield-0.57.1 → pyield-0.58.0}/pyield/vna/_consulta.py +17 -5
  24. {pyield-0.57.1 → pyield-0.58.0}/pyield/vna/_lft.py +2 -2
  25. {pyield-0.57.1 → pyield-0.58.0}/pyield/vna/_ntnb.py +4 -4
  26. {pyield-0.57.1 → pyield-0.58.0}/pyield/vna/_ntnc.py +5 -3
  27. {pyield-0.57.1 → pyield-0.58.0}/pyproject.toml +1 -1
  28. {pyield-0.57.1 → pyield-0.58.0}/pyproject.toml.orig +1 -1
  29. pyield-0.57.1/pyield/.DS_Store +0 -0
  30. pyield-0.57.1/pyield/cpm/probabilidades.py +0 -343
  31. {pyield-0.57.1 → pyield-0.58.0}/LICENSE +0 -0
  32. {pyield-0.57.1 → pyield-0.58.0}/pyield/_internal/__init__.py +0 -0
  33. {pyield-0.57.1 → pyield-0.58.0}/pyield/_internal/br_numbers.py +0 -0
  34. {pyield-0.57.1 → pyield-0.58.0}/pyield/_internal/converters.py +0 -0
  35. {pyield-0.57.1 → pyield-0.58.0}/pyield/_internal/data_cache.py +0 -0
  36. {pyield-0.57.1 → pyield-0.58.0}/pyield/_internal/excel.py +0 -0
  37. {pyield-0.57.1 → pyield-0.58.0}/pyield/_internal/numbers.py +0 -0
  38. {pyield-0.57.1 → pyield-0.58.0}/pyield/_internal/retry.py +0 -0
  39. {pyield-0.57.1 → pyield-0.58.0}/pyield/_internal/types.py +0 -0
  40. {pyield-0.57.1 → pyield-0.58.0}/pyield/anbima/__init__.py +0 -0
  41. {pyield-0.57.1 → pyield-0.58.0}/pyield/anbima/imaq.py +0 -0
  42. {pyield-0.57.1 → pyield-0.58.0}/pyield/anbima/taxas.py +0 -0
  43. {pyield-0.57.1 → pyield-0.58.0}/pyield/b3/__init__.py +0 -0
  44. {pyield-0.57.1 → pyield-0.58.0}/pyield/b3/_contratos.py +0 -0
  45. {pyield-0.57.1 → pyield-0.58.0}/pyield/b3/_validar_pregao.py +0 -0
  46. {pyield-0.57.1 → pyield-0.58.0}/pyield/b3/boletim.py +0 -0
  47. {pyield-0.57.1 → pyield-0.58.0}/pyield/b3/derivativos_intradia.py +0 -0
  48. {pyield-0.57.1 → pyield-0.58.0}/pyield/b3/di_over.py +0 -0
  49. {pyield-0.57.1 → pyield-0.58.0}/pyield/bc/__init__.py +0 -0
  50. {pyield-0.57.1 → pyield-0.58.0}/pyield/bc/_olinda.py +0 -0
  51. {pyield-0.57.1 → pyield-0.58.0}/pyield/bc/sgs.py +0 -0
  52. {pyield-0.57.1 → pyield-0.58.0}/pyield/compromissada.py +0 -0
  53. {pyield-0.57.1 → pyield-0.58.0}/pyield/copom/__init__.py +0 -0
  54. {pyield-0.57.1 → pyield-0.58.0}/pyield/copom/_calendario.py +0 -0
  55. {pyield-0.57.1 → pyield-0.58.0}/pyield/du/__init__.py +0 -0
  56. {pyield-0.57.1 → pyield-0.58.0}/pyield/du/core.py +0 -0
  57. {pyield-0.57.1 → pyield-0.58.0}/pyield/du/feriados_br/__init__.py +0 -0
  58. {pyield-0.57.1 → pyield-0.58.0}/pyield/du/feriados_br/anteriores.txt +0 -0
  59. {pyield-0.57.1 → pyield-0.58.0}/pyield/du/feriados_br/atuais.txt +0 -0
  60. {pyield-0.57.1 → pyield-0.58.0}/pyield/futuro/__init__.py +0 -0
  61. {pyield-0.57.1 → pyield-0.58.0}/pyield/futuro/contratos.py +0 -0
  62. {pyield-0.57.1 → pyield-0.58.0}/pyield/futuro/di1.py +0 -0
  63. {pyield-0.57.1 → pyield-0.58.0}/pyield/futuro/historico.py +0 -0
  64. {pyield-0.57.1 → pyield-0.58.0}/pyield/futuro/intradia.py +0 -0
  65. {pyield-0.57.1 → pyield-0.58.0}/pyield/ipca/__init__.py +0 -0
  66. {pyield-0.57.1 → pyield-0.58.0}/pyield/ipca/historico.py +0 -0
  67. {pyield-0.57.1 → pyield-0.58.0}/pyield/ipca/projetado.py +0 -0
  68. {pyield-0.57.1 → pyield-0.58.0}/pyield/py.typed +0 -0
  69. {pyield-0.57.1 → pyield-0.58.0}/pyield/relogio.py +0 -0
  70. {pyield-0.57.1/pyield/tpf → pyield-0.58.0/pyield}/rmd/__init__.py +0 -0
  71. {pyield-0.57.1/pyield/tpf → pyield-0.58.0/pyield}/rmd/_aba_1_3.py +0 -0
  72. {pyield-0.57.1/pyield/tpf → pyield-0.58.0/pyield}/rmd/_aba_2_1.py +0 -0
  73. {pyield-0.57.1/pyield/tpf → pyield-0.58.0/pyield}/rmd/_common.py +0 -0
  74. {pyield-0.57.1/pyield/tpf → pyield-0.58.0/pyield}/rmd/_download.py +0 -0
  75. {pyield-0.57.1 → pyield-0.58.0}/pyield/selic/__init__.py +0 -0
  76. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/_taxas.py +0 -0
  77. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/benchmark.py +0 -0
  78. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/dealers.py +0 -0
  79. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/secundario/__init__.py +0 -0
  80. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/secundario/_intradia.py +0 -0
  81. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/secundario/_mensal.py +0 -0
  82. {pyield-0.57.1 → pyield-0.58.0}/pyield/tpf/titulos/__init__.py +0 -0
  83. {pyield-0.57.1 → pyield-0.58.0}/pyield/vna/__init__.py +0 -0
  84. {pyield-0.57.1 → pyield-0.58.0}/pyield/vna/_calculo.py +0 -0
  85. {pyield-0.57.1 → pyield-0.58.0}/pyield/vna/_download.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyield
3
- Version: 0.57.1
3
+ Version: 0.58.0
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
@@ -84,7 +84,7 @@ uv add pyield
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` |
85
85
  | `yd.ntnbp` | módulo | NTN-B Principal | `cotacao`, `taxa`, `pu`, `dv01` |
86
86
  | `yd.ntnc` | módulo | NTN-C | `dados`, `datas_pagamento`, `fluxos_caixa`, `cotacao`, `pu`, `taxa`, `duration`, `duration_expr`, `dv01`, `dv01_expr` |
87
- | `yd.ntnf` | módulo | NTN-F | `dados`, `vencimentos`, `datas_pagamento`, `fluxos_caixa`, `pu`, `taxa`, `taxas_zero`, `premio`, `premio_limpo`, `premio_limpo_expr`, `rentabilidade`, `rentabilidade_expr`, `duration`, `duration_expr`, `dv01`, `dv01_expr` |
87
+ | `yd.ntnf` | módulo | NTN-F | `dados`, `vencimentos`, `datas_pagamento`, `fluxos_caixa`, `pu`, `taxa`, `taxas_zero`, `taxas_zero_forwards`, `premio`, `premio_limpo`, `premio_limpo_expr`, `rentabilidade`, `rentabilidade_expr`, `duration`, `duration_expr`, `dv01`, `dv01_expr` |
88
88
  | `yd.vna` | módulo | Valores nominais atualizados dos títulos públicos | `valor`, `historico`, `projetado`, `vigencia` |
89
89
  | `yd.copom` | módulo | Calendário automático do Copom | `calendario`, `proxima_reuniao` |
90
90
  | `yd.compromissadas(...)` | função | Leilões de operações compromissadas do BCB | `inicio`, `fim` |
@@ -102,6 +102,32 @@ documentação detalhada e as assinaturas públicas.
102
102
 
103
103
  ## Compatibilidade da API
104
104
 
105
+ ### Cotações em base 100 — 0.58.0
106
+
107
+ As funções escalares de cálculo dos títulos também aceitam taxas percentuais
108
+ explícitas, como `"5.75%"` ou `"5,75%"`, equivalentes a `0.0575`.
109
+ Números continuam em formato decimal; strings sem `%` são rejeitadas.
110
+
111
+ As funções `cotacao` de `yd.lft`, `yd.ntnb`, `yd.ntnc`, `yd.ntnbp` e
112
+ `yd.ntnb1` passam a retornar base 100 com quatro casas decimais:
113
+ `0.993651` passa a ser `99.3651`. As funções `pu` desses títulos recebem a
114
+ cotação nessa mesma base e aplicam `VNA * cotacao / 100`.
115
+
116
+ A coluna `valor_pagamento` de `fluxos_caixa` de NTN-B, NTN-C e NTN-B1 e o
117
+ retorno de `ntnb1.cotacao_curva_zero` também passam para base 100. Esta última
118
+ mantém a soma sem truncamento para calibrar a TIR equivalente.
119
+
120
+ Para migrar, multiplique cotações e fluxos antigos armazenados por 100, remova
121
+ multiplicações por 100 usadas apenas para exibir cotações e ajuste cálculos
122
+ manuais de PU para dividir por 100. A composição `pu(vna, cotacao(...))`
123
+ preserva a unidade e as regras de precisão do preço. Taxas continuam em formato
124
+ decimal, e VNA e PU continuam em reais.
125
+
126
+ As funções `yd.ltn.dv01` e `yd.ntnf.dv01` não recebem mais um PU de referência:
127
+ calculam a redução do PU teórico para um aumento de 1 ponto-base na taxa.
128
+
129
+ ### Demais alterações
130
+
105
131
  Este resumo parte da API da `0.56.0`. Mudanças que já faziam parte dessa versão
106
132
  ou de versões anteriores estão no histórico das
107
133
  [releases do GitHub](https://github.com/crdcj/PYield/releases).
@@ -119,8 +145,11 @@ ou de versões anteriores estão no histórico das
119
145
  | `vna_projetado(...)` nos módulos de títulos | `yd.vna.projetado(titulo, data, vna_base, inflacao)` |
120
146
  | `vigencia(data)` nos módulos de títulos | `yd.vna.vigencia(titulo, data)` |
121
147
 
122
- Os módulos públicos de títulos ficam disponíveis na raiz, enquanto as
123
- implementações continuam organizadas internamente em `pyield/tpf/titulos/`.
148
+ O namespace público segue a autonomia semântica do conceito que o usuário
149
+ precisa conhecer, e não apenas a fonte dos dados ou sua relação temática com
150
+ outro domínio. Por isso, conceitos como `yd.ltn`, `yd.ntnb`, `yd.vna` e `yd.rmd`
151
+ ficam na raiz, enquanto as implementações podem continuar agrupadas
152
+ internamente por domínio para manter o código coeso.
124
153
  Consulte as [releases](https://github.com/crdcj/PYield/releases) para o
125
154
  histórico completo.
126
155
 
@@ -59,7 +59,7 @@ uv add pyield
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` |
60
60
  | `yd.ntnbp` | módulo | NTN-B Principal | `cotacao`, `taxa`, `pu`, `dv01` |
61
61
  | `yd.ntnc` | módulo | NTN-C | `dados`, `datas_pagamento`, `fluxos_caixa`, `cotacao`, `pu`, `taxa`, `duration`, `duration_expr`, `dv01`, `dv01_expr` |
62
- | `yd.ntnf` | módulo | NTN-F | `dados`, `vencimentos`, `datas_pagamento`, `fluxos_caixa`, `pu`, `taxa`, `taxas_zero`, `premio`, `premio_limpo`, `premio_limpo_expr`, `rentabilidade`, `rentabilidade_expr`, `duration`, `duration_expr`, `dv01`, `dv01_expr` |
62
+ | `yd.ntnf` | módulo | NTN-F | `dados`, `vencimentos`, `datas_pagamento`, `fluxos_caixa`, `pu`, `taxa`, `taxas_zero`, `taxas_zero_forwards`, `premio`, `premio_limpo`, `premio_limpo_expr`, `rentabilidade`, `rentabilidade_expr`, `duration`, `duration_expr`, `dv01`, `dv01_expr` |
63
63
  | `yd.vna` | módulo | Valores nominais atualizados dos títulos públicos | `valor`, `historico`, `projetado`, `vigencia` |
64
64
  | `yd.copom` | módulo | Calendário automático do Copom | `calendario`, `proxima_reuniao` |
65
65
  | `yd.compromissadas(...)` | função | Leilões de operações compromissadas do BCB | `inicio`, `fim` |
@@ -77,6 +77,32 @@ documentação detalhada e as assinaturas públicas.
77
77
 
78
78
  ## Compatibilidade da API
79
79
 
80
+ ### Cotações em base 100 — 0.58.0
81
+
82
+ As funções escalares de cálculo dos títulos também aceitam taxas percentuais
83
+ explícitas, como `"5.75%"` ou `"5,75%"`, equivalentes a `0.0575`.
84
+ Números continuam em formato decimal; strings sem `%` são rejeitadas.
85
+
86
+ As funções `cotacao` de `yd.lft`, `yd.ntnb`, `yd.ntnc`, `yd.ntnbp` e
87
+ `yd.ntnb1` passam a retornar base 100 com quatro casas decimais:
88
+ `0.993651` passa a ser `99.3651`. As funções `pu` desses títulos recebem a
89
+ cotação nessa mesma base e aplicam `VNA * cotacao / 100`.
90
+
91
+ A coluna `valor_pagamento` de `fluxos_caixa` de NTN-B, NTN-C e NTN-B1 e o
92
+ retorno de `ntnb1.cotacao_curva_zero` também passam para base 100. Esta última
93
+ mantém a soma sem truncamento para calibrar a TIR equivalente.
94
+
95
+ Para migrar, multiplique cotações e fluxos antigos armazenados por 100, remova
96
+ multiplicações por 100 usadas apenas para exibir cotações e ajuste cálculos
97
+ manuais de PU para dividir por 100. A composição `pu(vna, cotacao(...))`
98
+ preserva a unidade e as regras de precisão do preço. Taxas continuam em formato
99
+ decimal, e VNA e PU continuam em reais.
100
+
101
+ As funções `yd.ltn.dv01` e `yd.ntnf.dv01` não recebem mais um PU de referência:
102
+ calculam a redução do PU teórico para um aumento de 1 ponto-base na taxa.
103
+
104
+ ### Demais alterações
105
+
80
106
  Este resumo parte da API da `0.56.0`. Mudanças que já faziam parte dessa versão
81
107
  ou de versões anteriores estão no histórico das
82
108
  [releases do GitHub](https://github.com/crdcj/PYield/releases).
@@ -94,8 +120,11 @@ ou de versões anteriores estão no histórico das
94
120
  | `vna_projetado(...)` nos módulos de títulos | `yd.vna.projetado(titulo, data, vna_base, inflacao)` |
95
121
  | `vigencia(data)` nos módulos de títulos | `yd.vna.vigencia(titulo, data)` |
96
122
 
97
- Os módulos públicos de títulos ficam disponíveis na raiz, enquanto as
98
- implementações continuam organizadas internamente em `pyield/tpf/titulos/`.
123
+ O namespace público segue a autonomia semântica do conceito que o usuário
124
+ precisa conhecer, e não apenas a fonte dos dados ou sua relação temática com
125
+ outro domínio. Por isso, conceitos como `yd.ltn`, `yd.ntnb`, `yd.vna` e `yd.rmd`
126
+ ficam na raiz, enquanto as implementações podem continuar agrupadas
127
+ internamente por domínio para manter o código coeso.
99
128
  Consulte as [releases](https://github.com/crdcj/PYield/releases) para o
100
129
  histórico completo.
101
130
 
@@ -11,13 +11,12 @@ from pyield.futuro import di1
11
11
  # Ordem intencional: alguns módulos importam `di1` a partir de `pyield`
12
12
  # durante a inicialização do pacote.
13
13
  from pyield import cpm, copom, selic, tpf, vna
14
- from pyield.cpm import probabilidades as _cpm_probabilidades # noqa: F401
15
14
  from pyield.bc.sgs import ptax, ptax_serie
16
15
  from pyield.compromissada import compromissadas
17
16
  from pyield.fwd import forward, forwards, forwards_expr
18
17
  from pyield.interpolador import Interpolador, interpolar
19
18
  from pyield.relogio import agora, hoje
20
- from pyield.tpf.rmd import rmd
19
+ from pyield.rmd import rmd
21
20
  from pyield.tpf.titulos import lft, ltn, ntnb, ntnb1, ntnbp, ntnc, ntnf
22
21
 
23
22
  try:
@@ -15,6 +15,11 @@ def ttl_cache(ttl: int = _TTL_PADRAO, maxsize: int = _TAMANHO_MAXIMO):
15
15
  Args:
16
16
  ttl: Tempo de vida de cada entrada em segundos.
17
17
  maxsize: Número máximo de entradas no cache.
18
+
19
+ Notes:
20
+ O wrapper consome _atualizar=True para ignorar e renovar apenas a
21
+ entrada consultada. Erros são propagados, preservando a entrada
22
+ anterior até sua expiração.
18
23
  """
19
24
 
20
25
  def decorador(func):
@@ -22,9 +27,10 @@ def ttl_cache(ttl: int = _TTL_PADRAO, maxsize: int = _TAMANHO_MAXIMO):
22
27
 
23
28
  @wraps(func)
24
29
  def wrapper(*args, **kwargs):
30
+ atualizar = kwargs.pop("_atualizar", False)
25
31
  chave = (args, tuple(sorted(kwargs.items())))
26
32
  agora = time.monotonic()
27
- if chave in _cache:
33
+ if not atualizar and chave in _cache:
28
34
  resultado, expira_em = _cache[chave]
29
35
  if agora < expira_em:
30
36
  return resultado
@@ -93,13 +93,13 @@ def _processar_df(df: pl.DataFrame) -> pl.DataFrame:
93
93
  pl.when(pl.col("titulo") == "LTN")
94
94
  .then(
95
95
  ltn.dv01_expr(
96
- "data_liquidacao", "data_vencimento", "taxa_media", "pu_medio"
96
+ "data_liquidacao", "data_vencimento", "taxa_media"
97
97
  )
98
98
  )
99
99
  .when(pl.col("titulo") == "NTN-F")
100
100
  .then(
101
101
  ntnf.dv01_expr(
102
- "data_liquidacao", "data_vencimento", "taxa_media", "pu_medio"
102
+ "data_liquidacao", "data_vencimento", "taxa_media"
103
103
  )
104
104
  )
105
105
  .when(pl.col("titulo") == "NTN-B")
@@ -350,3 +350,7 @@ def data(date: DateLike) -> pl.DataFrame:
350
350
  pl.col("preco_ajuste"),
351
351
  pl.col("dias_uteis"),
352
352
  ).sort("data_expiracao", "variacao_strike_bps")
353
+
354
+
355
+ # Importado após as definições porque probabilidades utiliza este módulo.
356
+ from pyield.cpm import probabilidades as probabilidades # noqa: E402, PLC0414
@@ -0,0 +1,347 @@
1
+ """
2
+ probabilidades — Probabilidades implícitas das reuniões do COPOM a partir dos
3
+ preços das opções CPM.
4
+
5
+ O contrato CPM é uma opção europeia cash-or-nothing. No apreçamento neutro ao
6
+ risco, o preço de ajuste da B3 em pontos (0–100) representa a probabilidade
7
+ implícita de cada cenário de alteração da Selic, descontada pela taxa DI até o
8
+ vencimento (Manual de Apreçamento da B3, §3.5).
9
+
10
+ Convenções de probabilidade
11
+ ---------------------------
12
+ prob_bruta = preco_ajuste * fator_desconto / 100
13
+
14
+ Probabilidade neutra ao risco direta segundo o Manual da B3 §3.5
15
+ (fórmula invertida):
16
+
17
+ p_n(K) = PR_n * exp(+n * r_n) / N
18
+
19
+ onde
20
+ PR_n = preco_ajuste ("Preço de Referência" da B3, pontos 0–100)
21
+ N = 100 (valor nocional fixo)
22
+ n = dias_uteis / 252 (tempo em anos, convenção de dias úteis)
23
+ r_n = ln(1 + taxa_di1) (taxa DI1 continuamente capitalizada até o
24
+ vencimento)
25
+ taxa_di1 = taxa DI1 interpolada por flat-forward entre a data de
26
+ referência e a data de expiração
27
+
28
+ O preco_ajuste de cpm.data() é o "Preço de Referência" oficial da B3,
29
+ obtido no endpoint CSV: o preço exibido no painel da B3 ("Probabilidades
30
+ da Taxa Selic Meta") e resultado da metodologia P1/P2/P3/P4 da B3. Pode
31
+ ser nulo para datas com mais de aproximadamente um mês, quando o
32
+ endpoint CSV não está disponível.
33
+
34
+ Pode não somar 1,0 por reunião devido ao spread entre oferta e demanda ou
35
+ ao apreçamento P1/P2 da B3. Quando dias_uteis == 0 (o próprio dia da
36
+ reunião), fator_desconto == 1,0 exatamente e prob_bruta se reduz a
37
+ preco_ajuste / 100.
38
+
39
+ prob = prob_bruta / soma(prob_bruta) no grupo de data_expiracao
40
+ Normalizada para que cada reunião some exatamente 1,0. Este é o ajuste de
41
+ apreçamento P3 da B3. Use esta coluna para análise de cenários e gráficos.
42
+
43
+ prob_acumulada = soma acumulada de prob, ordenada por
44
+ variacao_strike_bps em ordem crescente.
45
+
46
+ Observações sobre preco_ajuste nulo
47
+ -----------------------------------
48
+ Às vezes os contratos CPM são listados sem preço de ajuste (não há apreçamento
49
+ oficial da B3 para aquele strike na data, ou o endpoint CSV não está disponível
50
+ para datas antigas). Strikes com preco_ajuste nulo são excluídos da saída de
51
+ probabilidades porque:
52
+
53
+ 1. Sua contribuição para a distribuição normalizada é indefinida.
54
+ 2. ``group_by().agg(sum())`` do Polars retorna 0,0 (e não nulo) para grupos
55
+ totalmente nulos, o que quebraria a invariável ``prob.sum() == 1.0`` por
56
+ reunião.
57
+
58
+ Como consequência, uma reunião em que TODOS os strikes listados tenham preços
59
+ nulos (por exemplo, CPMH25 no dia da reunião do COPOM de janeiro de 2025) não
60
+ aparecerá na saída. O ranking_reuniao é, portanto, atribuído somente entre as
61
+ reuniões com preços e sempre forma uma sequência consecutiva [1, 2, ..., n].
62
+
63
+ Observações sobre o fallback do DI1
64
+ -----------------------------------
65
+ Se os dados do DI1 não estiverem disponíveis para a data de referência (erro de
66
+ rede, feriado etc.), taxa_di1 assume 0,0 e fator_desconto assume 1,0. Assim,
67
+ prob_bruta se reduz a preco_ajuste / 100, equivalente à fórmula antiga
68
+ (incorreta). Essa degradação é registrada como aviso, mas nunca gera uma
69
+ exceção.
70
+ """
71
+
72
+ import logging
73
+
74
+ import polars as pl
75
+
76
+ from pyield import cpm
77
+ from pyield._internal.converters import converter_datas
78
+ from pyield._internal.types import DateLike
79
+ from pyield.futuro import di1
80
+
81
+ logger = logging.getLogger(__name__)
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Schema
86
+ # ---------------------------------------------------------------------------
87
+
88
+
89
+ def _empty_schema() -> pl.DataFrame:
90
+ return pl.DataFrame(
91
+ schema={
92
+ "data_referencia": pl.Date,
93
+ "data_fim_reuniao": pl.Date,
94
+ "data_expiracao": pl.Date,
95
+ "ranking_reuniao": pl.Int32,
96
+ "variacao_strike_bps": pl.Int32,
97
+ "dias_uteis": pl.Int32,
98
+ "preco_ajuste": pl.Float64,
99
+ "taxa_di1": pl.Float64,
100
+ "fator_desconto": pl.Float64,
101
+ "prob_bruta": pl.Float64,
102
+ "prob": pl.Float64,
103
+ "prob_acumulada": pl.Float64,
104
+ }
105
+ )
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Internal helpers
110
+ # ---------------------------------------------------------------------------
111
+
112
+
113
+ def _add_meeting_rank(df: pl.DataFrame) -> pl.DataFrame:
114
+ """
115
+ Adiciona ranking_reuniao: 1 = data_expiracao mais próxima, 2 = seguinte, etc.
116
+ Calculado como dense rank sobre data_expiracao.
117
+ """
118
+ return df.with_columns(
119
+ ranking_reuniao=pl.col("data_expiracao").rank("dense").cast(pl.Int32)
120
+ )
121
+
122
+
123
+ def _add_discount_factors(df: pl.DataFrame) -> pl.DataFrame:
124
+ """
125
+ Adiciona as colunas de taxa DI1 e fator de desconto.
126
+
127
+ A função usa ``di1.interpolar_taxas()`` de forma vetorizada para buscar
128
+ todas as taxas DI1 em uma única chamada, com uma busca e um interpolador por
129
+ data_referencia única. Em seguida, calcula os fatores de desconto com
130
+ expressões Polars.
131
+
132
+ Método de interpolação
133
+ ----------------------
134
+ ``di1.interpolar_taxas()`` implementa o Manual da B3 §1.4.2 — Flat Forward
135
+ 252, que interpola de forma log-linear os fatores acumulados do DI1 (valores
136
+ de PU):
137
+
138
+ fa_j = (1 + r_j)^(du_j/252) # fator acumulado no vértice j
139
+ fa_k = (1 + r_k)^(du_k/252) # fator acumulado no vértice k
140
+ ft = (du - du_j) / (du_k - du_j) # fração do intervalo
141
+ r = (fa_j * (fa_k / fa_j)^ft)^(252/du) - 1
142
+
143
+ Isso equivale à interpolação log-linear dos preços de ajuste do DI1
144
+ (PU = 100_000 / (1+r)^(du/252)), daí a expressão "interpolação exponencial
145
+ dos preços de ajuste do DI1" no Manual de Apreçamento do CPM §3.5.
146
+
147
+ Não é o método §1.4.1 (Exponencial 252), que interpola diretamente as taxas:
148
+ r = (1 + r_j) * ((1 + r_k)/(1 + r_j))^ft - 1
149
+ Os dois métodos divergem alguns pontos-base em vencimentos intermediários
150
+ (por exemplo, aproximadamente 4,6 pontos-base em du=17 para taxas na faixa
151
+ da Selic em 2026).
152
+
153
+ Na ausência de dados do DI1, usa taxa_di1=0,0 e fator_desconto=1,0.
154
+ """
155
+ pairs = (
156
+ df.select("data_referencia", "data_expiracao", "dias_uteis")
157
+ .unique(subset=["data_referencia", "data_expiracao"])
158
+ .sort("data_referencia", "data_expiracao")
159
+ )
160
+
161
+ # Chamada vetorizada: um fetch por data_referencia única
162
+ try:
163
+ rates = di1.interpolar_taxas(
164
+ datas_referencia=pairs["data_referencia"],
165
+ datas_vencimento=pairs["data_expiracao"],
166
+ extrapolar=True,
167
+ )
168
+ except Exception:
169
+ logger.warning("Falha na busca DI1; usando fallback taxa=0.0.")
170
+ rates = pl.Series("taxa_interpolada", [None] * len(pairs), dtype=pl.Float64)
171
+
172
+ discount_df = (
173
+ pairs.with_columns(
174
+ taxa_di1=rates.fill_null(0.0).fill_nan(0.0),
175
+ )
176
+ .with_columns(
177
+ fator_desconto=(
178
+ (pl.col("dias_uteis") / 252 * (1 + pl.col("taxa_di1")).log()).exp()
179
+ ),
180
+ )
181
+ .select("data_referencia", "data_expiracao", "taxa_di1", "fator_desconto")
182
+ )
183
+
184
+ return df.join(discount_df, on=["data_referencia", "data_expiracao"], how="left")
185
+
186
+
187
+ def _add_probabilities(df: pl.DataFrame) -> pl.DataFrame:
188
+ """
189
+ Adiciona as colunas prob_bruta, prob e prob_acumulada conforme o Manual da
190
+ B3 §3.5.
191
+
192
+ Assume que o DataFrame já foi filtrado para um tipo de opção e para linhas
193
+ com preco_ajuste não nulo, e que ``_add_discount_factors`` já foi chamada
194
+ para adicionar taxa_di1 e fator_desconto.
195
+
196
+ prob_bruta = preco_ajuste * fator_desconto / 100
197
+ prob = prob_bruta / soma(prob_bruta) no grupo de data_expiracao
198
+ prob_acumulada = soma acumulada de prob, ordenada por
199
+ variacao_strike_bps em ordem crescente.
200
+ """
201
+ df = _add_discount_factors(df)
202
+
203
+ return (
204
+ df.sort("data_expiracao", "variacao_strike_bps")
205
+ .with_columns(
206
+ prob_bruta=(pl.col("preco_ajuste") * pl.col("fator_desconto") / 100),
207
+ )
208
+ .with_columns(
209
+ prob=(
210
+ pl.col("prob_bruta") / pl.col("prob_bruta").sum().over("data_expiracao")
211
+ ),
212
+ )
213
+ .with_columns(prob_acumulada=pl.col("prob").cum_sum().over("data_expiracao"))
214
+ )
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # API pública
219
+ # ---------------------------------------------------------------------------
220
+
221
+
222
+ def all_meetings(
223
+ date: DateLike,
224
+ option_type: str = "call",
225
+ ) -> pl.DataFrame:
226
+ """
227
+ Calcula as probabilidades implícitas de todas as reuniões do COPOM com
228
+ contratos CPM negociados na data informada em `date`.
229
+
230
+ Somente strikes com `preco_ajuste` não nulo são incluídos. Reuniões em que
231
+ todos os strikes listados têm preço nulo são excluídas integralmente (ver
232
+ as observações no nível do módulo).
233
+
234
+ Args:
235
+ date: Data de referência da consulta; aceita os formatos de `DateLike`.
236
+ option_type: Tipo de opção, `"call"` ou `"put"`. O padrão é `"call"`,
237
+ que na prática é o lado mais líquido.
238
+
239
+ Returns:
240
+ `polars.DataFrame` ordenado por `ranking_reuniao` e
241
+ `variacao_strike_bps`. Em caso de ausência de dados, retorna um
242
+ DataFrame vazio com o schema correto.
243
+
244
+ Output Columns:
245
+ - data_referencia (Date): Data de negociação.
246
+ - data_fim_reuniao (Date): Último dia da reunião do COPOM.
247
+ - data_expiracao (Date): Data de expiração do contrato CPM.
248
+ - ranking_reuniao (Int32): 1 para a reunião mais próxima com preços.
249
+ - variacao_strike_bps (Int32): Variação do strike em pontos-base,
250
+ crescente dentro de cada reunião.
251
+ - dias_uteis (Int32): Dias úteis até a expiração.
252
+ - preco_ajuste (Float64): Preço de Referência da B3 em pontos (0–100).
253
+ - taxa_di1 (Float64): Taxa DI1 interpolada até a expiração.
254
+ - fator_desconto (Float64): Fator aplicado na inversão da fórmula da B3,
255
+ calculado como `exp(dias_uteis / 252 * ln(1 + taxa_di1))`.
256
+ - prob_bruta (Float64): Probabilidade antes da normalização,
257
+ calculada como `preco_ajuste * fator_desconto / 100`.
258
+ - prob (Float64): Probabilidade normalizada, calculada como
259
+ `prob_bruta / soma(prob_bruta)` por `data_expiracao`, com soma
260
+ 1,0 por reunião.
261
+ - prob_acumulada (Float64): Probabilidade acumulada em ordem
262
+ crescente de strike.
263
+
264
+ Examples:
265
+ >>> import pyield as yd
266
+ >>> import polars as pl
267
+ >>> df = yd.cpm.probabilidades.all_meetings("29-01-2025") # doctest: +SKIP
268
+ >>> df.is_empty() or df["ranking_reuniao"].min() == 1 # doctest: +SKIP
269
+ True
270
+ >>> sums = df.group_by("data_expiracao").agg(
271
+ ... pl.col("prob").sum()
272
+ ... ) # doctest: +SKIP
273
+ >>> df.is_empty() or (sums["prob"] - 1.0).abs().max() < 1e-9 # doctest: +SKIP
274
+ True
275
+ """
276
+ raw = cpm.data(date)
277
+ if raw.is_empty():
278
+ return _empty_schema()
279
+
280
+ df = (
281
+ raw.filter(pl.col("tipo_opcao") == option_type)
282
+ # Excluir strikes sem preço de ajuste — ver docstring do módulo.
283
+ .filter(pl.col("preco_ajuste").is_not_null())
284
+ .pipe(_add_meeting_rank)
285
+ .pipe(_add_probabilities)
286
+ .select(
287
+ "data_referencia",
288
+ "data_fim_reuniao",
289
+ "data_expiracao",
290
+ "ranking_reuniao",
291
+ "variacao_strike_bps",
292
+ "dias_uteis",
293
+ "preco_ajuste",
294
+ "taxa_di1",
295
+ "fator_desconto",
296
+ "prob_bruta",
297
+ "prob",
298
+ "prob_acumulada",
299
+ )
300
+ .sort("ranking_reuniao", "variacao_strike_bps")
301
+ )
302
+
303
+ return df if not df.is_empty() else _empty_schema()
304
+
305
+
306
+ def meeting(
307
+ date: DateLike,
308
+ expiration: DateLike | None = None,
309
+ option_type: str = "call",
310
+ ) -> pl.DataFrame:
311
+ """
312
+ Calcula as probabilidades implícitas de uma única reunião do COPOM.
313
+
314
+ Args:
315
+ date: Data de referência da consulta.
316
+ expiration: Data de expiração do contrato CPM da reunião desejada, ou
317
+ `None` para selecionar a reunião mais próxima com preços. A expiração
318
+ corresponde ao primeiro dia útil após o término da reunião.
319
+ option_type: Tipo de opção, `"call"` ou `"put"`. O padrão é `"call"`.
320
+
321
+ Returns:
322
+ `polars.DataFrame` com o mesmo schema de `all_meetings()`, filtrado
323
+ para uma única reunião. `ranking_reuniao` é sempre 1 nesta saída,
324
+ relativo à reunião selecionada, e não ao conjunto de reuniões.
325
+
326
+ Examples:
327
+ >>> import pyield as yd
328
+ >>> df = yd.cpm.probabilidades.meeting("29-01-2025") # doctest: +SKIP
329
+ >>> df.is_empty() or abs(df["prob"].sum() - 1.0) < 1e-9 # doctest: +SKIP
330
+ True
331
+ >>> df.is_empty() or df["prob_acumulada"].tail(
332
+ ... 1
333
+ ... ).item() == 1.0 # doctest: +SKIP
334
+ True
335
+ """
336
+ df = all_meetings(date, option_type=option_type)
337
+ if df.is_empty():
338
+ return _empty_schema()
339
+
340
+ if expiration is None:
341
+ target_expiry = df.filter(pl.col("ranking_reuniao") == 1)["data_expiracao"][0]
342
+ else:
343
+ target_expiry = converter_datas(expiration)
344
+
345
+ return df.filter(pl.col("data_expiracao") == target_expiry).with_columns(
346
+ ranking_reuniao=pl.lit(1, dtype=pl.Int32)
347
+ )
@@ -20,15 +20,9 @@ def forwards_expr(
20
20
  DI1), onde ``agrupar_por`` define janelas independentes de cálculo sem que
21
21
  seja necessário extrair colunas, calcular fora e juntar de volta.
22
22
 
23
- A fórmula da taxa a termo entre os vértices ``j`` (anterior) e ``k``
24
- (atual) é:
25
-
26
- \[
27
- fwd_k = \left( \frac{f_k^{au_k}}{f_j^{au_j}} \right)^{\frac{1}{au_k - au_j}} - 1
28
- \]
29
-
30
- Onde ``fₓ = 1 + txₓ`` e ``auₓ = duₓ/252``. A primeira linha de cada grupo
31
- (menor ``dias_uteis``) é tratada como spot: ``fwd = tx``.
23
+ A taxa a termo entre os vértices ``j`` (anterior) e ``k`` (atual) segue a
24
+ fórmula documentada em :func:`forward`. A primeira linha de cada grupo
25
+ (menor ``dias_uteis``) é tratada como spot: ``f = tx``.
32
26
 
33
27
  Ordenação cronológica:
34
28
  A expressão usa ``shift(1).over(agrupar_por, order_by=dias_uteis)``,
@@ -161,14 +155,13 @@ def forwards_expr(
161
155
  tx_j = tx_k.shift(1).over(grupo, order_by=du_k)
162
156
  au_j = au_k.shift(1).over(grupo, order_by=du_k)
163
157
 
164
- # fwdₖ = (fₖ^auₖ / fⱼ^auⱼ) ^ (1/(auₖ - auⱼ)) - 1, com fₓ = 1 + txₓ
165
- fk = 1 + tx_k
166
- fj = 1 + tx_j
167
- taxa_forward = (fk**au_k / fj**au_j) ** (1 / (au_k - au_j)) - 1
158
+ ft_k = 1 + tx_k
159
+ ft_j = 1 + tx_j
160
+ f_k = (ft_k**au_k / ft_j**au_j) ** (1 / (au_k - au_j)) - 1
168
161
 
169
162
  # Primeira linha de cada grupo (menor dias_uteis) é a taxa spot
170
163
  eh_primeira = du_k == du_k.min().over(grupo)
171
- return pl.when(eh_primeira).then(tx_k).otherwise(taxa_forward)
164
+ return pl.when(eh_primeira).then(tx_k).otherwise(f_k)
172
165
 
173
166
 
174
167
  def forwards(
@@ -179,36 +172,10 @@ def forwards(
179
172
  r"""
180
173
  Calcula taxas a termo a partir de taxas zero.
181
174
 
182
- A taxa a termo no vértice 'n' é definida como:
183
-
184
- fwdₖ = fwdⱼ→ₖ (a taxa a termo de j para k)
185
-
186
- Definindo o fator de capitalização no vértice k como:
187
-
188
- fₖ = 1 + txₖ
189
-
190
- A fórmula utilizada é:
191
-
192
- fwdₖ = (fₖ^(duₖ/252) / fⱼ^(duⱼ/252))^(252/(duₖ - duⱼ)) - 1
193
-
194
- Como au = du/252 (tempo em anos úteis), a fórmula pode ser simplificada para:
195
-
196
- fwdₖ = (fₖ^auₖ / fⱼ^auⱼ)^(1/(auₖ - auⱼ)) - 1
197
-
198
- Em LaTeX, a fórmula é representada como:
199
-
200
- \[
201
- fwd_k = \left( \frac{f_k^{au_k}}{f_j^{au_j}} \right)^{\frac{1}{au_k - au_j}} - 1
202
- \]
203
-
204
- Onde:
205
- - fⱼ é o fator de capitalização no vértice anterior (fⱼ = 1 + txⱼ).
206
- - fₖ é o fator de capitalização no vértice atual (fₖ = 1 + txₖ).
207
- - txⱼ é a taxa zero para o vértice anterior.
208
- - txₖ é a taxa zero para o vértice atual.
209
- - auⱼ é o prazo em anos úteis no vértice anterior (auⱼ = duⱼ/252).
210
- - auₖ é o prazo em anos úteis no vértice atual (auₖ = duₖ/252).
211
- - A constante 252 representa o número de dias úteis no ano.
175
+ A taxa a termo em cada vértice é calculada aplicando sucessivamente a
176
+ fórmula documentada em :func:`forward` aos vértices consecutivos. A
177
+ primeira taxa de cada grupo é a taxa zero do primeiro vértice, pois não há
178
+ um vértice anterior para calcular a taxa a termo.
212
179
 
213
180
  A função preserva a ordem original dos dados de entrada. Nulos em
214
181
  ``dias_uteis`` ou ``taxas`` produzem nulo na linha correspondente; NaN
@@ -394,29 +361,16 @@ def forward(
394
361
  r"""
395
362
  Calcula a taxa a termo entre dois prazos (dias úteis).
396
363
 
397
- Utiliza a fórmula:
398
-
399
- f₁→₂ = (f₂^(du₂/252) / f₁^(du₁/252))^(252/(du₂ - du₁)) - 1
400
-
401
- Onde:
402
- - f₁ é o fator de capitalização do primeiro prazo (f₁ = 1 + tx₁).
403
- - f₂ é o fator de capitalização do segundo prazo (f₂ = 1 + tx₂).
404
- - tx₁ é a taxa zero para o primeiro prazo (du₁).
405
- - tx₂ é a taxa zero para o segundo prazo (du₂).
406
- - du₁ é o número de dias úteis até a primeira data.
407
- - du₂ é o número de dias úteis até a segunda data.
408
- - A constante 252 representa o número de dias úteis no ano.
409
-
410
- Como au = du/252 (tempo em anos úteis), a fórmula pode ser simplificada para:
411
-
412
- f₁→₂ = (f₂^au₂ / f₁^au₁)^(1/(au₂ - au₁)) - 1
413
-
414
- Que em latex fica:
364
+ Definindo \(F_i = 1 + tx_i\) e \(t_i = du_i/252\), onde 252 é o
365
+ número de dias úteis no ano, a fórmula é:
415
366
 
416
367
  \[
417
- f_{1 \rightarrow 2} = \left( \frac{f_2^{au_2}}{f_1^{au_1}} \right)^{\frac{1}{au_2 - au_1}} - 1
368
+ f_{1 \rightarrow 2} = \left( \frac{F_2^{t_2}}{F_1^{t_1}} \right)^{\frac{1}{t_2 - t_1}} - 1
418
369
  \]
419
370
 
371
+ Aqui, ``F₁`` e ``F₂`` são os fatores de capitalização dos prazos ``du1``
372
+ e ``du2``, e ``tx1`` e ``tx2`` são as respectivas taxas zero.
373
+
420
374
  Args:
421
375
  du1 (int): Número de dias úteis do primeiro ponto (prazo menor).
422
376
  du2 (int): Número de dias úteis do segundo ponto (prazo maior).
@@ -453,9 +407,7 @@ def forward(
453
407
  au2 = du2 / 252
454
408
 
455
409
  # Definição dos fatores de capitalização:
456
- # f₁ = 1 + tx₁ e f₂ = 1 + tx₂
457
- f1 = 1 + taxa1
458
- f2 = 1 + taxa2
410
+ ft1 = 1 + taxa1
411
+ ft2 = 1 + taxa2
459
412
 
460
- # f₁→₂ = (f₂^au₂ / f₁^au₁)^(1/(au₂ - au₁)) - 1
461
- return (f2**au2 / f1**au1) ** (1 / (au2 - au1)) - 1
413
+ return (ft2**au2 / ft1**au1) ** (1 / (au2 - au1)) - 1