Interest Rate Swap Valuation and Applications › Worked Example: Existing Swap Value and Rate Risk
Quantitative FinanceAll chapters Interest Rate Swap Valuation and Applications › Worked Example: Existing Swap Value and Rate RiskOne section · Sections in this chapter14 / 17 ↓
Reading tools Definitions & inline links
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
This worked example combines the bond-portfolio valuation, a matching current par comparator and signed rate-risk analysis for an existing swap. We then identify the additional inputs needed for a hold-or-unwind decision.
Problem Setup
Suppose we entered a 10-year receive-fixed swap three years ago at a rate of 5.00%. The current curve's matching seven-year par coupon is below this 5.00% contracted coupon. We need to:
- Value the existing swap position
- Calculate the DV01 exposure
- Discuss the position's conditional rate risk and the additional inputs needed for a hold-or-unwind decision
# Swap parameters
swap_details = {
"notional": 200_000_000,
"original_rate": 0.050,
"original_tenor": 10,
"years_elapsed": 3,
"remaining_tenor": 7,
}
# Hypothetical continuous-zero curve for the remaining annual schedule
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 annual-pay par rate, computed from the same current zero curve.
current_discounts = np.exp(-current_curve["zero_rates"] * current_curve["maturities"])
current_7y_swap_rate = calculate_par_swap_rate(
current_discounts, current_curve["maturities"], payment_freq=1.0
)# 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)================================================================= 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 ----------------------------------------------------------------- Curve-implied annual 7Y par rate: 4.8620% Rate Advantage: 13.8 bps 3. SWAP VALUATION ----------------------------------------------------------------- Fixed Bond PV (incl. hypothetical principal): $ 201,619,546.27 Floating Bond PV (incl. hypothetical principal): $ 200,000,000.00 Net Swap Value: $ 1,619,546.27
# Step 2: Calculate DV01
def swap_dv01_analysis(swap, curve, bump_size=0.0001):
"""Annual existing-swap risk immediately after the current fixing.
Keep that observed fixing unchanged under every parallel zero shock.
bump_size controls the central derivative, normalized back to one bp;
the displayed +/-1bp and +/-10bp stress scenarios remain fixed.
"""
N = swap["notional"]
K = swap["original_rate"]
remaining = swap["remaining_tenor"]
if isinstance(remaining, bool) or not isinstance(remaining, int) or remaining <= 0:
raise ValueError("Use a positive integer number of annual payments.")
maturities = np.asarray(curve["maturities"], dtype=float)[:remaining]
zero_rates = np.asarray(curve["zero_rates"], dtype=float)[:remaining]
if len(maturities) != remaining or len(zero_rates) != remaining:
raise ValueError("The curve must cover every remaining annual payment.")
dv01, _, _, _ = calculate_swap_dv01(N, K, maturities, zero_rates, bump=bump_size)
known_first_fixing = np.expm1(zero_rates[0] * maturities[0])
scenarios = {}
for name, shift in [
("down_10bp", -0.001),
("down_1bp", -0.0001),
("base", 0.0),
("up_1bp", 0.0001),
("up_10bp", 0.001),
]:
discounts = np.exp(-(zero_rates + shift) * maturities)
fixed_bond = N * (K * np.sum(discounts) + discounts[-1])
floating_bond = N * (1 + known_first_fixing) * discounts[0]
scenarios[name] = fixed_bond - floating_bond
return dv01, scenarios
dv01, scenarios = swap_dv01_analysis(swap_details, current_curve)4. RISK ANALYSIS ----------------------------------------------------------------- DV01: $ 102,356.18 Notional sensitivity D_N: 5.12 years Scenario Analysis: Scenario Swap Value P&L vs Base -------------------------------------------------- down_10bp $ 2,647,058.95 $ 1,027,512.69 down_1bp $ 1,721,941.86 $ 102,395.60 base $ 1,619,546.27 $ 0.00 up_1bp $ 1,517,229.51 $ -102,316.76 up_10bp $ 599,917.33 $-1,019,628.94
# Step 3: Decision analysis
def analyze_unwind_decision(swap_value, dv01, current_rate, original_rate, view):
"""A qualitative receive-fixed risk prompt, not an optimal trading decision.
No probabilities, holding horizon, carry/roll, execution costs or fund
liabilities are modeled. Use a matching current par rate as comparator.
"""
if view not in {"rates_rising", "rates_stable", "rates_falling"}:
raise ValueError("Use rates_rising, rates_stable or rates_falling.")
rate_advantage = original_rate - current_rate
analysis = {
"current_mtm": swap_value,
"rate_advantage_bps": rate_advantage * 10000,
"dv01": dv01,
}
if view == "rates_rising":
analysis["recommendation"] = "DISCUSS REDUCING RATE EXPOSURE"
analysis["reasoning"] = (
f"An instantaneous local 1bp parallel rate rise reduces this "
f"receive-fixed MTM by approximately ${dv01:,.0f}. "
f"Current MTM is ${swap_value:,.0f}; this is not cumulative profit."
)
else:
analysis["recommendation"] = "REVIEW CARRY, COSTS AND HEDGING OBJECTIVE"
analysis["reasoning"] = (
"Same-time flat or downward curve shocks do not imply a future "
"holding-period return. Compare coupon cash flows, curve roll, "
"costs and portfolio liabilities before choosing to hold or unwind. "
f"The contracted fixed coupon is {rate_advantage * 10000:.1f}bp "
"above the matching current par coupon, not a guaranteed net receipt."
)
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",
)5. TRADING DECISION ANALYSIS ----------------------------------------------------------------- Current MTM: $1,619,546.27 Rate Advantage: 13.8 bps DV01 Exposure: $102,356.18 RECOMMENDATION: DISCUSS REDUCING RATE EXPOSURE Reasoning: An instantaneous local 1bp parallel rate rise reduces this receive-fixed MTM by approximately $102,356. Current MTM is $1,619,546; this is not cumulative profit. =================================================================
The remaining receive-fixed position has positive current MTM because its contracted coupon exceeds the matching current par coupon. The inputs do not establish cumulative profit or a same-tenor historical rate decline. Positive DV01 identifies local exposure to an instantaneous parallel rate rise; a hold-or-unwind decision also needs a horizon, expected cash flows and curve roll, execution costs and the fund's wider hedging objective.
