Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

mortgagekit

A small Python package with mortgage math and modeling utilities used by the book. It serves as the companion computational package for the book (Mortgage Markets in the Age of AI) and the essay series (The Invisible System of Mortgages).

Core components reside across the following modules:

  • src/mortgagekit/mortgage.py — Scheduled monthly payment, amortization schedule, and tracking of principal and interest balances.
  • src/mortgagekit/prepayment.py — Conversions between annualized pre-payment speed (CPR), monthly pre-payment speed (SMM), and survival probability calculations.
  • src/mortgagekit/rates.py — Continuous-compounding discounting, portfolio present value, DV01, and CV01 risk sensitivity analytics.

Installation

Install in editable mode for local development:

pip install -e .

Module Reference

1. Amortization and Payments

Located in src/mortgagekit/mortgage.py. Contains functions to compute fixed monthly payments, remaining balances, and multi-period scheduling.

Mortgage Analytical Formulas

For a self-amortizing fixed-rate mortgage with principal $B$, monthly interest rate $r_m = \text{annual_rate} / 12$, and total periods $N = \text{months}$, the fixed monthly payment $P$ is calculated as:

$$P = B \times \frac{r_m}{1 - (1 + r_m)^{-N}}$$

The remaining principal balance $B_k$ after the $k$-th payment period is given recursively by:

$$B_k = B_{k-1} \times (1 + r_m) - P$$

Mortgage Code Examples

from mortgagekit import mortgage_payment, mortgage_balance, mortgage_schedule

# Compute monthly payment on a $400,000, 30-year (360-month) loan at 6.5% interest rate
payment = mortgage_payment(balance=400_000, annual_rate=0.065, months=360)
print(f"Monthly Payment: ${payment:.2f}")
# Output: Monthly Payment: $2528.27

# Check remaining scheduled balance after 5 years (60 months)
balance_after_5y = mortgage_balance(balance=400_000, annual_rate=0.065, months=360, month=60)
print(f"Balance after 5 Years: ${balance_after_5y:.2f}")

# Generate full amortization schedules
schedule = mortgage_schedule(balance=400_000, annual_rate=0.065, months=360)
# Look at month 1
m1 = schedule[0]
print(f"Month {m1.month}: Payment={m1.payment:.2f}, Interest={m1.interest:.2f}, Principal={m1.principal:.2f}, Remaining Balance={m1.balance:.2f}")

2. Prepayment Conventions

Located in src/mortgagekit/prepayment.py. Helps map annualized pre-payments into month-over-month conditional rates.

Prepayment Analytical Formulas

The relationship between the Conditional Prepayment Rate ($CPR$) and Single Monthly Mortality ($SMM$) is configured via:

$$SMM = 1 - (1 - CPR)^{\frac{1}{12}}$$

$$CPR = 1 - (1 - SMM)^{12}$$

Under a flat prepayment speed of $CPR$, the survival probability $S(t)$ representing the probability that a loan balance remains active (has not prepaid) after $t$ months is:

$$S(t) = (1 - SMM)^t$$

Prepayment Code Examples

from mortgagekit import cpr_to_smm, smm_to_cpr, survival_probability

# Convert a 12% CPR to monthly SMM
smm_12 = cpr_to_smm(0.12)
print(f"12% CPR corresponds to {smm_12 * 100:.4f}% SMM")
# Output: 12% CPR corresponds to 1.0596% SMM

# Convert a monthly SMM of 1.5% back to annualized CPR
cpr_reconstructed = smm_to_cpr(0.015)
print(f"1.5% SMM corresponds to {cpr_reconstructed * 100:.4f}% CPR")

# Find the survival probability of the outstanding balance after 36 months at 8% CPR
survival_pct = survival_probability(cpr=0.08, months=36)
print(f"Remaining active balance ratio after 3 years: {survival_pct * 100:.2f}%")

3. Yields, Rates, and Risk Sensitivities

Located in src/mortgagekit/rates.py. Applies continuous-compounded discount factors, computes flat yield bond cash flows, and evaluates risk metric vectors.

Yield and Risk Analytical Formulas

We use continuous compounding as the standard mathematical baseline for valuation modeling. The discount factor $d(t)$ at maturity time $t$ under a continuous rate $r$ is:

$$d(t) = e^{-r t}$$

Let ${ (t_i, C_i) }_{i=1}^n$ be a set of discrete future cash flows. The total bond price $B(y)$ evaluated under flat continuous yield $y$ is:

$$B(y) = \sum_{i=1}^n C_i e^{-y t_i}$$

The risk sensitivity metric DV01 represents the dollar value of a one-basis-point (0.01%, or $10^{-4}$) decline in rates. Formally, it is proportional to the first derivative of price with respect to yield:

$$\text{DV01} = -\frac{dB}{dy} \times 10^{-4} = \left( \sum_{i=1}^n t_i C_i e^{-y t_i} \right) \times 10^{-4}$$

The risk sensitivity metric CV01 (Convexity Value of a Basis Point) represents the second-order sensitivity scaled to a one-basis-point yield shock:

$$\text{CV01} = \frac{1}{2} \frac{d^2B}{dy^2} \times (10^{-4})^2 = \frac{1}{2} \left( \sum_{i=1}^n t_i^2 C_i e^{-y t_i} \right) \times 10^{-8}$$

Yield and Risk Code Examples

from mortgagekit import discount_factor, bond_price, dv01, cv01

# Continuous discount factor for 2.5 years at 5.0% rate
df = discount_factor(rate=0.05, t=2.5)
print(f"Discount Factor: {df:.6f}")

# Define a series of expected cash flows: (Time in years, Expected Cashflow)
cashflows = [
    (0.5, 12000.0),
    (1.0, 12000.0),
    (1.5, 12000.0),
    (2.0, 112000.0)
]

# Price cashflows under a flat 4.5% yield
price = bond_price(cashflows, y=0.045)
print(f"Bond Present Value: ${price:.2f}")

# Compute DV01 and CV01 sensitivities under the 4.5% rate environment
dollar_value_01 = dv01(cashflows, y=0.045)
convexity_value_01 = cv01(cashflows, y=0.045)

print(f"DV01 Risk: ${dollar_value_01:.4f} per basis point drop")
print(f"CV01 Risk: ${convexity_value_01:.6f} curvature sensitivity")

The Pricing and Risk Engine

Sections 1–3 above are the building blocks. This section is the engine built on top of them: it projects what a pool pays, discounts it against a curve, and measures how the value moves when rates do.

The engine is pure standard library — no numpy, no scipy — so it installs anywhere and every number in it can be traced by hand.

Modules

Module What it holds
src/mortgagekit/curve.py Curve — a continuously-compounded zero curve, linear between knots, flat outside them, with parallel shifts and single-knot bumps
src/mortgagekit/prepayment.py FlatCPR, PSA, RefiSCurve — the speed assumptions
src/mortgagekit/cashflows.py MortgagePool, pool_cashflows — scheduled principal, prepayments, and the servicing strip
src/mortgagekit/pricing.py present value, WAL, static (Z-)spread, cash-flow yield
src/mortgagekit/risk.py effective duration and convexity, DV01, key rate durations
src/mortgagekit/calculator.py analyze — one call that returns the whole report

A prepayment model is anything with smm(age, rate_shift)

The engine only ever asks a model one question: what fraction of the surviving balance prepays this month? That single interface is what lets the same pricing code run a flat CPR, a PSA ramp, or a rate-sensitive refinancing curve.

from mortgagekit import FlatCPR, PSA, RefiSCurve

FlatCPR(0.06)                                    # 6% CPR forever
PSA(200)                                         # 200 PSA: ramps to 12% CPR by month 30
RefiSCurve(wac=0.065, mortgage_rate=0.065)       # speeds respond to the refi incentive

rate_shift is the parallel rate move the risk engine is currently applying. Models that ignore it are static by definition — and that is exactly why they cannot show you negative convexity.

Projecting and pricing a pool

from mortgagekit import Curve, MortgagePool, PSA, pool_cashflows, price, weighted_average_life

# A $1mm pool of 6.5% loans passing 6.0% through to the investor after servicing
pool = MortgagePool(balance=1_000_000, wac=0.065, wam=360, net_coupon=0.060)

