PYield 0.43.8__tar.gz → 0.44.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 (76) hide show
  1. {pyield-0.43.8 → pyield-0.44.0}/PKG-INFO +1 -1
  2. pyield-0.44.0/pyield/__about__.py +1 -0
  3. {pyield-0.43.8 → pyield-0.44.0}/pyield/__init__.py +2 -0
  4. {pyield-0.43.8 → pyield-0.44.0}/pyield/_internal/data_cache.py +4 -4
  5. {pyield-0.43.8 → pyield-0.44.0}/pyield/b3/__init__.py +2 -0
  6. pyield-0.44.0/pyield/b3/_contracts.py +56 -0
  7. pyield-0.44.0/pyield/b3/_validar_pregao.py +36 -0
  8. {pyield-0.43.8 → pyield-0.44.0}/pyield/b3/di1.py +41 -50
  9. pyield-0.44.0/pyield/b3/futures/__init__.py +128 -0
  10. pyield-0.44.0/pyield/b3/futures/common.py +72 -0
  11. pyield-0.44.0/pyield/b3/futures/historical.py +238 -0
  12. pyield-0.44.0/pyield/b3/futures/intraday.py +158 -0
  13. pyield-0.44.0/pyield/b3/intraday_derivatives.py +137 -0
  14. pyield-0.44.0/pyield/b3/price_report.py +423 -0
  15. {pyield-0.43.8 → pyield-0.44.0}/pyield/selic/cpm.py +4 -5
  16. pyield-0.43.8/pyield/__about__.py +0 -1
  17. pyield-0.43.8/pyield/b3/common.py +0 -161
  18. pyield-0.43.8/pyield/b3/futures/__init__.py +0 -5
  19. pyield-0.43.8/pyield/b3/futures/core.py +0 -127
  20. pyield-0.43.8/pyield/b3/futures/historical/__init__.py +0 -1
  21. pyield-0.43.8/pyield/b3/futures/historical/core.py +0 -42
  22. pyield-0.43.8/pyield/b3/futures/historical/historical_b3.py +0 -222
  23. pyield-0.43.8/pyield/b3/futures/historical/historical_bmf.py +0 -314
  24. pyield-0.43.8/pyield/b3/futures/intraday.py +0 -204
  25. pyield-0.43.8/pyield/b3/price_report.py +0 -439
  26. {pyield-0.43.8 → pyield-0.44.0}/.gitignore +0 -0
  27. {pyield-0.43.8 → pyield-0.44.0}/LICENSE +0 -0
  28. {pyield-0.43.8 → pyield-0.44.0}/README.md +0 -0
  29. {pyield-0.43.8 → pyield-0.44.0}/pyield/_internal/__init__.py +0 -0
  30. {pyield-0.43.8 → pyield-0.44.0}/pyield/_internal/converters.py +0 -0
  31. {pyield-0.43.8 → pyield-0.44.0}/pyield/_internal/retry.py +0 -0
  32. {pyield-0.43.8 → pyield-0.44.0}/pyield/_internal/types.py +0 -0
  33. {pyield-0.43.8 → pyield-0.44.0}/pyield/anbima/__init__.py +0 -0
  34. {pyield-0.43.8 → pyield-0.44.0}/pyield/anbima/difusao.py +0 -0
  35. {pyield-0.43.8 → pyield-0.44.0}/pyield/anbima/ettj_intraday.py +0 -0
  36. {pyield-0.43.8 → pyield-0.44.0}/pyield/anbima/ettj_last.py +0 -0
  37. {pyield-0.43.8 → pyield-0.44.0}/pyield/anbima/ima.py +0 -0
  38. {pyield-0.43.8 → pyield-0.44.0}/pyield/anbima/imaq.py +0 -0
  39. {pyield-0.43.8 → pyield-0.44.0}/pyield/anbima/tpf.py +0 -0
  40. {pyield-0.43.8 → pyield-0.44.0}/pyield/b3/di_over.py +0 -0
  41. {pyield-0.43.8 → pyield-0.44.0}/pyield/bc/__init__.py +0 -0
  42. {pyield-0.43.8 → pyield-0.44.0}/pyield/bc/auction.py +0 -0
  43. {pyield-0.43.8 → pyield-0.44.0}/pyield/bc/copom.py +0 -0
  44. {pyield-0.43.8 → pyield-0.44.0}/pyield/bc/ptax_api.py +0 -0
  45. {pyield-0.43.8 → pyield-0.44.0}/pyield/bc/rates.py +0 -0
  46. {pyield-0.43.8 → pyield-0.44.0}/pyield/bc/repo.py +0 -0
  47. {pyield-0.43.8 → pyield-0.44.0}/pyield/bc/trades_intraday.py +0 -0
  48. {pyield-0.43.8 → pyield-0.44.0}/pyield/bc/trades_monthly.py +0 -0
  49. {pyield-0.43.8 → pyield-0.44.0}/pyield/bc/vna.py +0 -0
  50. {pyield-0.43.8 → pyield-0.44.0}/pyield/bday/__init__.py +0 -0
  51. {pyield-0.43.8 → pyield-0.44.0}/pyield/bday/core.py +0 -0
  52. {pyield-0.43.8 → pyield-0.44.0}/pyield/bday/holidays/br_holidays_new.txt +0 -0
  53. {pyield-0.43.8 → pyield-0.44.0}/pyield/bday/holidays/br_holidays_old.txt +0 -0
  54. {pyield-0.43.8 → pyield-0.44.0}/pyield/bday/holidays/brholidays.py +0 -0
  55. {pyield-0.43.8 → pyield-0.44.0}/pyield/clock.py +0 -0
  56. {pyield-0.43.8 → pyield-0.44.0}/pyield/fwd.py +0 -0
  57. {pyield-0.43.8 → pyield-0.44.0}/pyield/interpolator.py +0 -0
  58. {pyield-0.43.8 → pyield-0.44.0}/pyield/ipca/__init__.py +0 -0
  59. {pyield-0.43.8 → pyield-0.44.0}/pyield/ipca/historical.py +0 -0
  60. {pyield-0.43.8 → pyield-0.44.0}/pyield/ipca/projected.py +0 -0
  61. {pyield-0.43.8 → pyield-0.44.0}/pyield/py.typed +0 -0
  62. {pyield-0.43.8 → pyield-0.44.0}/pyield/rmd.py +0 -0
  63. {pyield-0.43.8 → pyield-0.44.0}/pyield/selic/__init__.py +0 -0
  64. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/__init__.py +0 -0
  65. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/auctions.py +0 -0
  66. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/benchmark.py +0 -0
  67. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/lft.py +0 -0
  68. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/ltn.py +0 -0
  69. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/ntnb.py +0 -0
  70. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/ntnb1.py +0 -0
  71. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/ntnbprinc.py +0 -0
  72. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/ntnc.py +0 -0
  73. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/ntnf.py +0 -0
  74. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/pre.py +0 -0
  75. {pyield-0.43.8 → pyield-0.44.0}/pyield/tn/tools.py +0 -0
  76. {pyield-0.43.8 → pyield-0.44.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyield
3
- Version: 0.43.8
3
+ Version: 0.44.0
4
4
  Summary: A Python library for analysis of fixed income instruments in Brazil
5
5
  Project-URL: Homepage, https://github.com/crdcj/PYield
6
6
  Project-URL: Documentation, https://crdcj.github.io/PYield
@@ -0,0 +1 @@
1
+ __version__ = "0.44.0"
@@ -7,6 +7,7 @@ from pyield.clock import now, today
7
7
  from pyield.fwd import forward, forwards
8
8
  from pyield.interpolator import Interpolator
9
9
  from pyield.rmd import rmd
10
+ from pyield.selic.cpm import data as copom_options
10
11
  from pyield.tn import lft, ltn, ntnb, ntnb1, ntnbprinc, ntnc, ntnf, pre
11
12
 
12
13
  __all__ = [
@@ -21,6 +22,7 @@ __all__ = [
21
22
  "forwards",
22
23
  "forward",
23
24
  "futures",
25
+ "copom_options",
24
26
  "Interpolator",
25
27
  "today",
26
28
  "now",
@@ -16,8 +16,8 @@ registro = logging.getLogger(__name__)
16
16
 
17
17
  # Estrutura interna única — usuário não vê isso
18
18
  class _Dataset(Enum):
19
- DI1 = ("b3_di.parquet", "TradeDate", "Futuro de DI (B3)")
20
19
  TPF = ("anbima_tpf.parquet", "ReferenceDate", "TPF (ANBIMA)")
20
+ PR = ("b3_price_report.parquet", "TradeDate", "Price Report (B3)")
21
21
 
22
22
  def __init__(self, nome_arquivo: str, coluna_data: str, descricao: str):
23
23
  self.nome_arquivo = nome_arquivo
@@ -25,7 +25,7 @@ class _Dataset(Enum):
25
25
  self.descricao = descricao
26
26
 
27
27
 
28
- type IdDataset = Literal["di1", "tpf"]
28
+ type IdDataset = Literal["tpf", "pr"]
29
29
 
30
30
 
31
31
  def _obter_chave_data_hoje() -> str:
@@ -37,7 +37,7 @@ def _validar_id_dataset(id_dataset: str) -> _Dataset:
37
37
  try:
38
38
  return _Dataset[dataset_normalizado.upper()]
39
39
  except KeyError as e:
40
- msg = f"id_dataset inválido: '{id_dataset}'. Valores aceitos: 'di1', 'tpf'."
40
+ msg = f"id_dataset inválido: '{id_dataset}'. Valores aceitos: 'tpf', 'pr'."
41
41
  raise ValueError(msg) from e
42
42
 
43
43
 
@@ -81,7 +81,7 @@ def obter_dataset_cacheado(id_dataset: IdDataset) -> pl.DataFrame:
81
81
  Obtém um dataset pelo ID. Cache expira diariamente.
82
82
 
83
83
  Args:
84
- id_dataset: "di1" ou "tpf"
84
+ id_dataset: "tpf" ou "pr"
85
85
  """
86
86
  df = _obter_dataset_com_ttl(id_dataset.lower(), _obter_chave_data_hoje())
87
87
  return df.clone()
@@ -1,9 +1,11 @@
1
1
  from pyield.b3 import di1
2
2
  from pyield.b3.di_over import di_over
3
3
  from pyield.b3.futures import futures
4
+ from pyield.b3.intraday_derivatives import fetch_intraday_derivatives
4
5
  from pyield.b3.price_report import fetch_price_report, read_price_report
5
6
 
6
7
  __all__ = [
8
+ "fetch_intraday_derivatives",
7
9
  "di_over",
8
10
  "futures",
9
11
  "fetch_price_report",
@@ -0,0 +1,56 @@
1
+ from collections.abc import Sequence
2
+
3
+ import polars as pl
4
+
5
+ from pyield._internal.types import any_is_empty
6
+
7
+
8
+ def normalizar_codigo_contrato(contract_code: str | object) -> str:
9
+ """Normaliza um código de contrato único para caixa alta.
10
+
11
+ Args:
12
+ contract_code: Código único de contrato.
13
+
14
+ Returns:
15
+ str: Código normalizado em caixa alta ou string vazia se vazio.
16
+ """
17
+ if any_is_empty(contract_code):
18
+ return ""
19
+
20
+ codigo = str(contract_code).strip().upper()
21
+ return codigo
22
+
23
+
24
+ def normalizar_codigos_contrato(
25
+ contract_code: str | Sequence[str] | pl.Series,
26
+ ) -> list[str]:
27
+ """Normaliza códigos de contrato para lista única em caixa alta.
28
+
29
+ Remove valores vazios e duplicados, preservando a ordem de entrada.
30
+
31
+ Args:
32
+ contract_code: Código único ou coleção de códigos de contrato.
33
+
34
+ Returns:
35
+ list[str]: Lista normalizada de códigos em caixa alta.
36
+ """
37
+ if isinstance(contract_code, str):
38
+ codigo = normalizar_codigo_contrato(contract_code)
39
+ return [codigo] if codigo else []
40
+
41
+ if isinstance(contract_code, pl.Series):
42
+ valores = contract_code.to_list()
43
+ else:
44
+ valores = list(contract_code)
45
+
46
+ codigos_unicos: list[str] = []
47
+ codigos_vistos: set[str] = set()
48
+ for valor in valores:
49
+ codigo = normalizar_codigo_contrato(valor)
50
+ if not codigo:
51
+ continue
52
+ if codigo not in codigos_vistos:
53
+ codigos_vistos.add(codigo)
54
+ codigos_unicos.append(codigo)
55
+
56
+ return codigos_unicos
@@ -0,0 +1,36 @@
1
+ import datetime as dt
2
+ import logging
3
+
4
+ from pyield import bday, clock
5
+
6
+ registro = logging.getLogger(__name__)
7
+
8
+
9
+ def data_negociacao_valida(data_negociacao: dt.date) -> bool:
10
+ """Valida se a data de referência é utilizável para consulta.
11
+
12
+ Critérios:
13
+ - Deve ser um dia útil brasileiro.
14
+ - Não pode estar no futuro (maior que a data corrente no Brasil).
15
+
16
+ Retorna True se válida, False caso contrário (e loga um aviso).
17
+ """
18
+ if data_negociacao > clock.today():
19
+ registro.warning(f"A data informada {data_negociacao} está no futuro.")
20
+ return False
21
+ if not bday.is_business_day(data_negociacao):
22
+ registro.warning(f"A data informada {data_negociacao} não é dia útil.")
23
+ return False
24
+
25
+ # Não tem pregão na véspera de Natal e Ano Novo
26
+ datas_fechadas_especiais = { # Datas especiais
27
+ dt.date(data_negociacao.year, 12, 24), # Véspera de Natal
28
+ dt.date(data_negociacao.year, 12, 31), # Véspera de Ano Novo
29
+ }
30
+ if data_negociacao in datas_fechadas_especiais:
31
+ registro.warning(
32
+ f"Não há pregão na véspera de Natal e de Ano Novo: {data_negociacao}"
33
+ )
34
+ return False
35
+
36
+ return True
@@ -7,10 +7,16 @@ import pyield._internal.converters as cv
7
7
  from pyield import b3, bday, interpolator
8
8
  from pyield._internal.data_cache import obter_dataset_cacheado
9
9
  from pyield._internal.types import ArrayLike, DateLike, any_is_collection, any_is_empty
10
+ from pyield.b3.futures import historical as futuros_historico
10
11
 
11
12
  registro = logging.getLogger(__name__)
12
13
 
13
14
 
15
+ def _carregar_cache_pr_di1(datas: list[dt.date]) -> pl.DataFrame:
16
+ """Carrega e processa DI1 a partir do dataset PR para as datas informadas."""
17
+ return futuros_historico.carregar_pr(datas=datas, codigo_contrato="DI1")
18
+
19
+
14
20
  def _carregar_com_intraday(datas: list[dt.date]) -> pl.DataFrame:
15
21
  """Busca dados de DI, incluindo dados intraday para datas ausentes no cache.
16
22
 
@@ -20,12 +26,15 @@ def _carregar_com_intraday(datas: list[dt.date]) -> pl.DataFrame:
20
26
  Returns:
21
27
  DataFrame com dados de DI para as datas solicitadas.
22
28
  """
23
- # 1. Busca inicial no cache com as datas solicitadas pelo usuário.
24
- df_cache = obter_dataset_cacheado("di1").filter(pl.col("TradeDate").is_in(datas))
29
+ # 1. Busca inicial no cache PR com as datas solicitadas pelo usuário.
30
+ df_cache = _carregar_cache_pr_di1(datas)
31
+ if df_cache.is_empty():
32
+ datas_cache = set()
33
+ else:
34
+ datas_cache = set(df_cache["TradeDate"].drop_nulls())
25
35
 
26
36
  # 2. Identifica datas solicitadas que não estão no cache
27
37
  datas_solicitadas = set(datas)
28
- datas_cache = set(df_cache["TradeDate"].unique())
29
38
  datas_faltantes = datas_solicitadas - datas_cache
30
39
 
31
40
  # 3. Para cada data faltante, tenta buscar dados via API
@@ -48,7 +57,7 @@ def _carregar_com_intraday(datas: list[dt.date]) -> pl.DataFrame:
48
57
 
49
58
  # 4. Retorna concatenação de todos os DataFrames disponíveis
50
59
  if len(dfs_concat) == 0:
51
- return pl.DataFrame() # Retorna DataFrame vazio se nada foi encontrado
60
+ return pl.DataFrame()
52
61
  elif len(dfs_concat) == 1:
53
62
  return dfs_concat[0]
54
63
  else:
@@ -56,19 +65,19 @@ def _carregar_com_intraday(datas: list[dt.date]) -> pl.DataFrame:
56
65
 
57
66
 
58
67
  def _obter_dados(datas: DateLike | ArrayLike) -> pl.DataFrame:
59
- df_datas = (
60
- pl.DataFrame({"TradeDate": datas})
61
- .with_columns(TradeDate=cv.converter_datas_expr("TradeDate"))
62
- .drop_nulls(subset=["TradeDate"])
63
- .select(pl.col("TradeDate").unique().sort())
64
- )
65
-
66
- if df_datas.is_empty():
68
+ datas_unicas = cv.converter_datas(datas)
69
+ if datas_unicas is None:
67
70
  return pl.DataFrame()
68
71
 
69
- lista_datas = df_datas.get_column("TradeDate").to_list()
72
+ if isinstance(datas_unicas, pl.Series):
73
+ datas_unicas = datas_unicas.drop_nulls().unique().sort().to_list()
74
+ else:
75
+ datas_unicas = [datas_unicas]
76
+
77
+ df = _carregar_com_intraday(datas_unicas)
70
78
 
71
- df = _carregar_com_intraday(lista_datas)
79
+ if df.is_empty():
80
+ return pl.DataFrame()
72
81
 
73
82
  return df.sort("TradeDate", "ExpirationDate")
74
83
 
@@ -102,31 +111,20 @@ def data(
102
111
  Examples:
103
112
  >>> from pyield import di1
104
113
  >>> df = di1.data(dates="16-10-2024", month_start=True)
105
- >>> df
106
- shape: (38, 22)
107
- ┌────────────┬────────────────┬──────────────┬───────────┬───┬─────────┬───────────┬────────────────┬─────────────┐
108
- │ TradeDate ┆ ExpirationDate ┆ TickerSymbol ┆ DaysToExp ┆ … ┆ MaxRate ┆ CloseRate ┆ SettlementRate ┆ ForwardRate │
109
- │ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
110
- │ date ┆ date ┆ str ┆ i64 ┆ ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
111
- ╞════════════╪════════════════╪══════════════╪═══════════╪═══╪═════════╪═══════════╪════════════════╪═════════════╡
112
- │ 2024-10-16 ┆ 2024-11-01 ┆ DI1X24 ┆ 16 ┆ … ┆ 0.10656 ┆ 0.10652 ┆ 0.10653 ┆ 0.10653 │
113
- │ 2024-10-16 ┆ 2024-12-01 ┆ DI1Z24 ┆ 47 ┆ … ┆ 0.10914 ┆ 0.10914 ┆ 0.1091 ┆ 0.110726 │
114
- │ 2024-10-16 ┆ 2025-01-01 ┆ DI1F25 ┆ 78 ┆ … ┆ 0.11174 ┆ 0.11164 ┆ 0.11164 ┆ 0.1154 │
115
- │ 2024-10-16 ┆ 2025-02-01 ┆ DI1G25 ┆ 110 ┆ … ┆ 0.1137 ┆ 0.11365 ┆ 0.11362 ┆ 0.118314 │
116
- │ 2024-10-16 ┆ 2025-03-01 ┆ DI1H25 ┆ 140 ┆ … ┆ 0.11595 ┆ 0.11565 ┆ 0.1157 ┆ 0.12343 │
117
- │ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
118
- │ 2024-10-16 ┆ 2035-01-01 ┆ DI1F35 ┆ 3730 ┆ … ┆ 0.1267 ┆ 0.1264 ┆ 0.1265 ┆ 0.124455 │
119
- │ 2024-10-16 ┆ 2036-01-01 ┆ DI1F36 ┆ 4095 ┆ … ┆ null ┆ null ┆ 0.1263 ┆ 0.124249 │
120
- │ 2024-10-16 ┆ 2037-01-01 ┆ DI1F37 ┆ 4461 ┆ … ┆ null ┆ null ┆ 0.1263 ┆ 0.1263 │
121
- │ 2024-10-16 ┆ 2038-01-01 ┆ DI1F38 ┆ 4828 ┆ … ┆ null ┆ null ┆ 0.1263 ┆ 0.1263 │
122
- │ 2024-10-16 ┆ 2039-01-01 ┆ DI1F39 ┆ 5192 ┆ … ┆ null ┆ null ┆ 0.1263 ┆ 0.1263 │
123
- └────────────┴────────────────┴──────────────┴───────────┴───┴─────────┴───────────┴────────────────┴─────────────┘
114
+ >>> {"TradeDate", "TickerSymbol", "ExpirationDate", "SettlementRate"}.issubset(
115
+ ... set(df.columns)
116
+ ... )
117
+ True
118
+ >>> df.shape[0] > 0
119
+ True
124
120
 
125
121
  """
126
122
  if any_is_empty(dates):
127
123
  return pl.DataFrame()
128
124
 
129
125
  df = _obter_dados(datas=dates)
126
+ if df.is_empty():
127
+ return df
130
128
 
131
129
  if pre_filter:
132
130
  df_tpf = (
@@ -386,10 +384,10 @@ def interpolate_rate(
386
384
 
387
385
 
388
386
  def available_trade_dates() -> pl.Series:
389
- """Retorna todas as datas de negociação disponíveis (completas) no dataset de DI.
387
+ """Retorna todas as datas de negociação disponíveis para DI1 em futures.
390
388
 
391
- Obtém valores distintos de 'TradeDate' presentes no cache de dados históricos
392
- de futuros de DI, ordenados cronologicamente.
389
+ Obtém valores distintos de 'TradeDate' para DI1, com base no mesmo
390
+ dataset PR utilizado por `futures`.
393
391
 
394
392
  Returns:
395
393
  Series ordenada de datas de negociação únicas (dt.date) para as quais
@@ -397,23 +395,16 @@ def available_trade_dates() -> pl.Series:
397
395
 
398
396
  Examples:
399
397
  >>> from pyield import di1
400
- >>> # Série de futuros de DI começa em 1995-01-02
398
+ >>> # Série disponível no dataset PR começa em 2018-01-02
401
399
  >>> di1.available_trade_dates().head(5)
402
400
  shape: (5,)
403
401
  Series: 'available_dates' [date]
404
402
  [
405
- 1995-01-02
406
- 1995-01-03
407
- 1995-01-04
408
- 1995-01-05
409
- 1995-01-06
403
+ 2018-01-02
404
+ 2018-01-03
405
+ 2018-01-04
406
+ 2018-01-05
407
+ 2018-01-08
410
408
  ]
411
409
  """
412
- datas_disponiveis = (
413
- obter_dataset_cacheado("di1")
414
- .get_column("TradeDate")
415
- .unique()
416
- .sort()
417
- .alias("available_dates")
418
- )
419
- return datas_disponiveis
410
+ return futuros_historico.listar_datas_disponiveis("DI1").alias("available_dates")
@@ -0,0 +1,128 @@
1
+ import logging
2
+ from collections.abc import Sequence
3
+ from typing import Literal
4
+
5
+ import polars as pl
6
+
7
+ import pyield._internal.converters as cv
8
+ from pyield import clock
9
+ from pyield._internal.types import DateLike, any_is_empty
10
+ from pyield.b3._contracts import normalizar_codigos_contrato
11
+ from pyield.b3._validar_pregao import data_negociacao_valida
12
+ from pyield.b3.futures import historical, intraday
13
+
14
+ OpcoesContrato = Literal[
15
+ "DI1",
16
+ "DDI",
17
+ "FRC",
18
+ "FRO",
19
+ "DAP",
20
+ "DOL",
21
+ "WDO",
22
+ "IND",
23
+ "WIN",
24
+ ]
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def _buscar_intraday_ou_historico(
30
+ data_negociacao,
31
+ codigos_contrato: list[str],
32
+ full_report: bool,
33
+ ) -> pl.DataFrame:
34
+ horario_atual = clock.now().time()
35
+ if horario_atual < intraday.HORA_INICIO_INTRADAY:
36
+ return pl.DataFrame()
37
+
38
+ if horario_atual >= intraday.HORA_FIM_INTRADAY:
39
+ df_historico = historical.historical(
40
+ data_negociacao, codigos_contrato, full_report
41
+ )
42
+ if not df_historico.is_empty():
43
+ logger.info("Dados consolidados disponíveis. Usando histórico.")
44
+ return df_historico
45
+
46
+ dataframes_intraday = [intraday.intraday(codigo) for codigo in codigos_contrato]
47
+ dataframes_intraday = [df for df in dataframes_intraday if not df.is_empty()]
48
+ if not dataframes_intraday:
49
+ return pl.DataFrame()
50
+ if len(dataframes_intraday) == 1:
51
+ return dataframes_intraday[0]
52
+ return pl.concat(dataframes_intraday, how="diagonal_relaxed").sort("TickerSymbol")
53
+
54
+
55
+ def futures(
56
+ date: DateLike,
57
+ contract_code: OpcoesContrato | str | Sequence[OpcoesContrato | str] | pl.Series,
58
+ full_report: bool = False,
59
+ ) -> pl.DataFrame:
60
+ """Busca dados de um contrato futuro da B3 para a data de referência.
61
+
62
+ Args:
63
+ date: Data de referência para consulta.
64
+ contract_code: Código do contrato futuro na B3 ou coleção de códigos.
65
+ full_report: Se False (padrão), usa o simplified price report (SPR),
66
+ arquivo leve (~2 KB) com apenas preços de ajuste. Se True, usa o
67
+ price report completo (PR, ~2 MB) com todos os dados de negociação.
68
+ Relevante apenas quando o dado não está no cache.
69
+
70
+ Returns:
71
+ DataFrame Polars com os dados do contrato informado.
72
+
73
+ Examples:
74
+ >>> df = futures("31-05-2024", "DI1")
75
+ >>> {"TradeDate", "TickerSymbol", "ExpirationDate", "SettlementRate"}.issubset(
76
+ ... set(df.columns)
77
+ ... )
78
+ True
79
+ >>> df.shape[0] > 0
80
+ True
81
+
82
+ >>> df = futures("31-05-2024", "DAP")
83
+ >>> {"TradeDate", "TickerSymbol", "ExpirationDate", "SettlementRate"}.issubset(
84
+ ... set(df.columns)
85
+ ... )
86
+ True
87
+ >>> df.shape[0] > 0
88
+ True
89
+
90
+ Véspera de Natal e Ano Novo não têm pregão:
91
+
92
+ >>> futures("24-12-2024", "DI1").is_empty()
93
+ True
94
+ >>> futures("31-12-2024", "DI1").is_empty()
95
+ True
96
+
97
+ Data futura e fim de semana retornam DataFrame vazio:
98
+
99
+ >>> import datetime as dt
100
+ >>> amanha = dt.date.today() + dt.timedelta(days=1)
101
+ >>> futures(amanha, "DI1").is_empty()
102
+ True
103
+ >>> futures("04-01-2025", "DI1").is_empty() # sábado
104
+ True
105
+
106
+ """
107
+ if any_is_empty(date, contract_code):
108
+ return pl.DataFrame()
109
+
110
+ codigos_contrato = normalizar_codigos_contrato(contract_code)
111
+ if not codigos_contrato:
112
+ return pl.DataFrame()
113
+
114
+ data_negociacao = cv.converter_datas(date)
115
+ if not data_negociacao_valida(data_negociacao):
116
+ return pl.DataFrame()
117
+
118
+ if intraday.data_intraday_valida(data_negociacao):
119
+ return _buscar_intraday_ou_historico(
120
+ data_negociacao=data_negociacao,
121
+ codigos_contrato=codigos_contrato,
122
+ full_report=full_report,
123
+ )
124
+
125
+ return historical.historical(data_negociacao, codigos_contrato, full_report)
126
+
127
+
128
+ __all__ = ["futures"]
@@ -0,0 +1,72 @@
1
+ import polars as pl
2
+
3
+ from pyield import bday
4
+
5
+ _MAPA_MESES: dict[str, int] = {
6
+ "F": 1,
7
+ "G": 2,
8
+ "H": 3,
9
+ "J": 4,
10
+ "K": 5,
11
+ "M": 6,
12
+ "N": 7,
13
+ "Q": 8,
14
+ "U": 9,
15
+ "V": 10,
16
+ "X": 11,
17
+ "Z": 12,
18
+ }
19
+
20
+
21
+ def expr_dv01(
22
+ coluna_dias_uteis: str,
23
+ coluna_taxa: str,
24
+ coluna_preco: str,
25
+ ) -> pl.Expr:
26
+ """Retorna a expressão Polars para cálculo de DV01.
27
+
28
+ Fórmula:
29
+ DV01 = (Duration / (1 + taxa)) * preço * 0,0001
30
+
31
+ Onde:
32
+ - Duration = dias_uteis / 252
33
+ - taxa deve estar em formato decimal (ex.: 0.145)
34
+ - preço é o PU do contrato.
35
+ """
36
+ duracao = pl.col(coluna_dias_uteis) / 252
37
+ duracao_modificada = duracao / (1 + pl.col(coluna_taxa))
38
+ return 0.0001 * duracao_modificada * pl.col(coluna_preco)
39
+
40
+
41
+ def adicionar_vencimento(
42
+ df: pl.DataFrame, codigo_contrato: str, coluna_ticker: str
43
+ ) -> pl.DataFrame:
44
+ """
45
+ Recebe um DataFrame Polars e ADICIONA a coluna 'ExpirationDate'.
46
+
47
+ - Pega a coluna 'coluna_ticker'.
48
+ - Extrai o código de vencimento.
49
+ - Converte para a data "bruta", sem ajuste de feriado.
50
+ - Garante que a data de vencimento é um dia útil.
51
+ - Retorna o DataFrame com a nova coluna ExpirationDate.
52
+
53
+ Assume tickers no formato padrão de futuros da B3 (ex.: DI1F25).
54
+ """
55
+ dia_vencimento = 15 if "DAP" in codigo_contrato else 1
56
+ df = df.with_columns(
57
+ pl.date(
58
+ # Ano: posição 4-5 (2 dígitos) -> Int -> Soma 2000
59
+ # Funciona para tickers padrão de futuros (ex.: DI1F25)
60
+ year=pl.col(coluna_ticker).str.slice(4, 2).cast(pl.Int32, strict=False)
61
+ + 2000,
62
+ # Mês: posição 3 (1 char = código do mês) -> mapeia para Int
63
+ month=pl.col(coluna_ticker)
64
+ .str.slice(3, 1)
65
+ .replace_strict(_MAPA_MESES, default=None, return_dtype=pl.Int8),
66
+ day=dia_vencimento,
67
+ ).alias("ExpirationDate")
68
+ )
69
+ # Garante que a data de vencimento é um dia útil
70
+ df = df.with_columns(ExpirationDate=bday.offset_expr("ExpirationDate", 0))
71
+
72
+ return df