PYield 0.28.2__tar.gz → 0.29.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.28.2 → pyield-0.29.0}/PKG-INFO +2 -1
- pyield-0.29.0/pyield/__about__.py +1 -0
- pyield-0.29.0/pyield/__init__.py +28 -0
- pyield-0.29.0/pyield/anbima/__init__.py +13 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/anbima/imaq.py +1 -1
- pyield-0.29.0/pyield/anbima/ipca.py +52 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/anbima/tpf.py +4 -4
- {pyield-0.28.2 → pyield-0.29.0}/pyield/b3_futures/di.py +1 -1
- pyield-0.29.0/pyield/bc/__init__.py +15 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/bc/auction.py +4 -2
- pyield-0.29.0/pyield/bc/bcdata.py +207 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/bc/repos.py +2 -0
- pyield-0.29.0/pyield/bc/sec.py +85 -0
- pyield-0.29.0/pyield/bc/vna.py +92 -0
- pyield-0.29.0/pyield/config.py +36 -0
- pyield-0.29.0/pyield/conftest.py +13 -0
- pyield-0.29.0/pyield/ibge/__init__.py +5 -0
- pyield-0.29.0/pyield/ibge/ipca.py +38 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/lft.py +1 -1
- {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/ltn.py +1 -1
- {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/ntnb.py +1 -1
- {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/ntnf.py +1 -1
- {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/tools.py +2 -2
- {pyield-0.28.2 → pyield-0.29.0}/pyproject.toml +1 -0
- pyield-0.28.2/pyield/__about__.py +0 -1
- pyield-0.28.2/pyield/__init__.py +0 -44
- pyield-0.28.2/pyield/b3_futures/__init__.py +0 -74
- pyield-0.28.2/pyield/conftest.py +0 -8
- pyield-0.28.2/pyield/indicators.py +0 -295
- pyield-0.28.2/pyield/projections.py +0 -81
- {pyield-0.28.2 → pyield-0.29.0}/.gitignore +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/LICENSE +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/README.md +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/anbima/ima.py +0 -0
- {pyield-0.28.2/pyield/anbima → pyield-0.29.0/pyield/b3_futures}/__init__.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/b3_futures/common.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/b3_futures/historical.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/b3_futures/intraday.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/b3_futures/xml.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/bc/ptax.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/bday.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/data_cache.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/date_converter.py +0 -0
- /pyield-0.28.2/pyield/tools.py → /pyield-0.29.0/pyield/forwards.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/holidays/__init__.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/holidays/br_holidays_new.txt +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/holidays/br_holidays_old.txt +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/holidays/brholidays.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/interpolator.py +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/py.typed +0 -0
- {pyield-0.28.2 → pyield-0.29.0}/pyield/tpf/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PYield
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.29.0
|
|
4
4
|
Summary: A Python library for analysis of fixed income instruments in Brazil
|
|
5
5
|
Project-URL: Source, https://github.com/crdcj/PYield
|
|
6
6
|
Author-email: Carlos Carvalho <cr.cj@outlook.com>
|
|
@@ -36,6 +36,7 @@ Requires-Python: >=3.11
|
|
|
36
36
|
Requires-Dist: numpy>=2.0.0
|
|
37
37
|
Requires-Dist: pandas[excel,html,parquet]>=2.0.0
|
|
38
38
|
Requires-Dist: requests>=2.31.0
|
|
39
|
+
Requires-Dist: tenacity>=9.0.0
|
|
39
40
|
Description-Content-Type: text/markdown
|
|
40
41
|
|
|
41
42
|
[](https://pypi.python.org/pypi/pyield)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.29.0"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from pyield import anbima, bc, bday, ibge
|
|
4
|
+
from pyield.__about__ import __version__
|
|
5
|
+
from pyield.b3_futures import futures
|
|
6
|
+
from pyield.b3_futures.di import DIFutures
|
|
7
|
+
from pyield.forwards import forward_rates
|
|
8
|
+
from pyield.interpolator import Interpolator
|
|
9
|
+
from pyield.tpf import lft, ltn, ntnb, ntnf
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"__version__",
|
|
13
|
+
"anbima",
|
|
14
|
+
"bday",
|
|
15
|
+
"DIFutures",
|
|
16
|
+
"forward_rates",
|
|
17
|
+
"futures",
|
|
18
|
+
"ibge",
|
|
19
|
+
"Interpolator",
|
|
20
|
+
"lft",
|
|
21
|
+
"ltn",
|
|
22
|
+
"ntnb",
|
|
23
|
+
"ntnf",
|
|
24
|
+
"bc",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Configura o logger do pacote principal com um NullHandler
|
|
28
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pyield.anbima.ima import last_ima
|
|
2
|
+
from pyield.anbima.imaq import imaq
|
|
3
|
+
from pyield.anbima.ipca import ipca_projection
|
|
4
|
+
from pyield.anbima.tpf import tpf_data, tpf_pre_maturities, tpf_rates
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"last_ima",
|
|
8
|
+
"imaq",
|
|
9
|
+
"ipca_projection",
|
|
10
|
+
"tpf_data",
|
|
11
|
+
"tpf_rates",
|
|
12
|
+
"tpf_pre_maturities",
|
|
13
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import locale
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class IndicatorProjection:
|
|
9
|
+
reference_period: pd.Period # Reference month as a pd.Period object
|
|
10
|
+
projected_value: float # Projected value
|
|
11
|
+
last_updated: pd.Timestamp # Date and time of the last update
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def ipca_projection() -> IndicatorProjection:
|
|
15
|
+
"""
|
|
16
|
+
This function retrieves and parses the Excel file that contains economic indicators,
|
|
17
|
+
specifically looking for the IPCA projection. It extracts the date of the last
|
|
18
|
+
update and the IPCA projection for the reference month.
|
|
19
|
+
|
|
20
|
+
Data file format example after parsing:
|
|
21
|
+
- ['Data e Hora da Última Atualização: 19/04/2024 - 18:55 h', '', '']
|
|
22
|
+
- ...
|
|
23
|
+
- ['IPCA1', 'Projeção (abr/24)', 0.35]
|
|
24
|
+
- ...
|
|
25
|
+
"""
|
|
26
|
+
# Define the URL and get the data
|
|
27
|
+
url = "https://www.anbima.com.br/informacoes/indicadores/arqs/indicadores.xls"
|
|
28
|
+
df = pd.read_excel(url, dtype_backend="numpy_nullable")
|
|
29
|
+
|
|
30
|
+
last_update_str = df.columns[0].split("Atualização:")[-1].strip()
|
|
31
|
+
last_update = pd.to_datetime(last_update_str, format="%d/%m/%Y - %H:%M h")
|
|
32
|
+
|
|
33
|
+
ipca_row = df[df.iloc[:, 0] == "IPCA1"]
|
|
34
|
+
|
|
35
|
+
ipca_value = ipca_row.iloc[0, 2]
|
|
36
|
+
ipca_value = pd.to_numeric(ipca_value, errors="coerce")
|
|
37
|
+
ipca_value = round(float(ipca_value) / 100, 4)
|
|
38
|
+
|
|
39
|
+
# Extract and format the reference month
|
|
40
|
+
ipca_date = ipca_row.iloc[0, 1]
|
|
41
|
+
ipca_date = str(ipca_date)
|
|
42
|
+
ipca_date = ipca_date.split("(")[-1].split(")")[0]
|
|
43
|
+
locale.setlocale(locale.LC_TIME, "pt_BR.UTF-8")
|
|
44
|
+
ipca_date = pd.to_datetime(ipca_date, format="%b/%y")
|
|
45
|
+
reference_period = ipca_date.to_period("M")
|
|
46
|
+
locale.setlocale(locale.LC_TIME, "") # Reset locale to default
|
|
47
|
+
|
|
48
|
+
return IndicatorProjection(
|
|
49
|
+
last_updated=last_update,
|
|
50
|
+
reference_period=reference_period,
|
|
51
|
+
projected_value=ipca_value,
|
|
52
|
+
)
|
|
@@ -106,7 +106,7 @@ def _process_raw_df(df_raw: pd.DataFrame) -> pd.DataFrame:
|
|
|
106
106
|
return df.sort_values(["BondType", "MaturityDate"], ignore_index=True)
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
def
|
|
109
|
+
def tpf_data(date: DateScalar, bond_type: str | None = None) -> pd.DataFrame:
|
|
110
110
|
"""Fetch and process TPF market data from ANBIMA.
|
|
111
111
|
|
|
112
112
|
This function retrieves bond market data from the ANBIMA website for a
|
|
@@ -141,7 +141,7 @@ def anbima_tpf_data(date: DateScalar, bond_type: str | None = None) -> pd.DataFr
|
|
|
141
141
|
return df.reset_index(drop=True)
|
|
142
142
|
|
|
143
143
|
|
|
144
|
-
def
|
|
144
|
+
def tpf_rates(
|
|
145
145
|
date: DateScalar,
|
|
146
146
|
bond_type: str | None = None,
|
|
147
147
|
adj_maturities: bool = False,
|
|
@@ -172,7 +172,7 @@ def anbima_tpf_rates(
|
|
|
172
172
|
|
|
173
173
|
if df.empty:
|
|
174
174
|
# Try to fetch the data from the Anbima website
|
|
175
|
-
df =
|
|
175
|
+
df = tpf_data(date)
|
|
176
176
|
|
|
177
177
|
if df.empty:
|
|
178
178
|
# If the data is still empty, return an empty DataFrame
|
|
@@ -203,7 +203,7 @@ def tpf_pre_maturities(date: DateScalar) -> pd.Series:
|
|
|
203
203
|
'NTN-F' bonds, sorted in ascending order.
|
|
204
204
|
"""
|
|
205
205
|
maturity_dates = (
|
|
206
|
-
|
|
206
|
+
tpf_rates(date)
|
|
207
207
|
.query("BondType in ['LTN', 'NTN-F']")["MaturityDate"]
|
|
208
208
|
.drop_duplicates()
|
|
209
209
|
.sort_values(ignore_index=True)
|
|
@@ -2,7 +2,7 @@ import pandas as pd
|
|
|
2
2
|
|
|
3
3
|
import pyield.b3_futures as b3
|
|
4
4
|
import pyield.date_converter as dc
|
|
5
|
-
import pyield.
|
|
5
|
+
import pyield.forwards as tl
|
|
6
6
|
from pyield import bday, interpolator
|
|
7
7
|
from pyield.data_cache import get_anbima_dataset, get_di_dataset
|
|
8
8
|
from pyield.date_converter import DateArray, DateScalar
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pyield.bc.auction import auctions
|
|
2
|
+
from pyield.bc.bcdata import di_over, selic_over, selic_target
|
|
3
|
+
from pyield.bc.ptax import ptax
|
|
4
|
+
from pyield.bc.repos import repos
|
|
5
|
+
from pyield.bc.vna import vna_lft
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"auctions",
|
|
9
|
+
"repos",
|
|
10
|
+
"ptax",
|
|
11
|
+
"vna_lft",
|
|
12
|
+
"selic_over",
|
|
13
|
+
"selic_target",
|
|
14
|
+
"di_over",
|
|
15
|
+
]
|
|
@@ -14,6 +14,7 @@ import requests
|
|
|
14
14
|
|
|
15
15
|
from pyield import bday
|
|
16
16
|
from pyield import date_converter as dc
|
|
17
|
+
from pyield.config import default_retry
|
|
17
18
|
from pyield.date_converter import DateScalar
|
|
18
19
|
from pyield.tpf.ntnb import duration as duration_b
|
|
19
20
|
from pyield.tpf.ntnf import duration as duration_f
|
|
@@ -48,6 +49,7 @@ COLUMN_MAPPING = {
|
|
|
48
49
|
BASE_API_URL = "https://olinda.bcb.gov.br/olinda/servico/leiloes_selic/versao/v1/odata/leiloesTitulosPublicos(dataMovimentoInicio=@dataMovimentoInicio,dataMovimentoFim=@dataMovimentoFim,dataLiquidacao=@dataLiquidacao,codigoTitulo=@codigoTitulo,dataVencimento=@dataVencimento,edital=@edital,tipoPublico=@tipoPublico,tipoOferta=@tipoOferta)?"
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
@default_retry
|
|
51
53
|
def _load_from_url(url: str) -> pd.DataFrame:
|
|
52
54
|
response = requests.get(url, timeout=10)
|
|
53
55
|
response.raise_for_status()
|
|
@@ -224,7 +226,7 @@ def _fetch_df_from_url(url: str) -> pd.DataFrame:
|
|
|
224
226
|
def auctions(
|
|
225
227
|
start: DateScalar | None = None,
|
|
226
228
|
end: DateScalar | None = None,
|
|
227
|
-
auction_type: Literal["Sell", "Buy"] = None,
|
|
229
|
+
auction_type: Literal["Sell", "Buy"] | None = None,
|
|
228
230
|
) -> pd.DataFrame:
|
|
229
231
|
"""
|
|
230
232
|
Recupera dados de leilões para um determinado período e tipo de leilão da API do BC.
|
|
@@ -318,7 +320,7 @@ def auctions(
|
|
|
318
320
|
|
|
319
321
|
# Mapeamento do auction_type para o valor esperado pela API
|
|
320
322
|
auction_type_mapping = {"Sell": "Venda", "Buy": "Compra"}
|
|
321
|
-
auction_type_api_value = auction_type_mapping.get(auction_type)
|
|
323
|
+
auction_type_api_value = auction_type_mapping.get(str(auction_type))
|
|
322
324
|
# Adiciona o parâmetro tipoOferta à URL se auction_type for fornecido
|
|
323
325
|
if auction_type_api_value:
|
|
324
326
|
url += f"&@tipoOferta='{auction_type_api_value}'"
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from urllib.error import HTTPError
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from pyield.config import default_retry
|
|
9
|
+
from pyield.date_converter import DateScalar, convert_input_dates
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
BASE_URL = "https://api.bcb.gov.br/dados/serie/bcdata.sgs."
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BCSerie(Enum):
|
|
16
|
+
"""Enum para as séries disponíveis no Banco Central."""
|
|
17
|
+
|
|
18
|
+
SELIC_OVER = 1178
|
|
19
|
+
SELIC_TARGET = 432
|
|
20
|
+
DI_OVER = 11
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _build_download_url(
|
|
24
|
+
serie: BCSerie, start: DateScalar | None = None, end: DateScalar | None = None
|
|
25
|
+
) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Builds the URL for downloading data from the Central Bank series.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
serie: The series enum value to fetch
|
|
31
|
+
start: The start date for the data to fetch
|
|
32
|
+
end: The end date for the data
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The formatted URL for the API request
|
|
36
|
+
"""
|
|
37
|
+
start_str = ""
|
|
38
|
+
if start:
|
|
39
|
+
start = convert_input_dates(start)
|
|
40
|
+
start_str = start.strftime("%d/%m/%Y")
|
|
41
|
+
|
|
42
|
+
end_str = ""
|
|
43
|
+
if end:
|
|
44
|
+
end = convert_input_dates(end)
|
|
45
|
+
end_str = end.strftime("%d/%m/%Y")
|
|
46
|
+
|
|
47
|
+
api_url = BASE_URL
|
|
48
|
+
api_url += f"{serie.value}/dados?formato=csv"
|
|
49
|
+
api_url += f"&dataInicial={start_str}&dataFinal={end_str}"
|
|
50
|
+
|
|
51
|
+
return api_url
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@default_retry
|
|
55
|
+
def _fetch_data_from_url(
|
|
56
|
+
serie: BCSerie, start: DateScalar | None = None, end: DateScalar | None = None
|
|
57
|
+
) -> pd.DataFrame:
|
|
58
|
+
"""
|
|
59
|
+
Fetches data from the Central Bank API with retry mechanism.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
serie: The series enum to fetch
|
|
63
|
+
start: The start date for the data to fetch
|
|
64
|
+
end: The end date for the data
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
DataFrame with the requested data
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
HTTPError: If the resource is not found (404) or other HTTP error
|
|
71
|
+
Various exceptions: For other errors after retry attempts are exhausted
|
|
72
|
+
"""
|
|
73
|
+
api_url = _build_download_url(serie, start, end)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
df = pd.read_csv(api_url, sep=";", decimal=",", dtype_backend="numpy_nullable")
|
|
77
|
+
if df.empty or "valor" not in df.columns:
|
|
78
|
+
logger.warning(f"No data available for the requested period: {api_url}")
|
|
79
|
+
return pd.DataFrame()
|
|
80
|
+
|
|
81
|
+
# Process the dataframe
|
|
82
|
+
df = df.rename(columns={"data": "Date", "valor": "Value"})
|
|
83
|
+
df["Date"] = pd.to_datetime(df["Date"], format="%d/%m/%Y")
|
|
84
|
+
# Value is in percentage format, so divide by 100
|
|
85
|
+
df["Value"] /= 100
|
|
86
|
+
return df
|
|
87
|
+
except HTTPError as e:
|
|
88
|
+
if e.code == 404: # noqa
|
|
89
|
+
logger.warning(f"Resource not found (404): {api_url}")
|
|
90
|
+
return pd.DataFrame()
|
|
91
|
+
else:
|
|
92
|
+
logger.error(f"HTTP error accessing Central Bank API: {e}")
|
|
93
|
+
raise
|
|
94
|
+
except pd.errors.ParserError as e:
|
|
95
|
+
# For parsing errors, log and re-raise to allow retry
|
|
96
|
+
logger.warning(f"CSV parsing error (possibly HTML or invalid format): {e}")
|
|
97
|
+
raise
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Error fetching data from Central Bank API: {e}")
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@lru_cache(maxsize=128)
|
|
104
|
+
def selic_over(
|
|
105
|
+
start: DateScalar | None = None, end: DateScalar | None = None
|
|
106
|
+
) -> pd.DataFrame:
|
|
107
|
+
"""
|
|
108
|
+
Fetches the SELIC Over rate for a specific reference date.
|
|
109
|
+
|
|
110
|
+
The SELIC Over rate is the daily average interest rate effectively practiced
|
|
111
|
+
between banks in the interbank market, using public securities as collateral.
|
|
112
|
+
|
|
113
|
+
API URL Example:
|
|
114
|
+
https://api.bcb.gov.br/dados/serie/bcdata.sgs.1178/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
start: The start date for the data to fetch
|
|
118
|
+
end: The end date for the data to fetch
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
DataFrame containing Date and Value columns with the SELIC Over rate,
|
|
122
|
+
or empty DataFrame if data is not available.
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
>>> yd.bc.selic_over("31-05-2024", "31-05-2024")
|
|
126
|
+
Date Value
|
|
127
|
+
0 2024-05-31 0.104
|
|
128
|
+
|
|
129
|
+
# Rate changed due to Copom meeting
|
|
130
|
+
>>> yd.bc.selic_over("26-01-2025") # No data on 26-01-2025 (sunday)
|
|
131
|
+
Date Value
|
|
132
|
+
0 2025-01-27 0.1215
|
|
133
|
+
1 2025-01-28 0.1215
|
|
134
|
+
2 2025-01-29 0.1215
|
|
135
|
+
3 2025-01-30 0.1315
|
|
136
|
+
4 2025-01-31 0.1315
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
df = _fetch_data_from_url(BCSerie.SELIC_OVER, start, end)
|
|
141
|
+
df["Value"] = df["Value"].round(4)
|
|
142
|
+
return df
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@lru_cache(maxsize=128)
|
|
146
|
+
def selic_target(
|
|
147
|
+
start: DateScalar | None = None, end: DateScalar | None = None
|
|
148
|
+
) -> pd.DataFrame:
|
|
149
|
+
"""
|
|
150
|
+
Fetches the SELIC Target rate for a specific reference date.
|
|
151
|
+
|
|
152
|
+
The SELIC Target rate is the official rate set by the Central Bank of Brazil.
|
|
153
|
+
|
|
154
|
+
API URL Example:
|
|
155
|
+
https://api.bcb.gov.br/dados/serie/bcdata.sgs.432/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
start: The start date for the data to fetch
|
|
159
|
+
end: The end date for the data to fetch
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
DataFrame containing Date and Value columns with the SELIC Target rate,
|
|
163
|
+
or empty DataFrame if data is not available.
|
|
164
|
+
|
|
165
|
+
Examples:
|
|
166
|
+
>>> yd.bc.selic_target("31-05-2024", "31-05-2024")
|
|
167
|
+
Date Value
|
|
168
|
+
0 2024-05-31 0.105
|
|
169
|
+
"""
|
|
170
|
+
df = _fetch_data_from_url(BCSerie.SELIC_TARGET, start, end)
|
|
171
|
+
df["Value"] = df["Value"].round(4)
|
|
172
|
+
return df
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@lru_cache(maxsize=128)
|
|
176
|
+
def di_over(
|
|
177
|
+
start: DateScalar | None = None,
|
|
178
|
+
end: DateScalar | None = None,
|
|
179
|
+
annualized: bool = True,
|
|
180
|
+
) -> pd.DataFrame:
|
|
181
|
+
"""
|
|
182
|
+
Fetches the DI (Interbank Deposit) rate for a specific reference date.
|
|
183
|
+
|
|
184
|
+
API URL Example:
|
|
185
|
+
https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
|
|
186
|
+
https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=csv&dataInicial=12/04/2024&dataFinal=12/04/2024
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
start: The start date for the data to fetch
|
|
190
|
+
end: The end date for the data to fetch
|
|
191
|
+
annualized: If True, returns the annualized rate (252 trading
|
|
192
|
+
days per year), otherwise returns the daily rate.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
DataFrame containing Date and Value columns with the DI rate,
|
|
196
|
+
or empty DataFrame if data is not available.
|
|
197
|
+
|
|
198
|
+
Examples:
|
|
199
|
+
>>> yd.bc.di_over("31-05-2024", "31-05-2024")
|
|
200
|
+
Date Value
|
|
201
|
+
0 2024-05-31 0.104
|
|
202
|
+
"""
|
|
203
|
+
df = _fetch_data_from_url(BCSerie.DI_OVER, start, end)
|
|
204
|
+
if annualized:
|
|
205
|
+
df["Value"] = (df["Value"] + 1) ** 252 - 1
|
|
206
|
+
df["Value"] = df["Value"].round(4)
|
|
207
|
+
return df
|
|
@@ -13,6 +13,7 @@ import requests
|
|
|
13
13
|
|
|
14
14
|
from pyield import bday
|
|
15
15
|
from pyield import date_converter as dc
|
|
16
|
+
from pyield.config import default_retry
|
|
16
17
|
from pyield.date_converter import DateScalar
|
|
17
18
|
|
|
18
19
|
"""Dicionário com o mapeamento das colunas da API do BC para o DataFrame final
|
|
@@ -37,6 +38,7 @@ COLUMN_MAPPING = {
|
|
|
37
38
|
BASE_API_URL = "https://olinda.bcb.gov.br/olinda/servico/leiloes_selic/versao/v1/odata/leiloes_compromissadas(dataLancamentoInicio=@dataLancamentoInicio,dataLancamentoFim=@dataLancamentoFim,horaInicio=@horaInicio,dataLiquidacao=@dataLiquidacao,dataRetorno=@dataRetorno,publicoPermitidoLeilao=@publicoPermitidoLeilao,nomeTipoOferta=@nomeTipoOferta)?"
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
@default_retry
|
|
40
42
|
def _load_from_url(url: str) -> pd.DataFrame:
|
|
41
43
|
response = requests.get(url, timeout=10)
|
|
42
44
|
response.raise_for_status()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from pyield.config import default_retry
|
|
6
|
+
from pyield.date_converter import DateScalar, convert_input_dates
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
COLUMN_MAPPING = {
|
|
11
|
+
"DATA MOV": "SettlementDate",
|
|
12
|
+
"SIGLA": "BondType",
|
|
13
|
+
"CODIGO": "SelicCode",
|
|
14
|
+
"CODIGO ISIN": "ISIN",
|
|
15
|
+
"EMISSAO": "IssueDate",
|
|
16
|
+
"VENCIMENTO": "Maturity",
|
|
17
|
+
"NUM DE OPER": "TradeCount",
|
|
18
|
+
"QUANT NEGOCIADA": "TradeQuantity",
|
|
19
|
+
"VALOR NEGOCIADO": "TradeValue",
|
|
20
|
+
"PU MIN": "MinPrice",
|
|
21
|
+
"PU MED": "AvgPrice",
|
|
22
|
+
"PU MAX": "MaxPrice",
|
|
23
|
+
"PU LASTRO": "UnderlyingPrice",
|
|
24
|
+
"VALOR PAR": "ParValue",
|
|
25
|
+
"TAXA MIN": "MinRate",
|
|
26
|
+
"TAXA MED": "AvgRate",
|
|
27
|
+
"TAXA MAX": "MaxRate",
|
|
28
|
+
"NUM OPER COM CORRETAGEM": "BrokerageTradeCount",
|
|
29
|
+
"QUANT NEG COM CORRETAGEM": "BrokerageTradeQuantity",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_download_url(target_date: DateScalar) -> str:
|
|
34
|
+
"""
|
|
35
|
+
URL com todos os arquivos disponíveis:
|
|
36
|
+
https://www4.bcb.gov.br/pom/demab/negociacoes/apresentacao.asp?frame=1
|
|
37
|
+
|
|
38
|
+
Exemplo de URL para download:
|
|
39
|
+
https://www4.bcb.gov.br/pom/demab/negociacoes/download/NegE202409.ZIP
|
|
40
|
+
|
|
41
|
+
File format: NegEYYYYMM.ZIP
|
|
42
|
+
"""
|
|
43
|
+
target_date = convert_input_dates(target_date)
|
|
44
|
+
file_date = target_date.strftime("%Y%m")
|
|
45
|
+
file_name = f"NegE{file_date}.ZIP"
|
|
46
|
+
base_url = "https://www4.bcb.gov.br/pom/demab/negociacoes/download"
|
|
47
|
+
return f"{base_url}/{file_name}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@default_retry
|
|
51
|
+
def _fetch_data_from_url(file_url: str) -> pd.DataFrame:
|
|
52
|
+
df = pd.read_csv(
|
|
53
|
+
file_url,
|
|
54
|
+
sep=";",
|
|
55
|
+
decimal=",",
|
|
56
|
+
dtype_backend="numpy_nullable",
|
|
57
|
+
)
|
|
58
|
+
for col in ["DATA MOV", "EMISSAO", "VENCIMENTO"]:
|
|
59
|
+
df[col] = pd.to_datetime(df[col], format="%d/%m/%Y", errors="coerce")
|
|
60
|
+
|
|
61
|
+
return df
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def sec(target_date: DateScalar) -> pd.DataFrame:
|
|
65
|
+
"""Fetches bond trading data for a specific date from the Brazilian Central Bank.
|
|
66
|
+
|
|
67
|
+
Downloads the daily bond trading data from the Brazilian Central Bank
|
|
68
|
+
website for the given target date. The data is downloaded as a ZIP file,
|
|
69
|
+
extracted, and read into a Pandas DataFrame. The columns are then
|
|
70
|
+
renamed to English names for easier use.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
target_date (DateScalar): The date for which to fetch the bond
|
|
74
|
+
trading data. This can be a date-like object understood by
|
|
75
|
+
`pyield.date_converter.convert_input_dates`.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
pd.DataFrame: A DataFrame containing the bond trading data for the
|
|
79
|
+
specified date. Columns are renamed according to `COLUMN_MAPPING`
|
|
80
|
+
for English readability.
|
|
81
|
+
"""
|
|
82
|
+
url = _build_download_url(target_date)
|
|
83
|
+
df = _fetch_data_from_url(url)
|
|
84
|
+
df = df.rename(columns=COLUMN_MAPPING)
|
|
85
|
+
return df
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from pyield.config import default_retry
|
|
4
|
+
from pyield.date_converter import DateScalar, convert_input_dates
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@default_retry
|
|
8
|
+
def _get_text(date: DateScalar) -> str:
|
|
9
|
+
# url example: https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/20240418APC238
|
|
10
|
+
url_base = "https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/"
|
|
11
|
+
date = convert_input_dates(date)
|
|
12
|
+
url_file = f"{date.strftime('%Y%m%d')}APC238"
|
|
13
|
+
url_vna = url_base + url_file
|
|
14
|
+
|
|
15
|
+
response = requests.get(url_vna)
|
|
16
|
+
response.raise_for_status()
|
|
17
|
+
return response.text
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _extract_vna_table_text(text: str) -> str:
|
|
21
|
+
"""Extrai o texto contendo a tabela VNA do texto bruto."""
|
|
22
|
+
start_of_table = text.find("EMISSAO")
|
|
23
|
+
end_of_table = text.find("99999999*")
|
|
24
|
+
table_text = text[start_of_table:end_of_table].strip()
|
|
25
|
+
return table_text
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_vna_table_lines(table_text: str) -> list[str]:
|
|
29
|
+
"""Processa o texto da tabela VNA para retornar uma lista de linhas."""
|
|
30
|
+
table_lines = table_text.splitlines()
|
|
31
|
+
table_lines = [
|
|
32
|
+
line.strip() for line in table_lines if line.strip()
|
|
33
|
+
] # Remove empty lines
|
|
34
|
+
body_lines = table_lines[1:] # Remove first line (header)
|
|
35
|
+
return body_lines
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _extract_vna_values_from_lines(body_lines: list[str]) -> list[float]:
|
|
39
|
+
"""Extrai valores VNA numéricos de uma lista de linhas de texto."""
|
|
40
|
+
vnas = []
|
|
41
|
+
for line in body_lines:
|
|
42
|
+
vna_str = line.split()[-1].replace(",", ".")
|
|
43
|
+
vnas.append(float(vna_str))
|
|
44
|
+
return vnas
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _validate_vna_values(vnas: list[float]) -> float:
|
|
48
|
+
"""Valida se todos os valores VNA são iguais e retorna o valor único."""
|
|
49
|
+
vna_value = vnas[0]
|
|
50
|
+
if any(vna_value != vna for vna in vnas):
|
|
51
|
+
bcb_url = "https://www.bcb.gov.br/estabilidadefinanceira/selicbaixar"
|
|
52
|
+
msg = f"VNA values are not the same. Please check data at {bcb_url}"
|
|
53
|
+
raise ValueError(msg)
|
|
54
|
+
return vna_value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def vna_lft(date: DateScalar) -> float:
|
|
58
|
+
"""Retrieves the VNA (Valor Nominal Atualizado) from the BCB for a given date.
|
|
59
|
+
|
|
60
|
+
This function fetches daily data from the BCB website, extracts the
|
|
61
|
+
VNA value from a specific table within the downloaded content, and
|
|
62
|
+
returns this value.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
date (DateScalar): The date for which to retrieve the VNA value.
|
|
66
|
+
This argument accepts various date formats, including string and
|
|
67
|
+
datetime objects, which are then standardized using the
|
|
68
|
+
`convert_input_dates` function.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
float: The VNA (Valor Nominal Atualizado) value for the specified date.
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> yd.bc.vna_lft("31-05-2024")
|
|
75
|
+
14903.01148
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
ValueError: If the extracted VNA values from the BCB website are
|
|
79
|
+
inconsistent (i.e., not all extracted values are identical),
|
|
80
|
+
suggesting potential data discrepancies on the source website.
|
|
81
|
+
The error message includes a link to the BCB website for manual
|
|
82
|
+
verification.
|
|
83
|
+
requests.exceptions.HTTPError: If the HTTP request to the BCB website
|
|
84
|
+
fails. This could be due to network issues, website unavailability,
|
|
85
|
+
or the requested data not being found for the given date.
|
|
86
|
+
"""
|
|
87
|
+
text = _get_text(date)
|
|
88
|
+
table_text = _extract_vna_table_text(text)
|
|
89
|
+
table_lines = _parse_vna_table_lines(table_text)
|
|
90
|
+
vnas = _extract_vna_values_from_lines(table_lines)
|
|
91
|
+
vna_value = _validate_vna_values(vnas)
|
|
92
|
+
return vna_value
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from urllib.error import HTTPError, URLError
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import tenacity
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def retry_if_not_404(exception):
|
|
11
|
+
"""
|
|
12
|
+
Retry if the exception is not a 404 error.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
exception: The exception that was raised
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Boolean indicating whether to retry
|
|
19
|
+
"""
|
|
20
|
+
# Don't retry for 404 errors (resource doesn't exist)
|
|
21
|
+
if isinstance(exception, HTTPError) and exception.code == 404: # noqa
|
|
22
|
+
logger.info("Resource not found (404), not retrying.")
|
|
23
|
+
return False
|
|
24
|
+
# Retry for parsing errors and other connection errors
|
|
25
|
+
return isinstance(
|
|
26
|
+
exception, (URLError, pd.errors.EmptyDataError, pd.errors.ParserError)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Custom retry configuration that handles parsing errors
|
|
31
|
+
default_retry = tenacity.retry(
|
|
32
|
+
retry=tenacity.retry_if_exception(retry_if_not_404),
|
|
33
|
+
stop=tenacity.stop_after_attempt(5),
|
|
34
|
+
wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
|
|
35
|
+
before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
|
|
36
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import pyield
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture(autouse=True)
|
|
7
|
+
def add_np(doctest_namespace):
|
|
8
|
+
doctest_namespace["yd"] = pyield
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def pytest_configure(config):
|
|
12
|
+
config.addinivalue_line("doctest_optionflags", "ELLIPSIS")
|
|
13
|
+
config.addinivalue_line("doctest_optionflags", "NORMALIZE_WHITESPACE")
|