PYield 0.8.0__tar.gz → 0.8.2__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 (34) hide show
  1. {pyield-0.8.0 → pyield-0.8.2}/PKG-INFO +13 -13
  2. {pyield-0.8.0 → pyield-0.8.2}/README.md +11 -11
  3. pyield-0.8.2/pyield/__about__.py +1 -0
  4. {pyield-0.8.0 → pyield-0.8.2}/pyield/bday.py +16 -11
  5. {pyield-0.8.0 → pyield-0.8.2}/pyield/data_access.py +8 -8
  6. {pyield-0.8.0 → pyield-0.8.2}/pyield/futures/historical.py +1 -1
  7. {pyield-0.8.0 → pyield-0.8.2}/pyield/futures/intraday.py +42 -33
  8. {pyield-0.8.0 → pyield-0.8.2}/pyield/futures/xml.py +9 -3
  9. {pyield-0.8.0 → pyield-0.8.2}/pyield/holidays/core.py +10 -6
  10. {pyield-0.8.0 → pyield-0.8.2}/pyield/indicators.py +6 -5
  11. pyield-0.8.2/pyield/interpolator.py +91 -0
  12. pyield-0.8.2/pyield/ntnb.py +408 -0
  13. {pyield-0.8.0 → pyield-0.8.2}/pyield/projections.py +1 -1
  14. {pyield-0.8.0 → pyield-0.8.2}/pyproject.toml +6 -2
  15. {pyield-0.8.0 → pyield-0.8.2}/tests/test_bday.py +12 -12
  16. {pyield-0.8.0 → pyield-0.8.2}/tests/test_futures.py +2 -2
  17. {pyield-0.8.0 → pyield-0.8.2}/tests/test_futures_xml.py +16 -16
  18. pyield-0.8.0/pyield/__about__.py +0 -1
  19. pyield-0.8.0/pyield/interpolator.py +0 -78
  20. pyield-0.8.0/pyield/ntnb.py +0 -261
  21. {pyield-0.8.0 → pyield-0.8.2}/LICENSE +0 -0
  22. {pyield-0.8.0 → pyield-0.8.2}/pyield/__init__.py +0 -0
  23. {pyield-0.8.0 → pyield-0.8.2}/pyield/date_validator.py +0 -0
  24. {pyield-0.8.0 → pyield-0.8.2}/pyield/futures/__init__.py +0 -0
  25. {pyield-0.8.0 → pyield-0.8.2}/pyield/futures/common.py +0 -0
  26. {pyield-0.8.0 → pyield-0.8.2}/pyield/holidays/__init__.py +0 -0
  27. {pyield-0.8.0 → pyield-0.8.2}/pyield/holidays/br_holidays_new.txt +0 -0
  28. {pyield-0.8.0 → pyield-0.8.2}/pyield/holidays/br_holidays_old.txt +0 -0
  29. {pyield-0.8.0 → pyield-0.8.2}/pyield/py.typed +0 -0
  30. {pyield-0.8.0 → pyield-0.8.2}/pyield/spreads.py +0 -0
  31. {pyield-0.8.0 → pyield-0.8.2}/pyield/tn_bonds.py +0 -0
  32. {pyield-0.8.0 → pyield-0.8.2}/tests/__init__.py +0 -0
  33. {pyield-0.8.0 → pyield-0.8.2}/tests/data/SPRD231222.zip +0 -0
  34. {pyield-0.8.0 → pyield-0.8.2}/tests/data/SPRD240426.zip +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PYield
3
- Version: 0.8.0
3
+ Version: 0.8.2
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>
@@ -31,7 +31,7 @@ Classifier: Operating System :: OS Independent
31
31
  Classifier: Intended Audience :: Financial and Insurance Industry
32
32
  Classifier: Topic :: Office/Business :: Financial :: Investment
33
33
  Project-URL: Source, https://github.com/crdcj/PYield
34
- Requires-Python: >=3.11
34
+ Requires-Python: >=3.12
35
35
  Requires-Dist: pandas>=2.0.0
36
36
  Requires-Dist: numpy
37
37
  Requires-Dist: beautifulsoup4
@@ -74,23 +74,23 @@ pip install pyield
74
74
 
75
75
  # Count the number of business days between two dates
76
76
  # Start date is included, end date is excluded
77
- >>> yd.count_bdays(start='2023-12-29', end='2024-01-02')
77
+ >>> yd.count_bdays(start='29-12-2023', end='02-01-2024')
78
78
  1
79
79
 
80
80
  # Get the next business day after a given date (offset=1)
