PYield 0.35.0__tar.gz → 0.35.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 (51) hide show
  1. {pyield-0.35.0 → pyield-0.35.2}/PKG-INFO +1 -1
  2. pyield-0.35.2/pyield/__about__.py +1 -0
  3. {pyield-0.35.0 → pyield-0.35.2}/pyield/b3/di1.py +93 -75
  4. {pyield-0.35.0 → pyield-0.35.2}/pyield/tpf/ltn.py +3 -1
  5. {pyield-0.35.0 → pyield-0.35.2}/pyield/tpf/ntnf.py +7 -6
  6. {pyield-0.35.0 → pyield-0.35.2}/pyield/tpf/tools.py +1 -4
  7. pyield-0.35.0/pyield/__about__.py +0 -1
  8. {pyield-0.35.0 → pyield-0.35.2}/.gitignore +0 -0
  9. {pyield-0.35.0 → pyield-0.35.2}/LICENSE +0 -0
  10. {pyield-0.35.0 → pyield-0.35.2}/README.md +0 -0
  11. {pyield-0.35.0 → pyield-0.35.2}/pyield/__init__.py +0 -0
  12. {pyield-0.35.0 → pyield-0.35.2}/pyield/anbima/__init__.py +0 -0
  13. {pyield-0.35.0 → pyield-0.35.2}/pyield/anbima/ettj.py +0 -0
  14. {pyield-0.35.0 → pyield-0.35.2}/pyield/anbima/ima.py +0 -0
  15. {pyield-0.35.0 → pyield-0.35.2}/pyield/anbima/imaq.py +0 -0
  16. {pyield-0.35.0 → pyield-0.35.2}/pyield/anbima/ipca.py +0 -0
  17. {pyield-0.35.0 → pyield-0.35.2}/pyield/anbima/tpf.py +0 -0
  18. {pyield-0.35.0 → pyield-0.35.2}/pyield/b3/__init__.py +0 -0
  19. {pyield-0.35.0 → pyield-0.35.2}/pyield/b3/di_over.py +0 -0
  20. {pyield-0.35.0 → pyield-0.35.2}/pyield/b3/futures/__init__.py +0 -0
  21. {pyield-0.35.0 → pyield-0.35.2}/pyield/b3/futures/common.py +0 -0
  22. {pyield-0.35.0 → pyield-0.35.2}/pyield/b3/futures/historical.py +0 -0
  23. {pyield-0.35.0 → pyield-0.35.2}/pyield/b3/futures/intraday.py +0 -0
  24. {pyield-0.35.0 → pyield-0.35.2}/pyield/b3/futures/xml.py +0 -0
  25. {pyield-0.35.0 → pyield-0.35.2}/pyield/bc/__init__.py +0 -0
  26. {pyield-0.35.0 → pyield-0.35.2}/pyield/bc/auction.py +0 -0
  27. {pyield-0.35.0 → pyield-0.35.2}/pyield/bc/bcdata.py +0 -0
  28. {pyield-0.35.0 → pyield-0.35.2}/pyield/bc/ptax.py +0 -0
  29. {pyield-0.35.0 → pyield-0.35.2}/pyield/bc/repo.py +0 -0
  30. {pyield-0.35.0 → pyield-0.35.2}/pyield/bc/trades_intraday.py +0 -0
  31. {pyield-0.35.0 → pyield-0.35.2}/pyield/bc/trades_monthly.py +0 -0
  32. {pyield-0.35.0 → pyield-0.35.2}/pyield/bc/vna.py +0 -0
  33. {pyield-0.35.0 → pyield-0.35.2}/pyield/bday.py +0 -0
  34. {pyield-0.35.0 → pyield-0.35.2}/pyield/conftest.py +0 -0
  35. {pyield-0.35.0 → pyield-0.35.2}/pyield/data_cache.py +0 -0
  36. {pyield-0.35.0 → pyield-0.35.2}/pyield/date_converter.py +0 -0
  37. {pyield-0.35.0 → pyield-0.35.2}/pyield/fwd.py +0 -0
  38. {pyield-0.35.0 → pyield-0.35.2}/pyield/holidays/__init__.py +0 -0
  39. {pyield-0.35.0 → pyield-0.35.2}/pyield/holidays/br_holidays_new.txt +0 -0
  40. {pyield-0.35.0 → pyield-0.35.2}/pyield/holidays/br_holidays_old.txt +0 -0
  41. {pyield-0.35.0 → pyield-0.35.2}/pyield/holidays/brholidays.py +0 -0
  42. {pyield-0.35.0 → pyield-0.35.2}/pyield/ibge/__init__.py +0 -0
  43. {pyield-0.35.0 → pyield-0.35.2}/pyield/ibge/ipca.py +0 -0
  44. {pyield-0.35.0 → pyield-0.35.2}/pyield/interpolator.py +0 -0
  45. {pyield-0.35.0 → pyield-0.35.2}/pyield/py.typed +0 -0
  46. {pyield-0.35.0 → pyield-0.35.2}/pyield/retry.py +0 -0
  47. {pyield-0.35.0 → pyield-0.35.2}/pyield/tpf/__init__.py +0 -0
  48. {pyield-0.35.0 → pyield-0.35.2}/pyield/tpf/lft.py +0 -0
  49. {pyield-0.35.0 → pyield-0.35.2}/pyield/tpf/ntnb.py +0 -0
  50. {pyield-0.35.0 → pyield-0.35.2}/pyield/tpf/ntnc.py +0 -0
  51. {pyield-0.35.0 → pyield-0.35.2}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PYield
