Macaulay or modified duration in Python

i was wondering to ask, is there any function in pyhton, that calculates macaulay or modified duration, when time to maturity is not a whole number, for example time to maturity is 1514 days, and you need precise answer for macaulay or modified duration.

Maybe somoenecan share their code.

Thank you

• Some people here use Quantlib Python. It is very general, and therefore somewhat complex, but can provide exact calculations per industry standards. Apr 14 '20 at 13:36

There is no internal function in python to get the duration (like excel for example ), although it's not that hard to program. You basically need daycounts, rates and discount factors.

As noob2 mentioned, it's possible to get it with QuantLib although there is a learning curve until you're comfortable with building the needed objects.

Here is a simple example that might get you started:

import QuantLib as ql

days = 1514
coupon = 0.028
yld = 0.000054

start = ql.Date().todaysDate()
maturity = start + ql.Period(days, ql.Days)

bond = ql.FixedRateBond(2, ql.TARGET(), 1000, start, maturity, ql.Period('1Y'), [coupon], ql.ActualActual())
rate = ql.InterestRate(yld, ql.ActualActual(), ql.Compounded, ql.Annual)
simple_duration = ql.BondFunctions.duration(bond, rate, ql.Duration.Simple)
mod_duration = ql.BondFunctions.duration(bond, rate, ql.Duration.Modified)
mac_duration = ql.BondFunctions.duration(bond, rate, ql.Duration.Macaulay)
print(mac_duration, mod_duration, )

Which would output: 3.9742030045989956 3.9739884092248974

Or you depending on how accurate you want it, you could define your own python function:

coupon = 0.028
yld = 0.000054

def durations(c, y, m, n):
macaulay_duration = ((1+y) / (m*y)) - ( (1 + y + n*(c-y)) / ((m*c* ((1+y)**n - 1)) + m*y) )
modified_duration = macaulay_duration / (1 + y)
return macaulay_duration, modified_duration

print( durations(coupon, yld, 1, days/365) )

Which would output: (3.98414223634245, 3.983927104278819)

• So for example If i have a bond, that matures in 1514 days, Face value = 1000, Yield = 0,0054%, Coupon rate = 2,8%, Coupons paid yearly, Price = 1092.50. How then should the code supposed to look like, to calculate modified duration? Sorry for asking, but i didn't really study programming, but trying to calculate Mod d for my thesis, because excel don't calculate Modified duration with negative yield. Thank you :)
– Tom
Apr 14 '20 at 22:25
• Are you sure that yield is correct? Because from the details of that bond and the price, the yield should be negative Apr 15 '20 at 12:25
• The example that i am trying to solve is: settlment= 2016-03-30 maturity=2020-05-22 rate=2.8% pr=1092.5034 redemption=1000 frequency=1 When I use excel function YIELD i get answer 0.005369 When I use function MDURATION on the same bond i get an answer 3.874379 But when I use your code and input the same numbers I get the same modified duration answer as you used earlier approximately 3.97. Which is about 0.1 off from the answer i get from excel. Maybe you know why these two answers are different? Thank you again, just trying to understand how to calculate duration with negative yield
– Tom
Apr 15 '20 at 13:46
• I'm not sure where the diference is, but using your inputs I get a yield of -1.85%. Notice that the absolute sum of the cashflows on your bond is higher than the price, so the yield can only be negative. Apr 15 '20 at 15:31

Part 1

This is not exactly what you asked for, but an intermediate step to get there. It is easy enough to put together a simple function that calculates the Macaulay duration of a set of cashflow, taking as inputs the pv rate, the cashflow amounts and the periods - not the dates, just the periods, ie period 1, 2, 3, etc.

Below I have a toy example of a bond which pays 6% twice a year (ie 3% every 6 months) and which quotes at par.

If you pass the periods in years, i.e. the first semester is period 0.5, then the rate must be annualised, so that 6% becomes 6.09%. If you pass the period in semesters, so that the first semester is period 1, the rate must be 3%, and the final result must be divided by two because we want duration as a weighted average measure of time in years.

def mac_duration(periods, cash, pv_rate):

periods= np.float64(periods)
cash = np.float64(cash)
pv = np.zeros(cash.size)

for i in range(cash.size):
pv[i] = cash[i] / ( 1 + pv_rate)** periods[i]
sum_pv = np.sum(pv)

return np.dot(pv/sum_pv, periods)

def mod_duration(periods, cash, my_rate):
return mac_duration(periods, cash, my_rate) / (1 + my_rate)

cash = [30,30,30,30,30,1030]
rate = 6e-2
rate_sem = rate/2
rate_annual = (1 + rate_sem)**2 - 1
periods_sem = np.arange(1,7)
periods_years = periods_sem / 2

mac_dur_ann = mac_duration( periods_years, cash, rate_annual)
mac_dur_sem = mac_duration( periods_sem, cash, rate_sem) / 2

Part 2

Now all you need is a function which calculates the day count between dates, converts that into year fractions, and passes the result to the function above.

You will notice the result is slightly different now that we are using act/365, because the payments no longer happen at exactly half year (July 1st is 181 days after Jan 1st, and 181 != 365/2). If you calculate it on a bond which pays only once a year (see cash2), you get the same result, as long as no calculation is in a leap year.

Also, you now need to specify when to start counting the days from. With the other function, this was implicit in the periods, i.e. period 1 was, well, 1 period from the starting point.

def mac_duration_dates(dates, cash, pv_rate, day_0, day_count = 365):

yearfrac = ([ (d - day_0).days / day_count for d in dates])
out = mac_duration( yearfrac, cash, pv_rate )
return out

day_0 = pd.to_datetime(date(2013,1,1))
df = pd.DataFrame()
df['month'] = np.arange(6,42,6)
df['dates'] = df.apply(lambda x: day_0 + pd.DateOffset(months = x['month']), axis = 1)
mac_dur_with_dates = mac_duration_dates(df['dates'], cash, rate_annual, day_0 = day_0, day_count = 365)

cash2 = [0,60,0,60,0,1060]
mac_dur2 = mac_duration( periods_years, cash2, 6e-2)
mac_dur_with_dates2 = mac_duration_dates(df['dates'], cash2, 6e-2, day_0 = day_0, day_count = 365)
print( np.isclose(mac_dur_with_dates2, mac_dur2))