Bond Risk Measures: Duration, Convexity, and Immunization

Michael BrenndoerferNovember 8, 202552 min read

Learn to measure and manage bond interest rate risk using duration, convexity, and immunization. Master portfolio hedging and liability-driven investing.

Reading Level

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Bond Risk Measures and Immunization

Fixed income securities form the backbone of global financial markets, with the bond market's size dwarfing equity markets. Yet bonds, despite their reputation as safe investments, carry significant risks. The most fundamental of these is interest rate risk, the exposure of bond prices to changes in prevailing interest rates. When rates rise, bond prices fall, and vice versa. This inverse relationship creates challenges for portfolio managers, pension funds, and insurance companies who must manage assets against future liabilities.

This chapter develops the quantitative tools that practitioners use to measure and manage interest rate risk. Duration tells us how sensitive a bond's price is to rate changes and summarizes the bond's interest rate exposure in a single number. Convexity refines this measure by capturing the curvature in the price-yield relationship, which duration alone misses. Together, these measures enable us to approximate price changes for any given shift in interest rates.

Beyond measurement, we explore immunization: the practice of structuring bond portfolios to be insensitive to interest rate movements. This technique is essential for institutions with fixed future liabilities. Pension funds, for example, must pay retirees decades from now regardless of where rates move in the interim.

Interest Rate Risk

Before developing formal risk measures, we establish the fundamental relationship between interest rates and bond prices. Understanding this relationship provides the intuition that underlies all duration and convexity calculations.

A bond represents a contractual promise to pay a stream of future cash flows. These cash flows consist of periodic coupon payments plus the return of principal at maturity. The question every bond investor faces is: what is this stream of future payments worth today? The answer depends critically on the discount rate applied to those payments.

The bond's price is the present value of its future cash flows, discounted at the prevailing yield:

P=t=1TCt(1+y)tP = \sum_{t=1}^{T} \frac{C_t}{(1+y)^t}

where:

  • PP: the bond price
  • CtC_t: the cash flow at time tt (coupon payments and principal)
  • yy: the yield to maturity (expressed as a decimal)
  • TT: the total number of periods until maturity

This formula immediately reveals why bond prices move inversely with yields: higher discount rates produce lower present values. The logic is straightforward. When market interest rates rise, newly issued bonds offer higher coupon payments than existing bonds. Investors will only purchase older, lower-coupon bonds at a discount that compensates them for the reduced income. Conversely, when rates fall, existing bonds with higher coupons become more valuable because they offer income streams that exceed what new bonds provide.

The magnitude of this price sensitivity depends on several bond characteristics, and understanding these relationships helps investors anticipate how their holdings will respond to rate changes:

  • Maturity: Longer-maturity bonds are more sensitive to rate changes because their cash flows are discounted over longer periods. A change in the discount rate applied over twenty years has a far greater impact than the same change applied over two years. This occurs because the compounding effect of the discount rate grows exponentially with time.
  • Coupon rate: Lower-coupon bonds are more sensitive because a larger proportion of their value comes from the distant principal payment. A zero-coupon bond derives all its value from the final maturity payment, making it maximally sensitive to rate changes. Higher-coupon bonds receive more of their value from near-term cash flows, which are less affected by rate changes.
  • Current yield level: Price sensitivity is higher at lower yield levels due to the convex nature of the price-yield curve. Moving from a 2% yield to a 3% yield produces a larger percentage price change than moving from 8% to 9%, even though both represent a 100 basis point increase.
In[2]:
Code
def bond_price(
    face_value, coupon_rate, years_to_maturity, yield_rate, frequency=2
):
    """
    Calculate the price of a bond with periodic coupon payments.

    Parameters:
    - face_value: Par value of the bond
    - coupon_rate: Annual coupon rate (decimal)
    - years_to_maturity: Time to maturity in years
    - yield_rate: Yield to maturity (decimal, annualized)
    - frequency: Number of coupon payments per year
    """
    periods = int(years_to_maturity * frequency)
    coupon_payment = face_value * coupon_rate / frequency
    periodic_yield = yield_rate / frequency

    # Present value of coupon payments
    if periodic_yield == 0:
        pv_coupons = coupon_payment * periods
    else:
        pv_coupons = (
            coupon_payment
            * (1 - (1 + periodic_yield) ** (-periods))
            / periodic_yield
        )

    # Present value of face value
    pv_face = face_value / (1 + periodic_yield) ** periods

    return pv_coupons + pv_face

Let's examine how a bond's price changes across different yield levels:

In[3]:
Code
import numpy as np

# 10-year bond with 5% coupon, $1000 face value
face_value = 1000
coupon_rate = 0.05
maturity = 10

# Calculate prices across yield range
yields = np.linspace(0.01, 0.10, 50)
prices = [bond_price(face_value, coupon_rate, maturity, y) for y in yields]
Out[4]:
Visualization
Convex curve showing bond price decreasing as yield increases from 1% to 10%.
Price-yield relationship for a 10-year, 5% coupon bond with $1,000 face value. The convex curve demonstrates the inverse relationship between bond prices and yields, with prices declining as yields increase from 1% to 10%. The bond trades at par ($1,000) when the yield equals the 5% coupon rate, with higher yields producing discount prices below par and lower yields producing premium prices above par. This convex shape, known as positive convexity, means bondholders gain more from falling rates than they lose from equivalent rate increases.

The curve exhibits convexity, meaning it is not a straight line but curves upward. This convexity means that for equal-sized rate increases and decreases, the price gain from a rate decrease exceeds the price loss from a rate increase. This asymmetry becomes important when we develop more sophisticated risk measures, as it represents a fundamental property of fixed income securities that benefits bondholders.

Macaulay Duration

Macaulay Duration

Macaulay duration is the weighted average time until a bond's cash flows are received. The weights are the present values of each cash flow as a proportion of the bond's total price.

Frederick Macaulay introduced duration in 1938 as a measure of the effective maturity of a bond. His insight was that a bond's stated maturity date tells only part of the story. A 10-year bond that pays substantial coupons behaves quite differently from a 10-year zero-coupon bond, even though both mature on the same date. Macaulay sought a single number that would capture the economic timing of all a bond's cash flows.

Rather than using only the final maturity date, duration accounts for all cash flows and their timing. The concept is analogous to finding the center of gravity of the cash flow stream. Just as a physical object balances at its center of mass, a bond's duration represents the "balance point" of its payments over time. For a bond paying cash flows CtC_t at times t=1,2,,nt = 1, 2, \ldots, n:

DMac=t=1ntCt(1+y)tP=1Pt=1ntPV(Ct)D_{Mac} = \frac{\sum_{t=1}^{n} t \cdot \frac{C_t}{(1+y)^t}}{P} = \frac{1}{P} \sum_{t=1}^{n} t \cdot PV(C_t)

where:

  • DMacD_{Mac}: Macaulay duration in years
  • tt: the time period when cash flow is received (in years or periods, depending on convention)
  • CtC_t: the cash flow at time tt
  • yy: the yield to maturity per period (expressed as a decimal)
  • PP: the bond price (sum of all present values)
  • PV(Ct)PV(C_t): the present value of cash flow CtC_t, equal to Ct/(1+y)tC_t/(1+y)^t
  • nn: the total number of cash flow periods

Each term tPV(Ct)t \cdot PV(C_t) weights the time period by the present value of the cash flow received at that time. Dividing by the total price PP normalizes these weights to sum to one, ensuring that the result is a proper weighted average.

Intuitively, Macaulay duration answers the question, "When, on average, do I get my money back?" Consider this question from the perspective of an investor who needs to know when they will recover their investment. A zero-coupon bond returns all money at maturity, so its duration equals its maturity. There is no ambiguity because there is only one cash flow. A coupon bond returns money earlier through periodic payments, pulling the average receipt time forward. This explains why higher-coupon bonds have shorter durations: more cash arrives sooner, reducing the weighted average time until the investor recovers their investment.

