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.
- {pyield-0.37.0 → pyield-0.37.2}/PKG-INFO +1 -1
- pyield-0.37.2/pyield/__about__.py +1 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/ima.py +10 -6
- {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/tpf.py +138 -7
- {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/di1.py +8 -8
- {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/futures/historical.py +28 -21
- {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/futures/intraday.py +14 -11
- {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/futures/xml.py +4 -4
- {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/__init__.py +3 -2
- {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/auction.py +3 -6
- pyield-0.37.0/pyield/bc/ptax.py → pyield-0.37.2/pyield/bc/ptax_api.py +44 -3
- pyield-0.37.0/pyield/bc/bcdata.py → pyield-0.37.2/pyield/bc/rates.py +5 -3
- {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/repo.py +6 -2
- {pyield-0.37.0 → pyield-0.37.2}/pyield/bday.py +6 -6
- {pyield-0.37.0 → pyield-0.37.2}/pyield/data_cache.py +20 -15
- {pyield-0.37.0 → pyield-0.37.2}/pyield/date_converter.py +4 -1
- {pyield-0.37.0 → pyield-0.37.2}/pyield/fwd.py +4 -4
- {pyield-0.37.0 → pyield-0.37.2}/pyield/ibge/ipca.py +1 -1
- {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/ltn.py +1 -1
- {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/ntnb.py +14 -15
- {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/ntnc.py +8 -7
- {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/ntnf.py +6 -5
- {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/pre.py +3 -3
- pyield-0.37.0/pyield/__about__.py +0 -1
- {pyield-0.37.0 → pyield-0.37.2}/.gitignore +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/LICENSE +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/README.md +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/__init__.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/__init__.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/ettj.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/imaq.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/anbima/ipca.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/__init__.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/di_over.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/futures/__init__.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/b3/futures/common.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/trades_intraday.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/trades_monthly.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/bc/vna.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/conftest.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/holidays/__init__.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/holidays/br_holidays_new.txt +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/holidays/br_holidays_old.txt +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/holidays/brholidays.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/ibge/__init__.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/interpolator.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/py.typed +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/retry.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/__init__.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/benchmark.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/lft.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyield/tn/tools.py +0 -0
- {pyield-0.37.0 → pyield-0.37.2}/pyproject.toml +0 -0
|
@@ -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="
|
|
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
|
-
|
|
75
|
-
|
|
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"] = (
|
|
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="
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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("
|
|
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:
|
|
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="
|
|
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").
|
|
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("
|
|
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
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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"] =
|
|
200
|
-
|
|
201
|
-
|
|
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("
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
|
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)
|
|
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="
|
|
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("
|
|
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("
|
|
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.
|
|
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
|
|
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 =
|
|
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='
|
|
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='
|
|
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='
|
|
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
|
-
|
|
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
|
|
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="
|
|
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"] = (
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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="
|
|
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",
|
|
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",
|
|
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
|
-
"""
|
|
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
|
-
|
|
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 {
|
|
74
|
-
raise
|
|
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
|
-
"""
|
|
79
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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("
|
|
87
|
-
rates = rates.astype("
|
|
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("
|
|
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
|
|
@@ -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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
168
|
-
|
|
165
|
+
coupon_date = maturity
|
|
166
|
+
coupon_dates = []
|
|
169
167
|
|
|
170
168
|
# Iterate backwards from the maturity date to the settlement date
|
|
171
|
-
while
|
|
172
|
-
|
|
169
|
+
while coupon_date > settlement:
|
|
170
|
+
coupon_dates.append(coupon_date)
|
|
173
171
|
# Move the coupon date back 6 months
|
|
174
|
-
|
|
172
|
+
coupon_date -= pd.DateOffset(months=6)
|
|
175
173
|
|
|
176
|
-
|
|
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
|
|
414
|
-
df["SpotRate"] = df["SpotRate"].astype("
|
|
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("
|
|
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:
|
|
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
|
-
|
|
105
|
-
|
|
104
|
+
coupon_date = maturity
|
|
105
|
+
coupon_dates = []
|
|
106
106
|
|
|
107
107
|
# Iterate backwards from the maturity date to the settlement date
|
|
108
|
-
while
|
|
109
|
-
|
|
108
|
+
while coupon_date > settlement:
|
|
109
|
+
coupon_dates.append(coupon_date)
|
|
110
110
|
# Move the coupon date back 6 months
|
|
111
|
-
|
|
111
|
+
coupon_date -= pd.DateOffset(months=6)
|
|
112
112
|
|
|
113
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|