Calculating Bond Duration for Fractional Maturities with Day Counts
Summary
This document explains ways to calculate Macaulay and modified duration when a bond’s remaining maturity is not a whole number of years. One approach uses a bond analytics library to construct a fixed-rate bond with a maturity specified in days, then requests the desired duration measure using a stated day-count convention and compounding setup. The example reports slightly different results from a second calculation based on a fractional-year approximation.
A more general method discounts each cash flow at its period-specific time, weights the time points by present value, and sums those weights to obtain Macaulay duration. Modified duration is derived by adjusting Macaulay duration for the yield convention. For dated cash flows, the document adds a step to convert dates into year fractions using a day-count basis. It emphasizes that frequency, annualization, start date, leap years, and day-count conventions can affect results; the examples are illustrative and do not establish one universally correct convention for every bond.
Key ideas
- Macaulay duration is calculated as a present-value-weighted average of cash-flow times.
- Modified duration adjusts Macaulay duration according to the yield convention.
- Fractional maturity can be represented with year fractions derived from dates and a day-count basis.
- A bond library can calculate duration after the bond schedule and conventions are specified.
- Payment frequency, leap years, and day-count assumptions can change the result.
Tags
Full text
# Macaulay or modified duration in Python
# 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
## Answer by David Duarte (score 1)
https://quant.stackexchange.com/a/53293
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)
## Answer by Pythonista anonymous (score 0)
https://quant.stackexchange.com/a/61025
### 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))
```Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.