81
- >>> yd.offset_bdays(dates="2023-12-29", offset=1)
81
+ >>> yd.offset_bdays(dates="29-12-2023", offset=1)
82
82
  Timestamp('2024-01-02 00:00:00')
83
83
 
84
84
  # Get the next business day if it is not a business day (offset=0)
85
- >>> yd.offset_bdays(dates="2023-12-30", offset=0)
85
+ >>> yd.offset_bdays(dates="30-12-2023", offset=0)
86
86
  Timestamp('2024-01-02 00:00:00')
87
87
 
88
88
  # Since 2023-12-29 is a business day, it returns the same date (offset=0)
89
- >>> yd.offset_bdays(dates="2023-12-29", offset=0)
89
+ >>> yd.offset_bdays(dates="29-12-2023", offset=0)
90
90
  Timestamp('2023-12-29 00:00:00')
91
91
 
92
92
  # Generate a pandas series with the business days between two dates
93
- >>> yd.generate_bdays(start='2023-12-29', end='2024-01-03')
93
+ >>> yd.generate_bdays(start='29-12-2023', end='03-01-2024')
94
94
  0 2023-12-29
95
95
  1 2024-01-02
96
96
  2 2024-01-03
@@ -111,7 +111,7 @@ TradeTime TickerSymbol ExpirationDate BDaysToExp ... MaxRate LastAskRate La
111
111
  2024-04-21 13:37:39 DI1F39 2039-01-03 3683 ... <NA> <NA> <NA> <NA>
112
112
 
113
113
  # Fetch historical DI Futures data from B3
114
- >>> yd.fetch_asset(asset_code="DI1", reference_date='2024-03-08')
114
+ >>> yd.fetch_asset(asset_code="DI1", reference_date='08-03-2024')
115
115
  TradeDate TickerSymbol ExpirationDate BDaysToExp ... LastRate LastAskRate LastBidRate SettlementRate
116
116
  2024-03-08 DI1J24 2024-04-01 15 ... 10.952 10.952 10.956 10.956
117
117
  2024-03-08 DI1K24 2024-05-02 37 ... 10.776 10.774 10.780 10.777
@@ -127,7 +127,7 @@ TradeDate TickerSymbol ExpirationDate BDaysToExp ... LastRate LastAskRate LastB
127
127
  # Fetch a DataFrame with the NTN-B data from ANBIMA
128
128
  # Anbima data is available for the last 5 working days
129
129
  # Obs: Anbima members have access to the full history
130
- >>> yd.fetch_asset(asset_code="NTN-B", reference_date='2024-04-12')
130
+ >>> yd.fetch_asset(asset_code="NTN-B", reference_date='12-04-2024')
131
131
 
132
132
  BondType ReferenceDate MaturityDate BidRate AskRate IndicativeRate Price
133
133
  NTN-B 2024-04-12 2024-08-15 0.07540 0.07504 0.07523 4,271.43565
@@ -142,7 +142,7 @@ BondType ReferenceDate MaturityDate BidRate AskRate IndicativeRate Price
142
142
  ### Spreads Calculation
143
143
  ```python
144
144
  # Calculate the spread between two DI Futures contracts and the pre-fix bonds
145
- >>> yd.calculate_spreads(spread_type="di_vs_pre", reference_date="2024-4-11")
145
+ >>> yd.calculate_spreads(spread_type="di_pre", reference_date="11-04-2024")
146
146
 
147
147
  BondType ReferenceDate MaturityDate DISpread
148
148
  LTN 2024-04-11 2024-07-01 -20.28
@@ -157,15 +157,15 @@ BondType ReferenceDate MaturityDate DISpread
157
157
  ### Indicators Data
158
158
  ```python
159
159
  # Fetch the SELIC target rate from the Central Bank of Brazil
160
- >>> yd.fetch_indicator(indicator_code="SELIC", reference_date='2024-04-12')
160
+ >>> yd.fetch_indicator(indicator_code="SELIC", reference_date='12-04-2024')
161
161
  0.1075 # 10.75%
162
162
 
163
163
  # Fetch the IPCA monthly inflation rate from IBGE
164
- >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='2024-03-18')
164
+ >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='18-03-2024')
165
165
  0.16 # 0.16%
166
166
 
167
167
  # If no data is yet available for the indicator, the function returns None
168
- >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='2024-04-10')
168
+ >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='10-04-2024')
169
169
  None
