Interest Rate Swap Valuation: Bond Portfolio & FRA Methods

Michael BrenndoerferUpdated January 5, 202653 min read

Master interest rate swap valuation through bond portfolio and FRA methods. Learn curve bootstrapping, DV01 risk measures, and hedging applications.

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.

Interest Rate Swap Valuation and Applications

In the previous chapter, we introduced the mechanics of interest rate swaps: the exchange of fixed-rate payments for floating-rate payments between two counterparties. You learned how these instruments work, what drives their use, and the basic terminology that market participants employ. Now we turn to the critical question: how do we determine the value of a swap?

Swap valuation lies at the heart of trading, risk management, and financial engineering. Whether you are quoting prices to clients, hedging interest rate exposure, or constructing synthetic assets, you need to understand how to price these instruments from first principles. This chapter develops two equivalent approaches to swap valuation, shows how to extract the swap curve from market data, and explores the practical applications that make swaps indispensable to modern finance.

The key insight is that a swap can be decomposed into more fundamental instruments we've already studied. We can view it either as a portfolio of bonds or as a portfolio of forward rate agreements. Both perspectives lead to the same value, and understanding both deepens your intuition about how interest rate risk flows through these contracts.

Swap Valuation as a Bond Portfolio

The most intuitive approach to swap valuation treats each leg of the swap as a bond. This perspective leverages our existing knowledge of bond pricing and allows us to value swaps using familiar present value techniques. Consider a plain vanilla interest rate swap from your perspective if you are receiving fixed and paying floating. This position is economically equivalent to:

  • Being long a fixed-rate bond (receiving fixed coupon payments)
  • Being short a floating-rate bond (making floating-rate payments)

The value of the swap is simply the difference between these two bond values. This decomposition works because the cash flows of the swap exactly replicate the combined cash flows of holding a fixed-rate bond while having borrowed at a floating rate. Understanding this equivalence transforms swap valuation from an abstract exercise into a straightforward application of bond mathematics.

Fixed-Rate Bond Valuation

The fixed leg of a swap generates a stream of known cash flows, which makes it the more straightforward component to value. Unlike the floating leg, where future payments depend on interest rates that have not yet been determined, the fixed leg's payments are contractually specified at inception. This certainty allows us to apply standard present value techniques directly.

If the swap has a notional principal NN, a fixed rate KK, and payment dates at times t1,t2,,tnt_1, t_2, \ldots, t_n, we can calculate the present value of the fixed leg by discounting each cash flow back to today. The formula for this present value is:

Vfixed=i=1nKδiNZ(0,ti)+NZ(0,tn)V_{\text{fixed}} = \sum_{i=1}^{n} K \cdot \delta_i \cdot N \cdot Z(0, t_i) + N \cdot Z(0, t_n)

where:

  • VfixedV_{\text{fixed}}: present value of the fixed leg
  • KK: fixed interest rate
  • δi\delta_i: day count fraction for period ii
  • NN: notional principal amount
  • Z(0,ti)Z(0, t_i): discount factor from today to time tit_i
  • tit_i: payment date for period ii
  • nn: total number of payment periods
  • i=1nKδiNZ(0,ti)\sum_{i=1}^{n} K \cdot \delta_i \cdot N \cdot Z(0, t_i): present value of the coupon payments
  • NZ(0,tn)N \cdot Z(0, t_n): present value of the notional principal repayment

The final term represents the notional principal "repayment" at maturity. Even though no principal actually exchanges hands in a swap, we include it for the bond equivalence to hold. This might look like an accounting trick, but it's essential: by adding and subtracting the notional principal on both legs, we maintain the mathematical equivalence between the swap and the bond portfolio. The notional terms cancel when we compute the net swap value, but including them allows us to value each leg independently using standard bond pricing formulas.

Recall from our discussion of bond pricing in Part II, Chapter 2 that the discount factor Z(0,t)Z(0, t) represents the present value of receiving one dollar at time tt. These discount factors are derived from the term structure of interest rates and encode all the information we need about the time value of money across different horizons.

Floating-Rate Bond Valuation

The floating leg presents an interesting challenge: we don't know the future floating rates. At first glance, this uncertainty might seem to make valuation impossible, since we cannot discount cash flows that we cannot predict. However, a useful property simplifies the calculation and clarifies how floating-rate instruments behave.

Just after a floating-rate reset, a floating-rate bond trades at par because each future payment will be set to the prevailing market rate. This property is key to understanding floating-rate instruments.

To understand why, consider what happens at a reset date. The floating rate for the upcoming period is set to the current market rate, which by definition represents fair compensation for lending money over that period. At the next reset, the same thing happens: the rate adjusts to whatever the market rate is at that time. Since each coupon payment reflects the market rate at the time it was set, the bond always offers a fair market return. Consequently, investors are willing to pay exactly face value for it, and the bond's value equals its face value at each reset date.

This logic extends to any future reset date as well. Looking forward from today, we know that at each future reset, the bond will be worth par. This recursive property dramatically simplifies our valuation task.

Between reset dates, the floating leg's value equals the present value of the next payment plus par:

Vfloat=(LkδkN+N)Z(0,tk)V_{\text{float}} = (L_k \cdot \delta_k \cdot N + N) \cdot Z(0, t_k)

where:

  • VfloatV_{\text{float}}: value of the floating leg between resets
  • LkL_k: floating rate set for the current period
  • δk\delta_k: day count fraction for the current period
  • NN: notional principal
  • Z(0,tk)Z(0, t_k): discount factor to the next payment date tkt_k
  • tkt_k: next payment date
  • LkδkNL_k \cdot \delta_k \cdot N: the fixed coupon payment for the current period
  • LkδkN+NL_k \cdot \delta_k \cdot N + N: total cash flow at the next reset (coupon plus par value)

The intuition here is straightforward: between reset dates, we know exactly what the next coupon payment will be because the rate has already been fixed. We also know that immediately after that payment, the bond will be worth par because a new rate will be set. Therefore, the current value is simply the present value of the known coupon plus the present value of receiving par at the next reset.

At inception or immediately after a reset, this simplifies to:

Vfloat=NV_{\text{float}} = N

where:

  • VfloatV_{\text{float}}: value of the floating leg immediately after a reset
  • NN: notional principal amount

The floating-rate bond is worth exactly par. This result means that at reset dates, we do not need to forecast any future interest rates to value the floating leg. We simply know it equals the notional amount.

The Swap Value

Combining these results, the value of a receive-fixed, pay-floating swap is:

Vswap=VfixedVfloatV_{\text{swap}} = V_{\text{fixed}} - V_{\text{float}}

where:

  • VswapV_{\text{swap}}: total value of the swap
  • VfixedV_{\text{fixed}}: present value of the fixed leg
  • VfloatV_{\text{float}}: present value of the floating leg

This formula encapsulates the bond portfolio view of swaps: you are long a fixed-rate bond and short a floating-rate bond. When fixed rates embedded in the swap exceed current market rates, the fixed leg is worth more than par, making the swap valuable to you. Conversely, when market rates exceed the swap's fixed rate, the fixed leg is worth less than par, and the swap has negative value to you.

At inception, swaps are typically structured so that Vswap=0V_{\text{swap}} = 0. This means the fixed rate KK is chosen to make the present value of fixed payments equal to the present value of floating payments. Neither party pays or receives money upfront; instead, the fixed rate is set at a level that makes the exchange fair given current market conditions.

::: {.callout-note title="Par Swap Rate"} The par swap rate is the fixed rate that makes a swap have zero initial value. It represents the market's expectation of average short-term rates over the swap's life, adjusted for the time value of money. Let's implement this valuation approach.

In[2]:
Code
import numpy as np

# Define discount factors from a hypothetical yield curve
# These would come from the term structure in practice
maturities = np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0])  # Years
zero_rates = np.array(
    [0.035, 0.038, 0.040, 0.042, 0.043, 0.044]
)  # Continuous compounding

# Calculate discount factors
discount_factors = np.exp(-zero_rates * maturities)

# Swap parameters
notional = 10_000_000  # $10 million
fixed_rate = 0.05  # 5% annual fixed rate
payment_frequency = 0.5  # Semiannual payments
n_payments = 6  # 3-year swap with semiannual payments
In[3]:
Code
def value_fixed_leg(notional, fixed_rate, discount_factors, payment_freq):
    """
    Value the fixed leg of a swap as a fixed-rate bond.
    """
    # Fixed payments each period
    fixed_payment = notional * fixed_rate * payment_freq

    # PV of coupon payments
    pv_coupons = np.sum(fixed_payment * discount_factors)

    # PV of notional at maturity
    pv_notional = notional * discount_factors[-1]

    return pv_coupons + pv_notional


def value_floating_leg_at_reset(notional, next_discount_factor=None):
    """
    Value the floating leg at a reset date (equals par).
    Between resets, would need to include accrued payment.
    """
    # At reset, floating-rate bond is worth par
    return notional


# Calculate values
fixed_leg_value = value_fixed_leg(
    notional, fixed_rate, discount_factors, payment_frequency
)
floating_leg_value = value_floating_leg_at_reset(notional)
swap_value = fixed_leg_value - floating_leg_value
Out[4]:
Console
Swap Valuation Using Bond Portfolio Approach
==================================================
Notional Principal:    $10,000,000
Fixed Rate:            5.00%
Swap Tenor:            3.0 years

Fixed Leg Value:       $10,158,654.57
Floating Leg Value:    $10,000,000.00
Swap Value (Rec Fix):  $158,654.57
Out[5]:
Visualization
Nominal and present value of fixed leg cash flows over a 3-year swap tenor. Discounting progressively reduces the present value of later payments, while the final payment dominates the total value due to the inclusion of the notional principal.
Nominal and present value of fixed leg cash flows over a 3-year swap tenor. Discounting progressively reduces the present value of later payments, while the final payment dominates the total value due to the inclusion of the notional principal.

The positive swap value indicates that the fixed rate of 5% is favorable compared to current market rates. The receive-fixed party holds a valuable position because they are locked into receiving a higher rate than what the market currently offers for new swaps.

Key Parameters

The key parameters for swap valuation using the bond portfolio approach are fundamental inputs that determine the swap's cash flows and their present values:

  • N: Notional principal amount. Used to scale the cash flows, though it is not exchanged in a standard swap. This amount determines the size of each interest payment and serves as the reference for calculating percentage-based coupon payments.
  • K: Fixed interest rate. The rate paid by you, contractually determined at inception and remaining constant throughout the swap's life.
  • r: Risk-free interest rate. Derived from the discount factors used to value the cash flows. In practice, this comes from the term structure observed in the market.
  • δ: Payment frequency. Determines the accrual periods (e.g., 0.5 for semiannual). This parameter affects both the timing of cash flows and the calculation of each payment amount through the day count fraction.

Swap Valuation as a Portfolio of FRAs

An alternative and equally valid approach views a swap as a series of forward rate agreements. Rather than thinking of the swap as two bonds, we can decompose it into individual exchanges of fixed for floating payments at each payment date. Each payment exchange in the swap can be treated as a separate FRA. This perspective connects swap pricing directly to forward rates, which we discussed in the context of the term structure in Part II, Chapter 3.

The FRA approach offers different insights than the bond approach. While the bond view emphasizes the swap's relationship to fixed income securities, the FRA view highlights how each payment period contributes to the swap's total value. This decomposition is especially useful for analyzing where swap value comes from along the yield curve.

Forward Rate Agreement Review

Before applying this to swaps, let's review forward rate agreements and their valuation. A forward rate agreement is a contract to exchange a fixed interest payment for a floating payment at a future date, based on a rate determined at an earlier fixing date. The contract specifies a notional amount, a forward period, and a contracted rate against which the realized floating rate will be compared.

The payoff of a receive-fixed FRA settling at time T+δT+\delta for a period of length δ\delta starting at TT is:

FRA Payoff=δN(KLT)\text{FRA Payoff} = \delta \cdot N \cdot (K - L_T)

where:

  • δ\delta: accrual period length
  • NN: notional principal
  • KK: contracted forward rate
  • LTL_T: realized floating rate at time TT

The payoff is positive when the contracted rate exceeds the realized floating rate, meaning you receive the difference. Conversely, the payoff is negative when the floating rate exceeds the contracted rate.

To determine the present value before settlement, we need to handle the uncertainty about the future floating rate LTL_T. The key insight is that the forward rate implied by today's term structure represents the market's risk-neutral expectation of the future spot rate. We replace the unknown future floating rate LTL_T with the implied forward rate F(0,T,T+δ)F(0, T, T+\delta) and discount the expected payoff:

VFRA=δN(KF(0,T,T+δ))Z(0,T+δ)V_{\text{FRA}} = \delta \cdot N \cdot (K - F(0, T, T+\delta)) \cdot Z(0, T+\delta)

where:

  • VFRAV_{\text{FRA}}: present value of the FRA
  • F(0,T,T+δ)F(0, T, T+\delta): forward rate observed today for the period starting at TT
  • Z(0,T+δ)Z(0, T+\delta): discount factor to the payment date
  • TT: start date of the forward period
  • δ\delta: accrual period length
  • NN: notional principal
  • KK: contracted forward rate

This formula tells us that the FRA has positive value when the contracted rate KK exceeds the market forward rate FF, and negative value when the market forward rate exceeds the contracted rate.

Decomposing the Swap

With the FRA valuation framework established, we can now view the entire swap as a portfolio of these individual contracts. A swap with nn payment dates can be decomposed into nn forward rate agreements, each corresponding to one payment exchange. The value of the entire swap is the sum of these FRA values:

Vswap=i=1nδiN(KFi)Z(0,ti)V_{\text{swap}} = \sum_{i=1}^{n} \delta_i \cdot N \cdot (K - F_i) \cdot Z(0, t_i)

where:

  • VswapV_{\text{swap}}: total value of the swap
  • nn: total number of payment dates
  • δi\delta_i: day count fraction for period ii
  • NN: notional principal
  • KK: fixed rate
  • FiF_i: forward rate for period ii (F(0,ti1,ti)F(0, t_{i-1}, t_i))
  • Z(0,ti)Z(0, t_i): discount factor for payment at time tit_i

This formula reveals something important: at the inception of a swap, the fixed rate KK is set so that the swap has zero value (Vswap=0V_{\text{swap}} = 0). By setting the swap value to zero and solving for the fixed rate, we can derive the par swap rate. The derivation proceeds as follows:

0=i=1nδiN(KFi)Z(0,ti)(set swap value to zero)0=N(Ki=1nδiZ(0,ti)i=1nδiFiZ(0,ti))(factor out N and separate terms)Ki=1nδiZ(0,ti)=i=1nδiFiZ(0,ti)(divide by N and rearrange)\begin{aligned} 0 &= \sum_{i=1}^{n} \delta_i \cdot N \cdot (K - F_i) \cdot Z(0, t_i) && \text{(set swap value to zero)} \\ 0 &= N \left( K \sum_{i=1}^{n} \delta_i Z(0, t_i) - \sum_{i=1}^{n} \delta_i F_i Z(0, t_i) \right) && \text{(factor out } N \text{ and separate terms)} \\ K \sum_{i=1}^{n} \delta_i Z(0, t_i) &= \sum_{i=1}^{n} \delta_i F_i Z(0, t_i) && \text{(divide by } N \text{ and rearrange)} \end{aligned}

Solving for KK:

K=i=1nδiFiZ(0,ti)i=1nδiZ(0,ti)K = \frac{\sum_{i=1}^{n} \delta_i \cdot F_i \cdot Z(0, t_i)}{\sum_{i=1}^{n} \delta_i \cdot Z(0, t_i)}

where:

  • KK: par swap rate
  • nn: number of payment periods
  • FiF_i: forward rate for period ii
  • δi\delta_i: day count fraction
  • Z(0,ti)Z(0, t_i): discount factor
  • i=1nδiFiZ(0,ti)\sum_{i=1}^{n} \delta_i \cdot F_i \cdot Z(0, t_i): present value of the expected floating leg payments per unit of notional
  • i=1nδiZ(0,ti)\sum_{i=1}^{n} \delta_i \cdot Z(0, t_i): present value of a fixed annuity paying 1 unit (the PV01)

The par swap rate is a weighted average of forward rates, with weights determined by the discount factors. This tells us that the par swap rate reflects the market's consensus about average short-term rates over the swap's life, with each period's forward rate weighted by its present value contribution. Periods further in the future receive lower weights because their discount factors are smaller.

In[6]:
Code
def calculate_forward_rates(zero_rates, maturities):
    """
    Calculate forward rates from zero rates.
    Forward rate from t1 to t2 given continuous zero rates.
    """
    forward_rates = []

    for i in range(len(maturities)):
        if i == 0:
            # First period: forward rate equals spot rate
            f = zero_rates[i]
        else:
            # Forward rate: (r2*t2 - r1*t1) / (t2 - t1)
            t1, t2 = maturities[i - 1], maturities[i]
            r1, r2 = zero_rates[i - 1], zero_rates[i]
            f = (r2 * t2 - r1 * t1) / (t2 - t1)
        forward_rates.append(f)

    return np.array(forward_rates)


def value_swap_via_fra(
    notional, fixed_rate, forward_rates, discount_factors, payment_freq
):
    """
    Value a swap as a portfolio of FRAs.
    """
    total_value = 0

    for i, (fwd, disc) in enumerate(zip(forward_rates, discount_factors)):
        # Each FRA: delta * N * (K - F) * Z
        fra_value = payment_freq * notional * (fixed_rate - fwd) * disc
        total_value += fra_value

    return total_value


# Calculate forward rates
forward_rates = calculate_forward_rates(zero_rates, maturities)

# Value swap via FRA approach
swap_value_fra = value_swap_via_fra(
    notional, fixed_rate, forward_rates, discount_factors, payment_frequency
)

# Calculate individual FRA values for analysis
fra_values = []
for fwd, disc in zip(forward_rates, discount_factors):
    fra_values.append(payment_frequency * notional * (fixed_rate - fwd) * disc)
Out[7]:
Console
Forward Rates and FRA Decomposition
============================================================
Period      Forward Rate   Discount Factor      FRA Value
------------------------------------------------------------
0.0-0.5y         3.5000%         0.982652   $   73,698.92
0.5-1.0y         4.1000%         0.962713   $   43,322.08
1.0-1.5y         4.4000%         0.941765   $   28,252.94
1.5-2.0y         4.8000%         0.919431   $    9,194.31
2.0-2.5y         4.7000%         0.898077   $   13,471.15
2.5-3.0y         4.9000%         0.876341   $    4,381.70
------------------------------------------------------------
Total Swap Value:                             $  172,321.10

Verification - Bond approach value: $158,654.57
Out[8]:
Visualization
FRA value contribution by period for a receive-fixed swap. Positive bars indicate periods where the fixed rate exceeds the forward rate, while the declining magnitude reflects the impact of discounting on more distant cash flows.
FRA value contribution by period for a receive-fixed swap. Positive bars indicate periods where the fixed rate exceeds the forward rate, while the declining magnitude reflects the impact of discounting on more distant cash flows.
Comparison of the swap's fixed rate against the implied forward rate curve. The upward-sloping term structure shows forward rates rising from 3.5% to near 4.8%, converging toward the 5% fixed rate by the final period.
Comparison of the swap's fixed rate against the implied forward rate curve. The upward-sloping term structure shows forward rates rising from 3.5% to near 4.8%, converging toward the 5% fixed rate by the final period.

Both approaches yield the same swap value, confirming their equivalence. The FRA approach provides additional insight by breaking down the swap's value across individual payment periods. This decomposition reveals which parts of the yield curve contribute most to the swap's value and helps you understand the term structure exposure embedded in your positions.

Key Parameters

The key parameters for the FRA valuation approach connect swap pricing directly to the forward rate curve and the term structure of interest rates:

  • FiF_i: Forward rate for period ii. Derived from the term structure, representing the market's expectation of future rates. These rates are implied by the relationship between spot rates at different maturities.
  • KK: Fixed rate of the swap. Compared against the forward rate to determine the value of each period. The difference between KK and FiF_i drives each FRA's contribution to the swap value.
  • ΔiΔ_i: Accrual period length (day count fraction). Used to calculate the cash flow amount for each FRA and to ensure proper annualization of interest payments.
  • Z(0,t)Z(0, t): Discount factor. Used to bring the future FRA payoff back to present value. These factors encode the time value of money embedded in the current term structure.

The Swap Curve

The swap curve is one of the most important interest rate curves in financial markets. It shows the par swap rates for different maturities and serves as a benchmark for pricing a wide range of interest rate products. The swap curve is essential because it's the foundation for valuing many derivatives.

Why the Swap Curve Matters

Unlike government bond yields, which can be affected by supply-demand imbalances, tax considerations, and liquidity preferences, swap rates reflect pure interest rate expectations plus credit risk adjustments. Government bond markets, while deep and liquid, are subject to various technical factors that can distort yields. For example, certain maturities may trade at premium or discount prices due to pension fund demand or central bank purchases. The swap curve is particularly important because:

  • It provides a continuous term structure out to 30 years or more, offering a complete picture of interest rate expectations across the entire maturity spectrum.
  • It serves as the discounting curve for many derivative products, making it the reference point for pricing options, structured products, and other interest rate instruments.
  • It represents the borrowing cost for AA-rated financial institutions, providing a credit-adjusted benchmark that reflects the cost of funds in the interbank market.
  • It's highly liquid, with active trading across all tenors, ensuring that the rates reflect current market conditions rather than stale prices.

Bootstrapping the Swap Curve

Bootstrapping is the process of extracting discount factors from market swap rates. The term "bootstrapping" refers to the iterative nature of the procedure: we use information derived from shorter maturities to solve for discount factors at longer maturities. The technique works iteratively, starting from the shortest maturity and working outward.

For a par swap with maturity TT and swap rate STS_T, the no-arbitrage condition requires that the value of the fixed leg equals the value of the floating leg. Since the floating leg is valued at par (11 per unit notional) at inception, the present value of the fixed leg cash flows plus the notional repayment must also equal 11:

1=i=1nSTδiZ(0,ti)+Z(0,T)1 = \sum_{i=1}^{n} S_T \cdot \delta_i \cdot Z(0, t_i) + Z(0, T)

where:

  • STS_T: par swap rate for maturity TT
  • nn: number of payment periods up to maturity TT
  • δi\delta_i: day count fraction for period ii
  • Z(0,ti)Z(0, t_i): discount factor for time tit_i (already known)
  • Z(0,T)Z(0, T): discount factor for the final maturity TT (the unknown variable)

The key observation is that at each step of the bootstrap, we know all discount factors except the one at the current maturity. All the discount factors for earlier payment dates have been determined in previous iterations of the procedure.

We can rearrange the no-arbitrage condition to solve explicitly for the unknown discount factor Z(0,T)Z(0, T) by separating the final term and isolating Z(0,T)Z(0, T):

