PYield 0.22.2__tar.gz → 0.23.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {pyield-0.22.2 → pyield-0.23.0}/PKG-INFO +1 -1
  2. pyield-0.23.0/pyield/__about__.py +1 -0
  3. {pyield-0.22.2 → pyield-0.23.0}/pyield/bonds/bond_tools.py +1 -1
  4. {pyield-0.22.2 → pyield-0.23.0}/pyield/bonds/ntnb.py +76 -3
  5. {pyield-0.22.2 → pyield-0.23.0}/pyield/di.py +36 -19
  6. {pyield-0.22.2 → pyield-0.23.0}/pyield/indicators.py +71 -37
  7. {pyield-0.22.2 → pyield-0.23.0}/pyproject.toml +1 -1
  8. pyield-0.22.2/pyield/__about__.py +0 -1
  9. {pyield-0.22.2 → pyield-0.23.0}/LICENSE +0 -0
  10. {pyield-0.22.2 → pyield-0.23.0}/README.md +0 -0
  11. {pyield-0.22.2 → pyield-0.23.0}/pyield/__init__.py +0 -0
  12. {pyield-0.22.2 → pyield-0.23.0}/pyield/anbima.py +0 -0
  13. {pyield-0.22.2 → pyield-0.23.0}/pyield/b3_futures/__init__.py +0 -0
  14. {pyield-0.22.2 → pyield-0.23.0}/pyield/b3_futures/common.py +0 -0
  15. {pyield-0.22.2 → pyield-0.23.0}/pyield/b3_futures/historical.py +0 -0
  16. {pyield-0.22.2 → pyield-0.23.0}/pyield/b3_futures/intraday.py +0 -0
  17. {pyield-0.22.2 → pyield-0.23.0}/pyield/b3_futures/xml.py +0 -0
  18. {pyield-0.22.2 → pyield-0.23.0}/pyield/bday.py +0 -0
  19. {pyield-0.22.2 → pyield-0.23.0}/pyield/bonds/__init__.py +0 -0
  20. {pyield-0.22.2 → pyield-0.23.0}/pyield/bonds/lft.py +0 -0
  21. {pyield-0.22.2 → pyield-0.23.0}/pyield/bonds/ltn.py +0 -0
  22. {pyield-0.22.2 → pyield-0.23.0}/pyield/bonds/ntnf.py +0 -0
  23. {pyield-0.22.2 → pyield-0.23.0}/pyield/conftest.py +0 -0
  24. {pyield-0.22.2 → pyield-0.23.0}/pyield/data_cache.py +0 -0
  25. {pyield-0.22.2 → pyield-0.23.0}/pyield/date_converter.py +0 -0
  26. {pyield-0.22.2 → pyield-0.23.0}/pyield/holidays/__init__.py +0 -0
  27. {pyield-0.22.2 → pyield-0.23.0}/pyield/holidays/br_holidays_new.txt +0 -0
  28. {pyield-0.22.2 → pyield-0.23.0}/pyield/holidays/br_holidays_old.txt +0 -0
  29. {pyield-0.22.2 → pyield-0.23.0}/pyield/holidays/brholidays.py +0 -0
  30. {pyield-0.22.2 → pyield-0.23.0}/pyield/interpolator.py +0 -0
  31. {pyield-0.22.2 → pyield-0.23.0}/pyield/projections.py +0 -0
  32. {pyield-0.22.2 → pyield-0.23.0}/pyield/py.typed +0 -0
  33. {pyield-0.22.2 → pyield-0.23.0}/pyield/tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PYield
3
- Version: 0.22.2
3
+ Version: 0.23.0
4
4
  Summary: A Python library for analysis of fixed income instruments in Brazil
5
5
  Keywords: fixed-income, brazil, finance, analysis, bonds
6
6
  Author-Email: Carlos Carvalho <cr.cj@outlook.com>
@@ -0,0 +1 @@
1
+ __version__ = "0.23.0"
@@ -79,7 +79,7 @@ def di_spreads(reference_date: str | pd.Timestamp) -> pd.DataFrame:
79
79
  # Fetch DI rates for the reference date
80
80
  reference_date = dc.convert_input_dates(reference_date)
81
81
  di = DIFutures(reference_date, adj_expirations=True)
82
- df_di = di.data()
82
+ df_di = di.data
83
83
  if "SettlementRate" not in df_di.columns:
84
84
  raise ValueError("DI rates data is missing the 'SettlementRate' column.")
85
85
 
@@ -60,6 +60,29 @@ def rates(reference_date: str | pd.Timestamp) -> pd.DataFrame:
60
60
 
61
61
  Returns:
62
62
  pd.DataFrame: DataFrame with columns "MaturityDate" and "IndicativeRate".
63
+
64
+ Returned columns:
65
+ - MaturityDate: The maturity date of the bond.
66
+ - IndicativeRate: The indicative rate for the bond.
67
+
68
+ Examples:
69
+ >>> yd.ntnb.rates("16-08-2024")
70
+ MaturityDate IndicativeRate
71
+ 0 2025-05-15 0.063893
72
+ 1 2026-08-15 0.066095
73
+ 2 2027-05-15 0.064164
74
+ 3 2028-08-15 0.063199
75
+ 4 2029-05-15 0.061753
76
+ 5 2030-08-15 0.059997
77
+ 6 2032-08-15 0.060025
78
+ 7 2033-05-15 0.059876
79
+ 8 2035-05-15 0.059557
80
+ 9 2040-08-15 0.058873
81
+ 10 2045-05-15 0.060013
82
+ 11 2050-08-15 0.060247
83
+ 12 2055-05-15 0.059926
84
+ 13 2060-08-15 0.060179
85
+
63
86
  """
64
87
  ntnb_rates = anbima.rates(reference_date, "NTN-B")
65
88
  if ntnb_rates.empty:
@@ -76,9 +99,31 @@ def maturities(reference_date: str | pd.Timestamp) -> pd.Series:
76
99
 
77
100
  Returns:
78
101
  pd.Series: Series containing the maturity dates for the NTN-B bonds.
102
+
103
+ Examples:
104
+
105
+ >>> yd.ntnb.maturities("16-08-2024")
106
+ 0 2025-05-15
107
+ 1 2026-08-15
108
+ 2 2027-05-15
109
+ 3 2028-08-15
110
+ 4 2029-05-15
111
+ 5 2030-08-15
112
+ 6 2032-08-15
113
+ 7 2033-05-15
114
+ 8 2035-05-15
115
+ 9 2040-08-15
116
+ 10 2045-05-15
117
+ 11 2050-08-15
118
+ 12 2055-05-15
119
+ 13 2060-08-15
120
+ dtype: datetime64[ns]
121
+
79
122
  """
80
123
  df_rates = rates(reference_date)
81
- return df_rates["MaturityDate"]
124
+ s_maturities = df_rates["MaturityDate"]
125
+ s_maturities.name = None
126
+ return s_maturities
82
127
 
83
128
 