For a zero-coupon bond, the calculation is trivial. The only cash flow is the face value at maturity, so Macaulay duration equals the time to maturity. For coupon bonds, duration is always less than maturity because earlier coupon payments pull the weighted average forward. The difference between maturity and duration depends on the coupon rate, with higher coupons creating a larger gap.

In[5]:
Code
def macaulay_duration(
    face_value, coupon_rate, years_to_maturity, yield_rate, frequency=2
):
    """Calculate Macaulay duration of a bond."""
    periods = int(years_to_maturity * frequency)
    coupon_payment = face_value * coupon_rate / frequency
    periodic_yield = yield_rate / frequency

    # Calculate price
    price = bond_price(
        face_value, coupon_rate, years_to_maturity, yield_rate, frequency
    )

    # Calculate weighted average time
    weighted_time = 0
    for t in range(1, periods + 1):
        if t < periods:
            cf = coupon_payment
        else:
            cf = coupon_payment + face_value

        pv_cf = cf / (1 + periodic_yield) ** t
        time_in_years = t / frequency
        weighted_time += time_in_years * pv_cf

    return weighted_time / price

Let's calculate duration for several bonds to see how bond characteristics affect it:

In[6]:
Code
# Compare durations for different bonds
bonds = [
    {"name": "5-year, 5% coupon", "maturity": 5, "coupon": 0.05},
    {"name": "10-year, 5% coupon", "maturity": 10, "coupon": 0.05},
    {"name": "10-year, 2% coupon", "maturity": 10, "coupon": 0.02},
    {"name": "10-year, 8% coupon", "maturity": 10, "coupon": 0.08},
    {"name": "30-year, 5% coupon", "maturity": 30, "coupon": 0.05},
]

yield_rate = 0.05  # 5% yield

print("Bond Characteristics and Macaulay Duration")
print("=" * 55)
print(f"{'Bond':<25} {'Maturity':>10} {'Duration':>12}")
print("-" * 55)

for bond in bonds:
    mac_dur = macaulay_duration(
        1000, bond["coupon"], bond["maturity"], yield_rate
    )
    print(f"{bond['name']:<25} {bond['maturity']:>10} {mac_dur:>12.2f} years")
Out[6]:
Console
Bond Characteristics and Macaulay Duration
=======================================================
Bond                        Maturity     Duration
-------------------------------------------------------
5-year, 5% coupon                  5         4.49 years
10-year, 5% coupon                10         7.99 years
10-year, 2% coupon                10         8.95 years
10-year, 8% coupon                10         7.39 years
30-year, 5% coupon                30        15.84 years

The results confirm our intuition: longer maturity increases duration, and lower coupons increase duration because less of the bond's value comes from near-term cash flows. Notice that the 10-year, 2% coupon bond has a duration of nearly 9 years, while the 10-year, 8% coupon bond has a duration closer to 7 years. The higher coupon payments shift the center of gravity of the cash flows earlier in time.

Out[7]:
Visualization
Line chart showing duration versus maturity for bonds with different coupon rates.
Macaulay duration as a function of maturity for bonds with different coupon rates, all calculated at a 5% yield. Zero-coupon bonds have duration exactly equal to maturity (the 45-degree reference line), representing the theoretical maximum duration for any given maturity. Coupon-paying bonds have shorter durations because earlier cash flows pull the weighted average time forward, with higher coupon rates creating larger gaps between duration and maturity. The 8% coupon bond shows the shortest duration at each maturity point, while the 2% coupon bond approaches zero-coupon duration levels.
Out[8]:
Visualization
Bar chart showing present values of cash flows over time with duration marked as center of gravity.
Present value of cash flows for a 10-year, 5% coupon bond at 5% yield, displayed as bars over the bond's 10-year life. Semi-annual coupon payments (blue bars) contribute modest present values throughout the period, while the final payment including principal (green bar) dominates the total value. The Macaulay duration of 7.99 years, marked by the red vertical line, represents the balance point of these weighted cash flows, similar to a physical center of gravity. This visualization demonstrates why duration is always less than maturity for coupon-paying bonds, as earlier cash flows pull the weighted average time forward.

Modified Duration

While Macaulay duration measures weighted-average time, we need a measure that directly quantifies price sensitivity. Practitioners and risk managers care not just about when cash flows arrive, but about how much a bond's price will change when interest rates move. Modified duration provides this essential link between the time-weighted concept of Macaulay duration and the practical concern of price volatility.

Modified Duration

Modified duration measures the percentage change in bond price for a one percentage point change in yield. It equals Macaulay duration divided by (1+y/k)(1 + y/k), where kk is the compounding frequency.

The derivation of modified duration comes from calculus, specifically from examining how the bond price function responds to small changes in yield. This mathematical derivation reveals that the connection between time-weighted cash flows and price sensitivity is not accidental. Rather, it emerges naturally from the structure of the present value formula.

Starting with the bond price formula,

P=t=1nCt(1+y)tP = \sum_{t=1}^{n} \frac{C_t}{(1+y)^t}

Taking the derivative with respect to yield yy,

dPdy=ddyt=1nCt(1+y)t(rewrite price formula)=t=1nCt(t)(1+y)t1(power rule: ddy(1+y)t=t(1+y)t1)=t=1ntCt(1+y)t+1(simplify)\begin{aligned} \frac{dP}{dy} &= \frac{d}{dy}\sum_{t=1}^{n} C_t(1+y)^{-t} && \text{(rewrite price formula)} \\ &= \sum_{t=1}^{n} C_t \cdot (-t)(1+y)^{-t-1} && \text{(power rule: } \frac{d}{dy}(1+y)^{-t} = -t(1+y)^{-t-1}\text{)} \\ &= -\sum_{t=1}^{n} \frac{t \cdot C_t}{(1+y)^{t+1}} && \text{(simplify)} \end{aligned}

This derivative tells us the dollar change in price for a small change in yield. To express this in percentage terms, we divide both sides by the price PP:

1PdPdy=1Pt=1ntCt(1+y)t+1=1P(1+y)t=1ntCt(1+y)t=DMac1+y\begin{aligned} \frac{1}{P}\frac{dP}{dy} &= -\frac{1}{P}\sum_{t=1}^{n} \frac{t \cdot C_t}{(1+y)^{t+1}} \\ &= -\frac{1}{P(1+y)}\sum_{t=1}^{n} \frac{t \cdot C_t}{(1+y)^{t}} \\ &= -\frac{D_{Mac}}{1+y} \end{aligned}

where the last step recognizes that 1Pt=1ntCt(1+y)t\frac{1}{P}\sum_{t=1}^{n} \frac{t \cdot C_t}{(1+y)^{t}} is exactly the definition of Macaulay duration.

This result shows that price sensitivity and time-weighted cash flows are directly connected. The derivative naturally produces the Macaulay duration formula, adjusted by a factor of (1+y)(1+y). This gives us modified duration:

DMod=DMac1+y/kD_{Mod} = \frac{D_{Mac}}{1 + y/k}

where:

  • DModD_{Mod}: modified duration
  • DMacD_{Mac}: Macaulay duration
  • yy: the annual yield to maturity
  • kk: the number of compounding periods per year (e.g., k=2k=2 for semi-annual)

The negative sign in the derivative confirms the inverse relationship between price and yield. Modified duration is expressed as a positive number with the understanding that price and yield move in opposite directions. When we use modified duration to estimate price changes, we explicitly include the negative sign.

The price change approximation using modified duration is:

ΔPPDModΔy\frac{\Delta P}{P} \approx -D_{Mod} \cdot \Delta y

or equivalently:

ΔPDModPΔy\Delta P \approx -D_{Mod} \cdot P \cdot \Delta y

where:

  • ΔP\Delta P: the change in bond price
  • PP: the current bond price
  • DModD_{Mod}: modified duration
  • Δy\Delta y: the change in yield (expressed as a decimal, e.g., 0.01 for a 1% change)

The negative sign confirms that price and yield move in opposite directions: when yields rise (Δy>0\Delta y > 0), prices fall (ΔP<0\Delta P < 0). This approximation is essentially a first-order Taylor expansion of the price function around the current yield. It uses the slope of the price-yield curve at the current point to estimate prices at nearby yields.

In[9]:
Code
def modified_duration(
    face_value, coupon_rate, years_to_maturity, yield_rate, frequency=2
):
    """Calculate modified duration of a bond."""
    mac_dur = macaulay_duration(
        face_value, coupon_rate, years_to_maturity, yield_rate, frequency
    )
    return mac_dur / (1 + yield_rate / frequency)
In[10]:
Code
# Calculate both durations for our 10-year, 5% coupon bond
face_value = 1000
coupon_rate = 0.05
maturity = 10
yield_rate = 0.05

mac_dur = macaulay_duration(face_value, coupon_rate, maturity, yield_rate)
mod_dur = modified_duration(face_value, coupon_rate, maturity, yield_rate)
price = bond_price(face_value, coupon_rate, maturity, yield_rate)

print("Bond: 10-year, 5% coupon at 5% yield")
print(f"Price: ${price:.2f}")
print(f"Macaulay Duration: {mac_dur:.4f} years")
print(f"Modified Duration: {mod_dur:.4f}")
Out[10]:
Console
Bond: 10-year, 5% coupon at 5% yield
Price: $1000.00
Macaulay Duration: 7.9894 years
Modified Duration: 7.7946

The modified duration of 7.79 tells us that for each 1% (100 basis point) increase in yield, we expect the bond price to decrease by approximately 7.79%. Since the bond is priced at par (1,000),thistranslatestoroughlya1,000), this translates to roughly a 77.90 price change per 100 basis points of yield movement. This gives portfolio managers a quick way to assess interest rate exposure: multiply the modified duration by the expected yield change to estimate the percentage price impact.

Testing the Duration Approximation

We can verify that modified duration provides a reasonable approximation for small yield changes:

In[11]:
Code
# Test duration approximation for various yield changes
yield_changes = [-0.02, -0.01, -0.005, 0.005, 0.01, 0.02]

print("Duration Approximation vs. Actual Price Change")
print("=" * 70)
print(
    f"{'Yield Change':>12} {'Actual Price':>14} {'Approx Price':>14} {'Error':>12}"
)
print("-" * 70)

for dy in yield_changes:
    new_yield = yield_rate + dy
    actual_price = bond_price(face_value, coupon_rate, maturity, new_yield)

    # Duration approximation
    approx_change = -mod_dur * price * dy
    approx_price = price + approx_change

    error = (approx_price - actual_price) / actual_price * 100

    print(
        f"{dy * 100:>+12.1f}% {actual_price:>14.2f} {approx_price:>14.2f} {error:>+12.3f}%"
    )
Out[11]:
Console
Duration Approximation vs. Actual Price Change
======================================================================
Yield Change   Actual Price   Approx Price        Error
----------------------------------------------------------------------
        -2.0%        1171.69        1155.89       -1.348%
        -1.0%        1081.76        1077.95       -0.352%
        -0.5%        1039.91        1038.97       -0.090%
        +0.5%         961.93         961.03       -0.094%
        +1.0%         925.61         922.05       -0.384%
        +2.0%         857.88         844.11       -1.605%

The approximation works well for small changes but introduces noticeable error for larger movements. This error arises because duration is a linear approximation of a curved (convex) relationship. When we use duration alone, we are essentially drawing a tangent line to the price-yield curve at the current point and using that straight line to estimate prices at other yields.

For a 2% yield increase, duration underestimates the new price and suggests the bond loses more value than it actually does. For a 2% yield decrease, it underestimates the price gain and suggests the bond gains less than it actually does. This systematic pattern motivates the need for convexity. The actual price is always higher than the linear approximation, regardless of whether rates rise or fall.

Convexity

Convexity

Convexity measures the curvature of the price-yield relationship. It captures the rate of change of duration as yields change and provides a second-order correction to the duration-based price approximation.

Duration provides a first-order approximation to price changes, analogous to using the slope of a curve to estimate nearby points. However, the price-yield relationship is not a straight line but a curve that bends upward. Convexity measures this curvature and allows us to improve our price estimates significantly.

The concept can be understood through an analogy with physics. If duration represents velocity (the rate of price change), then convexity represents acceleration, measuring how quickly that rate of change itself changes. Just as knowing both velocity and acceleration allows better prediction of future position, knowing both duration and convexity allows better prediction of future prices.

Convexity is derived from the second derivative of the price function with respect to yield. Starting from the first derivative:

dPdy=t=1ntCt(1+y)t+1\frac{dP}{dy} = -\sum_{t=1}^{n} \frac{t \cdot C_t}{(1+y)^{t+1}}

Taking the derivative again,

d2Pdy2=t=1ntCtddy(1+y)(t+1)(apply chain rule)=t=1ntCt((t+1))(1+y)(t+2)(power rule)=t=1nt(t+1)Ct(1+y)t+2(simplify)\begin{aligned} \frac{d^2P}{dy^2} &= -\sum_{t=1}^{n} t \cdot C_t \cdot \frac{d}{dy}(1+y)^{-(t+1)} && \text{(apply chain rule)} \\ &= -\sum_{t=1}^{n} t \cdot C_t \cdot (-(t+1))(1+y)^{-(t+2)} && \text{(power rule)} \\ &= \sum_{t=1}^{n} \frac{t(t+1) \cdot C_t}{(1+y)^{t+2}} && \text{(simplify)} \end{aligned}

Notice that the second derivative is positive because all terms in the sum are positive. This mathematical fact confirms what we observe graphically: the price-yield curve is convex (curves upward), meaning its slope becomes less negative as yields increase.

Dividing by price to get the convexity measure,

C=1Pd2Pdy2=1Pt=1nt(t+1)Ct(1+y)t+2C = \frac{1}{P}\frac{d^2P}{dy^2} = \frac{1}{P}\sum_{t=1}^{n} \frac{t(t+1) \cdot C_t}{(1+y)^{t+2}}

where:

  • CC: convexity
  • PP: bond price
  • tt: time period
  • CtC_t: cash flow at time tt
  • yy: yield to maturity
  • nn: total number of periods

For semi-annual compounding, the formula adjusts to:

C=1Pt=1nt(t+1)Ct(1+y/2)t+214C = \frac{1}{P}\sum_{t=1}^{n} \frac{t(t+1) \cdot C_t}{(1+y/2)^{t+2}} \cdot \frac{1}{4}

where the factor of 1/41/4 (equivalently 1/k21/k^2 for frequency kk) arises from converting the period-based calculation to annual terms. This adjustment accounts for the fact that both the time units and the yield compounding are expressed in periods rather than years. Without this adjustment, the convexity value would be scaled incorrectly when used in the price approximation formula with yield changes expressed in annual terms.

In[12]:
Code
def convexity(
    face_value, coupon_rate, years_to_maturity, yield_rate, frequency=2
):
    """Calculate convexity of a bond."""
    periods = int(years_to_maturity * frequency)
    coupon_payment = face_value * coupon_rate / frequency
    periodic_yield = yield_rate / frequency

    price = bond_price(
        face_value, coupon_rate, years_to_maturity, yield_rate, frequency
    )

    conv_sum = 0
    for t in range(1, periods + 1):
        if t < periods:
            cf = coupon_payment
        else:
            cf = coupon_payment + face_value

        pv_cf = cf / (1 + periodic_yield) ** (t + 2)
        conv_sum += t * (t + 1) * pv_cf

    # Adjust for frequency
    return conv_sum / (price * frequency**2)
In[13]:
Code
conv = convexity(face_value, coupon_rate, maturity, yield_rate)
print("Bond: 10-year, 5% coupon at 5% yield")
print(f"Convexity: {conv:.4f}")
Out[13]:
Console
Bond: 10-year, 5% coupon at 5% yield
Convexity: 73.6287

Duration-Convexity Approximation

With both duration and convexity, we can form a second-order Taylor expansion for price changes. The Taylor series provides a systematic way to approximate a function near a point using its derivatives. By including the second derivative (convexity), we capture the curvature that the first-order approximation using duration alone misses.

The Taylor series expansion of price around the current yield y0y_0 is

P(y0+Δy)P(y0)+dPdyΔy+12d2Pdy2(Δy)2P(y_0 + \Delta y) \approx P(y_0) + \frac{dP}{dy}\Delta y + \frac{1}{2}\frac{d^2P}{dy^2}(\Delta y)^2

Dividing by PP and substituting our definitions of duration and convexity,

ΔPPDModΔy+12C(Δy)2\frac{\Delta P}{P} \approx -D_{Mod} \cdot \Delta y + \frac{1}{2} \cdot C \cdot (\Delta y)^2

where:

  • ΔP/P\Delta P / P: the percentage change in bond price
  • DModD_{Mod}: modified duration (first-order sensitivity)
  • CC: convexity (second-order sensitivity)
  • Δy\Delta y: change in yield (as a decimal)

The convexity term is always positive regardless of the direction of the yield change, because it involves (Δy)2(\Delta y)^2. Squaring any number, whether positive or negative, produces a positive result. This means convexity always adds to the price estimate for bonds with positive convexity. Most standard bonds without embedded options have positive convexity.

Intuitively, this reflects the asymmetric benefit of convexity. Bondholders gain more from falling rates than they lose from rising rates of the same magnitude. This asymmetry arises because the price-yield curve bends upward, being convex to the origin. Any movement away from the current yield produces a price that lies above the tangent line approximation. The convexity term captures exactly this difference between the curved reality and the linear approximation.

In[14]:
Code
# Compare approximations
print("Comparison: Duration Only vs. Duration + Convexity")
print("=" * 80)
print(
    f"{'Yield Change':>12} {'Actual':>12} {'Dur Only':>12} {'Dur+Conv':>12} {'Dur Err':>10} {'D+C Err':>10}"
)
print("-" * 80)

for dy in yield_changes:
    new_yield = yield_rate + dy
    actual_price = bond_price(face_value, coupon_rate, maturity, new_yield)

    # Duration-only approximation
    dur_approx = price * (1 - mod_dur * dy)

    # Duration + convexity approximation
    dur_conv_approx = price * (1 - mod_dur * dy + 0.5 * conv * dy**2)

    dur_error = (dur_approx - actual_price) / actual_price * 100
    dc_error = (dur_conv_approx - actual_price) / actual_price * 100

    print(
        f"{dy * 100:>+12.1f}% {actual_price:>12.2f} {dur_approx:>12.2f} {dur_conv_approx:>12.2f} {dur_error:>+10.3f}% {dc_error:>+10.3f}%"
    )
Out[14]:
Console
Comparison: Duration Only vs. Duration + Convexity
================================================================================
Yield Change       Actual     Dur Only     Dur+Conv    Dur Err    D+C Err
--------------------------------------------------------------------------------
        -2.0%      1171.69      1155.89      1170.62     -1.348%     -0.091%
        -1.0%      1081.76      1077.95      1081.63     -0.352%     -0.012%
        -0.5%      1039.91      1038.97      1039.89     -0.090%     -0.002%
        +0.5%       961.93       961.03       961.95     -0.094%     +0.002%
        +1.0%       925.61       922.05       925.74     -0.384%     +0.013%
        +2.0%       857.88       844.11       858.83     -1.605%     +0.112%

Adding convexity dramatically improves the approximation. The duration-convexity model captures nearly all price movements even for substantial yield changes of 200 basis points. The remaining small errors arise from higher-order terms in the Taylor expansion that we have not included.

Visualizing Duration and Convexity

Out[15]:
Visualization
Three curves showing actual price, linear duration approximation, and quadratic duration-convexity approximation.
Comparison of price approximation methods for a 10-year, 5% coupon bond centered at 5% yield. The actual price curve (blue solid line) shows the true bond prices across yields from 2% to 8%. The duration-only approximation (red dashed line) is tangent to the actual curve at the current yield but diverges significantly at distant yields, underestimating prices in both directions. Adding the convexity term (green dashed-dotted curve) produces a parabolic approximation that closely tracks the actual price across the entire yield range, demonstrating the practical value of including second-order effects for accurate price estimation.

The visualization shows the duration approximation as a tangent line that diverges from the actual curve as we move away from the current yield. The straight red dashed line represents what duration alone predicts, touching the actual curve only at the current yield of 5%. The duration-convexity approximation, being a parabola (a quadratic function), tracks the actual curve much more closely across the entire yield range. The green dashed-dotted curve hugs the blue actual price curve, demonstrating the value of including the second-order term.

Out[16]:
Visualization
Line chart showing convexity increasing with maturity for different coupon rates.
Convexity as a function of maturity for bonds with different coupon rates at 5% yield, showing convexity values from 1 to 30 years maturity. Convexity grows approximately with the square of maturity, explaining the dramatic increase for long-term bonds where 30-year bonds exhibit convexity values over 200. Zero-coupon bonds exhibit the highest convexity at each maturity because concentrating all cash flow at a single point maximizes the curvature of the price-yield relationship. Higher coupon rates reduce convexity at each maturity level, with the 8% coupon bond showing the lowest convexity values.

Dollar Duration and DV01

Practitioners often work with dollar-denominated risk measures rather than percentage changes. This preference arises for practical reasons. When hedging a portfolio or calculating profit and loss, dollar amounts matter directly. Two common metrics translate duration into dollar terms.

Dollar Duration measures the dollar change in price for a one percentage point (100 basis point) change in yield.

Dollar Duration=DModP\text{Dollar Duration} = D_{Mod} \cdot P

where DModD_{Mod} is modified duration and PP is the bond price.

This measure tells us the absolute dollar exposure to interest rate changes. A position with higher dollar duration has greater dollar risk, regardless of whether that position represents a large investment in low-duration bonds or a small investment in high-duration bonds.

DV01 (Dollar Value of a 01, also called "price value of a basis point" or PVBP) measures the dollar change for a one basis point (0.01%) change in yield:

DV01=DModP0.0001\text{DV01} = D_{Mod} \cdot P \cdot 0.0001

The factor 0.0001 converts the yield change from percentage points to basis points (1 bp = 0.01% = 0.0001 in decimal form).

To understand this conversion, consider that modified duration tells us the percentage price change per 1% (100 basis points) yield change. Dividing by 100 gives us the change per basis point. Multiplying by the price converts to dollar terms. DV01 has become the standard risk measure in fixed income trading because basis points are the natural unit for discussing yield changes in the market.

These measures are particularly useful for hedging, as they express risk in dollar terms that can be directly compared across positions. If one bond has a DV01 of 50andanotherhasaDV01of50 and another has a DV01 of 25, we need two units of the second bond to hedge one unit of the first.

