PYield 0.16.2__tar.gz → 0.16.4__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.16.2 → pyield-0.16.4}/PKG-INFO +1 -1
  2. pyield-0.16.4/pyield/__about__.py +1 -0
  3. {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/bond_tools.py +13 -0
  4. {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/lft.py +18 -39
  5. {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/ntnb.py +43 -62
  6. {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/ntnf.py +35 -55
  7. {pyield-0.16.2 → pyield-0.16.4}/pyield/data/indicators.py +39 -22
  8. {pyield-0.16.2 → pyield-0.16.4}/pyproject.toml +1 -1
  9. pyield-0.16.2/pyield/__about__.py +0 -1
  10. {pyield-0.16.2 → pyield-0.16.4}/LICENSE +0 -0
  11. {pyield-0.16.2 → pyield-0.16.4}/README.md +0 -0
  12. {pyield-0.16.2 → pyield-0.16.4}/pyield/__init__.py +0 -0
  13. {pyield-0.16.2 → pyield-0.16.4}/pyield/bday.py +0 -0
  14. {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/__init__.py +0 -0
  15. {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/ltn.py +0 -0
  16. {pyield-0.16.2 → pyield-0.16.4}/pyield/data/__init__.py +0 -0
  17. {pyield-0.16.2 → pyield-0.16.4}/pyield/data/anbima.py +0 -0
  18. {pyield-0.16.2 → pyield-0.16.4}/pyield/data/data_cache.py +0 -0
  19. {pyield-0.16.2 → pyield-0.16.4}/pyield/data/di.py +0 -0
  20. {pyield-0.16.2 → pyield-0.16.4}/pyield/data/futures_data/__init__.py +0 -0
  21. {pyield-0.16.2 → pyield-0.16.4}/pyield/data/futures_data/common.py +0 -0
  22. {pyield-0.16.2 → pyield-0.16.4}/pyield/data/futures_data/historical.py +0 -0
  23. {pyield-0.16.2 → pyield-0.16.4}/pyield/data/futures_data/intraday.py +0 -0
  24. {pyield-0.16.2 → pyield-0.16.4}/pyield/data/futures_data/xml.py +0 -0
  25. {pyield-0.16.2 → pyield-0.16.4}/pyield/data/projections.py +0 -0
  26. {pyield-0.16.2 → pyield-0.16.4}/pyield/date_converter.py +0 -0
  27. {pyield-0.16.2 → pyield-0.16.4}/pyield/holidays/__init__.py +0 -0
  28. {pyield-0.16.2 → pyield-0.16.4}/pyield/holidays/br_holidays_new.txt +0 -0
  29. {pyield-0.16.2 → pyield-0.16.4}/pyield/holidays/br_holidays_old.txt +0 -0
  30. {pyield-0.16.2 → pyield-0.16.4}/pyield/holidays/holidays_manager.py +0 -0
  31. {pyield-0.16.2 → pyield-0.16.4}/pyield/interpolator.py +0 -0
  32. {pyield-0.16.2 → pyield-0.16.4}/pyield/py.typed +0 -0
  33. {pyield-0.16.2 → pyield-0.16.4}/tests/__init__.py +0 -0
  34. {pyield-0.16.2 → pyield-0.16.4}/tests/data/SPRD231222.zip +0 -0
  35. {pyield-0.16.2 → pyield-0.16.4}/tests/data/SPRD240426.zip +0 -0
  36. {pyield-0.16.2 → pyield-0.16.4}/tests/test_bday.py +0 -0
  37. {pyield-0.16.2 → pyield-0.16.4}/tests/test_futures.py +0 -0
  38. {pyield-0.16.2 → pyield-0.16.4}/tests/test_futures_xml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PYield
3
- Version: 0.16.2
3
+ Version: 0.16.4
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.16.4"
@@ -40,6 +40,19 @@ def calculate_present_value(
40
40
  rates: pd.Series,
41
41
  periods: pd.Series,
42
42
  ) -> float:
43
+ # Return 0 if any input is empty
44
+ if cash_flows.empty or rates.empty or periods.empty:
45
+ return 0
46
+
47
+ # Reset the index to avoid issues with the series alignment
48
+ cash_flows = cash_flows.reset_index(drop=True)
49
+ rates = rates.reset_index(drop=True)
50
+ periods = periods.reset_index(drop=True)
51
+
52
+ # Check if data have the same length
53
+ if len(cash_flows) != len(rates) or len(cash_flows) != len(periods):
54
+ raise ValueError("All series must have the same length.")
55
+
43
56
  return (cash_flows / (1 + rates) ** periods).sum()
44
57
 
45
58
 
@@ -2,7 +2,7 @@ import pandas as pd
2
2
 
3
3
  from .. import bday
4
4
  from .. import date_converter as dc
5
- from ..data import anbima, di
5
+ from ..data import anbima
6
6
  from . import bond_tools as bt
7
7
 
8
8
 
@@ -77,47 +77,26 @@ def quotation(
77
77
  return bt.truncate(100 * discount_factor, 4)
78
78
 
79
79
 
80
- def premium(lft_rate: float, di_rate: float) -> float:
81
- di_factor = (1 + di_rate) ** (1 / 252)
82
- lft_factor = (1 + lft_rate) ** (1 / 252) * di_factor
83
-
84
- return (lft_factor - 1) / (di_factor - 1)
85
-
86
-
87
- def historical_premium(
88
- reference_date: str | pd.Timestamp,
89
- maturity: str | pd.Timestamp,
90
- ) -> float:
80
+ def premium(lft_rate: float, selic_over: float) -> float:
91
81
  """
92
- Calculate the premium of the LFT bond over the DI Future rate for a given date.
82
+ Calculate the premium of the LFT bond over the Selic rate (overnight).
83
+ Obs: The Selic rate (overnight) is not the same as the Selic target rate
93
84
 
94
85
  Args:
95
- reference_date (str | pd.Timestamp): The reference date to fetch the rates.
96
- maturity (str | pd.Timestamp): The maturity date of the LFT bond.
86
+ lft_rate (float): The LFT rate for the bond.
87
+ selic_over (float): The Selic overnight rate.
97
88
 
98
89
  Returns:
99
- float: The premium of the LFT bond over the DI Future rate for the given date.
100
- If the data is not available, returns NaN.
101
- """
102
- # Convert input dates to a consistent format
103
- reference_date = dc.convert_date(reference_date)
104
- maturity = dc.convert_date(maturity)
90
+ float: The premium of the LFT bond over the Selic rate.
105
91
 
106
- # Retrieve LFT rates for the reference date
107
- df_anbima = rates(reference_date)
108
- if df_anbima.empty:
109
- return float("NaN")
110
-
111
- # Extract the LFT rate for the specified maturity date
112
- lft_rates = df_anbima.query("MaturityDate == @maturity")["IndicativeRate"]
113
- if lft_rates.empty:
114
- return float("NaN")
115
- lft_rate = float(lft_rates.iloc[0])
116
-
117
- # Retrieve DI rate for the reference date and maturity
118
- di_rate = di.rate(trade_date=reference_date, expiration=maturity)
119
- if pd.isnull(di_rate): # Check if the DI rate is NaN
120
- return float("NaN")
121
-
122
- # Calculate and return the premium using the extracted rates
123
- return premium(lft_rate, di_rate)
92
+ Examples:
93
+ Calculate the premium of a LFT bond with a 0.02 yield rate over the Selic rate:
94
+ >>> lft_rate = 0.1695 / 100 # 0.1695%
95
+ >>> selic_over = 10.40 / 100 # 10.40%
96
+ >>> lft.premium(lft_rate, selic_over)
97
+ 101.71205192837589
98
+ """
99
+ adjusted_lft_rate = (lft_rate + 1) * (selic_over + 1) - 1
100
+ f1 = (adjusted_lft_rate + 1) ** (1 / 252) - 1
101
+ f2 = (selic_over + 1) ** (1 / 252) - 1
102
+ return f1 / f2
@@ -65,7 +65,7 @@ def rates(reference_date: str | pd.Timestamp) -> pd.DataFrame:
65
65
  pd.DataFrame: DataFrame containing the maturity dates and indicative rates
66
66
  for the NTN-B bonds.
67
67
  """
68
- ntnb_rates = anbima.rates(reference_date, "LTN")
68
+ ntnb_rates = anbima.rates(reference_date, "NTN-B")
69
69
  if ntnb_rates.empty:
70
70
  return pd.DataFrame()
71
71
  return ntnb_rates[["MaturityDate", "IndicativeRate"]]
@@ -121,7 +121,7 @@ def _coupon_dates_map(
121
121
  return pd.Series(coupon_dates).reset_index(drop=True)
122
122
 
123
123
 
124
- def coupon_dates(
124
+ def payment_dates(
125
125
  settlement: str | pd.Timestamp,
126
126
  maturity: str | pd.Timestamp,
127
127
  ) -> pd.Series:
@@ -186,13 +186,13 @@ def cash_flows(
186
186
  maturity = dc.convert_date(maturity)
187
187
 
188
188
  # Get the coupon dates between the settlement and maturity dates
189
- payment_dates = coupon_dates(settlement, maturity)
189
+ p_dates = payment_dates(settlement, maturity)
190
190
 
191
191
  # Set the cash flow at maturity to FINAL_PMT and the others to COUPON_PMT
192
- cfs = np.where(payment_dates == maturity, FINAL_PMT, COUPON_PMT).tolist()
192
+ cfs = np.where(p_dates == maturity, FINAL_PMT, COUPON_PMT).tolist()
193
193
 
194
194
  # Return a dataframe with the payment dates and cash flows
195
- return pd.DataFrame(data={"PaymentDate": payment_dates, "CashFlow": cfs})
195
+ return pd.DataFrame(data={"PaymentDate": p_dates, "CashFlow": cfs})
196
196
 
197
197
 
198
198
  def quotation(
@@ -231,12 +231,12 @@ def quotation(
231
231
  maturity = dc.convert_date(maturity)
232
232
  _check_maturities(maturity)
233
233
 
234
- df_cf = cash_flows(settlement, maturity)
235
- payment_dates = df_cf["PaymentDate"]
236
- cf_values = df_cf["CashFlow"]
234
+ cf_df = cash_flows(settlement, maturity)
235
+ cf_dates = cf_df["PaymentDate"]
236
+ cf_values = cf_df["CashFlow"]
237
237
 
238
238
  # Calculate the number of business days between settlement and cash flow dates
239
- bdays = bday.count(settlement, payment_dates)
239
+ bdays = bday.count(settlement, cf_dates)
240
240
 
241
241
  # Calculate the number of periods truncated as per Anbima rules
242
242
  num_of_years = bt.truncate(bdays / 252, 14)
@@ -244,10 +244,10 @@ def quotation(
244
244
  discount_factor = (1 + rate) ** num_of_years
245
245
 
246
246
  # Calculate the present value of each cash flow (DCF) rounded as per Anbima rules
247
- discounted_cash_flows = (cf_values / discount_factor).round(10)
247
+ cf_present_value = (cf_values / discount_factor).round(10)
248
248
 
249
249
  # Return the quotation (the dcf sum) truncated as per Anbima rules
250
- return bt.truncate(discounted_cash_flows.sum(), 4)
250
+ return bt.truncate(cf_present_value.sum(), 4)
251
251
 
252
252
 
253
253
  def price(
@@ -274,32 +274,10 @@ def price(
274
274
  return bt.truncate(vna * quotation / 100, 6)
275
275
 
276
276
 
277
- def _calculate_coupons_pv(
278
- bootstrap_df: pd.DataFrame,
279
- settlement: pd.Timestamp,
280
- maturity: pd.Timestamp,
281
- ) -> float:
282
- # Create a subset DataFrame with only the coupon payments (without last payment)
283
- cp_dates_wo_last = coupon_dates(settlement, maturity)[:-1] # noqa
284
- if len(cp_dates_wo_last) == 0:
285
- return 0
286
-
287
- df_coupons = bootstrap_df.query("MaturityDate in @cp_dates_wo_last").copy()
288
- df_coupons["Coupon"] = COUPON_PMT
289
-
290
- # Calculate the present value of the coupon payments
291
- pv = bt.calculate_present_value(
292
- cash_flows=df_coupons["Coupon"],
293
- rates=df_coupons["SpotRate"],
294
- periods=df_coupons["BDays"] / 252,
295
- )
296
- return pv
297
-
298
-
299
277
  def spot_rates(
300
278
  settlement: str | pd.Timestamp,
301
- rates: pd.Series,
302
279
  maturities: pd.Series,
280
+ rates: pd.Series,
303
281
  ) -> pd.DataFrame:
304
282
  """
305
283
  Calculate the spot rates for NTN-B bonds using the bootstrap method.
@@ -312,9 +290,8 @@ def spot_rates(
312
290
 
313
291
  Args:
314
292
  settlement (str | pd.Timestamp): The reference date for settlement.
315
- rates (pd.Series): Series of yield to maturity rates indexed by the
316
- maturity dates of the bonds.
317
293
  maturities (pd.Series): Series of maturity dates for the bonds.
294
+ rates (pd.Series): Series of yield to maturity rates.
318
295
 
319
296
  Returns:
320
297
  pd.DataFrame: DataFrame containing the maturity dates and corresponding real
@@ -329,51 +306,55 @@ def spot_rates(
329
306
  """
330
307
  # Process and validate the input data
331
308
  settlement = dc.convert_date(settlement)
332
- maturities = pd.to_datetime(maturities, errors="coerce", dayfirst=True)
309
+ maturities = pd.to_datetime(maturities, errors="raise", dayfirst=True)
333
310
  _check_maturities(maturities)
334
311
 
335
312
  # Create the interpolator object
336
- ytm_rate_interpolator = it.Interpolator(
313
+ ff_interpolator = it.Interpolator(
337
314
  method="flat_forward",
338
315
  known_bdays=bday.count(settlement, maturities),
339
316
  known_rates=rates,
340
317
  )
341
318
 
342
- last_ntnb = maturities.max()
343
-
344
319
  # Generate coupon dates up to the longest maturity date
345
- all_coupon_dates = _coupon_dates_map(start=settlement, end=last_ntnb)
320
+ all_coupon_dates = _coupon_dates_map(start=settlement, end=maturities.max())
346
321
 
347
322
  # Create a DataFrame with all coupon dates and the corresponding YTM
348
- df_spot = pd.DataFrame(data=all_coupon_dates, columns=["MaturityDate"])
349
- df_spot["BDays"] = bday.count(settlement, df_spot["MaturityDate"])
350
- df_spot["YTM"] = df_spot["BDays"].apply(ytm_rate_interpolator)
323
+ df = pd.DataFrame(data=all_coupon_dates, columns=["MaturityDate"])
324
+ df["BDays"] = bday.count(settlement, df["MaturityDate"])
325
+ df["BYears"] = df["BDays"] / 252
326
+ df["YTM"] = df["BDays"].apply(ff_interpolator)
327
+ df["Coupon"] = COUPON_PMT
328
+ df["SpotRate"] = pd.NA
351
329
 
352
330
  # The Bootstrap loop to calculate spot rates
353
- for index in df_spot.index:
354
- # Get the row values using the index
355
- maturity = df_spot.at[index, "MaturityDate"]
356
- bdays = df_spot.at[index, "BDays"]
357
- ytm = df_spot.at[index, "YTM"]
358
-
359
- # Get the coupon dates for the bond without the last one (principal + coupon)
360
- cp_dates = coupon_dates(settlement, maturity)
331
+ for index, row in df.iterrows():
332
+ # Get the cash flow dates for the bond
333
+ cf_dates = payment_dates(settlement, row["MaturityDate"])
361
334
 
362
335
  # If there is only one coupon date and this date is the first maturity date
363
336
  # of an existing bond, the ytm rate is also a spot rate.
364
- if len(cp_dates) == 1 and cp_dates[0] == rates[0]:
365
- df_spot.at[index, "SpotRate"] = ytm
337
+ if len(cf_dates) == 1 and cf_dates[0] == maturities[0]:
338
+ df.at[index, "SpotRate"] = row["YTM"]
366
339
  continue
367
340
 
368
- # Calculate the real spot rate for the bond
369
- coupons_pv = _calculate_coupons_pv(df_spot, settlement, maturity)
370
- bond_price = quotation(settlement, maturity, ytm)
371
- spot_rate = (FINAL_PMT / (bond_price - coupons_pv)) ** (252 / bdays) - 1
372
- df_spot.at[index, "SpotRate"] = spot_rate
373
-
374
- df_spot.drop(columns=["BDays"], inplace=True)
341
+ # Calculate the present value of the cash flows without last payment
342
+ cf_dates = cf_dates[:-1]
343
+ cf_df = df.query("MaturityDate in @cf_dates").reset_index(drop=True)
344
+ cf_present_value = bt.calculate_present_value(
345
+ cash_flows=cf_df["Coupon"],
346
+ rates=cf_df["SpotRate"],
347
+ periods=cf_df["BYears"],
348
+ )
349
+
350
+ # Calculate the Spot Rate for the bond
351
+ bond_price = quotation(settlement, row["MaturityDate"], row["YTM"])
352
+ price_factor = FINAL_PMT / (bond_price - cf_present_value)
353
+ df.at[index, "SpotRate"] = price_factor ** (1 / row["BYears"]) - 1
354
+
355
+ df.drop(columns=["BDays", "BYears", "Coupon"], inplace=True)
375
356
  # Return the result without the intermediate coupon dates (virtual bonds)
376
- return df_spot.query("MaturityDate in @maturities").reset_index(drop=True)
357
+ return df.query("MaturityDate in @maturities").reset_index(drop=True)
377
358
 
378
359
 
379
360
  def _get_nir_df(reference_date: pd.Timestamp) -> pd.DataFrame:
@@ -180,9 +180,9 @@ def price(
180
180
  >>> price("05-07-2024", "01-01-2035", 0.11921)
181
181
  895.359254
182
182
  """
183
- df_cf = cash_flows(settlement, maturity)
184
- cf_values = df_cf["CashFlow"]
185
- bdays = bday.count(settlement, df_cf["PaymentDate"])
183
+ cf_df = cash_flows(settlement, maturity)
184
+ cf_values = cf_df["CashFlow"]
185
+ bdays = bday.count(settlement, cf_df["PaymentDate"])
186
186
  byears = bt.truncate(bdays / 252, 14)
187
187
  discount_factors = (1 + rate) ** byears
188
188
  # Calculate the present value of each cash flow (DCF) rounded as per Anbima rules
@@ -191,31 +191,12 @@ def price(
191
191
  return bt.truncate(dcf.sum(), 6)
192
192
 
193
193
 
194
- def _calculate_coupons_pv(
195
- bootstrap_df: pd.DataFrame,
196
- settlement: pd.Timestamp,
197
- maturity: pd.Timestamp,
198
- ) -> float:
199
- # Create a subset DataFrame with only the coupon payments (without last payment)
200
- cp_dates_wo_last = coupon_dates(settlement, maturity)[:-1] # noqa
201
- df_coupons = bootstrap_df.query("MaturityDate in @cp_dates_wo_last").copy()
202
- df_coupons["Coupon"] = COUPON_PMT
203
-
204
- # Calculate the present value of the coupon payments
205
- pv = bt.calculate_present_value(
206
- cash_flows=df_coupons["Coupon"],
207
- rates=df_coupons["SpotRate"],
208
- periods=df_coupons["BDays"] / 252,
209
- )
210
- return pv
211
-
212
-
213
194
  def spot_rates(
214
195
  settlement: str | pd.Timestamp,
215
- ltn_rates: pd.Series,
216
196
  ltn_maturities: pd.Series,
217
- ntnf_rates: pd.Series,
197
+ ltn_rates: pd.Series,
218
198
  ntnf_maturities: pd.Series,
199
+ ntnf_rates: pd.Series,
219
200
  ) -> pd.DataFrame:
220
201
  """
221
202
  Calculate the spot rates for NTN-F bonds using the bootstrap method.
@@ -252,36 +233,38 @@ def spot_rates(
252
233
  known_rates=ntnf_rates,
253
234
  )
254
235
 
255
- # Determine the last maturity dates for LTN and NTN-F rates
256
- last_ltn = ltn_maturities.max()
257
- last_ntnf = ntnf_maturities.max()
258
-
259
236
  # Generate all coupon dates up to the last NTN-F maturity date
260
- all_coupon_dates = coupon_dates(settlement, last_ntnf)
237
+ all_coupon_dates = coupon_dates(settlement, ntnf_maturities.max())
261
238
 
262
239
  # Create a DataFrame with all coupon dates and the corresponding YTM
263
- df_spot = pd.DataFrame(data=all_coupon_dates, columns=["MaturityDate"])
264
- df_spot["BDays"] = bday.count(start=settlement, end=df_spot["MaturityDate"])
265
- df_spot["YTM"] = df_spot["BDays"].apply(ntnf_rate_interpolator)
240
+ df = pd.DataFrame(data=all_coupon_dates, columns=["MaturityDate"])
241
+ df["BDays"] = bday.count(start=settlement, end=df["MaturityDate"])
242
+ df["BYears"] = df["BDays"] / 252
243
+ df["Coupon"] = COUPON_PMT
244
+ df["YTM"] = df["BDays"].apply(ntnf_rate_interpolator)
266
245
 
267
246
  # The Bootstrap loop to calculate spot rates
268
- for index in df_spot.index:
269
- maturity = df_spot.at[index, "MaturityDate"]
270
- bdays = df_spot.at[index, "BDays"]
271
-
272
- if maturity <= last_ltn:
247
+ for index, row in df.iterrows():
248
+ if row["MaturityDate"] <= ltn_maturities.max():
273
249
  # Use LTN rates for maturities before the last LTN maturity date
274
- df_spot.at[index, "SpotRate"] = ltn_rate_interpolator(bdays)
250
+ df.at[index, "SpotRate"] = ltn_rate_interpolator(row["BDays"])
275
251
  continue
276
252
 
277
- # Calculate the spot rate for the bond
278
- coupons_pv = _calculate_coupons_pv(df_spot, settlement, maturity)
279
- ytm = df_spot.at[index, "YTM"]
280
- bond_price = price(settlement, maturity, ytm)
281
- spot_rate = (FINAL_PMT / (bond_price - coupons_pv)) ** (252 / bdays) - 1
282
- df_spot.at[index, "SpotRate"] = spot_rate
253
+ # Calculate the present value of the coupon payments
254
+ cf_dates = coupon_dates(settlement, row["MaturityDate"])[:-1] # noqa
255
+ cf_df = df.query("MaturityDate in @cf_dates").reset_index(drop=True)
256
+ cf_present_value = bt.calculate_present_value(
257
+ cash_flows=cf_df["Coupon"],
258
+ rates=cf_df["SpotRate"],
259
+ periods=cf_df["BDays"] / 252,
260
+ )
261
+
262
+ bond_price = price(settlement, row["MaturityDate"], row["YTM"])
263
+ price_factor = FINAL_PMT / (bond_price - cf_present_value)
264
+ df.at[index, "SpotRate"] = price_factor ** (1 / row["BYears"]) - 1
283
265
 
284
- return df_spot
266
+ # Remove temporary columns and return the spot rates DataFrame
267
+ return df[["MaturityDate", "BDays", "YTM", "SpotRate"]].copy()
285
268
 
286
269
 
287
270
  def di_spreads(reference_date: str | pd.Timestamp) -> pd.DataFrame:
@@ -328,7 +311,6 @@ def _bisection_method(func, a, b, tol=1e-8, maxiter=100):
328
311
  def _solve_spread(
329
312
  price_difference_func: Callable,
330
313
  initial_guess: float | None = None,
331
- range_width_bps: float = 50,
332
314
  ) -> float:
333
315
  """
334
316
  Solve for the spread that zeroes the price difference using a bisection method.
@@ -337,8 +319,6 @@ def _solve_spread(
337
319
  price_difference_func (callable): The function that computes the difference
338
320
  between the bond's market price and its discounted cash flows.
339
321
  initial_guess (float, optional): An initial guess for the spread.
340
- range_width_bps (float, optional): The width of the range around the initial
341
- guess in bps. Defaults to 50 bps.
342
322
 
343
323
  Returns:
344
324
  float: The solution for the spread in bps or NaN if no solution is found.
@@ -346,9 +326,9 @@ def _solve_spread(
346
326
  try:
347
327
  if initial_guess is not None:
348
328
  # range_width_bps below the initial guess
349
- a = initial_guess - range_width_bps / 10_000
329
+ a = initial_guess - 0.005 # 50 bps
350
330
  # range_width_bps above the initial guess
351
- b = initial_guess + range_width_bps / 10_000
331
+ b = initial_guess + 0.005 # 50 bps
352
332
  else:
353
333
  a = -0.01 # Initial guess of -100 bps
354
334
  b = 0.01 # Initial guess of 100 bps
@@ -366,8 +346,8 @@ def di_net_spread( # noqa
366
346
  settlement: str | pd.Timestamp,
367
347
  maturity: str | pd.Timestamp,
368
348
  ytm: float,
369
- di_rates: pd.Series,
370
349
  di_expirations: pd.Series,
350
+ di_rates: pd.Series,
371
351
  initial_guess: float | None = None,
372
352
  ) -> float:
373
353
  """
@@ -388,7 +368,7 @@ def di_net_spread( # noqa
388
368
  di_rates (pd.Series): A Series of DI rates.
389
369
  di_expirations (pd.Series): A list or Series of DI expiration dates.
390
370
  initial_guess (float, optional): An initial guess for the spread. Defaults to
391
- None.
371
+ None. A good initial guess is the DI gross spread for the bond.
392
372
 
393
373
  Returns:
394
374
  float: The net DI spread in basis points.
@@ -422,7 +402,7 @@ def di_net_spread( # noqa
422
402
  # Difference between the bond's price and its discounted cash flows
423
403
  return (bond_cash_flows / (1 + di_interp + p) ** byears).sum() - bond_price
424
404
 
425
- # Solve for the spread that zeroes the price difference using the Brent method
405
+ # Solve for the spread that zeroes the price difference using the bisection method
426
406
  p_solution = _solve_spread(price_difference, initial_guess)
427
407
  # Convert the solution to basis points (bps) and round to two decimal places
428
408
  return round((p_solution * 10_000), 2)
@@ -430,10 +410,10 @@ def di_net_spread( # noqa
430
410
 
431
411
  def premium(
432
412
  settlement: str | pd.Timestamp,
433
- ntnf_rate: float,
434
413
  ntnf_maturity: str | pd.Timestamp,
435
- di_rates: pd.Series,
414
+ ntnf_rate: float,
436
415
  di_expirations: pd.Series,
416
+ di_rates: pd.Series,
437
417
  ) -> float:
438
418
  ntnf_maturity = dc.convert_date(ntnf_maturity)
439
419
  settlement = dc.convert_date(settlement)
@@ -7,15 +7,17 @@ from .. import date_converter as dc
7
7
 
8
8
 
9
9
  def indicator(
10
- indicator_type: Literal["IPCA_MR", "SELIC_TARGET", "DI", "VNA_LFT"],
10
+ indicator_type: Literal["IPCA_MR", "SELIC_TARGET", "SELIC_OVER", "DI", "VNA_LFT"],
11
11
  reference_date: str | pd.Timestamp,
12
- ) -> float | None:
12
+ ) -> float:
13
13
  reference_date = dc.convert_date(reference_date)
14
14
  ind_type = str(indicator_type).upper()
15
15
  if ind_type == "IPCA_MR":
16
16
  return ipca_monthly_rate(reference_date)
17
17
  elif ind_type == "SELIC_TARGET":
18
18
  return selic_target(reference_date)
19
+ elif ind_type == "SELIC_OVER":
20
+ return selic_over(reference_date)
19
21
  elif ind_type == "DI":
20
22
  return di(reference_date)
21
23
  elif ind_type == "VNA_LFT":
@@ -24,7 +26,7 @@ def indicator(
24
26
  raise ValueError(f"Invalid indicator type: {ind_type}")
25
27
 
26
28
 
27
- def ipca_monthly_rate(reference_date: pd.Timestamp) -> float | None:
29
+ def ipca_monthly_rate(reference_date: pd.Timestamp) -> float:
28
30
  """
29
31
  Fetches the IPCA (Índice Nacional de Preços ao Consumidor Amplo) monthly rate
30
32
  from the IBGE (Instituto Brasileiro de Geografia e Estatística) for a given
@@ -35,7 +37,7 @@ def ipca_monthly_rate(reference_date: pd.Timestamp) -> float | None:
35
37
  is fetched. If a string is passed, it should be in 'DD-MM-YYYY' format.
36
38
 
37
39
  Returns:
38
- float | None: The IPCA monthly rate for the specified date as a float.
40
+ float: The IPCA monthly rate for the specified date as a float.
39
41
  Returns None if data is not found or in case of an error.
40
42
 
41
43
  Notes:
@@ -65,15 +67,14 @@ def ipca_monthly_rate(reference_date: pd.Timestamp) -> float | None:
65
67
  # Parse the JSON response
66
68
  data = response.json()
67
69
 
70
+ if not data:
71
+ return float("nan")
68
72
  # Extract and return the IPCA monthly growth rate if data is available
69
- if data:
70
- ipca_str = data[0]["resultados"][0]["series"][0]["serie"][ipca_date]
71
- return round(float(ipca_str) / 100, 4)
72
- else:
73
- return None
73
+ ipca_str = data[0]["resultados"][0]["series"][0]["serie"][ipca_date]
74
+ return round(float(ipca_str) / 100, 4)
74
75
 
75
76
 
76
- def selic_target(reference_date: pd.Timestamp) -> float | None:
77
+ def selic_target(reference_date: pd.Timestamp) -> float:
77
78
  """
78
79
  Examples:
79
80
  >>> selic_taget("31-05-2024")
@@ -85,14 +86,31 @@ def selic_target(reference_date: pd.Timestamp) -> float | None:
85
86
  response = requests.get(api_url, timeout=10)
86
87
  response.raise_for_status()
87
88
 
88
- if selic_date in response.text:
89
- data = response.json()
90
- return round(float(data[0]["valor"]) / 100, 4)
91
- else:
92
- return None
89
+ if selic_date not in response.text:
90
+ return float("nan")
91
+ data = response.json()
92
+ return round(float(data[0]["valor"]) / 100, 4)
93
+
94
+
95
+ def selic_over(reference_date: pd.Timestamp) -> float:
96
+ """
97
+ Examples:
98
+ >>> selic_over("26-08-2024")
99
+ 0.1040 # Indicates a SELIC rate of 10.40% p.a.
100
+ """
101
+ # https://api.bcb.gov.br/dados/serie/bcdata.sgs.1178/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
102
+ formatted_date = reference_date.strftime("%d/%m/%Y")
103
+ api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.1178/dados?formato=json&dataInicial={formatted_date}&dataFinal={formatted_date}"
104
+ response = requests.get(api_url, timeout=10)
105
+ response.raise_for_status()
93
106
 
107
+ if formatted_date not in response.text:
108
+ return float("nan")
109
+ data = response.json()
110
+ return round(float(data[0]["valor"]) / 100, 4)
94
111
 
95
- def di(reference_date: pd.Timestamp) -> float | None:
112
+
113
+ def di(reference_date: pd.Timestamp) -> float:
96
114
  """
97
115
  Examples:
98
116
  >>> di("31-05-2024")
@@ -104,14 +122,13 @@ def di(reference_date: pd.Timestamp) -> float | None:
104
122
  response = requests.get(api_url, timeout=10)
105
123
  response.raise_for_status()
106
124
 
107
- if di_date in response.text:
108
- data = response.json()
109
- return round(float(data[0]["valor"]) / 100, 8)
110
- else:
111
- return None
125
+ if di_date not in response.text:
126
+ return float("nan")
127
+ data = response.json()
128
+ return round(float(data[0]["valor"]) / 100, 8)
112
129
 
113
130
 
114
- def vna_lft(reference_date: pd.Timestamp) -> float | None:
131
+ def vna_lft(reference_date: pd.Timestamp) -> float:
115
132
  # url example: https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/20240418APC238
116
133
  url_base = "https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/"
117
134
  url_file = f"{reference_date.strftime('%Y%m%d')}APC238"
@@ -32,7 +32,7 @@ dependencies = [
32
32
  "types-requests",
33
33
  ]
34
34
  dynamic = []
35
- version = "0.16.2"
35
+ version = "0.16.4"
36
36
 
37
37
  [project.license]
38
38
  file = "LICENSE"
@@ -1 +0,0 @@
1
- __version__ = "0.16.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