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.
Files changed (35) hide show
  1. {pyield-0.20.2 → pyield-0.22.0}/PKG-INFO +1 -1
  2. pyield-0.22.0/pyield/__about__.py +1 -0
  3. {pyield-0.20.2 → pyield-0.22.0}/pyield/__init__.py +4 -1
  4. {pyield-0.20.2 → pyield-0.22.0}/pyield/anbima.py +3 -3
  5. {pyield-0.20.2 → pyield-0.22.0}/pyield/b3_futures/__init__.py +3 -2
  6. {pyield-0.20.2 → pyield-0.22.0}/pyield/b3_futures/historical.py +1 -1
  7. {pyield-0.20.2 → pyield-0.22.0}/pyield/bday.py +136 -79
  8. {pyield-0.20.2 → pyield-0.22.0}/pyield/data_cache.py +5 -4
  9. {pyield-0.20.2 → pyield-0.22.0}/pyield/date_converter.py +4 -2
  10. pyield-0.22.0/pyield/di.py +198 -0
  11. pyield-0.22.0/pyield/holidays/brholidays.py +94 -0
  12. {pyield-0.20.2 → pyield-0.22.0}/pyproject.toml +1 -1
  13. pyield-0.20.2/pyield/__about__.py +0 -1
  14. pyield-0.20.2/pyield/di.py +0 -190
  15. pyield-0.20.2/pyield/holidays/brholidays.py +0 -62
  16. {pyield-0.20.2 → pyield-0.22.0}/LICENSE +0 -0
  17. {pyield-0.20.2 → pyield-0.22.0}/README.md +0 -0
  18. {pyield-0.20.2 → pyield-0.22.0}/pyield/b3_futures/common.py +0 -0
  19. {pyield-0.20.2 → pyield-0.22.0}/pyield/b3_futures/intraday.py +0 -0
  20. {pyield-0.20.2 → pyield-0.22.0}/pyield/b3_futures/xml.py +0 -0
  21. {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/__init__.py +0 -0
  22. {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/bond_tools.py +0 -0
  23. {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/lft.py +0 -0
  24. {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/ltn.py +0 -0
  25. {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/ntnb.py +0 -0
  26. {pyield-0.20.2 → pyield-0.22.0}/pyield/bonds/ntnf.py +0 -0
  27. {pyield-0.20.2 → pyield-0.22.0}/pyield/conftest.py +0 -0
  28. {pyield-0.20.2 → pyield-0.22.0}/pyield/holidays/__init__.py +0 -0
  29. {pyield-0.20.2 → pyield-0.22.0}/pyield/holidays/br_holidays_new.txt +0 -0
  30. {pyield-0.20.2 → pyield-0.22.0}/pyield/holidays/br_holidays_old.txt +0 -0
  31. {pyield-0.20.2 → pyield-0.22.0}/pyield/indicators.py +0 -0
  32. {pyield-0.20.2 → pyield-0.22.0}/pyield/interpolator.py +0 -0
  33. {pyield-0.20.2 → pyield-0.22.0}/pyield/projections.py +0 -0
  34. {pyield-0.20.2 → pyield-0.22.0}/pyield/py.typed +0 -0
  35. {pyield-0.20.2 → pyield-0.22.0}/pyield/tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PYield
3
- Version: 0.20.2
3
+ Version: 0.22.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.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
- "di",
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 get_anbima_dataframe
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 = get_anbima_dataframe()
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 = get_anbima_dataframe()
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
- TIMEZONE_BZ = timezone("America/Sao_Paulo")
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, holiday_list="old")
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 the BrHolidays class
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
- Offsets the dates to the next or previous business day, considering brazilian
61
- holidays. This function supports both single dates and collections of dates,
62
- handling them intelligently to return either a single offset date or a series of
63
- offset dates. It is a wrapper for `numpy.busday_offset` adapted for Pandas data
64
- types and holiday adjustments.
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
- future dates, negative for past dates. Zero will return the same date if
74
- it's a business day, or the next business day otherwise.
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: https://numpy.org/doc/stable/reference/generated/numpy.busday_offset.html
140
+ documentation:
141
+ https://numpy.org/doc/stable/reference/generated/numpy.busday_offset.html
149
142
  """
150
- converted_dates = dc.convert_input_dates(dates)
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
- result_dti = pd.to_datetime(offsetted_dates_np)
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
- holiday_list (Literal["old", "new", "infer"], optional):
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
- converted_start = dc.convert_input_dates(start)
267
- converted_end = dc.convert_input_dates(end)
268
-
269
- # Determine which list of holidays to use
270
- selected_holidays = br_holidays.get_applicable_holidays(
271
- converted_start, holiday_list
272
- )
273
- selected_holidays_np = dc.convert_to_numpy_date(selected_holidays)
274
-
275
- # Convert inputs to numpy datetime64[D] before calling numpy.busday_count
276
- start_np = dc.convert_to_numpy_date(converted_start)
277
- end_np = dc.convert_to_numpy_date(converted_end)
278
-
279
- result_np = np.busday_count(start_np, end_np, holidays=selected_holidays_np)
280
- if isinstance(result_np, np.integer):
281
- return int(result_np)
282
- else:
283
- return pd.Series(result_np, dtype="Int64")
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
- holiday_list: Literal["old", "new", "infer"] = "infer",
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
- holiday_list (Literal["old", "new", "infer"], optional):
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
- converted_start = dc.convert_input_dates(start)
393
+ start_pd = dc.convert_input_dates(start)
334
394
  else:
335
- converted_start = pd.Timestamp.today()
395
+ start_pd = pd.Timestamp.today()
336
396
 
337
397
  if end:
338
- converted_end = dc.convert_input_dates(end)
398
+ end_pd = dc.convert_input_dates(end)
339
399
  else:
340
- converted_end = pd.Timestamp.today()
400
+ end_pd = pd.Timestamp.today()
341
401
 
342
- selected_holidays = br_holidays.get_applicable_holidays(
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=converted_start,
350
- end=converted_end,
406
+ start=start_pd,
407
+ end=end_pd,
351
408
  freq="C",
352
409
  inclusive=inclusive,
353
- holidays=selected_holidays_list,
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
- converted_date = dc.convert_input_dates(date)
374
- shifted_date = offset(converted_date, 0) # Shift the date if it is not a bus. day
375
- return converted_date == shifted_date
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 = timezone("America/Sao_Paulo")
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 get_di_dataframe():
68
+ def get_di_dataset():
68
69
  return _data_cache.get_dataframe("di")
69
70
 
70
71
 
71
- def get_anbima_dataframe():
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 convert_to_numpy_date(
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
- else:
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]")
@@ -32,7 +32,7 @@ dependencies = [
32
32
  "types-requests",
33
33
  ]
34
34
  dynamic = []
35
- version = "0.20.2"
35
+ version = "0.22.0"
36
36
 
37
37
  [project.license]
38
38
  file = "LICENSE"
@@ -1 +0,0 @@
1
- __version__ = "0.20.2"
@@ -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