84
129
  def _generate_coupon_dates_map(
@@ -236,6 +281,8 @@ def quotation(
236
281
  99.3651
237
282
  >>> yd.ntnb.quotation("31-05-2024", "15-08-2060", 0.061878)
238
283
  99.5341
284
+ >>> yd.ntnb.quotation("15-08-2024", "15-08-2032", 0.05929)
285
+ 100.6409
239
286
  """
240
287
  # Validate and normalize dates
241
288
  settlement = dc.convert_input_dates(settlement)
@@ -281,6 +328,8 @@ def price(
281
328
  Examples:
282
329
  >>> yd.ntnb.price(4299.160173, 99.3651)
283
330
  4271.864805
331
+ >>> yd.ntnb.price(4315.498383, 100.6409)
332
+ 4343.156412
284
333
  """
285
334
  return bt.truncate(vna * quotation / 100, 6)
286
335
 
@@ -298,7 +347,6 @@ def spot_rates(
298
347
  the spot rates that discount each bond's cash flows to its current
299
348
  price.
300
349
 
301
-
302
350
  Args:
303
351
  settlement (str | pd.Timestamp): The reference date for settlement.
304
352
  maturities (pd.Series): Series of maturity dates for the bonds.
@@ -307,6 +355,32 @@ def spot_rates(
307
355
  Returns:
308
356
  pd.DataFrame: DataFrame with columns "MaturityDate", "SpotRate".
309
357
 
358
+ Examples:
359
+ # Get the NTN-B rates for a specific reference date
360
+ >>> df_rates = yd.ntnb.rates("16-08-2024")
361
+
362
+ # Calculate the spot rates considering the settlement at the reference date
363
+ >>> yd.ntnb.spot_rates(
364
+ ... settlement="16-08-2024",
365
+ ... maturities=df_rates["MaturityDate"],
366
+ ... rates=df_rates["IndicativeRate"],
367
+ ... )
368
+ MaturityDate SpotRate
369
+ 0 2025-05-15 0.063894
370
+ 1 2026-08-15 0.066141
371
+ 2 2027-05-15 0.064087
372
+ 3 2028-08-15 0.063057
373
+ 4 2029-05-15 0.061458
374
+ 5 2030-08-15 0.059491
375
+ 6 2032-08-15 0.059652
376
+ 7 2033-05-15 0.059497
377
+ 8 2035-05-15 0.059151
378
+ 9 2040-08-15 0.058326
379
+ 10 2045-05-15 0.060371
380
+ 11 2050-08-15 0.060772
381
+ 12 2055-05-15 0.059909
382
+ 13 2060-08-15 0.060652
383
+
310
384
  Notes:
311
385
  The calculation of the spot rates for NTN-B bonds considers the following steps:
312
386
  - Map all all possible payment dates up to the longest maturity date.
@@ -315,7 +389,6 @@ def spot_rates(
315
389
  - Calculate the real spot rates for each maturity date.
316
390
  - Columns in the returned DataFrame:
317
391
  - MaturityDate: The maturity date of the bond.
318
- - YTM: The yield to maturity rate for the bond.
319
392
  - SpotRate: The real spot rate for the bond.
320
393
  """
321
394
  # Process and validate the input data
@@ -30,19 +30,19 @@ class DIFutures:
30
30
  Initialize the DIFutures instance with a specific trade date.
31
31
 
32
32
  Args:
33
- trade_date (str | pd.Timestamp): The trade date for which DI data is
34
- required.
35
- adj_expirations (bool): If True, adjusts the expiration dates to
36
- the start of the month.
37
- prefixed_filter (bool): If True, filters the DI
38
- contracts to match LTN and NTN-F bond maturities.
33
+ trade_date (str | pd.Timestamp): The date to retrieve the contract data.
34
+ adj_expirations (bool): If True, adjusts the expiration dates to the start
35
+ of the month.
36
+ prefixed_filter (bool): If True, filters the DI contracts to match LTN and
37
+ NTN-F bond maturities.
39
38
  """
40
39
  self._trade_date = dc.convert_input_dates(trade_date)
41
- self.adj_expirations = adj_expirations
42
- self.prefixed_filter = prefixed_filter
40
+ self._adj_expirations = adj_expirations
41
+ self._prefixed_filter = prefixed_filter
43
42
 
43
+ @property
44
44
  def data(self) -> pd.DataFrame:
45
- """Retrieve DI contract data for the initialized trade date."""
45
+ """Retrieve DI contract DataFrame for the initialized trade date."""
46
46
  df = (
47
47
  get_di_dataset()
48
48
  .query("TradeDate == @self.trade_date")
@@ -51,6 +51,7 @@ class DIFutures:
51
51
 
52
52
  if df.empty:
53
53
  df = futures(contract_code="DI1", trade_date=self._trade_date)
54
+
54
55
  if df.empty:
55
56
  return pd.DataFrame()
56
57
 
@@ -58,10 +59,18 @@ class DIFutures:
58
59
  if "DaysToExpiration" in df.columns:
59
60
  df.drop(columns=["DaysToExpiration"], inplace=True)
60
61
 
61
- if self.prefixed_filter:
62
+ if self._prefixed_filter:
63
+ df_anbima = get_anbima_dataset()
64
+
65
+ # Find the closest Anbima reference date to the trade date
66
+ anbima_dates = (
67
+ df_anbima["ReferenceDate"].drop_duplicates().reset_index(drop=True)
68
+ )
69
+ dates_diff = (anbima_dates - self._trade_date).abs()
70
+ closest_date = anbima_dates[dates_diff.idxmin()] # noqa
71
+
62
72
  df_pre = (
63
- get_anbima_dataset()
64
- .query("ReferenceDate == @self.trade_date")
73
+ df_anbima.query("ReferenceDate == @closest_date")
65
74
  .query("BondType in ['LTN', 'NTN-F']")
66
75
  .drop_duplicates(ignore_index=True)
67
76
  )
@@ -69,7 +78,7 @@ class DIFutures:
69
78
  adj_pre_maturities = bday.offset(pre_maturities, 0) # noqa
70
79
  df.query("ExpirationDate in @adj_pre_maturities", inplace=True)
71
80
 
72
- if self.adj_expirations:
81
+ if self._adj_expirations:
73
82
  df["ExpirationDate"] = (
74
83
  df["ExpirationDate"].dt.to_period("M").dt.to_timestamp()
75
84
  )
@@ -87,7 +96,7 @@ class DIFutures:
87
96
  Returns:
88
97
  pd.Series: A Series of unique expiration dates for DI contracts.
89
98
  """
90
- df = self.data().drop_duplicates(subset=["ExpirationDate"])
99
+ df = self.data.drop_duplicates(subset=["ExpirationDate"])
91
100
  return df["ExpirationDate"].sort_values(ignore_index=True)
92
101
 
93
102
  @property
@@ -107,7 +116,7 @@ class DIFutures:
107
116
  - ForwardRate: The forward rate calculated between successive expiration
108
117
  dates.
109
118
  """
110
- df = self.data()
119
+ df = self.data
111
120
  if df.empty:
112
121
  return pd.DataFrame()
113
122
 
@@ -125,21 +134,30 @@ class DIFutures:
125
134
  extrapolate: bool = False,
126
135
  ) -> float:
127
136
  """Retrieve the DI rate for a specified expiration date."""
128
- expiration = bday.offset(expiration, 0)
137
+ expiration = dc.convert_input_dates(expiration)
138
+
139
+ if self._adj_expirations:
140
+ # Force the expiration date to be the start of the month
141
+ expiration = expiration.to_period("M").to_timestamp()
142
+ else:
143
+ # Force the expiration date to be a business day
144
+ expiration = bday.offset(expiration, 0)
129
145
 
130
146
  if not interpolate and extrapolate:
131
147
  raise ValueError("Extrapolation is not allowed without interpolation.")
132
148
 
133
- df = self.data()
149
+ df = self.data
150
+
134
151
  if df.empty:
135
152
  return float("NaN")
136
153
 
137
154
  df_exp = df.query("ExpirationDate == @expiration")
155
+
138
156
  if df_exp.empty and not interpolate:
139
157
  return float("NaN")
140
158
 
141
159
  if expiration in df_exp["ExpirationDate"].values:
142
- return float(df_exp["SettlementRate"].iloc[0])
160
+ return float(df_exp["SettlementRate"].iat[0])
143
161
 
144
162
  if not interpolate:
145
163
  return float("NaN")
@@ -166,7 +184,6 @@ class DIFutures:
166
184
  @trade_date.setter
167
185
  def trade_date(self, value: str | pd.Timestamp):
168
186
  self._trade_date = dc.convert_input_dates(value)
169
- return self._trade_date
170
187
 
171
188
  @property
172
189
  def adj_expirations(self) -> bool:
@@ -1,8 +1,10 @@
1
- import time
1
+ import logging
2
2
  from typing import Literal
3
3
 
4
4
  import pandas as pd
5
5
  import requests
6
+ from requests.adapters import HTTPAdapter
7
+ from urllib3.util import Retry
6
8
 
7
9
  from pyield import date_converter as dc
8
10
 
@@ -105,60 +107,92 @@ def ipca_monthly_rate(reference_date: pd.Timestamp) -> float:
105
107
 
106
108
 
107
109
  def _selic_target(reference_date: pd.Timestamp) -> float:
110
+ """
111
+ Fetches the SELIC Target rate for a specific reference date.
112
+
113
+ The SELIC Target rate is the official rate set by the Central Bank of Brazil.
114
+
115
+ Args:
116
+ reference_date (pd.Timestamp): The date for which to fetch the SELIC Target.
117
+
118
+ Returns:
119
+ float: The SELIC Target rate as a float rounded to 4 decimal places or NaN if
120
+ the rate is not available.
121
+ """
108
122
  # https://api.bcb.gov.br/dados/serie/bcdata.sgs.432/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
109
123
  selic_date = reference_date.strftime("%d/%m/%Y")
110
124
  api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.432/dados?formato=json&dataInicial={selic_date}&dataFinal={selic_date}"
111
- response = requests.get(api_url, timeout=10)
112
- response.raise_for_status()
113
125
 
114
- if selic_date not in response.text:
126
+ s = requests.Session()
127
+ retries = Retry(
128
+ total=5, # Retry 5 times
129
+ backoff_factor=1, # Exponential backoff: 1, 2, 4, 8, 16 seconds
130
+ status_forcelist=[500, 502, 503, 504], # Retry on these HTTP status codes
131
+ )
132
+ s.mount("https://", HTTPAdapter(max_retries=retries))
133
+
134
+ try:
135
+ response = s.get(api_url, timeout=(5, 20))
136
+ response.raise_for_status() # Raise an error for bad HTTP response
137
+
138
+ if selic_date not in response.text:
139
+ return float("nan")
140
+
141
+ data = response.json()
142
+ if not data or "valor" not in data[0]:
143
+ return float("nan")
144
+
145
+ value = data[0]["valor"]
146
+ return round(float(value) / 100, 4)
147
+
148
+ except requests.exceptions.RequestException as e:
149
+ logging.error(f"Error fetching SELIC Target rate: {e}")
115
150
  return float("nan")
116
- data = response.json()
117
- return round(float(data[0]["valor"]) / 100, 4)
118
151
 
119
152
 
120
153
  def _selic_over(reference_date: pd.Timestamp) -> float:
121
154
  """
122
- The SELIC Over rate is the daily average interest rate effectively practiced between
123
- banks in the interbank market, using public securities as collateral. This rate may
124
- vary daily depending on the supply and demand for resources between financial
125
- institutions.
126
- """
127
- formatted_date = reference_date.strftime("%d/%m/%Y")
128
- # https://api.bcb.gov.br/dados/serie/bcdata.sgs.1178/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
129
- api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.1178/dados?formato=json&dataInicial={formatted_date}&dataFinal={formatted_date}"
155
+ Fetches the SELIC Over rate for a specific reference date.
130
156
 
131
- # Implementação interna de retries e timeout
132
- retries = 3
133
- timeout = 10
134
- backoff_factor = 2
157
+ The SELIC Over rate is the daily average interest rate effectively practiced
158
+ between banks in the interbank market, using public securities as collateral.
135
159
 
136
- for attempt in range(retries):
137
- try:
138
- response = requests.get(api_url, timeout=timeout)
139
- response.raise_for_status() # Check for HTTP errors
160
+ Args:
161
+ reference_date (pd.Timestamp): The date for which to fetch the SELIC Over rate.
140
162
 
141
- if formatted_date not in response.text:
142
- return float("nan")
163
+ Returns:
164
+ float: The SELIC Over rate as a float rounded to 4 decimal places or NaN if
165
+ the rate is not available.
166
+ """
143
167
 
144
- data = response.json()
168
+ formatted_date = reference_date.strftime("%d/%m/%Y")
169
+ # https://api.bcb.gov.br/dados/serie/bcdata.sgs.1178/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
170
+ api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.1178/dados?formato=json&dataInicial={formatted_date}&dataFinal={formatted_date}"
145
171
 
146
- if not data or "valor" not in data[0]:
147
- return float("nan")
148
- value = data[0]["valor"]
149
- return round(float(value) / 100, 4)
172
+ s = requests.Session()
173
+ retries = Retry(
174
+ total=5,
175
+ backoff_factor=1,
176
+ status_forcelist=[500, 502, 503, 504],
177
+ )
178
+ s.mount("https://", HTTPAdapter(max_retries=retries))
179
+ try:
180
+ response = s.get(api_url, timeout=(5, 20))
181
+ response.raise_for_status() # Raise an error for bad HTTP response
182
+
183
+ if formatted_date not in response.text:
184
+ return float("nan")
150
185
 
151
- except requests.exceptions.Timeout:
152
- print(f"Timeout on attempt {attempt + 1}. Retrying...")
153
- except requests.exceptions.ConnectionError as e:
154
- print(f"Connection error on attempt {attempt + 1}: {e}. Retrying...")
155
- except requests.exceptions.RequestException as e:
156
- print(f"An error occurred: {e}")
186
+ data = response.json()
187
+ if not data or "valor" not in data[0]:
157
188
  return float("nan")
158
189
 
159
- time.sleep(backoff_factor**attempt)
190
+ value = data[0]["valor"]
191
+ return round(float(value) / 100, 4)
160
192
 
161
- return float("nan")
193
+ except requests.exceptions.RequestException as e:
194
+ logging.error(f"Error fetching SELIC Over rate: {e}")
195
+ return float("nan")
162
196
 
163
197
 
164
198
  def _di(reference_date: pd.Timestamp) -> float:
@@ -32,7 +32,7 @@ dependencies = [
32
32
  "types-requests",
33
33
  ]
34
34
  dynamic = []
35
- version = "0.22.2"
35
+ version = "0.23.0"
36
36
 
37
37
  [project.license]
38
38
  file = "LICENSE"
@@ -1 +0,0 @@
1
- __version__ = "0.22.2"
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