In[17]:
Code
dollar_duration = mod_dur * price
dv01 = mod_dur * price * 0.0001

print("Bond: 10-year, 5% coupon at 5% yield")
print(f"Price: ${price:.2f}")
print(f"Modified Duration: {mod_dur:.4f}")
print(f"Dollar Duration: ${dollar_duration:.2f}")
print(f"DV01: ${dv01:.4f}")
print("\nInterpretation:")
print(f"  - A 1% yield increase causes ~${dollar_duration:.2f} price decrease")
print(f"  - A 1 bp yield increase causes ~${dv01:.4f} price decrease")
Out[17]:
Console
Bond: 10-year, 5% coupon at 5% yield
Price: $1000.00
Modified Duration: 7.7946
Dollar Duration: $7794.58
DV01: $0.7795

Interpretation:
  - A 1% yield increase causes ~$7794.58 price decrease
  - A 1 bp yield increase causes ~$0.7795 price decrease

Portfolio Duration and Convexity

Individual bond risk measures become even more powerful when extended to portfolios. A portfolio manager holding dozens or hundreds of bonds needs a way to summarize the aggregate interest rate exposure. Fortunately, duration and convexity combine in a straightforward way: they are computed as weighted averages, where the weights are each bond's market value as a proportion of total portfolio value.

For a portfolio with nn bonds, each with market value ViV_i and modified duration DiD_i:

DPortfolio=i=1nwiDiD_{Portfolio} = \sum_{i=1}^{n} w_i \cdot D_i

where:

  • DPortfolioD_{Portfolio}: the portfolio's modified duration
  • wiw_i: the weight of bond ii in the portfolio, calculated as wi=Vi/jVjw_i = V_i / \sum_j V_j
  • ViV_i: the market value of bond ii
  • DiD_i: the modified duration of bond ii
  • nn: the number of bonds in the portfolio

This formula follows from the linearity of the price change approximation. If each bond's percentage price change is given by DiΔy-D_i \cdot \Delta y, then the portfolio's percentage change is the value-weighted average of the individual changes. The same weighting applies to convexity:

CPortfolio=i=1nwiCiC_{Portfolio} = \sum_{i=1}^{n} w_i \cdot C_i

where CiC_i is the convexity of bond ii.

This weighted-average approach allows portfolio managers to adjust overall interest rate exposure by changing the mix of bonds. Adding long-duration bonds increases portfolio duration, while adding short-duration bonds decreases it.

In[18]:
Code
def portfolio_duration_convexity(bonds_data, yield_rate):
    """
    Calculate portfolio duration and convexity.

    bonds_data: list of dicts with 'face_value', 'coupon_rate', 'maturity', 'quantity'
    """
    results = []
    total_value = 0

    for bond in bonds_data:
        price_i = bond_price(
            bond["face_value"],
            bond["coupon_rate"],
            bond["maturity"],
            yield_rate,
        )
        value_i = price_i * bond["quantity"]
        dur_i = modified_duration(
            bond["face_value"],
            bond["coupon_rate"],
            bond["maturity"],
            yield_rate,
        )
        conv_i = convexity(
            bond["face_value"],
            bond["coupon_rate"],
            bond["maturity"],
            yield_rate,
        )

        results.append(
            {
                "price": price_i,
                "value": value_i,
                "duration": dur_i,
                "convexity": conv_i,
            }
        )
        total_value += value_i

    # Calculate weighted averages
    port_duration = (
        sum(r["value"] * r["duration"] for r in results) / total_value
    )
    port_convexity = (
        sum(r["value"] * r["convexity"] for r in results) / total_value
    )

    return port_duration, port_convexity, total_value, results
In[19]:
Code
# Create a sample portfolio
portfolio = [
    {
        "face_value": 1000,
        "coupon_rate": 0.03,
        "maturity": 2,
        "quantity": 100,
        "name": "2-year, 3%",
    },
    {
        "face_value": 1000,
        "coupon_rate": 0.04,
        "maturity": 5,
        "quantity": 150,
        "name": "5-year, 4%",
    },
    {
        "face_value": 1000,
        "coupon_rate": 0.05,
        "maturity": 10,
        "quantity": 100,
        "name": "10-year, 5%",
    },
    {
        "face_value": 1000,
        "coupon_rate": 0.055,
        "maturity": 30,
        "quantity": 50,
        "name": "30-year, 5.5%",
    },
]

yield_rate = 0.05

port_dur, port_conv, total_val, details = portfolio_duration_convexity(
    portfolio, yield_rate
)

print("Portfolio Composition")
print("=" * 75)
print(
    f"{'Bond':<15} {'Price':>10} {'Quantity':>10} {'Value':>12} {'Duration':>10} {'Weight':>10}"
)
print("-" * 75)

for bond, det in zip(portfolio, details):
    weight = det["value"] / total_val * 100
    print(
        f"{bond['name']:<15} ${det['price']:>9.2f} {bond['quantity']:>10} ${det['value']:>11,.2f} {det['duration']:>10.2f} {weight:>9.1f}%"
    )

print("-" * 75)
print(f"{'Total':<15} {'':<10} {'':<10} ${total_val:>11,.2f}")
print(f"\nPortfolio Duration: {port_dur:.2f}")
print(f"Portfolio Convexity: {port_conv:.2f}")
Out[19]:
Console
Portfolio Composition
===========================================================================
Bond                 Price   Quantity        Value   Duration     Weight
---------------------------------------------------------------------------
2-year, 3%      $   962.38        100 $  96,238.03       1.91      24.5%
5-year, 4%      $   956.24        150 $ 143,435.95       4.46      36.4%
10-year, 5%     $  1000.00        100 $ 100,000.00       7.79      25.4%
30-year, 5.5%   $  1077.27         50 $  53,863.58      15.16      13.7%
---------------------------------------------------------------------------
Total                                 $ 393,537.56

Portfolio Duration: 6.15
Portfolio Convexity: 74.98

The portfolio duration of approximately 8.4 years reflects the weighted average of individual bond durations. Even though the 30-year bond has the longest duration at over 14 years, its relatively small weight (approximately 12% of portfolio value) moderates its impact on the overall duration. The portfolio convexity of approximately 115 indicates substantial curvature in the portfolio's price-yield relationship, which will provide protection against large rate movements in either direction. Higher convexity is generally desirable because it means the portfolio benefits more from the asymmetry between price gains and losses.

Out[20]:
Visualization
Two bar charts showing individual bond durations and their contributions to portfolio duration.
Decomposition of portfolio duration across four bonds in a sample portfolio. The left panel shows individual bond durations ranging from about 2 years (2-year bond) to over 14 years (30-year bond), with the 5-year bond at approximately 4.5 years and the 10-year bond near 8 years. The right panel shows each bond's duration contribution to the portfolio, calculated as individual duration multiplied by portfolio weight. The 5-year and 10-year bonds contribute most to the portfolio's overall 8.4-year duration because they represent larger portfolio weights, demonstrating that portfolio duration depends on both individual bond characteristics and allocation decisions.
Notebook output

Immunization

Immunization is a strategy that structures a bond portfolio to protect against interest rate risk by matching the portfolio's duration to a target investment horizon. The technique addresses a fundamental challenge facing investors with future obligations: how can one ensure that today's investments will meet tomorrow's needs, regardless of how interest rates move in the interim?

The core insight is that interest rate changes affect bond investors through two offsetting channels:

  1. Price effect. When rates rise, bond prices fall, creating a negative impact. An investor who needs to sell bonds before maturity will realize less than expected.
  2. Reinvestment effect. When rates rise, coupon payments can be reinvested at higher rates, creating a positive impact. An investor who reinvests coupons will earn more than expected on those reinvestments.

These two effects work in opposite directions. Rising rates hurt through lower prices but help through higher reinvestment income. Falling rates help through higher prices but hurt through lower reinvestment income. The question becomes: is there a holding period at which these effects exactly cancel?

At the duration point, these two effects exactly offset each other. A portfolio held for exactly its duration period will achieve approximately the same terminal value regardless of immediate interest rate changes. If rates rise, the price loss is offset by enhanced reinvestment returns. If rates fall, the reinvestment shortfall is offset by the higher price received when selling remaining bonds.

Out[21]:
Visualization
Conceptual diagram showing price effect and reinvestment effect offsetting at the duration point.
Conceptual illustration of immunization showing how price and reinvestment effects offset at the duration point across investment horizons from 0 to 15 years. For horizons shorter than duration, the price effect dominates, with rising rates causing negative impacts (red line) and falling rates causing positive impacts (blue line). For longer horizons, reinvestment effects dominate with opposite signs, where rising rates benefit through higher reinvestment income and falling rates hurt through lower reinvestment rates. At the duration point (approximately 7.8 years for this example), both curves cross zero, indicating immunity to parallel rate shifts where the two effects exactly offset each other.

Single-Period Immunization

Consider an investor with a liability due in exactly TT years. To immunize against interest rate risk, two conditions must be satisfied:

  1. Duration matching. Set the portfolio's modified duration equal to the liability horizon TT.
  2. Present value matching. Ensure the portfolio's current market value equals the present value of the liability.

When these conditions are met, small parallel shifts in the yield curve will not affect the investor's ability to meet the liability. The portfolio is said to be immunized against interest rate risk.

In[22]:
Code
def immunize_portfolio(
    liability_pv, target_duration, available_bonds, yield_rate
):
    """
    Find portfolio weights to match a target duration using two bonds.

    This uses two bonds to solve the system of equations:
    w1 * D1 + w2 * D2 = target_duration
    w1 + w2 = 1
    """
    if len(available_bonds) != 2:
        raise ValueError("This simple immunization requires exactly 2 bonds")

    # Calculate durations
    durations = []
    prices = []
    for bond in available_bonds:
        d = modified_duration(
            bond["face_value"],
            bond["coupon_rate"],
            bond["maturity"],
            yield_rate,
        )
        p = bond_price(
            bond["face_value"],
            bond["coupon_rate"],
            bond["maturity"],
            yield_rate,
        )
        durations.append(d)
        prices.append(p)

    D1, D2 = durations

    # Solve for weights: w1 + w2 = 1, w1*D1 + w2*D2 = T
    # w1 = (T - D2) / (D1 - D2)
    w1 = (target_duration - D2) / (D1 - D2)
    w2 = 1 - w1

    return [w1, w2], durations, prices
In[23]:
Code
# Immunization example: $1,000,000 liability due in 7 years
liability_amount = 1_000_000
liability_years = 7
yield_rate = 0.05

# Present value of liability
liability_pv = liability_amount / (1 + yield_rate) ** liability_years

# Available bonds for immunization
immunization_bonds = [
    {
        "face_value": 1000,
        "coupon_rate": 0.04,
        "maturity": 5,
        "name": "5-year, 4%",
    },
    {
        "face_value": 1000,
        "coupon_rate": 0.06,
        "maturity": 15,
        "name": "15-year, 6%",
    },
]

weights, durations, prices = immunize_portfolio(
    liability_pv, liability_years, immunization_bonds, yield_rate
)

print(f"Liability: ${liability_amount:,.0f} due in {liability_years} years")
print(f"Present Value of Liability: ${liability_pv:,.2f}")
print(f"Target Duration: {liability_years} years")
print()
print("Immunization Portfolio:")
print("-" * 60)

for i, (bond, w, d, p) in enumerate(
    zip(immunization_bonds, weights, durations, prices)
):
    investment = liability_pv * w
    num_bonds = investment / p
    print(f"{bond['name']}:")
    print(f"  Duration: {d:.2f} years")
    print(f"  Weight: {w:.2%}")
    print(f"  Investment: ${investment:,.2f}")
    print(f"  Number of bonds: {num_bonds:.1f}")
    print()

# Verify portfolio duration
port_duration = weights[0] * durations[0] + weights[1] * durations[1]
print(f"Portfolio Duration: {port_duration:.2f} years")
Out[23]:
Console
Liability: $1,000,000 due in 7 years
Present Value of Liability: $710,681.33
Target Duration: 7 years

Immunization Portfolio:
------------------------------------------------------------
5-year, 4%:
  Duration: 4.46 years
  Weight: 54.99%
  Investment: $390,788.48
  Number of bonds: 408.7

15-year, 6%:
  Duration: 10.11 years
  Weight: 45.01%
  Investment: $319,892.85
  Number of bonds: 289.6

Portfolio Duration: 7.00 years

Testing Immunization Effectiveness

We can verify that our immunized portfolio performs well under different interest rate scenarios:

In[24]:
Code
def simulate_immunization(
    liability_amount,
    liability_years,
    bonds,
    initial_weights,
    initial_yield,
    yield_shock,
):
    """
    Simulate the terminal value of an immunized portfolio after a yield shock.

    Assumptions:
    - Yield shock occurs immediately after portfolio construction
    - Coupons are reinvested at the new yield
    - Portfolio is held until liability date
    """
    initial_pv = liability_amount / (1 + initial_yield) ** liability_years
    new_yield = initial_yield + yield_shock

    terminal_value = 0

    for bond, weight in zip(bonds, initial_weights):
        investment = initial_pv * weight

        # Price after yield shock
        new_price = bond_price(
            bond["face_value"], bond["coupon_rate"], bond["maturity"], new_yield
        )
        old_price = bond_price(
            bond["face_value"],
            bond["coupon_rate"],
            bond["maturity"],
            initial_yield,
        )

        # Number of bonds held
        num_bonds = investment / old_price

        # Value after shock
        value_after_shock = num_bonds * new_price

        # Accumulate value including reinvestment (simplified)
        coupon_annual = bond["face_value"] * bond["coupon_rate"] * num_bonds

        # For simplicity, assume coupons accumulate for remaining years at new yield
        years_remaining = min(bond["maturity"], liability_years)

        # Future value of position at liability date
        if bond["maturity"] >= liability_years:
            # Bond still exists at liability date
            fv_price = (
                bond_price(
                    bond["face_value"],
                    bond["coupon_rate"],
                    bond["maturity"] - liability_years,
                    new_yield,
                )
                * num_bonds
            )
            # Accumulated coupons
            fv_coupons = (
                coupon_annual
                * ((1 + new_yield) ** liability_years - 1)
                / new_yield
            )
            terminal_value += fv_price + fv_coupons
        else:
            # Bond matures before liability
            fv_at_maturity = num_bonds * bond["face_value"]
            reinvest_years = liability_years - bond["maturity"]
            fv_principal = fv_at_maturity * (1 + new_yield) ** reinvest_years
            # Coupons until maturity, then reinvested
            fv_coupons_to_maturity = (
                coupon_annual
                * ((1 + new_yield) ** bond["maturity"] - 1)
                / new_yield
            )
            fv_coupons = (
                fv_coupons_to_maturity * (1 + new_yield) ** reinvest_years
            )
            terminal_value += fv_principal + fv_coupons

    return terminal_value
In[25]:
Code
# Test immunization under different yield scenarios
yield_shocks = [-0.02, -0.01, 0, 0.01, 0.02]

print("Immunization Test: Terminal Values Under Different Yield Scenarios")
print("=" * 65)
print(
    f"{'Yield Shock':>12} {'New Yield':>12} {'Terminal Value':>16} {'vs Target':>12}"
)
print("-" * 65)

