Quickstart

A 30-year fixed-rate residential mortgage

from mortgagemath import us_30_year_fixed, periodic_payment, amortization_schedule

loan = us_30_year_fixed("300000", "6.5")
pmt = periodic_payment(loan)         # Decimal("1896.21")
sched = amortization_schedule(loan)
print(sched[1].interest)             # Decimal("1625.00")
print(sched[1].principal)            # Decimal("271.21")
print(sched[-1].balance)             # Decimal("0.00") — exact closure

Returns a cent-accurate periodic payment and a lender-style amortization schedule that lands exactly at $0.00 on the final payment. The schedule is a plain list[Installment]sched[0] is the initial balance (no payment), sched[1] through sched[360] are the monthly payments.

# Useful schedule queries
print(sched[-1].total_interest)      # Decimal("382628.90") — lifetime interest
print(len(sched) - 1)               # 360 — number of payments

The convenience constructors return ordinary LoanParams objects. Use LoanParams directly when you need an uncommon configuration, or pass optional rounding overrides when a published source requires them.

Quick loan summary

For the headline numbers without iterating the schedule:

from mortgagemath import us_30_year_fixed, loan_summary

s = loan_summary(us_30_year_fixed("300000", "6.5"))
print(s.periodic_payment)     # Decimal("1896.21")
print(s.total_interest)       # Decimal("382628.90")
print(s.total_paid)           # Decimal("682628.90")
print(s.num_payments)         # 360
print(s.balloon_balance)      # Decimal("0.00") — fully amortizing
print(s.total_cost)           # Decimal("682628.90") — same as total_paid

For balloon loans, total_paid is the sum of scheduled payments only; total_cost adds the balloon balance — the total cash required to extinguish the debt at maturity:

from datetime import date
from mortgagemath import us_actual_360_commercial, loan_summary

# 10-year term on a 30-year amortization basis — balloon at term.
loan = us_actual_360_commercial(
    "25000000", "5.5",
    term_years=10,
    amortization_years=30,
    start_date=date(2018, 12, 1),
)
s = loan_summary(loan)
print(s.balloon_balance)      # Decimal("20885505.83")
print(s.total_cost)           # total_paid + balloon

Pandas integration

import pandas as pd
from mortgagemath import us_30_year_fixed, amortization_schedule

loan = us_30_year_fixed("300000", "6.5")
df = pd.DataFrame(amortization_schedule(loan))
print(df[["number", "payment", "interest", "principal", "balance"]].head())

Canadian semi-annual mortgages

The Canadian Interest Act §6 quotes rates as semi-annually compounded. The canada_fixed_j2 helper chooses that convention:

from mortgagemath import canada_fixed_j2, periodic_payment

# Canadian 5-year-term mortgage on a 25-year amortization basis,
# monthly payments at j_2 = 5%.
loan = canada_fixed_j2("300000", "5", amortization_years=25, term_years=5)
print(periodic_payment(loan))              # Decimal("1744.81")

See Vignettes for a full Canadian-mortgage walkthrough.

Adjustable-rate mortgages

ARMs are modelled via a tuple of RateChange entries:

from decimal import Decimal
from mortgagemath import LoanParams, RateChange, PaymentRounding, amortization_schedule

# 5/1 ARM: $200,000 at 5.7%, single rate change at month 61 to 7.2%.
loan = LoanParams(
    principal=Decimal("200000"),
    annual_rate=Decimal("5.7"),
    term_months=360,
    payment_rounding=PaymentRounding.ROUND_HALF_UP,
    interest_rounding=PaymentRounding.ROUND_HALF_UP,
    rate_schedule=(
        RateChange(effective_payment_number=61, new_annual_rate=Decimal("7.2")),
    ),
)
sched = amortization_schedule(loan)
print(sched[60].payment)                   # Decimal("1160.80") — initial
print(sched[61].payment)                   # Decimal("1334.16") — recast

RateChange also supports an optional payment_cap_factor for payment-capped ARMs with negative amortization. See the Vignettes page for the full Reg Z H-14 and ProEducate walkthroughs.

Commercial Actual/360 with a balloon

from datetime import date
from mortgagemath import (
    amortization_schedule,
    periodic_payment,
    us_actual_360_commercial,
)

# Fannie Mae §1103 Tier 2 SARM: $25M at 5.5%, 10-year term on a
# 30-year amortization basis, Actual/360 day count.
loan = us_actual_360_commercial(
    "25000000",
    "5.5",
    term_years=10,
    amortization_years=30,
    start_date=date(2018, 12, 1),
)
sched = amortization_schedule(loan)
print(periodic_payment(loan))              # Decimal("141947.25")
print(sched[120].balance)                  # Decimal("20885505.83") — balloon at term

Pinning the payment (FHLBB 1935 given-payment convention)

Pre-1968 American building-and-loan practice often chose a round periodic payment (typically 1% of original principal per month) and accepted whatever final-payment trueup the math produced. The payment_override field reproduces this:

from mortgagemath import LoanParams, BalanceTracking, PaymentRounding

# FHLBB Federal Home Loan Bank Review (March 1935) Plan A:
# $3,000 / 6% / payment chosen as 1% = $30 / month.
loan = LoanParams(
    principal=Decimal("3000.00"),
    annual_rate=Decimal("6"),
    term_months=139,
    payment_rounding=PaymentRounding.ROUND_HALF_UP,
    interest_rounding=PaymentRounding.ROUND_HALF_UP,
    balance_tracking=BalanceTracking.CARRY_PRECISION,
    payment_override=Decimal("30.00"),
)
sched = amortization_schedule(loan)
print(sched[138].payment)   # Decimal("30.00")
print(sched[139].payment)   # Decimal("29.27") — final-row trueup

Fee-loaded French schedule

A fee_per_period field on LoanParams adds a flat amount per period to each Installment.payment. It models the modern French tableau d’amortissement convention of pricing assurance emprunteur as taux × original_principal paid as a flat amount per month.

loan = LoanParams(
    principal=Decimal("10000"),
    annual_rate=Decimal("5"),
    term_months=12,
    payment_rounding=PaymentRounding.ROUND_HALF_UP,
    interest_rounding=PaymentRounding.ROUND_HALF_UP,
    fee_per_period=Decimal("2.92"),  # MoneyVox 0.35% assurance / 12
)
print(periodic_payment(loan))         # Decimal("856.07") — pure P+I
sched = amortization_schedule(loan)
print(sched[1].payment)               # Decimal("858.99") — gross mensualité
print(sched[1].fee)                   # Decimal("2.92")

The closed-form periodic_payment(loan) continues to return the actuarially-pure interest+principal value; the fee rides on top in Installment.payment, with Installment.fee exposing the loading separately.

Command line

# Just the periodic payment
mortgagemath payment --principal 131250 --rate 4.25 --term-months 360

# Full schedule as table (default), CSV, or JSON
mortgagemath schedule --principal 131250 --rate 4.25 --term-months 360 \
    --format csv > schedule.csv

# ARM with rate change at month 61
mortgagemath schedule --principal 200000 --rate 5.7 --term-months 360 \
    --payment-rounding ROUND_HALF_UP --interest-rounding ROUND_HALF_UP \
    --rate-change 61:7.2 --format json

# FHLBB 1935 Plan A — pinned payment with final-row trueup
mortgagemath schedule --principal 3000 --rate 6 --term-months 139 \
    --payment-rounding ROUND_HALF_UP --interest-rounding ROUND_HALF_UP \
    --balance-tracking carry_precision --payment-override 30 --format table

# Fee-loaded French assurance schedule
mortgagemath schedule --principal 10000 --rate 5 --term-months 12 \
    --payment-rounding ROUND_HALF_UP --interest-rounding ROUND_HALF_UP \
    --fee-per-period 2.92 --format table

mortgagemath --help and mortgagemath <subcommand> --help document the full flag surface.