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.
Files changed (49) hide show
  1. {pyield-0.30.1 → pyield-0.30.2}/PKG-INFO +1 -1
  2. pyield-0.30.2/pyield/__about__.py +1 -0
  3. {pyield-0.30.1 → pyield-0.30.2}/pyield/anbima/__init__.py +2 -0
  4. pyield-0.30.2/pyield/anbima/ettj.py +135 -0
  5. {pyield-0.30.1 → pyield-0.30.2}/pyield/anbima/tpf.py +18 -7
  6. {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/di_over.py +12 -12
  7. {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/lft.py +2 -2
  8. {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/ltn.py +3 -3
  9. {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/ntnf.py +2 -2
  10. pyield-0.30.1/pyield/__about__.py +0 -1
  11. {pyield-0.30.1 → pyield-0.30.2}/.gitignore +0 -0
  12. {pyield-0.30.1 → pyield-0.30.2}/LICENSE +0 -0
  13. {pyield-0.30.1 → pyield-0.30.2}/README.md +0 -0
  14. {pyield-0.30.1 → pyield-0.30.2}/pyield/__init__.py +0 -0
  15. {pyield-0.30.1 → pyield-0.30.2}/pyield/anbima/ima.py +0 -0
  16. {pyield-0.30.1 → pyield-0.30.2}/pyield/anbima/imaq.py +0 -0
  17. {pyield-0.30.1 → pyield-0.30.2}/pyield/anbima/ipca.py +0 -0
  18. {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/__init__.py +0 -0
  19. {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/di.py +0 -0
  20. {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/futures/__init__.py +0 -0
  21. {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/futures/common.py +0 -0
  22. {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/futures/historical.py +0 -0
  23. {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/futures/intraday.py +0 -0
  24. {pyield-0.30.1 → pyield-0.30.2}/pyield/b3/futures/xml.py +0 -0
  25. {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/__init__.py +0 -0
  26. {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/auction.py +0 -0
  27. {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/bcdata.py +0 -0
  28. {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/ptax.py +0 -0
  29. {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/repo.py +0 -0
  30. {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/sec.py +0 -0
  31. {pyield-0.30.1 → pyield-0.30.2}/pyield/bc/vna.py +0 -0
  32. {pyield-0.30.1 → pyield-0.30.2}/pyield/bday.py +0 -0
  33. {pyield-0.30.1 → pyield-0.30.2}/pyield/conftest.py +0 -0
  34. {pyield-0.30.1 → pyield-0.30.2}/pyield/data_cache.py +0 -0
  35. {pyield-0.30.1 → pyield-0.30.2}/pyield/date_converter.py +0 -0
  36. {pyield-0.30.1 → pyield-0.30.2}/pyield/forwards.py +0 -0
  37. {pyield-0.30.1 → pyield-0.30.2}/pyield/holidays/__init__.py +0 -0
  38. {pyield-0.30.1 → pyield-0.30.2}/pyield/holidays/br_holidays_new.txt +0 -0
  39. {pyield-0.30.1 → pyield-0.30.2}/pyield/holidays/br_holidays_old.txt +0 -0
  40. {pyield-0.30.1 → pyield-0.30.2}/pyield/holidays/brholidays.py +0 -0
  41. {pyield-0.30.1 → pyield-0.30.2}/pyield/ibge/__init__.py +0 -0
  42. {pyield-0.30.1 → pyield-0.30.2}/pyield/ibge/ipca.py +0 -0
  43. {pyield-0.30.1 → pyield-0.30.2}/pyield/interpolator.py +0 -0
  44. {pyield-0.30.1 → pyield-0.30.2}/pyield/py.typed +0 -0
  45. {pyield-0.30.1 → pyield-0.30.2}/pyield/retry.py +0 -0
  46. {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/__init__.py +0 -0
  47. {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/ntnb.py +0 -0
  48. {pyield-0.30.1 → pyield-0.30.2}/pyield/tpf/tools.py +0 -0
  49. {pyield-0.30.1 → pyield-0.30.2}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PYield
3
- Version: 0.30.1
3
+ Version: 0.30.2
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>
@@ -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
- # Data has 6 decimal places, so we round to 7 decimal places
63
- df[rate_cols] = (df[rate_cols] / 100).round(7)
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 as e:
113
- logger.error(f"Error fetching TPF data for {date_log}: {e}")
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
- return "Empty file or no data"
52
+ logger.error(f"No data found for date {date}")
53
+ return float("nan")
50
54
 
51
55
  except ValueError as e:
52
- return f"Date format error: {e}"
56
+ logger.error(f"Date format error: {e}")
57
+ raise
53
58
  except ftplib.error_perm as e:
54
- return f"File access error: {e}"
55
- except Exception as e:
56
- return f"Unexpected error: {e}"
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.017121
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 round(f1 / f2, 6)
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.012072
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 round((ltn_factor - 1) / (di_factor - 1), 6)
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.012072
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.00996
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 round(float(premium_np), 6)
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