curve = Curve.from_zeros([(0.25, 0.043), (2, 0.041), (5, 0.042), (10, 0.045), (30, 0.047)])

flows = pool_cashflows(pool, PSA(200))
print(f"WAL: {weighted_average_life(flows):.2f} years")     # WAL: 7.73 years
print(f"Value: {price(pool, curve, PSA(200)):,.0f}")        # Value: 1,096,766

Each month, in the order a servicer applies them: interest accrues on the opening balance at the net coupon; the scheduled payment is recomputed on the opening balance over the remaining term, giving scheduled principal; and the prepayment is that month's SMM applied to what is left after the scheduled payment. Principal always returns in full — prepayment changes when the money comes back, never how much.

The whole report in one call

from mortgagekit import analyze, RefiSCurve

report = analyze(pool, curve, RefiSCurve(wac=0.065, mortgage_rate=0.065), spread=0.0075)
print(report.summary())
Price                   104.5184 per 100
WAL                         6.43 years
Cash-flow yield           5.1078 %  (monthly nominal)
Static spread               75.0 bp over the curve
Effective duration          3.83
Static duration             5.01
Duration given up           1.18  to the borrower's option
Effective convexity      -363.02
Effective DV01          400.7424 per 1bp fall in rates
Final cash flow              360 months out
Key rate durations    0.25y +0.09  2y +0.79  5y +1.70  10y +2.06  30y +0.37

Why effective and static duration differ

A static duration differentiates the price of a fixed set of cash flows. An effective duration reprices the pool with the cash flows allowed to move. For a Treasury the two agree. For a mortgage they do not, and the gap between them is the borrower's option:

$$D_{\text{eff}} = \frac{P_{-} - P_{+}}{2 P_0 \Delta y} \qquad C_{\text{eff}} = \frac{P_{+} + P_{-} - 2 P_0}{P_0 (\Delta y)^2}$$

where $P_{\pm}$ are repriced after reprojecting the cash flows at the shocked rate level. Holding the cash flows still while shifting the curve is the mistake that makes a mortgage look positively convex.

Priced at a 75bp spread, a 6% pass-through against the same pool with the option switched off:

Shock CPR With option No option Given up
−300bp 43.7% 111.329 126.114 −14.785
−100bp 29.1% 107.061 111.322 −4.260
0 15.6% 104.518 104.978 −0.460
+100bp 8.6% 98.849 99.219 −0.370
+300bp 6.1% 85.760 89.177 −3.417

The position loses on both sides: it shortens into the rally and extends into the sell-off. That is what a convexity of −363 means, and why a duration-hedged mortgage book still bleeds.

Static spread is not an OAS

static_spread solves for the one number added to every zero rate that reprices the pool, holding a single projected cash flow path fixed. That is a Z-spread. An OAS asks what spread survives once the cash flows are allowed to change down every rate path, which needs the simulation of Chapter 6. The engine is deliberately deterministic and does not claim to compute one.

Key rate durations

from mortgagekit import key_rate_durations

for t, d in key_rate_durations(pool, curve, PSA(200)):
    print(f"{t:>5}y  {d:+.3f}")

Because the curve is linear in zero rate, bumping a single knot produces the triangular hump key rate durations are defined against. With the prepayment model held still they sum to the static duration — exactly in the limit, and at a finite shock up to an $O(\Delta y^2)$ convexity term, since summing finite differences is not the finite difference of the sum.

Command line

python -m mortgagekit --balance 1000000 --wac 6.5 --coupon 6.0 --refi 6.5 --spread 75
python -m mortgagekit --psa 200 --json --schedule 12

Rates are entered the way people say them — 6.5 means 6.5% — and the spread is in basis points. --psa, --cpr and --refi select the prepayment model; --json emits the report as JSON, and --schedule N prints the first N months of projected cash flow.


Running Tests

To verify code correctness, you can run tests directly after installing development dependencies (if any are added) or via standard Python unittest.

PYTHONPATH=src python -m unittest discover -s tests

