PYield 0.40.3__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.3 → pyield-0.40.4}/PKG-INFO +1 -1
- pyield-0.40.4/pyield/__about__.py +1 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/anbima/tpf.py +4 -5
- {pyield-0.40.3 → pyield-0.40.4}/pyield/b3/di1.py +25 -28
- {pyield-0.40.3 → pyield-0.40.4}/pyield/tn/ntnb.py +113 -46
- {pyield-0.40.3 → pyield-0.40.4}/pyield/tn/ntnc.py +7 -2
- {pyield-0.40.3 → pyield-0.40.4}/pyield/tn/ntnf.py +24 -5
- pyield-0.40.3/pyield/__about__.py +0 -1
- {pyield-0.40.3 → pyield-0.40.4}/.gitignore +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/LICENSE +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/README.md +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/__init__.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/anbima/__init__.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/anbima/difusao.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/anbima/ettj_intraday.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/anbima/ettj_last.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/anbima/ima.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/anbima/imaq.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/b3/__init__.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/b3/di_over.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/b3/futures/__init__.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/b3/futures/common.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/b3/futures/historical.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/b3/futures/intraday.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/b3/futures/xml.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bc/__init__.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bc/auction.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bc/ptax_api.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bc/rates.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bc/repo.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bc/trades_intraday.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bc/trades_monthly.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bc/vna.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bday/__init__.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bday/core.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bday/holidays/__init__.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bday/holidays/br_holidays_new.txt +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bday/holidays/br_holidays_old.txt +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/bday/holidays/brholidays.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/config.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/conftest.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/converters.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/data_cache.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/fwd.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/interpolator.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/ipca/__init__.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/ipca/historical.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/ipca/projected.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/py.typed +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/retry.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/tn/__init__.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/tn/auctions.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/tn/benchmark.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/tn/lft.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/tn/ltn.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/tn/pre.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/tn/tools.py +0 -0
- {pyield-0.40.3 → pyield-0.40.4}/pyield/types.py +0 -0
- {pyield-0.40.3 → 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:
|
|
@@ -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),
|
|
@@ -650,6 +662,7 @@ def di_net_spread( # noqa
|
|
|
650
662
|
|
|
651
663
|
Returns:
|
|
652
664
|
float: The net DI spread in decimal format (e.g., 0.0012 for 12 bps).
|
|
665
|
+
If calculation fails, returns NaN.
|
|
653
666
|
|
|
654
667
|
Examples:
|
|
655
668
|
# Obs: only some of the DI rates will be used in the example.
|
|
@@ -678,7 +691,7 @@ def di_net_spread( # noqa
|
|
|
678
691
|
|
|
679
692
|
# 2. Validação dos inputs de DI
|
|
680
693
|
if len(di_rates) != len(di_expirations):
|
|
681
|
-
|
|
694
|
+
return float("nan")
|
|
682
695
|
|
|
683
696
|
# 3. Criação do interpolador
|
|
684
697
|
ff_interpolator = ip.Interpolator(
|
|
@@ -689,6 +702,8 @@ def di_net_spread( # noqa
|
|
|
689
702
|
|
|
690
703
|
# 4. Geração dos fluxos de caixa do NTN-F
|
|
691
704
|
df = cash_flows(settlement, ntnf_maturity)
|
|
705
|
+
if df.is_empty():
|
|
706
|
+
return float("nan")
|
|
692
707
|
|
|
693
708
|
bdays_to_payment = bday.count(settlement, df["PaymentDate"])
|
|
694
709
|
byears_to_payment = bdays_to_payment / 252
|
|
@@ -730,7 +745,8 @@ def duration(
|
|
|
730
745
|
rate (float): The yield to maturity (YTM) used to discount the cash flows.
|
|
731
746
|
|
|
732
747
|
Returns:
|
|
733
|
-
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.
|
|
734
750
|
|
|
735
751
|
Examples:
|
|
736
752
|
>>> from pyield import ntnf
|
|
@@ -744,6 +760,9 @@ def duration(
|
|
|
744
760
|
maturity = cv.convert_dates(maturity)
|
|
745
761
|
|
|
746
762
|
df = cash_flows(settlement, maturity)
|
|
763
|
+
if df.is_empty():
|
|
764
|
+
return float("nan")
|
|
765
|
+
|
|
747
766
|
byears = bday.count(settlement, df["PaymentDate"]) / 252
|
|
748
767
|
dcf = df["CashFlow"] / (1 + rate) ** byears
|
|
749
768
|
duration = (dcf * byears).sum() / dcf.sum()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.40.3"
|
|
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
|
|
File without changes
|
|
File without changes
|