1=i=1n1STδiZ(0,ti)+STδnZ(0,T)+Z(0,T)(separate final terms)1i=1n1STδiZ(0,ti)=Z(0,T)(1+STδn)(group Z(0,T) terms)Z(0,T)=1i=1n1STδiZ(0,ti)1+STδn(solve for Z(0,T))\begin{aligned} 1 &= \sum_{i=1}^{n-1} S_T \cdot \delta_i \cdot Z(0, t_i) + S_T \cdot \delta_n \cdot Z(0, T) + Z(0, T) && \text{(separate final terms)} \\ 1 - \sum_{i=1}^{n-1} S_T \cdot \delta_i \cdot Z(0, t_i) &= Z(0, T) (1 + S_T \cdot \delta_n) && \text{(group } Z(0, T) \text{ terms)} \\ Z(0, T) &= \frac{1 - \sum_{i=1}^{n-1} S_T \cdot \delta_i \cdot Z(0, t_i)}{1 + S_T \cdot \delta_n} && \text{(solve for } Z(0, T) \text{)} \end{aligned}

where:

  • Z(0,T)Z(0, T): discount factor for maturity TT
  • STS_T: par swap rate for maturity TT
  • nn: number of payment periods
  • δi\delta_i: day count fraction for period ii
  • Z(0,ti)Z(0, t_i): known discount factor for time tit_i
  • δn\delta_n: day count fraction for the final period
  • 1i=1n1STδiZ(0,ti)1 - \sum_{i=1}^{n-1} S_T \cdot \delta_i \cdot Z(0, t_i): remaining value to be covered by the final payment
  • 1+STδn1 + S_T \cdot \delta_n: total final cash flow per unit of discount factor

At each step of bootstrapping, we know all discount factors except the last one (Z(0,T)Z(0, T)), which we solve for using the known swap rate STS_T. This iterative process builds the complete discount curve one maturity point at a time.

In[9]:
Code
def bootstrap_swap_curve(swap_rates, maturities, payment_freq=0.5):
    """
    Bootstrap discount factors from par swap rates.

    Parameters:
    -----------
    swap_rates : array-like
        Par swap rates for each maturity
    maturities : array-like
        Maturity in years for each swap rate
    payment_freq : float
        Payment frequency (0.5 for semiannual)

    Returns:
    --------
    discount_factors : ndarray
        Bootstrapped discount factors
    zero_rates : ndarray
        Implied zero rates (continuous compounding)
    """
    n = len(maturities)
    discount_factors = np.zeros(n)
    zero_rates = np.zeros(n)

    # Build payment schedule
    payment_times = []
    for mat in maturities:
        # Assume payments at regular intervals
        times = np.arange(payment_freq, mat + payment_freq / 2, payment_freq)
        payment_times.append(times)

    # Bootstrap iteratively
    for i in range(n):
        swap_rate = swap_rates[i]
        times = payment_times[i]

        if i == 0:
            # First discount factor: solve 1 = S*delta*Z + Z
            delta = times[0]
            discount_factors[i] = 1 / (1 + swap_rate * delta)
        else:
            # Sum of known discount factors
            known_sum = 0
            for j, t in enumerate(times[:-1]):
                # Find the discount factor for this time
                # Use known zero rates up to previous maturity
                # Interpolate using available zero rates
                z = np.exp(-np.interp(t, maturities[:i], zero_rates[:i]) * t)
                known_sum += swap_rate * payment_freq * z

            # Solve for final discount factor
            # 1 = known_sum + S*delta*Z_n + Z_n
            discount_factors[i] = (1 - known_sum) / (
                1 + swap_rate * payment_freq
            )

        # Update zero rates
        zero_rates[i] = -np.log(discount_factors[i]) / maturities[i]

    return discount_factors, zero_rates


# Market swap rates (typical market data)
market_maturities = np.array([1, 2, 3, 5, 7, 10])
market_swap_rates = np.array([0.047, 0.050, 0.052, 0.055, 0.057, 0.058])
In[10]:
Code
# Simplified bootstrap for illustration
def simple_bootstrap(swap_rates, maturities):
    """
    Simplified bootstrapping assuming annual payments.
    """
    n = len(maturities)
    discount_factors = np.zeros(n)

    for i in range(n):
        s = swap_rates[i]

        if i == 0:
            # First discount factor
            discount_factors[i] = 1 / (1 + s)
        else:
            # Sum of previous terms
            numerator = 1 - s * np.sum(discount_factors[:i])
            discount_factors[i] = numerator / (1 + s)

    # Convert to continuously compounded zero rates
    zero_rates = -np.log(discount_factors) / maturities

    return discount_factors, zero_rates


# Bootstrap the curve
boot_disc_factors, boot_zero_rates = simple_bootstrap(
    market_swap_rates, market_maturities
)
Out[11]:
Console
Bootstrapped Swap Curve
=================================================================
Maturity    Swap Rate      Discount Factor         Zero Rate
-----------------------------------------------------------------
     1 yr        4.700%           0.955110          4.5929%
     2 yr        5.000%           0.906900          4.8862%
     3 yr        5.200%           0.858532          5.0844%
     5 yr        5.500%           0.806038          4.3125%
     7 yr        5.700%           0.755899          3.9978%
    10 yr        5.800%           0.710412          3.4191%
Out[12]:
Visualization
Line chart comparing swap rates and zero rates across maturities from 1 to 10 years.
Comparison of par swap rates and bootstrapped zero rates. The zero curve lies above the swap curve in a normal (upward-sloping) rate environment because discount factors compound the effect of higher forward rates.

The bootstrapped zero rates lie slightly above the par swap rates in an upward-sloping yield environment. This occurs because par swap rates represent a complex average of forward rates, while zero rates reflect the pure time value of money to each specific maturity. In an upward-sloping curve, the averaging effect of the coupon payments in a par swap pulls the par rate below the zero rate for the same maturity.

Out[13]:
Visualization
Term structure of swap, zero, and forward rates derived from market data. Forward rates exhibit the steepest slope, rising from 4.7% to nearly 6.5%, which pulls the zero curve above the par swap curve as maturities extend.
Term structure of swap, zero, and forward rates derived from market data. Forward rates exhibit the steepest slope, rising from 4.7% to nearly 6.5%, which pulls the zero curve above the par swap curve as maturities extend.

Key Parameters

The key parameters for bootstrapping the swap curve are the inputs and outputs of the iterative procedure that builds the discount function from market data:

  • STS_T: Par swap rate. The market rate observed for a specific maturity, representing the fixed rate at which a new swap would have zero value.
  • Z(0,t)Z(0, t): Discount factor. The unknown variable being solved for iteratively at each maturity point. These factors encode the present value of future cash flows.
  • Maturities: The tenor points of the input swap curve (e.g., 1y, 2y, 5y). Market data is typically available at standard tenors, and interpolation may be needed for intermediate points.
  • Zero Rate: The continuously compounded spot rate implied by the discount factor. Calculated as r=ln(Z(0,t))/tr = -\ln(Z(0,t))/t, providing an alternative representation of the same information contained in the discount factors.

Determining the Par Swap Rate

Finding the par swap rate is essential for quoting new swaps and understanding fair value. When a client requests a swap quote, you must calculate the fixed rate that makes the swap worth zero at inception. We've seen the mathematical relationship between par swap rates and forward rates, but let's implement a practical calculation that works directly from discount factors.

The par swap rate can be derived from a simple no-arbitrage argument. At inception, the present value of the fixed leg must equal the present value of the floating leg, which is simply the notional amount. Rearranging this condition yields the par rate formula.

In[14]:
Code
def calculate_par_swap_rate(discount_factors, maturities, payment_freq=1.0):
    """
    Calculate the par swap rate given discount factors.

    The par swap rate makes the swap value equal to zero at inception.
    """
    # Sum of discounted annuity payments
    # Each payment is delta * Z
    annuity = np.sum(payment_freq * discount_factors)

    # Par swap rate: (1 - Z_n) / Annuity
    par_rate = (1 - discount_factors[-1]) / annuity

    return par_rate


# Calculate par rates for different tenors
tenors = [1, 2, 3, 5, 7, 10]
calculated_par_rates = []