170
170
  ```
171
171
 
@@ -31,23 +31,23 @@ pip install pyield
31
31
 
32
32
  # Count the number of business days between two dates
33
33
  # Start date is included, end date is excluded
34
- >>> yd.count_bdays(start='2023-12-29', end='2024-01-02')
34
+ >>> yd.count_bdays(start='29-12-2023', end='02-01-2024')
35
35
  1
36
36
 
37
37
  # Get the next business day after a given date (offset=1)
38
- >>> yd.offset_bdays(dates="2023-12-29", offset=1)
38
+ >>> yd.offset_bdays(dates="29-12-2023", offset=1)
39
39
  Timestamp('2024-01-02 00:00:00')
40
40
 
41
41
  # Get the next business day if it is not a business day (offset=0)
42
- >>> yd.offset_bdays(dates="2023-12-30", offset=0)
42
+ >>> yd.offset_bdays(dates="30-12-2023", offset=0)
43
43
  Timestamp('2024-01-02 00:00:00')
44
44
 
45
45
  # Since 2023-12-29 is a business day, it returns the same date (offset=0)
46
- >>> yd.offset_bdays(dates="2023-12-29", offset=0)
46
+ >>> yd.offset_bdays(dates="29-12-2023", offset=0)
47
47
  Timestamp('2023-12-29 00:00:00')
48
48
 
49
49
  # Generate a pandas series with the business days between two dates
50
- >>> yd.generate_bdays(start='2023-12-29', end='2024-01-03')
50
+ >>> yd.generate_bdays(start='29-12-2023', end='03-01-2024')
51
51
  0 2023-12-29
52
52
  1 2024-01-02
53
53
  2 2024-01-03
@@ -68,7 +68,7 @@ TradeTime TickerSymbol ExpirationDate BDaysToExp ... MaxRate LastAskRate La
68
68
  2024-04-21 13:37:39 DI1F39 2039-01-03 3683 ... <NA> <NA> <NA> <NA>
69
69
 
70
70
  # Fetch historical DI Futures data from B3
71
- >>> yd.fetch_asset(asset_code="DI1", reference_date='2024-03-08')
71
+ >>> yd.fetch_asset(asset_code="DI1", reference_date='08-03-2024')
72
72
  TradeDate TickerSymbol ExpirationDate BDaysToExp ... LastRate LastAskRate LastBidRate SettlementRate
73
73
  2024-03-08 DI1J24 2024-04-01 15 ... 10.952 10.952 10.956 10.956
74
74
  2024-03-08 DI1K24 2024-05-02 37 ... 10.776 10.774 10.780 10.777
@@ -84,7 +84,7 @@ TradeDate TickerSymbol ExpirationDate BDaysToExp ... LastRate LastAskRate LastB
84
84
  # Fetch a DataFrame with the NTN-B data from ANBIMA
85
85
  # Anbima data is available for the last 5 working days
86
86
  # Obs: Anbima members have access to the full history
87
- >>> yd.fetch_asset(asset_code="NTN-B", reference_date='2024-04-12')
87
+ >>> yd.fetch_asset(asset_code="NTN-B", reference_date='12-04-2024')
88
88
 
89
89
  BondType ReferenceDate MaturityDate BidRate AskRate IndicativeRate Price
90
90
  NTN-B 2024-04-12 2024-08-15 0.07540 0.07504 0.07523 4,271.43565
@@ -99,7 +99,7 @@ BondType ReferenceDate MaturityDate BidRate AskRate IndicativeRate Price
99
99
  ### Spreads Calculation
100
100
  ```python
101
101
  # Calculate the spread between two DI Futures contracts and the pre-fix bonds
102
- >>> yd.calculate_spreads(spread_type="di_vs_pre", reference_date="2024-4-11")
102
+ >>> yd.calculate_spreads(spread_type="di_pre", reference_date="11-04-2024")
103
103
 
104
104
  BondType ReferenceDate MaturityDate DISpread
105
105
  LTN 2024-04-11 2024-07-01 -20.28
@@ -114,15 +114,15 @@ BondType ReferenceDate MaturityDate DISpread
114
114
  ### Indicators Data
115
115
  ```python
116
116
  # Fetch the SELIC target rate from the Central Bank of Brazil
117
- >>> yd.fetch_indicator(indicator_code="SELIC", reference_date='2024-04-12')
117
+ >>> yd.fetch_indicator(indicator_code="SELIC", reference_date='12-04-2024')
118
118
  0.1075 # 10.75%
119
119
 
120
120
  # Fetch the IPCA monthly inflation rate from IBGE
121
- >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='2024-03-18')
121
+ >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='18-03-2024')
122
122
  0.16 # 0.16%
123
123
 
124
124
  # If no data is yet available for the indicator, the function returns None
125
- >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='2024-04-10')
125
+ >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='10-04-2024')
126
126
  None
127
127
  ```
