PYield 0.9.7__tar.gz → 0.9.8__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 (38) hide show
  1. {pyield-0.9.7 → pyield-0.9.8}/PKG-INFO +5 -5
  2. {pyield-0.9.7 → pyield-0.9.8}/README.md +4 -4
  3. pyield-0.9.8/pyield/__about__.py +1 -0
  4. {pyield-0.9.7 → pyield-0.9.8}/pyield/__init__.py +7 -5
  5. pyield-0.9.8/pyield/bonds/ltn.py +82 -0
  6. pyield-0.9.8/pyield/bonds/ntnb.py +431 -0
  7. pyield-0.9.8/pyield/bonds/ntnf.py +279 -0
  8. pyield-0.9.8/pyield/bonds/utils.py +36 -0
  9. pyield-0.9.8/pyield/date_validator.py +66 -0
  10. {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/anbima.py +2 -1
  11. {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/futures/__init__.py +1 -1
  12. {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/indicators.py +4 -4
  13. {pyield-0.9.7 → pyield-0.9.8}/pyield/interpolator.py +46 -49
  14. {pyield-0.9.7 → pyield-0.9.8}/pyield/spreads.py +1 -1
  15. {pyield-0.9.7 → pyield-0.9.8}/pyproject.toml +2 -2
  16. pyield-0.9.8/tests/__init__.py +0 -0
  17. pyield-0.9.7/pyield/__about__.py +0 -1
  18. pyield-0.9.7/pyield/date_validator.py +0 -62
  19. pyield-0.9.7/pyield/ntnb.py +0 -400
  20. {pyield-0.9.7 → pyield-0.9.8}/LICENSE +0 -0
  21. {pyield-0.9.7 → pyield-0.9.8}/pyield/bday.py +0 -0
  22. {pyield-0.9.7/pyield/fetchers → pyield-0.9.8/pyield/bonds}/__init__.py +0 -0
  23. {pyield-0.9.7/tests → pyield-0.9.8/pyield/fetchers}/__init__.py +0 -0
  24. {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/futures/common.py +0 -0
  25. {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/futures/historical.py +0 -0
  26. {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/futures/intraday.py +0 -0
  27. {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/futures/xml.py +0 -0
  28. {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/projections.py +0 -0
  29. {pyield-0.9.7 → pyield-0.9.8}/pyield/holidays/__init__.py +0 -0
  30. {pyield-0.9.7 → pyield-0.9.8}/pyield/holidays/br_holidays_new.txt +0 -0
  31. {pyield-0.9.7 → pyield-0.9.8}/pyield/holidays/br_holidays_old.txt +0 -0
  32. {pyield-0.9.7 → pyield-0.9.8}/pyield/holidays/holidays_manager.py +0 -0
  33. {pyield-0.9.7 → pyield-0.9.8}/pyield/py.typed +0 -0
  34. {pyield-0.9.7 → pyield-0.9.8}/tests/data/SPRD231222.zip +0 -0
  35. {pyield-0.9.7 → pyield-0.9.8}/tests/data/SPRD240426.zip +0 -0
  36. {pyield-0.9.7 → pyield-0.9.8}/tests/test_bday.py +0 -0
  37. {pyield-0.9.7 → pyield-0.9.8}/tests/test_futures.py +0 -0
  38. {pyield-0.9.7 → pyield-0.9.8}/tests/test_futures_xml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PYield
3
- Version: 0.9.7
3
+ Version: 0.9.8
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>
@@ -206,14 +206,14 @@ IndicatorProjection(
206
206
  # Initialize the Interpolator with known business days and interest rates.
207
207
  >>> known_bdays = [30, 60, 90]
208
208
  >>> known_rates = [0.045, 0.05, 0.055]
209
- >>> linear_itp = yd.Interpolator("linear", known_bdays, known_rates)
209
+ >>> linear_interpolator = yd.Interpolator("linear", known_bdays, known_rates)
210
210
 
211
211
  # Interpolate the interest rate for a given number of business days.
212
- >>> linear_itp.interpolate(45)
212
+ >>> linear_interpolator(45)
213
213
  0.0475
214
214
 
215
215
  # Use the flat forward method for interpolation.
216
- >>> ff_itp = yd.Interpolator("flat_forward", known_bdays, known_rates)
217
- >>> ff_itp.interpolate(45)
216
+ >>> ff_interpolator = yd.Interpolator("flat_forward", known_bdays, known_rates)
217
+ >>> ff_interpolator(45)
218
218
  0.04833068080970859
219
219
  ```
@@ -163,14 +163,14 @@ IndicatorProjection(
163
163
  # Initialize the Interpolator with known business days and interest rates.
164
164
  >>> known_bdays = [30, 60, 90]
165
165
  >>> known_rates = [0.045, 0.05, 0.055]
166
- >>> linear_itp = yd.Interpolator("linear", known_bdays, known_rates)
166
+ >>> linear_interpolator = yd.Interpolator("linear", known_bdays, known_rates)
167
167
 
168
168
  # Interpolate the interest rate for a given number of business days.
169
- >>> linear_itp.interpolate(45)
169
+ >>> linear_interpolator(45)
170
170
  0.0475
171
171
 
172
172
  # Use the flat forward method for interpolation.
173
- >>> ff_itp = yd.Interpolator("flat_forward", known_bdays, known_rates)
174
- >>> ff_itp.interpolate(45)
173
+ >>> ff_interpolator = yd.Interpolator("flat_forward", known_bdays, known_rates)
174
+ >>> ff_interpolator(45)
175
175
  0.04833068080970859
176
176
  ```
@@ -0,0 +1 @@
1
+ __version__ = "0.9.8"
@@ -1,5 +1,6 @@
1
- from . import bday, interpolator, ntnb
1
+ from . import bday, interpolator
2
2
  from .__about__ import __version__
3
+ from .bonds import ltn, ntnb, ntnf
3
4
  from .fetchers.anbima import anbima
4
5
  from .fetchers.futures import futures
5
6
  from .fetchers.indicators import indicator
@@ -10,11 +11,12 @@ __all__ = [
10
11
  "__version__",
11
12
  "anbima",
12
13
  "bday",
13
- "ntnb",
14
- "indicator",
15
- "projection",
16
- "spread",
17
14
  "futures",
15
+ "indicator",
18
16
  "interpolator",
17
+ "ntnb",
18
+ "ntnf",
19
+ "ltn",
20
+ "projection",
19
21
  "spread",
20
22
  ]
@@ -0,0 +1,82 @@
1
+ import pandas as pd
2
+
3
+ from .. import bday
4
+ from .. import date_validator as dv
5
+ from ..fetchers.anbima import anbima
6
+ from .utils import truncate
7
+
8
+ FACE_VALUE = 1000
9
+
10
+
11
+ def anbima_data(reference_date: str | pd.Timestamp) -> pd.DataFrame:
12
+ """
13
+ Fetch LTN Anbima data for the given reference date.
14
+
15
+ Args:
16
+ reference_date (str | pd.Timestamp): The reference date for fetching the data.
17
+
18
+ Returns:
19
+ pd.DataFrame: A DataFrame containing the Anbima data for the reference date.
20
+ """
21
+ return anbima(bond_type="LTN", reference_date=reference_date)
22
+
23
+
24
+ def anbima_rates(reference_date: str | pd.Timestamp) -> pd.Series:
25
+ """
26
+ Fetch LTN Anbima indicative rates for the given reference date.
27
+
28
+ Args:
29
+ reference_date (str | pd.Timestamp): The reference date for fetching the data.
30
+
31
+ Returns:
32
+ pd.Series: A Series containing the rates indexed by maturity date.
33
+ """
34
+ df = anbima_data(reference_date)
35
+ # Set MaturityDate as index
36
+ df = df.set_index("MaturityDate")
37
+ df.index.name = None
38
+ # Return as Series
39
+ return df["IndicativeRate"]
40
+
41
+
42
+ def price(
43
+ settlement_date: str | pd.Timestamp,
44
+ maturity_date: str | pd.Timestamp,
45
+ discount_rate: float,
46
+ ) -> float:
47
+ """
48
+ Calculate the LTN price using Anbima rules.
49
+
50
+ Args:
51
+ settlement_date (str | pd.Timestamp): The settlement date in 'DD-MM-YYYY' format
52
+ or a pandas Timestamp.
53
+ maturity_date (str | pd.Timestamp): The maturity date in 'DD-MM-YYYY' format or
54
+ a pandas Timestamp.
55
+ discount_rate (float): The discount rate used to calculate the present value of
56
+ the cash flows, which is the yield to maturity (YTM) of the NTN-F.
57
+
58
+ Returns:
59
+ float: The LTN price using Anbima rules.
60
+
61
+ References:
62
+ - https://www.anbima.com.br/data/files/A0/02/CC/70/8FEFC8104606BDC8B82BA2A8/Metodologias%20ANBIMA%20de%20Precificacao%20Titulos%20Publicos.pdf
63
+
64
+ Examples:
65
+ >>> price("05-07-2024", "01-01-2030", 0.12145)
66
+ 535.279902
67
+ """
68
+
69
+ # Validate and normalize dates
70
+ settlement_date = dv.standardize_date(settlement_date)
71
+ maturity_date = dv.standardize_date(maturity_date)
72
+
73
+ # Calculate the number of business days between settlement and cash flow dates
74
+ bdays = bday.count(settlement_date, maturity_date)
75
+
76
+ # Calculate the number of periods truncated as per Anbima rule
77
+ num_periods = truncate(bdays / 252, 14)
78
+
79
+ discount_factor = (1 + discount_rate) ** num_periods
80
+
81
+ # Truncate the price to 6 decimal places as per Anbima rules
82
+ return truncate(FACE_VALUE / discount_factor, 6)
@@ -0,0 +1,431 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ from .. import bday
5
+ from .. import date_validator as dv
6
+ from ..fetchers.anbima import anbima
7
+ from ..fetchers.futures import futures
8
+ from ..interpolator import Interpolator
9
+ from ..spreads import spread
10
+ from . import utils as bu # bonds utils
11
+
12
+ """
13
+ Constants calculated as per Anbima Rules and in base 100
14
+ COUPON_RATE = (0.06 + 1) ** 0.5 - 1 # 6% annual rate compounded semi-annually
15
+ COUPON_PMT = round(100 * COUPON_RATE, 6) -> 2.956301
16
+ FINAL_PMT = principal + last coupon payment = 100 + 2.956301
17
+ """
18
+ COUPON_DAY = 15
19
+ COUPON_MONTHS = {2, 5, 8, 11}
20
+ COUPON_PMT = 2.956301
21
+ FINAL_PMT = 102.956301
22
+
23
+
24
+ def anbima_data(reference_date: str | pd.Timestamp) -> pd.DataFrame:
25
+ """
26
+ Fetch NTN-B Anbima data for the given reference date.
27
+
28
+ Args:
29
+ reference_date (str | pd.Timestamp): The reference date for fetching the data.
30
+
31
+ Returns:
32
+ pd.DataFrame: A DataFrame containing the Anbima data for the reference date.
33
+ """
34
+ return anbima(bond_type="NTN-B", reference_date=reference_date)
35
+
36
+
37
+ def anbima_rates(reference_date: str | pd.Timestamp) -> pd.Series:
38
+ """
39
+ Fetch NTN-B Anbima indicative rates for the given reference date.
40
+
41
+ Args:
42
+ reference_date (str | pd.Timestamp): The reference date for fetching the data.
43
+
44
+ Returns:
45
+ pd.Series: A Series containing the rates indexed by maturity date.
46
+ """
47
+ df = anbima_data(reference_date)
48
+ # Set MaturityDate as index
49
+ df = df.set_index("MaturityDate")
50
+ df.index.name = None
51
+ # Return as Series
52
+ return df["IndicativeRate"]
53
+
54
+
55
+ def coupon_dates_map(
56
+ start: str | pd.Timestamp,
57
+ end: str | pd.Timestamp,
58
+ ) -> pd.Series:
59
+ """
60
+ Generate a map of all possible coupon dates between the start and end dates.
61
+ The dates are inclusive. Coupon payments are made on the 15th of February, May,
62
+ August, and November (15-02, 15-05, 15-08, and 15-11 of each year).
63
+
64
+ Args:
65
+ start (str | pd.Timestamp): The start date.
66
+ end (str | pd.Timestamp): The end date.
67
+
68
+ Returns:
69
+ pd.Series: Series of coupon dates within the specified range.
70
+ """
71
+ # Validate and normalize dates
72
+ start = dv.standardize_date(start)
73
+ end = dv.standardize_date(end)
74
+
75
+ # Initialize the first coupon date based on the reference date
76
+ reference_year = start.year
77
+ first_coupon_date = pd.Timestamp(f"{reference_year}-02-01")
78
+
79
+ # Generate coupon dates
80
+ dates = pd.date_range(start=first_coupon_date, end=end, freq="3MS")
81
+
82
+ # Offset dates by 14 in order to have day 15 of the month
83
+ dates += pd.Timedelta(days=14)
84
+
85
+ # First coupon date must be after the reference date
86
+ dates = dates[dates >= start]
87
+ return pd.Series(dates).reset_index(drop=True)
88
+
89
+
90
+ def coupon_dates(
91
+ start_date: str | pd.Timestamp,
92
+ maturity_date: str | pd.Timestamp,
93
+ ) -> pd.Series:
94
+ """
95
+ Generate all remaining coupon dates between a given date and the maturity date.
96
+ The dates are inclusive. Coupon payments are made on the 15th of February, May,
97
+ August, and November (15-02, 15-05, 15-08, and 15-11 of each year). The NTN-B
98
+ bond is determined by its maturity date.
99
+
100
+ Args:
101
+ start_date (str | pd.Timestamp): The date to start generating coupon dates.
102
+ maturity_date (str | pd.Timestamp): The maturity date.
103
+
104
+ Returns:
105
+ list[pd.Timestamp]: List of coupon dates between start and maturity dates.
106
+ """
107
+ # Validate and normalize dates
108
+ start_date = dv.standardize_date(start_date)
109
+ maturity_date = dv.standardize_date(maturity_date)
110
+
111
+ # Check if maturity date is after the start date
112
+ if maturity_date < start_date:
113
+ raise ValueError("Maturity date must be after the start date.")
114
+
115
+ # Check if the maturity date is a valid NTN-B maturity date
116
+ if maturity_date.day != COUPON_DAY or maturity_date.month not in COUPON_MONTHS:
117
+ raise ValueError("NTN-B maturity must be 15/02, 15/05, 15/08, or 15/11.")
118
+
119
+ # Initialize loop variables
120
+ cp_date = maturity_date
121
+ cp_dates = []
122
+
123
+ # Iterate backwards from the maturity date to the settlement date
124
+ while cp_date >= start_date:
125
+ cp_dates.append(cp_date)
126
+ # Move the coupon date back 6 months
127
+ cp_date -= pd.DateOffset(months=6)
128
+
129
+ return pd.Series(cp_dates).sort_values(ignore_index=True)
130
+
131
+
132
+ def quotation(
133
+ settlement_date: str | pd.Timestamp,
134
+ maturity_date: str | pd.Timestamp,
135
+ discount_rate: float,
136
+ ) -> float:
137
+ """
138
+ Calculate the NTN-B quotation in base 100 using Anbima rules.
139
+
140
+ Args:
141
+ settlement_date (str | pd.Timestamp): The settlement date in 'DD-MM-YYYY' format
142
+ or a pandas Timestamp.
143
+ maturity_date (str | pd.Timestamp): The maturity date in 'DD-MM-YYYY' format or
144
+ a pandas Timestamp.
145
+ discount_rate (float): The discount rate used to calculate the present value of
146
+ the cash flows, which is the yield to maturity (YTM) of the NTN-B.
147
+
148
+ Returns:
149
+ float: The NTN-B quotation truncated to 4 decimal places.
150
+
151
+ References:
152
+ - https://www.anbima.com.br/data/files/A0/02/CC/70/8FEFC8104606BDC8B82BA2A8/Metodologias%20ANBIMA%20de%20Precificacao%20Titulos%20Publicos.pdf
153
+ - The semi-annual coupon is set to 2.956301, which represents a 6% annual
154
+ coupon rate compounded semi-annually and rounded to 6 decimal places as per
155
+ Anbima rules.
156
+
157
+ Examples:
158
+ >>> quotation("31-05-2024", "15-05-2035", 0.061490)
159
+ 99.3651
160
+ >>> quotation("31-05-2024", "15-08-2060", 0.061878)
161
+ 99.5341
162
+ """
163
+ # Validate and normalize dates
164
+ settlement_date = dv.standardize_date(settlement_date)
165
+ maturity_date = dv.standardize_date(maturity_date)
166
+
167
+ # Get the coupon dates between the settlement and maturity dates
168
+ payment_dates = coupon_dates(settlement_date, maturity_date)
169
+
170
+ # Calculate the number of business days between settlement and cash flow dates
171
+ bdays = bday.count(settlement_date, payment_dates)
172
+
173
+ # Set the cash flow at maturity to FINAL_PMT and the others to COUPON_PMT
174
+ cash_flows = np.where(payment_dates == maturity_date, FINAL_PMT, COUPON_PMT)
175
+
176
+ # Calculate the number of periods truncated as per Anbima rules
177
+ num_periods = bu.truncate(bdays / 252, 14)
178
+
179
+ discount_factor = (1 + discount_rate) ** num_periods
180
+
181
+ # Calculate the present value of each cash flow (DCF) rounded as per Anbima rules
182
+ discounted_cash_flows = (cash_flows / discount_factor).round(10)
183
+
184
+ # Return the quotation (the dcf sum) truncated as per Anbima rules
185
+ return bu.truncate(discounted_cash_flows.sum(), 4)
186
+
187
+
188
+ def _calculate_coupons_pv(
189
+ bootstrap_df: pd.DataFrame,
190
+ settlement_date: pd.Timestamp,
191
+ maturity_date: pd.Timestamp,
192
+ ) -> float:
193
+ # Create a subset DataFrame with only the coupon payments (without last payment)
194
+ cp_dates_wo_last = coupon_dates(settlement_date, maturity_date)[:-1] # noqa
195
+ df_coupons = bootstrap_df.query("MaturityDate in @cp_dates_wo_last").copy()
196
+ df_coupons["Coupon"] = COUPON_PMT
197
+
198
+ # Calculate the present value of the coupon payments
199
+ pv = bu.calculate_present_value(
200
+ cash_flows=df_coupons["Coupon"],
201
+ discount_rates=df_coupons["SpotRate"],
202
+ time_periods=df_coupons["BDays"] / 252,
203
+ )
204
+ return pv
205
+
206
+
207
+ def spot_rates(
208
+ settlement_date: str | pd.Timestamp,
209
+ ytm_rates: pd.Series,
210
+ ) -> pd.DataFrame:
211
+ """
212
+ Calculate the spot rates for NTN-B bonds using the bootstrap method.
213
+
214
+ The bootstrap method is a process used to determine spot rates from
215
+ the yields of a series of bonds. It involves iteratively solving for
216
+ the spot rates that discount each bond's cash flows to its current
217
+ price.
218
+
219
+
220
+ Args:
221
+ settlement_date (str | pd.Timestamp): The reference date for settlement.
222
+ maturity_dates (pd.Series): Series of maturity dates for the bonds. ytm_rates
223
+ (pd.Series): Series of Yield to Maturity rates corresponding to the
224
+ maturity dates.
225
+
226
+ Returns:
227
+ pd.DataFrame: DataFrame containing the maturity dates and corresponding real
228
+ spot rates.
229
+
230
+ Notes:
231
+ The calculation of the spot rates for NTN-B bonds considers the following steps:
232
+ - Map all all possible payment dates up to the longest maturity date.
233
+ - Interpolate the YTM rates in the intermediate payment dates.
234
+ - Calculate the NTN-B quotation for each maturity date.
235
+ - Calculate the real spot rates for each maturity date.
236
+ """
237
+ # Process and validate the input data
238
+ settlement_date = dv.standardize_date(settlement_date)
239
+ ytm_rates = bu.standardize_rates(ytm_rates)
240
+
241
+ # Create the interpolator object
242
+ ytm_rate_interpolator = Interpolator(
243
+ method="flat_forward",
244
+ known_bdays=bday.count(settlement_date, ytm_rates.index),
245
+ known_rates=ytm_rates,
246
+ )
247
+
248
+ last_ntnb = ytm_rates.index.max()
249
+
250
+ # Generate coupon dates up to the longest maturity date
251
+ all_coupon_dates = coupon_dates_map(start=settlement_date, end=last_ntnb)
252
+
253
+ # Create a DataFrame with all coupon dates and the corresponding YTM
254
+ df_spot = pd.DataFrame(data=all_coupon_dates, columns=["MaturityDate"])
255
+ df_spot["BDays"] = bday.count(settlement_date, df_spot["MaturityDate"])
256
+ df_spot["YTM"] = df_spot["BDays"].apply(ytm_rate_interpolator)
257
+
258
+ # The Bootstrap loop to calculate spot rates
259
+ for index in df_spot.index:
260
+ # Get the row values using the index
261
+ maturity = df_spot.at[index, "MaturityDate"]
262
+ bdays = df_spot.at[index, "BDays"]
263
+ ytm = df_spot.at[index, "YTM"]
264
+
265
+ # Get the coupon dates for the bond without the last one (principal + coupon)
266
+ cp_dates = coupon_dates(settlement_date, maturity)
267
+
268
+ # If there is only one coupon date and this date is the first maturity date
269
+ # of an existing bond, the ytm rate is also a spot rate.
270
+ if len(cp_dates) == 1 and cp_dates[0] == ytm_rates.index[0]:
271
+ df_spot.at[index, "SpotRate"] = ytm
272
+ continue
273
+
274
+ # Calculate the real spot rate for the bond
275
+ coupons_pv = _calculate_coupons_pv(df_spot, settlement_date, maturity)
276
+ bond_price = quotation(settlement_date, maturity, ytm)
277
+ spot_rate = (FINAL_PMT / (bond_price - coupons_pv)) ** (252 / bdays) - 1
278
+ df_spot.at[index, "SpotRate"] = spot_rate
279
+
280
+ df_spot.drop(columns=["BDays"], inplace=True)
281
+ # Return the result without the intermediate coupon dates (virtual bonds)
282
+ existing_maturities = ytm_rates.index # noqa
283
+ return df_spot.query("MaturityDate in @existing_maturities").reset_index(drop=True)
284
+
285
+
286
+ def anbima_spot_rates(
287
+ reference_date: str | pd.Timestamp,
288
+ settlement_date: str | pd.Timestamp,
289
+ ) -> pd.DataFrame:
290
+ """
291
+ Fetch the NTN-B Anbima indicative rates and calculate the spot rates for the bonds.
292
+
293
+ Args:
294
+ reference_date (str | pd.Timestamp): The reference date for fetching the data.
295
+ settlement_date (str | pd.Timestamp): The reference date for settlement.
296
+
297
+ Returns:
298
+ pd.DataFrame: DataFrame containing the maturity dates and corresponding real
299
+ spot rates.
300
+ """
301
+ df_ytm = anbima_rates(reference_date)
302
+ maturity_dates = df_ytm["MaturityDate"]
303
+ ytm_rates = df_ytm["IndicativeRate"]
304
+ return spot_rates(settlement_date, maturity_dates, ytm_rates)
305
+
306
+
307
+ def _get_nir_df(reference_date: pd.Timestamp) -> pd.DataFrame:
308
+ """
309
+ Fetch the Nominal Interest Rate (NIR) data for NTN-B bonds.
310
+
311
+ Args:
312
+ reference_date (str | pd.Timestamp): The reference date for fetching the data.
313
+
314
+ Returns:
315
+ pd.DataFrame: DataFrame containing the NIR data for NTN-B bonds.
316
+ """
317
+ df = futures(contract_code="DI1", reference_date=reference_date)
318
+ if "CurrentRate" in df.columns:
319
+ df = df.rename(columns={"CurrentRate": "NIR_DI"})
320
+ keep_cols = [
321
+ "TradeDate",
322
+ "TradeTime",
323
+ "TickerSymbol",
324
+ "ExpirationDate",
325
+ "BDaysToExp",
326
+ "CurrentAskRate",
327
+ "CurrentBidRate",
328
+ "NIR_DI",
329
+ ]
330
+ elif "SettlementRate" in df.columns:
331
+ df = df.rename(columns={"SettlementRate": "NIR_DI"})
332
+ keep_cols = [
333
+ "TradeDate",
334
+ "TickerSymbol",
335
+ "ExpirationDate",
336
+ "BDaysToExp",
337
+ "NIR_DI",
338
+ ]
339
+ else:
340
+ raise ValueError("NIR data not found in the DataFrame.")
341
+
342
+ df = df[keep_cols].dropna(subset=["NIR_DI"])
343
+
344
+ # Add DI spreads for prefixed bonds (LTN) and adjust NIR
345
+ today = pd.Timestamp.today().normalize()
346
+ anbima_date = reference_date
347
+ if reference_date == today:
348
+ # If the reference date is today, use the previous business day
349
+ anbima_date = bday.offset(reference_date, -1)
350
+ df_pre = spread(spread_type="DI_PRE", reference_date=anbima_date)
351
+ df_pre.query("BondType == 'LTN'", inplace=True)
352
+ df_pre["MaturityDate"] = bday.offset(df_pre["MaturityDate"], 0)
353
+ df_pre["DISpread"] /= 10_000 # Remove BPS (basis points) from the spread
354
+ df_pre.drop(columns=["BondType"], inplace=True)
355
+
356
+ df = pd.merge_asof(df, df_pre, left_on="ExpirationDate", right_on="MaturityDate")
357
+ df["NIR_PRE"] = df["NIR_DI"] + df["DISpread"]
358
+
359
+ return df
360
+
361
+
362
+ def bei_rates(
363
+ reference_date: str | pd.Timestamp,
364
+ settlement_date: str | pd.Timestamp,
365
+ ytm_rates: pd.Series,
366
+ ) -> pd.DataFrame:
367
+ """
368
+ Calculate the Breakeven Inflation (BEI) for NTN-B bonds based on nominal and real
369
+ interest rates.
370
+
371
+ Args:
372
+ reference_date (str or pd.Timestamp): The reference date for fetching data and
373
+ performing calculations.
374
+ settlement_date (str or pd.Timestamp): The settlement date for the bonds.
375
+ ytm_rates (pd.Series): A series of Yield to Maturity (YTM) rates corresponding
376
+ to the maturity dates of the bonds indexed by the maturity dates.
377
+
378
+ Returns:
379
+ pd.DataFrame: DataFrame containing the breakeven inflation rates.
380
+
381
+ Returned columns:
382
+ - MaturityDate: Maturity date of the bond.
383
+ - BDays: Number of business days from the settlement date to the maturity.
384
+ - YTM: Yield to Maturity rate for the bond.
385
+ - RIR: Real Interest Rate for the bond.
386
+ - NIR_DI: Nominal Interest Rate for the bond.
387
+ - NIR_PRE: Nominal Interest Rate for the bond with DI spread.
388
+ - BEI_DI: Breakeven Inflation Rate for the bond.
389
+ - BEI_PRE: Breakeven Inflation Rate for the bond adjusted for DI spread.
390
+ """
391
+ # Normalize input dates
392
+ reference_date = dv.standardize_date(reference_date)
393
+ settlement_date = dv.standardize_date(settlement_date)
394
+
395
+ # Fetch Nominal Interest Rate (NIR) data
396
+ df_nir = _get_nir_df(reference_date)
397
+
398
+ ytm_interplator = Interpolator(
399
+ method="flat_forward",
400
+ known_bdays=df_nir["BDaysToExp"],
401
+ known_rates=df_nir["NIR_DI"],
402
+ )
403
+ # Calculate Real Interest Rate (RIR)
404
+ df = spot_rates(settlement_date, ytm_rates)
405
+ df = df.rename(columns={"SpotRate": "RIR"})
406
+ df["BDays"] = bday.count(reference_date, df["MaturityDate"])
407
+ df["NIR_DI"] = df["BDays"].apply(ytm_interplator)
408
+
409
+ # Calculate Breakeven Inflation Rate (BEI)
410
+ df["BEI_DI"] = ((df["NIR_DI"] + 1) / (df["RIR"] + 1)) - 1
411
+
412
+ # Adjust BEI for DI spread in prefixed bonds
413
+ ytm_interplator = Interpolator(
414
+ method="flat_forward",
415
+ known_bdays=df_nir["BDaysToExp"],
416
+ known_rates=df_nir["NIR_PRE"],
417
+ )
418
+ df["NIR_PRE"] = df["BDays"].apply(ytm_interplator)
419
+ df["BEI_PRE"] = ((df["NIR_PRE"] + 1) / (df["RIR"] + 1)) - 1
420
+
421
+ cols_reordered = [
422
+ "MaturityDate",
423
+ "BDays",
424
+ "YTM",
425
+ "RIR",
426
+ "NIR_DI",
427
+ "NIR_PRE",
428
+ "BEI_DI",
429
+ "BEI_PRE",
430
+ ]
431
+ return df[cols_reordered].copy()