The engine's tests check results against closed forms and market conventions rather than against numbers this code once produced: that a zero prepayment speed reproduces the amortization schedule, that 100 PSA hits its published anchors, that principal returns in full at every speed, that premium pools lose and discount pools gain as speeds rise, that spread and yield solvers round-trip, and that key rate durations converge on the parallel shift as the shock shrinks.

Note: Code in this library contains typing definitions using standard PEP 484 type annotations and ships with the src/mortgagekit/py.typed marker, which enables automatic static analysis and autocompletion interfaces in modern IDE environments.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

mortgagekit-0.0.1.dev0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (127.5 kB view details)

Uploaded CPython 3.14manylinux: glibc 2.17+ x86-64manylinux: glibc 2.28+ x86-64

mortgagekit-0.0.1.dev0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (121.6 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64manylinux: glibc 2.28+ x86-64

mortgagekit-0.0.1.dev0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (121.9 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64manylinux: glibc 2.28+ x86-64

mortgagekit-0.0.1.dev0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (125.9 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64manylinux: glibc 2.28+ x86-64

mortgagekit-0.0.1.dev0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (95.8 kB view details)

Uploaded CPython 3.10manylinux: glibc 2.17+ x86-64manylinux: glibc 2.28+ x86-64

mortgagekit-0.0.1.dev0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (94.6 kB view details)

Uploaded CPython 3.9manylinux: glibc 2.17+ x86-64manylinux: glibc 2.28+ x86-64

File details

Details for the file mortgagekit-0.0.1.dev0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for mortgagekit-0.0.1.dev0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 190fc47e540579ded5a076badaf8ab9f676dc97637b6e59b24d29e2287258de5
MD5 b7badacdde660c3c1d13f3967876380f
BLAKE2b-256 c3f059848c7789558f12969efc15e43dcb11cf92c7479c95070461c0cca3db77

See more details on using hashes here.

File details

Details for the file mortgagekit-0.0.1.dev0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for mortgagekit-0.0.1.dev0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 0374f47b1eb2fbd8d60ad47f004e0011fd5f67100e2f4e2168aa013af4ba0ffc
MD5 7ff92ff0711ed3cfe52b5392aa4a52a1
BLAKE2b-256 e3da07fc6fc27aa01f6f26c79e9e08fcd91a36e1dbf17c2442c288301b1300bf

See more details on using hashes here.

File details

Details for the file mortgagekit-0.0.1.dev0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for mortgagekit-0.0.1.dev0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 df39a6774757a4f5685c2ec764399299b8325ed4ab4c760d3e31e4f9b3326a3b
MD5 e68e401122cca16397174f1e6c4cbae1
BLAKE2b-256 4240f2a141dff28d89506a3025846d575df5c0f8348f280dcef3a15a2e50c7f8

See more details on using hashes here.

File details

Details for the file mortgagekit-0.0.1.dev0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for mortgagekit-0.0.1.dev0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 1f7c5d6da84c24fb6f7ca07153d6f4ce7a102d11657982d23dfa7317b38c562a
MD5 3ddf207caad4d71aa336b016f13b10b1
BLAKE2b-256 f953c77dc6ccbbf5907f076d619aa38f737f8732093fb0eb4d14383c839f2657

See more details on using hashes here.

File details

Details for the file mortgagekit-0.0.1.dev0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for mortgagekit-0.0.1.dev0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 add2ad7064bf2cd867883e8ed346997630fcba766946f94b6d7d8e9fbca06046
MD5 1634ecb54d3228e4f5e19a909258c2d0
BLAKE2b-256 4405154eea1d92390e25d5c7b98a458eb4e3daa6f9d71b232938aa21426d374c

See more details on using hashes here.

File details

Details for the file mortgagekit-0.0.1.dev0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for mortgagekit-0.0.1.dev0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 44b0577ed3dc62fb0d2739ce95512c0d2f83488b01a0f03b0dd7bc292e35c00c
MD5 0066a67ee6b00a60d3f015bafe585d64
BLAKE2b-256 2346dda363e2f67d6f9e9ebff7254e80df82469e6f54fada991a33ef987158f5

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.0.1.dev0 This release

6 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page