3
- Version: 0.35.0
3
+ Version: 0.35.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.35.2"
@@ -15,7 +15,7 @@ TIMEZONE_BZ = ZoneInfo("America/Sao_Paulo")
15
15
 
16
16
 
17
17
  def data(
18
- date: DateScalar,
18
+ dates: DateScalar | DateArray,
19
19
  month_start: bool = False,
20
20
  pre_filter: bool = False,
21
21
  all_columns: bool = True,
@@ -27,9 +27,7 @@ def data(
27
27
  (to month start) and optional filtering based on LTN and NTN-F bond maturities.
28
28
 
29
29
  Args:
30
- date (DateScalar): The trade date for which to retrieve DI contract data.
31
- If the date is invalid, a holiday, in the future, or None, an empty
32
- DataFrame is returned with appropriate logging.
30
+ dates (DateScalar): The trade dates for which to retrieve DI contract data.
33
31
  month_start (bool, optional): If True, adjusts all expiration dates to the
34
32
  first day of their respective month (e.g., 2025-02-01 becomes
35
33
  2025-01-01). Defaults to False.
@@ -43,12 +41,12 @@ def data(
43
41
 
44
42
  Returns:
45
43
  pd.DataFrame: A DataFrame containing the DI futures contract data for the
46
- specified date, sorted by expiration date. Returns an empty DataFrame
47
- if no data is found, the date is invalid, a holiday, or in the future.
44
+ specified dates, sorted by trade dates and expiration dates.
45
+ Returns an empty DataFrame if no data is found
48
46
 
49
47
  Examples:
50
48
  >>> from pyield import di1
51
- >>> df = di1.data(date="16-10-2024", month_start=True)
49
+ >>> df = di1.data(dates="16-10-2024", month_start=True)
52
50
  >>> df.iloc[:5, :5] # Show the first five rows and columns
53
51
  TradeDate ExpirationDate TickerSymbol BDaysToExp OpenContracts
54
52
  0 2024-10-16 2024-11-01 DI1X24 12 1744269
@@ -57,31 +55,24 @@ def data(
57
55
  3 2024-10-16 2025-02-01 DI1G25 74 279491
58
56
  4 2024-10-16 2025-03-01 DI1H25 94 344056
59
57
  """
60
- if date:
61
- date = dc.convert_input_dates(date)
62
- bz_today = dt.now(TIMEZONE_BZ).date()
63
- if date.date() > bz_today:
64
- logger.warning(f"DI date ({date}) after current date {bz_today}")
65
- logger.warning("Returning empty DataFrame.")
66
- return pd.DataFrame()
67
- else:
68
- logger.info("No date specified. Returning empty DataFrame.")
69
- return pd.DataFrame()
70
- # Return an empty DataFrame if the trade date is a holiday
71
- if not bday.is_business_day(date):
72
- logger.warning("Specified date is not a business day.")
73
- logger.warning("Returning empty DataFrame.")
74
- return pd.DataFrame()
58
+ dates = dc.convert_input_dates(dates)
59
+ # Force dates to be a Series for consistency
60
+ dates = pd.Series(dates) if isinstance(dates, pd.Timestamp) else dates
61
+ # Remove duplicates and sort dates
62
+ dates = dates.drop_duplicates().sort_values().reset_index(drop=True)
75
63
 
76
- # Get historical data
77
- df = get_cached_dataset("DI").query("TradeDate == @date").reset_index(drop=True)
64
+ df = get_cached_dataset("DI").query("TradeDate in @dates").reset_index(drop=True)
78
65
 
79
- if df.empty:
80
- logger.info("No historical data found. Trying real-time data.")
81
- df = b3.futures(contract_code="DI1", date=date)
66
+ today = dt.now(TIMEZONE_BZ)
67
+ last_bday = bday.last_business_day()
68
+ is_trading_day = today.date() == last_bday.date()
69
+ if is_trading_day and last_bday in df["TradeDate"]:
70
+ logger.info("Trying to get DI Futures data for today.")
71
+ df_intraday = b3.futures(contract_code="DI1", date=last_bday)
72
+ df = pd.concat([df, df_intraday]).reset_index(drop=True)
82
73
 
83
74
  if df.empty:
84
- logger.warning("No DI Futures data found for the specified date.")
75
+ logger.warning("No DI Futures data found for the specified dates.")
85
76
  logger.warning("Returning empty DataFrame.")
86
77
  return pd.DataFrame()
87
78
 
@@ -92,24 +83,37 @@ def data(
92
83
  df_pre = (
93
84
  get_cached_dataset("TPF")
94
85
  .query("BondType in ['LTN', 'NTN-F']")
95
- .reset_index(drop=True)[["ReferenceDate", "MaturityDate"]]
86
+ .drop_duplicates(subset=["ReferenceDate", "MaturityDate"])
87
+ .sort_values(["ReferenceDate", "MaturityDate"])
88
+ .reset_index(drop=True)[["ReferenceDate", "MaturityDate", "BondType"]]
96
89
  )
90
+ # Assure that the MaturityDate is a business day
91
+ df_pre["MaturityDate"] = bday.offset(df_pre["MaturityDate"], 0)
97
92
 
98
- nearest_date = _find_nearest_date(date, df_pre["ReferenceDate"])
99
- if nearest_date is pd.NaT:
100
- logger.warning("No matching reference date found in TPF dataset.")
101
- return pd.DataFrame()
93
+ # Rename columns for merging
94
+ df_pre = df_pre.rename(
95
+ columns={"ReferenceDate": "TradeDate", "MaturityDate": "ExpirationDate"}
96
+ )
97
+ # merge_asof will be used to filter the most recent LTN and NTN-F
98
+ # maturities for the given trade date. Therefore, the intraday data
99
+ # will use LTN and NTN-F maturities from the previous day.
100
+ df = pd.merge_asof(
101
+ df, df_pre, on="TradeDate", by="ExpirationDate", direction="backward"
102
+ )
102
103
 
103
- # Filter the TPF dataset for the nearest reference date
104
- pre_maturities = (
105
- df_pre.query("ReferenceDate == @nearest_date")["MaturityDate"]
106
- .drop_duplicates()
104
+ # BondType was used as flag to filter the maturities
105
+ df = (
106
+ df.query("BondType.notna()")
107
+ .drop(columns=["BondType"])
107
108
  .reset_index(drop=True)
108
109
  )
109
110
 
110
- # Force the expirations to be a business day as DI contracts
111
- pre_maturities = bday.offset(pre_maturities, 0)
112
- df = df.query("ExpirationDate in @pre_maturities").reset_index(drop=True)
111
+ if df.empty:
112
+ logger.warning(
113
+ "No DI Futures data found for the specified dates after pre-filtering."
114
+ "Returning empty DataFrame."
115
+ )
116
+ return pd.DataFrame()
113
117
 
114
118
  if month_start:
115
119
  df["ExpirationDate"] = df["ExpirationDate"].dt.to_period("M").dt.to_timestamp()
@@ -139,33 +143,6 @@ def data(
139
143
  return df.sort_values(by=["TradeDate", "ExpirationDate"]).reset_index(drop=True)
140
144
 
141
145
 
142
- def _find_nearest_date(
143
- target_date: pd.Timestamp, date_series: pd.Series
144
- ) -> pd.Timestamp:
145
- """Finds the date in a Series closest to the target date.
146
-
147
- Args:
148
- target_date (pd.Timestamp): The reference date.
149
- date_series (pd.Series): A Series of dates to search within.
150
-
151
- Returns:
152
- pd.Timestamp: The date from `date_series` that is closest in time
153
- to `target_date`. Returns pd.NaT if the series is empty or
154
- target_date is None.
155
- """
156
- if date_series.empty or target_date is None:
157
- return pd.NaT
158
- # The result will be a Series of positive Timedeltas (durations)
159
- abs_differences = (date_series - target_date).abs()
160
-
161
- # Find the index of the minimum difference in the differences Series
162
- # idxmin() returns the index label where the minimum value occurs
163
- closest_index = abs_differences.idxmin()
164
-
165
- # Use the found index to get the corresponding date from the original Series
166
- return date_series.loc[closest_index] # or date_series[closest_index]
167
-
168
-
169
146
  def interpolate_rates(
170
147
  dates: DateScalar | DateArray,
171
148
  expirations: DateScalar | DateArray,
@@ -174,7 +151,7 @@ def interpolate_rates(
174
151
  """
175
152
  Interpolates DI rates for specified trade dates and expiration dates.
176
153
 
177
- Calculates interpolated DI rates using the flat-forward method for given
154
+ Calculates interpolated DI rates using the **flat-forward** method for given
178
155
  sets of trade dates and expiration dates. This function is well-suited
179
156
  for vectorized calculations across multiple date pairs.
180
157
 
@@ -199,6 +176,52 @@ def interpolate_rates(
199
176
  Raises:
200
177
  ValueError: If `dates` and `expirations` are both array-like but have
201
178
  different lengths.
179
+
180
+ Examples:
181
+ >>> from pyield import di1
182
+ >>> # Note: by default, pandas shows floats with 6 decimal places
183
+ >>> # Interpolate rates for multiple trade and expiration dates
184
+ >>> # There is a contract with expiration 01-01-2027 in 08-05-2025
185
+ >>> # The rate is not interpolated (settlement rate is used)
186
+ >>> # There is no contract with expiration 25-11-2027 in 09-05-2025
187
+ >>> # The rate is interpolated (flat-forward method)
188
+ >>> # There is no data for trade date 10-05-2025 (Saturday) -> NaN
189
+ >>> # Note: 0.13461282461562996 is shown as 0.134613
190
+ >>> di1.interpolate_rates(
191
+ ... dates=["08-05-2025", "09-05-2025", "10-05-2025"],
192
+ ... expirations=["01-01-2027", "25-11-2027", "01-01-2030"],
193
+ ... )
194
+ 0 0.13972
195
+ 1 0.134613
196
+ 2 <NA>
197
+ dtype: Float64
198
+
199
+ >>> # Interpolate rates for a single trade date and multiple expiration dates
200
+ >>> # There is no DI Contract in 09-05-2025 with expiration 01-01-2050
201
+ >>> # The longest available contract is used to extrapolate the rate
202
+ >>> # Note: extrapolation is allowed by default
203
+ >>> di1.interpolate_rates(
204
+ ... dates="25-04-2025",
205
+ ... expirations=["01-01-2027", "01-01-2050"],
206
+ ... )
207
+ 0 0.13901
208
+ 1 0.13881
209
+ dtype: Float64
210
+
211
+ >>> # With extrapolation set to False, the second rate will be NaN
212
+ >>> # Note: 0.13576348733268917 is shown as 0.135763
213
+ >>> di1.interpolate_rates(
214
+ ... dates="25-04-2025",
215
+ ... expirations=["01-11-2027", "01-01-2050"],
216
+ ... extrapolate=False,
217
+ ... )
218
+ 0 0.135763
219
+ 1 <NA>
220
+ dtype: Float64
221
+
222
+ Notes:
223
+ - All available settlement rates are used for the flat-forward interpolation.
224
+ - The function handles broadcasting of scalar and array-like inputs.
202
225
  """
203
226
  # Convert input dates to a consistent format
204
227
  dates = dc.convert_input_dates(dates)
@@ -230,11 +253,7 @@ def interpolate_rates(
230
253
  dfi["irate"] = dfi["irate"].astype("Float64")
231
254
 
232
255
  # Load DI rates dataset filtered by the provided reference dates
233
- dfr = (
234
- get_cached_dataset("DI")
235
- .query("TradeDate in @dfi['tdate'].unique()")
236
- .reset_index(drop=True)
237
- )
256
+ dfr = data(dates=dates)
238
257
 
239
258
  # Return an empty DataFrame if no rates are found
240
259
  if dfr.empty:
@@ -308,13 +327,12 @@ def interpolate_rate(
308
327
  >>> di1.interpolate_rate("25-04-2025", "01-01-2050", extrapolate=True)
309
328
  0.13881
310
329
  """
311
-
312
330
  expiration = dc.convert_input_dates(expiration)
313
331
  if not date:
314
332
  return float("NaN")
315
333
 
316
334
  # Get the DI contract DataFrame
317
- df = data(date=date)
335
+ df = data(dates=date)
318
336
 
319
337
  if df.empty:
320
338
  return float("NaN")
@@ -136,7 +136,9 @@ def di_spreads(date: DateScalar) -> pd.DataFrame:
136
136
 
137
137
  Examples:
138
138
  >>> from pyield import ltn
139
- >>> ltn.di_spreads("22-08-2024")
139
+ >>> spreads = ltn.di_spreads("22-08-2024")
140
+ >>> spreads["DISpread"] = spreads["DISpread"] * 10000 # Convert to bps
141
+ >>> spreads
140
142
  MaturityDate DISpread
141
143
  0 2024-10-01 -3.06
142
144
  1 2025-01-01 -9.95
@@ -344,7 +344,9 @@ def di_spreads(date: DateScalar) -> pd.DataFrame:
344
344
 
345
345
  Examples:
346
346
  >>> from pyield import ntnf
347
- >>> ntnf.di_spreads("23-08-2024")
347
+ >>> df_spreads = ntnf.di_spreads("23-08-2024")
348
+ >>> df_spreads["DISpread"] = df_spreads["DISpread"] * 10_000 # Convert to bps
349
+ >>> df_spreads
348
350
  MaturityDate DISpread
349
351
  0 2025-01-01 -5.38
350
352
  1 2027-01-01 4.39
@@ -444,19 +446,20 @@ def di_net_spread( # noqa
444
446
  None. A good initial guess is the DI gross spread for the bond.
445
447
 
446
448
  Returns:
447
- float: The net DI spread in basis points.
449
+ float: The net DI spread in decimal format (e.g., 0.0012 for 12 bps).
448
450
 
449
451
  Examples:
450
452
  # Obs: only some of the DI rates will be used in the example.
451
453
  >>> exp_dates = pd.to_datetime(["2025-01-01", "2030-01-01", "2035-01-01"])
452
454
  >>> di_rates = pd.Series([0.10823, 0.11594, 0.11531])
453
- >>> di_net_spread(
455
+ >>> spread = di_net_spread(
454
456
  ... settlement="23-08-2024",
455
457
  ... ntnf_maturity="01-01-2035",
456
458
  ... ntnf_rate=0.116586,
457
459
  ... di_expirations=exp_dates,
458
460
  ... di_rates=di_rates,
459
461
  ... )
462
+ >>> round(spread * 10_000, 2) # Convert to bps for display
460
463
  12.13
461
464
  """
462
465
  # Create an interpolator for the DI rates using the flat-forward method
@@ -489,9 +492,7 @@ def di_net_spread( # noqa
489
492
  return (bond_cash_flows / (1 + di_interp + p) ** byears).sum() - bond_price
490
493
 
491
494
  # Solve for the spread that zeroes the price difference using the bisection method
492
- p_solution = _solve_spread(price_difference, initial_guess)
493
- # Convert the solution to basis points (bps) and round to two decimal places
494
- return round((p_solution * 10_000), 2)
495
+ return _solve_spread(price_difference, initial_guess)
495
496
 
496
497
 
497
498
  def premium(
@@ -77,7 +77,7 @@ def pre_spreads(date: DateScalar) -> pd.DataFrame:
77
77
  """
78
78
  # Fetch DI rates for the reference date
79
79
  converted_date = dc.convert_input_dates(date)
80
- df_di = di1.data(date=converted_date, month_start=True)
80
+ df_di = di1.data(dates=converted_date, month_start=True)
81
81
  if "SettlementRate" not in df_di.columns or df_di.empty:
82
82
  raise ValueError("DI data is missing the 'SettlementRate' column or is empty.")
83
83
 
@@ -97,9 +97,6 @@ def pre_spreads(date: DateScalar) -> pd.DataFrame:
97
97
  # Calculate the DI spread as the difference between indicative and settlement rates
98
98
  df_spreads["DISpread"] = df_spreads["IndicativeRate"] - df_spreads["SettlementRate"]
99
99
 
100
- # Convert spread to basis points for clarity
101
- df_spreads["DISpread"] = (10_000 * df_spreads["DISpread"]).round(2)
102
-
103
100
  # Prepare and return the final sorted DataFrame
104
101
  df_spreads = df_spreads.sort_values(["BondType", "MaturityDate"], ignore_index=True)
105
102
  return df_spreads[["BondType", "MaturityDate", "DISpread"]].copy()
@@ -1 +0,0 @@
1
- __version__ = "0.35.0"
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