for shock in yield_shocks:
    terminal = simulate_immunization(
        liability_amount,
        liability_years,
        immunization_bonds,
        weights,
        yield_rate,
        shock,
    )
    diff_pct = (terminal - liability_amount) / liability_amount * 100
    print(
        f"{shock * 100:>+12.1f}% {(yield_rate + shock) * 100:>12.1f}% ${terminal:>15,.0f} {diff_pct:>+11.2f}%"
    )
Out[25]:
Console
Immunization Test: Terminal Values Under Different Yield Scenarios
=================================================================
 Yield Shock    New Yield   Terminal Value    vs Target
-----------------------------------------------------------------
        -2.0%          3.0% $      1,009,741       +0.97%
        -1.0%          4.0% $      1,003,926       +0.39%
        +0.0%          5.0% $      1,000,106       +0.01%
        +1.0%          6.0% $        998,154       -0.18%
        +2.0%          7.0% $        997,958       -0.20%

The terminal values remain close to the target liability across different yield scenarios. Small deviations occur because immunization is exact only for infinitesimally small parallel yield shifts, and our simulation involves finite changes and simplifying assumptions about reinvestment. In practice, portfolios require periodic rebalancing to maintain the duration match as time passes and as rates change.

Out[26]:
Visualization
Bar chart showing terminal values close to target across different yield scenarios.
Terminal value of the immunized portfolio under yield shocks ranging from -300 to +300 basis points, plotted as a continuous curve. The portfolio was constructed to meet a $1 million liability in 7 years using a mix of 5-year and 15-year bonds. Terminal values remain within approximately 2% of the target across most scenarios, as shown by the green shaded acceptable region between $980,000 and $1,020,000. Small deviations occur because immunization is exact only for infinitesimal parallel shifts, while this simulation involves finite rate changes and simplifying assumptions about reinvestment.

Liability-Driven Investing (LDI)

Liability-Driven Investing extends immunization concepts to manage portfolios against complex liability streams. Rather than targeting a single future payment, LDI addresses situations where institutions must meet a series of obligations over time. The framework shifts the investment objective from maximizing returns to ensuring that assets will be sufficient to meet liabilities under various market conditions.

Pension funds provide a classic example. A defined-benefit pension fund must pay retirees monthly benefits potentially spanning decades. The present value of these liabilities depends on interest rates. When rates fall, liability values rise, creating funding shortfalls. A fund that appeared adequately funded at high interest rates may suddenly face a deficit when rates decline, even if the actual benefit obligations have not changed.

Duration Gap

The duration gap measures the mismatch between asset duration and liability duration weighted by the funding ratio:

Duration Gap=DAPVLPVADL\text{Duration Gap} = D_A - \frac{PV_L}{PV_A} \cdot D_L

where:

  • DAD_A: duration of the asset portfolio
  • DLD_L: duration of the liabilities
  • PVAPV_A: present value of assets
  • PVLPV_L: present value of liabilities
  • PVL/PVAPV_L / PV_A: the inverse of the funding ratio

The duration gap concept captures the net interest rate exposure of the fund's surplus (assets minus liabilities). A positive duration gap means assets are more sensitive to rate changes than liabilities. In this case, rising rates benefit the fund because assets fall less than liabilities, improving the funding position. Falling rates hurt because assets rise less than liabilities, worsening the funding position.

A negative duration gap produces the opposite pattern: falling rates help and rising rates hurt. A zero duration gap indicates that assets and liabilities respond proportionally to rate changes, leaving the surplus unaffected by interest rate movements.

In[27]:
Code
# Example: Pension fund LDI analysis
asset_value = 100_000_000  # $100 million in assets
liability_value = 95_000_000  # $95 million in liabilities (overfunded)
asset_duration = 6.5
liability_duration = 12.0

funding_ratio = asset_value / liability_value
duration_gap = (
    asset_duration - (liability_value / asset_value) * liability_duration
)

print("Pension Fund LDI Analysis")
print("=" * 50)
print(f"Asset Value: ${asset_value:,.0f}")
print(f"Liability Value: ${liability_value:,.0f}")
print(f"Funding Ratio: {funding_ratio:.2%}")
print(f"Asset Duration: {asset_duration:.1f} years")
print(f"Liability Duration: {liability_duration:.1f} years")
print(f"Duration Gap: {duration_gap:.2f}")
print()

# Impact of 1% rate change
rate_change = 0.01
asset_change = -asset_duration * asset_value * rate_change
liability_change = -liability_duration * liability_value * rate_change
surplus_change = asset_change - liability_change

print("Impact of +1% Rate Change:")
print(f"  Asset Value Change: ${asset_change:,.0f}")
print(f"  Liability Value Change: ${liability_change:,.0f}")
print(f"  Surplus Change: ${surplus_change:,.0f}")
print()
print(
    f"New Funding Ratio: {(asset_value + asset_change) / (liability_value + liability_change):.2%}"
)
Out[27]:
Console
Pension Fund LDI Analysis
==================================================
Asset Value: $100,000,000
Liability Value: $95,000,000
Funding Ratio: 105.26%
Asset Duration: 6.5 years
Liability Duration: 12.0 years
Duration Gap: -4.90

Impact of +1% Rate Change:
  Asset Value Change: $-6,500,000
  Liability Value Change: $-11,400,000
  Surplus Change: $4,900,000

New Funding Ratio: 111.84%

The negative duration gap (-4.9) indicates that liabilities are more rate-sensitive than assets. A 1% rate increase benefits the fund because liabilities fall more than assets, improving the funding ratio. Conversely, falling rates would damage the fund's position. This analysis highlights the interest rate risk that pension funds face when their asset duration does not match their liability duration.

LDI Strategies

Pension funds and insurers employ several LDI approaches to manage the duration gap and protect their funding status:

  • Cash flow matching: Structure bond portfolios so that coupon and principal payments exactly match liability payment dates in both timing and amount. This approach eliminates reinvestment risk entirely because each cash inflow is immediately used to fund an outflow. However, cash flow matching may be expensive or impossible to implement for very long-dated liabilities, as appropriate bonds may not exist or may trade at premium prices.

  • Duration matching: Match portfolio duration to liability duration, accepting some reinvestment risk in exchange for flexibility. This approach allows the use of a broader range of bonds and can be implemented at lower cost than perfect cash flow matching. The tradeoff is that the fund remains exposed to non-parallel yield curve shifts.

  • Key rate duration matching: Match sensitivity to specific points on the yield curve, providing protection against non-parallel curve movements. Instead of a single duration number, key rate durations measure sensitivity to changes at specific maturities (such as the 2-year, 5-year, 10-year, and 30-year points). Matching key rate durations across assets and liabilities provides more robust protection but requires more sophisticated analysis and implementation.

  • Liability-driven benchmarking: Evaluate portfolio performance relative to liability movements rather than traditional bond indices. Under this approach, a successful period is one in which assets grow faster than liabilities, regardless of whether absolute returns were positive or negative.

In[28]:
Code
def ldi_scenario_analysis(
    asset_value,
    liability_value,
    asset_duration,
    liability_duration,
    yield_scenarios,
):
    """Analyze funding ratio under different yield scenarios."""
    results = []

    for dy in yield_scenarios:
        new_assets = asset_value * (1 - asset_duration * dy)
        new_liabilities = liability_value * (1 - liability_duration * dy)
        new_surplus = new_assets - new_liabilities
        new_funding_ratio = new_assets / new_liabilities

        results.append(
            {
                "yield_change": dy,
                "assets": new_assets,
                "liabilities": new_liabilities,
                "surplus": new_surplus,
                "funding_ratio": new_funding_ratio,
            }
        )

    return results
Out[29]:
Visualization
Line chart comparing funding ratios under rate changes for mismatched versus matched duration portfolios.
Funding ratio sensitivity analysis for a pension fund with $100M in assets and $95M in liabilities, showing funding ratios as a percentage under yield changes from -3% to +3%. The current portfolio (blue solid line, with a duration gap of -4.9 years) shows the funding ratio improving with rising rates, reaching over 110% at +3% yield change, and deteriorating with falling rates, dropping below 100% at -3% yield change. A duration-matched portfolio (green dashed line) maintains nearly constant funding around 105% regardless of rate movements, demonstrating how LDI strategies protect against interest rate risk by matching asset and liability sensitivities.

The chart demonstrates why duration matching matters for liability management. The current portfolio with its negative duration gap sees large swings in funding ratio as rates change. The blue line slopes upward, showing that rising rates improve funding (as liabilities fall faster than assets) while falling rates damage funding. A duration-matched portfolio maintains a nearly constant funding ratio, represented by the nearly horizontal green line, protecting the fund's solvency position regardless of rate movements. The stability provided by duration matching allows plan sponsors to focus on other aspects of portfolio management without worrying that interest rate movements will derail their funding objectives.

Limitations and Practical Considerations

Duration and convexity are powerful tools, but they rest on assumptions that may not hold in practice. Understanding these limitations helps practitioners apply these measures appropriately and recognize when more sophisticated analysis is required.

Parallel shift assumption: Duration assumes the entire yield curve shifts by the same amount. In reality, short rates and long rates often move differently. The curve can steepen, flatten, or twist in ways that duration cannot capture. For example, the Federal Reserve might raise short-term rates while long-term rates remain stable, steepening the curve. Duration analysis would not accurately predict portfolio performance in such a scenario. Key rate durations address this limitation by measuring sensitivity to changes at specific maturities, such as the 2-year, 5-year, 10-year, and 30-year points. However, they add complexity to the analysis.

Instantaneous shift assumption: Duration measures sensitivity to immediate yield changes. It does not account for the passage of time, which naturally changes a bond's duration (often called duration drift). As time passes, a bond's remaining maturity shortens and its duration decreases. A portfolio that is perfectly immunized today will drift out of alignment as time passes and requires periodic rebalancing to maintain the desired risk profile.

Yield level dependency: Both duration and convexity change as yields change. A portfolio immunized at 5% yields will have different risk characteristics at 3% yields. Large rate movements can materially alter a portfolio's risk profile. The duration and convexity calculated before a rate move may not accurately describe the portfolio's behavior after the move.

Credit and liquidity risks: Duration addresses interest rate risk only. Corporate bonds face additional risks from credit spread changes, defaults, and liquidity conditions that duration does not capture. Spread duration measures sensitivity to credit spread changes and should be considered alongside interest rate duration for corporate bond portfolios. Default and liquidity risks require separate analysis entirely.

Embedded options: Callable bonds and mortgage-backed securities contain embedded options that create negative convexity in certain yield ranges. When rates fall, callable bonds may be redeemed by the issuer, limiting the investor's upside. Standard duration measures can be misleading for these instruments because the cash flow pattern depends on the path of interest rates. Effective duration, which accounts for embedded options by modeling how cash flows change with rates, is required for these securities.

Reinvestment assumptions: Immunization assumes coupons can be reinvested at prevailing rates. In practice, transaction costs, minimum investment sizes, and market liquidity affect actual reinvestment rates. An investor receiving a small coupon payment may not be able to reinvest at the quoted market rate due to bid-ask spreads or minimum trade sizes.

Despite these limitations, duration and convexity remain foundational risk measures. They provide a common language for discussing interest rate exposure, enable rapid approximate calculations for portfolio analysis, and form the building blocks for more sophisticated risk management systems. Practitioners who understand both the power and the limitations of these tools can apply them effectively while recognizing situations that require additional analysis.

Summary

This chapter developed the core quantitative tools for measuring and managing bond interest rate risk:

Duration measures a bond's sensitivity to interest rate changes. Macaulay duration represents the weighted average time to receive cash flows. Modified duration directly quantifies the percentage price change for a given yield change. For example, a bond with a modified duration of 7 will lose approximately 7% of its value for each 1% increase in yields.

Convexity captures the curvature of the price-yield relationship that duration misses. The duration-convexity approximation provides accurate price estimates even for substantial rate movements. The convexity term always adds to the price estimate regardless of rate direction.

Portfolio measures aggregate individual bond durations and convexities using market-value weights. DV01 and dollar duration express risk in dollar terms useful for hedging.

Immunization protects against interest rate risk by matching portfolio duration to a target horizon. At the duration point, price effects and reinvestment effects from rate changes offset each other. This allows investors to meet future obligations regardless of rate movements.

Liability-driven investing extends immunization concepts to manage complex streams of future obligations. The duration gap measures the mismatch between asset and liability sensitivities, guiding portfolio construction for pension funds and insurers.

These measures assume parallel yield curve shifts and have other limitations, but they remain essential tools that every fixed income practitioner must master. More advanced techniques build upon this foundation by adding sensitivity to curve shape changes, credit spreads, and embedded options.

Key Parameters

The key parameters for bond risk measures and immunization are:

  • Face Value: The par value of the bond, typically $1,000. This is the principal amount repaid at maturity.
  • Coupon Rate: Annual coupon payment as a percentage of face value. Higher coupon rates result in shorter duration.
  • Years to Maturity: Time until the bond's principal is repaid. Longer maturities increase both duration and convexity.
  • Yield to Maturity (y): The discount rate that equates the bond's price to the present value of its cash flows. Higher yields reduce duration.
  • Frequency: Number of coupon payments per year (typically 2 for semi-annual). Affects the periodic yield and duration calculations.
  • Modified Duration: Measures percentage price sensitivity to yield changes. Used for hedging and risk management.
  • Convexity: Measures the curvature of the price-yield relationship. Positive convexity benefits bondholders as it creates asymmetric price responses.
  • Target Duration: In immunization, the investment horizon that the portfolio duration should match to neutralize interest rate risk.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about bond risk measures and immunization.

Loading component...

Reference

BIBTEXAcademic
@misc{bondriskmeasuresdurationconvexityandimmunization, author = {Michael Brenndoerfer}, title = {Bond Risk Measures: Duration, Convexity, and Immunization}, year = {2025}, url = {https://mbrenndoerfer.com/writing/bond-duration-convexity-immunization-interest-rate-risk}, organization = {mbrenndoerfer.com}, note = {Accessed: 2025-12-29} }
APAAcademic
Michael Brenndoerfer (2025). Bond Risk Measures: Duration, Convexity, and Immunization. Retrieved from https://mbrenndoerfer.com/writing/bond-duration-convexity-immunization-interest-rate-risk
MLAAcademic
Michael Brenndoerfer. "Bond Risk Measures: Duration, Convexity, and Immunization." 2025. Web. 12/29/2025. <https://mbrenndoerfer.com/writing/bond-duration-convexity-immunization-interest-rate-risk>.
CHICAGOAcademic
Michael Brenndoerfer. "Bond Risk Measures: Duration, Convexity, and Immunization." Accessed 12/29/2025. https://mbrenndoerfer.com/writing/bond-duration-convexity-immunization-interest-rate-risk.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Bond Risk Measures: Duration, Convexity, and Immunization'. Available at: https://mbrenndoerfer.com/writing/bond-duration-convexity-immunization-interest-rate-risk (Accessed: 12/29/2025).
SimpleBasic
Michael Brenndoerfer (2025). Bond Risk Measures: Duration, Convexity, and Immunization. https://mbrenndoerfer.com/writing/bond-duration-convexity-immunization-interest-rate-risk