Quantlib, difficulties reconciling yield to maturity of bond cashflows

44 Views Asked by At

I am having problems reconciling the yield to maturity calculation for a stream of cashflows.

In the code, I take vectors of dates and amounts to create a BondLeg, to which I assign a dirty_value.

I use ql.Cashflows.yieldRate to calculate an Annually Compounding and Continuous Compounding Yield to Maturity.

I generate the yearFraction for the dates using the same daycount basis used to calculate the yields.

I then use a PYOMO Non Linear Optimization Solver (ipopt) to solve for the yield to maturities using the yearfractions, cashflow amounts and dirty_value.

The PYOMO annually compounding yield is within 1bp of the quantlib yield. However, the quantlib continuously compounded yield is 13bp higher than the PYOMO yield.

Code from my Jupyter Notebook is below:

Imports

import numpy as np
import QuantLib as ql

import pyomo.environ as pyo
from pyomo.opt import SolverFactory

ql.Settings.instance().evaluationDate = ql.Date(30,6,2023)

Cashflows and Dirty Value

dates = [ql.Date(14,11,2023), 
         ql.Date(14,11,2024), 
         ql.Date(14,11,2025), 
         ql.Date(16,11,2026), 
         ql.Date(15,11,2027), 
         ql.Date(14,11,2028), 
         ql.Date(14,11,2029), 
         ql.Date(14,11,2030), 
         ql.Date(14,11,2031), 
         ql.Date(15,11,2032)]

amounts = [84070, 
           84070, 
           84070, 
           84537.05555555558,
           65131.50532953604,
           65131.50532953604,
           65312.930135468014,
           65312.930135468014,
           65312.930135468014,
           1065494.3549413998]


dirty_val = 1050876.388888889


Quantlib Code

BondLeg = ql.Leg([ql.SimpleCashFlow(amt, dt) for dt,amt in zip(dates,amounts)])

ql_ra = ql.CashFlows.yieldRate(BondLeg, dirty_val, ql.Actual365Fixed(), ql.Compounded, ql.Annual, True)
ql_rc = ql.CashFlows.yieldRate(BondLeg, dirty_val, ql.Actual365Fixed(), ql.Compounded, ql.Continuous, True)

Use Quantlib to generate YearFractions

year_fracs = [ql.Actual365Fixed().yearFraction(ql.Date(30,6,2023),dt) for dt in dates]

periods = np.array([frac for frac in year_fracs])
p_int = np.int_(periods)
p_frac = periods - p_int

amount_vec = np.array(amounts)

Calculate using PYOMO Solver

Annually Compounding Yield

model_ra = pyo.ConcreteModel()

# Decision Variables
model_ra.ra = pyo.Var(domain = pyo.NonNegativeReals, initialize=0.05)
ra = model_ra.ra


# Objective Function

def ra_objective_rule(model_ra):
    
    return (amount_vec.dot(1/(((1+ra)**p_int)* (1+(ra*p_frac)))) - dirty_val)**2

model_ra.objf = pyo.Objective(rule=ra_objective_rule, sense=pyo.minimize)

# Constraints


Solver = SolverFactory('ipopt')

results_ra = Solver.solve(model_ra)

print(results_ra)
print(f'Objective Function = {model_ra.objf()}')
print(f'ra = {ra()}')

Continuously Compounding Yield

model_rc = pyo.ConcreteModel()

# Decision Variables
model_rc.rc = pyo.Var(domain = pyo.NonNegativeReals, initialize=0.05)
rc = model_rc.rc


# Objective Function

def rc_objective_rule(model_rc):
     
    return (amount_vec.dot(np.array([pyo.exp(-p*rc) for p in periods])) - dirty_val)**2
    
model_rc.objf = pyo.Objective(rule=rc_objective_rule, sense=pyo.minimize)

# Constraints


Solver = SolverFactory('ipopt')

results_rc = Solver.solve(model_rc)

print(results_rc)
print(f'Objective Function = {model_rc.objf()}')
print(f'rc = {rc()}')

Comparison of Results

print(f'QuantLib - Annually Compounded Rate : {ql_ra:.4%}')
print(f'QuantLib - Continuously Compounded Rate : {ql_rc:.4%}')

print(f'PYOMO - Annually Compounded Rate : {ra.value:.4%}')
print(f'PYOMO - Continuously Compounded Rate : {rc.value:.4%}')

QuantLib - Annually Compounded Rate : 7.3623% QuantLib - Continuously Compounded Rate : 7.2315%

PYOMO - Annually Compounded Rate : 7.3526% PYOMO - Continuously Compounded Rate : 7.1039%

1

There are 1 best solutions below

0
Luigi Ballabio On

[Copying the answer from the QuantLib mailing list in case someone lands here from searching]

The call in the code above,

ql_rc = ql.CashFlows.yieldRate(BondLeg, dirty_val, ql.Actual365Fixed(), ql.Compounded, ql.Continuous, True)

should instead be:

ql_rc = ql.CashFlows.yieldRate(BondLeg, dirty_val, ql.Actual365Fixed(), ql.Continuous, ql.NoFrequency, True)

that is, ql.Continuous is not a frequency to associate to ql.Compounded but a compounding method in its own right. The second call gives the result you expect.

Unfortunately, SWIG exports C++ enums as numeric constants with no particular type, so Python can't warn of the mismatch. The first call was interpreting the numeric value of ql.Continuous, 2, as a semiannual compounding frequency.