PYield 0.40.2__tar.gz → 0.40.4__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.40.2 → pyield-0.40.4}/PKG-INFO +1 -1
- pyield-0.40.4/pyield/__about__.py +1 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/anbima/tpf.py +4 -5
- {pyield-0.40.2 → pyield-0.40.4}/pyield/b3/di1.py +25 -28
- {pyield-0.40.2 → pyield-0.40.4}/pyield/data_cache.py +0 -3
- {pyield-0.40.2 → pyield-0.40.4}/pyield/tn/auctions.py +26 -37
- {pyield-0.40.2 → pyield-0.40.4}/pyield/tn/ntnb.py +113 -46
- {pyield-0.40.2 → pyield-0.40.4}/pyield/tn/ntnc.py +7 -2
- {pyield-0.40.2 → pyield-0.40.4}/pyield/tn/ntnf.py +24 -7
- pyield-0.40.2/pyield/__about__.py +0 -1
- {pyield-0.40.2 → pyield-0.40.4}/.gitignore +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/LICENSE +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/README.md +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/__init__.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/anbima/__init__.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/anbima/difusao.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/anbima/ettj_intraday.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/anbima/ettj_last.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/anbima/ima.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/anbima/imaq.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/b3/__init__.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/b3/di_over.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/b3/futures/__init__.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/b3/futures/common.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/b3/futures/historical.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/b3/futures/intraday.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/b3/futures/xml.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bc/__init__.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bc/auction.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bc/ptax_api.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bc/rates.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bc/repo.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bc/trades_intraday.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bc/trades_monthly.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bc/vna.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bday/__init__.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bday/core.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bday/holidays/__init__.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bday/holidays/br_holidays_new.txt +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bday/holidays/br_holidays_old.txt +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/bday/holidays/brholidays.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/config.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/conftest.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/converters.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/fwd.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/interpolator.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/ipca/__init__.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/ipca/historical.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/ipca/projected.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/py.typed +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/retry.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/tn/__init__.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/tn/benchmark.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/tn/lft.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/tn/ltn.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/tn/pre.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/tn/tools.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyield/types.py +0 -0
- {pyield-0.40.2 → pyield-0.40.4}/pyproject.toml +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.40.4"
|
|
@@ -211,15 +211,14 @@ def _add_dv01(df_input: pl.DataFrame) -> pl.DataFrame:
|
|
|
211
211
|
return df
|
|
212
212
|
|
|
213
213
|
|
|
214
|
-
def _add_di_rate(df: pl.DataFrame) -> pl.DataFrame:
|
|
214
|
+
def _add_di_rate(df: pl.DataFrame, ref_date: dt.date) -> pl.DataFrame:
|
|
215
215
|
"""Add the DI rate column to the DataFrame."""
|
|
216
|
-
reference_date = df["ReferenceDate"].item(0)
|
|
217
216
|
di_rates = di1.interpolate_rates(
|
|
218
|
-
dates=
|
|
217
|
+
dates=ref_date,
|
|
219
218
|
expirations=df["MaturityDate"],
|
|
220
219
|
extrapolate=True,
|
|
221
220
|
)
|
|
222
|
-
df = df.with_columns(di_rates
|
|
221
|
+
df = df.with_columns(DIRate=di_rates)
|
|
223
222
|
return df
|
|
224
223
|
|
|
225
224
|
|
|
@@ -298,7 +297,7 @@ def _fetch_tpf_data(date: dt.date) -> pl.DataFrame:
|
|
|
298
297
|
df = _process_raw_df(df)
|
|
299
298
|
df = _add_duration(df)
|
|
300
299
|
df = _add_dv01(df)
|
|
301
|
-
df = _add_di_rate(df)
|
|
300
|
+
df = _add_di_rate(df, date)
|
|
302
301
|
df = _custom_sort_and_order(df)
|
|
303
302
|
|
|
304
303
|
return df
|
|
@@ -5,7 +5,6 @@ import polars as pl
|
|
|
5
5
|
|
|
6
6
|
import pyield.converters as cv
|
|
7
7
|
from pyield import b3, bday, interpolator
|
|
8
|
-
from pyield.config import TIMEZONE_BZ
|
|
9
8
|
from pyield.data_cache import get_cached_dataset
|
|
10
9
|
from pyield.types import DateArray, DateScalar, has_null_args
|
|
11
10
|
|
|
@@ -13,42 +12,40 @@ logger = logging.getLogger(__name__)
|
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def _load_with_intraday(dates: list[dt.date]) -> pl.DataFrame:
|
|
16
|
-
"""Busca dados de DI, incluindo dados intraday para
|
|
15
|
+
"""Busca dados de DI, incluindo dados intraday para datas ausentes no cache."""
|
|
17
16
|
# 1. Busca inicial no cache com as datas solicitadas pelo usuário.
|
|
18
17
|
df_cached = get_cached_dataset("di1").filter(pl.col("TradeDate").is_in(dates))
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
# 2. Identifica datas solicitadas que não estão no cache
|
|
20
|
+
cached_dates = set(df_cached["TradeDate"].unique().to_list())
|
|
21
|
+
missing_dates = [d for d in dates if d not in cached_dates]
|
|
22
22
|
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
# e eles ainda não foram persistidos no cache (processo noturno).
|
|
26
|
-
# Condição 1: O dia de hoje foi solicitado.
|
|
27
|
-
has_today = today in dates
|
|
28
|
-
# Condição 2: E ainda não está no cache.
|
|
29
|
-
is_today_not_in_cache = df_cached.filter(pl.col("TradeDate") == today).is_empty()
|
|
30
|
-
# Condição 3: E hoje é o último dia útil.
|
|
31
|
-
is_today_last_bday = today == last_bday
|
|
23
|
+
# 3. Para cada data faltante, tenta buscar dados via API
|
|
24
|
+
dfs_to_concat = [df_cached] if not df_cached.is_empty() else []
|
|
32
25
|
|
|
33
|
-
|
|
26
|
+
for ref_date in missing_dates:
|
|
34
27
|
try:
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
df_missing = b3.futures(contract_code="DI1", date=ref_date)
|
|
29
|
+
|
|
30
|
+
# Só adiciona se tiver SettlementPrice
|
|
31
|
+
if "SettlementPrice" in df_missing.columns:
|
|
32
|
+
df_missing = df_missing.drop("DaysToExp", strict=False)
|
|
33
|
+
dfs_to_concat.append(df_missing)
|
|
34
|
+
else:
|
|
37
35
|
logger.warning(
|
|
38
|
-
f"
|
|
39
|
-
"
|
|
36
|
+
f"Dados para {ref_date} não contêm 'SettlementPrice'. "
|
|
37
|
+
"Pulando esta data."
|
|
40
38
|
)
|
|
41
|
-
df_intraday = df_intraday.drop("DaysToExp", strict=False)
|
|
42
|
-
|
|
43
|
-
return df_cached
|
|
44
|
-
if df_cached.is_empty():
|
|
45
|
-
return df_intraday
|
|
46
|
-
return pl.concat([df_cached, df_intraday], how="diagonal")
|
|
47
39
|
except Exception as e:
|
|
48
|
-
logger.error(f"Falha ao buscar dados
|
|
49
|
-
|
|
50
|
-
#
|
|
51
|
-
|
|
40
|
+
logger.error(f"Falha ao buscar dados para {ref_date}: {e}")
|
|
41
|
+
|
|
42
|
+
# 4. Retorna concatenação de todos os DataFrames disponíveis
|
|
43
|
+
if len(dfs_to_concat) == 0:
|
|
44
|
+
return pl.DataFrame() # Retorna DataFrame vazio se nada foi encontrado
|
|
45
|
+
elif len(dfs_to_concat) == 1:
|
|
46
|
+
return dfs_to_concat[0]
|
|
47
|
+
else:
|
|
48
|
+
return pl.concat(dfs_to_concat, how="diagonal")
|
|
52
49
|
|
|
53
50
|
|
|
54
51
|
def _get_data(dates: DateScalar | DateArray) -> pl.DataFrame:
|
|
@@ -72,9 +72,6 @@ def _get_dataset_with_ttl(dataset_id: str, date_key: str) -> pl.DataFrame:
|
|
|
72
72
|
raise
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
# --- API Pública ---
|
|
76
|
-
|
|
77
|
-
|
|
78
75
|
def get_cached_dataset(dataset_id: DatasetId) -> pl.DataFrame:
|
|
79
76
|
"""
|
|
80
77
|
Obtém um dataset configurado pelo seu ID, garantindo que o cache
|
|
@@ -130,13 +130,7 @@ def _transform_raw_data(raw_data: list[dict]) -> pl.DataFrame:
|
|
|
130
130
|
.rename(COLUMN_MAP)
|
|
131
131
|
.with_columns(
|
|
132
132
|
# Conversão de datas
|
|
133
|
-
cs.starts_with("data_")
|
|
134
|
-
.exclude("data_liquidacao_2v")
|
|
135
|
-
.str.strptime(pl.Date, "%d/%m/%Y"),
|
|
136
|
-
# Tratamento da data de 2ª volta que tem formato diferente
|
|
137
|
-
pl.col("data_liquidacao_2v").str.strptime(
|
|
138
|
-
pl.Date, "%Y-%m-%dT%H:%M:%S%.f%#z", strict=False
|
|
139
|
-
),
|
|
133
|
+
cs.starts_with("data_").str.strptime(pl.Date, "%d/%m/%Y"),
|
|
140
134
|
# Cálculos de totais
|
|
141
135
|
pl.sum_horizontal("quantidade_ofertada_1v", "quantidade_ofertada_2v").alias(
|
|
142
136
|
"quantidade_ofertada_total"
|
|
@@ -192,28 +186,25 @@ def _transform_raw_data(raw_data: list[dict]) -> pl.DataFrame:
|
|
|
192
186
|
cs.starts_with("financeiro_ofertado").round(2),
|
|
193
187
|
(cs.starts_with("taxa") / 100).round(7), # Percentual -> decimal
|
|
194
188
|
)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
]
|
|
210
|
-
]
|
|
211
|
-
)
|
|
189
|
+
)
|
|
190
|
+
ajustar_cols = [
|
|
191
|
+
"pu_minimo",
|
|
192
|
+
"pu_medio",
|
|
193
|
+
"tipo_pu_medio",
|
|
194
|
+
"taxa_media",
|
|
195
|
+
"taxa_maxima",
|
|
196
|
+
]
|
|
197
|
+
# Se a quantidade aceita for zero, ajustar colunas específicas para None
|
|
198
|
+
df = df.with_columns(
|
|
199
|
+
pl.when(pl.col("quantidade_aceita_1v") == 0)
|
|
200
|
+
.then(None)
|
|
201
|
+
.otherwise(pl.col(ajustar_cols))
|
|
202
|
+
.name.keep()
|
|
212
203
|
)
|
|
213
204
|
|
|
214
205
|
# Cálculo de dias úteis (requer acesso a colunas já convertidas)
|
|
215
206
|
dias_uteis = bday.count(df["data_liquidacao_1v"], df["data_vencimento"])
|
|
216
|
-
df = df.with_columns(
|
|
207
|
+
df = df.with_columns(dias_uteis.alias("dias_uteis"))
|
|
217
208
|
return df.sort(["data_1v", "titulo", "data_vencimento"])
|
|
218
209
|
|
|
219
210
|
|
|
@@ -244,13 +235,11 @@ def _add_duration(df: pl.DataFrame) -> pl.DataFrame:
|
|
|
244
235
|
|
|
245
236
|
df = df.with_columns(
|
|
246
237
|
pl.struct(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
"dias_uteis",
|
|
253
|
-
]
|
|
238
|
+
"titulo",
|
|
239
|
+
"data_liquidacao_1v",
|
|
240
|
+
"data_vencimento",
|
|
241
|
+
"taxa_media",
|
|
242
|
+
"dias_uteis",
|
|
254
243
|
)
|
|
255
244
|
.map_elements(calculate_duration_per_row, return_dtype=pl.Float64)
|
|
256
245
|
.alias("duration")
|
|
@@ -279,9 +268,9 @@ def _add_dv01(df: pl.DataFrame) -> pl.DataFrame:
|
|
|
279
268
|
|
|
280
269
|
df = df.with_columns(
|
|
281
270
|
# 2. Criar as colunas DV01 multiplicando a expressão base pelas quantidades.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
271
|
+
dv01_1v=dv01_unit_expr * pl.col("quantidade_aceita_1v"),
|
|
272
|
+
dv01_2v=dv01_unit_expr * pl.col("quantidade_aceita_2v"),
|
|
273
|
+
dv01_total=dv01_unit_expr * pl.col("quantidade_aceita_total"),
|
|
285
274
|
).with_columns(cs.starts_with("dv01").round(2))
|
|
286
275
|
|
|
287
276
|
return df
|
|
@@ -301,7 +290,7 @@ def _fetch_ptax_data(auction_date: dt.date) -> pl.DataFrame:
|
|
|
301
290
|
return pl.DataFrame()
|
|
302
291
|
|
|
303
292
|
return (
|
|
304
|
-
df.select(
|
|
293
|
+
df.select("Date", "MidRate")
|
|
305
294
|
.rename({"Date": "data_ref", "MidRate": "ptax"})
|
|
306
295
|
.sort("data_ref")
|
|
307
296
|
)
|
|
@@ -346,7 +335,7 @@ def auction(auction_date: DateScalar) -> pl.DataFrame:
|
|
|
346
335
|
{...},
|
|
347
336
|
{
|
|
348
337
|
"quantidade_bcb": 0,
|
|
349
|
-
"liquidacao_segunda_volta": "2025
|
|
338
|
+
"liquidacao_segunda_volta": "30/09/2025",
|
|
350
339
|
"oferta_segunda_volta": 37499,
|
|
351
340
|
"data_leilao": "30/09/2025",
|
|
352
341
|
"oferta": 150000,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import datetime as dt
|
|
2
|
+
import logging
|
|
2
3
|
|
|
3
4
|
import pandas as pd
|
|
4
5
|
import polars as pl
|
|
@@ -20,6 +21,8 @@ COUPON_MONTHS = {2, 5, 8, 11}
|
|
|
20
21
|
COUPON_PMT = 2.956301
|
|
21
22
|
FINAL_PMT = 102.956301
|
|
22
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
def data(date: DateScalar) -> pl.DataFrame:
|
|
25
28
|
"""
|
|
@@ -152,7 +155,8 @@ def payment_dates(
|
|
|
152
155
|
maturity (DateScalar): The maturity date.
|
|
153
156
|
|
|
154
157
|
Returns:
|
|
155
|
-
pl.Series: Series of coupon dates within the specified range.
|
|
158
|
+
pl.Series: Series of coupon dates within the specified range. Returns an empty
|
|
159
|
+
Series if the maturity date is before or equal to the settlement date.
|
|
156
160
|
|
|
157
161
|
Examples:
|
|
158
162
|
>>> from pyield import ntnb
|
|
@@ -171,7 +175,7 @@ def payment_dates(
|
|
|
171
175
|
maturity = cv.convert_dates(maturity)
|
|
172
176
|
|
|
173
177
|
if maturity <= settlement:
|
|
174
|
-
|
|
178
|
+
return pl.Series(dtype=pl.Date)
|
|
175
179
|
|
|
176
180
|
coupon_date = maturity
|
|
177
181
|
coupon_dates = []
|
|
@@ -198,6 +202,7 @@ def cash_flows(
|
|
|
198
202
|
|
|
199
203
|
Returns:
|
|
200
204
|
pl.DataFrame: DataFrame with columns "PaymentDate" and "CashFlow".
|
|
205
|
+
Returns empty DataFrame if settlement >= maturity.
|
|
201
206
|
|
|
202
207
|
Returned columns:
|
|
203
208
|
- PaymentDate: The payment date of the cash flow
|
|
@@ -226,6 +231,10 @@ def cash_flows(
|
|
|
226
231
|
# Get the coupon dates between the settlement and maturity dates
|
|
227
232
|
p_dates = payment_dates(settlement, maturity)
|
|
228
233
|
|
|
234
|
+
# Return empty DataFrame if no payment dates (settlement >= maturity)
|
|
235
|
+
if p_dates.is_empty():
|
|
236
|
+
return pl.DataFrame(schema={"PaymentDate": pl.Date, "CashFlow": pl.Float64})
|
|
237
|
+
|
|
229
238
|
df = pl.DataFrame({"PaymentDate": p_dates}).with_columns(
|
|
230
239
|
pl.when(pl.col("PaymentDate") == maturity)
|
|
231
240
|
.then(FINAL_PMT)
|
|
@@ -251,7 +260,8 @@ def quotation(
|
|
|
251
260
|
the cash flows, which is the yield to maturity (YTM) of the NTN-B.
|
|
252
261
|
|
|
253
262
|
Returns:
|
|
254
|
-
float: The NTN-B quotation truncated to 4 decimal places.
|
|
263
|
+
float: The NTN-B quotation truncated to 4 decimal places. Returns float('nan')
|
|
264
|
+
if the calculation cannot be performed due to invalid inputs.
|
|
255
265
|
|
|
256
266
|
References:
|
|
257
267
|
- https://www.anbima.com.br/data/files/A0/02/CC/70/8FEFC8104606BDC8B82BA2A8/Metodologias%20ANBIMA%20de%20Precificacao%20Titulos%20Publicos.pdf
|
|
@@ -275,6 +285,9 @@ def quotation(
|
|
|
275
285
|
maturity = cv.convert_dates(maturity)
|
|
276
286
|
|
|
277
287
|
cf_df = cash_flows(settlement, maturity)
|
|
288
|
+
if cf_df.is_empty():
|
|
289
|
+
return float("nan")
|
|
290
|
+
|
|
278
291
|
cf_dates = cf_df["PaymentDate"]
|
|
279
292
|
cf_values = cf_df["CashFlow"]
|
|
280
293
|
|
|
@@ -322,6 +335,98 @@ def price(
|
|
|
322
335
|
return tl.truncate(vna * quotation / 100, 6)
|
|
323
336
|
|
|
324
337
|
|
|
338
|
+
def _validate_spot_rate_inputs(
|
|
339
|
+
settlement: DateScalar,
|
|
340
|
+
maturities: DateArray,
|
|
341
|
+
rates: FloatArray,
|
|
342
|
+
) -> tuple[dt.date, pl.Series, pl.Series]:
|
|
343
|
+
# Process and validate the input data
|
|
344
|
+
settlement = cv.convert_dates(settlement)
|
|
345
|
+
maturities = cv.convert_dates(maturities)
|
|
346
|
+
|
|
347
|
+
# Structural validation: ensure maturities and rates have the same length
|
|
348
|
+
if len(maturities) != len(rates):
|
|
349
|
+
raise ValueError(
|
|
350
|
+
f"Maturities and rates must have the same length. "
|
|
351
|
+
f"Got {len(maturities)} maturities and {len(rates)} rates."
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Create base dataframe and filter out invalid data
|
|
355
|
+
df_clean = pl.DataFrame(
|
|
356
|
+
data={"maturities": maturities, "rates": rates},
|
|
357
|
+
schema={"maturities": pl.Date, "rates": pl.Float64},
|
|
358
|
+
).filter(pl.col("maturities") > settlement)
|
|
359
|
+
|
|
360
|
+
# Warn about filtered maturities
|
|
361
|
+
filtered_count = len(maturities) - df_clean.height
|
|
362
|
+
if filtered_count > 0:
|
|
363
|
+
logger.warning(
|
|
364
|
+
"Maturities on or before settlement were ignored: "
|
|
365
|
+
f"{filtered_count} entries removed."
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
return settlement, df_clean["maturities"], df_clean["rates"]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _create_bootstrap_dataframe(
|
|
372
|
+
settlement: dt.date,
|
|
373
|
+
rates: pl.Series,
|
|
374
|
+
maturities: pl.Series,
|
|
375
|
+
) -> pl.DataFrame:
|
|
376
|
+
"""Cria o DataFrame base para o bootstrap."""
|
|
377
|
+
# Create the interpolator to calculate the YTM rates for intermediate dates
|
|
378
|
+
flat_fwd = ip.Interpolator(
|
|
379
|
+
method="flat_forward",
|
|
380
|
+
known_bdays=bday.count(settlement, maturities),
|
|
381
|
+
known_rates=rates,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Generate coupon dates up to the longest maturity date
|
|
385
|
+
all_coupon_dates = _generate_all_coupon_dates(settlement, maturities.max())
|
|
386
|
+
bdays_to_mat = bday.count(settlement, all_coupon_dates)
|
|
387
|
+
|
|
388
|
+
df = (
|
|
389
|
+
pl.DataFrame({"MaturityDate": all_coupon_dates, "BDToMat": bdays_to_mat})
|
|
390
|
+
.with_columns(
|
|
391
|
+
BYears=pl.col("BDToMat") / 252,
|
|
392
|
+
YTM=pl.col("BDToMat").map_elements(flat_fwd, return_dtype=pl.Float64),
|
|
393
|
+
Coupon=COUPON_PMT,
|
|
394
|
+
SpotRate=pl.lit(None, dtype=pl.Float64),
|
|
395
|
+
)
|
|
396
|
+
.sort("MaturityDate")
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
return df
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _update_spot_rate(
|
|
403
|
+
df: pl.DataFrame, maturity: dt.date, spot_rate: float
|
|
404
|
+
) -> pl.DataFrame:
|
|
405
|
+
"""Helper function to update the spot rate inside the bootstrap loop."""
|
|
406
|
+
return df.with_columns(
|
|
407
|
+
pl.when(pl.col("MaturityDate") == maturity)
|
|
408
|
+
.then(spot_rate)
|
|
409
|
+
.otherwise(pl.col("SpotRate"))
|
|
410
|
+
.alias("SpotRate")
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _calculate_coupons_present_value(
|
|
415
|
+
df: pl.DataFrame,
|
|
416
|
+
settlement: dt.date,
|
|
417
|
+
maturity: dt.date,
|
|
418
|
+
) -> float:
|
|
419
|
+
"""Calcula o valor presente dos cupons anteriores à maturidade."""
|
|
420
|
+
prev_cf_dates = payment_dates(settlement, maturity).to_list()[:-1]
|
|
421
|
+
df_temp = df.filter(pl.col("MaturityDate").is_in(prev_cf_dates))
|
|
422
|
+
|
|
423
|
+
return tl.calculate_present_value(
|
|
424
|
+
df_temp["Coupon"],
|
|
425
|
+
df_temp["SpotRate"],
|
|
426
|
+
df_temp["BYears"],
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
325
430
|
def spot_rates(
|
|
326
431
|
settlement: DateScalar,
|
|
327
432
|
maturities: DateArray,
|
|
@@ -389,42 +494,12 @@ def spot_rates(
|
|
|
389
494
|
"""
|
|
390
495
|
if has_null_args(settlement, maturities, rates):
|
|
391
496
|
return pl.DataFrame()
|
|
392
|
-
# Process and validate the input data
|
|
393
|
-
settlement = cv.convert_dates(settlement)
|
|
394
|
-
maturities = cv.convert_dates(maturities)
|
|
395
497
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
method="flat_forward",
|
|
399
|
-
known_bdays=bday.count(settlement, maturities),
|
|
400
|
-
known_rates=rates,
|
|
498
|
+
settlement, maturities, rates = _validate_spot_rate_inputs(
|
|
499
|
+
settlement, maturities, rates
|
|
401
500
|
)
|
|
402
501
|
|
|
403
|
-
|
|
404
|
-
all_coupon_dates = _generate_all_coupon_dates(settlement, maturities.max())
|
|
405
|
-
bdays_to_mat = bday.count(settlement, all_coupon_dates)
|
|
406
|
-
|
|
407
|
-
df = (
|
|
408
|
-
pl.DataFrame({"MaturityDate": all_coupon_dates, "BDToMat": bdays_to_mat})
|
|
409
|
-
.with_columns(
|
|
410
|
-
BYears=pl.col("BDToMat") / 252,
|
|
411
|
-
YTM=pl.col("BDToMat").map_elements(flat_fwd, return_dtype=pl.Float64),
|
|
412
|
-
Coupon=COUPON_PMT,
|
|
413
|
-
SpotRate=pl.lit(None, dtype=pl.Float64),
|
|
414
|
-
)
|
|
415
|
-
.sort("MaturityDate")
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
def _update_spot_rate(
|
|
419
|
-
df: pl.DataFrame, maturity: dt.date, spot_rate: float
|
|
420
|
-
) -> pl.DataFrame:
|
|
421
|
-
"""Helper function to update the spot rate inside the bootstrap loop."""
|
|
422
|
-
return df.with_columns(
|
|
423
|
-
pl.when(pl.col("MaturityDate") == maturity)
|
|
424
|
-
.then(spot_rate)
|
|
425
|
-
.otherwise(pl.col("SpotRate"))
|
|
426
|
-
.alias("SpotRate")
|
|
427
|
-
)
|
|
502
|
+
df = _create_bootstrap_dataframe(settlement, rates, maturities)
|
|
428
503
|
|
|
429
504
|
# Bootstrap method to calculate spot rates iteratively
|
|
430
505
|
df_dicts = df.to_dicts()
|
|
@@ -438,18 +513,10 @@ def spot_rates(
|
|
|
438
513
|
df = _update_spot_rate(df, maturity, spot_rate)
|
|
439
514
|
continue
|
|
440
515
|
|
|
441
|
-
# Get results for all previous cash flows already calculated
|
|
442
|
-
prev_cf_dates = payment_dates(settlement, maturity).to_list()[:-1]
|
|
443
|
-
df_temp = df.filter(pl.col("MaturityDate").is_in(prev_cf_dates))
|
|
444
|
-
|
|
445
516
|
# Calculate the spot rate for the current maturity
|
|
446
|
-
|
|
447
|
-
df_temp["Coupon"],
|
|
448
|
-
df_temp["SpotRate"],
|
|
449
|
-
df_temp["BYears"],
|
|
450
|
-
)
|
|
517
|
+
coupons_pv = _calculate_coupons_present_value(df, settlement, maturity)
|
|
451
518
|
bond_price = quotation(settlement, maturity, row["YTM"])
|
|
452
|
-
price_factor = FINAL_PMT / (bond_price -
|
|
519
|
+
price_factor = FINAL_PMT / (bond_price - coupons_pv)
|
|
453
520
|
spot_rate = price_factor ** (1 / row["BYears"]) - 1
|
|
454
521
|
|
|
455
522
|
df = _update_spot_rate(df, maturity, spot_rate)
|
|
@@ -81,7 +81,8 @@ def payment_dates(
|
|
|
81
81
|
maturity (DateScalar): The maturity date.
|
|
82
82
|
|
|
83
83
|
Returns:
|
|
84
|
-
pl.Series: Series of coupon dates within the specified range.
|
|
84
|
+
pl.Series: Series of coupon dates within the specified range. Returns an
|
|
85
|
+
empty Series if the maturity date is before the settlement date.
|
|
85
86
|
|
|
86
87
|
Examples:
|
|
87
88
|
>>> from pyield import ntnc
|
|
@@ -110,7 +111,7 @@ def payment_dates(
|
|
|
110
111
|
|
|
111
112
|
# Check if maturity date is after the start date
|
|
112
113
|
if maturity < settlement:
|
|
113
|
-
|
|
114
|
+
return pl.Series(dtype=pl.Date)
|
|
114
115
|
|
|
115
116
|
# Initialize loop variables
|
|
116
117
|
coupon_date = maturity
|
|
@@ -176,6 +177,10 @@ def cash_flows(
|
|
|
176
177
|
# Get the coupon dates between the settlement and maturity dates
|
|
177
178
|
pay_dates = payment_dates(settlement, maturity)
|
|
178
179
|
|
|
180
|
+
# Return empty DataFrame if no payment dates (settlement >= maturity)
|
|
181
|
+
if pay_dates.is_empty():
|
|
182
|
+
return pl.DataFrame(schema={"PaymentDate": pl.Date, "CashFlow": pl.Float64})
|
|
183
|
+
|
|
179
184
|
# Get the right coupon payment and final payment values
|
|
180
185
|
coupon_pmt = _get_coupon_pmt(maturity)
|
|
181
186
|
final_pmt = _get_final_pmt(maturity)
|
|
@@ -100,7 +100,8 @@ def payment_dates(
|
|
|
100
100
|
|
|
101
101
|
Returns:
|
|
102
102
|
pl.Series: A Series containing the coupon dates between the settlement
|
|
103
|
-
(exclusive) and maturity (inclusive) dates.
|
|
103
|
+
(exclusive) and maturity (inclusive) dates. Returns an empty Series if
|
|
104
|
+
the maturity date is before or equal to the settlement date.
|
|
104
105
|
|
|
105
106
|
Examples:
|
|
106
107
|
>>> from pyield import ntnf
|
|
@@ -124,7 +125,7 @@ def payment_dates(
|
|
|
124
125
|
|
|
125
126
|
# Check if maturity date is after the start date
|
|
126
127
|
if maturity <= settlement:
|
|
127
|
-
|
|
128
|
+
return pl.Series(dtype=pl.Date)
|
|
128
129
|
|
|
129
130
|
# Initialize loop variables
|
|
130
131
|
coupon_date = maturity
|
|
@@ -185,6 +186,10 @@ def cash_flows(
|
|
|
185
186
|
# Get the payment dates between the settlement and maturity dates
|
|
186
187
|
pay_dates = payment_dates(settlement, maturity)
|
|
187
188
|
|
|
189
|
+
# Return empty DataFrame if no payment dates (settlement >= maturity)
|
|
190
|
+
if pay_dates.is_empty():
|
|
191
|
+
return pl.DataFrame(schema={"PaymentDate": pl.Date, "CashFlow": pl.Float64})
|
|
192
|
+
|
|
188
193
|
# Set the cash flow at maturity to FINAL_PMT and the others to COUPON_PMT
|
|
189
194
|
df = pl.DataFrame(data={"PaymentDate": pay_dates}).with_columns(
|
|
190
195
|
pl.when(pl.col("PaymentDate") == maturity)
|
|
@@ -230,7 +235,11 @@ def price(
|
|
|
230
235
|
"""
|
|
231
236
|
if has_null_args(settlement, maturity, rate):
|
|
232
237
|
return float("nan")
|
|
238
|
+
|
|
233
239
|
cf_df = cash_flows(settlement, maturity)
|
|
240
|
+
if cf_df.is_empty():
|
|
241
|
+
return float("nan")
|
|
242
|
+
|
|
234
243
|
cf_values = cf_df["CashFlow"]
|
|
235
244
|
bdays = bday.count(settlement, cf_df["PaymentDate"])
|
|
236
245
|
byears = tools.truncate(bdays / 252, 14)
|
|
@@ -553,7 +562,7 @@ def premium( # noqa
|
|
|
553
562
|
|
|
554
563
|
Returns:
|
|
555
564
|
float: The premium of the NTN-F bond over the DI curve, expressed as a
|
|
556
|
-
factor.
|
|
565
|
+
factor. If calculation fails, returns NaN.
|
|
557
566
|
|
|
558
567
|
Examples:
|
|
559
568
|
>>> # Obs: only some of the DI rates will be used in the example.
|
|
@@ -584,6 +593,9 @@ def premium( # noqa
|
|
|
584
593
|
di_rates = pl.Series(di_rates)
|
|
585
594
|
|
|
586
595
|
df_cf = cash_flows(settlement, ntnf_maturity, adj_payment_dates=True)
|
|
596
|
+
if df_cf.is_empty():
|
|
597
|
+
return float("nan")
|
|
598
|
+
|
|
587
599
|
ff_interpolator = ip.Interpolator(
|
|
588
600
|
"flat_forward",
|
|
589
601
|
bday.count(settlement, di_expirations),
|
|
@@ -647,11 +659,10 @@ def di_net_spread( # noqa
|
|
|
647
659
|
ntnf_rate (float): The yield to maturity (YTM) of the bond.
|
|
648
660
|
di_rates (FloatArray): A Series of DI rates.
|
|
649
661
|
di_expirations (DateArray): A list or Series of DI expiration dates.
|
|
650
|
-
initial_guess (float, optional): An initial guess for the spread. Defaults to
|
|
651
|
-
None. A good initial guess is the DI gross spread for the bond.
|
|
652
662
|
|
|
653
663
|
Returns:
|
|
654
664
|
float: The net DI spread in decimal format (e.g., 0.0012 for 12 bps).
|
|
665
|
+
If calculation fails, returns NaN.
|
|
655
666
|
|
|
656
667
|
Examples:
|
|
657
668
|
# Obs: only some of the DI rates will be used in the example.
|
|
@@ -680,7 +691,7 @@ def di_net_spread( # noqa
|
|
|
680
691
|
|
|
681
692
|
# 2. Validação dos inputs de DI
|
|
682
693
|
if len(di_rates) != len(di_expirations):
|
|
683
|
-
|
|
694
|
+
return float("nan")
|
|
684
695
|
|
|
685
696
|
# 3. Criação do interpolador
|
|
686
697
|
ff_interpolator = ip.Interpolator(
|
|
@@ -691,6 +702,8 @@ def di_net_spread( # noqa
|
|
|
691
702
|
|
|
692
703
|
# 4. Geração dos fluxos de caixa do NTN-F
|
|
693
704
|
df = cash_flows(settlement, ntnf_maturity)
|
|
705
|
+
if df.is_empty():
|
|
706
|
+
return float("nan")
|
|
694
707
|
|
|
695
708
|
bdays_to_payment = bday.count(settlement, df["PaymentDate"])
|
|
696
709
|
byears_to_payment = bdays_to_payment / 252
|
|
@@ -732,7 +745,8 @@ def duration(
|
|
|
732
745
|
rate (float): The yield to maturity (YTM) used to discount the cash flows.
|
|
733
746
|
|
|
734
747
|
Returns:
|
|
735
|
-
float: The Macaulay duration in business business years.
|
|
748
|
+
float: The Macaulay duration in business business years. Returns NaN if
|
|
749
|
+
calculation is not possible due to invalid inputs.
|
|
736
750
|
|
|
737
751
|
Examples:
|
|
738
752
|
>>> from pyield import ntnf
|
|
@@ -746,6 +760,9 @@ def duration(
|
|
|
746
760
|
maturity = cv.convert_dates(maturity)
|
|
747
761
|
|
|
748
762
|
df = cash_flows(settlement, maturity)
|
|
763
|
+
if df.is_empty():
|
|
764
|
+
return float("nan")
|
|
765
|
+
|
|
749
766
|
byears = bday.count(settlement, df["PaymentDate"]) / 252
|
|
750
767
|
dcf = df["CashFlow"] / (1 + rate) ** byears
|
|
751
768
|
duration = (dcf * byears).sum() / dcf.sum()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.40.2"
|
|
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
|
|
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
|