Immunization with Interest Rate Swaps
How to hedge a portfolio's duration and convexity gaps using plain-vanilla interest rate swaps, as implemented in the ALM toolkit.
The Problem
An insurer holds assets (bonds, mortgages, private credit) backing liabilities (annuities, life insurance). If interest rates change, assets and liabilities reprice by different amounts because they have different duration and convexity profiles. The surplus (Assets − Liabilities) swings unpredictably.
Goal: Make surplus insensitive to parallel shifts in the yield curve by closing both the duration gap and the convexity gap.
Interest Rate Swaps as Hedging Instruments
A plain-vanilla fixed-for-floating swap exchanges:
- Fixed leg: periodic payments at a pre-agreed rate
- Floating leg: periodic payments that reset to market rates
| Position | Duration effect | Economic equivalent |
|---|---|---|
| Receive-fixed (pay floating) | Adds duration | Long a fixed-rate bond, short a floater |
| Pay-fixed (receive floating) | Removes duration | Short a fixed-rate bond, long a floater |
Swaps are off-balance-sheet — they adjust risk without buying or selling bonds.
Code: InterestRateSwap in src/alm/core.py
Step 1 — Measure the Gaps
Dollar Duration Gap (DV01)
DV01 measures the dollar change in value for a 1 bp parallel rate shift:
The gap to close is:
A positive gap means liabilities are more rate-sensitive than assets — rates falling would widen the deficit.
Code: dv01() in src/alm/core.py
Dollar Convexity Gap
Dollar convexity measures how DV01 itself changes as rates move (the second derivative of PV with respect to yield):
The gap:
Duration-only hedging leaves the portfolio exposed to large rate moves. Closing the convexity gap fixes this.
Code: dollar_convexity() in src/alm/core.py
Step 2 — Choose Two Hedging Instruments
You need two instruments with linearly independent sensitivity profiles to target both DV01 and convexity simultaneously. A short- tenor and a long-tenor swap work well because:
- Both have non-zero DV01 (from their fixed legs)
- The longer swap has proportionally more convexity
- The ratio of DV01-to-convexity differs between them
| Instrument | Why it works |
|---|---|
| 5-year swap | Lower duration, lower convexity per unit notional |
| 10-year swap | Higher duration, higher convexity per unit notional |
Important — parallel shift convention for swaps: When computing swap sensitivities, bump the floating rates and the discount rate together. This reflects a true parallel curve shift. If you only bump the discount rate while holding floating rates fixed, an at-par swap (where fixed rate = floating rate = discount rate) will show zero PV change and zero convexity, making the hedge matrix singular.
# Correct: floating rates move with the discount rate
dc_5y = dollar_convexity(
lambda r: swap_5y.present_value([r] * swap_5y.n_periods, r),
discount_rate,
) / swap_5y.notional
# Wrong: floating rates are frozen — convexity ≈ 0
dc_5y = dollar_convexity(
lambda r: swap_5y.present_value(flat_floats, r),
discount_rate,
) / swap_5y.notional
Step 3 — Solve the 2×2 System
Given per-unit sensitivities of each hedging instrument, solve for the notionals \(n_1\) and \(n_2\):
The solution via Cramer's rule:
The denominator is the determinant of the sensitivity matrix. If it is zero (or near-zero), the two instruments are linearly dependent and cannot independently target both gaps — pick instruments with more separation in tenor.
Interpreting the sign of \(n\):
| Sign | Meaning |
|---|---|
| \(n > 0\) | Enter a receive-fixed position of that notional |
| \(n < 0\) | Enter a pay-fixed position of $ |
Code: immunize() in src/alm/core.py
Step 4 — Verify Under Stress
After computing the hedge, re-run the portfolio under parallel rate shocks (e.g. ±100 bp, ±200 bp) and compare surplus with and without the hedge. A well-immunized portfolio should show:
- Surplus nearly flat across small shocks (duration matched)
- Surplus slightly gaining under large shocks in either direction (positive net convexity)
Worked Example
from alm.core import (
InterestRateSwap, dv01, dollar_convexity, immunize,
)
DISCOUNT_RATE = 0.04
# 1. Measure gaps (computed from your asset/liability portfolio)
asset_dv01 = sum(dv01(a.present_value, DISCOUNT_RATE) for a in assets)
liab_dv01 = sum(dv01(l.present_value, DISCOUNT_RATE) for l in liabilities)
dd_gap = liab_dv01 - asset_dv01
asset_dc = sum(dollar_convexity(a.present_value, DISCOUNT_RATE) for a in assets)
liab_dc = sum(dollar_convexity(l.present_value, DISCOUNT_RATE) for l in liabilities)
dc_gap = liab_dc - asset_dc
# 2. Define hedging instruments
swap_5y = InterestRateSwap(notional=1, fixed_rate=0.04, tenor=5, pay_fixed=False)
swap_10y = InterestRateSwap(notional=1, fixed_rate=0.04, tenor=10, pay_fixed=False)
# 3. Compute per-unit sensitivities (parallel shift convention)
dd_5y = swap_5y.dv01([0.04] * swap_5y.n_periods, DISCOUNT_RATE)
dc_5y = dollar_convexity(
lambda r: swap_5y.present_value([r] * swap_5y.n_periods, r),
DISCOUNT_RATE,
)
dd_10y = swap_10y.dv01([0.04] * swap_10y.n_periods, DISCOUNT_RATE)
dc_10y = dollar_convexity(
lambda r: swap_10y.present_value([r] * swap_10y.n_periods, r),
DISCOUNT_RATE,
)
# 4. Solve
n1, n2 = immunize(dd_gap, dc_gap, dd_5y, dc_5y, dd_10y, dc_10y)
# n1 = notional for the 5Y swap
# n2 = notional for the 10Y swap
Common Pitfalls
| Pitfall | Consequence | Fix |
|---|---|---|
| Only matching DV01 (ignoring convexity) | Surplus exposed to large rate moves | Use two instruments and match both |
| Freezing floating rates when computing swap convexity | Sensitivity matrix is singular (ValueError) |
Bump floating rates with the discount rate |
| Using two swaps with very similar tenors | Near-singular matrix, unstable notionals | Separate tenors by at least 3–5 years |
| Forgetting to re-hedge after asset/liability changes | Gaps drift over time | Re-run periodically or after material changes |
Quick Reference
| Function | Module | Purpose |
|---|---|---|
dv01(pv_func, rate) |
core |
Dollar duration via central difference |
dollar_convexity(pv_func, rate) |
core |
Dollar convexity via central difference |
immunize(dd_gap, dc_gap, ...) |
core |
Solve 2×2 hedge for notionals |
InterestRateSwap |
core |
Swap cashflows, PV, DV01, convexity |