for i, tenor in enumerate(tenors):
    # Use discount factors up to this tenor
    idx = i + 1
    par_rate = calculate_par_swap_rate(
        boot_disc_factors[:idx], market_maturities[:idx]
    )
    calculated_par_rates.append(par_rate)
Out[15]:
Console
Par Swap Rate Calculation
==================================================
Tenor       Market Rate         Calculated
--------------------------------------------------
    1 yr        4.7000%          4.7000%
    2 yr        5.0000%          5.0000%
    3 yr        5.2000%          5.2000%
    5 yr        5.5000%          5.5000%
    7 yr        5.7000%          5.7000%
   10 yr        5.8000%          5.8000%

Note: Small differences due to simplified annual payment assumption

The calculated par rates closely match the market rates, showing that our bootstrapping and rate calculation are internally consistent. The small differences arise from our simplifying assumption of annual payments, whereas real swaps often have semiannual or quarterly payment frequencies.

Key Parameters

The key parameters for calculating the par swap rate connect the discount function to the fair fixed rate for a new swap:

  • Z(0,t)Z(0, t): Discount factors derived from the yield curve. Used to value the annuity stream of fixed payments and to determine the present value of the notional exchange.
  • δδ: Payment frequency (e.g., 1.0 for annual). Determines the weighting of each discount factor in the annuity calculation and affects the precise value of the par rate.
  • SparS_{par}: The calculated par swap rate. It sets the present value of the fixed leg equal to the floating leg (at par), ensuring zero initial value for the swap.

Mark-to-Market Swap Valuation

Once a swap is traded, its value changes as interest rates move. The initial zero value at inception gives way to positive or negative values as market conditions evolve. Mark-to-market (MTM) valuation calculates the current value of an existing swap position, which is critical for:

  • Regulatory capital calculations, where financial institutions must hold capital against the potential future exposure of their derivative positions
  • Collateral management under Credit Support Annexes (CSAs), where parties exchange margin based on the current value of their positions
  • Profit and loss attribution, where you need to understand how rate movements have affected portfolio value
  • Risk management and hedging, where understanding current exposure is essential for making informed decisions about position adjustments

Valuation After Rate Changes

Consider a 5-year receive-fixed swap entered at a rate of 5.5%. After rates change, we need to revalue the swap using current market discount factors. The mechanics are identical to initial valuation: we calculate the present value of the fixed leg and subtract the floating leg value.

In[16]:
Code
def value_existing_swap(
    notional, fixed_rate, remaining_payments, discount_factors
):
    """
    Value an existing swap given current discount factors.

    Parameters:
    -----------
    notional : float
        Swap notional amount
    fixed_rate : float
        Contracted fixed rate
    remaining_payments : int
        Number of remaining payment periods
    discount_factors : array
        Current discount factors for remaining payment dates
    """
    # Fixed leg value
    fixed_leg = 0
    for i, df in enumerate(discount_factors):
        fixed_leg += notional * fixed_rate * df  # Annual payments assumed
    fixed_leg += notional * discount_factors[-1]  # Notional at maturity

    # Floating leg value at par (at reset)
    floating_leg = notional

    return fixed_leg - floating_leg


# Original swap parameters
original_swap = {
    "notional": 50_000_000,
    "fixed_rate": 0.055,
    "original_tenor": 5,
    "elapsed_years": 2,
}

# Remaining payments: 3 years
remaining_years = (
    original_swap["original_tenor"] - original_swap["elapsed_years"]
)

# Current market conditions (rates have fallen)
new_maturities = np.array([1, 2, 3])
new_zero_rates = np.array([0.042, 0.044, 0.046])  # Rates are lower now
new_discount_factors = np.exp(-new_zero_rates * new_maturities)

# Value the swap
current_value = value_existing_swap(
    original_swap["notional"],
    original_swap["fixed_rate"],
    remaining_years,
    new_discount_factors,
)
Out[17]:
Console
Mark-to-Market Swap Valuation
=======================================================

Original Swap Terms:
  Notional:          $50,000,000
  Fixed Rate:        5.50%
  Original Tenor:    5 years
  Time Elapsed:      2 years
  Remaining:         3 years

Current Market Zero Rates:
  1-year: 4.20%
  2-year: 4.40%
  3-year: 4.60%

Swap Valuation:
  Mark-to-Market Value: $1,105,690.30

When interest rates fall, receiving fixed payments becomes more valuable because you're locked into a higher rate than currently available in the market. The MTM value represents what you would pay to take over the swap position, or equivalently, the amount you could receive by unwinding the trade.

Key Parameters

The key parameters for mark-to-market valuation distinguish between the original contract terms and current market conditions:

  • KoldK_{old}: The original fixed rate on the swap. This rate was set at inception and remains constant throughout the swap's life, regardless of how market rates move.
  • NN: Notional principal amount. The reference amount used to calculate payment sizes, unchanged from inception.
  • Znew(0,t)Z_{new}(0, t): Current market discount factors. These reflect the new interest rate environment and are used to present-value the remaining cash flows.
  • TremainingT_{remaining}: Remaining time to maturity. Only remaining cash flows are valued; past payments have no effect on current mark-to-market value.

Swap Risk Measures

Understanding how swap values respond to rate changes is crucial for risk management. You need to know how much money could be gained or lost as interest rates fluctuate. The key measures are DV01 (dollar value of a basis point) and swap duration, both of which quantify interest rate sensitivity.

DV01: Dollar Value of a Basis Point

DV01 measures the change in swap value for a one basis point (0.01%) parallel shift in the yield curve. This standardized measure allows comparison of interest rate risk across different instruments and positions. The mathematical definition is:

DV01=Vy×0.0001\text{DV01} = -\frac{\partial V}{\partial y} \times 0.0001

where:

  • DV01\text{DV01}: dollar value of a basis point
  • VV: value of the swap
  • yy: yield curve level (interest rate)
  • 0.00010.0001: one basis point scaling factor

The negative sign in the formula accounts for the inverse relationship between prices and yields for fixed income instruments. However, for a receive-fixed swap, DV01 is positive because the swap gains value when rates fall. This occurs because falling rates increase the present value of the fixed payments we are receiving, while the floating payments we are making adjust downward.

In[18]:
Code
def calculate_swap_dv01(
    notional, fixed_rate, maturities, zero_rates, bump=0.0001
):
    """
    Calculate DV01 by bumping rates and revaluing.
    """
    # Original discount factors
    df_original = np.exp(-zero_rates * maturities)

    # Bumped discount factors (rates up)
    df_up = np.exp(-(zero_rates + bump) * maturities)

    # Bumped discount factors (rates down)
    df_down = np.exp(-(zero_rates - bump) * maturities)

    # Value swap at each scenario
    def swap_value(disc_factors):
        fixed_leg = (
            np.sum(notional * fixed_rate * disc_factors)
            + notional * disc_factors[-1]
        )
        return fixed_leg - notional

    v_original = swap_value(df_original)
    v_up = swap_value(df_up)
    v_down = swap_value(df_down)

    # DV01: average of up and down moves
    dv01 = (v_down - v_up) / 2

    return dv01, v_original, v_up, v_down


# Calculate DV01 for a 5-year swap
maturities_5y = np.array([1, 2, 3, 4, 5])
rates_5y = np.array([0.045, 0.048, 0.050, 0.052, 0.053])

dv01, v_base, v_up, v_down = calculate_swap_dv01(
    10_000_000, 0.052, maturities_5y, rates_5y
)
Out[19]:
Console
Swap DV01 Calculation
==================================================

5-Year Receive-Fixed Swap
  Notional:     $10,000,000
  Fixed Rate:   5.20%

Scenario Analysis:
  Base Value:      $  -89,557.62
  Rates +1bp:      $  -94,039.45
  Rates -1bp:      $  -85,073.64

  DV01:            $    4,482.91

