23 — Effective Duration and MBS Negative Convexity¶
Free learning pack¶
resources/fixed_income.mdreference/fixed_income/effective_duration.md,mbs_convexity.md
Do not search for more material until these are insufficient.
PREDICT (effective duration as a technique)¶
For a plain vanilla bond whose cash flows don't change with yield, should bump-and-reprice "effective duration" give roughly the same answer as the closed-form modified duration? Why is effective duration still useful if so?
Formula¶
EffectiveDuration = (Price_down - Price_up) / (2 * Price_base * bump)
from pm.fixed_income.bond import bond_price
from pm.fixed_income.duration import modified_duration
ytm, face, coupon_rate, years, frequency = 0.05, 100.0, 0.05, 5.0, 2
bump = 0.0001
# MANUAL FIRST:
# price the bond at ytm, ytm+bump, and ytm-bump using bond_price, then
# compute effective duration from those three prices. Compare it to
# modified_duration(ytm, face, coupon_rate, years, frequency) - they
# should nearly match.
effective_dur = None
print(effective_dur, modified_duration(ytm, face, coupon_rate, years, frequency))
PREDICT (extension/contraction)¶
A pool's WAC is 6%. If market rates fall to 4% (strong refinancing incentive) versus rise to 8% (none), which scenario gives the shorter weighted average life?
Formula¶
CPR = refinancing_incentive_cpr(wac, market_rate)
SMM = single_monthly_mortality(CPR)
Apply SMM to the scheduled amortization, then compute WAL on the
resulting total-principal cash flows.
import numpy as np
from pm.fixed_income.mbs import mortgage_amortization_schedule
wac, balance, months = 0.06, 1_000_000.0, 360
beginning, scheduled, interest, _ = mortgage_amortization_schedule(balance, wac, months)
times = np.arange(1, months + 1) / 12
# MANUAL FIRST:
# for market_rate in [0.04, 0.08]: compute cpr, smm, apply_prepayment to
# get total_principal, then weighted_average_life(times, total_principal).
# Confirm WAL is shorter at 4% than at 8%.
wal_at_4pct = None
wal_at_8pct = None
print(wal_at_4pct, wal_at_8pct)
What this does not show¶
This confirms cash-flow timing shifts (extension/contraction) but does
not demonstrate MBS negative convexity in price terms — naively
discounting these cash flows at a flat market rate doesn't correctly
price the prepayment option (it misses that principal returns at par,
capping upside vs. an option-free bond). That needs OAS — see
reference/fixed_income/oas.md and reference/fixed_income/mbs_convexity.md
for why it isn't built here. Read both before the oral check.
Not implemented here: non-agency/CMBS¶
Tranche waterfalls need machinery beyond this repo's scope. Read
reference/fixed_income/non_agency_overview.md for what they are and why.
Experiment¶
Recompute WAL at 5% and 5.9% (both still below the 6% WAC, so both should show some incentive). Is the WAL response smooth or does it have a kink?
Reference¶
reference/fixed_income/effective_duration.md
reference/fixed_income/mbs_convexity.md
Promote¶
Use src/pm/fixed_income/mbs.py (effective_duration,
refinancing_incentive_cpr, single_monthly_mortality,
weighted_average_life) only after your own implementation.
Test¶
pytest tests/test_mbs.py
ORAL CHECK¶
Explain to a PM the difference between "this pool's average life shortens when rates fall" (which we just showed) and "this pool's price upside is capped when rates fall" (which needs OAS) — why doesn't proving the first one prove the second?
Try /tutor fixed-income mbs convexity for an adaptive walkthrough.