Bond Risk Measures: Duration, Convexity, and Immunization

Michael BrenndoerferNovember 8, 202551 min read

Measure and manage bond interest rate risk using duration, convexity, and immunization. Topics include portfolio hedging and liability-driven investing.

Quantitative FinanceAll chapters
Part 2 of 15Equity and Fixed Income Markets
Chapter 13Bond Risk Measures and Immunization
Reading the full chapter · 13 sections · 51 min13 sections · 51 min

All 13 sections · 51 min read

Bond Risk Measures and ImmunizationFull chapter · Sections in this chapter1 / 13 ↓
Reading tools Definitions, links & notes

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.

Article links

Make inline references clickable

Notes & comments

Bond Risk Measures and Immunization

A bond portfolio can gain value when rates fall and still leave a pension fund less able to meet its obligations. The liabilities may rise by more than the assets. Managing that mismatch requires a clear choice of what to hedge: a bond's price, the dollar surplus over liabilities, or the funding ratio. These are different quantities, even when all use duration.

This chapter develops duration and convexity as first- and second-order price sensitivities, then uses them to construct and test a portfolio against a future liability. We begin with fixed, nonnegative cash flows and a common yield convention. The approximations describe nearby yield changes; they are not exact repricing rules for arbitrary rate moves.

Immunization matches asset present value and cash-flow timing to a liability under stated assumptions. In the single-liability example, matching Macaulay duration to the payment horizon removes first-order exposure to an immediate persistent change in one flat yield. It does not guarantee payment under every future rate path or remove default and liquidity risk.

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.

For the fixed-cash-flow bonds considered here, price is the present value of promised payments. The first formula uses one yield per payment period, with positive discount base and nonnegative cash flows:

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

For these fixed positive payments, a higher yield lowers price. A lower-coupon bond generally trades below an otherwise comparable higher-coupon bond when both are priced at the same yield. The coupon itself is contractual: an issuer does not automatically change an existing bond's payments when market yields move.

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: A fixed payment due later has greater percentage yield sensitivity at the same yield convention. Longer coupon-bond maturity also increases duration in the examples below, but maturity alone is not a universal ordering rule across all coupon and yield combinations.
  • Coupon rate: Holding maturity, yield and payment frequency fixed, lower coupons increase duration when there is a coupon date strictly before maturity. A zero-coupon bond has all its present-value weight at maturity, giving the greatest duration for those terms. Higher coupons shift present-value weight to earlier receipts when those dates exist. If all remaining payments arrive at maturity, changing the coupon does not change duration or create an earlier receipt.
  • 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.