128
128
 
@@ -0,0 +1 @@
1
+ __version__ = "0.8.2"
@@ -57,9 +57,9 @@ def is_bday(date: SingleDateTypes | None = None) -> bool:
57
57
  bool: True if the input date is a business day, False otherwise.
58
58
 
59
59
  Examples:
60
- >>> yd.is_bday('2023-12-25') # Christmas
60
+ >>> yd.is_bday("2023-12-25") # Christmas
61
61
  False
62
- >>> yd.is_bday() # Check if today is a business day
62
+ >>> yd.is_bday() # Check if today is a business day
63
63
  True
64
64
  """
65
65
  normalized_date = _normalize_input_dates(date)
@@ -140,11 +140,11 @@ def offset_bdays(
140
140
  `Series` of offset dates.
141
141
 
142
142
  Examples:
143
- >>> date = '2023-12-23' # Saturday before Christmas
143
+ >>> date = "2023-12-23" # Saturday before Christmas
144
144
  >>> yd.offset_bdays(date, 0)
145
145
  Timestamp('2023-12-26')
146
146
 
147
- >>> date = '2023-12-22' # Friday before Christmas
147
+ >>> date = "2023-12-22" # Friday before Christmas
148
148
  >>> yd.offset_bdays(date, 0)
149
149
  Timestamp('2023-12-22') # No offset because it's a business day
150
150
 
@@ -172,10 +172,15 @@ def offset_bdays(
172
172
  dates_np, offsets=offset, roll=roll, holidays=selected_holidays_np
173
173
  )
174
174
  if isinstance(offsetted_dates_np, np.datetime64):
175
- return pd.Timestamp(offsetted_dates_np, unit="ns")
175
+ result = pd.Timestamp(offsetted_dates_np)
176
+ # Force to datetime[ns] if the input was a single date
177
+ result = result.as_unit("ns")
176
178
  else:
177
- result = pd.to_datetime(offsetted_dates_np, unit="ns")
178
- return pd.Series(result)
179
+ result = pd.to_datetime(offsetted_dates_np)
180
+ # Force the result to be a Series if the input was not a single date
181
+ result = pd.Series(result).astype("datetime64[ns]")
182
+
183
+ return result
179
184
 
180
185
 
181
186
  @overload
@@ -252,11 +257,11 @@ def count_bdays(
252
257
  https://numpy.org/doc/stable/reference/generated/numpy.busday_count.html.
253
258
 
254
259
  Examples:
255
- >>> yd.count_bdays('2023-12-15', '2024-01-01')
260
+ >>> yd.count_bdays("2023-12-15", "2024-01-01")
256
261
  10
257
262
 
258
- >>> start = '2023-01-01'
259
- >>> end = pd.to_datetime(['2023-01-31', '2023-03-01'])
263
+ >>> start = "2023-01-01"
264
+ >>> end = pd.to_datetime(["2023-01-31", "2023-03-01"])
260
265
  >>> yd.count_bdays(start, end)
261
266
  pd.Series([22, 40], dtype='int64')
262
267
  """
