PYield 0.7.7__tar.gz → 0.7.9__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 (32) hide show
  1. {pyield-0.7.7 → pyield-0.7.9}/PKG-INFO +7 -7
  2. {pyield-0.7.7 → pyield-0.7.9}/README.md +5 -5
  3. pyield-0.7.9/pyield/__about__.py +1 -0
  4. {pyield-0.7.7 → pyield-0.7.9}/pyield/__init__.py +6 -2
  5. {pyield-0.7.7 → pyield-0.7.9}/pyield/data_access.py +58 -24
  6. pyield-0.7.9/pyield/futures/__init__.py +4 -0
  7. pyield-0.7.7/pyield/futures/common.py → pyield-0.7.9/pyield/futures/historical.py +130 -78
  8. pyield-0.7.9/pyield/futures/intraday.py +107 -0
  9. pyield-0.7.9/pyield/spreads.py +56 -0
  10. {pyield-0.7.7 → pyield-0.7.9}/pyield/treasuries.py +0 -52
  11. {pyield-0.7.7 → pyield-0.7.9}/pyproject.toml +2 -2
  12. pyield-0.7.9/tests/test_futures.py +38 -0
  13. pyield-0.7.7/pyield/futures/di_xml.py → pyield-0.7.9/tests/xml.py +4 -4
  14. pyield-0.7.7/pyield/__about__.py +0 -1
  15. pyield-0.7.7/pyield/data_analysis.py +0 -33
  16. pyield-0.7.7/pyield/futures/__init__.py +0 -13
  17. pyield-0.7.7/pyield/futures/ddi.py +0 -101
  18. pyield-0.7.7/pyield/futures/di.py +0 -160
  19. pyield-0.7.7/pyield/futures/frc.py +0 -61
  20. pyield-0.7.7/tests/test_futures.py +0 -80
  21. {pyield-0.7.7 → pyield-0.7.9}/LICENSE +0 -0
  22. {pyield-0.7.7 → pyield-0.7.9}/pyield/bday.py +0 -0
  23. {pyield-0.7.7 → pyield-0.7.9}/pyield/holidays/__init__.py +0 -0
  24. {pyield-0.7.7 → pyield-0.7.9}/pyield/holidays/br_holidays_new.txt +0 -0
  25. {pyield-0.7.7 → pyield-0.7.9}/pyield/holidays/br_holidays_old.txt +0 -0
  26. {pyield-0.7.7 → pyield-0.7.9}/pyield/holidays/core.py +0 -0
  27. {pyield-0.7.7 → pyield-0.7.9}/pyield/indicators.py +0 -0
  28. {pyield-0.7.7 → pyield-0.7.9}/pyield/projections.py +0 -0
  29. {pyield-0.7.7 → pyield-0.7.9}/pyield/py.typed +0 -0
  30. {pyield-0.7.7 → pyield-0.7.9}/pyield/utils.py +0 -0
  31. {pyield-0.7.7 → pyield-0.7.9}/tests/__init__.py +0 -0
  32. {pyield-0.7.7 → pyield-0.7.9}/tests/test_bday.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PYield
3
- Version: 0.7.7
3
+ Version: 0.7.9
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>
@@ -32,13 +32,13 @@ 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
34
  Requires-Python: >=3.11
35
- Requires-Dist: requests
36
35
  Requires-Dist: pandas>=2.0.0
37
36
  Requires-Dist: numpy
38
37
  Requires-Dist: beautifulsoup4
39
38
  Requires-Dist: html5lib
40
39
  Requires-Dist: lxml
41
40
  Requires-Dist: python-calamine>=0.2.0
41
+ Requires-Dist: requests>=2.31.0
42
42
  Description-Content-Type: text/markdown
43
43
 
44
44
  [![PyPI version](https://img.shields.io/pypi/v/pyield.svg)](https://pypi.python.org/pypi/pyield)
@@ -101,7 +101,7 @@ dtype: datetime64[ns]
101
101
  ```python
102
102
  # Fetch current DI Futures data from B3 (15 minutes delay)
103
103
  >>> yd.fetch_asset(asset_code="DI1")
104
- TradeTimestamp TickerSymbol ExpirationDate BDaysToExp ... MaxRate LastAskRate LastBidRate LastRate
104
+ TradeTimestamp TickerSymbol ExpirationDate BDaysToExp ... MaxRate LastAskRate LastBidRate CurrentRate
105
105
  2024-04-21 13:37:39 DI1K24 2024-05-02 7 ... 0.10660 0.10652 0.10660 0.10660
106
106
  2024-04-21 13:37:39 DI1M24 2024-06-03 28 ... 0.10518 0.10510 0.10516 0.10518
107
107
  2024-04-21 13:37:39 DI1N24 2024-07-01 48 ... 0.10480 0.10456 0.10462 0.10460
@@ -158,11 +158,11 @@ BondType ReferenceDate MaturityDate DISpread
158
158
  ```python
159
159
  # Fetch the SELIC target rate from the Central Bank of Brazil
160
160
  >>> yd.fetch_indicator(indicator_code="SELIC", reference_date='2024-04-12')
161
- 10.75
161
+ 0.1075 # 10.75%
162
162
 
163
163
  # Fetch the IPCA monthly inflation rate from IBGE
164
164
  >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='2024-03-18')
165
- 0.16
165
+ 0.16 # 0.16%
166
166
 
167
167
  # If no data is yet available for the indicator, the function returns None
168
168
  >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='2024-04-10')
@@ -178,9 +178,9 @@ IndicatorProjection(
178
178
  last_updated=Timestamp('2024-04-19 18:55:00'),
179
179
  reference_month_ts=Timestamp('2024-04-01 00:00:00'),
180
180
  reference_month_br='ABR/2024',
181
- projected_value=0.0035
181
+ projected_value=0.0035 # 0.35%
182
182
  )
183
183
  >>> ipca.projected_value
184
- 0.0035
184
+ 0.0035 # 0.35%
185
185
  ```
186
186
 
@@ -58,7 +58,7 @@ dtype: datetime64[ns]
58
58
  ```python
59
59
  # Fetch current DI Futures data from B3 (15 minutes delay)
60
60
  >>> yd.fetch_asset(asset_code="DI1")
61
- TradeTimestamp TickerSymbol ExpirationDate BDaysToExp ... MaxRate LastAskRate LastBidRate LastRate
61
+ TradeTimestamp TickerSymbol ExpirationDate BDaysToExp ... MaxRate LastAskRate LastBidRate CurrentRate
62
62
  2024-04-21 13:37:39 DI1K24 2024-05-02 7 ... 0.10660 0.10652 0.10660 0.10660
63
63
  2024-04-21 13:37:39 DI1M24 2024-06-03 28 ... 0.10518 0.10510 0.10516 0.10518
64
64
  2024-04-21 13:37:39 DI1N24 2024-07-01 48 ... 0.10480 0.10456 0.10462 0.10460
@@ -115,11 +115,11 @@ BondType ReferenceDate MaturityDate DISpread
115
115
  ```python
116
116
  # Fetch the SELIC target rate from the Central Bank of Brazil
117
117
  >>> yd.fetch_indicator(indicator_code="SELIC", reference_date='2024-04-12')
118
- 10.75
118
+ 0.1075 # 10.75%
119
119
 
120
120
  # Fetch the IPCA monthly inflation rate from IBGE
121
121
  >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='2024-03-18')
122
- 0.16
122
+ 0.16 # 0.16%
123
123
 
124
124
  # If no data is yet available for the indicator, the function returns None
125
125
  >>> yd.fetch_indicator(indicator_code="IPCA", reference_date='2024-04-10')
@@ -135,9 +135,9 @@ IndicatorProjection(
135
135
  last_updated=Timestamp('2024-04-19 18:55:00'),
136
136
  reference_month_ts=Timestamp('2024-04-01 00:00:00'),
137
137
  reference_month_br='ABR/2024',
138
- projected_value=0.0035
138
+ projected_value=0.0035 # 0.35%
139
139
  )
140
140
  >>> ipca.projected_value
141
- 0.0035
141
+ 0.0035 # 0.35%
142
142
  ```
