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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PYield
3
- Version: 0.3.6
3
+ Version: 0.4.0
4
4
  Summary: A Python library for analysis of fixed income instruments in Brazil
5
5
  Keywords: fixed-income, brazil, finance, analysis, bonds
6
6
  Author-Email: Carlos Carvalho <cr.cj@outlook.com>
@@ -0,0 +1 @@
1
+ __version__ = "0.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 process_reference_date(
16
- reference_date: str | pd.Timestamp | None = None,
17
- ) -> pd.Timestamp:
18
- """
19
- Process the given reference date, converting it to a pandas Timestamp. If no date is provided,
20
- use the previous business day based on the Brazilian calendar.
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
- Parameters:
23
- - reference_date (str | pd.Timestamp | None): The reference date to process.
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
- Returns:
26
- - pd.Timestamp: The processed reference date.
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 processed_date
35
+ return normalized_date
35
36
 
36
37
 
37
38
  def get_raw_data(
38
- reference_date: str | pd.Timestamp | None = None, is_anbima_member: bool = False
39
- ) -> pd.DataFrame:
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
- validated_date = process_reference_date(reference_date)
52
+ normalized_date = normalize_date(reference_date)
52
53
 
53
54
  # Format the date to match the URL format
54
- url_date = validated_date.strftime("%y%m%d")
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 = validated_date.strftime("%d-%m-%Y")
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(df: pd.DataFrame) -> pd.DataFrame:
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 = df[select_columns].copy()
108
- df.rename(columns=selected_columns_dict, inplace=True)
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 | pd.Timestamp | None = None,
122
+ reference_date: str | Timestamp | None = None,
122
123
  return_raw=False,
123
124
  is_anbima_member=False,
124
- ) -> pd.DataFrame:
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
- df = get_raw_data(reference_date, is_anbima_member)
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 | pd.Timestamp | None = None,
151
+ reference_date: str | Timestamp | None = None,
150
152
  is_anbima_member=False,
151
- ) -> pd.DataFrame:
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
- # Process the reference date, defaulting to the previous business day if not provided
166
- validated_date = process_reference_date(reference_date)
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(validated_date)[["ExpirationDate", "SettlementRate"]]
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
- def convert_to_numpy_date(
13
- dates: pd.Timestamp | pd.Series,
14
- ) -> np.datetime64 | np.ndarray:
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 | pd.Timestamp | pd.Series,
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 | pd.Timestamp | pd.Series): A single date or a Series of dates to be offset.
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
- pd.Timestamp | pd.Series: The offset dates. Returns a single date if
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
- # Assure that the input is in pandas datetime64[ns] format
69
- dates_pd = pd.to_datetime(dates)
132
+ normalized_dates = normalize_input_dates(dates)
70
133
 
71
- selected_holidays = br_holidays.get_applicable_holidays(dates_pd, holiday_list)
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(dates_pd)
139
+ dates_np = convert_to_numpy_date(normalized_dates)
75
140
  offsetted_dates_np = np.busday_offset(
76
- dates_np, offsets=offset, roll="forward", holidays=selected_holidays_np
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
- # Convert the dates back to a pandas datetime64[ns] format
80
- return pd.to_datetime(offsetted_dates_np, unit="ns")
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
- def count_bdays(start, end, holiday_list: Literal["old", "new", "infer"] = "infer"):
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 | pd.Timestamp, optional): The start date. Defaults to None.
92
- end (str | pd.Timestamp optional): The end date. Defaults to None.
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
- np.int64 | np.ndarray: The number of business days between the start date and
99
- end dates. Returns a single integer if `end` is a single Timestamp, otherwise
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
- array([22, 40])
214
+ pd.Series([22, 40], dtype='int64')
119
215
  """
120
- # Assure that the inputs are in pandas datetime64[ns] format
121
- start_pd = pd.to_datetime(start)
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(start_pd, holiday_list)
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(start_pd)
130
- end_np = convert_to_numpy_date(end_pd)
226
+ start_np = convert_to_numpy_date(normalized_start)
227
+ end_np = convert_to_numpy_date(normalized_end)
131
228
 
