PYield 0.3.6__tar.gz → 0.4.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.3.6 → pyield-0.4.0}/PKG-INFO +1 -1
- pyield-0.4.0/pyield/__about__.py +1 -0
- pyield-0.4.0/pyield/__init__.py +16 -0
- {pyield-0.3.6 → pyield-0.4.0}/pyield/anbima.py +36 -36
- {pyield-0.3.6 → pyield-0.4.0}/pyield/br_calendar.py +152 -49
- {pyield-0.3.6 → pyield-0.4.0}/pyield/br_holidays.py +5 -4
- {pyield-0.3.6 → pyield-0.4.0}/pyield/di_futures.py +42 -17
- {pyield-0.3.6 → pyield-0.4.0}/pyield/di_web.py +8 -7
- {pyield-0.3.6 → pyield-0.4.0}/pyield/di_xml.py +11 -12
- {pyield-0.3.6 → pyield-0.4.0}/pyproject.toml +1 -1
- {pyield-0.3.6 → pyield-0.4.0}/tests/test_di.py +5 -3
- pyield-0.3.6/pyield/__about__.py +0 -1
- pyield-0.3.6/pyield/__init__.py +0 -4
- {pyield-0.3.6 → pyield-0.4.0}/LICENSE +0 -0
- {pyield-0.3.6 → pyield-0.4.0}/README.md +0 -0
- {pyield-0.3.6 → pyield-0.4.0}/pyield/br_holidays_new.txt +0 -0
- {pyield-0.3.6 → pyield-0.4.0}/pyield/br_holidays_old.txt +0 -0
- {pyield-0.3.6 → pyield-0.4.0}/pyield/py.typed +0 -0
- {pyield-0.3.6 → pyield-0.4.0}/tests/__init__.py +0 -0
- {pyield-0.3.6 → pyield-0.4.0}/tests/test_br_calendar.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .__about__ import __version__
|
|
2
|
+
from .di_futures import get_di, read_di
|
|
3
|
+
from .br_calendar import count_bdays, generate_bdays, offset_bdays, is_business_day
|
|
4
|
+
from .anbima import get_treasury_rates, calculate_treasury_di_spreads
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"__version__",
|
|
8
|
+
"get_di",
|
|
9
|
+
"read_di",
|
|
10
|
+
"count_bdays",
|
|
11
|
+
"generate_bdays",
|
|
12
|
+
"offset_bdays",
|
|
13
|
+
"is_business_day",
|
|
14
|
+
"get_treasury_rates",
|
|
15
|
+
"calculate_treasury_di_spreads",
|
|
16
|
+
]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pandas as pd
|
|
2
|
+
from pandas import Timestamp, DataFrame
|
|
2
3
|
from urllib.error import HTTPError
|
|
3
4
|
|
|
4
5
|
from . import di_futures as di
|
|
@@ -12,31 +13,31 @@ ANBIMA_MEMBER_URL = "http://www.anbima.associados.rtm/merc_sec/arqs/"
|
|
|
12
13
|
BP_CONVERSION_FACTOR = 10_000
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
def
|
|
16
|
-
reference_date
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
def normalize_date(reference_date: str | Timestamp | None = None) -> Timestamp:
|
|
17
|
+
if isinstance(reference_date, str):
|
|
18
|
+
normalized_date = pd.Timestamp(reference_date).normalize()
|
|
19
|
+
elif isinstance(reference_date, Timestamp):
|
|
20
|
+
normalized_date = reference_date.normalize()
|
|
21
|
+
elif reference_date is None:
|
|
22
|
+
today = pd.Timestamp.today().normalize()
|
|
23
|
+
normalized_date = cl.offset_bdays(today, -1)
|
|
24
|
+
else:
|
|
25
|
+
raise ValueError("Invalid date format.")
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
# Raise an error if the reference date is in the future
|
|
28
|
+
if normalized_date > pd.Timestamp.today().normalize():
|
|
29
|
+
raise ValueError("Reference date cannot be in the future.")
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if reference_date: # Force reference_date to be a pd.Timestamp
|
|
29
|
-
processed_date = pd.Timestamp(reference_date)
|
|
30
|
-
else: # If no reference_date is given, use the previous business day
|
|
31
|
-
today = pd.Timestamp.today().normalize()
|
|
32
|
-
processed_date = cl.offset_bdays(today, -1)
|
|
31
|
+
# Raise error if the reference date is not a business day
|
|
32
|
+
if not cl.is_business_day(normalized_date):
|
|
33
|
+
raise ValueError("Reference date must be a business day.")
|
|
33
34
|
|
|
34
|
-
return
|
|
35
|
+
return normalized_date
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def get_raw_data(
|
|
38
|
-
reference_date: str |
|
|
39
|
-
) ->
|
|
39
|
+
reference_date: str | Timestamp | None = None, is_anbima_member: bool = False
|
|
40
|
+
) -> DataFrame:
|
|
40
41
|
"""
|
|
41
42
|
Fetch indicative rates from ANBIMA for a specific date.
|
|
42
43
|
|
|
@@ -48,10 +49,10 @@ def get_raw_data(
|
|
|
48
49
|
- pd.DataFrame: DataFrame with the indicative rates for the given date.
|
|
49
50
|
"""
|
|
50
51
|
# Process the reference date, defaulting to the previous business day if not provided
|
|
51
|
-
|
|
52
|
+
normalized_date = normalize_date(reference_date)
|
|
52
53
|
|
|
53
54
|
# Format the date to match the URL format
|
|
54
|
-
url_date =
|
|
55
|
+
url_date = normalized_date.strftime("%y%m%d")
|
|
55
56
|
|
|
56
57
|
# Set the base URL according to the member status
|
|
57
58
|
base_url = ANBIMA_MEMBER_URL if is_anbima_member else ANBIMA_NON_MEMBER_URL
|
|
@@ -69,13 +70,13 @@ def get_raw_data(
|
|
|
69
70
|
dtype_backend="numpy_nullable",
|
|
70
71
|
)
|
|
71
72
|
except HTTPError:
|
|
72
|
-
error_date =
|
|
73
|
+
error_date = normalized_date.strftime("%d-%m-%Y")
|
|
73
74
|
raise ValueError(f"Failed to get ANBIMA rates for {error_date}")
|
|
74
75
|
|
|
75
76
|
return df
|
|
76
77
|
|
|
77
78
|
|
|
78
|
-
def process_raw_data(
|
|
79
|
+
def process_raw_data(df_raw: DataFrame) -> DataFrame:
|
|
79
80
|
"""
|
|
80
81
|
Process raw data from ANBIMA by filtering selected columns, renaming them, and adjusting data formats.
|
|
81
82
|
|
|
@@ -104,8 +105,8 @@ def process_raw_data(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
104
105
|
# "Criterio": "Criteria",
|
|
105
106
|
}
|
|
106
107
|
select_columns = list(selected_columns_dict.keys())
|
|
107
|
-
df =
|
|
108
|
-
df.rename(columns=selected_columns_dict
|
|
108
|
+
df = df_raw[select_columns].copy()
|
|
109
|
+
df = df.rename(columns=selected_columns_dict)
|
|
109
110
|
|
|
110
111
|
# Remove percentage from rates
|
|
111
112
|
rate_cols = ["BidRate", "AskRate", "IndicativeRate"]
|
|
@@ -118,10 +119,10 @@ def process_raw_data(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
118
119
|
|
|
119
120
|
|
|
120
121
|
def get_treasury_rates(
|
|
121
|
-
reference_date: str |
|
|
122
|
+
reference_date: str | Timestamp | None = None,
|
|
122
123
|
return_raw=False,
|
|
123
124
|
is_anbima_member=False,
|
|
124
|
-
) ->
|
|
125
|
+
) -> DataFrame:
|
|
125
126
|
"""
|
|
126
127
|
Fetch and process indicative rates from ANBIMA for a specific date.
|
|
127
128
|
If no date is provided, the previous business day based on the Brazilian calendar
|
|
@@ -137,7 +138,8 @@ def get_treasury_rates(
|
|
|
137
138
|
- pd.DataFrame: DataFrame with the indicative rates for the given date.
|
|
138
139
|
"""
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
normalized_date = normalize_date(reference_date)
|
|
142
|
+
df = get_raw_data(normalized_date, is_anbima_member)
|
|
141
143
|
|
|
142
144
|
if not return_raw:
|
|
143
145
|
df = process_raw_data(df)
|
|
@@ -146,9 +148,9 @@ def get_treasury_rates(
|
|
|
146
148
|
|
|
147
149
|
|
|
148
150
|
def calculate_treasury_di_spreads(
|
|
149
|
-
reference_date: str |
|
|
151
|
+
reference_date: str | Timestamp | None = None,
|
|
150
152
|
is_anbima_member=False,
|
|
151
|
-
) ->
|
|
153
|
+
) -> DataFrame:
|
|
152
154
|
"""
|
|
153
155
|
Calculate the DI spread for LTN and NTN-F bonds based on ANBIMA's indicative rates.
|
|
154
156
|
If no date is provided, the previous business day based on the Brazilian calendar
|
|
@@ -162,11 +164,11 @@ def calculate_treasury_di_spreads(
|
|
|
162
164
|
Returns:
|
|
163
165
|
- pd.DataFrame: A DataFrame containing the bond type, reference date, maturity date, and DI spread in basis points.
|
|
164
166
|
"""
|
|
165
|
-
#
|
|
166
|
-
|
|
167
|
+
# Validate the reference date, defaulting to the previous business day if not provided
|
|
168
|
+
normalized_date = normalize_date(reference_date)
|
|
167
169
|
|
|
168
170
|
# Fetch DI rates and adjust the maturity date format for compatibility
|
|
169
|
-
df_di = di.get_di(
|
|
171
|
+
df_di = di.get_di(normalized_date)[["ExpirationDate", "SettlementRate"]]
|
|
170
172
|
|
|
171
173
|
# Renaming the columns to match the ANBIMA structure
|
|
172
174
|
df_di.rename(columns={"ExpirationDate": "MaturityDate"}, inplace=True)
|
|
@@ -175,9 +177,7 @@ def calculate_treasury_di_spreads(
|
|
|
175
177
|
df_di["MaturityDate"] = df_di["MaturityDate"].dt.to_period("M").dt.to_timestamp()
|
|
176
178
|
|
|
177
179
|
# Fetch bond rates, filtering for LTN and NTN-F types
|
|
178
|
-
df_anbima = get_treasury_rates(
|
|
179
|
-
reference_date=validated_date, is_anbima_member=is_anbima_member
|
|
180
|
-
)
|
|
180
|
+
df_anbima = get_treasury_rates(normalized_date, False, is_anbima_member)
|
|
181
181
|
df_anbima.query("BondType in ['LTN', 'NTN-F']", inplace=True)
|
|
182
182
|
|
|
183
183
|
# Merge bond and DI rates by maturity date to calculate spreads
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from typing import Literal
|
|
1
|
+
from typing import Literal, overload
|
|
2
2
|
|
|
3
|
-
import pandas as pd
|
|
4
3
|
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from pandas import Series, Timestamp
|
|
5
6
|
|
|
6
7
|
from .br_holidays import BrHolidays
|
|
7
8
|
|
|
@@ -9,9 +10,50 @@ from .br_holidays import BrHolidays
|
|
|
9
10
|
br_holidays = BrHolidays()
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
@overload
|
|
14
|
+
def normalize_input_dates(dates: str | Timestamp | None) -> Timestamp: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@overload
|
|
18
|
+
def normalize_input_dates(dates: Series) -> Series: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_input_dates(dates: str | Timestamp | Series | None) -> Timestamp | Series:
|
|
22
|
+
if isinstance(dates, str):
|
|
23
|
+
return pd.Timestamp(dates).normalize()
|
|
24
|
+
elif isinstance(dates, Timestamp):
|
|
25
|
+
return dates.normalize()
|
|
26
|
+
elif isinstance(dates, Series):
|
|
27
|
+
return pd.to_datetime(dates)
|
|
28
|
+
elif dates is None:
|
|
29
|
+
return pd.Timestamp.today().normalize()
|
|
30
|
+
else:
|
|
31
|
+
raise ValueError("Invalid date format.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_business_day(date: str | Timestamp | None = None) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Checks if the input date is a business day.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
date (str | Timestamp): The date to check.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
bool: True if the input date is a business day, False otherwise.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
>>> yd.is_business_day('2023-12-25') # Christmas
|
|
46
|
+
False
|
|
47
|
+
>>> yd.is_business_day() # Check if today is a business day
|
|
48
|
+
True
|
|
49
|
+
"""
|
|
50
|
+
normalized_date = normalize_input_dates(date)
|
|
51
|
+
# Shift the date if it is not a business day
|
|
52
|
+
adjusted_date = offset_bdays(normalized_date, 0)
|
|
53
|
+
return normalized_date == adjusted_date
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def convert_to_numpy_date(dates: Timestamp | Series) -> np.datetime64 | np.ndarray:
|
|
15
57
|
"""
|
|
16
58
|
Converts the input dates to a numpy datetime64[D] format.
|
|
17
59
|
|
|
@@ -27,27 +69,49 @@ def convert_to_numpy_date(
|
|
|
27
69
|
return dates.to_numpy().astype("datetime64[D]")
|
|
28
70
|
|
|
29
71
|
|
|
72
|
+
@overload
|
|
30
73
|
def offset_bdays(
|
|
31
|
-
dates: str |
|
|
32
|
-
offset: int,
|
|
74
|
+
dates: str | Timestamp | None = None,
|
|
75
|
+
offset: int = 0,
|
|
76
|
+
roll: Literal["forward", "backward"] = "forward",
|
|
33
77
|
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
34
|
-
):
|
|
78
|
+
) -> Timestamp: ...
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
def offset_bdays(
|
|
83
|
+
dates: Series,
|
|
84
|
+
offset: int = 0,
|
|
85
|
+
roll: Literal["forward", "backward"] = "forward",
|
|
86
|
+
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
87
|
+
) -> Series: ...
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def offset_bdays(
|
|
91
|
+
dates: str | Timestamp | Series | None = None,
|
|
92
|
+
offset: int = 0,
|
|
93
|
+
roll: Literal["forward", "backward"] = "forward",
|
|
94
|
+
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
95
|
+
) -> Timestamp | Series:
|
|
35
96
|
"""
|
|
36
97
|
Offsets the dates to the next or previous business day. This function is a wrapper
|
|
37
98
|
for `numpy.busday_offset` to be used directly with Pandas data types that infers the
|
|
38
99
|
right list of holidays based on the most recent date in the input.
|
|
39
100
|
|
|
40
101
|
Args:
|
|
41
|
-
dates (str |
|
|
102
|
+
dates (str | Timestamp | Series): A single date or a Series of dates to be offset. If
|
|
103
|
+
None, the current date is used.
|
|
42
104
|
offset (int): The number of business days to offset the dates. Positive numbers
|
|
43
105
|
offset to the next business day, negative numbers offset to the previous
|
|
44
106
|
business day. Zero offsets to the same date if it's a business day, otherwise
|
|
45
107
|
offsets to the next business day.
|
|
108
|
+
roll (str, optional): The direction to roll the date if it falls on a holiday.
|
|
109
|
+
Valid options are 'forward' and 'backward'. Defaults to 'forward'.
|
|
46
110
|
holiday_list (str, optional): The list of holidays to use. Defaults to "infer", which
|
|
47
111
|
infers the right list of holidays based on the most recent date in the input.
|
|
48
112
|
|
|
49
113
|
Returns:
|
|
50
|
-
|
|
114
|
+
Timestamp | Series: The offset dates. Returns a single date if
|
|
51
115
|
`dates` is a single date, otherwise returns a Series of dates.
|
|
52
116
|
|
|
53
117
|
Note: For more information on error handling, see numpy.busday_offset documentation at
|
|
@@ -65,22 +129,53 @@ def offset_bdays(
|
|
|
65
129
|
>>> yd.offset_bdays(date, -1)
|
|
66
130
|
Timestamp('2023-12-21') # Offset to the previous business day
|
|
67
131
|
"""
|
|
68
|
-
|
|
69
|
-
dates_pd = pd.to_datetime(dates)
|
|
132
|
+
normalized_dates = normalize_input_dates(dates)
|
|
70
133
|
|
|
71
|
-
selected_holidays = br_holidays.get_applicable_holidays(
|
|
134
|
+
selected_holidays = br_holidays.get_applicable_holidays(
|
|
135
|
+
normalized_dates, holiday_list
|
|
136
|
+
)
|
|
72
137
|
selected_holidays_np = convert_to_numpy_date(selected_holidays)
|
|
73
138
|
|
|
74
|
-
dates_np = convert_to_numpy_date(
|
|
139
|
+
dates_np = convert_to_numpy_date(normalized_dates)
|
|
75
140
|
offsetted_dates_np = np.busday_offset(
|
|
76
|
-
dates_np, offsets=offset, roll=
|
|
141
|
+
dates_np, offsets=offset, roll=roll, holidays=selected_holidays_np
|
|
77
142
|
)
|
|
143
|
+
if isinstance(offsetted_dates_np, np.datetime64):
|
|
144
|
+
return pd.Timestamp(offsetted_dates_np, unit="ns")
|
|
145
|
+
else:
|
|
146
|
+
result = pd.to_datetime(offsetted_dates_np, unit="ns")
|
|
147
|
+
return pd.Series(result)
|
|
148
|
+
|
|
78
149
|
|
|
79
|
-
|
|
80
|
-
|
|
150
|
+
@overload
|
|
151
|
+
def count_bdays(
|
|
152
|
+
start: str | Timestamp | None = None,
|
|
153
|
+
end: str | Timestamp | None = None,
|
|
154
|
+
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
155
|
+
) -> int: ...
|
|
81
156
|
|
|
82
157
|
|
|
83
|
-
|
|
158
|
+
@overload
|
|
159
|
+
def count_bdays(
|
|
160
|
+
start: Series,
|
|
161
|
+
end: str | Timestamp | Series | None,
|
|
162
|
+
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
163
|
+
) -> Series: ...
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@overload
|
|
167
|
+
def count_bdays(
|
|
168
|
+
start: str | Timestamp | Series | None,
|
|
169
|
+
end: Series,
|
|
170
|
+
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
171
|
+
) -> Series: ...
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def count_bdays(
|
|
175
|
+
start: str | Timestamp | Series | None = None,
|
|
176
|
+
end: str | Timestamp | Series | None = None,
|
|
177
|
+
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
178
|
+
) -> int | Series:
|
|
84
179
|
"""
|
|
85
180
|
Counts the number of business days between a `start` (inclusive) and `end`
|
|
86
181
|
(exclusive). If an end date is earlier than the start date, the count will be
|
|
@@ -88,16 +183,17 @@ def count_bdays(start, end, holiday_list: Literal["old", "new", "infer"] = "infe
|
|
|
88
183
|
with Pandas data types.
|
|
89
184
|
|
|
90
185
|
Args:
|
|
91
|
-
start (str |
|
|
92
|
-
|
|
186
|
+
start (str | Timestamp | Series, optional): The start date. If None, the current
|
|
187
|
+
date is used. Defaults to None.
|
|
188
|
+
end (str | Timestamp | Series, optional): The end date. If None, the current date
|
|
189
|
+
is used. Defaults to None.
|
|
93
190
|
holiday_list (str, optional): The list of holidays to use. Defaults to "infer",
|
|
94
191
|
which infers the right list of holidays based on the most recent date in
|
|
95
192
|
the input.
|
|
96
193
|
|
|
97
194
|
Returns:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
returns an ndarray of integers.
|
|
195
|
+
int | Series: The number of business days between the start date and end date.
|
|
196
|
+
Returns an integer if the result is a single value, otherwise returns a Series.
|
|
101
197
|
|
|
102
198
|
Notes:
|
|
103
199
|
- For more information on error handling, see numpy.busday_count documentation at
|
|
@@ -115,30 +211,36 @@ def count_bdays(start, end, holiday_list: Literal["old", "new", "infer"] = "infe
|
|
|
115
211
|
>>> start = '2023-01-01'
|
|
116
212
|
>>> end = pd.to_datetime(['2023-01-31', '2023-03-01'])
|
|
117
213
|
>>> yd.count_bdays(start, end)
|
|
118
|
-
|
|
214
|
+
pd.Series([22, 40], dtype='int64')
|
|
119
215
|
"""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
end_pd = pd.to_datetime(end)
|
|
216
|
+
normalized_start = normalize_input_dates(start)
|
|
217
|
+
normalized_end = normalize_input_dates(end)
|
|
123
218
|
|
|
124
219
|
# Determine which list of holidays to use
|
|
125
|
-
selected_holidays = br_holidays.get_applicable_holidays(
|
|
220
|
+
selected_holidays = br_holidays.get_applicable_holidays(
|
|
221
|
+
normalized_start, holiday_list
|
|
222
|
+
)
|
|
126
223
|
selected_holidays_np = convert_to_numpy_date(selected_holidays)
|
|
127
224
|
|
|
128
225
|
# Convert inputs to numpy datetime64[D] before calling numpy.busday_count
|
|
129
|
-
start_np = convert_to_numpy_date(
|
|
130
|
-
end_np = convert_to_numpy_date(
|
|
226
|
+
start_np = convert_to_numpy_date(normalized_start)
|
|
227
|
+
end_np = convert_to_numpy_date(normalized_end)
|
|
131
228
|
|
|
132
|
-
|
|
229
|
+
result_np = np.busday_count(start_np, end_np, holidays=selected_holidays_np)
|
|
230
|
+
if isinstance(result_np, np.ndarray):
|
|
231
|
+
# Return pandas Int64 type for type consistency
|
|
232
|
+
return pd.Series(result_np, dtype="Int64")
|
|
233
|
+
else:
|
|
234
|
+
return int(result_np)
|
|
133
235
|
|
|
134
236
|
|
|
135
237
|
def generate_bdays(
|
|
136
|
-
start: str |
|
|
137
|
-
end: str |
|
|
138
|
-
inclusive="both",
|
|
238
|
+
start: str | Timestamp | None = None,
|
|
239
|
+
end: str | Timestamp | None = None,
|
|
240
|
+
inclusive: Literal["both", "neither", "left", "right"] = "both",
|
|
139
241
|
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
140
242
|
**kwargs,
|
|
141
|
-
) ->
|
|
243
|
+
) -> Series:
|
|
142
244
|
"""
|
|
143
245
|
Generates a Series of business days between a `start` (inclusive) and `end`
|
|
144
246
|
(inclusive) that takes into account the list of brazilian holidays as the default.
|
|
@@ -147,8 +249,10 @@ def generate_bdays(
|
|
|
147
249
|
|
|
148
250
|
|
|
149
251
|
Args:
|
|
150
|
-
start (str |
|
|
151
|
-
|
|
252
|
+
start (str | Timestamp, optional): The start date. Defaults to None. If None,
|
|
253
|
+
the current date is used.
|
|
254
|
+
end (str | Timestamp | Series, optional): The end date. Defaults to None. If None,
|
|
255
|
+
the current date is used.
|
|
152
256
|
inclusive (str, optional): Whether to include the start and end dates.
|
|
153
257
|
Valid options are 'both', 'neither', 'left', 'right'. Defaults to 'both'.
|
|
154
258
|
holiday_list (str, optional): The list of holidays to use. Defaults to "infer",
|
|
@@ -179,24 +283,23 @@ def generate_bdays(
|
|
|
179
283
|
2023-12-29 2023-12-29
|
|
180
284
|
dtype: object
|
|
181
285
|
"""
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
else:
|
|
185
|
-
start_pd = pd.Timestamp.today().normalize()
|
|
286
|
+
normalized_start = normalize_input_dates(start)
|
|
287
|
+
normalized_end = normalize_input_dates(end)
|
|
186
288
|
|
|
187
|
-
if
|
|
188
|
-
|
|
189
|
-
else:
|
|
190
|
-
end_pd = pd.Timestamp.today().normalize()
|
|
289
|
+
if isinstance(normalized_start, Series) or isinstance(normalized_end, Series):
|
|
290
|
+
raise ValueError("The start and end dates must be single dates.")
|
|
191
291
|
|
|
192
|
-
selected_holidays = br_holidays.get_applicable_holidays(
|
|
292
|
+
selected_holidays = br_holidays.get_applicable_holidays(
|
|
293
|
+
normalized_start, holiday_list
|
|
294
|
+
)
|
|
193
295
|
selected_holidays_list = selected_holidays.to_list()
|
|
194
296
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
297
|
+
result = pd.bdate_range(
|
|
298
|
+
normalized_start,
|
|
299
|
+
normalized_end,
|
|
198
300
|
freq="C",
|
|
199
301
|
inclusive=inclusive,
|
|
200
302
|
holidays=selected_holidays_list,
|
|
201
303
|
**kwargs,
|
|
202
304
|
)
|
|
305
|
+
return pd.Series(result)
|
|
@@ -2,6 +2,7 @@ from pathlib import Path
|
|
|
2
2
|
from typing import Literal
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
|
+
from pandas import Series, Timestamp
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class BrHolidays:
|
|
@@ -15,7 +16,7 @@ class BrHolidays:
|
|
|
15
16
|
self.new_holidays = self._load_holidays(new_holidays_path)
|
|
16
17
|
self.old_holidays = self._load_holidays(old_holidays_path)
|
|
17
18
|
|
|
18
|
-
def _load_holidays(self, file_path: Path) ->
|
|
19
|
+
def _load_holidays(self, file_path: Path) -> Series:
|
|
19
20
|
"""Loads the list of holidays from a text file and returns it as a pd.Series of pd.Timestamp."""
|
|
20
21
|
df = pd.read_csv(file_path, header=None, names=["date"], comment="#")
|
|
21
22
|
# Convert the dates to a pd.Series of pd.Timestamp
|
|
@@ -23,9 +24,9 @@ class BrHolidays:
|
|
|
23
24
|
|
|
24
25
|
def get_applicable_holidays(
|
|
25
26
|
self,
|
|
26
|
-
dates:
|
|
27
|
+
dates: Timestamp | Series,
|
|
27
28
|
holiday_list: Literal["old", "new", "infer"] = "infer",
|
|
28
|
-
) ->
|
|
29
|
+
) -> Series:
|
|
29
30
|
"""
|
|
30
31
|
Returns the correct list of holidays to use based on the most recent date in the input.
|
|
31
32
|
|
|
@@ -38,7 +39,7 @@ class BrHolidays:
|
|
|
38
39
|
Returns:
|
|
39
40
|
pd.Series: The list of holidays to use.
|
|
40
41
|
"""
|
|
41
|
-
if isinstance(dates,
|
|
42
|
+
if isinstance(dates, Timestamp):
|
|
42
43
|
earliest_date = dates
|
|
43
44
|
else:
|
|
44
45
|
earliest_date = dates.min()
|
|
@@ -1,14 +1,37 @@
|
|
|
1
|
-
from typing import Literal
|
|
1
|
+
from typing import Literal
|
|
2
2
|
from pathlib import Path
|
|
3
|
+
|
|
3
4
|
import pandas as pd
|
|
4
|
-
import
|
|
5
|
+
from pandas import DataFrame, Timestamp
|
|
5
6
|
|
|
6
7
|
from . import di_web as diw
|
|
7
8
|
from . import di_xml as dix
|
|
8
9
|
from . import br_calendar as brc
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
def
|
|
12
|
+
def normalize_date(trade_date: str | Timestamp | None = None) -> Timestamp:
|
|
13
|
+
if isinstance(trade_date, str):
|
|
14
|
+
normalized_date = pd.Timestamp(trade_date).normalize()
|
|
15
|
+
elif isinstance(trade_date, Timestamp):
|
|
16
|
+
normalized_date = trade_date.normalize()
|
|
17
|
+
elif trade_date is None:
|
|
18
|
+
today = pd.Timestamp.today().normalize()
|
|
19
|
+
normalized_date = brc.offset_bdays(today, -1)
|
|
20
|
+
else:
|
|
21
|
+
raise ValueError("Invalid date format.")
|
|
22
|
+
|
|
23
|
+
# Raise an error if the trade date is in the future
|
|
24
|
+
if normalized_date > pd.Timestamp.today().normalize():
|
|
25
|
+
raise ValueError("Trade date cannot be in the future.")
|
|
26
|
+
|
|
27
|
+
# Raise error if the reference date is not a business day
|
|
28
|
+
if not brc.is_business_day(normalized_date):
|
|
29
|
+
raise ValueError("Trade date must be a business day.")
|
|
30
|
+
|
|
31
|
+
return normalized_date
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_expiration_date(expiration_code: str) -> Timestamp:
|
|
12
35
|
"""
|
|
13
36
|
Internal function to convert the expiration code into its expiration date.
|
|
14
37
|
|
|
@@ -63,18 +86,21 @@ def get_expiration_date(expiration_code: str) -> Optional[pd.Timestamp]:
|
|
|
63
86
|
year = int("20" + expiration_code[-2:])
|
|
64
87
|
# The expiration date is always the first business day of the month
|
|
65
88
|
expiration = pd.Timestamp(year, month, 1)
|
|
89
|
+
|
|
66
90
|
# Adjust to the next business day when expiration date is a weekend or a holiday
|
|
67
|
-
|
|
91
|
+
adj_expiration = brc.offset_bdays(expiration, offset=0)
|
|
92
|
+
|
|
93
|
+
return adj_expiration
|
|
68
94
|
|
|
69
95
|
except (KeyError, ValueError):
|
|
70
|
-
|
|
96
|
+
raise ValueError("Invalid expiration code.")
|
|
71
97
|
|
|
72
98
|
|
|
73
99
|
def get_di(
|
|
74
|
-
trade_date: str |
|
|
100
|
+
trade_date: str | Timestamp | None = None,
|
|
75
101
|
source_type: Literal["bmf", "b3", "b3s"] = "bmf",
|
|
76
102
|
return_raw: bool = False,
|
|
77
|
-
) ->
|
|
103
|
+
) -> DataFrame:
|
|
78
104
|
"""
|
|
79
105
|
Gets the DI futures data for a given date from B3.
|
|
80
106
|
|
|
@@ -82,7 +108,8 @@ def get_di(
|
|
|
82
108
|
trade date. It's the primary external interface for accessing DI data.
|
|
83
109
|
|
|
84
110
|
Args:
|
|
85
|
-
trade_date: a datetime-like object representing the trade date.
|
|
111
|
+
trade_date: a datetime-like object representing the trade date. if not provided,
|
|
112
|
+
the function will use the previous business day.
|
|
86
113
|
source_type: a string indicating the source of the data. Options are "bmf",
|
|
87
114
|
"b3" and "b3s" (default is "bmf").
|
|
88
115
|
- "bmf" (fast) fetches DI data from old BM&FBOVESPA website. This is the default
|
|
@@ -91,15 +118,15 @@ def get_di(
|
|
|
91
118
|
- "b3s" (fast) fetches DI data from the simplified Price Report (XML file) provided by B3.
|
|
92
119
|
This option is faster than "b3" but it has less information.
|
|
93
120
|
return_raw: a boolean indicating whether to return the raw DI data.
|
|
94
|
-
data_path: a Path object indicating the path to the directory where the XML
|
|
95
|
-
files are stored. This argument is only used when source_type is "b3" or
|
|
96
|
-
"b3s". If not provided, data will be downloaded from B3's website.
|
|
97
121
|
|
|
98
122
|
Returns:
|
|
99
123
|
pd.DataFrame: A Pandas DataFrame containing the DI Futures data for
|
|
100
124
|
the given trade date.
|
|
101
125
|
|
|
102
126
|
Examples:
|
|
127
|
+
# Get the DI data for the previous business day
|
|
128
|
+
>>> get_di()
|
|
129
|
+
# Get the DI data for a specific date in ISO format (YYYY-MM-DD)
|
|
103
130
|
>>> get_di("2023-12-28")
|
|
104
131
|
|
|
105
132
|
Notes:
|
|
@@ -109,19 +136,17 @@ def get_di(
|
|
|
109
136
|
|
|
110
137
|
"""
|
|
111
138
|
# Force trade_date to be a pandas Timestamp
|
|
112
|
-
|
|
113
|
-
if not trade_date:
|
|
114
|
-
raise ValueError("Uma data de referência válida deve ser fornecida.")
|
|
139
|
+
normalized_trade_date = normalize_date(trade_date)
|
|
115
140
|
|
|
116
141
|
if source_type == "bmf":
|
|
117
|
-
return diw.get_di(
|
|
142
|
+
return diw.get_di(normalized_trade_date, return_raw)
|
|
118
143
|
elif source_type in ["b3", "b3s"]:
|
|
119
|
-
return dix.get_di(
|
|
144
|
+
return dix.get_di(normalized_trade_date, source_type, return_raw)
|
|
120
145
|
else:
|
|
121
146
|
raise ValueError("source_type must be either 'bmf', 'b3' or 'b3s'.")
|
|
122
147
|
|
|
123
148
|
|
|
124
|
-
def read_di(file_path: Path, return_raw: bool = False) ->
|
|
149
|
+
def read_di(file_path: Path, return_raw: bool = False) -> DataFrame:
|
|
125
150
|
"""
|
|
126
151
|
Reads a DI futures data file and returns a DataFrame.
|
|
127
152
|
|
|
@@ -3,6 +3,7 @@ import warnings
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
6
|
+
from pandas import Timestamp, Series, DataFrame
|
|
6
7
|
import requests
|
|
7
8
|
|
|
8
9
|
from . import br_calendar as brc
|
|
@@ -10,8 +11,8 @@ from . import di_futures as dif
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def get_old_expiration_date(
|
|
13
|
-
ExpirationCode: str, trade_date:
|
|
14
|
-
) -> Optional[
|
|
14
|
+
ExpirationCode: str, trade_date: Timestamp
|
|
15
|
+
) -> Optional[Timestamp]:
|
|
15
16
|
"""
|
|
16
17
|
Internal function to convert an old DI contract code into its ExpirationDate date. Valid for
|
|
17
18
|
contract codes up to 21-05-2006.
|
|
@@ -71,7 +72,7 @@ def get_old_expiration_date(
|
|
|
71
72
|
return pd.NaT # type: ignore
|
|
72
73
|
|
|
73
74
|
|
|
74
|
-
def get_raw_di(trade_date:
|
|
75
|
+
def get_raw_di(trade_date: Timestamp) -> DataFrame:
|
|
75
76
|
"""
|
|
76
77
|
Internal function to fetch raw DI futures data from B3 for a specific trade date.
|
|
77
78
|
|
|
@@ -119,7 +120,7 @@ def get_raw_di(trade_date: pd.Timestamp) -> pd.DataFrame:
|
|
|
119
120
|
return pd.DataFrame()
|
|
120
121
|
|
|
121
122
|
|
|
122
|
-
def convert_prices_to_rates(prices:
|
|
123
|
+
def convert_prices_to_rates(prices: Series, bd: Series) -> Series:
|
|
123
124
|
"""
|
|
124
125
|
Internal function to convert DI futures prices to rates.
|
|
125
126
|
|
|
@@ -136,7 +137,7 @@ def convert_prices_to_rates(prices: pd.Series, bd: pd.Series) -> pd.Series:
|
|
|
136
137
|
return 100 * rates
|
|
137
138
|
|
|
138
139
|
|
|
139
|
-
def convert_prices_in_older_contracts(df:
|
|
140
|
+
def convert_prices_in_older_contracts(df: DataFrame) -> DataFrame:
|
|
140
141
|
# Prior to 01/01/2002, prices were not converted to rates
|
|
141
142
|
convert_cols = [
|
|
142
143
|
"FirstRate",
|
|
@@ -156,7 +157,7 @@ def convert_prices_in_older_contracts(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
156
157
|
return df
|
|
157
158
|
|
|
158
159
|
|
|
159
|
-
def process_di(df:
|
|
160
|
+
def process_di(df: DataFrame, trade_date: Timestamp) -> DataFrame:
|
|
160
161
|
"""
|
|
161
162
|
Internal function to process and transform raw DI futures data.
|
|
162
163
|
|
|
@@ -265,7 +266,7 @@ def process_di(df: pd.DataFrame, trade_date: pd.Timestamp) -> pd.DataFrame:
|
|
|
265
266
|
return df[ordered_cols]
|
|
266
267
|
|
|
267
268
|
|
|
268
|
-
def get_di(trade_date:
|
|
269
|
+
def get_di(trade_date: Timestamp, return_raw: bool = False) -> DataFrame:
|
|
269
270
|
"""
|
|
270
271
|
Gets the DI futures data for a given date from B3.
|
|
271
272
|
|
|
@@ -4,13 +4,14 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
import requests
|
|
6
6
|
import pandas as pd
|
|
7
|
+
from pandas import Timestamp, DataFrame
|
|
7
8
|
from lxml import etree
|
|
8
9
|
|
|
9
10
|
from . import di_futures as dif
|
|
10
11
|
from . import br_calendar as brc
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
def get_file_from_url(trade_date:
|
|
14
|
+
def get_file_from_url(trade_date: Timestamp, source_type: str) -> io.BytesIO:
|
|
14
15
|
"""
|
|
15
16
|
Types of XML files available:
|
|
16
17
|
Full Price Report (all assets)
|
|
@@ -119,7 +120,7 @@ def extract_di_data_from_xml(xml_file: io.BytesIO) -> list[dict]:
|
|
|
119
120
|
return di_data
|
|
120
121
|
|
|
121
122
|
|
|
122
|
-
def create_df_from_di_data(di1_data: list) ->
|
|
123
|
+
def create_df_from_di_data(di1_data: list) -> DataFrame:
|
|
123
124
|
# Criar um DataFrame com os dados coletados
|
|
124
125
|
df = pd.DataFrame(di1_data)
|
|
125
126
|
|
|
@@ -128,7 +129,7 @@ def create_df_from_di_data(di1_data: list) -> pd.DataFrame:
|
|
|
128
129
|
return pd.read_csv(file, dtype_backend="numpy_nullable")
|
|
129
130
|
|
|
130
131
|
|
|
131
|
-
def filter_and_order_pr_df(df:
|
|
132
|
+
def filter_and_order_pr_df(df: DataFrame) -> DataFrame:
|
|
132
133
|
selected_columns = [
|
|
133
134
|
"TradDt",
|
|
134
135
|
"TckrSymb",
|
|
@@ -164,7 +165,7 @@ def filter_and_order_pr_df(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
164
165
|
return df[selected_columns]
|
|
165
166
|
|
|
166
167
|
|
|
167
|
-
def filter_and_order_sprd_df(df:
|
|
168
|
+
def filter_and_order_sprd_df(df: DataFrame) -> DataFrame:
|
|
168
169
|
cols = [
|
|
169
170
|
"TradDt",
|
|
170
171
|
"TckrSymb",
|
|
@@ -186,7 +187,7 @@ def filter_and_order_sprd_df(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
186
187
|
return df[cols]
|
|
187
188
|
|
|
188
189
|
|
|
189
|
-
def standardize_column_names(df:
|
|
190
|
+
def standardize_column_names(df: DataFrame) -> DataFrame:
|
|
190
191
|
rename_dict = {
|
|
191
192
|
"TradDt": "TradeDate",
|
|
192
193
|
"TckrSymb": "Ticker",
|
|
@@ -223,7 +224,7 @@ def standardize_column_names(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
223
224
|
return df.rename(columns=rename_dict)
|
|
224
225
|
|
|
225
226
|
|
|
226
|
-
def process_di_df(df_raw:
|
|
227
|
+
def process_di_df(df_raw: DataFrame) -> DataFrame:
|
|
227
228
|
df = df_raw.copy()
|
|
228
229
|
# Convert to datetime64[ns] since it is pandas default type for timestamps
|
|
229
230
|
df["TradDt"] = df["TradDt"].astype("datetime64[ns]")
|
|
@@ -243,11 +244,7 @@ def process_di_df(df_raw: pd.DataFrame) -> pd.DataFrame:
|
|
|
243
244
|
return df.sort_values(by=["ExpirationDate"], ignore_index=True)
|
|
244
245
|
|
|
245
246
|
|
|
246
|
-
def get_di(
|
|
247
|
-
trade_date: pd.Timestamp,
|
|
248
|
-
source_type: str,
|
|
249
|
-
return_raw: bool,
|
|
250
|
-
) -> pd.DataFrame:
|
|
247
|
+
def get_di(trade_date: Timestamp, source_type: str, return_raw: bool) -> DataFrame:
|
|
251
248
|
zip_file = get_file_from_url(trade_date, source_type)
|
|
252
249
|
|
|
253
250
|
xml_file = extract_xml_from_zip(zip_file)
|
|
@@ -263,6 +260,8 @@ def get_di(
|
|
|
263
260
|
df_di = filter_and_order_pr_df(df_raw)
|
|
264
261
|
elif source_type == "b3s":
|
|
265
262
|
df_di = filter_and_order_sprd_df(df_raw)
|
|
263
|
+
else:
|
|
264
|
+
raise ValueError("Invalid source type. Must be 'b3' or 'b3s'.")
|
|
266
265
|
|
|
267
266
|
# Process and transform data
|
|
268
267
|
df_di = process_di_df(df_di)
|
|
@@ -273,7 +272,7 @@ def get_di(
|
|
|
273
272
|
return df_di
|
|
274
273
|
|
|
275
274
|
|
|
276
|
-
def read_di(file_path: Path, return_raw: bool = False) ->
|
|
275
|
+
def read_di(file_path: Path, return_raw: bool = False) -> DataFrame:
|
|
277
276
|
if file_path:
|
|
278
277
|
if file_path.exists():
|
|
279
278
|
content = file_path.read_bytes()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pandas as pd
|
|
2
|
+
import pytest
|
|
2
3
|
|
|
3
4
|
from pyield import di_futures as dif
|
|
4
5
|
from pyield import di_web as diw
|
|
@@ -75,6 +76,7 @@ def test_settlement_rates_with_current_holiday_list():
|
|
|
75
76
|
assert results == list(settlement_rates.values())
|
|
76
77
|
|
|
77
78
|
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
def test_non_business_day():
|
|
80
|
+
non_business_day = "2023-12-24"
|
|
81
|
+
with pytest.raises(ValueError):
|
|
82
|
+
dif.get_di(trade_date=non_business_day)
|
pyield-0.3.6/pyield/__about__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.6"
|
pyield-0.3.6/pyield/__init__.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|