PYield 0.20.2__tar.gz → 0.22.0__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.20.2 → pyield-0.22.0}/PKG-INFO +1 -1
- pyield-0.22.0/pyield/__about__.py +1 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/__init__.py +4 -1
- {pyield-0.20.2 → pyield-0.22.0}/pyield/anbima.py +3 -3
- {pyield-0.20.2 → pyield-0.22.0}/pyield/b3_futures/__init__.py +3 -2
- {pyield-0.20.2 → pyield-0.22.0}/pyield/b3_futures/historical.py +1 -1
- {pyield-0.20.2 → pyield-0.22.0}/pyield/bday.py +136 -79
- {pyield-0.20.2 → pyield-0.22.0}/pyield/data_cache.py +5 -4
- {pyield-0.20.2 → pyield-0.22.0}/pyield/date_converter.py +4 -2
- pyield-0.22.0/pyield/di.py +198 -0
- pyield-0.22.0/pyield/holidays/brholidays.py +94 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyproject.toml +1 -1
- pyield-0.20.2/pyield/__about__.py +0 -1
- pyield-0.20.2/pyield/di.py +0 -190
- pyield-0.20.2/pyield/holidays/brholidays.py +0 -62
- {pyield-0.20.2 → pyield-0.22.0}/LICENSE +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/README.md +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/b3_futures/common.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/b3_futures/intraday.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/b3_futures/xml.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/__init__.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/bond_tools.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/lft.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/ltn.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/ntnb.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/ntnf.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/conftest.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/holidays/__init__.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/holidays/br_holidays_new.txt +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/holidays/br_holidays_old.txt +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/indicators.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/interpolator.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/projections.py +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/py.typed +0 -0
- {pyield-0.20.2 → pyield-0.22.0}/pyield/tools.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.22.0"
|
|
@@ -2,6 +2,7 @@ from pyield import anbima, bday, di, tools
|
|
|
2
2
|
from pyield.__about__ import __version__
|
|
3
3
|
from pyield.b3_futures import futures
|
|
4
4
|
from pyield.bonds import lft, ltn, ntnb, ntnf
|
|
5
|
+
from pyield.di import DIFutures
|
|
5
6
|
from pyield.indicators import indicator
|
|
6
7
|
from pyield.interpolator import Interpolator
|
|
7
8
|
from pyield.projections import projection
|
|
@@ -16,8 +17,10 @@ __all__ = [
|
|
|
16
17
|
"ntnf",
|
|
17
18
|
"ltn",
|
|
18
19
|
"lft",
|
|
19
|
-
"
|
|
20
|
+
"DIFutures",
|
|
20
21
|
"projection",
|
|
21
22
|
"Interpolator",
|
|
22
23
|
"tools",
|
|
24
|
+
"di",
|
|
25
|
+
"DIFutures",
|
|
23
26
|
]
|
|
@@ -7,7 +7,7 @@ import requests
|
|
|
7
7
|
|
|
8
8
|
from pyield import bday
|
|
9
9
|
from pyield import date_converter as dc
|
|
10
|
-
from pyield.data_cache import
|
|
10
|
+
from pyield.data_cache import get_anbima_dataset
|
|
11
11
|
|
|
12
12
|
# URL Constants
|
|
13
13
|
ANBIMA_URL = "https://www.anbima.com.br/informacoes/merc-sec/arqs"
|
|
@@ -132,7 +132,7 @@ def rates(
|
|
|
132
132
|
bond_type: str | None = None,
|
|
133
133
|
adj_maturities: bool = False,
|
|
134
134
|
) -> pd.DataFrame:
|
|
135
|
-
df =
|
|
135
|
+
df = get_anbima_dataset()
|
|
136
136
|
|
|
137
137
|
reference_date = dc.convert_input_dates(reference_date)
|
|
138
138
|
df.query("ReferenceDate == @reference_date", inplace=True)
|
|
@@ -158,7 +158,7 @@ def rates(
|
|
|
158
158
|
def pre_maturities(
|
|
159
159
|
reference_date: str | pd.Timestamp,
|
|
160
160
|
) -> pd.Series:
|
|
161
|
-
df =
|
|
161
|
+
df = get_anbima_dataset()
|
|
162
162
|
reference_date = dc.convert_input_dates(reference_date)
|
|
163
163
|
df.query("ReferenceDate == @reference_date", inplace=True)
|
|
164
164
|
df.query("BondType in ['LTN', 'NTN-F']", inplace=True)
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import datetime as dt
|
|
2
|
+
from zoneinfo import ZoneInfo
|
|
2
3
|
|
|
3
4
|
import pandas as pd
|
|
4
|
-
from pytz import timezone
|
|
5
5
|
|
|
6
6
|
from .. import date_converter as dc
|
|
7
7
|
from .historical import fetch_historical_df
|
|
8
8
|
from .intraday import fetch_intraday_df
|
|
9
9
|
|
|
10
10
|
SUPPORTED_FUTURES = ["DI1", "DDI", "FRC", "DAP", "DOL", "WDO", "IND", "WIN"]
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
TIMEZONE_BZ = ZoneInfo("America/Sao_Paulo")
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def futures(
|
|
@@ -73,7 +73,7 @@ def get_old_expiration_date(
|
|
|
73
73
|
expiration_date = pd.Timestamp(year, month, 1)
|
|
74
74
|
# Adjust to the next business day when the date is a weekend or a holiday.
|
|
75
75
|
# Must use old holiday list, since this contract code was used until 2006.
|
|
76
|
-
return bday.offset(dates=expiration_date, offset=0
|
|
76
|
+
return bday.offset(dates=expiration_date, offset=0)
|
|
77
77
|
|
|
78
78
|
except (KeyError, ValueError):
|
|
79
79
|
return pd.NaT # type: ignore
|
|
@@ -10,8 +10,10 @@ from pyield.holidays import BrHolidays
|
|
|
10
10
|
type ArrayIntTypes = np.ndarray | pd.Series | list | tuple
|
|
11
11
|
type ScalarIntTypes = int | np.integer
|
|
12
12
|
|
|
13
|
-
# Initialize
|
|
13
|
+
# Initialize Brazilian holidays data
|
|
14
14
|
br_holidays = BrHolidays()
|
|
15
|
+
OLD_HOLIDAYS_ARRAY = br_holidays.get_holiday_array(holiday_option="old")
|
|
16
|
+
NEW_HOLIDAYS_ARRAY = br_holidays.get_holiday_array(holiday_option="new")
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
@overload
|
|
@@ -19,7 +21,6 @@ def offset(
|
|
|
19
21
|
dates: ScalarDateTypes,
|
|
20
22
|
offset: ScalarIntTypes,
|
|
21
23
|
roll: Literal["forward", "backward"] = ...,
|
|
22
|
-
holiday_list: Literal["old", "new", "infer"] = ...,
|
|
23
24
|
) -> pd.Timestamp: ...
|
|
24
25
|
|
|
25
26
|
|
|
@@ -28,7 +29,6 @@ def offset(
|
|
|
28
29
|
dates: ArrayDateTypes,
|
|
29
30
|
offset: ArrayIntTypes,
|
|
30
31
|
roll: Literal["forward", "backward"] = ...,
|
|
31
|
-
holiday_list: Literal["old", "new", "infer"] = ...,
|
|
32
32
|
) -> pd.Series: ...
|
|
33
33
|
|
|
34
34
|
|
|
@@ -37,7 +37,6 @@ def offset(
|
|
|
37
37
|
dates: ScalarDateTypes,
|
|
38
38
|
offset: ArrayIntTypes,
|
|
39
39
|
roll: Literal["forward", "backward"] = ...,
|
|
40
|
-
holiday_list: Literal["old", "new", "infer"] = ...,
|
|
41
40
|
) -> pd.Series: ...
|
|
42
41
|
|
|
43
42
|
|
|
@@ -46,7 +45,6 @@ def offset(
|
|
|
46
45
|
dates: ArrayDateTypes,
|
|
47
46
|
offset: ScalarIntTypes,
|
|
48
47
|
roll: Literal["forward", "backward"] = ...,
|
|
49
|
-
holiday_list: Literal["old", "new", "infer"] = ...,
|
|
50
48
|
) -> pd.Series: ...
|
|
51
49
|
|
|
52
50
|
|
|
@@ -54,14 +52,13 @@ def offset(
|
|
|
54
52
|
dates: ScalarDateTypes | ArrayDateTypes,
|
|
55
53
|
offset: ScalarIntTypes | ArrayIntTypes,
|
|
56
54
|
roll: Literal["forward", "backward"] = "forward",
|
|
57
|
-
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
58
55
|
) -> pd.Timestamp | pd.Series:
|
|
59
56
|
"""
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
First adjusts the date to fall on a valid day according to the roll rule, then
|
|
58
|
+
applies offsets to the given dates to the next or previous business day, considering
|
|
59
|
+
brazilian holidays. This function supports both single dates and collections of
|
|
60
|
+
dates. It is a wrapper for `numpy.busday_offset` adapted for Pandas data types and
|
|
61
|
+
brazilian holidays.
|
|
65
62
|
|
|
66
63
|
Args:
|
|
67
64
|
dates (ScalarDateTypes | ArrayDateTypes):
|
|
@@ -69,16 +66,12 @@ def offset(
|
|
|
69
66
|
`datetime`, `Timestamp`, etc.) or a collection of dates (list, tuple,
|
|
70
67
|
`Series`, etc.).
|
|
71
68
|
offset (int | Series | np.ndarray | list[int] | tuple[int], optional):
|
|
72
|
-
The number of business days to offset the dates. Positive for
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
The number of business days to offset the dates. Positive for future dates,
|
|
70
|
+
negative for past dates. Zero will return the same date if it's a business
|
|
71
|
+
day, or the next business day otherwise.
|
|
75
72
|
roll (Literal["forward", "backward"], optional): Direction to roll the date if
|
|
76
73
|
it falls on a holiday or weekend. 'forward' to the next business day,
|
|
77
74
|
'backward' to the previous. Defaults to 'forward'.
|
|
78
|
-
holiday_list (Literal["old", "new", "infer"], optional):
|
|
79
|
-
The list of holidays to consider. 'old' or 'new' use predefined lists, while
|
|
80
|
-
'infer' determines the most appropriate list based on the input dates.
|
|
81
|
-
Defaults to "infer".
|
|
82
75
|
|
|
83
76
|
Returns:
|
|
84
77
|
pd.Timestamp | pd.Series: If a single date is provided, returns a single
|
|
@@ -86,7 +79,6 @@ def offset(
|
|
|
86
79
|
`Series` of offset dates.
|
|
87
80
|
|
|
88
81
|
Examples:
|
|
89
|
-
>>> from pandas import Timestamp
|
|
90
82
|
>>> from pyield import bday
|
|
91
83
|
|
|
92
84
|
Offset to the next business day if not a bday (offset=0 and roll="forward")
|
|
@@ -145,36 +137,68 @@ def offset(
|
|
|
145
137
|
This function uses `numpy.busday_offset` under the hood, which means it follows
|
|
146
138
|
the same conventions and limitations for business day calculations. For detailed
|
|
147
139
|
information on error handling and behavior, refer to the `numpy.busday_offset`
|
|
148
|
-
documentation:
|
|
140
|
+
documentation:
|
|
141
|
+
https://numpy.org/doc/stable/reference/generated/numpy.busday_offset.html
|
|
149
142
|
"""
|
|
150
|
-
|
|
143
|
+
dates_pd = dc.convert_input_dates(dates)
|
|
144
|
+
|
|
145
|
+
if isinstance(dates_pd, pd.Series):
|
|
146
|
+
# Divide the input in order to apply the correct holiday list
|
|
147
|
+
dates1 = dates_pd[dates_pd < br_holidays.TRANSITION_DATE]
|
|
148
|
+
dates2 = dates_pd[dates_pd >= br_holidays.TRANSITION_DATE]
|
|
149
|
+
dates3 = dates_pd[dates_pd.isna()]
|
|
150
|
+
|
|
151
|
+
offsetted_dates1 = np.busday_offset(
|
|
152
|
+
dc.to_numpy_date_type(dates1),
|
|
153
|
+
offsets=offset,
|
|
154
|
+
roll=roll,
|
|
155
|
+
holidays=OLD_HOLIDAYS_ARRAY,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
offsetted_dates2 = np.busday_offset(
|
|
159
|
+
dc.to_numpy_date_type(dates2),
|
|
160
|
+
offsets=offset,
|
|
161
|
+
roll=roll,
|
|
162
|
+
holidays=NEW_HOLIDAYS_ARRAY,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Convert from numpy.datetime64 to pandas.Timestamp
|
|
166
|
+
offsetted_dates1 = pd.to_datetime(offsetted_dates1)
|
|
167
|
+
offsetted_dates2 = pd.to_datetime(offsetted_dates2)
|
|
168
|
+
|
|
169
|
+
# 'pd.to_datetime' does not necessarily return datetime64[ns] Series
|
|
170
|
+
offsetted_dates1 = pd.Series(offsetted_dates1).astype("datetime64[ns]")
|
|
171
|
+
offsetted_dates2 = pd.Series(offsetted_dates2).astype("datetime64[ns]")
|
|
172
|
+
|
|
173
|
+
# Use old index to rejoin the results
|
|
174
|
+
offsetted_dates1.index = dates1.index
|
|
175
|
+
offsetted_dates2.index = dates2.index
|
|
176
|
+
|
|
177
|
+
offsetted_dates = pd.concat([offsetted_dates1, offsetted_dates2, dates3])
|
|
178
|
+
|
|
179
|
+
# Reorder the result to match the original input order
|
|
180
|
+
return offsetted_dates.sort_index()
|
|
181
|
+
|
|
182
|
+
elif isinstance(dates_pd, pd.Timestamp):
|
|
183
|
+
offsetted_dates_np = np.busday_offset(
|
|
184
|
+
dc.to_numpy_date_type(dates_pd),
|
|
185
|
+
offsets=offset,
|
|
186
|
+
roll=roll,
|
|
187
|
+
holidays=br_holidays.get_holiday_array(dates_pd),
|
|
188
|
+
)
|
|
189
|
+
if isinstance(offsetted_dates_np, np.datetime64):
|
|
190
|
+
return pd.Timestamp(offsetted_dates_np).as_unit("ns")
|
|
191
|
+
else:
|
|
192
|
+
return pd.Series(offsetted_dates_np).astype("datetime64[ns]")
|
|
151
193
|
|
|
152
|
-
selected_holidays = br_holidays.get_applicable_holidays(
|
|
153
|
-
converted_dates, holiday_list
|
|
154
|
-
)
|
|
155
|
-
selected_holidays_np = dc.convert_to_numpy_date(selected_holidays)
|
|
156
|
-
|
|
157
|
-
dates_np = dc.convert_to_numpy_date(converted_dates)
|
|
158
|
-
offsetted_dates_np = np.busday_offset(
|
|
159
|
-
dates_np, offsets=offset, roll=roll, holidays=selected_holidays_np
|
|
160
|
-
)
|
|
161
|
-
if isinstance(offsetted_dates_np, np.datetime64):
|
|
162
|
-
result = pd.Timestamp(offsetted_dates_np)
|
|
163
|
-
# Force to datetime[ns] if the input was a single date
|
|
164
|
-
result = result.as_unit("ns")
|
|
165
194
|
else:
|
|
166
|
-
|
|
167
|
-
# Force the result to be a Series if the input was not a single date
|
|
168
|
-
result = pd.Series(result_dti).astype("datetime64[ns]")
|
|
169
|
-
|
|
170
|
-
return result
|
|
195
|
+
raise ValueError("Invalid input type for 'dates'.")
|
|
171
196
|
|
|
172
197
|
|
|
173
198
|
@overload
|
|
174
199
|
def count(
|
|
175
200
|
start: ScalarDateTypes,
|
|
176
201
|
end: ScalarDateTypes,
|
|
177
|
-
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
178
202
|
) -> int: ...
|
|
179
203
|
|
|
180
204
|
|
|
@@ -182,7 +206,6 @@ def count(
|
|
|
182
206
|
def count(
|
|
183
207
|
start: ArrayDateTypes,
|
|
184
208
|
end: ScalarDateTypes,
|
|
185
|
-
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
186
209
|
) -> pd.Series: ...
|
|
187
210
|
|
|
188
211
|
|
|
@@ -190,7 +213,6 @@ def count(
|
|
|
190
213
|
def count(
|
|
191
214
|
start: ScalarDateTypes,
|
|
192
215
|
end: ArrayDateTypes,
|
|
193
|
-
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
194
216
|
) -> pd.Series: ...
|
|
195
217
|
|
|
196
218
|
|
|
@@ -198,14 +220,12 @@ def count(
|
|
|
198
220
|
def count(
|
|
199
221
|
start: ArrayDateTypes,
|
|
200
222
|
end: ArrayDateTypes,
|
|
201
|
-
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
202
223
|
) -> pd.Series: ...
|
|
203
224
|
|
|
204
225
|
|
|
205
226
|
def count(
|
|
206
227
|
start: ScalarDateTypes | ArrayDateTypes,
|
|
207
228
|
end: ScalarDateTypes | ArrayDateTypes,
|
|
208
|
-
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
209
229
|
) -> int | pd.Series:
|
|
210
230
|
"""
|
|
211
231
|
Counts the number of business days between a `start` date (inclusive) and an `end`
|
|
@@ -219,7 +239,7 @@ def count(
|
|
|
219
239
|
for counting.
|
|
220
240
|
end (ScalarDateTypes | ArrayDateTypes): The end date(s) for counting, which
|
|
221
241
|
is excluded from the count themselves.
|
|
222
|
-
|
|
242
|
+
holiday_option (Literal["old", "new", "infer"], optional):
|
|
223
243
|
Specifies which set of holidays to consider in the count. 'old' or 'new'
|
|
224
244
|
refer to predefined holiday lists, while 'infer' automatically selects the
|
|
225
245
|
list based on the most recent date in the input. Defaults to "infer".
|
|
@@ -263,31 +283,71 @@ def count(
|
|
|
263
283
|
1 19
|
|
264
284
|
dtype: Int64
|
|
265
285
|
"""
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
#
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
286
|
+
start_pd = dc.convert_input_dates(start)
|
|
287
|
+
end_pd = dc.convert_input_dates(end)
|
|
288
|
+
|
|
289
|
+
# If inputs are Series, check if they have different lengths
|
|
290
|
+
if isinstance(start_pd, pd.Series) and isinstance(end_pd, pd.Series):
|
|
291
|
+
if start_pd.size != end_pd.size:
|
|
292
|
+
raise ValueError("Input Series must have the same length.")
|
|
293
|
+
|
|
294
|
+
# Only start is used to determine the holiday list
|
|
295
|
+
if isinstance(start_pd, pd.Series):
|
|
296
|
+
# Divide the input in order to apply the correct holiday list
|
|
297
|
+
start1 = start_pd[start_pd < br_holidays.TRANSITION_DATE]
|
|
298
|
+
start2 = start_pd[start_pd >= br_holidays.TRANSITION_DATE]
|
|
299
|
+
start3 = start_pd[start_pd.isna()]
|
|
300
|
+
|
|
301
|
+
# If end is a Series, it must be divided as well
|
|
302
|
+
if isinstance(end_pd, pd.Series):
|
|
303
|
+
end1 = end_pd[start1.index]
|
|
304
|
+
end2 = end_pd[start2.index]
|
|
305
|
+
else:
|
|
306
|
+
end1 = end_pd
|
|
307
|
+
end2 = end_pd
|
|
308
|
+
|
|
309
|
+
result1 = np.busday_count(
|
|
310
|
+
begindates=dc.to_numpy_date_type(start1),
|
|
311
|
+
enddates=dc.to_numpy_date_type(end1),
|
|
312
|
+
holidays=OLD_HOLIDAYS_ARRAY,
|
|
313
|
+
)
|
|
314
|
+
result2 = np.busday_count(
|
|
315
|
+
begindates=dc.to_numpy_date_type(start2),
|
|
316
|
+
enddates=dc.to_numpy_date_type(end2),
|
|
317
|
+
holidays=NEW_HOLIDAYS_ARRAY,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Prepare results to be rejoined
|
|
321
|
+
result1 = pd.Series(result1, dtype="Int64")
|
|
322
|
+
result2 = pd.Series(result2, dtype="Int64")
|
|
323
|
+
# Convert the third result from NaT to NA
|
|
324
|
+
result3 = pd.Series(pd.NA, index=start3.index, dtype="Int64")
|
|
325
|
+
|
|
326
|
+
# Old index is used to rejoin the results
|
|
327
|
+
result1.index = start1.index
|
|
328
|
+
result2.index = start2.index
|
|
329
|
+
|
|
330
|
+
# Reorder the result to match the original input order
|
|
331
|
+
result = pd.concat([result1, result2, result3]).sort_index()
|
|
332
|
+
|
|
333
|
+
else: # Start is a single date
|
|
334
|
+
result = np.busday_count(
|
|
335
|
+
begindates=dc.to_numpy_date_type(start_pd),
|
|
336
|
+
enddates=dc.to_numpy_date_type(end_pd),
|
|
337
|
+
holidays=br_holidays.get_holiday_array(start_pd),
|
|
338
|
+
)
|
|
339
|
+
result = pd.Series(result, dtype="Int64")
|
|
340
|
+
|
|
341
|
+
if result.size == 1:
|
|
342
|
+
return int(result[0])
|
|
343
|
+
return result
|
|
284
344
|
|
|
285
345
|
|
|
286
346
|
def generate(
|
|
287
347
|
start: ScalarDateTypes | None = None,
|
|
288
348
|
end: ScalarDateTypes | None = None,
|
|
289
349
|
inclusive: Literal["both", "neither", "left", "right"] = "both",
|
|
290
|
-
|
|
350
|
+
holiday_option: Literal["old", "new", "infer"] = "infer",
|
|
291
351
|
) -> pd.Series:
|
|
292
352
|
"""
|
|
293
353
|
Generates a Series of business days between a `start` and `end` date, considering
|
|
@@ -304,7 +364,7 @@ def generate(
|
|
|
304
364
|
inclusive (Literal["both", "neither", "left", "right"], optional):
|
|
305
365
|
Determines which of the start and end dates are included in the result.
|
|
306
366
|
Valid options are 'both', 'neither', 'left', 'right'. Defaults to 'both'.
|
|
307
|
-
|
|
367
|
+
holiday_option (Literal["old", "new", "infer"], optional):
|
|
308
368
|
Specifies the list of holidays to consider. 'old' or 'new' refer to
|
|
309
369
|
predefined lists, 'infer' selects the list based on the most recent date in
|
|
310
370
|
the range. Defaults to "infer".
|
|
@@ -330,27 +390,24 @@ def generate(
|
|
|
330
390
|
https://pandas.pydata.org/docs/reference/api/pandas.bdate_range.html.
|
|
331
391
|
"""
|
|
332
392
|
if start:
|
|
333
|
-
|
|
393
|
+
start_pd = dc.convert_input_dates(start)
|
|
334
394
|
else:
|
|
335
|
-
|
|
395
|
+
start_pd = pd.Timestamp.today()
|
|
336
396
|
|
|
337
397
|
if end:
|
|
338
|
-
|
|
398
|
+
end_pd = dc.convert_input_dates(end)
|
|
339
399
|
else:
|
|
340
|
-
|
|
400
|
+
end_pd = pd.Timestamp.today()
|
|
341
401
|
|
|
342
|
-
|
|
343
|
-
converted_start, holiday_list
|
|
344
|
-
)
|
|
345
|
-
selected_holidays_list = selected_holidays.to_list()
|
|
402
|
+
holidays_list = br_holidays.get_holiday_series(start_pd).to_list()
|
|
346
403
|
|
|
347
404
|
# Get the result as a DatetimeIndex (dti)
|
|
348
405
|
result_dti = pd.bdate_range(
|
|
349
|
-
start=
|
|
350
|
-
end=
|
|
406
|
+
start=start_pd,
|
|
407
|
+
end=end_pd,
|
|
351
408
|
freq="C",
|
|
352
409
|
inclusive=inclusive,
|
|
353
|
-
holidays=
|
|
410
|
+
holidays=holidays_list,
|
|
354
411
|
)
|
|
355
412
|
return pd.Series(result_dti.values)
|
|
356
413
|
|
|
@@ -370,6 +427,6 @@ def is_business_day(date: ScalarDateTypes) -> bool:
|
|
|
370
427
|
>>> bday.is_business_day("25-12-2023") # Christmas
|
|
371
428
|
False
|
|
372
429
|
"""
|
|
373
|
-
|
|
374
|
-
shifted_date = offset(
|
|
375
|
-
return
|
|
430
|
+
date_pd = dc.convert_input_dates(date)
|
|
431
|
+
shifted_date = offset(date_pd, 0) # Shift the date if it is not a bus. day
|
|
432
|
+
return date_pd == shifted_date
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from datetime import datetime as dt
|
|
2
|
+
from zoneinfo import ZoneInfo
|
|
2
3
|
|
|
3
4
|
import pandas as pd
|
|
4
|
-
from pytz import timezone
|
|
5
5
|
|
|
6
6
|
from pyield import bday
|
|
7
7
|
|
|
8
|
-
TIMEZONE_BZ =
|
|
8
|
+
TIMEZONE_BZ = ZoneInfo("America/Sao_Paulo")
|
|
9
|
+
|
|
9
10
|
UPDATE_HOUR = 21
|
|
10
11
|
GIT_URL = "https://raw.githubusercontent.com/crdcj/pyield-data/main"
|
|
11
12
|
|
|
@@ -64,9 +65,9 @@ _data_cache = DataCache()
|
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
# Funções de acesso para os módulos
|
|
67
|
-
def
|
|
68
|
+
def get_di_dataset():
|
|
68
69
|
return _data_cache.get_dataframe("di")
|
|
69
70
|
|
|
70
71
|
|
|
71
|
-
def
|
|
72
|
+
def get_anbima_dataset():
|
|
72
73
|
return _data_cache.get_dataframe("anbima")
|
|
@@ -60,7 +60,7 @@ def convert_input_dates(
|
|
|
60
60
|
raise ValueError("Invalid input type for 'dates'.")
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
def
|
|
63
|
+
def to_numpy_date_type(
|
|
64
64
|
dates: pd.Timestamp | pd.Series,
|
|
65
65
|
) -> np.datetime64 | np.ndarray:
|
|
66
66
|
"""
|
|
@@ -74,5 +74,7 @@ def convert_to_numpy_date(
|
|
|
74
74
|
"""
|
|
75
75
|
if isinstance(dates, pd.Timestamp):
|
|
76
76
|
return np.datetime64(dates, "D")
|
|
77
|
-
|
|
77
|
+
elif isinstance(dates, pd.Series):
|
|
78
78
|
return dates.to_numpy().astype("datetime64[D]")
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError("Invalid input type for 'dates'.")
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from pyield import bday, interpolator
|
|
4
|
+
from pyield import date_converter as dc
|
|
5
|
+
from pyield.b3_futures import futures
|
|
6
|
+
from pyield.data_cache import get_anbima_dataset, get_di_dataset
|
|
7
|
+
from pyield.tools import forward_rates
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DIFutures:
|
|
11
|
+
trade_dates = (
|
|
12
|
+
get_di_dataset()
|
|
13
|
+
.drop_duplicates(subset=["TradeDate"])["TradeDate"]
|
|
14
|
+
.sort_values(ascending=True)
|
|
15
|
+
.reset_index(drop=True)
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
pd.Series: Sorted series of unique trade dates available in the DI dataset.
|
|
19
|
+
It does not include the current date. It is updated when `reload_data`
|
|
20
|
+
is called and can be used to check for available historical data.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
trade_date: str | pd.Timestamp,
|
|
26
|
+
adj_expirations: bool = False,
|
|
27
|
+
prefixed_filter: bool = False,
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Initialize the DIFutures instance with a specific trade date.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
trade_date (str | pd.Timestamp): The trade date for which DI data is
|
|
34
|
+
required.
|
|
35
|
+
adj_expirations (bool): If True, adjusts the expiration dates to
|
|
36
|
+
the start of the month.
|
|
37
|
+
prefixed_filter (bool): If True, filters the DI
|
|
38
|
+
contracts to match LTN and NTN-F bond maturities.
|
|
39
|
+
"""
|
|
40
|
+
self._trade_date = dc.convert_input_dates(trade_date)
|
|
41
|
+
self.adj_expirations = adj_expirations
|
|
42
|
+
self.prefixed_filter = prefixed_filter
|
|
43
|
+
|
|
44
|
+
def data(self) -> pd.DataFrame:
|
|
45
|
+
"""Retrieve DI contract data for the initialized trade date."""
|
|
46
|
+
df = (
|
|
47
|
+
get_di_dataset()
|
|
48
|
+
.query("TradeDate == @self.trade_date")
|
|
49
|
+
.reset_index(drop=True)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if df.empty:
|
|
53
|
+
df = futures(contract_code="DI1", trade_date=self._trade_date)
|
|
54
|
+
if df.empty:
|
|
55
|
+
return pd.DataFrame()
|
|
56
|
+
|
|
57
|
+
df.drop(columns=["TradeDate"], inplace=True)
|
|
58
|
+
if "DaysToExpiration" in df.columns:
|
|
59
|
+
df.drop(columns=["DaysToExpiration"], inplace=True)
|
|
60
|
+
|
|
61
|
+
if self.prefixed_filter:
|
|
62
|
+
df_pre = (
|
|
63
|
+
get_anbima_dataset()
|
|
64
|
+
.query("ReferenceDate == @self.trade_date")
|
|
65
|
+
.query("BondType in ['LTN', 'NTN-F']")
|
|
66
|
+
.drop_duplicates(ignore_index=True)
|
|
67
|
+
)
|
|
68
|
+
pre_maturities = df_pre["MaturityDate"]
|
|
69
|
+
adj_pre_maturities = bday.offset(pre_maturities, 0) # noqa
|
|
70
|
+
df.query("ExpirationDate in @adj_pre_maturities", inplace=True)
|
|
71
|
+
|
|
72
|
+
if self.adj_expirations:
|
|
73
|
+
df["ExpirationDate"] = (
|
|
74
|
+
df["ExpirationDate"].dt.to_period("M").dt.to_timestamp()
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return df.sort_values(["ExpirationDate"], ignore_index=True)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def expirations(self) -> pd.Series:
|
|
81
|
+
"""
|
|
82
|
+
Get the unique expiration dates for DI contracts on the initialized trade date.
|
|
83
|
+
|
|
84
|
+
This property returns the unique expiration dates for DI contracts
|
|
85
|
+
based on the instance's trade date and applied filters.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
pd.Series: A Series of unique expiration dates for DI contracts.
|
|
89
|
+
"""
|
|
90
|
+
df = self.data().drop_duplicates(subset=["ExpirationDate"])
|
|
91
|
+
return df["ExpirationDate"].sort_values(ignore_index=True)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def forwards(self) -> pd.DataFrame:
|
|
95
|
+
"""
|
|
96
|
+
Calculate the DI forward rates for the initialized trade date.
|
|
97
|
+
|
|
98
|
+
This property returns a DataFrame with both the SettlementRate and the
|
|
99
|
+
calculated ForwardRate for DI contracts, based on the instance's trade date and
|
|
100
|
+
applied filters.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
pd.DataFrame: A DataFrame containing the following columns:
|
|
104
|
+
- ExpirationDate: The expiration dates of the DI contracts.
|
|
105
|
+
- SettlementRate: The zero rate (interest rate) for each expiration
|
|
106
|
+
date.
|
|
107
|
+
- ForwardRate: The forward rate calculated between successive expiration
|
|
108
|
+
dates.
|
|
109
|
+
"""
|
|
110
|
+
df = self.data()
|
|
111
|
+
if df.empty:
|
|
112
|
+
return pd.DataFrame()
|
|
113
|
+
|
|
114
|
+
df["ForwardRate"] = forward_rates(
|
|
115
|
+
business_days=bday.count(self._trade_date, df["ExpirationDate"]),
|
|
116
|
+
zero_rates=df["SettlementRate"],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return df[["ExpirationDate", "SettlementRate", "ForwardRate"]]
|
|
120
|
+
|
|
121
|
+
def rate(
|
|
122
|
+
self,
|
|
123
|
+
expiration: str | pd.Timestamp,
|
|
124
|
+
interpolate: bool = True,
|
|
125
|
+
extrapolate: bool = False,
|
|
126
|
+
) -> float:
|
|
127
|
+
"""Retrieve the DI rate for a specified expiration date."""
|
|
128
|
+
expiration = bday.offset(expiration, 0)
|
|
129
|
+
|
|
130
|
+
if not interpolate and extrapolate:
|
|
131
|
+
raise ValueError("Extrapolation is not allowed without interpolation.")
|
|
132
|
+
|
|
133
|
+
df = self.data()
|
|
134
|
+
if df.empty:
|
|
135
|
+
return float("NaN")
|
|
136
|
+
|
|
137
|
+
df_exp = df.query("ExpirationDate == @expiration")
|
|
138
|
+
if df_exp.empty and not interpolate:
|
|
139
|
+
return float("NaN")
|
|
140
|
+
|
|
141
|
+
if expiration in df_exp["ExpirationDate"].values:
|
|
142
|
+
return float(df_exp["SettlementRate"].iloc[0])
|
|
143
|
+
|
|
144
|
+
if not interpolate:
|
|
145
|
+
return float("NaN")
|
|
146
|
+
|
|
147
|
+
ff_interpolator = interpolator.Interpolator(
|
|
148
|
+
method="flat_forward",
|
|
149
|
+
known_bdays=bday.count(self._trade_date, df["ExpirationDate"]),
|
|
150
|
+
known_rates=df["SettlementRate"],
|
|
151
|
+
extrapolate=extrapolate,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return ff_interpolator(bday.count(self._trade_date, expiration))
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def trade_date(self) -> pd.Timestamp:
|
|
158
|
+
"""
|
|
159
|
+
Get or set the trade date for this DIFutures instance.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
pd.Timestamp: The trade date set for this instance.
|
|
163
|
+
"""
|
|
164
|
+
return self._trade_date
|
|
165
|
+
|
|
166
|
+
@trade_date.setter
|
|
167
|
+
def trade_date(self, value: str | pd.Timestamp):
|
|
168
|
+
self._trade_date = dc.convert_input_dates(value)
|
|
169
|
+
return self._trade_date
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def adj_expirations(self) -> bool:
|
|
173
|
+
"""
|
|
174
|
+
Get or set the adj_expirations flag for this DIFutures instance.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
bool: Whether expiration dates are adjusted to the start of the month.
|
|
178
|
+
"""
|
|
179
|
+
return self._adj_expirations
|
|
180
|
+
|
|
181
|
+
@adj_expirations.setter
|
|
182
|
+
def adj_expirations(self, value: bool):
|
|
183
|
+
self._adj_expirations = value
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def prefixed_filter(self) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Get or set the prefixed_filter flag for this DIFutures instance.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
bool: Whether DI Futures are filtered to match prefixed Anbima bond
|
|
192
|
+
maturities.
|
|
193
|
+
"""
|
|
194
|
+
return self._prefixed_filter
|
|
195
|
+
|
|
196
|
+
@prefixed_filter.setter
|
|
197
|
+
def prefixed_filter(self, value: bool):
|
|
198
|
+
self._prefixed_filter = value
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BrHolidays:
|
|
9
|
+
# The date (inclusive) when the new holidays list starts to be valid
|
|
10
|
+
TRANSITION_DATE = pd.to_datetime("2023-12-26")
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
current_dir = Path(__file__).parent
|
|
14
|
+
new_holidays_path = current_dir / "br_holidays_new.txt"
|
|
15
|
+
old_holidays_path = current_dir / "br_holidays_old.txt"
|
|
16
|
+
self.new_holidays = self._load_holidays(new_holidays_path)
|
|
17
|
+
self.old_holidays = self._load_holidays(old_holidays_path)
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def _load_holidays(file_path: Path) -> pd.Series:
|
|
21
|
+
"""Loads the holidays from a file and returns it as a Series of Timestamps."""
|
|
22
|
+
df = pd.read_csv(file_path, header=None, names=["date"], comment="#")
|
|
23
|
+
holidays = pd.to_datetime(df["date"], format="%d/%m/%Y")
|
|
24
|
+
return holidays.astype("datetime64[ns]")
|
|
25
|
+
|
|
26
|
+
def get_holiday_series(
|
|
27
|
+
self,
|
|
28
|
+
dates: pd.Timestamp | pd.Series | None = None,
|
|
29
|
+
holiday_option: Literal["old", "new", "infer"] = "infer",
|
|
30
|
+
) -> pd.Series:
|
|
31
|
+
"""
|
|
32
|
+
Returns the correct list of holidays to use based on the most recent date in
|
|
33
|
+
the input.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
dates (pd.Timestamp | pd.Series | None): The dates to use for inferring
|
|
37
|
+
the holiday list. If a single date is provided, it is used directly.
|
|
38
|
+
If a Series of dates is provided, the earliest date is used.
|
|
39
|
+
|
|
40
|
+
holiday_option (Literal): The holidays list to use. Valid options are
|
|
41
|
+
'old', 'new' or 'infer'. If 'infer' is used, the list of holidays is
|
|
42
|
+
selected based on the earliest (minimum) date in the input.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
pd.Series: The list of holidays as a Series of Timestamps.
|
|
46
|
+
"""
|
|
47
|
+
match holiday_option:
|
|
48
|
+
case "old":
|
|
49
|
+
holidays = self.old_holidays
|
|
50
|
+
|
|
51
|
+
case "new":
|
|
52
|
+
return self.new_holidays
|
|
53
|
+
|
|
54
|
+
case "infer":
|
|
55
|
+
if dates is None:
|
|
56
|
+
msg = "Dates must be provided when using 'infer' option."
|
|
57
|
+
raise ValueError(msg)
|
|
58
|
+
if isinstance(dates, pd.Timestamp):
|
|
59
|
+
earliest_date = dates
|
|
60
|
+
else:
|
|
61
|
+
earliest_date = dates.min()
|
|
62
|
+
|
|
63
|
+
if earliest_date < BrHolidays.TRANSITION_DATE:
|
|
64
|
+
holidays = self.old_holidays
|
|
65
|
+
else:
|
|
66
|
+
holidays = self.new_holidays
|
|
67
|
+
case _:
|
|
68
|
+
raise ValueError("Invalid holiday list option.")
|
|
69
|
+
|
|
70
|
+
return holidays
|
|
71
|
+
|
|
72
|
+
def get_holiday_array(
|
|
73
|
+
self,
|
|
74
|
+
dates: pd.Timestamp | pd.Series | None = None,
|
|
75
|
+
holiday_option: Literal["old", "new", "infer"] = "infer",
|
|
76
|
+
) -> np.ndarray:
|
|
77
|
+
"""
|
|
78
|
+
Returns the correct list of holidays to use based on the most recent date in
|
|
79
|
+
the input.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
dates (pd.Timestamp | pd.Series | None): The dates to use for inferring
|
|
83
|
+
the holiday list. If a single date is provided, it is used directly.
|
|
84
|
+
If a Series of dates is provided, the earliest date is used.
|
|
85
|
+
|
|
86
|
+
holiday_option (Literal): The holidays list to use. Valid options are
|
|
87
|
+
'old', 'new' or 'infer'. If 'infer' is used, the list of holidays is
|
|
88
|
+
selected based on the earliest (minimum) date in the input.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
np.ndarray: The list of holidays as a NumPy array of datetime64[D].
|
|
92
|
+
"""
|
|
93
|
+
holidays = self.get_holiday_series(dates, holiday_option)
|
|
94
|
+
return holidays.to_numpy().astype("datetime64[D]")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.20.2"
|
pyield-0.20.2/pyield/di.py
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import pandas as pd
|
|
2
|
-
|
|
3
|
-
from pyield import bday, interpolator
|
|
4
|
-
from pyield import date_converter as dc
|
|
5
|
-
from pyield.b3_futures import futures
|
|
6
|
-
from pyield.data_cache import get_anbima_dataframe, get_di_dataframe
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def data(
|
|
10
|
-
trade_date: str | pd.Timestamp,
|
|
11
|
-
adj_expirations: bool = False,
|
|
12
|
-
prefixed_filter: bool = False,
|
|
13
|
-
) -> pd.DataFrame:
|
|
14
|
-
"""Retrieve DI contract data for a specified trade date.
|
|
15
|
-
|
|
16
|
-
This function retrieves DI contract data for the given trade date. If the historical
|
|
17
|
-
data is not available, it attempts to fetch the data from the B3 website. The
|
|
18
|
-
function can also filter and adjust expiration dates based on the provided options.
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
trade_date (str | pd.Timestamp): The trade date for which the DI data is
|
|
22
|
-
required.
|
|
23
|
-
adj_expirations (bool): If True, adjusts the expiration dates to the
|
|
24
|
-
start of the month.
|
|
25
|
-
prefixed_filter (bool): If True, filters the DI contracts to match prefixed
|
|
26
|
-
Anbima bond maturities.
|
|
27
|
-
|
|
28
|
-
Returns:
|
|
29
|
-
pd.DataFrame: DataFrame containing DI contract data, with columns for
|
|
30
|
-
expiration dates and selected rates. The DataFrame is sorted by the
|
|
31
|
-
expiration date.
|
|
32
|
-
"""
|
|
33
|
-
trade_date = dc.convert_input_dates(trade_date)
|
|
34
|
-
df = get_di_dataframe()
|
|
35
|
-
df.query("TradeDate == @trade_date", inplace=True)
|
|
36
|
-
|
|
37
|
-
if df.empty:
|
|
38
|
-
# There is no historical data for date provided.
|
|
39
|
-
# Let's try to fetch the data from the B3 website.
|
|
40
|
-
df = futures(contract_code="DI1", trade_date=trade_date)
|
|
41
|
-
if df.empty:
|
|
42
|
-
# If it is still empty, return an empty DataFrame.
|
|
43
|
-
return pd.DataFrame()
|
|
44
|
-
|
|
45
|
-
df.drop(columns=["TradeDate"], inplace=True)
|
|
46
|
-
if "DaysToExpiration" in df.columns:
|
|
47
|
-
df.drop(columns=["DaysToExpiration"], inplace=True)
|
|
48
|
-
|
|
49
|
-
if prefixed_filter:
|
|
50
|
-
df_pre = (
|
|
51
|
-
get_anbima_dataframe()
|
|
52
|
-
.query("ReferenceDate == @trade_date")
|
|
53
|
-
.query("BondType in ['LTN', 'NTN-F']")
|
|
54
|
-
.drop_duplicates(ignore_index=True)
|
|
55
|
-
)
|
|
56
|
-
pre_maturities = df_pre["MaturityDate"]
|
|
57
|
-
adj_pre_maturities = bday.offset(pre_maturities, 0) # noqa
|
|
58
|
-
df.query("ExpirationDate in @adj_pre_maturities", inplace=True)
|
|
59
|
-
|
|
60
|
-
if adj_expirations:
|
|
61
|
-
df["ExpirationDate"] = df["ExpirationDate"].dt.to_period("M")
|
|
62
|
-
df["ExpirationDate"] = df["ExpirationDate"].dt.to_timestamp()
|
|
63
|
-
|
|
64
|
-
return df.sort_values(["ExpirationDate"], ignore_index=True)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def expirations(
|
|
68
|
-
trade_date: str | pd.Timestamp,
|
|
69
|
-
adj_expirations: bool = False,
|
|
70
|
-
prefixed_filter: bool = False,
|
|
71
|
-
) -> pd.Series:
|
|
72
|
-
"""Retrieve unique expiration dates for DI contracts on a specified trade date.
|
|
73
|
-
|
|
74
|
-
This function returns a Series of unique expiration dates for DI contracts
|
|
75
|
-
for the given trade date. The expiration dates can be adjusted or filtered
|
|
76
|
-
based on the provided options.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
trade_date (str | pd.Timestamp): The trade date for which expiration dates are
|
|
80
|
-
required.
|
|
81
|
-
adj_expirations (bool): If True, adjusts the expiration dates to the start of
|
|
82
|
-
the month.
|
|
83
|
-
prefixed_filter (bool): If True, filters the DI contracts to match prefixed
|
|
84
|
-
Anbima bond maturities.
|
|
85
|
-
|
|
86
|
-
Returns:
|
|
87
|
-
pd.Series: A Series of unique expiration dates for DI contracts.
|
|
88
|
-
"""
|
|
89
|
-
trade_date = dc.convert_input_dates(trade_date)
|
|
90
|
-
df = data(trade_date, adj_expirations, prefixed_filter)
|
|
91
|
-
df = df.drop_duplicates(subset=["ExpirationDate"], ignore_index=True)
|
|
92
|
-
return df["ExpirationDate"]
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def rate(
|
|
96
|
-
trade_date: str | pd.Timestamp,
|
|
97
|
-
expiration: str | pd.Timestamp,
|
|
98
|
-
interpolate: bool = True,
|
|
99
|
-
extrapolate: bool = False,
|
|
100
|
-
) -> float:
|
|
101
|
-
"""Retrieve the DI rate for a specified trade date and expiration date.
|
|
102
|
-
|
|
103
|
-
This function returns the DI rate for the given trade date and expiration date.
|
|
104
|
-
The function uses the SettlementRate column from the DI contract data and
|
|
105
|
-
interpolates the rate using the flat forward method if required.
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
trade_date (str | pd.Timestamp): The trade date for which the DI rate is
|
|
109
|
-
required.
|
|
110
|
-
expiration (str | pd.Timestamp): The expiration date for the DI contract.
|
|
111
|
-
interpolate (bool): If True, interpolates the rate for the provided expiration
|
|
112
|
-
date.
|
|
113
|
-
extrapolate (bool): If True, extrapolates the rate for expiration dates beyond
|
|
114
|
-
the available data.
|
|
115
|
-
|
|
116
|
-
Returns:
|
|
117
|
-
float: The DI rate for the specified trade date and expiration date, or NaN
|
|
118
|
-
if the rate cannot be determined.
|
|
119
|
-
"""
|
|
120
|
-
# Convert input dates to consistent format
|
|
121
|
-
trade_date = dc.convert_input_dates(trade_date)
|
|
122
|
-
# Adjust expiration date to the nearest business day
|
|
123
|
-
expiration = bday.offset(expiration, 0)
|
|
124
|
-
|
|
125
|
-
# Return an error if interpolation is not allowed and extrapolation is required
|
|
126
|
-
if not interpolate and extrapolate:
|
|
127
|
-
raise ValueError("Extrapolation is not allowed without interpolation.")
|
|
128
|
-
|
|
129
|
-
# Retrieve the data for the given trade date
|
|
130
|
-
df = data(trade_date)
|
|
131
|
-
|
|
132
|
-
# Return NaN if no data is found for the trade date
|
|
133
|
-
if df.empty:
|
|
134
|
-
return float("NaN")
|
|
135
|
-
|
|
136
|
-
# Filter data for the specified expiration date
|
|
137
|
-
df_exp = df.query("ExpirationDate == @expiration")
|
|
138
|
-
|
|
139
|
-
# Return NaN if no exact match is found and interpolation is not allowed
|
|
140
|
-
if df_exp.empty and not interpolate:
|
|
141
|
-
return float("NaN")
|
|
142
|
-
|
|
143
|
-
if expiration in df_exp["ExpirationDate"]:
|
|
144
|
-
# Return the rate if an exact match is found
|
|
145
|
-
return float(df_exp["SettlementRate"].iloc[0])
|
|
146
|
-
|
|
147
|
-
if not interpolate:
|
|
148
|
-
# Return NaN if no exact match is found and interpolation is not allowed
|
|
149
|
-
return float("NaN")
|
|
150
|
-
|
|
151
|
-
# Perform flat forward interpolation if required
|
|
152
|
-
ff_interpolator = interpolator.Interpolator(
|
|
153
|
-
method="flat_forward",
|
|
154
|
-
known_bdays=bday.count(trade_date, df["ExpirationDate"]),
|
|
155
|
-
known_rates=df["SettlementRate"],
|
|
156
|
-
extrapolate=extrapolate,
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
# Return the interpolated rate for the calculated business days
|
|
160
|
-
return ff_interpolator(bday.count(trade_date, expiration))
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def trade_dates(
|
|
164
|
-
start_date: str | pd.Timestamp | None = None,
|
|
165
|
-
end_date: str | pd.Timestamp | None = None,
|
|
166
|
-
) -> pd.Series:
|
|
167
|
-
"""Retrieve unique trade dates for DI contracts within a specified date range.
|
|
168
|
-
|
|
169
|
-
This function returns a Series of unique trade dates for DI contracts within the
|
|
170
|
-
given date range.
|
|
171
|
-
|
|
172
|
-
Args:
|
|
173
|
-
start_date (str | pd.Timestamp): The start date of the date range. If None,
|
|
174
|
-
the earliest available date is used.
|
|
175
|
-
end_date (str | pd.Timestamp): The end date of the date range. If None, the
|
|
176
|
-
latest available date is used.
|
|
177
|
-
|
|
178
|
-
Returns:
|
|
179
|
-
pd.Series: A Series of unique trade dates for DI contracts.
|
|
180
|
-
"""
|
|
181
|
-
df = get_di_dataframe()
|
|
182
|
-
if start_date:
|
|
183
|
-
start_date = dc.convert_input_dates(start_date)
|
|
184
|
-
df = df.query("TradeDate >= @start_date")
|
|
185
|
-
if end_date:
|
|
186
|
-
end_date = dc.convert_input_dates(end_date)
|
|
187
|
-
df = df.query("TradeDate <= @end_date")
|
|
188
|
-
|
|
189
|
-
df = df.drop_duplicates(subset=["TradeDate"], ignore_index=True)
|
|
190
|
-
return df["TradeDate"]
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing import Literal
|
|
3
|
-
|
|
4
|
-
import pandas as pd
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class BrHolidays:
|
|
8
|
-
# The date (inclusive) when the new holidays list starts to be valid
|
|
9
|
-
TRANSITION_DATE = pd.to_datetime("2023-12-26")
|
|
10
|
-
|
|
11
|
-
def __init__(self):
|
|
12
|
-
current_dir = Path(__file__).parent
|
|
13
|
-
new_holidays_path = current_dir / "br_holidays_new.txt"
|
|
14
|
-
old_holidays_path = current_dir / "br_holidays_old.txt"
|
|
15
|
-
self.new_holidays = self._load_holidays(new_holidays_path)
|
|
16
|
-
self.old_holidays = self._load_holidays(old_holidays_path)
|
|
17
|
-
|
|
18
|
-
@staticmethod
|
|
19
|
-
def _load_holidays(file_path: Path) -> pd.Series:
|
|
20
|
-
"""
|
|
21
|
-
Loads the holidays from a text file and returns it as a Series of Timestamps.
|
|
22
|
-
"""
|
|
23
|
-
df = pd.read_csv(file_path, header=None, names=["date"], comment="#")
|
|
24
|
-
# Convert the dates to a pd.Series of pd.Timestamp
|
|
25
|
-
holidays = pd.to_datetime(df["date"], format="%d/%m/%Y")
|
|
26
|
-
return holidays.astype("datetime64[ns]")
|
|
27
|
-
|
|
28
|
-
def get_applicable_holidays(
|
|
29
|
-
self,
|
|
30
|
-
dates: pd.Timestamp | pd.Series,
|
|
31
|
-
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
32
|
-
) -> pd.Series:
|
|
33
|
-
"""
|
|
34
|
-
Returns the correct list of holidays to use based on the most recent date in
|
|
35
|
-
the input.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
dates (pd.Timestamp | pd.Series): The date(s) to use to infer the holidays.
|
|
39
|
-
holiday_list (str): The holidays list to use. Valid options are 'old', 'new'
|
|
40
|
-
or 'infer'. If 'infer' is used, the list of holidays is selected based
|
|
41
|
-
on the earliest (minimum) date in the input.
|
|
42
|
-
|
|
43
|
-
Returns:
|
|
44
|
-
pd.Series: The list of holidays to use.
|
|
45
|
-
"""
|
|
46
|
-
if isinstance(dates, pd.Timestamp):
|
|
47
|
-
earliest_date = dates
|
|
48
|
-
else:
|
|
49
|
-
earliest_date = dates.min()
|
|
50
|
-
|
|
51
|
-
match holiday_list:
|
|
52
|
-
case "old":
|
|
53
|
-
return self.old_holidays
|
|
54
|
-
case "new":
|
|
55
|
-
return self.new_holidays
|
|
56
|
-
case "infer":
|
|
57
|
-
if earliest_date < BrHolidays.TRANSITION_DATE:
|
|
58
|
-
return self.old_holidays
|
|
59
|
-
else:
|
|
60
|
-
return self.new_holidays
|
|
61
|
-
case _:
|
|
62
|
-
raise ValueError("Invalid holiday list option.")
|
|
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
|