Swap Duration

We can also express interest rate sensitivity as duration, which provides a measure that is independent of the notional amount and easier to compare across different positions. For a receive-fixed par swap, the effective duration is approximately equal to the duration of the fixed leg (which behaves like a fixed-rate bond). This is because:

  • The fixed leg has duration similar to a fixed-rate bond, with value decreasing when rates rise and increasing when rates fall.
  • The floating leg has duration close to zero (since it resets to market rates), meaning its value is largely insensitive to rate changes.
  • The net duration reflects primarily the fixed leg exposure, since the floating leg contributes minimal duration.
In[20]:
Code
# Calculate effective duration
def calculate_swap_duration(dv01, swap_value, notional):
    """
    Calculate modified duration from DV01.
    Duration = DV01 / (Value * 0.0001)

    For an at-par swap, use notional as the reference value.
    """
    # For a par swap, the value is close to zero
    # Use notional as the reference point
    duration = (dv01 / notional) * 10000
    return duration


swap_duration = calculate_swap_duration(dv01, v_base, 10_000_000)
Out[21]:
Console

Swap Duration Analysis
==================================================
  5-Year Swap Duration: 4.48 years
Out[22]:
Visualization
Dollar value of a basis point (DV01) across swap tenors from 1 to 10 years. Risk increases linearly with maturity, with a 10-year swap exhibiting nearly ten times the interest rate sensitivity of a 1-year swap.
Dollar value of a basis point (DV01) across swap tenors from 1 to 10 years. Risk increases linearly with maturity, with a 10-year swap exhibiting nearly ten times the interest rate sensitivity of a 1-year swap.
Effective duration of receive-fixed swaps for various maturities. Duration scales with the weighted average life of the fixed leg payments, reaching approximately 8 years for a 10-year swap.
Effective duration of receive-fixed swaps for various maturities. Duration scales with the weighted average life of the fixed leg payments, reaching approximately 8 years for a 10-year swap.

Practical Applications

Interest rate swaps are workhorses of fixed income markets, serving a variety of purposes for different market participants. Let's explore their primary applications through concrete examples that illustrate how these instruments create value.

Hedging Interest Rate Exposure

If you have floating-rate debt, you face uncertainty about future interest payments. When rates rise, borrowing costs increase, potentially squeezing profit margins or even threatening solvency. A pay-fixed, receive-floating swap allows you to convert your exposure to fixed-rate debt synthetically. You continue to make floating payments on your original loan but enter a swap to receive floating and pay fixed. The floating payments received on the swap offset the floating payments on the loan, leaving only the fixed swap payments.

In[23]:
Code
import numpy as np
import pandas as pd

# Corporate hedging example
corporate_loan = {
    "principal": 100_000_000,
    "spread": 0.015,  # SOFR + 150 bps
    "reference_rate": "SOFR",
}

hedge_swap = {
    "notional": 100_000_000,
    "fixed_rate": 0.048,  # Pay 4.80% fixed
    "receive": "SOFR",
}


def analyze_corporate_hedge(loan, swap, sofr_scenarios):
    """
    Analyze effective borrowing cost under different SOFR scenarios.
    """
    results = []

    for sofr in sofr_scenarios:
        # Without hedge
        unhedged_cost = sofr + loan["spread"]

        # With hedge
        # Loan: Pay SOFR + spread
        # Swap: Pay fixed, receive SOFR
        # Net: Pay fixed + spread
        hedged_cost = swap["fixed_rate"] + loan["spread"]

        results.append(
            {
                "sofr": sofr,
                "unhedged": unhedged_cost,
                "hedged": hedged_cost,
                "savings": unhedged_cost - hedged_cost,
            }
        )

    return pd.DataFrame(results)


sofr_scenarios = np.arange(0.02, 0.10, 0.01)
hedge_analysis = analyze_corporate_hedge(
    corporate_loan, hedge_swap, sofr_scenarios
)
Out[24]:
Console
Corporate Interest Rate Hedge Analysis
=================================================================

Loan Terms: $100M at SOFR + 1.50%
Swap Terms: Pay 4.80% fixed, receive SOFR

SOFR      Unhedged Cost     Hedged Cost        Savings/(Cost)
-----------------------------------------------------------------
  2.00%          3.50%             6.30%            -2.80%
  3.00%          4.50%             6.30%            -1.80%
  4.00%          5.50%             6.30%            -0.80%
  5.00%          6.50%             6.30%            +0.20%
  6.00%          7.50%             6.30%            +1.20%
  7.00%          8.50%             6.30%            +2.20%
  8.00%          9.50%             6.30%            +3.20%
  9.00%         10.50%             6.30%            +4.20%

The table shows that the hedge locks in a fixed borrowing cost of 6.30% (4.80% swap rate + 1.50% spread). When SOFR exceeds 4.80%, the hedge provides savings; when SOFR is below 4.80%, the unhedged position would have been cheaper, but the hedge provides certainty. For you, the value of certainty in budgeting and financial planning justifies accepting a potentially higher cost in low-rate environments.

Out[25]:
Visualization
Line chart comparing unhedged floating borrowing cost versus hedged fixed cost across SOFR rates.
Borrowing costs for unhedged and hedged scenarios across a range of SOFR rates. The pay-fixed swap locks in a 6.30% total cost, protecting you from rising rates while forfeiting potential gains if SOFR falls below the 4.8% strike.

Creating Synthetic Assets

Swaps enable the creation of synthetic instruments that may not be available directly in the market. If you own a floating-rate note but desire fixed-rate exposure, you can use a swap to transform the economic characteristics of the asset. Rather than selling the floating-rate note and purchasing a fixed-rate bond, which might involve transaction costs and tax consequences, you can overlay a swap to achieve the desired exposure.

In[26]:
Code
def create_synthetic_fixed_bond(floating_asset, swap):
    """
    Create a synthetic fixed-rate bond from a floating-rate asset plus swap.
    """
    # Floating asset: Receive SOFR + spread
    # Swap: Pay SOFR, receive fixed
    # Net: Receive fixed + spread (synthetic fixed bond)

    synthetic_yield = swap["receive_fixed_rate"] + floating_asset["spread"]

    return {
        "type": "Synthetic Fixed Bond",
        "yield": synthetic_yield,
        "components": {
            "Floating Asset": f"SOFR + {floating_asset['spread']:.2%}",
            "Swap Receive": f"{swap['receive_fixed_rate']:.2%}",
            "Swap Pay": "SOFR",
        },
    }


# Example: Convert floating-rate note to synthetic fixed
frn = {
    "spread": 0.0080,  # SOFR + 80 bps
    "principal": 25_000_000,
}

conversion_swap = {"pay": "SOFR", "receive_fixed_rate": 0.045}

synthetic = create_synthetic_fixed_bond(frn, conversion_swap)
Out[27]:
Console
Synthetic Fixed-Rate Bond Construction
=======================================================

Starting Position: Floating Rate Note
  Yield: SOFR + 0.80%
  Principal: $25,000,000

Swap Overlay:
  Pay: SOFR
  Receive: 4.50% fixed

Result: Synthetic Fixed-Rate Bond
  Fixed Yield: 5.30%
  (4.50% + 0.80% spread)

By combining the floating rate note with the swap, you secure a fixed yield of 5.30%. The swap converts the uncertain SOFR returns into a fixed stream, adding the note's spread to the swap's fixed rate. The resulting synthetic bond has the credit exposure of the original issuer but the interest rate characteristics of a fixed-rate instrument.

Asset-Liability Management

Banks and insurance companies use swaps to manage mismatches between asset and liability duration. A duration mismatch exposes the institution to interest rate risk: if asset duration exceeds liability duration, the equity value falls when rates rise because assets decline more in value than liabilities. Consider a scenario where you have long-duration assets (mortgages) funded by short-duration liabilities (deposits):

In[28]:
Code
def calculate_duration_gap(assets, liabilities):
    """
    Calculate duration gap for an institution.
    """
    # Weighted average duration of assets
    total_assets = sum(a["value"] for a in assets)
    asset_duration = (
        sum(a["value"] * a["duration"] for a in assets) / total_assets
    )

    # Weighted average duration of liabilities
    total_liabilities = sum(l["value"] for l in liabilities)
    liability_duration = (
        sum(l["value"] * l["duration"] for l in liabilities) / total_liabilities
    )

    # Duration gap (simplified)
    leverage = total_assets / (total_assets - total_liabilities)
    duration_gap = asset_duration - liability_duration * (
        total_liabilities / total_assets
    )

    return {
        "asset_duration": asset_duration,
        "liability_duration": liability_duration,
        "duration_gap": duration_gap,
        "total_assets": total_assets,
        "total_liabilities": total_liabilities,
    }


# Bank balance sheet
bank_assets = [
    {"type": "Mortgages", "value": 800_000_000, "duration": 7.5},
    {"type": "Commercial Loans", "value": 400_000_000, "duration": 3.0},
    {"type": "Securities", "value": 200_000_000, "duration": 5.0},
]

bank_liabilities = [
    {"type": "Deposits", "value": 1_000_000_000, "duration": 0.5},
    {"type": "CDs", "value": 250_000_000, "duration": 2.0},
    {"type": "Subordinated Debt", "value": 50_000_000, "duration": 6.0},
]

gap_analysis = calculate_duration_gap(bank_assets, bank_liabilities)
Out[29]:
Console
Bank Duration Gap Analysis
=======================================================

Asset Duration:     5.86 years
Liability Duration: 1.00 years
Duration Gap:       4.93 years

Total Assets:       $1.40 billion
Total Liabilities:  $1.30 billion
Equity:             $0.10 billion
Out[30]:
Visualization
Bank asset duration profile. Long-dated mortgages increase the weighted average asset duration.
Bank asset duration profile. Long-dated mortgages increase the weighted average asset duration.
Bank liability duration profile. Short-term deposits keep liability duration low, creating a gap.
Bank liability duration profile. Short-term deposits keep liability duration low, creating a gap.

To reduce the duration gap, you can enter a receive-floating, pay-fixed swap. This effectively shortens the duration of the asset portfolio by adding an instrument with negative duration. The pay-fixed position loses value when rates rise, offsetting some of the losses on the long-duration assets.

In[31]:
Code
def hedge_duration_gap(gap_analysis, target_gap, swap_duration):
    """
    Calculate swap notional needed to achieve target duration gap.
    """
    current_gap = gap_analysis["duration_gap"]
    total_assets = gap_analysis["total_assets"]

    # Receive-float swap has negative duration (from fixed payer's perspective)
    # Need to add negative duration to reduce gap
    duration_change_needed = current_gap - target_gap

    # Notional = (Duration Change * Assets) / Swap Duration
    notional = (duration_change_needed * total_assets) / swap_duration

    return notional


target_duration_gap = 1.0  # Target 1-year gap
swap_effective_duration = 4.0  # Duration of a pay-fixed swap

hedge_notional = hedge_duration_gap(
    gap_analysis, target_duration_gap, swap_effective_duration
)
Out[32]:
Console

Duration Gap Hedging Strategy
=======================================================
Current Duration Gap:  4.93 years
Target Duration Gap:   1.00 years
Gap Reduction Needed:  3.93 years

Hedging Swap (Pay Fixed, Receive Float):
  Swap Duration:       4.0 years
  Required Notional:   $1,375 million

A notional amount of \$350 million in the pay-fixed swap is required to reduce the duration gap from 2.40 years to the target of 1.00 years. The swap's negative duration (from your perspective) offsets the asset duration, bringing the overall balance sheet closer to immunization against interest rate movements.

Worked Example: Complete Swap Analysis

Let's work through a complete example that ties together all the concepts we've covered. We need to analyze a swap position and make a trading decision.

Problem Setup

Suppose we entered a 10-year receive-fixed swap three years ago at a rate of 5.00%. Current market conditions show lower rates. We need to:

  1. Value the existing swap position
  2. Calculate the DV01 exposure
  3. Determine whether to unwind or maintain the position
In[33]:
Code
# Swap parameters
swap_details = {
    "notional": 200_000_000,
    "original_rate": 0.050,
    "original_tenor": 10,
    "years_elapsed": 3,
    "remaining_tenor": 7,
}

# Current market data (7-year swap curve)
current_curve = {
    "maturities": np.array([1, 2, 3, 4, 5, 6, 7]),
    "zero_rates": np.array([0.038, 0.040, 0.042, 0.044, 0.046, 0.047, 0.048]),
}

# Current 7-year par swap rate (calculated from zero rates)
current_7y_swap_rate = 0.047  # 4.70%
In[34]:
Code
# Step 1: Value the existing swap
def comprehensive_swap_valuation(swap, curve):
    """
    Complete swap valuation with breakdown.
    """
    N = swap["notional"]
    K = swap["original_rate"]
    remaining = swap["remaining_tenor"]

    maturities = curve["maturities"][:remaining]
    zero_rates = curve["zero_rates"][:remaining]
    discount_factors = np.exp(-zero_rates * maturities)

    # Fixed leg components
    fixed_payments = N * K * np.ones(remaining)  # Annual payments
    pv_fixed_payments = fixed_payments * discount_factors
    pv_notional = N * discount_factors[-1]
    fixed_leg_pv = np.sum(pv_fixed_payments) + pv_notional

    # Floating leg (at par since we're at a reset date)
    floating_leg_pv = N

    # Swap value
    swap_value = fixed_leg_pv - floating_leg_pv

    return {
        "fixed_leg_pv": fixed_leg_pv,
        "floating_leg_pv": floating_leg_pv,
        "swap_value": swap_value,
        "fixed_payments": fixed_payments,
        "discount_factors": discount_factors,
        "pv_fixed_payments": pv_fixed_payments,
    }


valuation = comprehensive_swap_valuation(swap_details, current_curve)
Out[35]:
Console
=================================================================
PENSION FUND SWAP ANALYSIS REPORT
=================================================================

1. POSITION DETAILS
-----------------------------------------------------------------
   Type:              Receive Fixed, Pay Floating
   Notional:          $200,000,000
   Fixed Rate:        5.00%
   Original Tenor:    10 years
   Time Elapsed:      3 years
   Remaining Tenor:   7 years

2. CURRENT MARKET CONDITIONS
-----------------------------------------------------------------
   Current 7Y Swap Rate: 4.70%
   Rate Advantage:       0 bps

3. SWAP VALUATION
-----------------------------------------------------------------
   Fixed Leg PV:      $ 201,619,546.27
   Floating Leg PV:   $ 200,000,000.00
   Net Swap Value:    $   1,619,546.27
In[36]:
Code
# Step 2: Calculate DV01
def swap_dv01_analysis(swap, curve, bump_size=0.0001):
    """
    Calculate DV01 with scenario analysis.
    """
    N = swap["notional"]
    K = swap["original_rate"]
    remaining = swap["remaining_tenor"]

    maturities = curve["maturities"][:remaining]
    zero_rates = curve["zero_rates"][:remaining]

    scenarios = {}
    for shift_name, shift in [
        ("down_10bp", -0.001),
        ("down_1bp", -0.0001),
        ("base", 0),
        ("up_1bp", 0.0001),
        ("up_10bp", 0.001),
    ]:
        shifted_rates = zero_rates + shift
        df = np.exp(-shifted_rates * maturities)
        fixed_leg = np.sum(N * K * df) + N * df[-1]
        floating_leg = N
        scenarios[shift_name] = fixed_leg - floating_leg

    dv01 = (scenarios["down_1bp"] - scenarios["up_1bp"]) / 2

    return dv01, scenarios


