PYield 0.37.0__tar.gz → 0.37.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 (53) hide show
  1. {pyield-0.37.0 → pyield-0.37.2}/PKG-INFO +1 -1
  2. pyield-0.37.2/pyield/__about__.py +1 -0
  3. {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/ima.py +10 -6
  4. {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/tpf.py +138 -7
  5. {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/di1.py +8 -8
  6. {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/futures/historical.py +28 -21
  7. {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/futures/intraday.py +14 -11
  8. {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/futures/xml.py +4 -4
  9. {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/__init__.py +3 -2
  10. {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/auction.py +3 -6
  11. pyield-0.37.0/pyield/bc/ptax.py → pyield-0.37.2/pyield/bc/ptax_api.py +44 -3
  12. pyield-0.37.0/pyield/bc/bcdata.py → pyield-0.37.2/pyield/bc/rates.py +5 -3
  13. {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/repo.py +6 -2
  14. {pyield-0.37.0 → pyield-0.37.2}/pyield/bday.py +6 -6
  15. {pyield-0.37.0 → pyield-0.37.2}/pyield/data_cache.py +20 -15
  16. {pyield-0.37.0 → pyield-0.37.2}/pyield/date_converter.py +4 -1
  17. {pyield-0.37.0 → pyield-0.37.2}/pyield/fwd.py +4 -4
  18. {pyield-0.37.0 → pyield-0.37.2}/pyield/ibge/ipca.py +1 -1
  19. {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/ltn.py +1 -1
  20. {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/ntnb.py +14 -15
  21. {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/ntnc.py +8 -7
  22. {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/ntnf.py +6 -5
  23. {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/pre.py +3 -3
  24. pyield-0.37.0/pyield/__about__.py +0 -1
  25. {pyield-0.37.0 → pyield-0.37.2}/.gitignore +0 -0
  26. {pyield-0.37.0 → pyield-0.37.2}/LICENSE +0 -0
  27. {pyield-0.37.0 → pyield-0.37.2}/README.md +0 -0
  28. {pyield-0.37.0 → pyield-0.37.2}/pyield/__init__.py +0 -0
  29. {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/__init__.py +0 -0
  30. {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/ettj.py +0 -0
  31. {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/imaq.py +0 -0
  32. {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/ipca.py +0 -0
  33. {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/__init__.py +0 -0
  34. {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/di_over.py +0 -0
  35. {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/futures/__init__.py +0 -0
  36. {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/futures/common.py +0 -0
  37. {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/trades_intraday.py +0 -0
  38. {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/trades_monthly.py +0 -0
  39. {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/vna.py +0 -0
  40. {pyield-0.37.0 → pyield-0.37.2}/pyield/conftest.py +0 -0
  41. {pyield-0.37.0 → pyield-0.37.2}/pyield/holidays/__init__.py +0 -0
  42. {pyield-0.37.0 → pyield-0.37.2}/pyield/holidays/br_holidays_new.txt +0 -0
  43. {pyield-0.37.0 → pyield-0.37.2}/pyield/holidays/br_holidays_old.txt +0 -0
  44. {pyield-0.37.0 → pyield-0.37.2}/pyield/holidays/brholidays.py +0 -0
  45. {pyield-0.37.0 → pyield-0.37.2}/pyield/ibge/__init__.py +0 -0
  46. {pyield-0.37.0 → pyield-0.37.2}/pyield/interpolator.py +0 -0
  47. {pyield-0.37.0 → pyield-0.37.2}/pyield/py.typed +0 -0
  48. {pyield-0.37.0 → pyield-0.37.2}/pyield/retry.py +0 -0
  49. {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/__init__.py +0 -0
  50. {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/benchmark.py +0 -0
  51. {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/lft.py +0 -0
  52. {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/tools.py +0 -0
  53. {pyield-0.37.0 → pyield-0.37.2}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PYield
3
- Version: 0.37.0
3
+ Version: 0.37.2
4
4
  Summary: A Python library for analysis of fixed income instruments in Brazil
5
5
  Project-URL: Source, https://github.com/crdcj/PYield
6
6
  Author-email: Carlos Carvalho <cr.cj@outlook.com>
@@ -0,0 +1 @@
1
+ __version__ = "0.37.2"
@@ -59,7 +59,7 @@ def _fetch_last_ima() -> pd.DataFrame:
59
59
  sep="@",
60
60
  decimal=",",
61
61
  thousands=".",
62
- dtype_backend="numpy_nullable",
62
+ dtype_backend="pyarrow",
63
63
  na_values="--",
64
64
  )
65
65
 
@@ -69,10 +69,12 @@ def _fetch_last_ima() -> pd.DataFrame:
69
69
  def _process_last_ima(df: pd.DataFrame) -> pd.DataFrame:
70
70
  df = df.rename(columns=IMA_COL_MAPPING)[IMA_COL_MAPPING.values()]
71
71
  df["IndicativeRate"] = (df["IndicativeRate"] / 100).round(6)
72
- df["Date"] = pd.to_datetime(df["Date"], format="%d/%m/%Y")
73
- df["Maturity"] = pd.to_datetime(df["Maturity"], format="%d/%m/%Y")
74
- df["MarketQuantity"] = (1000 * df["MarketQuantity"]).astype("Int64")
75
- df["MarketValue"] = (1000 * df["MarketValue"]).round(0).astype("Int64")
72
+ df["Date"] = pd.to_datetime(df["Date"], format="%d/%m/%Y").astype("date32[pyarrow]")
73
+ df["Maturity"] = pd.to_datetime(df["Maturity"], format="%d/%m/%Y").astype(
74
+ "date32[pyarrow]"
75
+ )
76
+ df["MarketQuantity"] = (1000 * df["MarketQuantity"]).astype("int64[pyarrow]")
77
+ df["MarketValue"] = (1000 * df["MarketValue"]).round(0).astype("int64[pyarrow]")
76
78
  df["Price"] = df["Price"].round(6)
77
79
  # Duration is in business days, convert to years
78
80
  df["Duration"] /= 252
@@ -81,7 +83,9 @@ def _process_last_ima(df: pd.DataFrame) -> pd.DataFrame:
81
83
  # LFT DV01 is zero
82
84
  df["DV01"] = df["DV01"].where(df["BondType"] != "LFT", 0)
83
85
  # Since MarketDV01 is the total stock value, we round them to integer
84
- df["MarketDV01"] = (df["DV01"] * df["MarketQuantity"]).round(0).astype("Int64")
86
+ df["MarketDV01"] = (
87
+ (df["DV01"] * df["MarketQuantity"]).round(0).astype("int64[pyarrow]")
88
+ )
85
89
 
86
90
  return df
87
91
 
@@ -8,9 +8,13 @@ from zoneinfo import ZoneInfo
8
8
  import pandas as pd
9
9
 
10
10
  from pyield import bday
11
+ from pyield.bc.ptax_api import ptax
11
12
  from pyield.data_cache import get_cached_dataset
12
13
  from pyield.date_converter import DateScalar, convert_input_dates
13
14
  from pyield.retry import default_retry
15
+ from pyield.tn.ntnb import duration as duration_b
16
+ from pyield.tn.ntnc import duration as duration_c
17
+ from pyield.tn.ntnf import duration as duration_f
14
18
 
15
19
  BZ_TIMEZONE = ZoneInfo("America/Sao_Paulo")
16
20
 
@@ -64,7 +68,7 @@ def _read_raw_df(file_url: str) -> pd.DataFrame:
64
68
  decimal=",",
65
69
  thousands=".",
66
70
  na_values=["--"],
67
- dtype_backend="numpy_nullable",
71
+ dtype_backend="pyarrow",
68
72
  )
69
73
  return df
70
74
 
@@ -96,15 +100,123 @@ def _process_raw_df(df_raw: pd.DataFrame) -> pd.DataFrame:
96
100
  # We will round to 6 decimal places to avoid floating point errors
97
101
  df[rate_cols] = (df[rate_cols] / 100).round(6)
98
102
 
99
- df["ReferenceDate"] = pd.to_datetime(df["ReferenceDate"], format="%Y%m%d")
100
- df["MaturityDate"] = pd.to_datetime(df["MaturityDate"], format="%Y%m%d")
101
- df["IssueBaseDate"] = pd.to_datetime(df["IssueBaseDate"], format="%Y%m%d")
103
+ for col in ["ReferenceDate", "MaturityDate", "IssueBaseDate"]:
104
+ df[col] = pd.to_datetime(df[col], format="%Y%m%d").astype("date32[pyarrow]")
102
105
 
103
- return df.sort_values(["BondType", "MaturityDate"], ignore_index=True)
106
+ return df
107
+
108
+
109
+ def _process_dv01(df: pd.DataFrame) -> pd.DataFrame:
110
+ df["BDToMat"] = bday.count(start=df["ReferenceDate"], end=df["MaturityDate"])
111
+
112
+ df["Duration"] = 0.0
113
+ df["Duration"] = df["Duration"].astype("float64[pyarrow]")
114
+
115
+ df_others = df.query(
116
+ "BondType not in ['LTN', 'NTN-F', 'NTN-B', 'NTN-C']"
117
+ ).reset_index(drop=True)
118
+
119
+ df_ltn = df.query("BondType == 'LTN'").reset_index(drop=True)
120
+ if not df_ltn.empty:
121
+ df_ltn["Duration"] = df_ltn["BDToMat"] / 252
122
+
123
+ df_ntnf = df.query("BondType == 'NTN-F'").reset_index(drop=True)
124
+ if not df_ntnf.empty:
125
+ df_ntnf["Duration"] = df_ntnf.apply(
126
+ lambda row: duration_f(
127
+ row["ReferenceDate"], row["MaturityDate"], row["IndicativeRate"]
128
+ ),
129
+ axis=1,
130
+ )
131
+
132
+ df_ntnb = df.query("BondType == 'NTN-B'").reset_index(drop=True)
133
+ if not df_ntnb.empty:
134
+ df_ntnb["Duration"] = df_ntnb.apply(
135
+ lambda row: duration_b(
136
+ row["ReferenceDate"], row["MaturityDate"], row["IndicativeRate"]
137
+ ),
138
+ axis=1,
139
+ )
140
+
141
+ df_ntnc = df.query("BondType == 'NTN-C'").reset_index(drop=True)
142
+ if not df_ntnc.empty:
143
+ df_ntnc["Duration"] = df_ntnc.apply(
144
+ lambda row: duration_c(
145
+ row["ReferenceDate"], row["MaturityDate"], row["IndicativeRate"]
146
+ ),
147
+ axis=1,
148
+ )
149
+
150
+ df = pd.concat([df_others, df_ltn, df_ntnf, df_ntnb, df_ntnc])
151
+ df["Duration"] = df["Duration"].astype("float64[pyarrow]")
152
+
153
+ mduration = df["Duration"] / (1 + df["IndicativeRate"])
154
+ df["DV01"] = 0.0001 * mduration * df["Price"]
155
+
156
+ return df
157
+
158
+
159
+ def _add_usd_dv01(df: pd.DataFrame) -> pd.DataFrame:
160
+ """Add the USD DV01 column to the DataFrame."""
161
+ try:
162
+ reference_date = df["ReferenceDate"].iloc[0]
163
+ ptax_rate = ptax(date=reference_date)
164
+ df["DV01USD"] = df["DV01"] / ptax_rate
165
+ except Exception as e:
166
+ logger.error(f"Error adding USD DV01: {e}")
167
+ return df
168
+
169
+
170
+ def _reorder_columns(df: pd.DataFrame) -> pd.DataFrame:
171
+ """Reorder the columns of the DataFrame according to the specified order."""
172
+ column_order = [
173
+ "BondType",
174
+ "ReferenceDate",
175
+ "SelicCode",
176
+ "IssueBaseDate",
177
+ "MaturityDate",
178
+ "BDToMat",
179
+ "Duration",
180
+ "DV01",
181
+ "DV01USD",
182
+ "Price",
183
+ "BidRate",
184
+ "AskRate",
185
+ "IndicativeRate",
186
+ "StdDev",
187
+ "LowerBoundRateD0",
188
+ "UpperBoundRateD0",
189
+ "LowerBoundRateD1",
190
+ "UpperBoundRateD1",
191
+ "Criteria",
192
+ ]
193
+ column_order = [col for col in column_order if col in df.columns]
194
+ return df[column_order].copy()
195
+
196
+
197
+ def _select_main_columns(df: pd.DataFrame) -> pd.DataFrame:
198
+ """Select only the main columns from the DataFrame."""
199
+ main_columns = [
200
+ "BondType",
201
+ "ReferenceDate",
202
+ "MaturityDate",
203
+ "BDToMat",
204
+ "Duration",
205
+ "DV01",
206
+ "DV01USD",
207
+ "Price",
208
+ "BidRate",
209
+ "AskRate",
210
+ "IndicativeRate",
211
+ ]
212
+ main_columns = [col for col in main_columns if col in df.columns]
213
+ return df[main_columns].copy()
104
214
 
105
215
 
106
216
  def fetch_tpf_data(
107
- date: DateScalar, bond_type: str | BOND_TYPES | None = None
217
+ date: DateScalar,
218
+ bond_type: str | BOND_TYPES | None = None,
219
+ all_columns: bool = True,
108
220
  ) -> pd.DataFrame:
109
221
  """Fetch and process TPF secondary market data directly from the ANBIMA website.
110
222
  This is a low-level function intended for internal use or specific backend
@@ -121,6 +233,8 @@ def fetch_tpf_data(
121
233
  date (DateScalar): The reference date for the data.
122
234
  bond_type (str, optional): Filter data by bond type.
123
235
  Defaults to None, which returns data for all bond types.
236
+ all_columns (bool, optional): If True, all columns are returned.
237
+ Defaults to True. If False, only the main columns are returned.
124
238
 
125
239
  Returns:
126
240
  pd.DataFrame: A DataFrame containing bond market data.
@@ -165,6 +279,13 @@ def fetch_tpf_data(
165
279
  )
166
280
  return df
167
281
  df = _process_raw_df(df)
282
+ df = _process_dv01(df)
283
+ df = _add_usd_dv01(df)
284
+ df = _reorder_columns(df)
285
+
286
+ if not all_columns:
287
+ df = _select_main_columns(df)
288
+
168
289
  if bond_type:
169
290
  norm_bond_type = _bond_type_mapping(bond_type) # noqa
170
291
  df = df.query("BondType == @norm_bond_type").reset_index(drop=True)
@@ -215,8 +336,18 @@ def tpf_data(
215
336
  - MaturityDate: The maturity date of the bond.
216
337
  - IndicativeRate: The indicative rate of the bond.
217
338
  - Price: The price (PU) of the bond.
218
- """
219
339
 
340
+ Examples:
341
+ >>> from pyield import anbima
342
+ >>> anbima.tpf_data(date="22-08-2025")
343
+ ReferenceDate BondType MaturityDate IndicativeRate Price
344
+ 0 2025-08-22 LFT 2025-09-01 0.000165 17200.957952
345
+ 1 2025-08-22 LFT 2026-03-01 -0.000116 17202.058818
346
+ 2 2025-08-22 LFT 2026-09-01 -0.000107 17202.901668
347
+ 3 2025-08-22 LFT 2027-03-01 0.000302 17193.200289
348
+ 4 2025-08-22 LFT 2027-09-01 0.000411 17186.767105
349
+ ...
350
+ """
220
351
  df = get_cached_dataset("tpf")
221
352
  date = convert_input_dates(date)
222
353
  df = df.query("ReferenceDate == @date").reset_index(drop=True)
@@ -116,7 +116,7 @@ def data(
116
116
  return pd.DataFrame()
117
117
 
118
118
  if month_start:
119
- df["ExpirationDate"] = df["ExpirationDate"].dt.to_period("M").dt.to_timestamp()
119
+ df["ExpirationDate"] = df["ExpirationDate"].dt.floor("MS")
120
120
 
121
121
  if not all_columns:
122
122
  cols = [
@@ -194,7 +194,7 @@ def interpolate_rates(
194
194
  0 0.13972
195
195
  1 0.134613
196
196
  2 <NA>
197
- dtype: Float64
197
+ dtype: double[pyarrow]
198
198
 
199
199
  >>> # Interpolate rates for a single trade date and multiple expiration dates
200
200
  >>> # There is no DI Contract in 09-05-2025 with expiration 01-01-2050
@@ -206,7 +206,7 @@ def interpolate_rates(
206
206
  ... )
207
207
  0 0.13901
208
208
  1 0.13881
209
- dtype: Float64
209
+ dtype: double[pyarrow]
210
210
 
211
211
  >>> # With extrapolation set to False, the second rate will be NaN
212
212
  >>> # Note: 0.13576348733268917 is shown as 0.135763
@@ -217,7 +217,7 @@ def interpolate_rates(
217
217
  ... )
218
218
  0 0.135763
219
219
  1 <NA>
220
- dtype: Float64
220
+ dtype: double[pyarrow]
221
221
 
222
222
  Notes:
223
223
  - All available settlement rates are used for the flat-forward interpolation.
@@ -250,7 +250,7 @@ def interpolate_rates(
250
250
 
251
251
  # Initialize the interpolated rate column with NaN
252
252
  dfi["irate"] = pd.NA
253
- dfi["irate"] = dfi["irate"].astype("Float64")
253
+ dfi["irate"] = dfi["irate"].astype("float64[pyarrow]")
254
254
 
255
255
  # Load DI rates dataset filtered by the provided reference dates
256
256
  dfr = data(dates=dates)
@@ -327,8 +327,8 @@ def interpolate_rate(
327
327
  >>> di1.interpolate_rate("25-04-2025", "01-01-2050", extrapolate=True)
328
328
  0.13881
329
329
  """
330
- date = dc.convert_input_dates(date)
331
- expiration = dc.convert_input_dates(expiration)
330
+ date = dc.convert_input_dates(date).date()
331
+ expiration = dc.convert_input_dates(expiration).date()
332
332
  if not date or not expiration:
333
333
  return float("NaN")
334
334
 
@@ -383,7 +383,7 @@ def eod_dates() -> pd.Series:
383
383
  2 1995-01-04
384
384
  3 1995-01-05
385
385
  4 1995-01-06
386
- dtype: datetime64[ns]
386
+ dtype: date32[day][pyarrow]
387
387
  """
388
388
  available_dates = (
389
389
  get_cached_dataset("di1")
@@ -127,19 +127,19 @@ def _fetch_url_data(contract_code: str, date: pd.Timestamp) -> pd.DataFrame:
127
127
  thousands=".",
128
128
  decimal=",",
129
129
  na_values=["-"],
130
- dtype_backend="numpy_nullable",
130
+ dtype_backend="pyarrow",
131
131
  encoding="iso-8859-1",
132
132
  )[0]
133
133
 
134
134
  # Remove rows and columns with all NaN values
135
- df = df.dropna(axis=0, how="all").dropna(axis=1, how="all").copy()
135
+ df = df.dropna(axis=0, how="all").dropna(axis=1, how="all").reset_index(drop=True)
136
136
 
137
137
  # Force "VAR. PTOS." to be string, since it can also be read as float
138
- df["VAR. PTOS."] = df["VAR. PTOS."].astype("string")
138
+ df["VAR. PTOS."] = df["VAR. PTOS."].astype("string[pyarrow]")
139
139
 
140
140
  # Force "AJUSTE CORRIG. (4)" to be float, since it can be also read as int
141
141
  if "AJUSTE CORRIG. (4)" in df.columns:
142
- df["AJUSTE CORRIG. (4)"] = df["AJUSTE CORRIG. (4)"].astype("Float64")
142
+ df["AJUSTE CORRIG. (4)"] = df["AJUSTE CORRIG. (4)"].astype("float64[pyarrow]")
143
143
 
144
144
  return df
145
145
 
@@ -180,29 +180,32 @@ def _rename_columns(df: pd.DataFrame) -> pd.DataFrame:
180
180
  return df.rename(columns=rename_dict)
181
181
 
182
182
 
183
- def process_df(
184
- input_df: pd.DataFrame, date: pd.Timestamp, contract_code: str
183
+ def _process_df(
184
+ df: pd.DataFrame, date: pd.Timestamp, contract_code: str
185
185
  ) -> pd.DataFrame:
186
- df = input_df.copy()
187
186
  df["TradeDate"] = date
188
- # Convert to datetime64[ns] since it is pandas default type for timestamps
189
- df["TradeDate"] = df["TradeDate"].astype("datetime64[ns]")
187
+ df["TradeDate"] = df["TradeDate"].astype("date32[pyarrow]")
190
188
  df["TickerSymbol"] = contract_code + df["ExpirationCode"]
191
189
 
192
190
  # Contract code format was changed in 22/05/2006
193
- if date < pd.Timestamp("2006-05-22"):
194
- df["ExpirationDate"] = df["ExpirationCode"].apply(
195
- lambda code: get_old_expiration_date(code, date)
196
- ) # type: ignore
191
+ code_format_change = pd.to_datetime("22-05-2006", dayfirst=True)
192
+ if date < code_format_change:
193
+ df["ExpirationDate"] = (
194
+ df["ExpirationCode"]
195
+ .apply(lambda code: get_old_expiration_date(code, date))
196
+ .astype("date32[pyarrow]")
197
+ )
197
198
  else:
198
199
  expiration_day = 15 if contract_code == "DAP" else 1
199
- df["ExpirationDate"] = df["ExpirationCode"].apply(
200
- lambda code: common.get_expiration_date(code, expiration_day)
201
- ) # type: ignore
200
+ df["ExpirationDate"] = (
201
+ df["ExpirationCode"]
202
+ .apply(lambda code: common.get_expiration_date(code, expiration_day))
203
+ .astype("date32[pyarrow]")
204
+ )
202
205
 
203
206
  df["DaysToExp"] = (df["ExpirationDate"] - date).dt.days
204
207
  # Convert to nullable integer, since it is the default type in the library
205
- df["DaysToExp"] = df["DaysToExp"].astype("Int64")
208
+ df["DaysToExp"] = df["DaysToExp"].astype("int64[pyarrow]")
206
209
 
207
210
  df["BDaysToExp"] = bday.count(date, df["ExpirationDate"])
208
211
 
@@ -218,7 +221,8 @@ def process_df(
218
221
 
219
222
  rate_cols = [col for col in df.columns if "Rate" in col]
220
223
  # Prior to 17/01/2002 (inclusive), DI prices were not converted to rates
221
- if date <= pd.Timestamp("2002-01-17") and contract_code == "DI1":
224
+ switch_date = pd.to_datetime("17-01-2002", dayfirst=True)
225
+ if date <= switch_date and contract_code == "DI1":
222
226
  df = _adjust_older_contracts_rates(df, rate_cols)
223
227
  else:
224
228
  # Remove % and round to 5 (3 in %) dec. places in rate columns
@@ -244,7 +248,10 @@ def process_df(
244
248
  df["DV01"] = 0.0001 * modified_duration * df["SettlementPrice"]
245
249
 
246
250
  if contract_code in {"DI1", "DAP"}: # Calculate forwards for DI1 and DAP contracts
247
- df["ForwardRate"] = forwards(bdays=df["BDaysToExp"], rates=df["SettlementRate"])
251
+ df["ForwardRate"] = forwards(
252
+ bdays=df["BDaysToExp"],
253
+ rates=df["SettlementRate"],
254
+ )
248
255
 
249
256
  return df
250
257
 
@@ -276,7 +283,7 @@ def _select_and_reorder_columns(df: pd.DataFrame):
276
283
  "ForwardRate",
277
284
  ]
278
285
  reordered_columns = [col for col in all_columns if col in df.columns]
279
- return df[reordered_columns]
286
+ return df[reordered_columns].copy()
280
287
 
281
288
 
282
289
  def fetch_bmf_data(contract_code: str, date: pd.Timestamp) -> pd.DataFrame:
@@ -305,6 +312,6 @@ def fetch_bmf_data(contract_code: str, date: pd.Timestamp) -> pd.DataFrame:
305
312
  return pd.DataFrame()
306
313
 
307
314
  df = _rename_columns(df_raw)
308
- df = process_df(df, date, contract_code)
315
+ df = _process_df(df, date, contract_code)
309
316
  df = _select_and_reorder_columns(df)
310
317
  return df
@@ -52,7 +52,7 @@ def _fetch_b3_df(contract_code: str) -> pd.DataFrame:
52
52
  df = pd.json_normalize(r.json()["Scty"])
53
53
 
54
54
  # Convert columns to the most appropriate data type
55
- return df.convert_dtypes()
55
+ return df.convert_dtypes(dtype_backend="pyarrow")
56
56
 
57
57
 
58
58
  def _rename_columns(df: pd.DataFrame) -> pd.DataFrame:
@@ -89,10 +89,8 @@ def _rename_columns(df: pd.DataFrame) -> pd.DataFrame:
89
89
  return df
90
90
 
91
91
 
92
- def _process_df(raw_df: pd.DataFrame, contract_code: str) -> pd.DataFrame:
92
+ def _process_df(df: pd.DataFrame, contract_code: str) -> pd.DataFrame:
93
93
  """Process the raw DataFrame from B3 API."""
94
- df = raw_df.copy()
95
-
96
94
  # Clean and reformat the DataFrame columns
97
95
  df.columns = (df.columns
98
96
  .str.replace("SctyQtn.", "")
@@ -103,25 +101,30 @@ def _process_df(raw_df: pd.DataFrame, contract_code: str) -> pd.DataFrame:
103
101
  df = _rename_columns(df)
104
102
 
105
103
  # Convert maturity codes to datetime and drop rows with missing values
106
- df["ExpirationDate"] = pd.to_datetime(df["ExpirationDate"], errors="coerce")
107
- df.dropna(subset=["ExpirationDate"], inplace=True)
104
+ df["ExpirationDate"] = pd.to_datetime(df["ExpirationDate"], errors="coerce").astype(
105
+ "date32[pyarrow]"
106
+ )
108
107
 
109
108
  # Sort the DataFrame by maturity code and reset the index
110
- df.sort_values("ExpirationDate", inplace=True, ignore_index=True)
109
+ df = (
110
+ df.dropna(subset=["ExpirationDate"])
111
+ .sort_values("ExpirationDate")
112
+ .reset_index(drop=True)
113
+ )
111
114
 
112
115
  # Get currante date in Brazil
113
116
  df["TradeDate"] = bday.last_business_day()
117
+ df["TradeDate"] = df["TradeDate"].astype("date32[pyarrow]")
114
118
 
115
119
  # Get current date and time
116
- now = pd.Timestamp.now(BZ_TIMEZONE).round("s").tz_localize(None)
120
+ # now = pd.Timestamp.now(BZ_TIMEZONE).round("s").tz_localize(None)
121
+ now = dt.datetime.now(BZ_TIMEZONE)
117
122
  # Subtract 15 minutes from the current time to account for API delay
118
123
  df["LastUpdate"] = now - pd.Timedelta(minutes=15)
119
124
 
120
125
  df["BDaysToExp"] = bday.count(df["TradeDate"], df["ExpirationDate"])
121
126
 
122
127
  df["DaysToExp"] = (df["ExpirationDate"] - df["TradeDate"]).dt.days
123
- # Convert to nullable integer, since it is the default type in the library
124
- df["DaysToExp"] = df["DaysToExp"].astype("Int64")
125
128
 
126
129
  # Remove expired contracts
127
130
  df = df.query("DaysToExp >= 0").reset_index(drop=True)
@@ -136,7 +139,7 @@ def _process_df(raw_df: pd.DataFrame, contract_code: str) -> pd.DataFrame:
136
139
  if contract_code in {"DI1", "DAP"}: # Add LastPrice for DI1 and DAP
137
140
  byears = df["BDaysToExp"] / 252
138
141
  df["LastPrice"] = 100_000 / ((1 + df["LastRate"]) ** byears)
139
- df["LastPrice"] = df["LastPrice"].round(2).astype("Float64")
142
+ df["LastPrice"] = df["LastPrice"].round(2)
140
143
  df["ForwardRate"] = forwards(bdays=df["BDaysToExp"], rates=df["LastRate"])
141
144
 
142
145
  if contract_code == "DI1": # Add DV01 for DI1
@@ -150,7 +150,7 @@ def _create_df_from_data(di1_data: list) -> pd.DataFrame:
150
150
 
151
151
  # Convert to CSV and then back to pandas to get automatic type conversion
152
152
  file = io.StringIO(df.to_csv(index=False))
153
- return pd.read_csv(file, dtype_backend="numpy_nullable")
153
+ return pd.read_csv(file, dtype_backend="pyarrow")
154
154
 
155
155
 
156
156
  def _rename_columns(df: pd.DataFrame) -> pd.DataFrame:
@@ -193,18 +193,18 @@ def _rename_columns(df: pd.DataFrame) -> pd.DataFrame:
193
193
  def _process_df(df_raw: pd.DataFrame) -> pd.DataFrame:
194
194
  df = df_raw.copy()
195
195
  # Convert to datetime64[ns] since it is pandas default type for timestamps
196
- df["TradeDate"] = df["TradeDate"].astype("datetime64[ns]")
196
+ df["TradeDate"] = df["TradeDate"].astype("date32[pyarrow]")
197
197
 
198
198
  expiration_code = df["TickerSymbol"].str[3:]
199
199
  contract_code = df["TickerSymbol"].str[:3].loc[0]
200
200
  expiration_day = 15 if contract_code == "DAP" else 1
201
201
  df["ExpirationDate"] = expiration_code.apply(
202
202
  common.get_expiration_date, args=(expiration_day,)
203
- )
203
+ ).astype("date32[pyarrow]")
204
204
 
205
205
  df["DaysToExp"] = (df["ExpirationDate"] - df["TradeDate"]).dt.days
206
206
  # Convert to nullable integer, since it is the default type in the library
207
- df["DaysToExp"] = df["DaysToExp"].astype("Int64")
207
+ df["DaysToExp"] = df["DaysToExp"].astype("int64[pyarrow]")
208
208
 
209
209
  # Remove expired contracts
210
210
  df = df.query("DaysToExp > 0").reset_index(drop=True)
@@ -1,5 +1,6 @@
1
1
  from pyield.bc.auction import auctions
2
- from pyield.bc.bcdata import (
2
+ from pyield.bc.ptax_api import ptax, ptax_series
3
+ from pyield.bc.rates import (
3
4
  di_over,
4
5
  di_over_series,
5
6
  selic_over,
@@ -7,7 +8,6 @@ from pyield.bc.bcdata import (
7
8
  selic_target,
8
9
  selic_target_series,
9
10
  )
10
- from pyield.bc.ptax import ptax_series
11
11
  from pyield.bc.repo import repos
12
12
  from pyield.bc.trades_intraday import tpf_intraday_trades
13
13
  from pyield.bc.trades_monthly import tpf_monthly_trades
@@ -20,6 +20,7 @@ __all__ = [
20
20
  "ptax_series",
21
21
  "repos",
22
22
  "tpf_monthly_trades",
23
+ "ptax",
23
24
  "tpf_intraday_trades",
24
25
  "selic_over",
25
26
  "selic_over_series",
@@ -15,7 +15,7 @@ import requests
15
15
 
16
16
  from pyield import bday
17
17
  from pyield import date_converter as dc
18
- from pyield.bc import ptax
18
+ from pyield.bc import ptax_api as pt
19
19
  from pyield.date_converter import DateScalar
20
20
  from pyield.retry import default_retry
21
21
  from pyield.tn.ntnb import duration as duration_b
@@ -153,10 +153,7 @@ def _process_df(df: pd.DataFrame) -> pd.DataFrame:
153
153
  df["AvgPrice"] = df["AvgPrice"].where(keep_avg_price, adjusted_avg_price)
154
154
 
155
155
  # Usar a data de liquidação para calcular o número de dias úteis até o vencimento
156
- df["BDToMat"] = bday.count(
157
- start=df["Settlement"],
158
- end=df["Maturity"],
159
- ).astype("int64[pyarrow]")
156
+ df["BDToMat"] = bday.count(start=df["Settlement"], end=df["Maturity"])
160
157
 
161
158
  return df
162
159
 
@@ -220,7 +217,7 @@ def _add_dv01(df: pd.DataFrame) -> pd.DataFrame:
220
217
 
221
218
  def _get_ptax_df(start_date: dt.date, end_date: dt.date) -> pd.DataFrame:
222
219
  # A série de leilões começa em 2007
223
- df = ptax.ptax_series(start=start_date, end=end_date)
220
+ df = pt.ptax_series(start=start_date, end=end_date)
224
221
  return df[["Date", "MidRate"]].rename(columns={"MidRate": "PTAX"})
225
222
 
226
223
 
@@ -100,17 +100,17 @@ def ptax_series(
100
100
  - Para consultar dados de um intervalo, forneça as datas de `start` e `end`.
101
101
  Exemplo:
102
102
 
103
- `ptax_series(start='2024-10-20', end='2024-10-27')`
103
+ `ptax_series(start='20-10-2024', end='27-10-2024')`
104
104
 
105
105
  - Se apenas `start` for fornecido, a API do BC retornará dados a partir
106
106
  da data de `start` até a data mais recente disponível. Exemplo:
107
107
 
108
- `ptax_series(start='2024-10-20')`
108
+ `ptax_series(start='20-10-2024')`
109
109
 
110
110
  - Se apenas `end` for fornecido, a API do BC retornará dados desde a data mais
111
111
  antiga disponível até a data de `end`. Exemplo:
112
112
 
113
- `ptax_series(end='2024-10-27')`
113
+ `ptax_series(end='27-10-2024')`
114
114
 
115
115
  Série Histórica Completa:
116
116
 
@@ -197,3 +197,44 @@ def ptax_series(
197
197
  url += "&$format=text/csv" # Adiciona o formato CSV ao final
198
198
 
199
199
  return _fetch_df_from_url(url)
200
+
201
+
202
+ def ptax(date: DateScalar) -> float:
203
+ """Busca a cotação PTAX média de fechamento para uma data específica.
204
+
205
+ Esta função é um wrapper para a função `ptax_series`, otimizada para
206
+ buscar o valor de um único dia.
207
+
208
+ Args:
209
+ date (DateScalar): A data para a qual a cotação PTAX é desejada.
210
+ Pode ser uma string no formato "dd-mm-aaaa" ou um objeto date/datetime.
211
+
212
+ Returns:
213
+ float: O valor da PTAX (taxa média) para a data especificada.
214
+ Retorna `np.nan` se não houver cotação para a data
215
+ (ex: feriado, fim de semana ou data futura).
216
+
217
+ Examples:
218
+ >>> from pyield import bc
219
+ >>> # Busca a PTAX para um dia útil
220
+ >>> bc.ptax("22-08-2025")
221
+ 5.4389
222
+
223
+ >>> # Busca a PTAX para um fim de semana (sem dados)
224
+ >>> ptax_rate_weekend = bc.ptax("23-08-2025")
225
+ >>> pd.isna(ptax_rate_weekend)
226
+ True
227
+ """
228
+ # Reutiliza a função ptax_series para buscar os dados para o dia específico.
229
+ # Definir start e end com a mesma data busca a cotação para aquele dia.
230
+ df_ptax = ptax_series(start=date, end=date)
231
+
232
+ # Se o DataFrame estiver vazio, significa que não há cotação para a data.
233
+ # Isso ocorre em fins de semana, feriados ou datas futuras.
234
+ if df_ptax.empty:
235
+ logger.warning(f"No PTAX data found for date: {date}")
236
+ return float("nan")
237
+
238
+ # A API retorna uma única linha para a cotação de fechamento de um dia.
239
+ # A coluna "MidRate" representa a PTAX de fechamento.
240
+ return df_ptax["MidRate"].iloc[0]
@@ -97,8 +97,10 @@ def _fetch_request(
97
97
 
98
98
  df = pd.DataFrame(data, dtype="string")
99
99
  df = df.rename(columns={"data": "Date", "valor": "Value"})
100
- df["Date"] = pd.to_datetime(df["Date"], format="%d/%m/%Y")
101
- df["Value"] = (df["Value"].astype("Float64")) / 100
100
+ df["Date"] = pd.to_datetime(df["Date"], format="%d/%m/%Y").astype(
101
+ "date32[pyarrow]"
102
+ )
103
+ df["Value"] = (df["Value"].astype("float64[pyarrow]")) / 100
102
104
 
103
105
  return df
104
106
 
@@ -240,7 +242,7 @@ def selic_over(date: DateScalar) -> float:
240
242
  df = selic_over_series(date, date)
241
243
  if df.empty:
242
244
  return float("nan")
243
- return float(df.at[0, "Value"])
245
+ return round(df.at[0, "Value"], 4)
244
246
 
245
247
 
246
248
  def selic_target_series(
@@ -44,7 +44,7 @@ def _load_from_url(url: str) -> pd.DataFrame:
44
44
  response.raise_for_status()
45
45
  return pd.read_csv(
46
46
  io.StringIO(response.text),
47
- dtype_backend="numpy_nullable",
47
+ dtype_backend="pyarrow",
48
48
  decimal=",",
49
49
  date_format="%Y-%m-%d",
50
50
  parse_dates=["dataMovimento", "dataLiquidacao", "dataRetorno"],
@@ -58,9 +58,13 @@ def _pre_process_df(df: pd.DataFrame) -> pd.DataFrame:
58
58
 
59
59
 
60
60
  def _process_df(df: pd.DataFrame) -> pd.DataFrame:
61
+ df["Date"] = df["Date"].astype("date32[pyarrow]")
62
+ df["Settlement"] = df["Settlement"].astype("date32[pyarrow]")
61
63
  df["StartTime"] = pd.to_datetime(df["StartTime"], format="%H:%M").dt.time
62
64
  # AcceptedVolume é em milhares de reais -> converter para reais
63
- df["AcceptedVolume"] = (1_000 * df["AcceptedVolume"]).round(0).astype("Int64")
65
+ df["AcceptedVolume"] = (
66
+ (1_000 * df["AcceptedVolume"]).round(0).astype("int64[pyarrow]")
67
+ )
64
68
 
65
69
  # Remove the percentage sign and round to 6 decimal places (4 decimal places in %)
66
70
  df["CutRate"] = (df["CutRate"] / 100).round(6)
@@ -292,19 +292,19 @@ def count(
292
292
  >>> bday.count(start="01-01-2024", end=["01-02-2024", "01-03-2024"])
293
293
  0 22
294
294
  1 41
295
- dtype: Int64
295
+ dtype: int64[pyarrow]
296
296
 
297
297
  The remaining business days in January and February to the end of the year
298
298
  >>> bday.count(["01-01-2024", "01-02-2024"], "01-01-2025")
299
299
  0 253
300
300
  1 231
301
- dtype: Int64
301
+ dtype: int64[pyarrow]
302
302
 
303
303
  The total business days in January and February of 2024
304
304
  >>> bday.count(["01-01-2024", "01-02-2024"], ["01-02-2024", "01-03-2024"])
305
305
  0 22
306
306
  1 19
307
- dtype: Int64
307
+ dtype: int64[pyarrow]
308
308
 
309
309
  Null values are propagated
310
310
  >>> bday.count(pd.NaT, "01-01-2024") # NaT start
@@ -316,7 +316,7 @@ def count(
316
316
  >>> bday.count("01-01-2024", ["01-02-2024", pd.NaT]) # NaT in end array
317
317
  0 22
318
318
  1 <NA>
319
- dtype: Int64
319
+ dtype: int64[pyarrow]
320
320
 
321
321
  Original pandas index is preserved
322
322
  >>> start_dates = pd.Series(
@@ -327,7 +327,7 @@ def count(
327
327
  a 253
328
328
  b 231
329
329
  c 212
330
- dtype: Int64
330
+ dtype: int64[pyarrow]
331
331
  """
332
332
  # Padroniza as entradas para objetos do Pandas
333
333
  start_pd = dc.convert_input_dates(start)
@@ -364,7 +364,7 @@ def count(
364
364
 
365
365
  # Inicializa a série de resultados com o índice correto e tipo que suporta NA.
366
366
  # Nulos nas entradas (start_pd ou end_pd) resultarão em NA aqui.
367
- result = pd.Series(index=start_pd.index, dtype="Int64")
367
+ result = pd.Series(index=start_pd.index, dtype="int64[pyarrow]")
368
368
 
369
369
  # Cria máscaras para linhas não-nulas, divididas pelo calendário de feriados.
370
370
  # A lógica de feriados depende APENAS da data de início.
@@ -18,12 +18,12 @@ BASE_DATA_URL = f"{GIT_URL}/data"
18
18
  # Dicionário global para configurações dos datasets usando 'filename'
19
19
  DATASET_CONFIGS = {
20
20
  "di1": {
21
- "filename": "b3_di.parquet", # Apenas o nome do arquivo
21
+ "filename": "b3_di.parquet",
22
22
  "date_column": "TradeDate",
23
23
  "name": "Futuro de DI (B3)",
24
24
  },
25
25
  "tpf": {
26
- "filename": "anbima_tpf.parquet", # Apenas o nome do arquivo
26
+ "filename": "anbima_tpf.parquet",
27
27
  "date_column": "ReferenceDate",
28
28
  "name": "TPF (ANBIMA)",
29
29
  },
@@ -51,29 +51,34 @@ def _load_github_file(file_url: str) -> pd.DataFrame:
51
51
 
52
52
  @functools.lru_cache(maxsize=len(DATASET_CONFIGS))
53
53
  def _get_dataset_with_ttl(dataset_id: str, date_key: str) -> pd.DataFrame:
54
- """Carrega o dataset a partir do ID e da chave de data. Cache expira diariamente."""
54
+ """
55
+ Função interna e cacheada. Sua única responsabilidade é carregar os dados
56
+ e colocá-los no cache.
57
+ """
55
58
  if dataset_id not in DATASET_CONFIGS:
56
59
  raise ValueError(f"Dataset com ID '{dataset_id}' não encontrado.")
57
60
 
58
61
  config = DATASET_CONFIGS[dataset_id]
59
62
  filename = config["filename"]
60
- date_column = config["date_column"]
61
- dataset_name = config.get("name", dataset_id)
62
-
63
- # Constrói a URL completa
64
63
  full_url = f"{BASE_DATA_URL}/{filename}"
65
64
 
66
65
  try:
66
+ # Apenas carrega e retorna. O .copy() aqui é opcional, mas inofensivo.
67
67
  df = _load_github_file(full_url)
68
- last_date = df[date_column].max().date()
69
- logger.info(f"Dataset {dataset_name} carregado. Última data: {last_date}")
70
- return df.copy()
71
-
68
+ return df
72
69
  except Exception:
73
- logger.exception(f"Erro ao carregar dataset {dataset_name} ({dataset_id})")
74
- raise # Re-raise a exceção para que o chamador saiba que falhou
70
+ logger.exception(f"Erro ao carregar dataset {dataset_id}")
71
+ raise
75
72
 
76
73
 
77
74
  def get_cached_dataset(dataset_id: Literal["di1", "tpf"]) -> pd.DataFrame:
78
- """Obtém um dataset configurado pelo seu ID. O cache expira diariamente."""
79
- return _get_dataset_with_ttl(dataset_id.lower(), _get_today_date_key())
75
+ """
76
+ Obtém um dataset configurado pelo seu ID, garantindo que o cache
77
+ não seja modificado pelo chamador.
78
+ O cache expira diariamente.
79
+ """
80
+ # 1. Pega o DataFrame do cache (ou aciona o carregamento)
81
+ df_from_cache = _get_dataset_with_ttl(dataset_id.lower(), _get_today_date_key())
82
+
83
+ # 2. Retorna uma CÓPIA para o usuário. Este é o passo crucial de proteção.
84
+ return df_from_cache.copy()
@@ -43,7 +43,10 @@ def convert_input_dates(
43
43
  validate_date_format(dates)
44
44
  return pd.to_datetime(dates, dayfirst=True)
45
45
 
46
- case dt.datetime() | dt.date() | np.datetime64() | pd.Timestamp():
46
+ case dt.date():
47
+ return pd.Timestamp(dates)
48
+
49
+ case dt.datetime() | np.datetime64() | pd.Timestamp():
47
50
  return pd.Timestamp(dates).normalize()
48
51
 
49
52
  case pd.Series() | np.ndarray() | list() | tuple():
@@ -67,7 +67,7 @@ def forwards(
67
67
  0 0.05
68
68
  1 0.070095
69
69
  2 0.090284
70
- dtype: Float64
70
+ dtype: double[pyarrow]
71
71
 
72
72
  >>> # Exemplo com agrupamento (a última está isolada em outro grupo)
73
73
  >>> groupby_dates = pd.Series([1, 1, 2])
@@ -75,7 +75,7 @@ def forwards(
75
75
  0 0.05
76
76
  1 0.070095
77
77
  2 0.07
78
- dtype: Float64
78
+ dtype: double[pyarrow]
79
79
 
80
80
  Note:
81
81
  - A função ordena os dados de entrada primeiro por `groupby_dates`,
@@ -83,8 +83,8 @@ def forwards(
83
83
  correta no cálculo das taxas a termo.
84
84
  - Os resultados são retornados na mesma ordem dos dados de entrada.
85
85
  """ # noqa: E501
86
- bdays = bdays.astype("Int64")
87
- rates = rates.astype("Float64")
86
+ bdays = bdays.astype("int64[pyarrow]")
87
+ rates = rates.astype("float64[pyarrow]")
88
88
 
89
89
  # Check if indexes are the same
90
90
  if not bdays.index.equals(rates.index):
@@ -46,7 +46,7 @@ def _process_ipca_dataframe(
46
46
  else:
47
47
  df["Value"] = df["Value"].round(2)
48
48
 
49
- df["Value"] = df["Value"].astype("Float64")
49
+ df["Value"] = df["Value"].astype("float64[pyarrow]")
50
50
  df["Period"] = pd.to_datetime(df["Period"], format="%Y%m").dt.to_period("M")
51
51
 
52
52
  return df
@@ -67,7 +67,7 @@ def maturities(date: DateScalar) -> pd.Series:
67
67
  10 2028-01-01
68
68
  11 2028-07-01
69
69
  12 2030-01-01
70
- dtype: datetime64[ns]
70
+ dtype: date32[day][pyarrow]
71
71
 
72
72
  """
73
73
  df_rates = data(date)
@@ -51,7 +51,6 @@ def data(date: DateScalar) -> pd.DataFrame:
51
51
  11 2024-08-23 NTN-B 2050-08-15 0.06114 4279.434119
52
52
  12 2024-08-23 NTN-B 2055-05-15 0.060892 4355.145036
53
53
  13 2024-08-23 NTN-B 2060-08-15 0.061005 4282.308398
54
-
55
54
  """
56
55
  return anbima.tpf_data(date, "NTN-B")
57
56
 
@@ -83,8 +82,7 @@ def maturities(date: DateScalar) -> pd.Series:
83
82
  11 2050-08-15
84
83
  12 2055-05-15
85
84
  13 2060-08-15
86
- dtype: datetime64[ns]
87
-
85
+ dtype: date32[day][pyarrow]
88
86
  """
89
87
  df_rates = data(date)
90
88
  s_maturities = df_rates["MaturityDate"]
@@ -125,8 +123,8 @@ def _generate_all_coupon_dates(
125
123
  # First coupon date must be after the reference date, otherwise, it can lead to
126
124
  # division by zero where BDays == 0 (bootstrap method for instance)
127
125
  coupon_dates = coupon_dates[coupon_dates > start]
128
-
129
- return pd.Series(coupon_dates).reset_index(drop=True)
126
+ coupon_dates = pd.Series(coupon_dates).astype("date32[pyarrow]")
127
+ return coupon_dates.reset_index(drop=True)
130
128
 
131
129
 
132
130
  def payment_dates(
@@ -153,7 +151,7 @@ def payment_dates(
153
151
  0 2024-05-15
154
152
  1 2024-11-15
155
153
  2 2025-05-15
156
- dtype: datetime64[ns]
154
+ dtype: date32[day][pyarrow]
157
155
  """
158
156
  # Validate and normalize dates
159
157
  settlement = dc.convert_input_dates(settlement)
@@ -164,16 +162,17 @@ def payment_dates(
164
162
  raise ValueError("Maturity date must be after the start date.")
165
163
 
166
164
  # Initialize loop variables
167
- coupon_dates = maturity
168
- cp_dates = []
165
+ coupon_date = maturity
166
+ coupon_dates = []
169
167
 
170
168
  # Iterate backwards from the maturity date to the settlement date
171
- while coupon_dates > settlement:
172
- cp_dates.append(coupon_dates)
169
+ while coupon_date > settlement:
170
+ coupon_dates.append(coupon_date)
173
171
  # Move the coupon date back 6 months
174
- coupon_dates -= pd.DateOffset(months=6)
172
+ coupon_date -= pd.DateOffset(months=6)
175
173
 
176
- return pd.Series(cp_dates).sort_values(ignore_index=True)
174
+ coupon_dates = pd.Series(coupon_dates).astype("date32[pyarrow]")
175
+ return coupon_dates.sort_values().reset_index(drop=True)
177
176
 
178
177
 
179
178
  def cash_flows(
@@ -410,8 +409,8 @@ def spot_rates(
410
409
  df.at[index, "SpotRate"] = price_factor ** (1 / row["BYears"]) - 1
411
410
 
412
411
  df = df[["MaturityDate", "BDToMat", "SpotRate"]].copy()
413
- # Force Float64 type in float columns to standardize the output
414
- df["SpotRate"] = df["SpotRate"].astype("Float64")
412
+ # Force float64 type in float columns to standardize the output
413
+ df["SpotRate"] = df["SpotRate"].astype("float64[pyarrow]")
415
414
 
416
415
  # Filter the result without the intermediate coupon dates (virtual bonds)
417
416
  if not show_coupons:
@@ -506,7 +505,7 @@ def bei_rates(
506
505
  extrapolate=True,
507
506
  )
508
507
 
509
- df["NIR"] = df["BDToMat"].apply(nir_interplator).astype("Float64")
508
+ df["NIR"] = df["BDToMat"].apply(nir_interplator).astype("float64[pyarrow]")
510
509
  # Calculate Breakeven Inflation Rate (BEI)
511
510
  df["BEI"] = ((df["NIR"] + 1) / (df["RIR"] + 1)) - 1
512
511
 
@@ -90,7 +90,7 @@ def payment_dates(
90
90
  9 2030-01-01
91
91
  10 2030-07-01
92
92
  11 2031-01-01
93
- dtype: datetime64[ns]
93
+ dtype: date32[day][pyarrow]
94
94
  """
95
95
  # Validate and normalize dates
96
96
  settlement = dc.convert_input_dates(settlement)
@@ -101,16 +101,17 @@ def payment_dates(
101
101
  raise ValueError("Maturity date must be after the start date.")
102
102
 
103
103
  # Initialize loop variables
104
- coupon_dates = maturity
105
- cp_dates = []
104
+ coupon_date = maturity
105
+ coupon_dates = []
106
106
 
107
107
  # Iterate backwards from the maturity date to the settlement date
108
- while coupon_dates > settlement:
109
- cp_dates.append(coupon_dates)
108
+ while coupon_date > settlement:
109
+ coupon_dates.append(coupon_date)
110
110
  # Move the coupon date back 6 months
111
- coupon_dates -= pd.DateOffset(months=6)
111
+ coupon_date -= pd.DateOffset(months=6)
112
112
 
113
- return pd.Series(cp_dates).sort_values(ignore_index=True)
113
+ coupon_dates = pd.Series(coupon_dates).astype("date32[pyarrow]")
114
+ return coupon_dates.sort_values().reset_index(drop=True)
114
115
 
115
116
 
116
117
  def cash_flows(
@@ -66,7 +66,7 @@ def maturities(date: DateScalar) -> pd.Series:
66
66
  3 2031-01-01
67
67
  4 2033-01-01
68
68
  5 2035-01-01
69
- dtype: datetime64[ns]
69
+ dtype: date32[day][pyarrow]
70
70
  """
71
71
  df_rates = data(date)
72
72
  s_maturities = df_rates["MaturityDate"]
@@ -111,7 +111,7 @@ def payment_dates(
111
111
  >>> ntnf.payment_dates("15-05-2024", "01-01-2025")
112
112
  0 2024-07-01
113
113
  1 2025-01-01
114
- dtype: datetime64[ns]
114
+ dtype: date32[day][pyarrow]
115
115
  """
116
116
  # Validate and normalize dates
117
117
  settlement = dc.convert_input_dates(settlement)
@@ -135,7 +135,8 @@ def payment_dates(
135
135
  coupon_date -= pd.DateOffset(months=6)
136
136
 
137
137
  # Return the coupon dates as a sorted Series
138
- return pd.Series(coupon_dates).sort_values(ignore_index=True)
138
+ coupon_dates = pd.Series(coupon_dates).astype("date32[pyarrow]")
139
+ return coupon_dates.sort_values().reset_index(drop=True)
139
140
 
140
141
 
141
142
  def cash_flows(
@@ -276,7 +277,7 @@ def spot_rates( # noqa
276
277
  5 2035-01-01 2587 0.121398
277
278
  """
278
279
  # Process and validate the input data
279
- settlement = dc.convert_input_dates(settlement)
280
+ settlement = dc.convert_input_dates(settlement).date()
280
281
 
281
282
  # Create flat forward interpolators for LTN and NTN-F rates
282
283
  ltn_rate_interpolator = ip.Interpolator(
@@ -321,7 +322,7 @@ def spot_rates( # noqa
321
322
  df.at[index, "SpotRate"] = price_factor ** (1 / row["BYears"]) - 1
322
323
 
323
324
  df = df[["MaturityDate", "BDToMat", "SpotRate"]].copy()
324
- df["SpotRate"] = df["SpotRate"].astype("Float64")
325
+ df["SpotRate"] = df["SpotRate"].astype("float64[pyarrow]")
325
326
 
326
327
  if not show_coupons:
327
328
  df = df.query("MaturityDate in @ntnf_maturities").reset_index(drop=True)
@@ -87,14 +87,14 @@ def spot_rates(date: DateScalar) -> pd.DataFrame:
87
87
  ltn_subset = _process_additional_ltn(date, ltn_not_in_ntnf)
88
88
 
89
89
  # Ensure consistent data types
90
- ltn_subset["BDToMat"] = ltn_subset["BDToMat"].astype("int64")
91
- df_spots["BDToMat"] = df_spots["BDToMat"].astype("int64")
90
+ ltn_subset["BDToMat"] = ltn_subset["BDToMat"].astype("int64[pyarrow]")
91
+ df_spots["BDToMat"] = df_spots["BDToMat"].astype("int64[pyarrow]")
92
92
 
93
93
  # Combine LTN and NTN-F derived spot rates
94
94
  df_combined = pd.concat([df_spots, ltn_subset], ignore_index=True)
95
95
  else:
96
96
  df_combined = df_spots.copy()
97
- df_combined["BDToMat"] = df_combined["BDToMat"].astype("int64")
97
+ df_combined["BDToMat"] = df_combined["BDToMat"].astype("int64[pyarrow]")
98
98
 
99
99
  # Final validation - ensure no NaN values in the result
100
100
  _validate_final_result(df_combined)
@@ -1 +0,0 @@
1
- __version__ = "0.37.0"
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes