# QuantLib Python: how to calculate duration and convexity for irregular cashflows? Can I use SimpleCashFlow or must I define a custom bond?

I have 2 questions:

If I want to discount a set of irregular cashflows, I can do it using the SimpleCashFlow class, or defining a bond with custom cashflows (thank you to Ballabio and David Duarte for their previous clarifications on this. I have copied a toy example below.

1. However, can I calculate duration and convexity with SimpleCashFlow, too, or does that work only for bonds?

I see it in the docs that CashFlows does have the duration and convexity methods, but I can't seem to get the syntax right.

The last lines of my toy example are what I have tried - to no avail.

1. May I ask where this is documented, or how to interpret the documentation? The lines I wrote seem to match the syntax I find on https://quantlib-python-docs.readthedocs.io/en/latest/cashflows.html?highlight=duration#id5 , but evidently I am wrong because they don't work.

If instead I search github https://rkapl123.github.io/QLAnnotatedSource/d3/d50/struct_quant_lib_1_1_duration.html I honestly understand absolutely nothing at all on how the syntax should look like.

Could any one please be kind enough to point me to which docs/where explain what the correct syntax should be? I am basically in a position where I can't make sense of the docs and must ask for help for even the most banal calculations. Thank you!

import QuantLib as ql
import pandas as pd
from datetime import date
import numpy as np

d1 = ql.Date(15,1,2001)
ql.Settings.instance().setEvaluationDate(d1)
cfs = [ql.SimpleCashFlow(0, d1),
ql.SimpleCashFlow(110, d1 + 365)]

calc_date = d1
risk_free_rate = 0.1
curve_act_365 = ql.YieldTermStructureHandle(
ql.FlatForward(calc_date, risk_free_rate, ql.Actual365Fixed() , ql.Compounded, ql.Annual ))

curve_30_360 = ql.YieldTermStructureHandle(
ql.FlatForward(calc_date, risk_free_rate, ql.Thirty360(), ql.Compounded, ql.Annual  ))

# The present value is the same 9as it should be!)
# whether I calculate it as
# 1) cashflows - act365
# 2) cashflows - 30/360
# 3) custom bond - act465

pv_act_365 = ql.CashFlows.npv(cfs, curve_act_365, True)
pv_30_360 = ql.CashFlows.npv(cfs, curve_30_360, True)

custom_bond = ql.Bond(0, ql.TARGET(), 100.0, ql.Date(), ql.Date(), cfs)
bond_engine = ql.DiscountingBondEngine(curve_act_365)
custom_bond.setPricingEngine(bond_engine)
pv_bond = custom_bond.NPV()

# I can also use the custom bond to calculate duration and convexity

y = ql.InterestRate(0.1, ql.Actual365Fixed(), ql.Compounded, ql.Annual)
dur_bond = ql.BondFunctions.duration(custom_bond, y, ql.Duration.Macaulay)
conv_bond = ql.BondFunctions.convexity(custom_bond, y)

# manually recalculating the convexity just as a check
conv_check = 1 / (100 * 1.1**2)*110* (1+1**2)/(1.1)

# Can I use the CashFlows class to calculate duration and convexity, too?
# I have tried all the combinations below but none works

cf_dur = ql.CashFlows.duration(cfs, y, ql.Duration.Macaulay)

leg = ql.Leg(cfs)
cf_dur = ql.CashFlows.duration(leg, y, ql.Duration.Macaulay)

cf_dur = ql.CashFlows.duration(leg, 0.1, ql.Duration.Macaulay, False)

cf_conv = x = ql.CashFlows.convexity(cfs, y)


## UPDATE in light of Francis' answer below:

1. So the duration function can take either a list of dates, or a Leg object created from the same list, right? Is there any difference between the two?
2. The docs state that settlementDate = ql.Date() and that npvDate = ql.Date(). Does this mean that ql.Date() is the default value? Or that they are of type ql.Date()? I don't understand, because running ql.Date() returns an empty object. I would have expected ql.Date() to equal today's date, or the date set with ql.Settings.instance().setEvaluationDate(mydate) .

I think that you almost had both. You were just missing the combination of an InterestRate and the includeSettlementDateFlows boolean flag. The following works:

import QuantLib as Ql
import logging
ql = Ql

# Set up logging
numeric_level = getattr(logging, 'INFO', None)
logging.basicConfig(level=numeric_level, format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S')

d1 = ql.Date(15, 1, 2001)
ql.Settings.instance().setEvaluationDate(d1)
cfs = [ql.SimpleCashFlow(0, d1), ql.SimpleCashFlow(110, d1 + 365)]

calc_date = d1
risk_free_rate = 0.1
curve_act_365 = ql.YieldTermStructureHandle(
ql.FlatForward(calc_date, risk_free_rate, ql.Actual365Fixed(), ql.Compounded, ql.Annual))

curve_30_360 = ql.YieldTermStructureHandle(
ql.FlatForward(calc_date, risk_free_rate, ql.Thirty360(), ql.Compounded, ql.Annual))

# The present value is the same 9as it should be!)
# whether I calculate it as
# 1) cashflows - act365
# 2) cashflows - 30/360
# 3) custom bond - act465

pv_act_365 = ql.CashFlows.npv(cfs, curve_act_365, True)
logging.info('pv_act_365: %f', pv_act_365)

pv_30_360 = ql.CashFlows.npv(cfs, curve_30_360, True)
logging.info('pv_30_360: %f', pv_30_360)

custom_bond = ql.Bond(0, ql.TARGET(), 100.0, ql.Date(), ql.Date(), cfs)
bond_engine = ql.DiscountingBondEngine(curve_act_365)
custom_bond.setPricingEngine(bond_engine)
pv_bond = custom_bond.NPV()
logging.info('pv_bond: %f', pv_bond)

# I can also use the custom bond to calculate duration and convexity
y = ql.InterestRate(0.1, ql.Actual365Fixed(), ql.Compounded, ql.Annual)
dur_bond = ql.BondFunctions.duration(custom_bond, y, ql.Duration.Macaulay)
conv_bond = ql.BondFunctions.convexity(custom_bond, y)
logging.info('dur_bond: %f', dur_bond)
logging.info('conv_bond: %f', conv_bond)

# manually recalculating the convexity just as a check
conv_check = 1 / (100 * 1.1 ** 2) * 110 * (1 + 1 ** 2) / 1.1
logging.info('conv_check: %f', conv_check)

# Can I use the CashFlows class to calculate duration and convexity, too?
# I have tried all the combinations below but none works

# cf_dur = ql.CashFlows.duration(cfs, y, ql.Duration.Macaulay)
# logging.info('cf_dur: %f', cf_dur)

leg = ql.Leg(cfs)
# cf_dur = ql.CashFlows.duration(leg, y, ql.Duration.Macaulay)
# logging.info('cf_dur: %f', cf_dur)

cf_dur = ql.CashFlows.duration(leg, y, ql.Duration.Macaulay, False)
logging.info('cf_dur: %f', cf_dur)

cf_conv = x = ql.CashFlows.convexity(cfs, y, False)
logging.info('cf_conv: %f', cf_conv)


and outputs

2021-02-07 20:09:07 INFO     pv_act_365: 100.000000
2021-02-07 20:09:07 INFO     pv_30_360: 100.000000
2021-02-07 20:09:07 INFO     pv_bond: 100.000000
2021-02-07 20:09:07 INFO     dur_bond: 1.000000
2021-02-07 20:09:07 INFO     conv_bond: 1.652893
2021-02-07 20:09:07 INFO     conv_check: 1.652893
2021-02-07 20:09:07 INFO     cf_dur: 1.000000
2021-02-07 20:09:07 INFO     cf_conv: 1.652893


I should have linked to the relevant sections of the documentation here also for completeness:

1. There is no difference between providing a list of cash flows verus a Leg. In the underlying C++ code, the QuantLib::Leg is simply a typedef of a vector of cashflows here.
2. The easiest way to understand the logic here is to look at the underlying C++ code here. If settlementDate is an empty date, it is taken as the evaluation date i.e. Settings::instance().evaluationDate() in C++. If the npvDate is empty, it is taken as the settlementDate, after the adjustment in the previous sentence if it was necessary.