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.
- {pyield-0.16.2 → pyield-0.16.4}/PKG-INFO +1 -1
- pyield-0.16.4/pyield/__about__.py +1 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/bond_tools.py +13 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/lft.py +18 -39
- {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/ntnb.py +43 -62
- {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/ntnf.py +35 -55
- {pyield-0.16.2 → pyield-0.16.4}/pyield/data/indicators.py +39 -22
- {pyield-0.16.2 → pyield-0.16.4}/pyproject.toml +1 -1
- pyield-0.16.2/pyield/__about__.py +0 -1
- {pyield-0.16.2 → pyield-0.16.4}/LICENSE +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/README.md +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/__init__.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/bday.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/__init__.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/bonds/ltn.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/data/__init__.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/data/anbima.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/data/data_cache.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/data/di.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/data/futures_data/__init__.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/data/futures_data/common.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/data/futures_data/historical.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/data/futures_data/intraday.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/data/futures_data/xml.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/data/projections.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/date_converter.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/holidays/__init__.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/holidays/br_holidays_new.txt +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/holidays/br_holidays_old.txt +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/holidays/holidays_manager.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/interpolator.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/pyield/py.typed +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/tests/__init__.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/tests/data/SPRD231222.zip +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/tests/data/SPRD240426.zip +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/tests/test_bday.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/tests/test_futures.py +0 -0
- {pyield-0.16.2 → pyield-0.16.4}/tests/test_futures_xml.py +0 -0
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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, "
|
|
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
|
|
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
|
-
|
|
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(
|
|
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":
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
cf_values =
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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="
|
|
309
|
+
maturities = pd.to_datetime(maturities, errors="raise", dayfirst=True)
|
|
333
310
|
_check_maturities(maturities)
|
|
334
311
|
|
|
335
312
|
# Create the interpolator object
|
|
336
|
-
|
|
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=
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
354
|
-
# Get the
|
|
355
|
-
|
|
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(
|
|
365
|
-
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
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
|
-
|
|
184
|
-
cf_values =
|
|
185
|
-
bdays = bday.count(settlement,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
|
269
|
-
|
|
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
|
-
|
|
250
|
+
df.at[index, "SpotRate"] = ltn_rate_interpolator(row["BDays"])
|
|
275
251
|
continue
|
|
276
252
|
|
|
277
|
-
# Calculate the
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
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 -
|
|
329
|
+
a = initial_guess - 0.005 # 50 bps
|
|
350
330
|
# range_width_bps above the initial guess
|
|
351
|
-
b = initial_guess +
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
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"
|
|
@@ -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
|
|
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
|