PYield 0.30.1__tar.gz → 0.30.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyield-0.30.1 → pyield-0.30.2}/PKG-INFO +1 -1
- pyield-0.30.2/pyield/__about__.py +1 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/anbima/__init__.py +2 -0
- pyield-0.30.2/pyield/anbima/ettj.py +135 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/anbima/tpf.py +18 -7
- {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/di_over.py +12 -12
- {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/lft.py +2 -2
- {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/ltn.py +3 -3
- {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/ntnf.py +2 -2
- pyield-0.30.1/pyield/__about__.py +0 -1
- {pyield-0.30.1 → pyield-0.30.2}/.gitignore +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/LICENSE +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/README.md +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/__init__.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/anbima/ima.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/anbima/imaq.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/anbima/ipca.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/__init__.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/di.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/futures/__init__.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/futures/common.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/futures/historical.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/futures/intraday.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/futures/xml.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/__init__.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/auction.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/bcdata.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/ptax.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/repo.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/sec.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/vna.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/bday.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/conftest.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/data_cache.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/date_converter.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/forwards.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/holidays/__init__.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/holidays/br_holidays_new.txt +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/holidays/br_holidays_old.txt +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/holidays/brholidays.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/ibge/__init__.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/ibge/ipca.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/interpolator.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/py.typed +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/retry.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/__init__.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/ntnb.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/tools.py +0 -0
- {pyield-0.30.1 → pyield-0.30.2}/pyproject.toml +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.30.2"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from pyield.anbima.ettj import last_ettj
|
|
1
2
|
from pyield.anbima.ima import last_ima
|
|
2
3
|
from pyield.anbima.imaq import imaq
|
|
3
4
|
from pyield.anbima.ipca import ipca_projection
|
|
@@ -10,4 +11,5 @@ __all__ = [
|
|
|
10
11
|
"tpf_data",
|
|
11
12
|
"tpf_rates",
|
|
12
13
|
"tpf_pre_maturities",
|
|
14
|
+
"last_ettj",
|
|
13
15
|
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from io import StringIO
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from pyield.forwards import forward_rates
|
|
8
|
+
from pyield.retry import default_retry
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
LAST_ETTJ_URL = "https://www.anbima.com.br/informacoes/est-termo/CZ-down.asp"
|
|
12
|
+
|
|
13
|
+
# Anbima ETTJ data has 4 decimal places in percentage values
|
|
14
|
+
# We will round to 6 decimal places to avoid floating point errors
|
|
15
|
+
ROUND_DIGITS = 6
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@default_retry
|
|
19
|
+
def _get_last_content() -> str:
|
|
20
|
+
"""Fetches the raw yield curve data from ANBIMA."""
|
|
21
|
+
request_payload = {
|
|
22
|
+
"Idioma": "PT",
|
|
23
|
+
"Dt_Ref": "",
|
|
24
|
+
"saida": "csv",
|
|
25
|
+
}
|
|
26
|
+
r = requests.post(LAST_ETTJ_URL, data=request_payload)
|
|
27
|
+
r.raise_for_status()
|
|
28
|
+
return r.text
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _convert_text_to_df(text: str) -> pd.DataFrame:
|
|
32
|
+
"""Converts the raw yield curve text to a DataFrame."""
|
|
33
|
+
raw_df = pd.read_csv(
|
|
34
|
+
StringIO(text),
|
|
35
|
+
sep=";",
|
|
36
|
+
encoding="latin1",
|
|
37
|
+
skiprows=5,
|
|
38
|
+
dtype_backend="numpy_nullable",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
idx = raw_df.query("Vertices =='PREFIXADOS (CIRCULAR 3.361)'").first_valid_index() # noqa
|
|
42
|
+
raw_df.query("index < @idx", inplace=True)
|
|
43
|
+
csv_buffer = StringIO()
|
|
44
|
+
|
|
45
|
+
# Save the DataFrame as CSV in memory buffer to be read again
|
|
46
|
+
# It's easier than handling values with commas and dots directly in the DataFrame
|
|
47
|
+
raw_df.to_csv(csv_buffer, index=False)
|
|
48
|
+
csv_buffer.seek(0) # Move the buffer cursor to the beginning
|
|
49
|
+
|
|
50
|
+
df = pd.read_csv(
|
|
51
|
+
csv_buffer,
|
|
52
|
+
decimal=",",
|
|
53
|
+
thousands=".",
|
|
54
|
+
dtype_backend="numpy_nullable",
|
|
55
|
+
)
|
|
56
|
+
file_date_str = text[0:10] # 09/09/2024
|
|
57
|
+
file_date = pd.to_datetime(file_date_str, format="%d/%m/%Y")
|
|
58
|
+
df["date"] = file_date
|
|
59
|
+
|
|
60
|
+
return df
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _process_df(df: pd.DataFrame) -> pd.DataFrame:
|
|
64
|
+
"""Processes the raw yield curve DataFrame to calculate rates and forward rates."""
|
|
65
|
+
# Rename columns
|
|
66
|
+
rename_dict = {
|
|
67
|
+
"Vertices": "vertex",
|
|
68
|
+
"ETTJ IPCA": "real_rate",
|
|
69
|
+
"ETTJ PREF": "nominal_rate",
|
|
70
|
+
"Inflação Implícita": "implied_inflation",
|
|
71
|
+
}
|
|
72
|
+
df.rename(columns=rename_dict, inplace=True)
|
|
73
|
+
|
|
74
|
+
# Divide float columns by 100 and round to 6 decimal places
|
|
75
|
+
df["real_rate"] = (df["real_rate"] / 100).round(ROUND_DIGITS)
|
|
76
|
+
df["nominal_rate"] = (df["nominal_rate"] / 100).round(ROUND_DIGITS)
|
|
77
|
+
df["implied_inflation"] = (df["implied_inflation"] / 100).round(ROUND_DIGITS)
|
|
78
|
+
|
|
79
|
+
df["real_rate_fwd"] = forward_rates(
|
|
80
|
+
business_days=df["vertex"],
|
|
81
|
+
zero_rates=df["real_rate"],
|
|
82
|
+
groupby_dates=df["date"],
|
|
83
|
+
)
|
|
84
|
+
df["nominal_rate_fwd"] = forward_rates(
|
|
85
|
+
business_days=df["vertex"],
|
|
86
|
+
zero_rates=df["nominal_rate"],
|
|
87
|
+
groupby_dates=df["date"],
|
|
88
|
+
)
|
|
89
|
+
df["implied_inflation_fwd"] = forward_rates(
|
|
90
|
+
business_days=df["vertex"],
|
|
91
|
+
zero_rates=df["implied_inflation"],
|
|
92
|
+
groupby_dates=df["date"],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
column_order = [
|
|
96
|
+
"date",
|
|
97
|
+
"vertex",
|
|
98
|
+
"nominal_rate",
|
|
99
|
+
"real_rate",
|
|
100
|
+
"implied_inflation",
|
|
101
|
+
"nominal_rate_fwd",
|
|
102
|
+
"real_rate_fwd",
|
|
103
|
+
"implied_inflation_fwd",
|
|
104
|
+
]
|
|
105
|
+
df = df[column_order].copy()
|
|
106
|
+
|
|
107
|
+
return df
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def last_ettj() -> pd.DataFrame:
|
|
111
|
+
"""
|
|
112
|
+
Retrieves and processes the latest Brazilian yield curve data from ANBIMA.
|
|
113
|
+
|
|
114
|
+
This function fetches the most recent yield curve data published by ANBIMA,
|
|
115
|
+
containing real rates (IPCA-indexed), nominal rates, and implied inflation
|
|
116
|
+
at various vertices (time points). Both spot rates and forward rates are
|
|
117
|
+
calculated and included in the returned DataFrame.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
pd.DataFrame: A DataFrame containing the following columns:
|
|
121
|
+
- date: Reference date of the yield curve
|
|
122
|
+
- vertex: Time point in business days
|
|
123
|
+
- nominal_rate: Zero-coupon nominal interest rate
|
|
124
|
+
- real_rate: Zero-coupon real interest rate (IPCA-indexed)
|
|
125
|
+
- implied_inflation: Implied inflation rate (break-even inflation)
|
|
126
|
+
- nominal_rate_fwd: Forward nominal interest rate
|
|
127
|
+
- real_rate_fwd: Forward real interest rate
|
|
128
|
+
- implied_inflation_fwd: Forward implied inflation rate
|
|
129
|
+
|
|
130
|
+
Note:
|
|
131
|
+
All rates are expressed in decimal format (e.g., 0.12 for 12%).
|
|
132
|
+
"""
|
|
133
|
+
text = _get_last_content()
|
|
134
|
+
df = _convert_text_to_df(text)
|
|
135
|
+
return _process_df(df)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from typing import Literal
|
|
2
3
|
from urllib.error import HTTPError
|
|
3
4
|
|
|
4
5
|
import pandas as pd
|
|
@@ -8,12 +9,19 @@ from pyield.data_cache import get_anbima_dataset
|
|
|
8
9
|
from pyield.date_converter import DateScalar, convert_input_dates
|
|
9
10
|
from pyield.retry import default_retry
|
|
10
11
|
|
|
12
|
+
BOND_TYPES = Literal["LTN", "NTN-B", "NTN-C", "NTN-F", "LFT"]
|
|
11
13
|
ANBIMA_URL = "https://www.anbima.com.br/informacoes/merc-sec/arqs"
|
|
12
14
|
# URL example: https://www.anbima.com.br/informacoes/merc-sec/arqs/ms240614.txt
|
|
13
15
|
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
def _bond_type_mapping(bond_type: str) -> str:
|
|
20
|
+
bond_type = bond_type.upper()
|
|
21
|
+
bond_type_mapping = {"NTNB": "NTN-B", "NTNC": "NTN-C", "NTNF": "NTN-F"}
|
|
22
|
+
return bond_type_mapping.get(bond_type, bond_type)
|
|
23
|
+
|
|
24
|
+
|
|
17
25
|
def _build_file_url(date: pd.Timestamp) -> str:
|
|
18
26
|
url_date = date.strftime("%y%m%d")
|
|
19
27
|
filename = f"ms{url_date}.txt"
|
|
@@ -59,8 +67,9 @@ def _process_raw_df(df_raw: pd.DataFrame) -> pd.DataFrame:
|
|
|
59
67
|
|
|
60
68
|
# Remove percentage from rates
|
|
61
69
|
rate_cols = [col for col in df.columns if "Rate" in col]
|
|
62
|
-
#
|
|
63
|
-
|
|
70
|
+
# Rate columns have percentage values with 4 decimal places in percentage values
|
|
71
|
+
# We will round to 6 decimal places to avoid floating point errors
|
|
72
|
+
df[rate_cols] = (df[rate_cols] / 100).round(6)
|
|
64
73
|
|
|
65
74
|
df["ReferenceDate"] = pd.to_datetime(df["ReferenceDate"], format="%Y%m%d")
|
|
66
75
|
df["MaturityDate"] = pd.to_datetime(df["MaturityDate"], format="%Y%m%d")
|
|
@@ -86,9 +95,10 @@ def tpf_data(date: DateScalar, bond_type: str | None = None) -> pd.DataFrame:
|
|
|
86
95
|
Returns an empty DataFrame if data is not available for the
|
|
87
96
|
specified date (weekends, holidays, or unavailable data).
|
|
88
97
|
"""
|
|
98
|
+
date = convert_input_dates(date)
|
|
99
|
+
date_log = date.strftime("%d/%m/%Y")
|
|
100
|
+
|
|
89
101
|
try:
|
|
90
|
-
date = convert_input_dates(date)
|
|
91
|
-
date_log = date.strftime("%d/%m/%Y")
|
|
92
102
|
file_url = _build_file_url(date)
|
|
93
103
|
df = _read_raw_df(file_url)
|
|
94
104
|
if df.empty:
|
|
@@ -99,8 +109,9 @@ def tpf_data(date: DateScalar, bond_type: str | None = None) -> pd.DataFrame:
|
|
|
99
109
|
return df
|
|
100
110
|
df = _process_raw_df(df)
|
|
101
111
|
if bond_type:
|
|
112
|
+
bond_type = _bond_type_mapping(bond_type)
|
|
102
113
|
df = df.query("BondType == @bond_type").reset_index(drop=True)
|
|
103
|
-
return df
|
|
114
|
+
return df.sort_values(["BondType", "MaturityDate"]).reset_index(drop=True)
|
|
104
115
|
except HTTPError as e:
|
|
105
116
|
if e.code == 404: # noqa
|
|
106
117
|
logger.info(
|
|
@@ -109,8 +120,8 @@ def tpf_data(date: DateScalar, bond_type: str | None = None) -> pd.DataFrame:
|
|
|
109
120
|
)
|
|
110
121
|
return pd.DataFrame()
|
|
111
122
|
raise # Propagate other HTTP errors
|
|
112
|
-
except Exception
|
|
113
|
-
logger.
|
|
123
|
+
except Exception:
|
|
124
|
+
logger.exception(f"Error fetching TPF data for {date_log}")
|
|
114
125
|
raise
|
|
115
126
|
|
|
116
127
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import ftplib
|
|
2
|
+
import logging
|
|
2
3
|
|
|
3
4
|
from pyield.date_converter import DateScalar, convert_input_dates
|
|
4
5
|
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
5
8
|
|
|
6
9
|
def di_over(date: DateScalar) -> float:
|
|
7
10
|
"""
|
|
@@ -46,14 +49,18 @@ def di_over(date: DateScalar) -> float:
|
|
|
46
49
|
rate = int(raw_rate) / 10000
|
|
47
50
|
return round(rate, 5)
|
|
48
51
|
else:
|
|
49
|
-
|
|
52
|
+
logger.error(f"No data found for date {date}")
|
|
53
|
+
return float("nan")
|
|
50
54
|
|
|
51
55
|
except ValueError as e:
|
|
52
|
-
|
|
56
|
+
logger.error(f"Date format error: {e}")
|
|
57
|
+
raise
|
|
53
58
|
except ftplib.error_perm as e:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
logger.error(f"File access error: {e}")
|
|
60
|
+
raise
|
|
61
|
+
except Exception:
|
|
62
|
+
logger.exception("Unexpected error")
|
|
63
|
+
raise
|
|
57
64
|
finally:
|
|
58
65
|
# Ensure FTP connection is closed
|
|
59
66
|
if ftp:
|
|
@@ -61,10 +68,3 @@ def di_over(date: DateScalar) -> float:
|
|
|
61
68
|
ftp.quit()
|
|
62
69
|
except: # noqa
|
|
63
70
|
pass
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# Example usage
|
|
67
|
-
if __name__ == "__main__":
|
|
68
|
-
date = "28/02/2025"
|
|
69
|
-
rate = di_over(date)
|
|
70
|
-
print(f"DI Rate for {date}: {rate}")
|
|
@@ -94,9 +94,9 @@ def premium(lft_rate: float, selic_over: float) -> float:
|
|
|
94
94
|
>>> lft_rate = 0.1695 / 100 # 0.1695%
|
|
95
95
|
>>> selic_over = 10.40 / 100 # 10.40%
|
|
96
96
|
>>> yd.lft.premium(lft_rate, selic_over)
|
|
97
|
-
1.
|
|
97
|
+
1.017120519283759
|
|
98
98
|
"""
|
|
99
99
|
adjusted_lft_rate = (lft_rate + 1) * (selic_over + 1) - 1
|
|
100
100
|
f1 = (adjusted_lft_rate + 1) ** (1 / 252) - 1
|
|
101
101
|
f2 = (selic_over + 1) ** (1 / 252) - 1
|
|
102
|
-
return
|
|
102
|
+
return f1 / f2
|
|
@@ -175,14 +175,14 @@ def premium(ltn_rate: float, di_rate: float) -> float:
|
|
|
175
175
|
LTN rate for 01-01-2030: 0.118746
|
|
176
176
|
DI (JAN30) Settlement rate: 0.11725
|
|
177
177
|
>>> yd.ltn.premium(0.118746, 0.11725)
|
|
178
|
-
1.
|
|
178
|
+
1.0120718007994287
|
|
179
179
|
"""
|
|
180
180
|
# Cálculo das taxas diárias
|
|
181
181
|
ltn_factor = (1 + ltn_rate) ** (1 / 252)
|
|
182
182
|
di_factor = (1 + di_rate) ** (1 / 252)
|
|
183
183
|
|
|
184
184
|
# Retorno do cálculo do prêmio
|
|
185
|
-
return
|
|
185
|
+
return (ltn_factor - 1) / (di_factor - 1)
|
|
186
186
|
|
|
187
187
|
|
|
188
188
|
def historical_premium(
|
|
@@ -202,7 +202,7 @@ def historical_premium(
|
|
|
202
202
|
|
|
203
203
|
Examples:
|
|
204
204
|
>>> yd.ltn.historical_premium("22-08-2024", "01-01-2030")
|
|
205
|
-
1.
|
|
205
|
+
1.0120718007994287
|
|
206
206
|
|
|
207
207
|
"""
|
|
208
208
|
# Convert input dates to a consistent format
|
|
@@ -532,7 +532,7 @@ def premium(
|
|
|
532
532
|
... di_expirations=exp_dates,
|
|
533
533
|
... di_rates=di_rates,
|
|
534
534
|
... )
|
|
535
|
-
1.
|
|
535
|
+
1.0099602136954626
|
|
536
536
|
|
|
537
537
|
Notes:
|
|
538
538
|
- The function adjusts coupon payment dates to business days and calculates
|
|
@@ -572,7 +572,7 @@ def premium(
|
|
|
572
572
|
factor_ntnf = (1 + ntnf_rate) ** (1 / 252)
|
|
573
573
|
factor_di = (1 + di_ytm) ** (1 / 252)
|
|
574
574
|
premium_np = (factor_ntnf - 1) / (factor_di - 1)
|
|
575
|
-
return
|
|
575
|
+
return float(premium_np)
|
|
576
576
|
|
|
577
577
|
|
|
578
578
|
def historical_premium(
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.30.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|