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.
- {pyield-0.9.7 → pyield-0.9.8}/PKG-INFO +5 -5
- {pyield-0.9.7 → pyield-0.9.8}/README.md +4 -4
- pyield-0.9.8/pyield/__about__.py +1 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/__init__.py +7 -5
- pyield-0.9.8/pyield/bonds/ltn.py +82 -0
- pyield-0.9.8/pyield/bonds/ntnb.py +431 -0
- pyield-0.9.8/pyield/bonds/ntnf.py +279 -0
- pyield-0.9.8/pyield/bonds/utils.py +36 -0
- pyield-0.9.8/pyield/date_validator.py +66 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/anbima.py +2 -1
- {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/futures/__init__.py +1 -1
- {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/indicators.py +4 -4
- {pyield-0.9.7 → pyield-0.9.8}/pyield/interpolator.py +46 -49
- {pyield-0.9.7 → pyield-0.9.8}/pyield/spreads.py +1 -1
- {pyield-0.9.7 → pyield-0.9.8}/pyproject.toml +2 -2
- pyield-0.9.8/tests/__init__.py +0 -0
- pyield-0.9.7/pyield/__about__.py +0 -1
- pyield-0.9.7/pyield/date_validator.py +0 -62
- pyield-0.9.7/pyield/ntnb.py +0 -400
- {pyield-0.9.7 → pyield-0.9.8}/LICENSE +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/bday.py +0 -0
- {pyield-0.9.7/pyield/fetchers → pyield-0.9.8/pyield/bonds}/__init__.py +0 -0
- {pyield-0.9.7/tests → pyield-0.9.8/pyield/fetchers}/__init__.py +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/futures/common.py +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/futures/historical.py +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/futures/intraday.py +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/futures/xml.py +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/fetchers/projections.py +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/holidays/__init__.py +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/holidays/br_holidays_new.txt +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/holidays/br_holidays_old.txt +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/holidays/holidays_manager.py +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/pyield/py.typed +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/tests/data/SPRD231222.zip +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/tests/data/SPRD240426.zip +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/tests/test_bday.py +0 -0
- {pyield-0.9.7 → pyield-0.9.8}/tests/test_futures.py +0 -0
- {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.
|
|
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
|
-
>>>
|
|
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
|
-
>>>
|
|
212
|
+
>>> linear_interpolator(45)
|
|
213
213
|
0.0475
|
|
214
214
|
|
|
215
215
|
# Use the flat forward method for interpolation.
|
|
216
|
-
>>>
|
|
217
|
-
>>>
|
|
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
|
-
>>>
|
|
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
|
-
>>>
|
|
169
|
+
>>> linear_interpolator(45)
|
|
170
170
|
0.0475
|
|
171
171
|
|
|
172
172
|
# Use the flat forward method for interpolation.
|
|
173
|
-
>>>
|
|
174
|
-
>>>
|
|
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
|
|
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()
|