1 votos

PV diferente de precio sucio en QuantLib

Por lo que entiendo, el precio sucio es la suma del precio limpio y el monto devengado y debe ser igual al Valor Presente (PV) de un bono a cierta tasa de rendimiento. Sin embargo, no puedo replicar este comportamiento en QuantLib-python (1.31.1) ya que los valores son diferentes utilizando la misma tasa de interés.

Código:

from datetime import date
import QuantLib as ql

# Define bond parameters
face_value = 1000
issue_date = ql.Date(27, 6, 2023)
coupon_rate = 0.05  # Tasa de cupón anual (5%)
frequency = ql.Semiannual
day_count = ql.ActualActual(ql.ActualActual.ISMA)

# Crear el calendario de bonos
dates = [issue_date.to_date(), date(2023, 12, 27), date(2024, 6, 27), date(2024, 12, 27), date(2025, 6, 27), date(2025, 12, 27), date(2026, 6, 27),
        date(2026, 12, 27), date(2027, 6, 27), date(2027, 12, 27), date(2028, 6, 27), date(2028, 12, 27), date(2029, 6, 27)]
dates = [ql.Date(d.day, d.month, d.year) for d in dates]
schedule = ql.Schedule(dates)

# Crear el bono
bond = ql.FixedRateBond(0, face_value, schedule, [coupon_rate], day_count)

# Valoraciones
val_date = ql.Date(14,9,2023)
yield_rate_value = 0.05
yield_rate = ql.InterestRate(yield_rate_value, day_count, ql.Compounded, frequency)
pv = sum([c.amount()*yield_rate.discountFactor(val_date, c.date()) for c in bond.cashflows()])
clean_price = ql.BondFunctions.cleanPrice(bond, yield_rate, val_date)
accrued_amount = ql.BondFunctions.accruedAmount(bond, val_date)
dirty_price = clean_price + accrued_amount

print('Precio limpio:', clean_price)
print('Días devengados:', ql.BondFunctions.accruedDays(bond, val_date))
print('Período devengado:', ql.BondFunctions.accruedPeriod(bond, val_date))
print('Días de devengo:', ql.BondFunctions.accrualDays(bond, val_date))
print('Período de devengo:', ql.BondFunctions.accrualPeriod(bond, val_date))
print('Monto devengado:', accrued_amount)
print('Precio sucio:', dirty_price)
print('Valor presente:', pv*100/face_value)

import pandas as pd
print(pd.DataFrame([(c.date().to_date().isoformat(), c.amount(), yield_rate.discountFactor(val_date, c.date()), c.amount()*yield_rate.discountFactor(val_date, c.date()))
       for c in bond.cashflows()]))

Salida del Terminal:

Precio limpio: 99.99243192098663
Días devengados: 79
Período devengado: 0.21584699453551912
Días de devengo: 183
Período de devengo: 0.5
Monto devengado: 1.0792349726775896
Precio sucio: 101.07166689366423
Valor presente: 101.24228365658294
             0       1         2           3
0   2023-12-27    25.0  0.987730   24.693240
1   2024-06-27    25.0  0.963639   24.090966
2   2024-12-27    25.0  0.940135   23.503381
3   2025-06-27    25.0  0.917205   22.930128
4   2025-12-27    25.0  0.894834   22.370857
5   2026-06-27    25.0  0.873009   21.825226
6   2026-12-27    25.0  0.851716   21.292903
7   2027-06-27    25.0  0.830943   20.773564
8   2027-12-27    25.0  0.810676   20.266892
9   2028-06-27    25.0  0.790903   19.772578
10  2028-12-27    25.0  0.771613   19.290320
11  2029-06-27    25.0  0.752793   18.819824
12  2029-06-27  1000.0  0.752793  752.792958

3voto

Brad Tutterow Puntos 5628

Es la combinación de dos cosas.

Primero: cuando se pasa una tasa de interés y y una serie de cupones que pagan en fechas d[1], d[2], ..., d[n], la función BondFunctions.cleanPrice no calcula el factor de descuento en la fecha d[i] como y.discountFactor(val_date, d[i]) como lo haces al calcular el VP, sino como y.discountFactor(val_date, d[1]) * y.discountFactor(d[1], d[2]) * ... * y.discountFactor(d[i-1], d[i]); es decir, recorre los cupones y compone los factores a medida que avanza.

Segundo: act/act ISMA es una convención de conteo de días complicada. Además de la fecha de inicio y fin, también requiere el inicio y fin del período de referencia correspondiente; esto hace que haya una diferencia para y.discountFactor(val_date, d[1]), que no es un período completo.

Juntando las dos cosas, puedes reproducir el precio de esta manera:

pv = 0.0
B = 1.0
cs = bond.cashflows()

for i in range(len(cs)):
    if i == 0:
        B *= yield_rate.discountFactor(val_date, cs[0].date(), dates[0], dates[1])
    elif cs[i].date() != cs[i-1].date():
        B *= yield_rate.discountFactor(cs[i-1].date(), cs[i].date(), dates[i], dates[i+1])
    pv += cs[i].amount() * B
print(pv*100/face_value)

lo que me da 101.07166689366423, igual al precio sucio.

Finanhelp.com

FinanHelp es una comunidad para personas con conocimientos de economía y finanzas, o quiere aprender. Puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X