143
143
 
@@ -0,0 +1 @@
1
+ __version__ = "0.7.9"
@@ -1,7 +1,11 @@
1
1
  from .__about__ import __version__
2
2
  from .bday import count_bdays, generate_bdays, is_bday, offset_bdays
3
- from .data_access import fetch_asset, fetch_indicator, fetch_projection
4
- from .data_analysis import calculate_spreads
3
+ from .data_access import (
4
+ calculate_spreads,
5
+ fetch_asset,
6
+ fetch_indicator,
7
+ fetch_projection,
8
+ )
5
9
 
6
10
  __all__ = [
7
11
  "__version__",
@@ -3,14 +3,13 @@ import pandas as pd
3
3
  from . import futures as ft
4
4
  from . import indicators as it
5
5
  from . import projections as pr
6
+ from . import spreads as sp
6
7
  from . import treasuries as tr
7
8
  from .utils import _normalize_date
8
9
 
9
10
 
10
11
  def fetch_asset(
11
- asset_code: str,
12
- reference_date: str | pd.Timestamp | None = None,
13
- **kwargs,
12
+ asset_code: str, reference_date: str | pd.Timestamp | None = None
14
13
  ) -> pd.DataFrame:
15
14
  """
16
15
  Fetches data for a specified asset based on type and reference date.
@@ -24,6 +23,11 @@ def fetch_asset(
24
23
  - "DI1": One-day Interbank Deposit Futures (Futuro de DI) from B3.
25
24
  - "DDI": DI x U.S. Dollar Spread Futures (Futuro de Cupom Cambial) from B3.
26
25
  - "FRC": Forward Rate Agreement (FRA) from B3.
26
+ - "DAP": DI x IPCA Spread Futures.
27
+ - "DOL": U.S. Dollar Futures from B3.
28
+ - "WDO": Mini U.S. Dollar Futures from B3.
29
+ - "IND": Ibovespa Futures from B3.
30
+ - "WIN": Mini Ibovespa Futures from B3.
27
31
  reference_date (str | pd.Timestamp | None): The reference date for which data is
28
32
  fetched. Defaults to the last business day if None.
29
33
  **kwargs: Additional keyword arguments, specifically:
@@ -40,27 +44,28 @@ def fetch_asset(
40
44
  >>> fetch_asset('TRB', '2023-04-01')
41
45
  >>> fetch_asset('DI1', '2023-04-01', return_raw=True)
42
46
  """
43
- return_raw = kwargs.get("return_raw", False)
47
+ SUPPORTED_BONDS = ["LTN", "LFT", "NTN-F", "NTN-B"]
48
+ SUPPORTED_FUTURES = ["DI1", "DDI", "FRC", "DAP", "DOL", "WDO", "IND", "WIN"]
49
+
44
50
  normalized_date = _normalize_date(reference_date)
45
51
 
46
- if asset_code.lower() == "trb":
47
- return tr.fetch_bonds(reference_date=normalized_date, return_raw=return_raw)
48
- elif asset_code.lower() in ["ltn", "lft", "ntn-f", "ntn-b"]:
52
+ today = pd.Timestamp.today().normalize()
53
+ if normalized_date == today:
54
+ return ft.fetch_intraday_df(future_code=asset_code.upper())
55
+
56
+ if asset_code.upper() == "TRB":
57
+ return tr.fetch_bonds(reference_date=normalized_date)
58
+
59
+ if asset_code.upper() in SUPPORTED_BONDS:
49
60
  df = tr.fetch_bonds(reference_date=normalized_date)
50
61
  return df.query(f"BondType == '{asset_code.upper()}'")
51
62
 
52
- elif asset_code.lower() == "di1":
53
- today = pd.Timestamp.today().normalize()
54
- if normalized_date == today:
55
- return ft.fetch_last_di()
56
- else:
57
- return ft.fetch_past_di(trade_date=normalized_date, return_raw=return_raw)
58
- elif asset_code.lower() == "ddi":
59
- return ft.fetch_past_ddi(trade_date=normalized_date, return_raw=return_raw)
60
- elif asset_code.lower() == "frc":
61
- return ft.fetch_past_frc(trade_date=normalized_date, return_raw=return_raw)
62
- else:
63
- raise ValueError("Asset type not supported.")
63
+ if asset_code.upper() in SUPPORTED_FUTURES:
64
+ return ft.fetch_historical_df(
65
+ asset_code=asset_code.upper(), trade_date=normalized_date
66
+ )
67
+
68
+ raise ValueError("Asset type not supported.")
64
69
 
65
70
 
66
71
  def fetch_indicator(
@@ -99,13 +104,13 @@ def fetch_indicator(
99
104
  """
100
105
  normalized_date = _normalize_date(reference_date)
101
106
 
102
- if indicator_code.lower() == "selic":
107
+ if indicator_code.upper() == "SELIC":
103
108
  return it.fetch_selic_target(reference_date=normalized_date)
104
- elif indicator_code.lower() == "ipca":
109
+ elif indicator_code.upper() == "IPCA":
105
110
  return it.fetch_ipca_mr(reference_date=normalized_date)
106
- elif indicator_code.lower() == "di":
111
+ elif indicator_code.upper() == "DI":
107
112
  return it.fetch_di(reference_date=normalized_date)
108
- elif indicator_code.lower() == "vna_lft":
113
+ elif indicator_code.upper() == "VNA_LFT":
109
114
  return it.fetch_vna_selic(reference_date=normalized_date)
110
115
  else:
111
116
  raise ValueError("Indicator type not supported.")
@@ -142,7 +147,36 @@ def fetch_projection(projection_code: str) -> pr.IndicatorProjection:
142
147
  )
143
148
  """
144
149
 
145
- if projection_code.lower() == "ipca_cm":
150
+ if projection_code.upper() == "IPCA_CM":
146
151
  return pr.fetch_current_month_ipca_projection()
147
152
  else:
148
153
  raise ValueError("Projection type not supported.")
154
+
155
+
156
+ def calculate_spreads(
157
+ spread_type: str, reference_date: str | pd.Timestamp | None = None
158
+ ) -> pd.DataFrame:
159
+ """
160
+ Calculates spreads between assets based on the specified spread type.
161
+ If no reference date is provided, the function uses the previous business day.
162
+
163
+ Parameters:
164
+ spread_type (str): The type of spread to calculate. Available options are:
165
+ - "DI_vs_PRE": the spread between DI Futures and Treasury Pre-Fixed bonds.
166
+ reference_date (str | pd.Timestamp, optional): The reference date for the
167
+ spread calculation. If None or not provided, defaults to the previous
168
+ business day according to the Brazilian calendar.
169
+
170
+ Returns:
171
+ pd.DataFrame: A DataFrame containing the calculated spread in basis points.
172
+ The data is sorted by asset type and maturity/expiration date.
173
+
174
+ Raises:
175
+ ValueError: If an invalid spread type is provided.
176
+ """
177
+ # Normalize the reference date
178
+ normalized_date = _normalize_date(reference_date)
179
+ if spread_type.upper() == "DI_VS_PRE":
180
+ return sp.calculate_di_spreads(normalized_date)
181
+ else:
182
+ raise ValueError("Invalid spread type.")
@@ -0,0 +1,4 @@
1
+ from .historical import fetch_historical_df
2
+ from .intraday import fetch_intraday_df
3
+
4
+ __all__ = ["fetch_historical_df", "fetch_intraday_df"]
@@ -5,26 +5,15 @@ import requests
5
5
 
6
6
  from .. import bday
7
7
 
8
- COLUMNS_MAPPING = {
9
- "VENCTO": "ExpirationCode",
10
- "CONTR. ABERT.(1)": "OpenContracts", # At the start of the day
11
- "CONTR. FECH.(2)": "OpenContractsEndSession", # At the end of the day
12
- "NÚM. NEGOC.": "TradeCount",
13
- "CONTR. NEGOC.": "TradeVolume",
14
- "VOL.": "FinancialVolume",
15
- "AJUSTE": "SettlementPrice",
16
- "AJUSTE ANTER. (3)": "PrevSettlementRate",
17
- "AJUSTE CORRIG. (4)": "AdjSettlementRate",
18
- "AJUSTE DE REF.": "SettlementRate", # FRC
19
- "PREÇO MÍN.": "MinRate",
20
- "PREÇO MÉD.": "AvgRate",
21
- "PREÇO MÁX.": "MaxRate",
22
- "PREÇO ABERTU.": "FirstRate",
23
- "ÚLT. PREÇO": "CloseRate",
24
- "VAR. PTOS.": "PointsVariation",
25
- # Attention: bid/ask rates are inverted
26
- "ÚLT.OF. COMPRA": "CloseAskRate",
27
- "ÚLT.OF. VENDA": "CloseBidRate",
8
+ COUNT_CONVENTIONS = {
9
+ "DAP": 252,
10
+ "DI1": 252,
11
+ "DDI": 360,
12
+ "FRC": None,
13
+ "DOL": None,
14
+ "WDO": None,
15
+ "IND": None,
16
+ "WIN": None,
28
17
  }
29
18
 
30
19
 
@@ -153,7 +142,33 @@ def get_old_expiration_date(
153
142
  return pd.NaT # type: ignore
154
143
 
155
144
 
156
- def fetch_past_raw_df(asset_code: str, trade_date: pd.Timestamp) -> pd.DataFrame:
145
+ def convert_prices_to_rates(
146
+ prices: pd.Series, days_to_expiration: pd.Series, count_convention: int
147
+ ) -> pd.Series:
148
+ """
149
+ Internal function to convert DI futures prices to rates.
150
+
151
+ Args:
152
+ prices (pd.Series): The futures prices to be converted.
153
+ days_to_expiration (pd.Series): The number of days to expiration for each price.
154
+ count_convention (int): The count convention for the DI futures contract.
155
+ Can be 252 business days or 360 calendar days.
156
+
157
+ Returns:
158
+ pd.Series: A pd.Series containing the futures rates.
159
+ """
160
+ if count_convention == 252:
161
+ rates = (100_000 / prices) ** (252 / days_to_expiration) - 1
162
+ elif count_convention == 360:
163
+ rates = (100_000 / prices - 1) * (360 / days_to_expiration)
164
+ else:
165
+ raise ValueError("Invalid count_convention. Must be 252 or 360.")
166
+
167
+ # Round to 5 (3 in %) dec. places (contract's current max. precision)
168
+ return rates.round(5)
169
+
170
+
171
+ def fetch_raw_df(asset_code: str, trade_date: pd.Timestamp) -> pd.DataFrame:
157
172
  """
158
173
  Fetch the historical futures data from B3 for a specific trade date. If the data is
159
174
  not available, an empty DataFrame is returned.
@@ -199,60 +214,17 @@ def fetch_past_raw_df(asset_code: str, trade_date: pd.Timestamp) -> pd.DataFrame
199
214
  return df
200
215
 
201
216
 
202
- def fetch_last_raw_df(future_code: str) -> pd.DataFrame:
203
- """
204
- Fetch the latest data for a given future code from B3 derivatives quotation API.
205
-
206
- Args:
207
- future_code (str): The future code to fetch data for.
208
-
209
- Returns:
210
- pd.DataFrame: A DataFrame containing the normalized and cleaned data from the API.
211
-
212
- Raises:
213
- Exception: An exception is raised if the data fetch operation fails.
214
- """
215
-
216
- url = f"https://cotacao.b3.com.br/mds/api/v1/DerivativeQuotation/{future_code}"
217
-
218
- try:
219
- r = requests.get(url)
220
- r.raise_for_status() # Check for HTTP request errors
221
- except requests.exceptions.RequestException:
222
- raise Exception(f"Failed to fetch data for {future_code}.") from None
223
-
224
- r.encoding = "utf-8" # Explicitly set response encoding to utf-8 for consistency
217
+ def _adjust_older_contracts_rates(df: pd.DataFrame, rate_cols: list) -> pd.DataFrame:
218
+ for col in rate_cols:
219
+ df[col] = convert_prices_to_rates(df[col], df["BDaysToExp"], 252)
225
220
 
226
- # Normalize JSON response into a flat table
227
- df = pd.json_normalize(r.json()["Scty"])
228
-
229
- # Clean and reformat the DataFrame columns
230
- df.columns = (df.columns
231
- .str.replace("SctyQtn.", "")
232
- .str.replace("asset.AsstSummry.", "")
233
- ) # fmt: skip
234
- df.drop(columns=["desc", "asset.code", "mkt.cd"], inplace=True)
235
-
236
- # Convert maturity codes to datetime and drop rows with missing values
237
- df["mtrtyCode"] = pd.to_datetime(df["mtrtyCode"], errors="coerce")
238
- df.dropna(subset=["mtrtyCode"], inplace=True)
239
-
240
- # Sort the DataFrame by maturity code and reset the index
241
- df.sort_values("mtrtyCode", inplace=True, ignore_index=True)
242
-
243
- # Get current date and time
244
- now = pd.Timestamp.now().round("s")
245
- # Subtract 15 minutes from the current time to account for API delay
246
- trade_ts = now - pd.Timedelta(minutes=15)
247
- df["TradeTimestamp"] = trade_ts
248
-
249
- # Convert DataFrame to use nullable data types for better type consistency
250
- df = df.convert_dtypes(dtype_backend="numpy_nullable")
221
+ # Invert low and high prices
222
+ df["MinRate"], df["MaxRate"] = df["MaxRate"], df["MinRate"]
251
223
 
252
224
  return df
253
225
 
254
226
 
255
- def rename_columns(df: pd.DataFrame):
227
+ def rename_columns(df: pd.DataFrame) -> pd.DataFrame:
256
228
  all_columns = {
257
229
  "VENCTO": "ExpirationCode",
258
230
  "CONTR. ABERT.(1)": "OpenContracts", # At the start of the day
@@ -274,13 +246,68 @@ def rename_columns(df: pd.DataFrame):
274
246
  "ÚLT.OF. COMPRA": "CloseAskRate",
275
247
  "ÚLT.OF. VENDA": "CloseBidRate",
276
248
  }
277
- rename_dict = {}
278
- for col in all_columns:
279
- if col in df.columns:
280
- rename_dict[col] = all_columns[col]
249
+ rename_dict = {c: all_columns[c] for c in all_columns if c in df.columns}
281
250
  return df.rename(columns=rename_dict)
282
251
 
283
252
 
253
+ def process_df(
254
+ df: pd.DataFrame,
255
+ trade_date: pd.Timestamp,
256
+ asset_code: str,
257
+ ) -> pd.DataFrame:
258
+ df["TradeDate"] = trade_date
259
+ # Convert to datetime64[ns] since it is pandas default type for timestamps
260
+ df["TradeDate"] = df["TradeDate"].astype("datetime64[ns]")
261
+
262
+ df["TickerSymbol"] = asset_code + df["ExpirationCode"]
263
+
264
+ # Contract code format was changed in 22/05/2006
265
+ if trade_date < pd.Timestamp("2006-05-22"):
266
+ df["ExpirationDate"] = df["ExpirationCode"].apply(
267
+ get_old_expiration_date, args=(trade_date,)
268
+ )
269
+ else:
270
+ df["ExpirationDate"] = df["ExpirationCode"].apply(get_expiration_date)
271
+
272
+ df["DaysToExp"] = (df["ExpirationDate"] - trade_date).dt.days
273
+ # Convert to nullable integer, since it is the default type in the library
274
+ df["DaysToExp"] = df["DaysToExp"].astype(pd.Int64Dtype())
275
+ # Remove expired contracts
276
+ df.query("DaysToExp > 0", inplace=True)
277
+
278
+ df["BDaysToExp"] = bday.count_bdays(trade_date, df["ExpirationDate"])
279
+
280
+ # Columns where 0 means NaN
281
+ cols_with_nan = [col for col in df.columns if "Rate" in col]
282
+ if "SettlementPrice" in df.columns:
283
+ cols_with_nan.append("SettlementPrice")
284
+ # Replace 0 with NaN in these columns
285
+ df[cols_with_nan] = df[cols_with_nan].replace(0, pd.NA)
286
+
287
+ rate_cols = [col for col in df.columns if "Rate" in col]
288
+ # Prior to 17/01/2002 (inclusive), DI prices were not converted to rates
289
+ if trade_date <= pd.Timestamp("2002-01-17") and asset_code == "DI1":
290
+ df = _adjust_older_contracts_rates(df, rate_cols)
291
+ else:
292
+ # Remove % and round to 5 (3 in %) dec. places in rate columns
293
+ df[rate_cols] = df[rate_cols].div(100).round(5)
294
+
295
+ if COUNT_CONVENTIONS[asset_code] == 252:
296
+ df["SettlementRate"] = convert_prices_to_rates(
297
+ prices=df["SettlementPrice"],
298
+ days_to_expiration=df["BDaysToExp"],
299
+ count_convention=252,
300
+ )
301
+ elif COUNT_CONVENTIONS[asset_code] == 360:
302
+ df["SettlementRate"] = convert_prices_to_rates(
303
+ prices=df["SettlementPrice"],
304
+ days_to_expiration=df["DaysToExp"],
305
+ count_convention=360,
306
+ )
307
+
308
+ return df
309
+
310
+
284
311
  def reorder_columns(df: pd.DataFrame):
285
312
  all_columns = [
286
313
  "TradeDate",
@@ -305,8 +332,33 @@ def reorder_columns(df: pd.DataFrame):
305
332
  "CloseBidRate",
306
333
  "CloseRate",
307
334
  ]
308
- reordered_columns = []
309
- for col in all_columns:
310
- if col in df.columns:
311
- reordered_columns.append(col)
335
+ reordered_columns = [col for col in all_columns if col in df.columns]
312
336
  return df[reordered_columns]
337
+
338
+
339
+ def fetch_historical_df(
340
+ asset_code: str,
341
+ trade_date: pd.Timestamp,
342
+ ) -> pd.DataFrame:
343
+ """
344
+ Fetchs the futures data for a given date from B3.
345
+
346
+ This function fetches and processes the futures data from B3 for a specific
347
+ trade date. It's the primary external interface for accessing futures data.
348
+
349
+ Args:
350
+ asset_code (str): The asset code to fetch the futures data.
351
+ trade_date (pd.Timestamp): The trade date to fetch the futures data.
352
+ count_convention (int): The count convention for the DI futures contract.
353
+ Can be 252 business days or 360 calendar days.
354
+
355
+ Returns:
356
+ pd.DataFrame: A Pandas pd.DataFrame containing processed futures data.
357
+ """
358
+ df_raw = fetch_raw_df(asset_code=asset_code, trade_date=trade_date)
359
+ if df_raw.empty:
360
+ return df_raw
361
+ df = rename_columns(df_raw)
362
+ df = process_df(df, trade_date, asset_code)
363
+ df = reorder_columns(df)
364
+ return df
@@ -0,0 +1,107 @@
1
+ import pandas as pd
2
+ import requests
3
+
4
+
5
+ def _fetch_raw_df(future_code: str) -> pd.DataFrame:
6
+ """
7
+ Fetch the latest data for a given future code from B3 derivatives quotation API.
8
+
9
+ Args:
10
+ future_code (str): The future code to fetch data for.
11
+
12
+ Returns:
13
+ pd.DataFrame: A DataFrame containing the normalized and cleaned data from the API.
14
+
15
+ Raises:
16
+ Exception: An exception is raised if the data fetch operation fails.
17
+ """
18
+
19
+ url = f"https://cotacao.b3.com.br/mds/api/v1/DerivativeQuotation/{future_code}"
20
+
21
+ try:
22
+ r = requests.get(url)
23
+ r.raise_for_status() # Check for HTTP request errors
24
+ except requests.exceptions.RequestException:
25
+ raise Exception(f"Failed to fetch data for {future_code}.") from None
26
+
27
+ r.encoding = "utf-8" # Explicitly set response encoding to utf-8 for consistency
28
+
29
+ if "Quotation not available" in r.text:
30
+ return pd.DataFrame()
31
+
32
+ # Normalize JSON response into a flat table
33
+ df = pd.json_normalize(r.json()["Scty"])
34
+
35
+ # Clean and reformat the DataFrame columns
36
+ df.columns = (df.columns
37
+ .str.replace("SctyQtn.", "")
38
+ .str.replace("asset.AsstSummry.", "")
39
+ ) # fmt: skip
40
+ df.drop(columns=["desc", "asset.code", "mkt.cd"], inplace=True)
41
+
42
+ # Convert maturity codes to datetime and drop rows with missing values
43
+ df["mtrtyCode"] = pd.to_datetime(df["mtrtyCode"], errors="coerce")
44
+ df.dropna(subset=["mtrtyCode"], inplace=True)
45
+
46
+ # Sort the DataFrame by maturity code and reset the index
47
+ df.sort_values("mtrtyCode", inplace=True, ignore_index=True)
48
+
49
+ # Get current date and time
50
+ now = pd.Timestamp.now().round("s")
51
+ # Subtract 15 minutes from the current time to account for API delay
52
+ trade_ts = now - pd.Timedelta(minutes=15)
53
+ df["TradeTimestamp"] = trade_ts
54
+
55
+ # Convert DataFrame to use nullable data types for better type consistency
56
+ df = df.convert_dtypes(dtype_backend="numpy_nullable")
57
+
58
+ return df
59
+
60
+
61
+ def _process_raw_df(raw_df: pd.DataFrame) -> pd.DataFrame:
62
+ df = raw_df.copy()
63
+
64
+ # Columns to be renamed
65
+ all_columns = {
66
+ "TradeTimestamp": "TradeTimestamp",
67
+ "symb": "TickerSymbol",
68
+ "mtrtyCode": "ExpirationDate",
69
+ "BDaysToExp": "BDaysToExp",
70
+ "opnCtrcts": "OpenContracts",
71
+ "tradQty": "TradeCount",
72
+ "traddCtrctsQty": "TradeVolume",
73
+ "grssAmt": "FinancialVolume",
74
+ "prvsDayAdjstmntPric": "PrevSettlementRate",
75
+ "bottomLmtPric": "MinLimitRate",
76
+ "topLmtPric": "MaxLimitRate",
77
+ "opngPric": "OpenRate",
78
+ "minPric": "MinRate",
79
+ "avrgPric": "AvgRate",
80
+ "maxPric": "MaxRate",
81
+ "buyOffer.price": "CurrentAskRate",
82
+ "sellOffer.price": "CurrentBidRate",
83
+ "curPrc": "CurrentRate",
84
+ }
85
+ # Check which columns are present in the DataFrame before renaming
86
+ rename_dict = {c: all_columns[c] for c in all_columns if c in df.columns}
87
+ df = df.rename(columns=rename_dict)
88
+
89
+ # df["BDaysToExp"] = bd.count_bdays(df["TradeTimestamp"], df["ExpirationDate"])
90
+
91
+ # Remove percentage in all rate columns
92
+ rate_cols = [col for col in df.columns if "Rate" in col]
93
+ df[rate_cols] = df[rate_cols] / 100
94
+
95
+ # Reorder columns based on the order of the dictionary
96
+ return df[rename_dict.values()]
97
+
98
+
99
+ def fetch_intraday_df(future_code: str) -> pd.DataFrame:
100
+ """
101
+ Fetch the latest DI futures data from B3.
102
+
103
+ Returns:
104
+ pd.DataFrame: A Pandas pd.DataFrame containing the latest DI futures data.
105
+ """
106
+ raw_df = _fetch_raw_df(future_code)
107
+ return _process_raw_df(raw_df)
@@ -0,0 +1,56 @@
1
+ import pandas as pd
2
+
3
+ from . import treasuries as tr
4
+ from .futures import historical as fh
5
+
6
+ # Constant for conversion to basis points
7
+ BPS_CONVERSION_FACTOR = 10_000
8
+
9
+
10
+ def calculate_di_spreads(reference_date: pd.Timestamp) -> pd.DataFrame:
11
+ """
12
+ Calculates the DI spread for Brazilian treasury bonds (LTN and NTN-F) based on
13
+ ANBIMA's indicative rates.
14
+
15
+ This function fetches the indicative rates for Brazilian treasury securities (LTN
16
+ and NTN-F bonds) and the DI futures rates for a specified reference date,
17
+ calculating the spread between these rates in basis points. If no reference date is
18
+ provided, the function uses the previous business day.
19
+
20
+ Parameters:
21
+ reference_date (str | pd.Timestamp, optional): The reference date for the DI
22
+ spread calculation.
23
+
24
+ Returns:
25
+ pd.DataFrame: A DataFrame containing the bond type, reference date, maturity
26
+ date, and the calculated DI spread in basis points. The data is sorted by
27
+ bond type and maturity date.
28
+ """
29
+ # Fetch DI rates for the reference date
30
+ df_di = fh.fetch_historical_df(asset_code="DI1", trade_date=reference_date)[
31
+ ["ExpirationDate", "SettlementRate"]
32
+ ]
33
+
34
+ # Renaming the columns to match the ANBIMA structure
35
+ df_di.rename(columns={"ExpirationDate": "MaturityDate"}, inplace=True)
36
+
37
+ # Adjusting maturity date to match bond data format
38
+ df_di["MaturityDate"] = df_di["MaturityDate"].dt.to_period("M").dt.to_timestamp()
39
+
40
+ # Fetch bond rates, filtering for LTN and NTN-F types
41
+ df_anbima = tr.fetch_bonds(reference_date, False)
42
+ df_anbima.query("BondType in ['LTN', 'NTN-F']", inplace=True)
43
+
44
+ # Merge bond and DI rates by maturity date to calculate spreads
45
+ df_final = pd.merge(df_anbima, df_di, how="left", on="MaturityDate")
46
+
47
+ # Calculate the DI spread as the difference between indicative and settlement rates
48
+ df_final["DISpread"] = df_final["IndicativeRate"] - df_final["SettlementRate"]
49
+
50
+ # Convert spread to basis points for clarity
51
+ df_final["DISpread"] = (BPS_CONVERSION_FACTOR * df_final["DISpread"]).round(2)
52
+
53
+ # Prepare and return the final sorted DataFrame
54
+ select_columns = ["BondType", "ReferenceDate", "MaturityDate", "DISpread"]
55
+ df_final = df_final[select_columns].copy()
56
+ return df_final.sort_values(["BondType", "MaturityDate"], ignore_index=True)
@@ -3,15 +3,10 @@ import io
3
3
  import pandas as pd
4
4
  import requests
5
5
 
6
- from . import futures as ft
7
-
8
6
  # URL Constants
9
7
  ANBIMA_NON_MEMBER_URL = "https://www.anbima.com.br/informacoes/merc-sec/arqs/"
10
8
  ANBIMA_MEMBER_URL = "http://www.anbima.associados.rtm/merc_sec/arqs/"
11
9
 
12
- # Constant for conversion to basis points
13
- BPS_CONVERSION_FACTOR = 10_000
14
-
15
10
 
16
11
  def _get_anbima_content(reference_date: pd.Timestamp) -> str:
17
12
  url_date = reference_date.strftime("%y%m%d")
@@ -128,50 +123,3 @@ def fetch_bonds(reference_date: pd.Timestamp, return_raw=False) -> pd.DataFrame:
128
123
  df = _process_raw_df(df)
129
124
 
130
125
  return df
131
-
132
-
133
- def calculate_di_spreads(reference_date: pd.Timestamp) -> pd.DataFrame:
134
- """
135
- Calculates the DI spread for Brazilian treasury bonds (LTN and NTN-F) based on
136
- ANBIMA's indicative rates.
137
-
138
- This function fetches the indicative rates for Brazilian treasury securities (LTN
139
- and NTN-F bonds) and the DI futures rates for a specified reference date,
140
- calculating the spread between these rates in basis points. If no reference date is
141
- provided, the function uses the previous business day.
142
-
143
- Parameters:
144
- reference_date (str | pd.Timestamp, optional): The reference date for the DI
145
- spread calculation.
146
-
147
- Returns:
148
- pd.DataFrame: A DataFrame containing the bond type, reference date, maturity
149
- date, and the calculated DI spread in basis points. The data is sorted by
150
- bond type and maturity date.
151
- """
152
- # Fetch DI rates and adjust the maturity date format for compatibility
153
- df_di = ft.fetch_past_di(reference_date)[["ExpirationDate", "SettlementRate"]]
154
-
155
- # Renaming the columns to match the ANBIMA structure
156
- df_di.rename(columns={"ExpirationDate": "MaturityDate"}, inplace=True)
157
-
158
- # Adjusting maturity date to match bond data format
159
- df_di["MaturityDate"] = df_di["MaturityDate"].dt.to_period("M").dt.to_timestamp()
160
-
161
- # Fetch bond rates, filtering for LTN and NTN-F types
162
- df_anbima = fetch_bonds(reference_date, False)
163
- df_anbima.query("BondType in ['LTN', 'NTN-F']", inplace=True)
164
-
165
- # Merge bond and DI rates by maturity date to calculate spreads
166
- df_final = pd.merge(df_anbima, df_di, how="left", on="MaturityDate")
167
-
168
- # Calculate the DI spread as the difference between indicative and settlement rates
169
- df_final["DISpread"] = df_final["IndicativeRate"] - df_final["SettlementRate"]
170
-
171
- # Convert spread to basis points for clarity
172
- df_final["DISpread"] = (BPS_CONVERSION_FACTOR * df_final["DISpread"]).round(2)
173
-
174
- # Prepare and return the final sorted DataFrame
175
- select_columns = ["BondType", "ReferenceDate", "MaturityDate", "DISpread"]
176
- df_final = df_final[select_columns].copy()
177
- return df_final.sort_values(["BondType", "MaturityDate"], ignore_index=True)
@@ -23,16 +23,16 @@ classifiers = [
23
23
  "Topic :: Office/Business :: Financial :: Investment",
24
24
  ]
25
25
  dependencies = [
26
- "requests",
27
26
  "pandas>=2.0.0",
28
27
  "numpy",
29
28
  "beautifulsoup4",
30
29
  "html5lib",
31
30
  "lxml",
32
31
  "python-calamine>=0.2.0",
32
+ "requests>=2.31.0",
33
33
  ]
34
34
  dynamic = []
35
- version = "0.7.7"
35
+ version = "0.7.9"
36
36
 
37
37
  [project.license]
38
38
  file = "LICENSE"
@@ -0,0 +1,38 @@
1
+ import pyield as yd
2
+
3
+
4
+ def test_settlement_rate_with_old_holiday_list():
5
+ settlement_rates = {
6
+ "DI1N27": 0.09809,
7
+ "DI1F33": 0.10368,
8
+ }
9
+
10
+ # 22-12-2023 is before the new holiday calendar
11
+ df = yd.fetch_asset(asset_code="DI1", reference_date="2023-12-22")
12
+ tickers = list(settlement_rates.keys()) # noqa: F841
13
+ result = df.query("TickerSymbol in @tickers")["SettlementRate"].to_list()
14
+ assert result == list(settlement_rates.values())
15
+
16
+
17
+ def test_settlement_rates_with_current_holiday_list():
18
+ settlement_rates = {
19
+ "DI1F24": 0.11644,
20
+ "DI1J24": 0.11300,
21
+ "DI1N24": 0.10786,
22
+ "DI1V24": 0.10321,
23
+ "DI1F25": 0.10031,
24
+ "DI1J25": 0.09852,
25
+ "DI1N25": 0.09715,
26
+ "DI1V25": 0.09651,
27
+ "DI1F26": 0.09583,
28
+ "DI1N26": 0.09631,
29
+ "DI1F27": 0.09683,
30
+ "DI1N27": 0.09794,
31
+ "DI1F29": 0.10042,
32
+ "DI1F31": 0.10240,
33
+ "DI1F33": 0.10331,
34
+ }
35
+ df = yd.fetch_asset(asset_code="DI1", reference_date="2023-12-26")
36
+ tickers = list(settlement_rates.keys()) # noqa: F841
37
+ results = df.query("TickerSymbol in @tickers")["SettlementRate"].to_list()
38
+ assert results == list(settlement_rates.values())
@@ -7,8 +7,8 @@ import requests
7
7
  from lxml import etree
8
8
  from pandas import DataFrame, Timestamp
9
9
 
10
- from .. import bday
11
- from . import common as cm
10
+ from ..pyield import bday
11
+ from ..pyield.futures import historical as ht
12
12
 
13
13
 
14
14
  def _get_file_from_url(trade_date: Timestamp, source_type: str) -> io.BytesIO:
@@ -206,7 +206,7 @@ def _standardize_column_names(df: DataFrame) -> DataFrame:
206
206
  "TradAvrgPric": "AvgRate",
207
207
  "MaxPric": "MaxRate",
208
208
  "FrstPric": "FirstRate",
209
- "LastPric": "LastRate",
209
+ "LastPric": "CloseRate",
210
210
  "AdjstdQtTax": "SettlementRate",
211
211
  # "RglrTxsQty"
212
212
  # "RglrTraddCtrcts"
@@ -229,7 +229,7 @@ def _process_di_df(df_raw: DataFrame) -> DataFrame:
229
229
  # Convert to datetime64[ns] since it is pandas default type for timestamps
230
230
  df["TradDt"] = df["TradDt"].astype("datetime64[ns]")
231
231
 
232
- expiration = df["TckrSymb"].str[3:].apply(cm.get_expiration_date)
232
+ expiration = df["TckrSymb"].str[3:].apply(ht.get_expiration_date)
233
233
  df.insert(2, "ExpirationDate", expiration)
234
234
 
235
235
  business_days = bday.count_bdays(df["TradDt"], df["ExpirationDate"])
@@ -1 +0,0 @@
1
- __version__ = "0.7.7"
@@ -1,33 +0,0 @@
1
- import pandas as pd
2
-
3
- from . import treasuries as tr
4
- from .utils import _normalize_date
5
-
6
-
7
- def calculate_spreads(
8
- spread_type: str, reference_date: str | pd.Timestamp | None = None
9
- ) -> pd.DataFrame:
10
- """
11
- Calculates spreads between assets based on the specified spread type.
12
- If no reference date is provided, the function uses the previous business day.
13
-
14
- Parameters:
15
- spread_type (str): The type of spread to calculate. Available options are:
16
- - "DI_vs_PRE": the spread between DI Futures and Treasury Pre-Fixed bonds.
17
- reference_date (str | pd.Timestamp, optional): The reference date for the
18
- spread calculation. If None or not provided, defaults to the previous
19
- business day according to the Brazilian calendar.
20
-
21
- Returns:
22
- pd.DataFrame: A DataFrame containing the calculated spread in basis points.
23
- The data is sorted by asset type and maturity/expiration date.
24
-
25
- Raises:
26
- ValueError: If an invalid spread type is provided.
27
- """
28
- # Normalize the reference date
29
- normalized_date = _normalize_date(reference_date)
30
- if spread_type.lower() == "di_vs_pre":
31
- return tr.calculate_di_spreads(normalized_date)
32
- else:
33
- raise ValueError("Invalid spread type.")
@@ -1,13 +0,0 @@
1
- from .common import get_expiration_date, get_old_expiration_date
2
- from .ddi import fetch_past_ddi
3
- from .di import fetch_last_di, fetch_past_di
4
- from .frc import fetch_past_frc
5
-
6
- __all__ = [
7
- "fetch_past_di",
8
- "fetch_last_di",
9
- "fetch_past_ddi",
10
- "fetch_past_frc",
11
- "get_expiration_date",
12
- "get_old_expiration_date",
13
- ]
@@ -1,101 +0,0 @@
1
- import pandas as pd
2
-
3
- from . import common as cm
4
-
5
-
6
- def _convert_prices_to_rates(prices: pd.Series, n_days: pd.Series) -> pd.Series:
7
- """
8
- Internal function to convert DDI futures prices to rates.
9
-
10
- Args:
11
- prices (pd.Series): A pd.Series containing DDI futures prices.
12
- bd (pd.Series): A serie containing the number of days to expiration.
13
-
14
- Returns:
15
- pd.Series: A pd.Series containing DDI futures rates.
16
- """
17
- rates = (100_000 / prices - 1) * (360 / n_days)
18
- # Round to 5 (3 in %) dec. places (contract's current max. precision)
19
- return rates.round(5)
20
-
21
-
22
- def _process_raw_df(df: pd.DataFrame, trade_date: pd.Timestamp) -> pd.DataFrame:
23
- """
24
- Internal function to process and transform raw DI futures data.
25
-
26
- Args:
27
- df (pd.DataFrame): the raw DI DataFrame.
28
- trade_date: a datetime-like object representing the trade date.
29
-
30
- Returns:
31
- pd.DataFrame: Processed and transformed data as a Pandas pd.DataFrame.
32
- """
33
- df = cm.rename_columns(df)
34
-
35
- df["TradeDate"] = trade_date
36
- # Convert to datetime64[ns] since it is pandas default type for timestamps
37
- df["TradeDate"] = df["TradeDate"].astype("datetime64[ns]")
38
-
39
- # Contract code format was changed in 22/05/2006
40
- if trade_date < pd.Timestamp("2006-05-22"):
41
- df["ExpirationDate"] = df["ExpirationCode"].apply(
42
- cm.get_old_expiration_date, args=(trade_date,)
43
- )
44
- else:
45
- df["ExpirationDate"] = df["ExpirationCode"].apply(cm.get_expiration_date)
46
-
47
- df["DaysToExp"] = (df["ExpirationDate"] - trade_date).dt.days
48
- # Convert to nullable integer, since other columns use this data type
49
- df["DaysToExp"] = df["DaysToExp"].astype(pd.Int64Dtype())
50
-
51
- # Remove expired contracts
52
- df.query("DaysToExp > 0", inplace=True)
53
-
54
- # Columns where 0 means NaN
55
- rate_cols = [col for col in df.columns if "Rate" in col]
56
- cols_with_nan = rate_cols + ["SettlementPrice"]
57
- # Replace 0 with NaN in these columns
58
- df[cols_with_nan] = df[cols_with_nan].replace(0, pd.NA)
59
-
60
- df[rate_cols] = df[rate_cols].div(100).round(5)
61
-
62
- # Calculate SettlementRate
63
- df["SettlementRate"] = _convert_prices_to_rates(
64
- df["SettlementPrice"], df["DaysToExp"]
65
- )
66
-
67
- df["TickerSymbol"] = "DI1" + df["ExpirationCode"]
68
-
69
- # Filter and order columns
70
- df = cm.reorder_columns(df)
71
-
72
- return df
73
-
74
-
75
- def fetch_past_ddi(trade_date: pd.Timestamp, return_raw: bool = False) -> pd.DataFrame:
76
- """
77
- Fetchs the DDI futures data for a given date from B3.
78
-
79
- This function fetches and processes the DI futures data from B3 for a specific
80
- trade date. It's the primary external interface for accessing DI data.
81
-
82
- Args:
83
- trade_date (pd.Timestamp): The trade date to fetch the DI futures data.
84
- raw (bool): If True, returns the raw data as a Pandas pd.DataFrame.
85
- Defaults to False.
86
-
87
- Returns:
88
- pd.DataFrame: A Pandas pd.DataFrame containing processed DI futures data.
89
-
90
- Examples:
91
- >>> from pyield.futures import di
92
- >>> di.fetch_ddi(pd.Timestamp("2021-01-04"))
93
-
94
- Notes:
95
- - DaysToExp: number of business days to ExpirationDate.
96
- - OpenContracts: number of open contracts at the start of the trading day.
97
- """
98
- df_raw = cm.fetch_past_raw_df(asset_code="DDI", trade_date=trade_date)
99
- if return_raw or df_raw.empty:
100
- return df_raw
101
- return _process_raw_df(df_raw, trade_date)
@@ -1,160 +0,0 @@
1
- import pandas as pd
2
-
3
- from .. import bday as bd
4
- from . import common as cm
5
-
6
-
7
- def _convert_prices_to_rates(prices: pd.Series, bd: pd.Series) -> pd.Series:
8
- """
9
- Internal function to convert DI futures prices to rates.
10
-
11
- Args:
12
- prices (pd.Series): A pd.Series containing DI futures prices.
13
- bd (pd.Series): A serie containing the number of business days to expiration.
14
-
15
- Returns:
16
- pd.Series: A pd.Series containing DI futures rates.
17
- """
18
- rates = (100_000 / prices) ** (252 / bd) - 1
19
-
20
- # Round to 5 (3 in %) dec. places (contract's current max. precision)
21
- return rates.round(5)
22
-
23
-
24
- def _adjust_older_contracts_rates(df: pd.DataFrame, rate_cols: list) -> pd.DataFrame:
25
- for col in rate_cols:
26
- df[col] = _convert_prices_to_rates(df[col], df["BDaysToExp"])
27
-
28
- # Invert low and high prices
29
- df["MinRate"], df["MaxRate"] = df["MaxRate"], df["MinRate"]
30
-
31
- return df
32
-
33
-
34
- def _process_past_raw_df(df: pd.DataFrame, trade_date: pd.Timestamp) -> pd.DataFrame:
35
- """
36
- Internal function to process and transform raw DI futures data.
37
-
38
- Args:
39
- df (pd.DataFrame): the raw DI DataFrame.
40
- trade_date: a datetime-like object representing the trade date.
41
-
42
- Returns:
43
- pd.DataFrame: Processed and transformed data as a Pandas pd.DataFrame.
44
- """
45
- df = cm.rename_columns(df)
46
-
47
- df["TradeDate"] = trade_date
48
- # Convert to datetime64[ns] since it is pandas default type for timestamps
49
- df["TradeDate"] = df["TradeDate"].astype("datetime64[ns]")
50
-
51
- # Contract code format was changed in 22/05/2006
52
- if trade_date < pd.Timestamp("2006-05-22"):
53
- df["ExpirationDate"] = df["ExpirationCode"].apply(
54
- cm.get_old_expiration_date, args=(trade_date,)
55
- )
56
- else:
57
- df["ExpirationDate"] = df["ExpirationCode"].apply(cm.get_expiration_date)
58
-
59
- df["BDaysToExp"] = bd.count_bdays(trade_date, df["ExpirationDate"])
60
-
61
- # Remove expired contracts
62
- df.query("BDaysToExp > 0", inplace=True)
63
-
64
- # Columns where 0 means NaN
65
- rate_cols = [col for col in df.columns if "Rate" in col]
66
- cols_with_nan = rate_cols + ["SettlementPrice"]
67
- # Replace 0 with NaN in these columns
68
- df[cols_with_nan] = df[cols_with_nan].replace(0, pd.NA)
69
-
70
- # Prior to 17/01/2002 (inclusive), prices were not converted to rates
71
- if trade_date > pd.Timestamp("2002-01-17"):
72
- # Remove % and round to 5 (3 in %) dec. places in rate columns
73
- df[rate_cols] = df[rate_cols].div(100).round(5)
74
- else:
75
- df = _adjust_older_contracts_rates(df, rate_cols)
76
-
77
- # Calculate SettlementRate
78
- df["SettlementRate"] = _convert_prices_to_rates(
79
- df["SettlementPrice"], df["BDaysToExp"]
80
- )
81
-
82
- df["TickerSymbol"] = "DI1" + df["ExpirationCode"]
83
-
84
- # Filter and order columns
85
- df = cm.reorder_columns(df)
86
- return df
87
-
88
-
89
- def _process_last_raw_di_df(raw_df: pd.DataFrame) -> pd.DataFrame:
90
- df = raw_df.copy()
91
-
92
- # Columns to be renamed
93
- rename_columns = {
94
- "TradeTimestamp": "TradeTimestamp",
95
- "symb": "TickerSymbol",
96
- "mtrtyCode": "ExpirationDate",
97
- "BDaysToExp": "BDaysToExp",
98
- "opnCtrcts": "OpenContracts",
99
- "tradQty": "TradeCount",
100
- "traddCtrctsQty": "TradeVolume",
101
- "grssAmt": "FinancialVolume",
102
- "prvsDayAdjstmntPric": "PrevSettlementRate",
103
- "bottomLmtPric": "MinLimitRate",
104
- "topLmtPric": "MaxLimitRate",
105
- "opngPric": "OpenRate",
106
- "minPric": "MinRate",
107
- "avrgPric": "AvgRate",
108
- "maxPric": "MaxRate",
109
- "buyOffer.price": "LastAskRate",
110
- "sellOffer.price": "LastBidRate",
111
- "curPrc": "LastRate",
112
- }
113
- # Rename columns
114
- df = df.rename(columns=rename_columns)
115
-
116
- df["BDaysToExp"] = bd.count_bdays(df["TradeTimestamp"], df["ExpirationDate"])
117
-
118
- # Remove percentage in all rate columns
119
- rate_cols = [col for col in df.columns if "Rate" in col]
120
- df[rate_cols] = df[rate_cols] / 100
121
-
122
- # Reorder columns based on the order of the dictionary
123
- return df[rename_columns.values()]
124
-
125
-
126
- def fetch_last_di() -> pd.DataFrame:
127
- """
128
- Fetch the latest DI futures data from B3.
129
-
130
- Returns:
131
- pd.DataFrame: A Pandas pd.DataFrame containing the latest DI futures data.
132
- """
133
- raw_df = cm.fetch_last_raw_df(future_code="DI1")
134
- return _process_last_raw_di_df(raw_df)
135
-
136
-
137
- def fetch_past_di(trade_date: pd.Timestamp, return_raw: bool = False) -> pd.DataFrame:
138
- """
139
- Fetchs the DI futures data for a given date from B3.
140
-
141
- This function fetches and processes the DI futures data from B3 for a specific
142
- trade date. It's the primary external interface for accessing DI data. If the data
143
- is not available, an empty DataFrame is returned.
144
-
145
- Args:
146
- trade_date (pd.Timestamp): The trade date to fetch the DI futures data.
147
- return_raw (bool): If True, returns the raw data as a Pandas pd.DataFrame.
148
- Defaults to False.
149
-
150
- Returns:
151
- pd.DataFrame: A Pandas pd.DataFrame containing processed DI futures data.
152
-
153
- Notes:
154
- - BDaysToExp: number of business days to ExpirationDate.
155
- - OpenContracts: number of open contracts at the start of the trading day.
156
- """
157
- df_raw = cm.fetch_past_raw_df(asset_code="DI1", trade_date=trade_date)
158
- if return_raw or df_raw.empty:
159
- return df_raw
160
- return _process_past_raw_df(df_raw, trade_date)
@@ -1,61 +0,0 @@
1
- import pandas as pd
2
-
3
- from . import common as cm
4
-
5
-
6
- def _process_past_data(df: pd.DataFrame, trade_date: pd.Timestamp) -> pd.DataFrame:
7
- """
8
- Internal function to process and transform raw DI futures data.
9
-
10
- Args:
11
- df (pd.DataFrame): the raw DI DataFrame.
12
- trade_date: a datetime-like object representing the trade date.
13
-
14
- Returns:
15
- pd.DataFrame: Processed and transformed data as a Pandas pd.DataFrame.
16
- """
17
- df = cm.rename_columns(df)
18
-
19
- df["TradeDate"] = trade_date
20
- # Convert to datetime64[ns] since it is pandas default type for timestamps
21
- df["TradeDate"] = df["TradeDate"].astype("datetime64[ns]")
22
-
23
- # Contract code format was changed in 22/05/2006
24
- if trade_date < pd.Timestamp("2006-05-22"):
25
- df["ExpirationDate"] = df["ExpirationCode"].apply(
26
- cm.get_old_expiration_date, args=(trade_date,)
27
- )
28
- else:
29
- df["ExpirationDate"] = df["ExpirationCode"].apply(cm.get_expiration_date)
30
-
31
- # Replace 0 values in rate columns with pd.NA and remove percentage
32
- rate_cols = [col for col in df.columns if "Rate" in col]
33
- # Round to 5 decimal places (3 in %) since it is the contract's precision
34
- df[rate_cols] = df[rate_cols].replace(0, pd.NA).div(100).round(5)
35
-
36
- df["TickerSymbol"] = "FRC" + df["ExpirationCode"]
37
-
38
- # Filter and order columns
39
- df = cm.reorder_columns(df)
40
- return df
41
-
42
-
43
- def fetch_past_frc(trade_date: pd.Timestamp, return_raw: bool = False) -> pd.DataFrame:
44
- """
45
- Fetchs the DI futures data for a given date from B3.
46
-
47
- This function fetches and processes the DI futures data from B3 for a specific
48
- trade date. It's the primary external interface for accessing DI data.
49
-
50
- Args:
51
- trade_date (pd.Timestamp): The trade date to fetch the DI futures data.
52
- raw (bool): If True, returns the raw data as a Pandas pd.DataFrame.
53
- Defaults to False.
54
-
55
- Returns:
56
- pd.DataFrame: A Pandas pd.DataFrame containing processed DI futures data.
57
- """
58
- df_raw = cm.fetch_past_raw_df(asset_code="FRC", trade_date=trade_date)
59
- if return_raw or df_raw.empty:
60
- return df_raw
61
- return _process_past_data(df_raw, trade_date)
@@ -1,80 +0,0 @@
1
- import pandas as pd
2
-
3
- from pyield import futures as ft
4
-
5
-
6
- def test_valid_old_contract_code1():
7
- expiration_code = "JAN3" # Valid contract code
8
- trade_date = pd.Timestamp("2001-05-21")
9
- result = ft.get_old_expiration_date(expiration_code, trade_date)
10
- contract_expiration = pd.Timestamp("2003-01-02")
11
- assert result == contract_expiration
12
-
13
-
14
- def test_valid_old_contract_code2():
15
- expiration_code = "JAN3" # Valid contract code
16
- trade_date = pd.Timestamp("1990-01-01")
17
- result = ft.get_old_expiration_date(expiration_code, trade_date)
18
- contract_expiration = pd.Timestamp("1993-01-04")
19
- assert result == contract_expiration
20
-
21
-
22
- def test_invalid_old_contract_code():
23
- expiration_code = "J3" # Invalid contract code
24
- trade_date = pd.Timestamp("2001-01-02")
25
- # Must return NaT
26
- result = ft.get_old_expiration_date(expiration_code, trade_date)
27
- assert pd.isnull(result)
28
-
29
-
30
- def test_new_contract_code():
31
- expiration_code = "F23" # Valid contract code
32
- result = ft.get_expiration_date(expiration_code)
33
- contract_expiration = pd.Timestamp("2023-01-02")
34
- assert result == contract_expiration
35
-
36
-
37
- def test_settlement_rate_with_old_holiday_list():
38
- settlement_rates = {
39
- "DI1N27": 0.09809,
40
- "DI1F33": 0.10368,
41
- }
42
-
43
- # 22-12-2023 is before the new holiday calendar
44
- test_date = pd.Timestamp("2023-12-22")
45
- df = ft.fetch_past_di(trade_date=test_date)
46
- tickers = list(settlement_rates.keys()) # noqa: F841
47
- result = df.query("TickerSymbol in @tickers")["SettlementRate"].to_list()
48
- assert result == list(settlement_rates.values())
49
-
50
-
51
- def test_settlement_rates_with_current_holiday_list():
52
- settlement_rates = {
53
- "DI1F24": 0.11644,
54
- "DI1J24": 0.11300,
55
- "DI1N24": 0.10786,
56
- "DI1V24": 0.10321,
57
- "DI1F25": 0.10031,
58
- "DI1J25": 0.09852,
59
- "DI1N25": 0.09715,
60
- "DI1V25": 0.09651,
61
- "DI1F26": 0.09583,
62
- "DI1N26": 0.09631,
63
- "DI1F27": 0.09683,
64
- "DI1N27": 0.09794,
65
- "DI1F29": 0.10042,
66
- "DI1F31": 0.10240,
67
- "DI1F33": 0.10331,
68
- }
69
- test_date = pd.Timestamp("2023-12-26")
70
- df = ft.fetch_past_di(trade_date=test_date)
71
- tickers = list(settlement_rates.keys()) # noqa: F841
72
- results = df.query("TickerSymbol in @tickers")["SettlementRate"].to_list()
73
- assert results == list(settlement_rates.values())
74
-
75
-
76
- def test_non_business_day():
77
- non_business_day = pd.Timestamp("2023-12-24")
78
- # Test if it an empty DataFrame is returned
79
- df = ft.fetch_past_di(trade_date=non_business_day)
80
- assert df.empty
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