PYield 0.44.5__tar.gz → 0.45.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.
- {pyield-0.44.5 → pyield-0.45.0}/PKG-INFO +1 -2
- pyield-0.45.0/pyield/__about__.py +1 -0
- pyield-0.45.0/pyield/_internal/cache.py +39 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/_internal/converters.py +7 -2
- {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/ettj_intraday.py +2 -2
- {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/ettj_last.py +2 -2
- {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/ima.py +2 -2
- {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/di1.py +0 -4
- {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/futures/__init__.py +1 -1
- {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/futures/historical.py +2 -2
- {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/futures/intraday.py +1 -1
- {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/intraday_derivatives.py +2 -2
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/ptax_api.py +2 -2
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/rates.py +2 -2
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/trades_intraday.py +2 -2
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/trades_monthly.py +8 -36
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/vna.py +2 -2
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bday/core.py +4 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/fwd.py +0 -4
- pyield-0.45.0/pyield/ipca/projected.py +103 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/rmd.py +13 -35
- {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/lft.py +66 -34
- {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ltn.py +63 -37
- {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ntnb.py +128 -81
- {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ntnb1.py +17 -25
- {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ntnbprinc.py +6 -6
- {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ntnc.py +63 -28
- {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/ntnf.py +118 -185
- pyield-0.45.0/pyield/tn/utils.py +216 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyproject.toml +3 -2
- pyield-0.44.5/pyield/__about__.py +0 -1
- pyield-0.44.5/pyield/ipca/projected.py +0 -117
- pyield-0.44.5/pyield/tn/tools.py +0 -112
- {pyield-0.44.5 → pyield-0.45.0}/.gitignore +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/LICENSE +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/README.md +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/__init__.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/_internal/__init__.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/_internal/data_cache.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/_internal/retry.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/_internal/types.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/__init__.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/difusao.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/imaq.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/anbima/tpf.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/__init__.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/_contracts.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/_validar_pregao.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/di_over.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/futures/common.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/b3/price_report.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/__init__.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/auction.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/copom.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bc/repo.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bday/__init__.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bday/holidays/br_holidays_new.txt +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bday/holidays/br_holidays_old.txt +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/bday/holidays/brholidays.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/clock.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/interpolator.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/ipca/__init__.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/ipca/historical.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/py.typed +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/selic/__init__.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/selic/cpm.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/selic/probabilities.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/__init__.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/auctions.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/benchmark.py +0 -0
- {pyield-0.44.5 → pyield-0.45.0}/pyield/tn/pre.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyield
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.45.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
|
|
@@ -36,7 +36,6 @@ Classifier: Operating System :: OS Independent
|
|
|
36
36
|
Classifier: Programming Language :: Python :: 3
|
|
37
37
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
38
38
|
Requires-Python: >=3.12
|
|
39
|
-
Requires-Dist: cachetools>=5.0.0
|
|
40
39
|
Requires-Dist: fastexcel>=0.19.0
|
|
41
40
|
Requires-Dist: lxml>=6.0.0
|
|
42
41
|
Requires-Dist: polars>=1.39.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.45.0"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Cache com expiração por tempo (TTL) para proteger APIs externas contra
|
|
2
|
+
chamadas repetidas acidentais (ex.: re-execução de célula em notebook).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from functools import wraps
|
|
7
|
+
|
|
8
|
+
_TTL_PADRAO = 60 # segundos
|
|
9
|
+
_TAMANHO_MAXIMO = 16
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ttl_cache(ttl: int = _TTL_PADRAO, maxsize: int = _TAMANHO_MAXIMO):
|
|
13
|
+
"""Decorador de cache com expiração por tempo.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
ttl: Tempo de vida de cada entrada em segundos.
|
|
17
|
+
maxsize: Número máximo de entradas no cache.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def decorador(func):
|
|
21
|
+
_cache: dict = {}
|
|
22
|
+
|
|
23
|
+
@wraps(func)
|
|
24
|
+
def wrapper(*args):
|
|
25
|
+
agora = time.monotonic()
|
|
26
|
+
if args in _cache:
|
|
27
|
+
resultado, expira_em = _cache[args]
|
|
28
|
+
if agora < expira_em:
|
|
29
|
+
return resultado
|
|
30
|
+
resultado = func(*args)
|
|
31
|
+
_cache[args] = (resultado, agora + ttl)
|
|
32
|
+
# Remove entrada mais antiga quando excede o tamanho máximo
|
|
33
|
+
if len(_cache) > maxsize:
|
|
34
|
+
_cache.pop(next(iter(_cache)))
|
|
35
|
+
return resultado
|
|
36
|
+
|
|
37
|
+
return wrapper
|
|
38
|
+
|
|
39
|
+
return decorador
|
|
@@ -121,6 +121,10 @@ def converter_datas(
|
|
|
121
121
|
>>> converter_datas("") is None
|
|
122
122
|
True
|
|
123
123
|
|
|
124
|
+
Data inválida retorna None:
|
|
125
|
+
>>> converter_datas("31-02-2024") is None
|
|
126
|
+
True
|
|
127
|
+
|
|
124
128
|
Lista com valores nulos propaga os nulls:
|
|
125
129
|
>>> converter_datas(["01-01-2024", None])
|
|
126
130
|
shape: (2,)
|
|
@@ -140,12 +144,13 @@ def converter_datas(
|
|
|
140
144
|
]
|
|
141
145
|
|
|
142
146
|
Formatos mistos por linha:
|
|
143
|
-
>>> converter_datas(["25-12-2024", "2024-12-
|
|
144
|
-
shape: (
|
|
147
|
+
>>> converter_datas(["25-12-2024", "26/12/2024", "2024-12-27"])
|
|
148
|
+
shape: (3,)
|
|
145
149
|
Series: 'dates' [date]
|
|
146
150
|
[
|
|
147
151
|
2024-12-25
|
|
148
152
|
2024-12-26
|
|
153
|
+
2024-12-27
|
|
149
154
|
]
|
|
150
155
|
"""
|
|
151
156
|
if not types.is_collection(dates):
|
|
@@ -2,10 +2,10 @@ import datetime as dt
|
|
|
2
2
|
import logging
|
|
3
3
|
from io import StringIO
|
|
4
4
|
|
|
5
|
-
import cachetools
|
|
6
5
|
import polars as pl
|
|
7
6
|
import requests
|
|
8
7
|
|
|
8
|
+
from pyield._internal.cache import ttl_cache
|
|
9
9
|
from pyield._internal.retry import retry_padrao
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
@@ -19,7 +19,7 @@ URL_ETTJ_INTRADAY = (
|
|
|
19
19
|
CASAS_DECIMAIS = 6
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
@
|
|
22
|
+
@ttl_cache()
|
|
23
23
|
@retry_padrao
|
|
24
24
|
def _buscar_texto_intraday() -> str:
|
|
25
25
|
carga_requisicao = {"Dt_Ref": "", "saida": "csv"}
|
|
@@ -2,10 +2,10 @@ import datetime as dt
|
|
|
2
2
|
import logging
|
|
3
3
|
from io import StringIO
|
|
4
4
|
|
|
5
|
-
import cachetools
|
|
6
5
|
import polars as pl
|
|
7
6
|
import requests
|
|
8
7
|
|
|
8
|
+
from pyield._internal.cache import ttl_cache
|
|
9
9
|
from pyield._internal.retry import retry_padrao
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
@@ -16,7 +16,7 @@ URL_ULTIMA_ETTJ = "https://www.anbima.com.br/informacoes/est-termo/CZ-down.asp"
|
|
|
16
16
|
CASAS_DECIMAIS = 6
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
@
|
|
19
|
+
@ttl_cache()
|
|
20
20
|
@retry_padrao
|
|
21
21
|
def _buscar_texto_ultima_ettj() -> str:
|
|
22
22
|
"""Busca o texto bruto da curva de juros na ANBIMA."""
|
|
@@ -2,10 +2,10 @@ import io
|
|
|
2
2
|
import logging
|
|
3
3
|
from typing import Literal
|
|
4
4
|
|
|
5
|
-
import cachetools
|
|
6
5
|
import polars as pl
|
|
7
6
|
import requests
|
|
8
7
|
|
|
8
|
+
from pyield._internal.cache import ttl_cache
|
|
9
9
|
from pyield._internal.retry import retry_padrao
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
@@ -82,7 +82,7 @@ ORDEM_COLUNAS_FINAL = [
|
|
|
82
82
|
URL_ULTIMO_IMA = "https://www.anbima.com.br/informacoes/ima/arqs/ima_completo.txt"
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
@
|
|
85
|
+
@ttl_cache()
|
|
86
86
|
@retry_padrao
|
|
87
87
|
def _buscar_texto_ultimo_ima() -> str:
|
|
88
88
|
resposta = requests.get(URL_ULTIMO_IMA, timeout=3)
|
|
@@ -293,10 +293,6 @@ def interpolate_rate(
|
|
|
293
293
|
>>> # Extrapola taxa para uma data de vencimento futura
|
|
294
294
|
>>> di1.interpolate_rate("25-04-2025", "01-01-2050", extrapolate=True)
|
|
295
295
|
0.13881
|
|
296
|
-
|
|
297
|
-
>>> # Entradas nulas retornam NaN
|
|
298
|
-
>>> di1.interpolate_rate(None, "01-01-2030")
|
|
299
|
-
nan
|
|
300
296
|
"""
|
|
301
297
|
if any_is_collection(date, expiration):
|
|
302
298
|
raise ValueError("As entradas 'date' e 'expiration' devem ser datas escalares.")
|
|
@@ -118,7 +118,7 @@ def _buscar_varias_datas(
|
|
|
118
118
|
# Bulk: carrega todas as datas de uma vez do cache, por contrato
|
|
119
119
|
resultados = []
|
|
120
120
|
for codigo in codigos:
|
|
121
|
-
df_cache = historical.
|
|
121
|
+
df_cache = historical._obter_futuros_pr(datas_validas, codigo)
|
|
122
122
|
if not df_cache.is_empty():
|
|
123
123
|
resultados.append(df_cache)
|
|
124
124
|
|
|
@@ -79,7 +79,7 @@ def historical(
|
|
|
79
79
|
codigos_sem_cache: list[str] = []
|
|
80
80
|
|
|
81
81
|
for codigo in codigos:
|
|
82
|
-
df_cache =
|
|
82
|
+
df_cache = _obter_futuros_pr([data], codigo)
|
|
83
83
|
if df_cache.is_empty():
|
|
84
84
|
codigos_sem_cache.append(codigo)
|
|
85
85
|
else:
|
|
@@ -116,7 +116,7 @@ def historical(
|
|
|
116
116
|
return df_resultado.sort(*colunas_ordenacao)
|
|
117
117
|
|
|
118
118
|
|
|
119
|
-
def
|
|
119
|
+
def _obter_futuros_pr(datas: list[dt.date], codigo_contrato: str) -> pl.DataFrame:
|
|
120
120
|
"""Carrega histórico de futuros do dataset PR para uma lista de datas."""
|
|
121
121
|
if not datas:
|
|
122
122
|
return pl.DataFrame()
|
|
@@ -18,7 +18,7 @@ HORA_INICIO_INTRADAY = dt.time(9, 16)
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def
|
|
21
|
+
def _data_intraday_valida(data_verificacao: dt.date) -> bool:
|
|
22
22
|
"""Verifica se a data é elegível para consulta intraday."""
|
|
23
23
|
if not data_negociacao_valida(data_verificacao):
|
|
24
24
|
return False
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
import cachetools
|
|
4
3
|
import polars as pl
|
|
5
4
|
import requests
|
|
6
5
|
|
|
6
|
+
from pyield._internal.cache import ttl_cache
|
|
7
7
|
from pyield._internal.retry import retry_padrao
|
|
8
8
|
|
|
9
9
|
URL_BASE_INTRADAY = "https://cotacao.b3.com.br/mds/api/v1/DerivativeQuotation"
|
|
@@ -50,7 +50,7 @@ def _mapa_renomeacao_intraday() -> dict[str, str]:
|
|
|
50
50
|
return {nome_orig: nome_novo for nome_orig, nome_novo, _ in COLUNAS_INTRADAY}
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
@
|
|
53
|
+
@ttl_cache(ttl=15)
|
|
54
54
|
@retry_padrao
|
|
55
55
|
def _buscar_json_intraday(codigo_contrato: str) -> list[dict]:
|
|
56
56
|
url = f"{URL_BASE_INTRADAY}/{codigo_contrato}"
|
|
@@ -18,12 +18,12 @@ cotacaoCompra, cotacaoVenda, dataHoraCotacao
|
|
|
18
18
|
import datetime as dt
|
|
19
19
|
import logging
|
|
20
20
|
|
|
21
|
-
import cachetools
|
|
22
21
|
import polars as pl
|
|
23
22
|
import requests
|
|
24
23
|
|
|
25
24
|
import pyield._internal.converters as cv
|
|
26
25
|
from pyield import clock
|
|
26
|
+
from pyield._internal.cache import ttl_cache
|
|
27
27
|
from pyield._internal.retry import retry_padrao
|
|
28
28
|
from pyield._internal.types import DateLike
|
|
29
29
|
|
|
@@ -54,7 +54,7 @@ def _montar_url_api(inicio: dt.date, fim: dt.date) -> str:
|
|
|
54
54
|
return url
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
@
|
|
57
|
+
@ttl_cache(ttl=15)
|
|
58
58
|
@retry_padrao
|
|
59
59
|
def _buscar_texto_api(url: str) -> bytes:
|
|
60
60
|
resposta = requests.get(url, timeout=10)
|
|
@@ -15,11 +15,11 @@ Notas de implementação:
|
|
|
15
15
|
import logging
|
|
16
16
|
from enum import Enum
|
|
17
17
|
|
|
18
|
-
import cachetools
|
|
19
18
|
import polars as pl
|
|
20
19
|
import requests
|
|
21
20
|
|
|
22
21
|
from pyield import clock
|
|
22
|
+
from pyield._internal.cache import ttl_cache
|
|
23
23
|
from pyield._internal.converters import converter_datas
|
|
24
24
|
from pyield._internal.retry import retry_padrao
|
|
25
25
|
from pyield._internal.types import DateLike, any_is_empty
|
|
@@ -43,7 +43,7 @@ class SerieBC(Enum):
|
|
|
43
43
|
DI_OVER = 11
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
@
|
|
46
|
+
@ttl_cache(ttl=15)
|
|
47
47
|
@retry_padrao
|
|
48
48
|
def _chamar_api(url_api: str) -> list[dict[str, str]]:
|
|
49
49
|
"""Executa uma chamada GET na API do BCB e retorna o JSON.
|
|
@@ -7,12 +7,12 @@ import datetime as dt
|
|
|
7
7
|
import io
|
|
8
8
|
import logging
|
|
9
9
|
|
|
10
|
-
import cachetools
|
|
11
10
|
import polars as pl
|
|
12
11
|
import polars.selectors as cs
|
|
13
12
|
import requests
|
|
14
13
|
|
|
15
14
|
from pyield import bday, clock
|
|
15
|
+
from pyield._internal.cache import ttl_cache
|
|
16
16
|
from pyield._internal.retry import retry_padrao
|
|
17
17
|
|
|
18
18
|
HORA_INICIO_TEMPO_REAL = dt.time(9, 0, 0)
|
|
@@ -94,7 +94,7 @@ ORDEM_COLUNAS_FINAL = [
|
|
|
94
94
|
]
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
@
|
|
97
|
+
@ttl_cache(ttl=15)
|
|
98
98
|
@retry_padrao
|
|
99
99
|
def _buscar_csv() -> str:
|
|
100
100
|
"""
|
|
@@ -18,6 +18,7 @@ import zipfile as zf
|
|
|
18
18
|
import polars as pl
|
|
19
19
|
import requests
|
|
20
20
|
|
|
21
|
+
from pyield._internal.cache import ttl_cache
|
|
21
22
|
from pyield._internal.converters import converter_datas
|
|
22
23
|
from pyield._internal.retry import retry_padrao
|
|
23
24
|
from pyield._internal.types import DateLike, any_is_empty
|
|
@@ -51,28 +52,6 @@ MAPA_COLUNAS = [
|
|
|
51
52
|
ESQUEMA_CSV = {csv: tipo for csv, _, tipo in MAPA_COLUNAS}
|
|
52
53
|
MAPEAMENTO_COLUNAS = {csv: novo for csv, novo, _ in MAPA_COLUNAS}
|
|
53
54
|
|
|
54
|
-
ORDEM_COLUNAS_FINAL = [
|
|
55
|
-
"SettlementDate",
|
|
56
|
-
"BondType",
|
|
57
|
-
"SelicCode",
|
|
58
|
-
"ISIN",
|
|
59
|
-
"IssueDate",
|
|
60
|
-
"MaturityDate",
|
|
61
|
-
"Trades",
|
|
62
|
-
"Quantity",
|
|
63
|
-
"Value",
|
|
64
|
-
"MinPrice",
|
|
65
|
-
"AvgPrice",
|
|
66
|
-
"MaxPrice",
|
|
67
|
-
"UnderlyingPrice",
|
|
68
|
-
"ParValue",
|
|
69
|
-
"MinRate",
|
|
70
|
-
"AvgRate",
|
|
71
|
-
"MaxRate",
|
|
72
|
-
"BrokerageTrades",
|
|
73
|
-
"BrokerageQuantity",
|
|
74
|
-
]
|
|
75
|
-
|
|
76
55
|
CHAVES_ORDENACAO = ["SettlementDate", "BondType", "MaturityDate"]
|
|
77
56
|
|
|
78
57
|
|
|
@@ -90,6 +69,7 @@ def _montar_url(data_alvo: dt.date, extragroup: bool) -> str:
|
|
|
90
69
|
return f"{URL_BASE}/Neg{sufixo_operacao}{ano_mes}.ZIP"
|
|
91
70
|
|
|
92
71
|
|
|
72
|
+
@ttl_cache()
|
|
93
73
|
@retry_padrao
|
|
94
74
|
def _baixar_zip(url_arquivo: str) -> bytes:
|
|
95
75
|
"""Baixa o conteúdo ZIP e retorna os bytes."""
|
|
@@ -116,7 +96,7 @@ def _ler_df_zip(conteudo_csv: bytes) -> pl.DataFrame:
|
|
|
116
96
|
|
|
117
97
|
|
|
118
98
|
def _processar_df(df: pl.DataFrame) -> pl.DataFrame:
|
|
119
|
-
"""
|
|
99
|
+
"""Renomeia colunas, converte datas, calcula valor e ordena."""
|
|
120
100
|
colunas_data = ["SettlementDate", "IssueDate", "MaturityDate"]
|
|
121
101
|
return (
|
|
122
102
|
df.rename(MAPEAMENTO_COLUNAS)
|
|
@@ -128,12 +108,6 @@ def _processar_df(df: pl.DataFrame) -> pl.DataFrame:
|
|
|
128
108
|
)
|
|
129
109
|
|
|
130
110
|
|
|
131
|
-
def _ordenar_selecionar_colunas(df: pl.DataFrame) -> pl.DataFrame:
|
|
132
|
-
"""Reordena colunas e ordena linhas para saída consistente e determinística."""
|
|
133
|
-
colunas_selecionadas = [col for col in ORDEM_COLUNAS_FINAL if col in df.columns]
|
|
134
|
-
return df.select(colunas_selecionadas).sort(by=CHAVES_ORDENACAO)
|
|
135
|
-
|
|
136
|
-
|
|
137
111
|
def tpf_monthly_trades(target_date: DateLike, extragroup: bool = False) -> pl.DataFrame:
|
|
138
112
|
"""Consulta negociações mensais no mercado secundário de TPF
|
|
139
113
|
registradas no sistema Selic do BCB.
|
|
@@ -164,19 +138,19 @@ def tpf_monthly_trades(target_date: DateLike, extragroup: bool = False) -> pl.Da
|
|
|
164
138
|
- ISIN (str): código ISIN (International Securities Identification Number).
|
|
165
139
|
- IssueDate (Date): data de emissão do título.
|
|
166
140
|
- MaturityDate (Date): data de vencimento do título.
|
|
167
|
-
- Trades (Int64): número de operações realizadas.
|
|
168
|
-
- Quantity (Int64): quantidade negociada.
|
|
141
|
+
- Trades (Int64): número total de operações realizadas.
|
|
142
|
+
- Quantity (Int64): quantidade total negociada.
|
|
169
143
|
- Value (Float64): valor financeiro negociado (Quantity * AvgPrice).
|
|
170
144
|
- MinPrice (Float64): preço unitário mínimo.
|
|
171
145
|
- AvgPrice (Float64): preço unitário médio.
|
|
172
146
|
- MaxPrice (Float64): preço unitário máximo.
|
|
173
147
|
- UnderlyingPrice (Float64): PU lastro.
|
|
174
|
-
- ParValue (Float64): valor
|
|
148
|
+
- ParValue (Float64): valor nominal atualizado (VNA) do título.
|
|
175
149
|
- MinRate (Float64): taxa mínima.
|
|
176
150
|
- AvgRate (Float64): taxa média.
|
|
177
151
|
- MaxRate (Float64): taxa máxima.
|
|
178
|
-
- BrokerageTrades (Int64):
|
|
179
|
-
- BrokerageQuantity (Int64):
|
|
152
|
+
- BrokerageTrades (Int64): subconjunto de Trades com corretagem.
|
|
153
|
+
- BrokerageQuantity (Int64): subconjunto de Quantity com corretagem.
|
|
180
154
|
|
|
181
155
|
Notes:
|
|
182
156
|
- Dados ordenados por: SettlementDate, BondType, MaturityDate.
|
|
@@ -187,7 +161,6 @@ def tpf_monthly_trades(target_date: DateLike, extragroup: bool = False) -> pl.Da
|
|
|
187
161
|
|
|
188
162
|
"""
|
|
189
163
|
if any_is_empty(target_date):
|
|
190
|
-
registro.warning("Nenhuma data informada. Retornando DataFrame vazio.")
|
|
191
164
|
return pl.DataFrame()
|
|
192
165
|
try:
|
|
193
166
|
data_alvo = converter_datas(target_date)
|
|
@@ -197,7 +170,6 @@ def tpf_monthly_trades(target_date: DateLike, extragroup: bool = False) -> pl.Da
|
|
|
197
170
|
arquivo_extraido = _descompactar_zip(conteudo_zip)
|
|
198
171
|
df = _ler_df_zip(arquivo_extraido)
|
|
199
172
|
df = _processar_df(df)
|
|
200
|
-
df = _ordenar_selecionar_colunas(df)
|
|
201
173
|
except Exception as e:
|
|
202
174
|
registro.exception(f"Erro ao coletar dados mensais do BCB: {e}")
|
|
203
175
|
return pl.DataFrame()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
import cachetools
|
|
4
3
|
import requests
|
|
5
4
|
|
|
5
|
+
from pyield._internal.cache import ttl_cache
|
|
6
6
|
from pyield._internal.converters import converter_datas
|
|
7
7
|
from pyield._internal.retry import retry_padrao
|
|
8
8
|
from pyield._internal.types import DateLike, any_is_empty
|
|
@@ -10,7 +10,7 @@ from pyield._internal.types import DateLike, any_is_empty
|
|
|
10
10
|
logger = logging.getLogger(__name__)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
@
|
|
13
|
+
@ttl_cache(ttl=15)
|
|
14
14
|
@retry_padrao
|
|
15
15
|
def _baixar_texto(date: DateLike) -> str:
|
|
16
16
|
"""Baixa o arquivo diário do SELIC no site do BCB."""
|
|
@@ -143,6 +143,10 @@ def count(
|
|
|
143
143
|
>>> bday.count("15-12-2023", "01-01-2024")
|
|
144
144
|
10
|
|
145
145
|
|
|
146
|
+
Contagem negativa quando ``start`` é posterior a ``end``:
|
|
147
|
+
>>> bday.count("08-01-2023", "01-01-2023")
|
|
148
|
+
-5
|
|
149
|
+
|
|
146
150
|
Total de dias úteis em janeiro e fevereiro desde o início do ano:
|
|
147
151
|
>>> bday.count(start="01-01-2024", end=["01-02-2024", "01-03-2024"])
|
|
148
152
|
shape: (2,)
|
|
@@ -266,10 +266,6 @@ def forward(
|
|
|
266
266
|
>>> print(yd.forward(20, 10, 0.06, 0.05))
|
|
267
267
|
nan
|
|
268
268
|
|
|
269
|
-
>>> # Argumentos nulos retornam nan
|
|
270
|
-
>>> print(yd.forward(10, 20, 0.05, None))
|
|
271
|
-
nan
|
|
272
|
-
|
|
273
269
|
Notes:
|
|
274
270
|
`du₂` precisa ser necessariamente maior que `du₁` para que
|
|
275
271
|
o cálculo da taxa a termo seja matematicamente válido.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import polars as pl
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from pyield._internal.cache import ttl_cache
|
|
8
|
+
from pyield._internal.retry import retry_padrao
|
|
9
|
+
|
|
10
|
+
_URL_XLS = "https://www.anbima.com.br/informacoes/indicadores/arqs/indicadores.xls"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ProjecaoIndicador:
|
|
15
|
+
ultima_atualizacao: dt.datetime # Data e hora da última atualização
|
|
16
|
+
periodo_referencia: str # Mês de referência no formato "MMM/YY"
|
|
17
|
+
valor_projetado: float # Valor projetado
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@ttl_cache()
|
|
21
|
+
@retry_padrao
|
|
22
|
+
def _baixar_planilha() -> bytes:
|
|
23
|
+
"""Baixa o arquivo XLS de indicadores da ANBIMA e retorna os bytes."""
|
|
24
|
+
try:
|
|
25
|
+
r = requests.get(_URL_XLS, timeout=10)
|
|
26
|
+
r.raise_for_status()
|
|
27
|
+
return r.content
|
|
28
|
+
except requests.exceptions.RequestException as e:
|
|
29
|
+
raise ConnectionError(f"Erro ao acessar a planilha da ANBIMA: {e}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _extrair_datetime(texto: str) -> dt.datetime:
|
|
33
|
+
"""Extrai datetime do texto da primeira célula do XLS.
|
|
34
|
+
|
|
35
|
+
Formato esperado:
|
|
36
|
+
"Data e Hora da Última Atualização: 13/03/2026 - 15:20 h"
|
|
37
|
+
"""
|
|
38
|
+
# Prefixo fixo → restante é " 13/03/2026 - 15:20 h"
|
|
39
|
+
prefixo = "Data e Hora da Última Atualização:"
|
|
40
|
+
parte_data = texto.split(prefixo, maxsplit=1)[1]
|
|
41
|
+
parte_data = parte_data.strip().removesuffix("h").strip()
|
|
42
|
+
return dt.datetime.strptime(parte_data, "%d/%m/%Y - %H:%M")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _extrair_periodo(texto: str) -> str:
|
|
46
|
+
"""Extrai o período de referência de 'Projeção (mar/26)' → 'mar/26'."""
|
|
47
|
+
inicio = texto.index("(") + 1
|
|
48
|
+
fim = texto.index(")")
|
|
49
|
+
return texto[inicio:fim]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def projected_rate() -> ProjecaoIndicador:
|
|
53
|
+
"""
|
|
54
|
+
Obtém a projeção atual do IPCA no site da ANBIMA.
|
|
55
|
+
|
|
56
|
+
A função baixa a planilha XLS de indicadores da ANBIMA e extrai os
|
|
57
|
+
dados da projeção do IPCA.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
ProjecaoIndicador: Objeto contendo:
|
|
61
|
+
- ultima_atualizacao (dt.datetime): Data e hora da última
|
|
62
|
+
atualização.
|
|
63
|
+
- periodo_referencia (str): Período de referência no formato
|
|
64
|
+
"MMM/YY" (ex.: "mar/26").
|
|
65
|
+
- valor_projetado (float): Valor projetado do IPCA (decimal).
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ConnectionError: Se houver erro de conexão com o site da ANBIMA.
|
|
69
|
+
ValueError: Se os dados esperados não forem encontrados na
|
|
70
|
+
planilha.
|
|
71
|
+
|
|
72
|
+
Notes:
|
|
73
|
+
Requer conexão com a internet para acessar o site da ANBIMA.
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
>>> from pyield import ipca
|
|
77
|
+
>>> # Obter a projeção atual do IPCA na ANBIMA
|
|
78
|
+
>>> ipca.projected_rate()
|
|
79
|
+
ProjecaoIndicador(ultima_atualizacao=..., periodo_referencia=..., valor_projetado=...)
|
|
80
|
+
"""
|
|
81
|
+
conteudo = _baixar_planilha()
|
|
82
|
+
df = pl.read_excel(conteudo, has_header=False)
|
|
83
|
+
|
|
84
|
+
# Linha 0: "Data e Hora da Última Atualização: DD/MM/YYYY - HH:MM h"
|
|
85
|
+
texto_atualizacao = df.item(0, "column_1")
|
|
86
|
+
ultima_atualizacao = _extrair_datetime(texto_atualizacao)
|
|
87
|
+
|
|
88
|
+
# Linha do IPCA: column_1 começa com "IPCA" e column_2 com "Projeção"
|
|
89
|
+
linha_ipca = df.filter(
|
|
90
|
+
pl.col("column_1").str.starts_with("IPCA")
|
|
91
|
+
& pl.col("column_2").str.starts_with("Projeção")
|
|
92
|
+
)
|
|
93
|
+
if linha_ipca.is_empty():
|
|
94
|
+
raise ValueError("Não foi possível encontrar a projeção do IPCA na planilha.")
|
|
95
|
+
|
|
96
|
+
periodo_referencia = _extrair_periodo(linha_ipca.item(0, "column_2"))
|
|
97
|
+
valor_projetado = round(float(linha_ipca.item(0, "column_3")) / 100, 4)
|
|
98
|
+
|
|
99
|
+
return ProjecaoIndicador(
|
|
100
|
+
ultima_atualizacao=ultima_atualizacao,
|
|
101
|
+
periodo_referencia=periodo_referencia,
|
|
102
|
+
valor_projetado=valor_projetado,
|
|
103
|
+
)
|
|
@@ -17,6 +17,7 @@ URL_BASE = (
|
|
|
17
17
|
"https://www.tesourotransparente.gov.br/publicacoes/relatorio-mensal-da-divida-rmd"
|
|
18
18
|
)
|
|
19
19
|
_ABAS_DISPONIVEIS = ("1.3",)
|
|
20
|
+
_TIMEOUT_SEGUNDOS = 60
|
|
20
21
|
|
|
21
22
|
# Índices de linha (0-based) na planilha após leitura com fastexcel (sem cabeçalho)
|
|
22
23
|
# O fastexcel compacta linhas totalmente vazias, resultando em 81 linhas ao invés das
|
|
@@ -89,17 +90,9 @@ def _parsear_periodo(periodo: str) -> datetime.date | None:
|
|
|
89
90
|
|
|
90
91
|
|
|
91
92
|
@retry_padrao
|
|
92
|
-
def
|
|
93
|
+
def _buscar_conteudo(url: str) -> bytes:
|
|
93
94
|
"""Busca o conteúdo de uma URL, seguindo redirects, com retry."""
|
|
94
|
-
resposta = requests.get(url, timeout=
|
|
95
|
-
resposta.raise_for_status()
|
|
96
|
-
return resposta.content
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@retry_padrao
|
|
100
|
-
def _baixar_zip(url: str) -> bytes:
|
|
101
|
-
"""Baixa o arquivo ZIP com retry."""
|
|
102
|
-
resposta = requests.get(url, timeout=60)
|
|
95
|
+
resposta = requests.get(url, timeout=_TIMEOUT_SEGUNDOS)
|
|
103
96
|
resposta.raise_for_status()
|
|
104
97
|
return resposta.content
|
|
105
98
|
|
|
@@ -110,7 +103,7 @@ def _buscar_url_anexo() -> str:
|
|
|
110
103
|
A URL base redireciona automaticamente para a página do mês atual.
|
|
111
104
|
O lxml localiza o link do anexo ZIP nessa página.
|
|
112
105
|
"""
|
|
113
|
-
conteudo_pagina =
|
|
106
|
+
conteudo_pagina = _buscar_conteudo(URL_BASE)
|
|
114
107
|
arvore = html.fromstring(conteudo_pagina)
|
|
115
108
|
resultado = arvore.xpath("//a[contains(@href, 'publicacao-anexo')]/@href")
|
|
116
109
|
if not isinstance(resultado, list) or not resultado:
|
|
@@ -169,25 +162,15 @@ def _classificar_categorias(
|
|
|
169
162
|
return eventos_titulos, eventos_diretos
|
|
170
163
|
|
|
171
164
|
|
|
172
|
-
def _construir_grupos(
|
|
173
|
-
eventos: list[tuple[int, str, str, str]],
|
|
174
|
-
) -> dict[tuple[str, str], dict[str, int]]:
|
|
175
|
-
"""Agrupa eventos por (grupo, subgrupo): {chave → {titulo → índice na matriz}}."""
|
|
176
|
-
grupos: dict[tuple[str, str], dict[str, int]] = {}
|
|
177
|
-
for idx, grupo, subgrupo, titulo in eventos:
|
|
178
|
-
chave = (grupo, subgrupo)
|
|
179
|
-
if chave not in grupos:
|
|
180
|
-
grupos[chave] = {}
|
|
181
|
-
grupos[chave][titulo] = idx
|
|
182
|
-
return grupos
|
|
183
|
-
|
|
184
|
-
|
|
185
165
|
def _montar_df_titulos(
|
|
186
|
-
|
|
166
|
+
eventos: list[tuple[int, str, str, str]],
|
|
187
167
|
datas_mensais: list[datetime.date],
|
|
188
168
|
matriz: pl.DataFrame,
|
|
189
169
|
) -> pl.DataFrame:
|
|
190
170
|
"""Monta DataFrame de detalhamento por título na ordem canônica, após unpivot."""
|
|
171
|
+
grupos: dict[tuple[str, str], dict[str, int]] = {}
|
|
172
|
+
for idx, grupo, subgrupo, titulo in eventos:
|
|
173
|
+
grupos.setdefault((grupo, subgrupo), {})[titulo] = idx
|
|
191
174
|
n_periodos = len(datas_mensais)
|
|
192
175
|
partes: list[pl.DataFrame] = []
|
|
193
176
|
for grupo, subgrupo in _ORDEM_SUBGRUPOS:
|
|
@@ -267,8 +250,7 @@ def _estruturar_dados(conteudo_excel: bytes) -> pl.DataFrame:
|
|
|
267
250
|
# Matriz de valores (apenas períodos mensais), cast para Float64
|
|
268
251
|
matriz = df_dados[:, 1:].cast(pl.Float64, strict=False)[:, indices_mensais]
|
|
269
252
|
|
|
270
|
-
|
|
271
|
-
df_titulos = _montar_df_titulos(grupos, datas_mensais, matriz)
|
|
253
|
+
df_titulos = _montar_df_titulos(eventos_titulos, datas_mensais, matriz)
|
|
272
254
|
partes_diretas = _montar_partes_diretos(eventos_diretos, datas_mensais, matriz)
|
|
273
255
|
return (
|
|
274
256
|
pl.concat([df_titulos] + partes_diretas)
|
|
@@ -277,9 +259,6 @@ def _estruturar_dados(conteudo_excel: bytes) -> pl.DataFrame:
|
|
|
277
259
|
)
|
|
278
260
|
|
|
279
261
|
|
|
280
|
-
_PROCESSADORES: dict[str, object] = {"1.3": _estruturar_dados}
|
|
281
|
-
|
|
282
|
-
|
|
283
262
|
def rmd(tab: str) -> pl.DataFrame:
|
|
284
263
|
"""Retorna dados do Relatório Mensal da Dívida (RMD) do Tesouro Nacional.
|
|
285
264
|
|
|
@@ -353,18 +332,17 @@ def rmd(tab: str) -> pl.DataFrame:
|
|
|
353
332
|
>>> round(resgates_2025, 2)
|
|
354
333
|
1395109062272.45
|
|
355
334
|
"""
|
|
356
|
-
if tab not in
|
|
357
|
-
disponiveis = ", ".join(f'"{t}"' for t in sorted(
|
|
335
|
+
if tab not in _ABAS_DISPONIVEIS:
|
|
336
|
+
disponiveis = ", ".join(f'"{t}"' for t in sorted(_ABAS_DISPONIVEIS))
|
|
358
337
|
raise ValueError(
|
|
359
338
|
f"Aba '{tab}' não disponível. Abas implementadas: {disponiveis}."
|
|
360
339
|
)
|
|
361
|
-
processador = _PROCESSADORES[tab]
|
|
362
340
|
try:
|
|
363
341
|
url_anexo = _buscar_url_anexo()
|
|
364
342
|
registro.debug(f"URL do anexo RMD: {url_anexo}")
|
|
365
|
-
conteudo_zip =
|
|
343
|
+
conteudo_zip = _buscar_conteudo(url_anexo)
|
|
366
344
|
conteudo_excel = _extrair_excel(conteudo_zip)
|
|
367
|
-
df =
|
|
345
|
+
df = _estruturar_dados(conteudo_excel)
|
|
368
346
|
except Exception as e:
|
|
369
347
|
registro.exception(f"Erro ao coletar dados do RMD (aba {tab!r}): {e}")
|
|
370
348
|
return pl.DataFrame()
|