132
- return np.busday_count(start_np, end_np, holidays=selected_holidays_np)
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 | pd.Timestamp | None = None,
137
- end: str | pd.Timestamp | None = None,
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
- ) -> pd.DatetimeIndex:
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 | pd.Timestamp, optional): The start date. Defaults to None.
151
- end (str | pd.Timestamp | pd.Series, optional): The end date. Defaults to None.
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
- if start:
183
- start_pd = pd.to_datetime(start)
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 end:
188
- end_pd = pd.to_datetime(end)
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(start_pd, holiday_list)
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
- return pd.bdate_range(
196
- start_pd,
197
- end_pd,
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) -> pd.Series:
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: pd.Timestamp | pd.Series,
27
+ dates: Timestamp | Series,
27
28
  holiday_list: Literal["old", "new", "infer"] = "infer",
28
- ) -> pd.Series:
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, pd.Timestamp):
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, Optional
1
+ from typing import Literal
2
2
  from pathlib import Path
3
+
3
4
  import pandas as pd
4
- import pandas.api.types
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 get_expiration_date(expiration_code: str) -> Optional[pd.Timestamp]:
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
- return brc.offset_bdays(expiration, offset=0)
91
+ adj_expiration = brc.offset_bdays(expiration, offset=0)
92
+
93
+ return adj_expiration
68
94
 
69
95
  except (KeyError, ValueError):
70
- return pd.NaT # type: ignore
96
+ raise ValueError("Invalid expiration code.")
71
97
 
72
98
 
73
99
  def get_di(
74
- trade_date: str | pd.Timestamp,
100
+ trade_date: str | Timestamp | None = None,
75
101
  source_type: Literal["bmf", "b3", "b3s"] = "bmf",
76
102
  return_raw: bool = False,
77
- ) -> pd.DataFrame:
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
- trade_date = pd.Timestamp(trade_date)
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(trade_date, return_raw)
142
+ return diw.get_di(normalized_trade_date, return_raw)
118
143
  elif source_type in ["b3", "b3s"]:
119
- return dix.get_di(trade_date, source_type, return_raw)
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) -> pd.DataFrame:
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: pd.Timestamp
14
- ) -> Optional[pd.Timestamp]:
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: pd.Timestamp) -> pd.DataFrame:
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: pd.Series, bd: pd.Series) -> pd.Series:
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: pd.DataFrame) -> pd.DataFrame:
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: pd.DataFrame, trade_date: pd.Timestamp) -> pd.DataFrame:
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: pd.Timestamp, return_raw: bool = False) -> pd.DataFrame:
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: pd.Timestamp, source_type: str) -> io.BytesIO:
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) -> pd.DataFrame:
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: pd.DataFrame) -> pd.DataFrame:
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: pd.DataFrame) -> pd.DataFrame:
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: pd.DataFrame) -> pd.DataFrame:
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: pd.DataFrame) -> pd.DataFrame:
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) -> pd.DataFrame:
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()
@@ -31,7 +31,7 @@ dependencies = [
31
31
  "lxml",
32
32
  ]
33
33
  dynamic = []
34
- version = "0.3.6"
34
+ version = "0.4.0"
35
35
 
36
36
  [project.license]
37
37
  file = "LICENSE"
@@ -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 test_invalid_date():
79
- result = dif.get_di(trade_date="2023-12-24")
80
- assert result.empty
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)
@@ -1 +0,0 @@
1
- __version__ = "0.3.6"
@@ -1,4 +0,0 @@
1
- from .__about__ import __version__ # noqa: F401
2
- from .di_futures import get_di, read_di # noqa: F401
3
- from .br_calendar import count_bdays, generate_bdays, offset_bdays # noqa: F401
4
- from .anbima import get_treasury_rates, calculate_treasury_di_spreads # noqa: F401
File without changes
File without changes
File without changes
File without changes
File without changes