dv01, scenarios = swap_dv01_analysis(swap_details, current_curve)
Out[37]:
Console

4. RISK ANALYSIS
-----------------------------------------------------------------
   DV01:              $     122,356.18
   Duration (approx):            6.12 years

   Scenario Analysis:
   Scenario               Swap Value       P&L vs Base
   --------------------------------------------------
   down_10bp      $   2,847,158.99   $ 1,227,612.72
   down_1bp       $   1,741,942.86   $   122,396.60
   base           $   1,619,546.27   $         0.00
   up_1bp         $   1,497,230.51   $  -122,315.76
   up_10bp        $     400,017.29   $-1,219,528.97
In[38]:
Code
# Step 3: Decision analysis
def analyze_unwind_decision(
    swap_value, dv01, current_rate, original_rate, view
):
    """
    Analyze whether to unwind or maintain the swap position.
    """
    rate_advantage = original_rate - current_rate

    analysis = {
        "current_mtm": swap_value,
        "rate_advantage_bps": rate_advantage * 10000,
        "dv01": dv01,
    }

    # If manager expects rates to rise, holding is risky
    if view == "rates_rising":
        expected_loss_per_bp = dv01
        analysis["recommendation"] = "CONSIDER UNWIND"
        analysis["reasoning"] = (
            f"With rates expected to rise, each 1bp increase costs ${dv01:,.0f}. "
            f"Current profit of ${swap_value:,.0f} may erode."
        )
    else:
        analysis["recommendation"] = "HOLD POSITION"
        analysis["reasoning"] = (
            f"With rates stable or falling, the position should maintain or increase value. "
            f"Continue collecting the {rate_advantage * 10000:.0f}bp rate advantage."
        )

    return analysis


# Assume manager expects rates to rise
decision = analyze_unwind_decision(
    valuation["swap_value"],
    dv01,
    current_7y_swap_rate,
    swap_details["original_rate"],
    "rates_rising",
)
Out[39]:
Console

5. TRADING DECISION ANALYSIS
-----------------------------------------------------------------
   Current P&L:       $1,619,546.27
   Rate Advantage:    30 bps
   DV01 Exposure:     $122,356.18

   RECOMMENDATION: CONSIDER UNWIND

   Reasoning: With rates expected to rise, each 1bp increase costs $122,356. Current profit of $1,619,546 may erode.

=================================================================

The analysis shows the swap has significant mark-to-market profit due to the rate decline since inception. However, the large DV01 exposure means this profit is vulnerable to rising rates. We must weigh the attractiveness of the current gain against the risk of future rate increases.

Out[40]:
Visualization
Line chart showing swap mark-to-market value across yield curve shifts from minus 50 to plus 50 basis points.
Mark-to-market sensitivity of a 7-year receive-fixed swap to parallel yield curve shifts. The relationship is nearly linear within the ±50bp range, with the swap gaining approximately $1.4 million in value for every 10bp decrease in market rates.

Limitations and Practical Considerations

While our valuation framework captures the essential mechanics, real-world swap pricing involves several complexities we've simplified. Understanding these limitations is important for bridging the gap between textbook models and market practice.

Credit and Counterparty Risk. Our models assume both parties will honor their obligations. In practice, counterparty credit risk affects swap pricing, particularly for uncollateralized trades. The 2008 financial crisis highlighted how counterparty concerns can destabilize swap markets. Modern swaps increasingly trade through central clearinghouses that mitigate this risk through margin requirements and mutualized default funds.

Discounting Curve Selection. We've used a single curve for both projecting forward rates and discounting cash flows. After the financial crisis, market practice shifted to using overnight indexed swap (OIS) rates for discounting collateralized trades, while using term SOFR or other curves for forward rate projections. This "dual curve" or "multi-curve" framework adds complexity but better reflects the economics of modern swap markets.

Day Count Conventions and Payment Timing. Real swaps involve specific day count conventions (30/360, actual/360, actual/actual) that affect payment amounts. Payment dates may be adjusted for weekends and holidays. Our simplified annual payment assumption works for understanding concepts but requires refinement for actual trading.

Liquidity and Bid-Ask Spreads. Market swap rates reflect mid-market levels. Actual execution involves bid-ask spreads that vary with tenor, notional size, and market conditions. Dealers quote wider spreads for off-market swaps, less liquid tenors, and larger sizes.

Basis Risk. When using swaps to hedge, the floating rate on the swap may not perfectly match the floating rate on the underlying exposure. For example, a company with prime-rate borrowing hedging with a SOFR-based swap retains prime-SOFR basis risk. This basis can fluctuate, reducing hedge effectiveness.

Despite these limitations, the valuation techniques we've covered provide a solid foundation for understanding swap markets. Professional systems build on these principles while adding the granular details required for actual trading and risk management.

Summary

This chapter developed the analytical tools for valuing and applying interest rate swaps:

  • Valuation Approaches: Swaps can be valued as portfolios of bonds (long fixed, short floating) or portfolios of forward rate agreements. Both methods yield identical values, offering complementary intuitions about swap economics.
  • The Swap Curve: Bootstrapping extracts discount factors from market swap rates, creating a term structure essential for pricing interest rate derivatives. The swap curve serves as a benchmark for the cost of funds for major financial institutions.
  • Par Swap Rate: The par swap rate is the weighted average of forward rates that gives a swap zero initial value. It represents the market's expectation of average short-term rates over the swap's life.
  • Mark-to-Market Valuation: As rates change, existing swap positions gain or lose value. You benefit from falling rates when receiving fixed, while you benefit from rising rates when paying fixed.
  • Risk Measures: DV01 measures the dollar value of a basis point change in rates. Swap duration approximates half the swap's remaining maturity, reflecting the fixed leg's interest rate exposure.
  • Practical Applications: Swaps enable hedging (converting floating exposure to fixed), synthetic asset creation (combining floating assets with swaps to create synthetic fixed instruments), and asset-liability management (adjusting duration gaps).

Understanding swap valuation opens the door to credit derivatives and structured products. In the next chapter, we'll examine credit default swaps, which apply similar contractual structures to credit risk rather than interest rate risk.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about interest rate swap valuation and applications.

Loading component...

Reference

BIBTEXAcademic
@misc{interestrateswapvaluationbondportfolioframethods, author = {Michael Brenndoerfer}, title = {Interest Rate Swap Valuation: Bond Portfolio & FRA Methods}, year = {2025}, url = {https://mbrenndoerfer.com/writing/interest-rate-swap-valuation-bond-fra-curve-bootstrapping}, organization = {mbrenndoerfer.com}, note = {Accessed: 2025-01-01} }
APAAcademic
Michael Brenndoerfer (2025). Interest Rate Swap Valuation: Bond Portfolio & FRA Methods. Retrieved from https://mbrenndoerfer.com/writing/interest-rate-swap-valuation-bond-fra-curve-bootstrapping
MLAAcademic
Michael Brenndoerfer. "Interest Rate Swap Valuation: Bond Portfolio & FRA Methods." 2026. Web. today. <https://mbrenndoerfer.com/writing/interest-rate-swap-valuation-bond-fra-curve-bootstrapping>.
CHICAGOAcademic
Michael Brenndoerfer. "Interest Rate Swap Valuation: Bond Portfolio & FRA Methods." Accessed today. https://mbrenndoerfer.com/writing/interest-rate-swap-valuation-bond-fra-curve-bootstrapping.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Interest Rate Swap Valuation: Bond Portfolio & FRA Methods'. Available at: https://mbrenndoerfer.com/writing/interest-rate-swap-valuation-bond-fra-curve-bootstrapping (Accessed: today).
SimpleBasic
Michael Brenndoerfer (2025). Interest Rate Swap Valuation: Bond Portfolio & FRA Methods. https://mbrenndoerfer.com/writing/interest-rate-swap-valuation-bond-fra-curve-bootstrapping