PythonIn [3]
CollapseExpand
def _bond_terms(face_value, coupon_rate, years_to_maturity, yield_rate, frequency):
    """Validate the fixed, nonnegative-cash-flow, regular-payment model."""
    from math import isclose, isfinite
    from numbers import Real

    values = (face_value, coupon_rate, years_to_maturity, yield_rate, frequency)
    if any(
        isinstance(x, bool) or not isinstance(x, Real) or not isfinite(x)
        for x in values
    ):
        raise ValueError("Bond inputs must be finite real numbers")
    if face_value <= 0 or coupon_rate < 0 or years_to_maturity <= 0:
        raise ValueError("Require positive face value and maturity, nonnegative coupon")
    if frequency <= 0 or frequency != int(frequency):
        raise ValueError("frequency must be a positive integer")
    frequency = int(frequency)
    raw_periods = years_to_maturity * frequency
    periods = round(raw_periods)
    if periods < 1 or not isclose(periods, raw_periods, rel_tol=0, abs_tol=1e-10):
        raise ValueError("Maturity must lie on the regular payment grid")
    periodic_yield = yield_rate / frequency
    if 1 + periodic_yield <= 0:
        raise ValueError("The periodic discount base must be positive")
    return periods, face_value * coupon_rate / frequency, periodic_yield


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
    """
    from math import expm1, log1p

    periods, coupon_payment, periodic_yield = _bond_terms(
        face_value, coupon_rate, years_to_maturity, yield_rate, frequency
    )

    # Present value of coupon payments
    if periodic_yield == 0:
        pv_coupons = coupon_payment * periods
    else:
        pv_coupons = (
            coupon_payment * -expm1(-periods * log1p(periodic_yield)) / 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:

PythonIn [4]
CollapseExpand
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]
VisualizationOut [5]
CollapseExpand
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.

For the fixed nonnegative cash flows used here, the price-yield curve bends upward. From the same starting yield, a nonzero yield decrease gives a larger price gain than the loss caused by an equal increase, provided the discount base remains positive. Embedded options can change this convexity pattern.

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=1nt⋅Ct(1+y)tP=1P∑t=1nt⋅PV(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 payment periods in this formula
  • tt: the integer payment-period index; divide by the annual payment frequency to express duration in years
  • 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 t⋅PV(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, so the result is a proper weighted average.

Macaulay duration summarizes the timing of present-value weights; it is not a payback date or a promise that accumulated coupons have recovered the purchase price. A zero-coupon bond has one receipt time, so its duration equals maturity. With positive earlier coupons, some present-value weight arrives sooner and the weighted mean is shorter than maturity.

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. With a positive coupon payment strictly before maturity, Macaulay duration is less than maturity because that earlier payment receives positive present-value weight. If all remaining cash flow arrives at maturity, duration equals maturity.

PythonIn [6]
CollapseExpand
def macaulay_duration(
    face_value, coupon_rate, years_to_maturity, yield_rate, frequency=2
):
    """Calculate Macaulay duration of a bond."""
    periods, coupon_payment, periodic_yield = _bond_terms(
        face_value, coupon_rate, years_to_maturity, 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:

PythonIn [7]
CollapseExpand
# 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")
ConsoleOut [7]
CollapseExpand
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.

VisualizationOut [8]
CollapseExpand
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.
VisualizationOut [9]
CollapseExpand
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 shows duration below maturity because this bond has positive coupon payments strictly before maturity, pulling its present-value-weighted receipt 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 about when cash flows arrive and 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=ddy∑t=1nCt(1+y)−t(rewrite price formula)=∑t=1nCt⋅(−t)(1+y)−t−1(power rule: ddy(1+y)−t=−t(1+y)−t−1)=−∑t=1nt⋅Ct(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 is the dollar price sensitivity per unit change in decimal yield; multiply it by a small yield change to approximate the dollar price change. To express this in percentage terms, we divide both sides by the price PP:

1PdPdy=−1P∑t=1nt⋅Ct(1+y)t+1=−1P(1+y)∑t=1nt⋅Ct(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 1P∑t=1nt⋅Ct(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.

The derivation above differentiates with respect to a per-period yield, so its duration is in periods. For nominal annual yield yy compounded kk times per year, differentiating (1+y/k)−t(1+y/k)^{-t} introduces t/kt/k. With DMacD_{Mac} now in years, this gives modified duration with respect to the quoted annual decimal yield:

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:

ΔPP≈−DMod⋅Δy\frac{\Delta P}{P} \approx -D_{Mod} \cdot \Delta y

or equivalently:

ΔP≈−DMod⋅P⋅Δ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.

PythonIn [10]
CollapseExpand
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)
PythonIn [11]
CollapseExpand
# 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}")
ConsoleOut [11]
CollapseExpand
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), 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:

PythonIn [12]
CollapseExpand
# 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}%"
    )
ConsoleOut [12]
CollapseExpand
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 overstates the bond's loss. For a 2% yield decrease, it understates the bond's price gain. 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 is the second derivative of price with respect to yield, divided by price. It provides a second-order correction to the duration estimate; it is not simply the derivative of modified duration.

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 first derivative describes the slope of the price-yield curve; the second derivative describes how that slope changes. Modified duration is the negative first derivative divided by price, while convexity is the second derivative divided by price. Consequently, dDMod/dy=DMod2−CdD_{Mod}/dy=D_{Mod}^2-C, rather than simply CC.

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

dPdy=−∑t=1nt⋅Ct(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=1nt⋅Ct⋅ddy(1+y)−(t+1)(apply chain rule)=−∑t=1nt⋅Ct⋅(−(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}

With nonnegative cash flows and a positive discount base, the second-derivative terms are nonnegative; at least the principal term is positive. Thus the fixed-cash-flow price-yield curve is convex: its slope becomes less negative as yield increases.

Dividing by price to get the convexity measure,

C=1Pd2Pdy2=1P∑t=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=1P∑t=1nt(t+1)⋅Ct(1+y/2)t+2⋅14C = \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.

PythonIn [13]
CollapseExpand
def convexity(face_value, coupon_rate, years_to_maturity, yield_rate, frequency=2):
    """Calculate convexity of a bond."""
    periods, coupon_payment, periodic_yield = _bond_terms(
        face_value, coupon_rate, years_to_maturity, 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)
PythonIn [14]
CollapseExpand
conv = convexity(face_value, coupon_rate, maturity, yield_rate)
print("Bond: 10-year, 5% coupon at 5% yield")
print(f"Convexity: {conv:.4f}")
ConsoleOut [14]
CollapseExpand
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,

ΔPP≈−DMod⋅Δy+12⋅C⋅(Δ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)

For positive convexity, the second-order term is nonnegative because (Δy)2≥0(\Delta y)^2\geq0. It is positive for a nonzero yield change and zero at the expansion point. Fixed nonnegative cash flows with positive discount bases have positive convexity under the yield convention used here; embedded options can change that conclusion.

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 approximates this difference to second order; higher-order terms remain.

PythonIn [15]
CollapseExpand
# 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}%"
    )
ConsoleOut [15]
CollapseExpand
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

VisualizationOut [16]
CollapseExpand
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 materially 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, showing 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, showing the value of including the second-order term.

VisualizationOut [17]
CollapseExpand
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. The zero-coupon curve grows approximately with the square of maturity. In these coupon-bond examples, convexity also rises with maturity but more slowly, because coupon payments distribute present-value weight before maturity; all four 30-year values exceed 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.

Money duration (also called dollar duration) is modified duration multiplied by the position's market value. It is the magnitude of the first-order dollar sensitivity to a unit change in decimal yield. Multiply it by 0.01 for a one-percentage-point move:

Dollar Duration=DMod⋅P\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=DMod⋅P⋅0.0001\text{DV01} = D_{Mod} \cdot P \cdot 0.0001

The factor 0.0001 is a one-basis-point yield move expressed in decimal units: 1 bp = 0.01 percentage points = 0.0001.

Use the same decimal-yield unit as the derivative: a one-basis-point increase is Δy=0.0001\Delta y=0.0001, so the first-order dollar loss is DModP×0.0001D_{Mod}P\times0.0001. Traders use DV01 to compare dollar sensitivities to small yield movements.

These measures are particularly useful for hedging, as they express risk in dollar terms that can be directly compared across positions. If one unit has a DV01 of $50 and another has a DV01 of $25 to the same yield factor, two short units of the second offset the first-order exposure of one long unit of the first. This does not also match their convexity, curve-shape or credit exposures.

PythonIn [18]
CollapseExpand
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 percentage point yield increase causes ~${dollar_duration * 0.01:.2f} price decrease"
)
print(f"  - A 1 bp yield increase causes ~${dv01:.4f} price decrease")
ConsoleOut [18]
CollapseExpand
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 percentage point yield increase causes ~$77.95 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=1nwi⋅DiD_{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=1nwi⋅CiC_{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 interest rate exposure by changing the mix of bonds. With positive portfolio value and a positive-value addition, holding the other positions fixed, a bond whose modified duration exceeds the current portfolio duration raises it; one below the current portfolio duration lowers it. These comparisons use the same yield-change coordinate.

PythonIn [19]
CollapseExpand
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
PythonIn [20]
CollapseExpand
# 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}")
ConsoleOut [20]
CollapseExpand
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's modified duration is approximately 6.15 years, the market-value-weighted average of the individual modified durations. The thirty-year bond has the longest modified duration, about 15.16 years, but represents only 13.69% of portfolio value. Portfolio convexity is about 74.98. For a small common yield change, these measures give the portfolio's first- and second-order price approximation; positive convexity does not prevent a loss when yields rise.

VisualizationsOut [21]
CollapseExpand
Bar chart showing individual bond durations for a 2-year, 5-year, 10-year, and 30-year bond.
Individual bond durations for a sample four-bond portfolio, showing durations from about 1.9 years (2-year bond) to 15.2 years (30-year bond), with the 5-year bond at approximately 4.5 years and the 10-year bond near 7.8 years.
Bar chart showing each bond's contribution to portfolio duration, with a horizontal line marking the total portfolio duration.
Duration contribution of each bond to the overall 6.1-year portfolio duration, calculated as individual duration multiplied by portfolio weight. The 30-year bond contributes the most at about 2.1 years, closely followed by the 10-year bond at 2.0 years, while the 5-year and 2-year bonds contribute about 1.6 and 0.5 years respectively.

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 a horizon equal to Macaulay duration, the price and reinvestment effects cancel to first order in a small immediate persistent parallel yield change, assuming fixed cash flows and reinvestment at the same shocked yield. Finite shocks leave higher-order exposure. Matching one duration does not protect against arbitrary subsequent rate paths.

VisualizationOut [22]
CollapseExpand
Conceptual diagram showing price effect and reinvestment effect offsetting at the duration point.
Stylized price and reinvestment effects across investment horizons from 0 to 15 years. The red rates-rise curve and blue rates-fall curve use arbitrary illustrative units, not measured or repriced percentage returns. Price effects dominate at shorter horizons and reinvestment effects at longer horizons. The curves are constructed to cross zero at the example bond's approximately 7.99-year Macaulay duration. This illustrates first-order cancellation under the stated common-yield assumptions, not exact finite-shock immunity.

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 portfolio Macaulay duration in years equal to the liability horizon TT. Equivalently, match modified duration to T/(1+y/k)T/(1+y/k) under the same flat nominal annual yield and frequency.
  2. Present value matching. Ensure the portfolio's current market value equals the present value of the liability.

In the common-flat-yield fixed-cash-flow model, these conditions remove first-order terminal-value exposure to an immediate persistent parallel shift. Positive asset convexity relative to the single-payment liability controls the local second-order surplus response. These conditions do not establish immunity to all finite shocks or future rate paths.

PythonIn [23]
CollapseExpand
def immunize_portfolio(liability_pv, target_duration, available_bonds, yield_rate):
    """Match a positive liability PV and its Macaulay horizon using two bonds."""
    from math import isclose, isfinite

    if not isfinite(liability_pv) or liability_pv <= 0:
        raise ValueError("liability_pv must be positive and finite")
    if not isfinite(target_duration) or target_duration <= 0:
        raise ValueError("target_duration must be positive and finite")
    if len(available_bonds) != 2:
        raise ValueError("This simple immunization requires exactly 2 bonds")
    durations = [
        macaulay_duration(b["face_value"], b["coupon_rate"], b["maturity"], yield_rate)
        for b in available_bonds
    ]
    prices = [
        bond_price(b["face_value"], b["coupon_rate"], b["maturity"], yield_rate)
        for b in available_bonds
    ]
    d1, d2 = durations
    if isclose(d1, d2, rel_tol=1e-12, abs_tol=1e-12):
        raise ValueError("Two distinct durations are required")
    w1 = (target_duration - d2) / (d1 - d2)
    w2 = 1 - w1
    if min(w1, w2) < 0:
        raise ValueError("The target horizon requires a short position")
    return [w1, w2], durations, prices
PythonIn [24]
CollapseExpand
# 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
frequency = 2
liability_pv = liability_amount / (1 + yield_rate / frequency) ** (
    frequency * 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 Macaulay 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 Macaulay Duration: {port_duration:.2f} years")
ConsoleOut [24]
CollapseExpand
Liability: $1,000,000 due in 7 years
Present Value of Liability: $707,727.20
Target Macaulay Duration: 7 years

Immunization Portfolio:
------------------------------------------------------------
5-year, 4%:
  Duration: 4.57 years
  Weight: 58.01%
  Investment: $410,560.62
  Number of bonds: 429.3

15-year, 6%:
  Duration: 10.36 years
  Weight: 41.99%
  Investment: $297,166.58
  Number of bonds: 269.0

Portfolio Macaulay Duration: 7.00 years

Testing Immunization Effectiveness

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

PythonIn [25]
CollapseExpand
def simulate_immunization(
    liability_amount,
    liability_years,
    bonds,
    initial_weights,
    initial_yield,
    yield_shock,
):
    """Horizon value of fixed semiannual cash flows after a permanent flat shock.

    Payments before the coupon-date horizon compound forward. Payments after
    the horizon are discounted back, giving the residual bond's sale value.
    Assume no default, frictionless reinvestment and fractional holdings.
    """
    from math import isclose, isfinite

    frequency = 2
    if not isfinite(liability_amount) or liability_amount <= 0:
        raise ValueError("liability_amount must be positive and finite")
    if not isfinite(liability_years) or liability_years <= 0:
        raise ValueError("liability_years must be positive and finite")
    horizon_periods = round(liability_years * frequency)
    if not isclose(
        horizon_periods, liability_years * frequency, rel_tol=0, abs_tol=1e-10
    ):
        raise ValueError("The horizon must lie on the semiannual payment grid")
    if len(bonds) != len(initial_weights) or not bonds:
        raise ValueError("Provide one weight per bond")
    if any(not isfinite(w) or w < 0 for w in initial_weights):
        raise ValueError("Weights must be finite and nonnegative")
    if not isclose(sum(initial_weights), 1.0, rel_tol=0, abs_tol=1e-12):
        raise ValueError("Weights must sum to one")
    if not isfinite(initial_yield) or not isfinite(yield_shock):
        raise ValueError("Yields must be finite")
    initial_base = 1 + initial_yield / frequency
    shocked_base = 1 + (initial_yield + yield_shock) / frequency
    if min(initial_base, shocked_base) <= 0:
        raise ValueError("Periodic discount bases must be positive")
    initial_pv = liability_amount / initial_base**horizon_periods
    terminal_value = 0.0
    for bond, weight in zip(bonds, initial_weights):
        old_price = bond_price(
            bond["face_value"],
            bond["coupon_rate"],
            bond["maturity"],
            initial_yield,
            frequency,
        )
        quantity = initial_pv * weight / old_price
        periods = round(bond["maturity"] * frequency)
        if not isclose(periods, bond["maturity"] * frequency, rel_tol=0, abs_tol=1e-10):
            raise ValueError("Bond maturity must lie on the payment grid")
        coupon = bond["face_value"] * bond["coupon_rate"] / frequency
        for period in range(1, periods + 1):
            cash_flow = coupon + (bond["face_value"] if period == periods else 0)
            terminal_value += (
                quantity * cash_flow * shocked_base ** (horizon_periods - period)
            )
    return terminal_value
PythonIn [26]
CollapseExpand
# 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}%"
    )
ConsoleOut [26]
CollapseExpand
Immunization Test: Terminal Values Under Different Yield Scenarios
=================================================================
 Yield Shock    New Yield   Terminal Value    vs Target
-----------------------------------------------------------------
        -2.0%          3.0% $      1,003,967       +0.40%
        -1.0%          4.0% $      1,000,973       +0.10%
        +0.0%          5.0% $      1,000,000       +0.00%
        +1.0%          6.0% $      1,000,939       +0.09%
        +2.0%          7.0% $      1,003,693       +0.37%

With consistent semiannual cash flows, the zero-shock terminal value equals the $1 million liability. Macaulay matching removes the first-order response at that point; the remaining finite-shock differences are higher-order exposure. This test assumes an immediate permanent parallel yield change, frictionless reinvestment, no default and fractional bond holdings. Rebalancing is needed to maintain a match as time and rates change.

VisualizationOut [27]
CollapseExpand
Line chart showing terminal values close to target across different yield scenarios.
Horizon value of a portfolio constructed for a $1 million liability in seven years, using five- and fifteen-year bonds with matched present value and Macaulay duration. The scenarios apply an immediate persistent parallel shock from -300 to +300 basis points to the same nominal annual yield compounded semiannually used for pricing and reinvestment. Zero shock gives $1 million; the endpoint values are about $1.0091 million and $1.0082 million. The shaded $980,000 to $1,020,000 band is an illustrative plus/minus 2% comparison, not an endorsed risk tolerance. Residual finite-shock variation remains after first-order immunization.

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, the present value of fixed liability payments rises. Funding can weaken if liabilities rise faster than assets in percentage terms; whether a deficit develops also depends on the initial funding position and the size of the rate move. 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 inverse funding ratio:

Duration Gap=DA−PVLPVA⋅DL\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 measures first-order exposure of dollar surplus S=A−LS=A-L. Under a common small decimal yield change, ΔS≈(−DAA+DLL)Δy=−A Duration Gap Δy\Delta S\approx(-D_AA+D_LL)\Delta y=-A\,\text{Duration Gap}\,\Delta y. A positive gap means rising yields reduce dollar surplus; a negative gap means rising yields increase it. This concerns dollar surplus, not the percentage changes in asset and liability values.

A zero gap equates asset and liability money durations, so dollar surplus has zero first-order yield sensitivity. The funding ratio F=A/LF=A/L has a different derivative: dF/dy=F(DL−DA)dF/dy=F(D_L-D_A). Its first-order sensitivity vanishes when the two modified durations are equal. For an overfunded fund, that is generally not the same condition as a zero surplus duration gap.

PythonIn [28]
CollapseExpand
# 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%}"
)
ConsoleOut [28]
CollapseExpand
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, and protects 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 gives more stable protection but requires more sophisticated analysis and implementation.

  • Liability-driven benchmarking: Compare the portfolio's cash-flow-adjusted percentage return with the percentage change in a consistently valued liability benchmark. Asset returns above that benchmark indicate relative outperformance, even when both percentage changes are negative. Contributions and benefit payments must be handled consistently, and liability remeasurement must be distinguished from investment performance. This comparison is one performance measure, not a complete judgment of a plan's success.

PythonIn [29]
CollapseExpand
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
VisualizationOut [30]
CollapseExpand
Line chart comparing funding ratios under rate changes for mismatched versus matched duration portfolios.
Funding ratios for a model fund with $100 million assets and $95 million liabilities under yield changes from -3 to +3 percentage points. The current portfolio uses asset and liability modified durations of 6.5 and 12 years. The surplus-matched portfolio sets asset duration to 11.4, making the surplus duration gap zero: dollar surplus stays at $5 million in this first-order value model, but the funding ratio changes from about 103.87% to 108.22%. The equal-duration portfolio instead holds the funding ratio at about 105.26% within the model. These are ratios of linear price approximations, not exact finite-shock valuations or protection against every source of risk.

The current portfolio's funding ratio improves when rates rise because its liability percentage sensitivity exceeds its asset sensitivity. The surplus-matched portfolio holds dollar surplus constant in this linear scenario model, but its funding ratio still changes. The added equal-duration series instead holds the funding ratio constant within the model. Neither construction removes convexity, credit, liquidity or general curve-shape risk, and the finite-shock curves use approximate rather than exactly repriced values.

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.

Common-factor scenario limitation: An individual bond's own-YTM modified duration is a local price sensitivity; it does not assume that all benchmark-curve yields move together. The portfolio and immunization scenarios in this chapter use a common yield change, so a single matched scalar exposure does not describe arbitrary curve-shape changes. In reality, short rates and long rates often move differently. The curve can steepen, flatten, or twist in ways that a single scalar exposure cannot capture. For example, if short-term yields rise while long-term yields stay fixed on an initially upward-sloping curve, the long-minus-short spread narrows: the curve flattens over those maturities. A single common-factor duration approximation would not fully describe 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 is a local price derivative at a fixed valuation time. Calendar passage changes remaining cash-flow dates, and coupon payments can cause jumps in the measured duration. A previous asset-liability match can therefore drift and require rebalancing; duration need not decrease smoothly or monotonically along every coupon-bond path.

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 uses scenario-based revaluation to account for how embedded-option cash flows change with rates, is an appropriate sensitivity measure for these securities, alongside full scenario repricing.

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 adds the second-order price term omitted by duration. It improves the approximation in the worked examples, but finite-shock errors remain. For positive convexity the term is positive for a nonzero yield change and zero at the expansion point.

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 against a single payment matches present value and Macaulay duration to the payment horizon under a consistent yield convention. It removes first-order exposure to the stated immediate persistent parallel shock in the fixed-cash-flow model, not every possible rate path or source of risk.

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.

Own-YTM duration and convexity are local price sensitivities, not assumptions that a benchmark yield curve moves in parallel. The common-yield portfolio and immunization scenarios here do not cover arbitrary curve-shape changes. These measures 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 contractual principal due at maturity. The examples use $1,000 per bond; denominations depend on the instrument.
  • Coupon Rate: Annual coupon payment as a percentage of face value. At fixed maturity, yield and payment frequency, higher coupons shorten Macaulay duration when there is a coupon date strictly before maturity. If all remaining payments arrive at maturity, duration is unchanged.
  • Years to Maturity: Time until principal is due. Moving cash flows later changes duration and convexity; compare the complete cash-flow schedules and yield conventions rather than relying on maturity alone.
  • Yield to Maturity (y): The discount rate that equates the bond's price to the present value of its cash flows. For fixed nonnegative cash flows with at least one positive future payment and a positive discount base, higher yields reduce modified duration. Macaulay duration does not increase; it falls strictly when positive cash flows occur at more than one date.
  • Frequency: Number of coupon payments per year. These examples use two, so both pricing and reinvestment use the corresponding semiannual periodic yield.
  • 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: For the single-liability flat-yield example, the horizon matched by portfolio Macaulay duration to remove first-order terminal-value yield exposure.

Quiz

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

Bond Risk Measures and Immunization

Question 1 of 80 of 8 completed
A 10-year bond has a Macaulay duration of 7.5 years. What does this value represent?
Join the discussionAsk a question or share what you learned.

Comments

Add to the conversation

A question, a correction, or an example from your own work.

No comments yet. Be the first to share your thoughts!

A different angle
All writing
Quantitative Finance · Vol. 1

Term Structure of Interest Rates: Yield Curve Construction

Covers yield curve construction through zero rates, forward rates, and bootstrapping. Interpret curve shapes and build production-quality curves.

47 min read
Quantitative Finance · Vol. 1

Bond Pricing Fundamentals: Yield to Maturity & Present Value

Covers bond pricing through present value calculations, yield to maturity analysis, and price-yield relationships. Topics include fixed income fundamentals.

42 min read
Quantitative Finance · Vol. 1

Equity Markets and Stock Instruments: Trading & Valuation

Covers equity market fundamentals including stock ownership, order book mechanics, trading execution, and key valuation metrics for quantitative finance.

55 min read

About the author

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2025bondrisk, 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: 2026-11-01} }
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." 2026. Web. November 1, 2026. <https://mbrenndoerfer.com/writing/bond-duration-convexity-immunization-interest-rate-risk>.
CHICAGOAcademic
Michael Brenndoerfer. "Bond Risk Measures: Duration, Convexity, and Immunization." Accessed November 1, 2026. 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: November 1, 2026).
SimpleBasic
Michael Brenndoerfer (2025). Bond Risk Measures: Duration, Convexity, and Immunization. https://mbrenndoerfer.com/writing/bond-duration-convexity-immunization-interest-rate-risk
Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy