PYield 0.3.7__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.7
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"
@@ -1,6 +1,6 @@
1
1
  from .__about__ import __version__
2
2
  from .di_futures import get_di, read_di
3
- from .br_calendar import count_bdays, generate_bdays, offset_bdays
3
+ from .br_calendar import count_bdays, generate_bdays, offset_bdays, is_business_day
4
4
  from .anbima import get_treasury_rates, calculate_treasury_di_spreads
5
5
 
6
6
  __all__ = [
@@ -10,6 +10,7 @@ __all__ = [
10
10
  "count_bdays",
11
11
  "generate_bdays",
12
12
  "offset_bdays",
13
+ "is_business_day",
13
14
  "get_treasury_rates",
14
15
  "calculate_treasury_di_spreads",
15
16
  ]
@@ -13,32 +13,26 @@ ANBIMA_MEMBER_URL = "http://www.anbima.associados.rtm/merc_sec/arqs/"
13
13
  BP_CONVERSION_FACTOR = 10_000
14
14
 
15
15
 
16
- def process_reference_date(reference_date: str | Timestamp | None = None) -> Timestamp:
17
- """
18
- Process the given reference date, converting it to a pandas Timestamp. If no date is provided,
19
- use the previous business day based on the Brazilian calendar.
20
-
21
- Parameters:
22
- - reference_date (str | pd.Timestamp | None): The reference date to process.
23
-
24
- Returns:
25
- - pd.Timestamp: The processed reference date.
26
- """
27
- if reference_date: # Force reference_date to be a pd.Timestamp
28
- processed_date = pd.to_datetime(reference_date)
29
- else: # If no reference_date is given, use the previous business day
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:
30
22
  today = pd.Timestamp.today().normalize()
31
- processed_date = cl.offset_bdays(today, -1) # type: ignore
32
-
33
- # Check if the reference date is Timestamp
34
- if not isinstance(processed_date, Timestamp):
35
- raise ValueError("Reference date must be a pandas Timestamp.")
23
+ normalized_date = cl.offset_bdays(today, -1)
24
+ else:
25
+ raise ValueError("Invalid date format.")
36
26
 
37
27
  # Raise an error if the reference date is in the future
38
- if processed_date > pd.Timestamp.today().normalize():
28
+ if normalized_date > pd.Timestamp.today().normalize():
39
29
  raise ValueError("Reference date cannot be in the future.")
40
30
 
41
- return processed_date
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.")
34
+
35
+ return normalized_date
42
36
 
43
37
 
44
38
  def get_raw_data(
@@ -55,10 +49,10 @@ def get_raw_data(
55
49
  - pd.DataFrame: DataFrame with the indicative rates for the given date.
56
50
  """
57
51
  # Process the reference date, defaulting to the previous business day if not provided
58
- validated_date = process_reference_date(reference_date)
52
+ normalized_date = normalize_date(reference_date)
59
53
 
60
54
  # Format the date to match the URL format
61
- url_date = validated_date.strftime("%y%m%d")
55
+ url_date = normalized_date.strftime("%y%m%d")
62
56
 
63
57
  # Set the base URL according to the member status
64
58
  base_url = ANBIMA_MEMBER_URL if is_anbima_member else ANBIMA_NON_MEMBER_URL
@@ -76,7 +70,7 @@ def get_raw_data(
76
70
  dtype_backend="numpy_nullable",
77
71
  )
78
72
  except HTTPError:
79
- error_date = validated_date.strftime("%d-%m-%Y")
73
+ error_date = normalized_date.strftime("%d-%m-%Y")
80
74
  raise ValueError(f"Failed to get ANBIMA rates for {error_date}")
81
75
 
82
76
  return df
@@ -144,7 +138,8 @@ def get_treasury_rates(
144
138
  - pd.DataFrame: DataFrame with the indicative rates for the given date.
145
139
  """
146
140
 
147
- 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)
148
143
 
149
144
  if not return_raw:
150
145
  df = process_raw_data(df)
@@ -169,11 +164,11 @@ def calculate_treasury_di_spreads(
169
164
  Returns:
170
165
  - pd.DataFrame: A DataFrame containing the bond type, reference date, maturity date, and DI spread in basis points.
171
166
  """
172
- # Process the reference date, defaulting to the previous business day if not provided
173
- 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)
174
169
 
175
170
  # Fetch DI rates and adjust the maturity date format for compatibility
176
- df_di = di.get_di(validated_date)[["ExpirationDate", "SettlementRate"]]
171
+ df_di = di.get_di(normalized_date)[["ExpirationDate", "SettlementRate"]]
177
172
 
178
173
  # Renaming the columns to match the ANBIMA structure
179
174
  df_di.rename(columns={"ExpirationDate": "MaturityDate"}, inplace=True)
@@ -182,9 +177,7 @@ def calculate_treasury_di_spreads(
182
177
  df_di["MaturityDate"] = df_di["MaturityDate"].dt.to_period("M").dt.to_timestamp()
183
178
 
184
179
  # Fetch bond rates, filtering for LTN and NTN-F types
185
- df_anbima = get_treasury_rates(
186
- reference_date=validated_date, is_anbima_member=is_anbima_member
187
- )
180
+ df_anbima = get_treasury_rates(normalized_date, False, is_anbima_member)
188
181
  df_anbima.query("BondType in ['LTN', 'NTN-F']", inplace=True)
189
182
 
190
183
  # Merge bond and DI rates by maturity date to calculate spreads
@@ -11,24 +11,46 @@ br_holidays = BrHolidays()
11
11
 
12
12
 
13
13
  @overload
14
- def format_input_dates(dates: str | Timestamp | None) -> Timestamp: ...
14
+ def normalize_input_dates(dates: str | Timestamp | None) -> Timestamp: ...
15
15
 
16
16
 
17
17
  @overload
18
- def format_input_dates(dates: Series) -> Series: ...
18
+ def normalize_input_dates(dates: Series) -> Series: ...
19
19
 
20
20
 
21
- # Implementação da função que atende às sobrecargas acima
22
- def format_input_dates(dates: str | Timestamp | Series | None) -> Timestamp | Series:
23
- if dates is None:
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:
24
29
  return pd.Timestamp.today().normalize()
25
- result = pd.to_datetime(dates)
26
- if isinstance(result, pd.Series):
27
- return result
28
- elif result is pd.NaT:
29
- raise ValueError("Invalid date format.")
30
30
  else:
31
- return pd.Timestamp(result).normalize()
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
32
54
 
33
55
 
34
56
  def convert_to_numpy_date(dates: Timestamp | Series) -> np.datetime64 | np.ndarray:
@@ -51,6 +73,7 @@ def convert_to_numpy_date(dates: Timestamp | Series) -> np.datetime64 | np.ndarr
51
73
  def offset_bdays(
52
74
  dates: str | Timestamp | None = None,
53
75
  offset: int = 0,
76
+ roll: Literal["forward", "backward"] = "forward",
54
77
  holiday_list: Literal["old", "new", "infer"] = "infer",
55
78
  ) -> Timestamp: ...
56
79
 
@@ -59,6 +82,7 @@ def offset_bdays(
59
82
  def offset_bdays(
60
83
  dates: Series,
61
84
  offset: int = 0,
85
+ roll: Literal["forward", "backward"] = "forward",
62
86
  holiday_list: Literal["old", "new", "infer"] = "infer",
63
87
  ) -> Series: ...
64
88
 
@@ -66,6 +90,7 @@ def offset_bdays(
66
90
  def offset_bdays(
67
91
  dates: str | Timestamp | Series | None = None,
68
92
  offset: int = 0,
93
+ roll: Literal["forward", "backward"] = "forward",
69
94
  holiday_list: Literal["old", "new", "infer"] = "infer",
70
95
  ) -> Timestamp | Series:
71
96
  """
@@ -74,16 +99,19 @@ def offset_bdays(
74
99
  right list of holidays based on the most recent date in the input.
75
100
 
76
101
  Args:
77
- 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.
78
104
  offset (int): The number of business days to offset the dates. Positive numbers
79
105
  offset to the next business day, negative numbers offset to the previous
80
106
  business day. Zero offsets to the same date if it's a business day, otherwise
81
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'.
82
110
  holiday_list (str, optional): The list of holidays to use. Defaults to "infer", which
83
111
  infers the right list of holidays based on the most recent date in the input.
84
112
 
85
113
  Returns:
86
- pd.Timestamp | pd.Series: The offset dates. Returns a single date if
114
+ Timestamp | Series: The offset dates. Returns a single date if
87
115
  `dates` is a single date, otherwise returns a Series of dates.
88
116
 
89
117
  Note: For more information on error handling, see numpy.busday_offset documentation at
@@ -101,16 +129,16 @@ def offset_bdays(
101
129
  >>> yd.offset_bdays(date, -1)
102
130
  Timestamp('2023-12-21') # Offset to the previous business day
103
131
  """
104
- formatted_dates = format_input_dates(dates)
132
+ normalized_dates = normalize_input_dates(dates)
105
133
 
106
134
  selected_holidays = br_holidays.get_applicable_holidays(
107
- formatted_dates, holiday_list
135
+ normalized_dates, holiday_list
108
136
  )
109
137
  selected_holidays_np = convert_to_numpy_date(selected_holidays)
110
138
 
111
- dates_np = convert_to_numpy_date(formatted_dates)
139
+ dates_np = convert_to_numpy_date(normalized_dates)
112
140
  offsetted_dates_np = np.busday_offset(
113
- dates_np, offsets=offset, roll="forward", holidays=selected_holidays_np
141
+ dates_np, offsets=offset, roll=roll, holidays=selected_holidays_np
114
142
  )
115
143
  if isinstance(offsetted_dates_np, np.datetime64):
116
144
  return pd.Timestamp(offsetted_dates_np, unit="ns")
@@ -155,14 +183,16 @@ def count_bdays(
155
183
  with Pandas data types.
156
184
 
157
185
  Args:
158
- start (str | Timestamp, optional): The start date. Defaults to None.
159
- end (str | 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.
160
190
  holiday_list (str, optional): The list of holidays to use. Defaults to "infer",
161
191
  which infers the right list of holidays based on the most recent date in
162
192
  the input.
163
193
 
164
194
  Returns:
165
- int | pd.Series: The number of business days between the start date and end date.
195
+ int | Series: The number of business days between the start date and end date.
166
196
  Returns an integer if the result is a single value, otherwise returns a Series.
167
197
 
168
198
  Notes:
@@ -183,18 +213,18 @@ def count_bdays(
183
213
  >>> yd.count_bdays(start, end)
184
214
  pd.Series([22, 40], dtype='int64')
185
215
  """
186
- formatted_start = format_input_dates(start)
187
- formatted_end = format_input_dates(end)
216
+ normalized_start = normalize_input_dates(start)
217
+ normalized_end = normalize_input_dates(end)
188
218
 
189
219
  # Determine which list of holidays to use
190
220
  selected_holidays = br_holidays.get_applicable_holidays(
191
- formatted_start, holiday_list
221
+ normalized_start, holiday_list
192
222
  )
193
223
  selected_holidays_np = convert_to_numpy_date(selected_holidays)
194
224
 
195
225
  # Convert inputs to numpy datetime64[D] before calling numpy.busday_count
196
- start_np = convert_to_numpy_date(formatted_start)
197
- end_np = convert_to_numpy_date(formatted_end)
226
+ start_np = convert_to_numpy_date(normalized_start)
227
+ end_np = convert_to_numpy_date(normalized_end)
198
228
 
199
229
  result_np = np.busday_count(start_np, end_np, holidays=selected_holidays_np)
200
230
  if isinstance(result_np, np.ndarray):
@@ -219,8 +249,10 @@ def generate_bdays(
219
249
 
220
250
 
221
251
  Args:
222
- start (str | pd.Timestamp, optional): The start date. Defaults to None.
223
- 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.
224
256
  inclusive (str, optional): Whether to include the start and end dates.
225
257
  Valid options are 'both', 'neither', 'left', 'right'. Defaults to 'both'.
226
258
  holiday_list (str, optional): The list of holidays to use. Defaults to "infer",
@@ -251,20 +283,20 @@ def generate_bdays(
251
283
  2023-12-29 2023-12-29
252
284
  dtype: object
253
285
  """
254
- formatted_start = format_input_dates(start)
255
- formatted_end = format_input_dates(end)
286
+ normalized_start = normalize_input_dates(start)
287
+ normalized_end = normalize_input_dates(end)
256
288
 
257
- if isinstance(formatted_start, Series) or isinstance(formatted_end, Series):
289
+ if isinstance(normalized_start, Series) or isinstance(normalized_end, Series):
258
290
  raise ValueError("The start and end dates must be single dates.")
259
291
 
260
292
  selected_holidays = br_holidays.get_applicable_holidays(
261
- formatted_start, holiday_list
293
+ normalized_start, holiday_list
262
294
  )
263
295
  selected_holidays_list = selected_holidays.to_list()
264
296
 
265
297
  result = pd.bdate_range(
266
- formatted_start,
267
- formatted_end,
298
+ normalized_start,
299
+ normalized_end,
268
300
  freq="C",
269
301
  inclusive=inclusive,
270
302
  holidays=selected_holidays_list,
@@ -9,6 +9,28 @@ from . import di_xml as dix
9
9
  from . import br_calendar as brc
10
10
 
11
11
 
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
+
12
34
  def get_expiration_date(expiration_code: str) -> Timestamp:
13
35
  """
14
36
  Internal function to convert the expiration code into its expiration date.
@@ -75,10 +97,10 @@ def get_expiration_date(expiration_code: str) -> Timestamp:
75
97
 
76
98
 
77
99
  def get_di(
78
- trade_date: str | Timestamp,
100
+ trade_date: str | Timestamp | None = None,
79
101
  source_type: Literal["bmf", "b3", "b3s"] = "bmf",
80
102
  return_raw: bool = False,
81
- ) -> pd.DataFrame:
103
+ ) -> DataFrame:
82
104
  """
83
105
  Gets the DI futures data for a given date from B3.
84
106
 
@@ -86,7 +108,8 @@ def get_di(
86
108
  trade date. It's the primary external interface for accessing DI data.
87
109
 
88
110
  Args:
89
- 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.
90
113
  source_type: a string indicating the source of the data. Options are "bmf",
91
114
  "b3" and "b3s" (default is "bmf").
92
115
  - "bmf" (fast) fetches DI data from old BM&FBOVESPA website. This is the default
@@ -95,15 +118,15 @@ def get_di(
95
118
  - "b3s" (fast) fetches DI data from the simplified Price Report (XML file) provided by B3.
96
119
  This option is faster than "b3" but it has less information.
97
120
  return_raw: a boolean indicating whether to return the raw DI data.
98
- data_path: a Path object indicating the path to the directory where the XML
99
- files are stored. This argument is only used when source_type is "b3" or
100
- "b3s". If not provided, data will be downloaded from B3's website.
101
121
 
102
122
  Returns:
103
123
  pd.DataFrame: A Pandas DataFrame containing the DI Futures data for
104
124
  the given trade date.
105
125
 
106
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)
107
130
  >>> get_di("2023-12-28")
108
131
 
109
132
  Notes:
@@ -113,14 +136,12 @@ def get_di(
113
136
 
114
137
  """
115
138
  # Force trade_date to be a pandas Timestamp
116
- trade_date = pd.Timestamp(trade_date)
117
- if not trade_date:
118
- raise ValueError("Uma data de referência válida deve ser fornecida.")
139
+ normalized_trade_date = normalize_date(trade_date)
119
140
 
120
141
  if source_type == "bmf":
121
- return diw.get_di(trade_date, return_raw)
142
+ return diw.get_di(normalized_trade_date, return_raw)
122
143
  elif source_type in ["b3", "b3s"]:
123
- return dix.get_di(trade_date, source_type, return_raw)
144
+ return dix.get_di(normalized_trade_date, source_type, return_raw)
124
145
  else:
125
146
  raise ValueError("source_type must be either 'bmf', 'b3' or 'b3s'.")
126
147
 
@@ -31,7 +31,7 @@ dependencies = [
31
31
  "lxml",
32
32
  ]
33
33
  dynamic = []
34
- version = "0.3.7"
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.7"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes