3 votos

Los cálculos de QuantLib para un bono corporativo canadiense a tasa fija difieren de BBG YAS

Estoy fijando el precio de un bono corporativo canadiense fijo no callable con los siguientes parámetros:

Nombre

Valor

CUSIP

12657ZAT0

Fecha de Evaluación

2/14/2024

Fecha de Liquidación

2/16/2024

Fecha de Emisión del Bono

3/6/2009

Fecha de Vencimiento

3/6/2024

Tasa de Cupón

6.215%

Frecuencia del Cupón

2

Día de Cálculo

ACT/ACT

Valor Nominal

1000

Rendimiento

5.437768%

Puedo igualar los días devengados y el monto devengado de Bloomberg, pero tengo que usar un día de cálculo diferente al construir mi objeto FixedRateBond (ver código a continuación). Esto se debe a que Bloomberg utiliza ACT/365 al calcular los valores devengados y ACT/ACT para el resto.

Sin embargo, no puedo igualar el precio limpio: Precio BBG: 100.041 Precio QuantLib: 100.021973

Sospecho que porque estoy usando ACT/ACT como parámetro en la llamada a bond.cleanPrice(...), el Valor Presente Neto no se calcula correctamente ya que no utiliza ACT/365 para el primer período. Podría estar completamente equivocado acerca de esta teoría.

¿Es correcta mi forma de pensar? ¿Quizás tengo un error en mi lógica a continuación? ¿Me falta algún parámetro en algún lugar o no estoy utilizando QuantLib correctamente?

Soy muy nuevo en el mundo de renta fija y QuantLib, así que por favor ten paciencia. ¡Gracias de antemano!

Aquí está mi código de referencia:

import QuantLib as ql

DÍAS_DE_ASENTAMIENTO = 2
VALOR_NOMINAL = 1000
DÍA_DE_CÁLCULO = ql.ActualActual(ql.ActualActual.ISMA)
DÍA_DE_CÁLCULO_DEV = ql.Actual365Fixed(ql.Actual365Fixed.NoLeap) # Nota - Los valores devengados solo coinciden cuando se usa este día de cálculo

fechaDeEvaluación = ql.Date(14, 2, 2024)
fechaDeEmisión = ql.Date(6, 3, 2009)
fechaDeVencimiento = ql.Date(6, 3, 2024)
rendimiento = 0.05437768
cupón = 0.06215
frecuencia = ql.Period("6M")

ql.Settings.instance().evaluationDate = fechaDeEvaluación

bono = ql.FixedRateBond(
    DÍAS_DE_ASENTAMIENTO,
    ql.TARGET(),
    VALOR_NOMINAL,
    fechaDeEmisión,
    fechaDeVencimiento,
    frecuencia,
    [cupón],
    DÍA_DE_CÁLCULO_DEV,
    ql.Unadjusted,
    ql.Unadjusted,
)

px = bono.cleanPrice(
    rendimiento,
    DÍA_DE_CÁLCULO,
    ql.CompoundedThenSimple,
    ql.Semiannual,
    fechaDeEvaluación,
)

# Imprimir precios para comparar
print(f"Precio BBG: {100.041}")
print(f"Precio QuantLib: {px:.6f}\n")

# Imprimir detalles sobre los intereses devengados
print(f"Período de Devengo: {ql.BondFunctions.accrualPeriod(bono)}")
print(f"Inicio de Devengo: {ql.BondFunctions.accrualStartDate(bono)}")
print(f"Fin de Devengo: {ql.BondFunctions.accrualEndDate(bono)}")
print(f"Días Devengados: {ql.BondFunctions.accrualDays(bono)}")

print(f"Monto Devengado: {ql.BondFunctions.accruedAmount(bono)}") # Coincide con BBG
print(f"Período Devengado: {ql.BondFunctions.accruedPeriod(bono)}")
print(f"Días Devengados: {ql.BondFunctions.accruedDays(bono)}") # Coincide con BBG

4voto

tekki Puntos 6

Lo complicado de los bonos canadienses es que usan la convención ACT/365F para el interés devengado para el asentamiento, pero ACT/ACT(ISMA) para el interés devengado en cálculos de rendimiento. Esto significa que en QuantLib necesitas construir dos objetos de bono diferentes si quieres realizar estos dos cálculos.

Los detalles están disponibles en el documento de referencia del gobierno.

[EDIT]

ver comentarios abajo, la sección 10.10.1 menciona que en el último periodo de cupón la fórmula cambia a un rendimiento simple con la convención ACT/365F. Ese cálculo produce 100.041. Gracias a @Attack68 por detectarlo.

Así es como puedes confirmarlo:

import QuantLib as ql
prevCouponDate =ql.Date(6, 9, 2023)
settlementDate = ql.Date(16, 2, 2024)
maturityDate = ql.Date(6, 3, 2024)
yld = 0.05437768
coupon = 0.06215
dc = ql.Actual365Fixed()
frac_ai = dc.yearFraction(prevCouponDate, settlementDate, prevCouponDate, maturityDate)
frac_y = dc.yearFraction(settlementDate, maturityDate, prevCouponDate, maturityDate)
dp_simple = 100.0 * (1.0 + coupon / 2) / (1 + yld * frac_y)
ai = 100*  coupon * frac_ai
cp_s = dp_simple - ai
print(f"clean price simple {cp_s}")

por lo tanto, ignora el código a continuación que usa la fórmula compuesta

[/EDIT]

En caso de este bono, dado que solo queda un cupón, es muy fácil realizar manualmente el cálculo relevante, utilizando la fórmula de la sección 10.1:

prevCouponDate =ql.Date(6, 9, 2023)
settlementDate = ql.Date(16, 2, 2024)
maturityDate = ql.Date(6, 3, 2024)
yld = 0.05437768
coupon = 0.06215
dcc = maturityDate - prevCouponDate
dcs = settlementDate - prevCouponDate
dsc = maturityDate - settlementDate
dp = 100.0 * (1.0 + coupon / 2) * (1+yld/2)**(-dsc/dcc)
ap = dcs / dcc / 2
ai = 100*  coupon * ap
cp = dp - ai
print(f"clean price {cp}")

0 votos

¡Gracias por el documento de referencia! Intentaré ahora los 2 enfoques de bono diferentes y veré si eso ayuda. Además, el código que proporcionaste aún parece resultar en un precio limpio diferente al que esperaría ver. Obtuve 100.03606130113818 cuando esperaba ver 100.041. ¿Me estoy perdiendo algo aquí?

0 votos

En realidad, la sección 10.10.1 menciona que en el último período de cupón la fórmula cambia a rendimiento simple con la convención ACT/365F. Ese cálculo produce 100.041. Gracias a @Attack68 por detectarlo.

0 votos

¡Gracias por la edición, este era en efecto el problema!

2voto

dotnetcoder Puntos 1262

Por lo que vale tu comentario menciona que @DenysUsynin devuelve 100.03606.

rateslib obtiene el mismo resultado. Utiliza una convención especial para bonos canadienses llamada "ActActICMA_stub365f".

from rateslib import *

frb = FixedRateBond(effective=dt(2009, 3, 6), termination=dt(2024, 3, 6),
                    spec="cadgb", fixed_rate=6.215)

frb.accrued(dt(2024, 2, 16))                                     # 2.77546
frb.price(ytm=5.437768, settlement=dt(2024, 2, 16))              # 100.036061 
frb.price(ytm=5.437768, settlement=dt(2024, 2, 16), dirty=True)  # 102.819151

Inútilmente, si tomas el precio sucio y restas lo devengado, terminas con 100.04369, lo cual también es causado por los problemas causados por los diferentes cálculos de devengados durante un cálculo de interés devengado o un cálculo de rendimiento al vencimiento.

0 votos

"Lo cual también se debe a los problemáticos cálculos acumulados diferentes durante un cálculo de interés acumulado o un cálculo de rendimiento al vencimiento - ¿Puedes ampliar sobre esto? Hasta donde sé, solo hay una fórmula cuando se trata de fijar el precio de un bono con tasa de cupón regular semestral (ver página 30 en iiac-accvm.ca/wp-content/uploads/…). No estoy seguro de cómo Bloomberg y otro software similar que uso producen el mismo resultado entre sí pero no coinciden con QuantLib o Excel."

0 votos

Sí, solo hay una fórmula para los bonos del gobierno canadiense, pero es poco convencional y no estándar. Al diseñar software para hacer esto para todos los bonos del mundo con todas sus convenciones, significa que se deben tomar decisiones sobre cómo codificarlos sin duplicar específicamente el código para cada uno. El problema que he señalado está directamente relacionado con este patrón de diseño que no se adapta idealmente al escenario de CadGB. Sin embargo, tenga en cuenta que 100.36061 es el precio limpio obtenido en Rateslib y según la fórmula en la sección 10.1 del documento.

2 votos

En Bloomberg, se indica específicamente que para bonos que se liquidan en el período final, se utiliza un cálculo simple de rendimiento del mercado de dinero. En este caso, la fórmula aplicada a este bono es 10.10.1. Rateslib y Quantlib no parecen estar realizando este cambio. Esto se debe a que rateslib y quantlib afirman que se requiere un "rendimiento al vencimiento" como entrada a la función de precios. El valor de 5.437768 no es un rendimiento al vencimiento, sino un rendimiento simple de mercado de dinero y, por lo tanto, requiere una fórmula o una construcción diferente.

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