mortgagekit
A high-performance, pure-Python quantitative library for mortgage mathematics, prepayment modeling, cash-flow projections, and risk analytics. It serves as the companion computational engine for the book:
Mortgage Markets in the Age of AI
From Cash Flows, Stochastic Processes to AI Modeling: From Household Financial Decisions to Secondary Market MBS Pricing
Authors: Chiu Yan & Xuan Xin
Get the Book: Available on Amazon
Repository: https://github.com/henrywoo/mortgage-markets-in-the-age-of-ai
Key Features
- Zero Mandatory Dependencies: Core pricing, amortization, and risk engines are built entirely on Python's standard library — installs anywhere with zero compile or dependency friction.
- Loan-Level Amortization: Exact payment solvers, remaining balance formulas, and multi-period principal/interest amortization schedules.
- Prepayment Mechanics: Bidirectional $CPR \leftrightarrow SMM$ conversions, survival probability analytics, and dynamic speed models (flat CPR, PSA ramps, and rate-sensitive refinancing S-curves).
- Yield Curves & Discounting: Continuously compounded zero curves, knot bumping, par curve bootstrapping, present value, DV01, and CV01 analytics.
- Institutional Valuation & Risk: Cash-flow projection with servicing strips, effective duration, effective convexity, effective DV01, key rate durations, static (Z-)spread, and cash-flow yield.
- One-Call Analysis: Unified
analyze(...)pipeline that delivers a complete institutional risk and valuation report in Python or JSON. - Built-in CLI: Full command-line interface for terminal-based pool pricing, scenario analysis, and cash-flow scheduling.
- Fully Typed: Shipped with complete PEP 484 type annotations for IDE autocompletion and static analysis.
Installation
Install mortgagekit via pip:
pip install mortgagekit
Optional Extras
For research workflows, visualization, and advanced machine learning modeling, optional dependency bundles are available:
pip install mortgagekit[figures] # matplotlib, numpy, mathicon (figure generation)
pip install mortgagekit[ml] # torch, scikit-learn, scipy, xgboost (AI/ML models)
Quick Start
import mortgagekit as mk
# 1. Calculate monthly payment on a $400,000, 30-year mortgage at 6.5%
payment = mk.mortgage_payment(balance=400_000, annual_rate=0.065, months=360)
print(f"Monthly Payment: ${payment:.2f}")
# Monthly Payment: $2528.27
# 2. Convert a 12% annualized CPR into monthly SMM
smm = mk.cpr_to_smm(0.12)
print(f"Monthly SMM: {smm * 100:.4f}%")
# Monthly SMM: 1.0596%
# 3. Analyze a mortgage pool against a zero curve in one call
pool = mk.MortgagePool(balance=1_000_000, wac=0.065, wam=360, net_coupon=0.060)
curve = mk.Curve.from_zeros([(0.25, 0.043), (2, 0.041), (5, 0.042), (10, 0.045), (30, 0.047)])
report = mk.analyze(pool, curve, mk.PSA(200), spread=0.0075)
print(report.summary())
Core Capabilities & Mathematical Reference
1. Amortization and Loan Schedules
Contains analytical functions to compute fixed monthly payments, remaining loan balances, and multi-period amortization schedules.
Mathematical Formulation
For a self-amortizing fixed-rate mortgage with initial balance $B$, monthly interest rate $r_m = \text{annual_rate} / 12$, and total payment periods $N = \text{months}$, the fixed monthly payment $P$ is:
$$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$$
Python Example
from mortgagekit import mortgage_balance, mortgage_payment, mortgage_schedule
# Compute monthly payment on a $400,000, 30-year (360-month) loan at 6.5% interest
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 multi-period amortization schedule
schedule = mortgage_schedule(balance=400_000, annual_rate=0.065, months=360)
m1 = schedule[0]
print(f"Month {m1.month}: Payment=${m1.payment:.2f}, Interest=${m1.interest:.2f}, Principal=${m1.principal:.2f}, Balance=${m1.balance:.2f}")
2. Prepayment Conventions and Speed Models
Maps annualized prepayment rates into month-over-month conditional rates and models behavioral prepayment speeds.
Mathematical Formulation
The relationship between the annualized Conditional Prepayment Rate ($CPR$) and monthly Single Monthly Mortality ($SMM$) is:
$$SMM = 1 - (1 - CPR)^{\frac{1}{12}}$$
$$CPR = 1 - (1 - SMM)^{12}$$
Under a constant prepayment speed of $CPR$, the survival probability $S(t)$ representing the probability that a loan balance has not prepaid after $t$ months is:
$$S(t) = (1 - SMM)^t$$
Prepayment Models
mortgagekit provides three prepayment speed specifications sharing the uniform smm(age, rate_shift) interface:
FlatCPR(cpr): Constant prepayment speed across all periods.PSA(speed): Standard PSA ramp (100 PSA = 0.2% CPR increase per month up to 6.0% CPR at month 30, flat thereafter).RefiSCurve(wac, mortgage_rate): S-curve model that accelerates prepayment when market rates fall below the loan's coupon and burns out as rates rise.
Python Example
from mortgagekit import FlatCPR, PSA, RefiSCurve, 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")
# Evaluate survival probability after 36 months at 8% CPR
survival_pct = survival_probability(cpr=0.08, months=36)
print(f"Active balance ratio after 3 years: {survival_pct * 100:.2f}%")
# Instantiate prepayment speed models
flat_model = FlatCPR(0.06) # 6% constant CPR
psa_model = PSA(200) # 200% PSA benchmark
refi_model = RefiSCurve(wac=0.065, mortgage_rate=0.065) # Rate-responsive S-curve
3. Yields, Rates, and Risk Sensitivities
Evaluates continuous-compounding discount factors, bond prices, DV01 (dollar value of 1 basis point), and CV01 (convexity value of 1 basis point).
Mathematical Formulation
Under continuous compounding with rate $r$, the discount factor $d(t)$ at maturity time $t$ is:
$$d(t) = e^{-r t}$$
For a set of discrete future cash flows ${ (t_i, C_i) }_{i=1}^n$, the bond price $B(y)$ under flat yield $y$ is:
$$B(y) = \sum_{i=1}^n C_i e^{-y t_i}$$
The first-order price sensitivity DV01 represents the dollar price increase from a one-basis-point ($10^{-4}$) drop in yields:
$$\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 second-order curvature sensitivity CV01 represents the convexity adjustment for a one-basis-point 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}$$
Python Example
from mortgagekit import bond_price, cv01, discount_factor, dv01
# 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 discrete cash flows: list of (time_in_years, cashflow_amount)
cashflows = [
(0.5, 12_000.0),
(1.0, 12_000.0),
(1.5, 12_000.0),
(2.0, 112_000.0),
]
# Price cash flows under a flat 4.5% yield
price = bond_price(cashflows, y=0.045)
print(f"Bond Present Value: ${price:.2f}")
# Compute DV01 and CV01 risk metrics
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 1 bp decline")
print(f"CV01 Risk: ${convexity_value_01:.6f} curvature sensitivity")
The Valuation and Risk Engine
The valuation engine projects what a mortgage pool pays, discounts each cash-flow slice along a term-structure zero curve, and measures how pool value moves across yield curve shifts.
Projecting and Pricing a Mortgage Pool
from mortgagekit import Curve, MortgagePool, PSA, pool_cashflows, price, weighted_average_life
# Create a $1mm pool of 6.5% loans passing 6.0% through to investors after 50bp servicing
pool = MortgagePool(balance=1_000_000, wac=0.065, wam=360, net_coupon=0.060)
# Build a term structure from benchmark zero rate knots
curve = Curve.from_zeros([(0.25, 0.043), (2, 0.041), (5, 0.042), (10, 0.045), (30, 0.047)])
# Project monthly cash flows under 200 PSA
flows = pool_cashflows(pool, PSA(200))
print(f"Weighted Average Life (WAL): {weighted_average_life(flows):.2f} years")
print(f"Discounted Price: ${price(pool, curve, PSA(200)):,.0f}")
Each month, cash flows follow standard servicer accounting:
- Interest: Accrues on opening balance at the net coupon.
- Scheduled Principal: Recomputed from opening balance over remaining maturity.
- Prepayment: SMM applied to the balance remaining after scheduled principal.
- Total Principal: Scheduled principal + prepayment. Principal always returns in full — prepayment changes when money returns, never how much.
Full Institutional Risk Report in One Call
from mortgagekit import Curve, MortgagePool, RefiSCurve, analyze
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)])
report = analyze(pool, curve, RefiSCurve(wac=0.065, mortgage_rate=0.065), spread=0.0075)
print(report.summary())
Output:
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
Effective vs. Static Duration & Negative Convexity
A static duration differentiates the price of fixed, unchanging cash flows. An effective duration reprices the pool while allowing prepayment cash flows to adjust dynamically to the rate shock:
$$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 cash flows under the shocked yield curve. Holding cash flows static during rate shifts is the classic modeling mistake that hides mortgage negative convexity.
Priced at a 75 bp spread, compare a 6% pass-through with embedded prepayment options against the same pool with the option turned off:
| Rate Shock | CPR | Price (With Option) | Price (No Option) | Option Drag |
|---|---|---|---|---|
| −300 bp | 43.7% | 111.329 | 126.114 | −14.785 |
| −100 bp | 29.1% | 107.061 | 111.322 | −4.260 |
| 0 bp | 15.6% | 104.518 | 104.978 | −0.460 |
| +100 bp | 8.6% | 98.849 | 99.219 | −0.370 |
| +300 bp | 6.1% | 85.760 | 89.177 | −3.417 |
The mortgage pool loses on both sides: it shortens into rallies (upside capped) and extends into selloffs (losses compounded). That asymmetry is what negative convexity ($-363$) means, and why duration-hedged mortgage desks still bleed without convexity protection.
Static Spread (Z-Spread) vs. OAS
static_spread: Solves for the single constant spread over the zero curve that reprices the pool along a single deterministic cash-flow projection. This is a Z-spread.- OAS (Option-Adjusted Spread): The spread that survives once cash flows are simulated across hundreds of stochastic interest rate paths.
mortgagekit's engine is deliberately deterministic, exact, and fast — stochastic OAS simulation is covered in Chapter 6 of the book.
Key Rate Durations (KRD)
from mortgagekit import Curve, MortgagePool, PSA, key_rate_durations
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)])
for tenor, krd in key_rate_durations(pool, curve, PSA(200)):
print(f"{tenor:>5}y: {krd:+.3f}")
Bumping individual knots produces triangular basis shifts along the yield curve. With prepayment held constant, the key rate durations sum to the static duration, isolating curve steepening, flattening, and butterfly risks.
Command-Line Interface (CLI)
mortgagekit includes a terminal CLI executable via mortgagekit or python -m mortgagekit:
# Price a pool with a 6.5% WAC, 6.0% net coupon, 75 bp spread, and refi S-curve
mortgagekit --balance 1000000 --wac 6.5 --coupon 6.0 --refi 6.5 --spread 75
# Price a pool under 200 PSA and output the first 12 months schedule in JSON
mortgagekit --balance 1000000 --wac 6.5 --coupon 6.0 --psa 200 --schedule 12 --json
CLI Options
| Argument | Description | Default |
|---|---|---|
--balance |
Initial pool balance | 1000000 |
--wac |
Weighted average coupon (in %, e.g., 6.5) |
6.5 |
--wam |
Weighted average maturity in months | 360 |
--coupon |
Pass-through net coupon (in %, e.g., 6.0) |
6.0 |
--spread |
Spread over the zero curve in basis points | 0.0 |
--psa |
Select PSA prepayment model with given speed | — |
--cpr |
Select constant CPR prepayment model (in %) | — |
--refi |
Select refinance S-curve model with market rate (in %) | — |
--schedule N |
Print the first $N$ months of projected cash flows | — |
--json |
Output the complete report and schedules as structured JSON | — |
Verification & Type Safety
Verify your installation:
python -c "import mortgagekit as mk; print(f'mortgagekit {mk.__version__} ready')"
mortgagekit includes comprehensive PEP 484 type annotations, providing immediate IDE autocomplete and static type validation for financial engineering pipelines.
Documentation & Resources
- Book: Mortgage Markets in the Age of AI on Amazon
- Code & Examples: GitHub Repository
- Bug Reports & Inquiries: Please submit via GitHub Issues
Metadata
Release files for mortgagekit 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
Total release size: 844.1 kB
Release files / mortgagekit-0.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | mortgagekit-0.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 157.7 kB |
| Tags | CPython 3.14 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
0deae93e6fdf952ffe6be38c78832a6248c7a584a7ca4302aabd6fd671b39262
|
|
BLAKE2b-256 checksum How to use checksums |
cf50dee9ccd153382c739cb29d3a00b2ea47e5af8c51f260b3ec548bd18e01f3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.12
|
Release files / mortgagekit-0.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | mortgagekit-0.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 150.1 kB |
| Tags | CPython 3.13 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
01e748f51e2acc5296025b33d6f43322dd39c793f26470144bfc7b6aeda608b9
|
|
BLAKE2b-256 checksum How to use checksums |
10981db0d8db6bcec5093de494ac4374ea5a0db4d89dd64f9fd3241f278162f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.12
|
Release files / mortgagekit-0.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | mortgagekit-0.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 150.2 kB |
| Tags | CPython 3.12 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
b1ce46c2943e26a7cd3a8894110498d3c89d7e482308ef1520373fda6965553c
|
|
BLAKE2b-256 checksum How to use checksums |
7d23f78a823797bcd68e9a63784d1eac047ad68e519df56f46d7d5b5c550ba63
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.12
|
Release files / mortgagekit-0.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | mortgagekit-0.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 155.4 kB |
| Tags | CPython 3.11 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
8cc97a72e1a24fd475502f2112804cee2aed824d130e8302f94477406a516c50
|
|
BLAKE2b-256 checksum How to use checksums |
b084ecf29f6fd5cdb573173a0c7cc325317d7d506bcbd5070bd700f105d50d8b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.12
|
Release files / mortgagekit-0.0.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | mortgagekit-0.0.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 116.1 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
d6209a028e1ad2021705ea0dac756f5c8ff2fa01d55afbd5abf422a821f8fda3
|
|
BLAKE2b-256 checksum How to use checksums |
6326472298a80136afdcb42e98ab157a83778d62060a181199aa5a0ca2318d19
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.12
|
Release files / mortgagekit-0.0.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
| Download URL | mortgagekit-0.0.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 114.6 kB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-64 Linux glibc 2.28+ x86-64 |
|
SHA-256 checksum How to use checksums |
d9322431121b1e87d4619fc9c3b6f8d358ed23402239ae7b55e9e467ae65daca
|
|
BLAKE2b-256 checksum How to use checksums |
e00ff946dfd84f52411c4d67276a9c4c56f5f57402ecbd1c7ba2bb1e4415c9ee
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.12
|
Security for mortgagekit 0.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.