Introduction
Welcome to our next lesson on interest rate risk. In our previous sessions, we defined Macaulay Duration as the weighted-average maturity of a bond's cash flows and derived Modified Duration as the first derivative of the price-yield function. We then applied this derivative-based concept to create a linear approximation for estimating bond price changes.
Today, we shift our focus from theory to practice. Building on your strong background in data science and engineering, we will implement the calculation of both Macaulay and Modified Duration from the ground up in Python. This lesson directly addresses the practical need to compute these critical risk metrics from standard bond market data (e.g., face value, coupon, maturity, and market price), completing the "theory to code" journey for duration that aligns with your preferred learning style.
The Calculation Framework in Python
Before we can calculate duration, we need to be able to compute a bond's cash flows and its price. We have done this in earlier lessons, but for this implementation, we will rely on a set of well-structured functions. The resource Assignment 2: Bonds and Bond Math provides an excellent Python-based template for this. We will be building upon its foundational functions for generating cash flows (bond_cashflows) and calculating yield-to-maturity (bond_yield_to_maturity).
1. Implementing Macaulay Duration
Recall the formula for Macaulay Duration, which we'll write for m coupon payments per year. It's the present-value-weighted average of the time to each cash flow, expressed in years:
Where:
- is the bond's price.
- is the number of years to maturity.
- is the number of coupon payments per year.
- is the index of the cash flow period (from 1 to ).
- is the cash flow at period .
- is the annualized yield to maturity (YTM).
- The term converts the cash flow period into years.
Notice that if the bond's price is given but the YTM is not, we must first solve for numerically, as we did in a previous lesson.
Let's break down how this formula is implemented in code. A good implementation encapsulates this logic, often by first calculating the YTM from the price and then using that YTM to compute the duration.
The image below shows the "by definition" calculation in a spreadsheet format. This table structure, where each row is a cash flow, is exactly what our Python code will automate. You can see the columns for time (), cash flow (), present value (), and the weighted time component (), which are all summed up to compute the duration.

Now, let's turn to the Python implementation. The following reading provides a robust function that handles all the necessary steps.
Assignment 2: Implementing Macaulay Duration
In this resource from Boston University, review the function macaulay_duration(fv, c, n, m, price).
Focus on the logic: The function first calls bond_yield_to_maturity(fv, c, n, m, price) to find the YTM. This is a crucial first step. It then uses this ytm to call another helper function, bond_duration(fv, c, n, m, ytm). Examine the bond_duration function (just above macaulay_duration in the text). It iterates through the cash flows, calculates the present value of each, multiplies by the time period, sums them up, and divides by the price. Notice that the function calculates duration in years, correctly handling different compounding frequencies. The examples provided demonstrate this for various bond types, including a zero-coupon bond.
2. Implementing Modified Duration
With Macaulay Duration implemented, calculating Modified Duration is straightforward. The formula directly relates the two, adjusting for the compounding frequency:
This relationship is simple to translate into code. The implementation should take the calculated Macaulay Duration, the YTM, and the compounding frequency as inputs.
Let's look at how to implement this.
Assignment 2: Implementing Modified Duration
Now, review the function modified_duration(fv, c, n, m, price) in the same resource.
This function follows a similar pattern to macaulay_duration. It first calculates the YTM, then finds the Macaulay Duration, and finally applies the conversion formula we just discussed to get the Modified Duration. Observe how it reuses the macaulay_duration function, which is good programming practice.
3. A Complete Python Implementation
Now we can consolidate these functions into a single, usable script. The following Python code brings together the necessary functions from the LINK resource. Given your interest in coding, you can treat this as a reference implementation that you could build into a more sophisticated bond analytics class later.
We will use the helper functions for cash flows, price, and YTM from the resource, and then implement our two main duration functions.
import math
from scipy.optimize import brentq
# --- Foundational Functions (based on resource [LINK](https://cs-people.bu.edu/azs/fe459/assignments/a2.html)) ---
def bond_cashflows(fv, c, n, m):
"""Generates a list of cashflows for a bond."""
coupon_payment = fv * c / m
num_periods = n * m
cashflows = [coupon_payment] * (num_periods - 1)
cashflows.append(coupon_payment + fv)
return cashflows
def bond_price(fv, c, n, m, r):
"""Calculates the price of a bond given a YTM."""
cashflows = bond_cashflows(fv, c, n, m)
periods = range(1, n * m + 1)
price = sum(cf / ((1 + r / m) ** period) for cf, period in zip(cashflows, periods))
return price
def bond_yield_to_maturity(fv, c, n, m, price):
"""Calculates the YTM of a bond using numerical root-finding."""
def price_error(r):
return bond_price(fv, c, n, m, r) - price
# Using a robust numerical solver (Brent's method)
# Search range from -100% to +100% yield
try:
ytm = brentq(price_error, -1.0, 1.0)
except ValueError:
return float('nan') # Solver failed to find a root
return ytm
# --- Duration Implementation ---
def macaulay_duration(fv, c, n, m, price):
"""Calculates the Macaulay Duration of a bond from its price."""
ytm = bond_yield_to_maturity(fv, c, n, m, price)
if math.isnan(ytm):
return float('nan')
cashflows = bond_cashflows(fv, c, n, m)
periods = range(1, n * m + 1)
weighted_time_sum = sum((i/m) * (cf / (1 + ytm/m)**i) for i, cf in zip(periods, cashflows))
# We use the known price for stability instead of re-calculating it
return weighted_time_sum / price
def modified_duration(fv, c, n, m, price):
"""Calculates the Modified Duration of a bond from its price."""
ytm = bond_yield_to_maturity(fv, c, n, m, price)
if math.isnan(ytm):
return float('nan')
mac_d = macaulay_duration(fv, c, n, m, price)
return mac_d / (1 + ytm / m)
# --- Example Usage ---
# 5-year bond, 8% annual coupon, semi-annual payments, FV=1000, selling at $950
fv = 1000
c = 0.08
n = 5
m = 2
price = 950
mac_d_val = macaulay_duration(fv, c, n, m, price)
mod_d_val = modified_duration(fv, c, n, m, price)
ytm_val = bond_yield_to_maturity(fv, c, n, m, price)
print(f"Bond Characteristics:")
print(f" Face Value: ${fv}, Coupon: {c*100}%, Years: {n}, Freq: {m}, Price: ${price}\n")
print(f"Calculated YTM: {ytm_val:.6f} ({ytm_val*100:.4f}%)")
print(f"Macaulay Duration: {mac_d_val:.4f} years")
print(f"Modified Duration: {mod_d_val:.4f}")
# Example for a zero-coupon bond
# 5-year zero-coupon bond, priced to yield 9.2% (semi-annual compounding)
fv_zcb = 1000
c_zcb = 0.0
n_zcb = 5
m_zcb = 2
ytm_zcb = 0.092
price_zcb = fv_zcb / (1 + ytm_zcb/m_zcb)**(n_zcb*m_zcb)
mac_d_zcb = macaulay_duration(fv_zcb, c_zcb, n_zcb, m_zcb, price_zcb)
print("\n--- Zero-Coupon Bond Example ---")
print(f"Price: ${price_zcb:.2f}")
print(f"Macaulay Duration: {mac_d_zcb:.4f} years (should be equal to maturity)")
This script provides a complete and reusable toolkit for calculating these key risk metrics. Note the test case for the zero-coupon bond, which correctly shows that its Macaulay Duration is equal to its maturity—a crucial theoretical check that confirms our implementation is sound.
Conclusion
In this lesson, we have successfully translated the mathematical definitions of Macaulay and Modified Duration into functional Python code. By building on foundational functions for cash flows and yield, we've created a practical tool for bond analysis.
Key Takeaways:
- Macaulay Duration implementation involves finding the YTM, calculating the present value of each cash flow, weighting each by its time to receipt, summing them up, and dividing by the bond's total price.
- Modified Duration implementation is a direct conversion from Macaulay Duration, requiring only the YTM and the compounding frequency: .
- These functions are essential components for any fixed-income analytics library, providing the first-order measure of interest rate risk.
We have now fully explored duration, from its theoretical derivation to its practical application and implementation. However, we've repeatedly noted that duration provides a linear approximation of a non-linear relationship. In our next lesson, we will address this limitation by introducing Convexity, the second-order risk measure that captures the curvature of the price-yield function and significantly improves our risk estimates.