@@ -315,7 +320,7 @@ def generate_bdays(
315
320
  start and end dates, considering the specified holidays.
316
321
 
317
322
  Examples:
318
- >>> yd.generate_bdays(start='2023-12-22', end='2024-01-02')
323
+ >>> yd.generate_bdays(start="2023-12-22", end="2024-01-02")
319
324
  pd.Series(['2023-12-22', '2023-12-26', '2023-12-27', '2023-12-28', '2023-12-29',
320
325
  '2024-01-02'], dtype='datetime64[ns]')
321
326
 
@@ -42,8 +42,8 @@ def fetch_asset(
42
42
  ValueError: If the asset code is not recognized or supported.
43
43
 
44
44
  Examples:
45
- >>> fetch_asset('TRB', '31-05-2024')
46
- >>> fetch_asset('DI1', '31-05-2024')
45
+ >>> fetch_asset("TRB", "31-05-2024")
46
+ >>> fetch_asset("DI1", "31-05-2024")
47
47
  """
48
48
  SUPPORTED_BONDS = ["LTN", "LFT", "NTN-F", "NTN-B"]
49
49
  SUPPORTED_FUTURES = ["DI1", "DDI", "FRC", "DAP", "DOL", "WDO", "IND", "WIN"]
@@ -99,11 +99,11 @@ def fetch_indicator(
99
99
  ValueError: If the indicator code is not recognized or supported.
100
100
 
101
101
  Examples:
102
- >>> fetch_indicator('SELIC', '31-05-2024')
102
+ >>> fetch_indicator("SELIC", "31-05-2024")
103
103
  0.1075 # Indicates a SELIC target rate of 10.75% p.a.
104
- >>> fetch_indicator('IPCA', '01-04-2024')
104
+ >>> fetch_indicator("IPCA", "01-04-2024")
105
105
  0.0038 # Indicates an IPCA monthly rate of 0.38% p.m.
106
- >>> fetch_indicator('DI', '31-05-2024')
106
+ >>> fetch_indicator("DI", "31-05-2024")
107
107
  0.00040168 # Indicates a DI daily rate of 0.02% p.d.
108
108
  """
109
109
  normalized_date = dv.normalize_date(reference_date)
@@ -142,7 +142,7 @@ def fetch_projection(projection_code: str) -> pr.IndicatorProjection:
142
142
  ValueError: If the projection code is not recognized or supported.
143
143
 
144
144
  Examples:
145
- >>> fetch_projection('IPCA_CM')
145
+ >>> fetch_projection("IPCA_CM")
146
146
  IndicatorProjection(
147
147
  last_updated=pd.Timestamp('2024-04-19 18:55:00'),
148
148
  reference_month_ts=pd.Timestamp('2024-04-01 00:00:00'),
@@ -166,7 +166,7 @@ def calculate_spreads(
166
166
 
167
167
  Parameters:
168
168
  spread_type (str): The type of spread to calculate. Available options are:
169
- - "DI_vs_PRE": the spread between DI Futures and Treasury Pre-Fixed bonds.
169
+ - "DI_PRE": the spread between DI Futures and Treasury Pre-Fixed bonds.
170
170
  reference_date (str | pd.Timestamp, optional): The reference date for the
171
171
  spread calculation. If None or not provided, defaults to the previous
172
172
  business day according to the Brazilian calendar.
@@ -180,7 +180,7 @@ def calculate_spreads(
180
180
  """
181
181
  # Normalize the reference date
182
182
  normalized_date = dv.normalize_date(reference_date)
183
- if spread_type.upper() == "DI_VS_PRE":
183
+ if spread_type.upper() == "DI_PRE":
184
184
  return sp.calculate_di_spreads(normalized_date)
185
185
  else:
186
186
  raise ValueError("Invalid spread type.")
@@ -119,7 +119,7 @@ def fetch_raw_df(asset_code: str, trade_date: pd.Timestamp) -> pd.DataFrame:
119
119
  url_date = trade_date.strftime("%d/%m/%Y")
120
120
  # url example: https://www2.bmf.com.br/pages/portal/bmfbovespa/boletim1/SistemaPregao_excel1.asp?Data=05/10/2023&Mercadoria=DI1
121
121
  url = f"https://www2.bmf.com.br/pages/portal/bmfbovespa/boletim1/SistemaPregao_excel1.asp?Data={url_date}&Mercadoria={asset_code}&XLS=false"
122
- r = requests.get(url)
122
+ r = requests.get(url, timeout=10)
123
123
 
124
124
  text = r.text
125
125
  if "VENCTO" not in text:
@@ -1,10 +1,20 @@
1
+ from io import StringIO
2
+
1
3
  import pandas as pd
2
4
  import requests
3
5
 
4
6
  from .. import bday
5
7
 
6
8
 
7
- def _fetch_raw_df(future_code: str) -> pd.DataFrame:
9
+ # Função para salvar DataFrame em CSV e ler com read_csv
10
+ def convert_with_read_csv(df):
11
+ buffer = StringIO()
12
+ df.to_csv(buffer, index=False)
13
+ buffer.seek(0) # Reposiciona o cursor no início do buffer
14
+ return pd.read_csv(buffer, dtype_backend="numpy_nullable")
15
+
16
+
17
+ def _fetch_b3_df(future_code: str) -> pd.DataFrame:
8
18
  """
9
19
  Fetch the latest data for a given future code from B3 derivatives quotation API.
10
20
 
@@ -21,47 +31,21 @@ def _fetch_raw_df(future_code: str) -> pd.DataFrame:
21
31
  url = f"https://cotacao.b3.com.br/mds/api/v1/DerivativeQuotation/{future_code}"
22
32
 
23
33
  try:
24
- r = requests.get(url)
34
+ r = requests.get(url, timeout=10)
25
35
  r.raise_for_status() # Check for HTTP request errors
26
36
  except requests.exceptions.RequestException:
27
37
  raise Exception(f"Failed to fetch data for {future_code}.") from None
28
38
 
29
39
  r.encoding = "utf-8" # Explicitly set response encoding to utf-8 for consistency
30
40
 
31
- if "Quotation not available" in r.text:
41
+ if "Quotation not available" in r.text or "curPrc" not in r.text:
32
42
  return pd.DataFrame()
33
43
 
34
44
  # Normalize JSON response into a flat table
35
45
  df = pd.json_normalize(r.json()["Scty"])
36
46
 
37
- # Clean and reformat the DataFrame columns
38
- df.columns = (df.columns
39
- .str.replace("SctyQtn.", "")
40
- .str.replace("asset.AsstSummry.", "")
41
- ) # fmt: skip
42
- df.drop(columns=["desc", "asset.code", "mkt.cd"], inplace=True)
43
-
44
- # Convert maturity codes to datetime and drop rows with missing values
45
- df["mtrtyCode"] = pd.to_datetime(df["mtrtyCode"], errors="coerce")
46
- df.dropna(subset=["mtrtyCode"], inplace=True)
47
-
48
- # Sort the DataFrame by maturity code and reset the index
49
- df.sort_values("mtrtyCode", inplace=True, ignore_index=True)
50
-
51
- # Get currante date
52
- today = pd.Timestamp.now().normalize()
53
- df["TradeDate"] = today
54
-
55
- # Get current date and time
56
- now = pd.Timestamp.now().round("s")
57
- # Subtract 15 minutes from the current time to account for API delay
58
- trade_ts = now - pd.Timedelta(minutes=15)
59
- df["TradeTime"] = trade_ts
60
-
61
47
  # Convert DataFrame to use nullable data types for better type consistency
62
- df = df.convert_dtypes(dtype_backend="numpy_nullable")
63
-
64
- return df
48
+ return convert_with_read_csv(df)
65
49
 
66
50
 
67
51
  def _rename_columns(df: pd.DataFrame) -> pd.DataFrame:
@@ -101,6 +85,32 @@ def _rename_columns(df: pd.DataFrame) -> pd.DataFrame:
101
85
  def _process_df(raw_df: pd.DataFrame) -> pd.DataFrame:
102
86
  df = raw_df.copy()
103
87
 
88
+ # Clean and reformat the DataFrame columns
89
+ df.columns = (df.columns
90
+ .str.replace("SctyQtn.", "")
91
+ .str.replace("asset.AsstSummry.", "")
92
+ ) # fmt: skip
93
+ df.drop(columns=["desc", "asset.code", "mkt.cd"], inplace=True)
94
+
95
+ df = _rename_columns(df)
96
+
97
+ # Convert maturity codes to datetime and drop rows with missing values
98
+ df["ExpirationDate"] = pd.to_datetime(df["ExpirationDate"], errors="coerce")
99
+ df.dropna(subset=["ExpirationDate"], inplace=True)
100
+
101
+ # Sort the DataFrame by maturity code and reset the index
102
+ df.sort_values("ExpirationDate", inplace=True, ignore_index=True)
103
+
104
+ # Get currante date
105
+ today = pd.Timestamp.now().normalize()
106
+ df["TradeDate"] = today
107
+
108
+ # Get current date and time
109
+ now = pd.Timestamp.now().round("s")
110
+ # Subtract 15 minutes from the current time to account for API delay
111
+ trade_ts = now - pd.Timedelta(minutes=15)
112
+ df["TradeTime"] = trade_ts
113
+
104
114
  df["BDaysToExp"] = bday.count_bdays(df["TradeDate"], df["ExpirationDate"])
105
115
 
106
116
  df["DaysToExp"] = (df["ExpirationDate"] - df["TradeDate"]).dt.days
@@ -156,10 +166,9 @@ def fetch_intraday_df(future_code: str) -> pd.DataFrame:
156
166
  Returns:
157
167
  pd.DataFrame: A Pandas pd.DataFrame containing the latest DI futures data.
158
168
  """
159
- raw_df = _fetch_raw_df(future_code)
169
+ raw_df = _fetch_b3_df(future_code)
160
170
  if raw_df.empty:
161
171
  return raw_df
162
- df = _rename_columns(raw_df)
163
- df = _process_df(df)
172
+ df = _process_df(raw_df)
164
173
  df = _select_and_reorder_columns(df)
165
174
  return df
@@ -43,7 +43,7 @@ def _get_file_from_url(trade_date: pd.Timestamp, source_type: str) -> io.BytesIO
43
43
  else:
44
44
  raise ValueError("Invalid source type. Must be either 'PR' or 'SPR'.")
45
45
 
46
- response = requests.get(url)
46
+ response = requests.get(url, timeout=10)
47
47
 
48
48
  # File will be considered invalid if it is too small
49
49
  if response.status_code != 200 or len(response.content) < 1024:
@@ -74,9 +74,15 @@ def _extract_xml_from_zip(zip_file: io.BytesIO) -> io.BytesIO:
74
74
 
75
75
  def _extract_data_from_xml(xml_file: io.BytesIO, asset_code: str) -> list[dict]:
76
76
  parser = etree.XMLParser(
77
- ns_clean=True, remove_blank_text=True, remove_comments=True, recover=True
77
+ ns_clean=True,
78
+ remove_blank_text=True,
79
+ remove_comments=True,
80
+ recover=True,
81
+ resolve_entities=False,
82
+ no_network=True,
83
+ load_dtd=False, # Disable DTD loading
78
84
  )
79
- tree = etree.parse(xml_file, parser)
85
+ tree = etree.parse(xml_file, parser=parser)
80
86
 
81
87
  # XPath para encontrar elementos cujo texto começa com código do ativo: DI1, FRC...
82
88
  namespaces = {"ns": "urn:bvmf.217.01.xsd"}
@@ -16,10 +16,13 @@ class BrHolidays:
16
16
  self.old_holidays = self._load_holidays(old_holidays_path)
17
17
 
18
18
  def _load_holidays(self, file_path: Path) -> pd.Series:
19
- """Loads the list of holidays from a text file and returns it as a pd.Series of pd.Timestamp."""
19
+ """
20
+ Loads the holidays from a text file and returns it as a Series of Timestamps.
21
+ """
20
22
  df = pd.read_csv(file_path, header=None, names=["date"], comment="#")
21
23
  # Convert the dates to a pd.Series of pd.Timestamp
22
- return pd.to_datetime(df["date"], format="%d/%m/%Y")
24
+ holidays = pd.to_datetime(df["date"], format="%d/%m/%Y")
25
+ return holidays.astype("datetime64[ns]")
23
26
 
24
27
  def get_applicable_holidays(
25
28
  self,
@@ -27,13 +30,14 @@ class BrHolidays:
27
30
  holiday_list: Literal["old", "new", "infer"] = "infer",
28
31
  ) -> pd.Series:
29
32
  """
30
- Returns the correct list of holidays to use based on the most recent date in the input.
33
+ Returns the correct list of holidays to use based on the most recent date in
34
+ the input.
31
35
 
32
36
  Args:
33
37
  dates (pd.Timestamp | pd.Series): The date(s) to use to infer the holidays.
34
- holiday_list (str): The holidays list to use. Valid options are 'old', 'new' or
35
- 'infer'. If 'infer' is used, the list of holidays is selected based on the
36
- earliest (minimum) date in the input.
38
+ holiday_list (str): The holidays list to use. Valid options are 'old', 'new'
39
+ or 'infer'. If 'infer' is used, the list of holidays is selected based
40
+ on the earliest (minimum) date in the input.
37
41
 
38
42
  Returns:
39
43
  pd.Series: The list of holidays to use.
@@ -30,7 +30,7 @@ def fetch_ipca_mr(reference_date: pd.Timestamp) -> float | None:
30
30
  api_url = f"https://servicodados.ibge.gov.br/api/v3/agregados/6691/periodos/{ipca_date}/variaveis/63?localidades=N1[all]"
31
31
 
32
32
  # Send a GET request to the API
33
- response = requests.get(api_url)
33
+ response = requests.get(api_url, timeout=10)
34
34
 
35
35
  # Raises HTTPError, if one occurred
36
36
  response.raise_for_status()
@@ -50,7 +50,7 @@ def fetch_selic_target(reference_date: pd.Timestamp) -> float | None:
50
50
  # https://api.bcb.gov.br/dados/serie/bcdata.sgs.432/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
51
51
  selic_date = reference_date.strftime("%d/%m/%Y")
52
52
  api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.432/dados?formato=json&dataInicial={selic_date}&dataFinal={selic_date}"
53
- response = requests.get(api_url)
53
+ response = requests.get(api_url, timeout=10)
54
54
  response.raise_for_status()
55
55
 
56
56
  if selic_date in response.text:
@@ -64,7 +64,7 @@ def fetch_di(reference_date: pd.Timestamp) -> float | None:
64
64
  # https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial=12/04/2024&dataFinal=12/04/2024
65
65
  di_date = reference_date.strftime("%d/%m/%Y")
66
66
  api_url = f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial={di_date}&dataFinal={di_date}"
67
- response = requests.get(api_url)
67
+ response = requests.get(api_url, timeout=10)
68
68
  response.raise_for_status()
69
69
 
70
70
  if di_date in response.text:
@@ -78,9 +78,10 @@ def fetch_vna_selic(reference_date: pd.Timestamp) -> float | None:
78
78
  # url example: https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/20240418APC238
79
79
 
80
80
  url_base = "https://www3.bcb.gov.br/novoselic/rest/arquivosDiarios/pub/download/3/"
81
- url_file = f"{reference_date.strftime("%Y%m%d")}APC238"
81
+ url_file = f"{reference_date.strftime('%Y%m%d')}APC238"
82
82
  url_vna = url_base + url_file
83
- response = requests.get(url_vna)
83
+
84
+ response = requests.get(url_vna, timeout=10)
84
85
  response.raise_for_status()
85
86
  file_text = response.text
86
87
 
@@ -0,0 +1,91 @@
1
+ import bisect
2
+
3
+
4
+ def interpolate_flat_forward(
5
+ prev_rate: float, prev_bdays: int, next_rate: float, next_bdays: int, bdays: int
6
+ ) -> float:
7
+ """Performs interest rate interpolation using the flat forward interpolation method
8
+ considering a base of 252 business days.
9
+
10
+ The interpolation is done between two known pairs of vertices (prev_rate,
11
+ prev_business_days) and (next_rate, next_business_days), for a third vertex defined
12
+ by the number of business days 'business_days' where 'prev_business_days <
13
+ business_days < next_business_days'. This third vertex is the point in time for
14
+ which the interest rate is being calculated.
15
+
16
+ Args:
17
+ - prev_rate (float): Interest rate of the previous vertex.
18
+ - prev_bdays (int): Number of business days of the previous vertex.
19
+ - next_rate (float): Interest rate of the next vertex.
20
+ - next_bdays (int): Number of business days of the next vertex.
21
+ - bdays (int): Number of business days for which the interest rate is to be
22
+ interpolated.
23
+
24
+ Example:
25
+ interpolated_rate = interpolate_flat_forward(0.045, 30, 0.05, 60, 45)
26
+
27
+ Returns:
28
+ float: The interpolated interest rate at the given `business_days`.
29
+ """
30
+ a = (1 + prev_rate) ** (prev_bdays / 252)
31
+ b = (1 + next_rate) ** (next_bdays / 252)
32
+ c = (bdays - prev_bdays) / (next_bdays - prev_bdays)
33
+
34
+ return (a * (b / a) ** c) ** (252 / bdays) - 1
35
+
36
+
37
+ def find_and_interpolate_flat_forward(
38
+ bdays: int,
39
+ known_bdays: list[int],
40
+ known_rates: list[float],
41
+ ) -> float:
42
+ """
43
+ Finds the appropriate interpolation point and returns the interest rate
44
+ interpolated by the flat forward method from that point. Uses the `bisect` module
45
+ for binary search in ordered lists.
46
+
47
+
48
+ Args:
49
+ business_days (int): Number of business days for which the flat forward interest
50
+ rate is to be calculated.
51
+ known_business_days (List[int]): List of business days where interest rates are
52
+ known.
53
+ known_rates (List[float]): List of known interest rates.
54
+
55
+ Notes:
56
+ - It is assumed that `known_business_days` and `known_rates` are sorted and have
57
+ the same size.
58
+ - The method uses 252 business days per year in the interpolation, which is the
59
+ standard in the Brazilian market.
60
+ - Special cases are handled for situations where `business_days` is less than
61
+ the first known business day, greater than the last known business day, or
62
+ exactly equal to a known business day, avoiding the need for interpolation.
63
+
64
+ Example:
65
+ known_bdays = [30, 60, 90]
66
+ known_rates = [0.045, 0.05, 0.055]
67
+ interpolated_rate = find_and_interpolate_flat_forward(45, known_bdays, known_rates)
68
+
69
+ Returns:
70
+ float: The interest rate interpolated by the flat forward method for the given
71
+ number of business days.
72
+ """
73
+ # Special cases
74
+ if bdays <= known_bdays[0]:
75
+ return known_rates[0]
76
+ elif bdays >= known_bdays[-1]:
77
+ return known_rates[-1]
78
+ # Do not interpolate vertex whose rate is known
79
+ elif bdays in known_bdays:
80
+ return known_rates[known_bdays.index(bdays)]
81
+
82
+ # Find i such that known_business_days[i-1] < business_days < known_business_days[i]
83
+ i = bisect.bisect_left(known_bdays, bdays)
84
+
85
+ return interpolate_flat_forward(
86
+ known_rates[i - 1],
87
+ known_bdays[i - 1],
88
+ known_rates[i],
89
+ known_bdays[i],
90
+ bdays,
91
+ )