API reference

The explicit listings below cover every public function, dataclass, enum, and warning. conf.py enables autodoc_default_options = {"members": True}, so each autoclass directive auto-includes its members; we omit a top-level automodule directive to avoid duplicate index entries.

Functions

Convenience constructors

These helpers choose common, fixture-backed defaults and return ordinary validated LoanParams objects. Use LoanParams directly when you need every parameter explicitly.

mortgagemath.fixed_rate_mortgage(principal, annual_rate, term_years, *, payment_rounding=PaymentRounding.ROUND_UP, interest_rounding=PaymentRounding.ROUND_HALF_UP, balance_tracking=BalanceTracking.ROUND_EACH)[source]

Return LoanParams for a fully amortizing fixed-rate mortgage.

This is the generic easy-mode constructor for the common 30/360, monthly-payment case. Use LoanParams directly when you need non-monthly payments, ARMs, Actual/360, balloon terms, or other less common conventions.

Parameters:
Return type:

LoanParams

mortgagemath.us_30_year_fixed(principal, annual_rate, *, payment_rounding=PaymentRounding.ROUND_UP, interest_rounding=PaymentRounding.ROUND_HALF_UP, balance_tracking=BalanceTracking.ROUND_EACH)[source]

Return LoanParams for a standard US 30-year fixed-rate mortgage.

Parameters:
Return type:

LoanParams

mortgagemath.us_15_year_fixed(principal, annual_rate, *, payment_rounding=PaymentRounding.ROUND_UP, interest_rounding=PaymentRounding.ROUND_HALF_UP, balance_tracking=BalanceTracking.ROUND_EACH)[source]

Return LoanParams for a standard US 15-year fixed-rate mortgage.

Parameters:
Return type:

LoanParams

mortgagemath.canada_fixed_j2(principal, annual_rate, *, amortization_years=25, term_years=None, payment_frequency=PaymentFrequency.MONTHLY, payment_rounding=PaymentRounding.ROUND_HALF_UP, interest_rounding=PaymentRounding.ROUND_HALF_UP, balance_tracking=BalanceTracking.ROUND_EACH)[source]

Return LoanParams for a Canadian fixed-rate j_2 mortgage.

Canadian residential rates are commonly quoted as j_2: nominal annual rates compounded semi-annually under Interest Act convention. If term_years is provided and is shorter than the amortization, the returned loan models the fixed term as a balloon period on the longer amortization basis.

Parameters:
Return type:

LoanParams

mortgagemath.canada_accelerated_biweekly(principal, annual_rate, *, amortization_years=25, payment_rounding=PaymentRounding.ROUND_HALF_UP, interest_rounding=PaymentRounding.ROUND_HALF_UP, balance_tracking=BalanceTracking.ROUND_EACH)[source]

Return LoanParams for a Canadian Accelerated Bi-Weekly mortgage.

Calculated by taking the standard monthly j_2 payment, dividing it by two, and applying that amount every two weeks (26 times per year). This results in approximately one extra monthly payment per year, paying the loan off significantly faster than a 25-year monthly schedule.

Parameters:
Return type:

LoanParams

mortgagemath.us_actual_360_commercial(principal, annual_rate, *, term_years, amortization_years, start_date, payment_rounding=PaymentRounding.ROUND_HALF_UP, interest_rounding=PaymentRounding.ROUND_HALF_UP)[source]

Return LoanParams for a US Actual/360 commercial mortgage.

This preset matches the Fannie Mae Multifamily style: monthly payments, monthly compounding, Actual/360 interest accrual, and a possible balloon when term_years is shorter than amortization_years. start_date is required because Actual/360 schedules compute interest from the calendar days in each accrual period. Accepts either a datetime.date or an ISO-format string ("YYYY-MM-DD").

Parameters:
Return type:

LoanParams

mortgagemath.fixed_payment_mortgage(principal, annual_rate, payment, *, term_months, payment_rounding=PaymentRounding.ROUND_HALF_UP, interest_rounding=PaymentRounding.ROUND_HALF_UP, balance_tracking=BalanceTracking.CARRY_PRECISION)[source]

Return LoanParams for a fixed-rate loan with a chosen payment.

The returned loan uses payment_override: the schedule applies the chosen periodic payment until the final row trues up the remaining balance. The default carry-precision balance tracking is the historical given-payment convention validated against the FHLBB 1935 Direct-Reduction Plan A fixture. term_months is explicit rather than a year count because historical fixed-payment examples can end after non-whole-year terms such as 139 months.

Parameters:
Return type:

LoanParams

Calculations

mortgagemath.periodic_payment(loan)[source]

Calculate the periodic principal-and-interest payment for a loan.

Uses the standard annuity formula with Decimal arithmetic throughout:

r = periodic interest rate (per payment period)
n = number of payments over the amortization period
payment = P * r * (1+r)^n / ((1+r)^n - 1)

The result is rounded according to loan.payment_rounding.

The same closed-form formula applies to both 30/360 and Actual/360 loans: the day-count convention determines how the schedule accrues interest each period, not how the level periodic payment is computed. Validated against the Fannie Mae Multifamily Selling and Servicing Guide §1103 worked example ($25M / 5.5% / 30yr → DSC 6.8134680% → monthly P&I $141,947.25).

For non-monthly compounding (Canadian Interest Act §6 semi-annual quoting; or annual compounding) the periodic rate is derived from the quoted annual rate; see _periodic_rate().

Parameters:

loan (LoanParams) – Loan parameters.

Returns:

Periodic P&I payment rounded to the loan’s currency unit.

Raises:

ValueError – If principal, term_months, or annual_rate is not positive, or the day_count is unsupported.

Return type:

Decimal

mortgagemath.monthly_payment(loan)

Calculate the periodic principal-and-interest payment for a loan.

Uses the standard annuity formula with Decimal arithmetic throughout:

r = periodic interest rate (per payment period)
n = number of payments over the amortization period
payment = P * r * (1+r)^n / ((1+r)^n - 1)

The result is rounded according to loan.payment_rounding.

The same closed-form formula applies to both 30/360 and Actual/360 loans: the day-count convention determines how the schedule accrues interest each period, not how the level periodic payment is computed. Validated against the Fannie Mae Multifamily Selling and Servicing Guide §1103 worked example ($25M / 5.5% / 30yr → DSC 6.8134680% → monthly P&I $141,947.25).

For non-monthly compounding (Canadian Interest Act §6 semi-annual quoting; or annual compounding) the periodic rate is derived from the quoted annual rate; see _periodic_rate().

Parameters:

loan (LoanParams) – Loan parameters.

Returns:

Periodic P&I payment rounded to the loan’s currency unit.

Raises:

ValueError – If principal, term_months, or annual_rate is not positive, or the day_count is unsupported.

Return type:

Decimal

mortgagemath.amortization_schedule(loan)[source]

Generate a full amortization schedule.

For 30/360 loans the schedule uses bank-style round-each-balance accounting:

  1. The periodic payment is computed once and rounded.

  2. Each period’s interest is computed on the (rounded) remaining balance and rounded to the cent.

  3. Principal is the difference: payment - interest.

  4. The final payment is adjusted so the balance lands exactly at zero.

For Actual/360 loans the schedule uses Fannie Mae §1103 conventions:

  1. loan.start_date is required; period 1 covers the calendar month containing the start date.

  2. The unrounded closed-form payment is carried internally; each month’s interest is computed in full Decimal precision as balance * annual_rate * days_in_month / 360.

  3. Display values (payment, interest, principal) are rounded to the cent; the running balance internally is unrounded.

  4. The final payment is adjusted to land balance at exactly zero.

Both modes guarantee principal + interest == payment for every installment and a final balance of exactly $0.00.

For very small principals the cent-rounded periodic payment can overpay the closed-form value by enough that the loan amortizes before the requested term. In that case the schedule is truncated at the actual payoff period and an EarlyPayoffWarning is emitted; len(schedule) will be smaller than total_payments + 1.

Parameters:

loan (LoanParams) – Loan parameters.

Returns:

A list of Installment objects from period 0 (initial state showing the starting balance) through the final payment.

Raises:

ValueError – If principal or term_months is not positive, the day_count is unsupported, or ACTUAL_360 is requested without a start_date.

Warns:

EarlyPayoffWarning – When 30/360 round-each-balance accounting amortizes the loan before its scheduled end due to periodic payment overpayment from rounding.

Return type:

list[Installment]

mortgagemath.loan_summary(loan)[source]

Compute a high-level summary of a loan’s cost and structure.

This is a convenience wrapper that calls periodic_payment() and amortization_schedule() internally and extracts the values most users need first.

Parameters:

loan (LoanParams) – Loan parameters.

Returns:

A LoanSummary with the periodic payment, total interest, total paid, total fees, balloon balance, total cost, and number of payments.

Return type:

LoanSummary

Example:

>>> from mortgagemath import us_30_year_fixed, loan_summary
>>> s = loan_summary(us_30_year_fixed("300000", "6.5"))
>>> s.periodic_payment
Decimal('1896.21')
>>> s.total_interest
Decimal('382628.90')
>>> s.total_cost
Decimal('682628.90')

Dataclasses

class mortgagemath.LoanSummary(periodic_payment, total_paid, total_interest, total_fees, total_principal, balloon_balance, total_cost, num_payments)[source]

Bases: object

High-level summary of a loan’s cost and structure.

Returned by loan_summary(). All monetary values are Decimal at the loan’s currency-unit precision.

Parameters:
periodic_payment: Decimal

Level payment per period (excludes fee_per_period).

total_paid: Decimal

Sum of all scheduled payments over the life of the loan (includes fees). For balloon loans this does NOT include the balloon balance — see total_cost.

total_interest: Decimal

Cumulative interest paid over the life of the loan.

total_fees: Decimal

Cumulative fees paid over the life of the loan.

total_principal: Decimal

Principal repaid through scheduled payments. For fully amortizing loans this equals the original principal. For balloon loans this is less than the original principal by the balloon amount.

balloon_balance: Decimal

Remaining balance at the end of the term. Zero for fully amortizing loans; the balloon amount for balloon loans.

total_cost: Decimal

Total cash required to extinguish the debt at term: total_paid + balloon_balance. For fully amortizing loans this equals total_paid.

num_payments: int

Number of payments in the schedule (excludes the initial balance row).

class mortgagemath.LoanParams(principal, annual_rate, term_months, day_count=DayCount.THIRTY_360, payment_rounding=PaymentRounding.ROUND_UP, interest_rounding=PaymentRounding.ROUND_HALF_UP, start_date=None, amortization_period_months=None, balance_tracking=BalanceTracking.ROUND_EACH, compounding=Compounding.MONTHLY, payment_frequency=PaymentFrequency.MONTHLY, rate_schedule=(), payment_override=None, currency_unit=Decimal('0.01'), interest_only_months=0, fee_per_period=Decimal('0'))[source]

Bases: object

Parameters defining a fixed-rate mortgage.

Parameters:
  • principal (Decimal) – Original loan amount (e.g. Decimal(“131250.00”)).

  • annual_rate (Decimal) – Annual interest rate as a percentage (e.g. Decimal(“4.25”) for 4.25%).

  • term_months (int) – Loan term in months — the number of payments the schedule will produce (e.g. 360 for a 30-year fully amortizing residential mortgage; 120 for a 10-year SARM).

  • day_count (DayCount) – Day-count convention. Defaults to 30/360.

  • payment_rounding (PaymentRounding) – How to round the monthly payment to the nearest cent. Defaults to ROUND_UP.

  • interest_rounding (PaymentRounding) – How to round each month’s interest charge. Defaults to ROUND_HALF_UP.

  • start_date (date | None) – Date of the first interest-accrual period (the issue date). Required for ACTUAL_360 schedules; ignored for 30/360. Period 1 covers the calendar month containing this date and the first payment is due on the same day of the next month. Per Fannie Mae §1103, an issue date of December 1, 2018 produces a first payment on January 1, 2019 with period 1 spanning December 2018 (31 days).

  • amortization_period_months (int | None) – Period over which the closed-form payment is computed. None (the default) means it equals term_months — i.e., the loan amortizes fully over its term. Set this larger than term_months for balloon loans: the level monthly payment is computed as if the loan amortized over amortization_period_months, but the schedule terminates after term_months payments, with the unpaid principal at term equal to Installment.balance on the final row (this is the borrower’s balloon payment). Per the Fannie Mae §1103 Tier 2 SARM example, a 10-year ($120 months) SARM on a 30-year (360 months) amortization basis is expressed as term_months=120, amortization_period_months=360; the published balloon at term 120 is $20,885,505.83.

  • balance_tracking (BalanceTracking) – How the running balance propagates between rows. ROUND_EACH (default) is the round-each-month convention used by US residential lenders and validated against CFPB sample disclosures. CARRY_PRECISION is the Excel-default convention used by graduate-level CRE finance textbooks (Geltner, LibreTexts, eCampus). Ignored for ACTUAL_360 day count, which always uses carry-precision.

  • compounding (Compounding) – How the annual rate compounds to a periodic rate. MONTHLY (default, US convention) divides the annual rate by payments_per_year. SEMI_ANNUAL is the Canadian convention required by Interest Act §6. ANNUAL treats the quoted rate as the effective annual rate.

  • payment_frequency (PaymentFrequency) – How often payments are made. Defaults to MONTHLY (12/yr). Other supported cadences are SEMI_MONTHLY (24/yr), BIWEEKLY (26/yr), WEEKLY (52/yr), QUARTERLY (4/yr), and ANNUAL (1/yr). term_months * payments_per_year must be divisible by 12.

  • payment_override (Decimal | None) – When set, pin the periodic payment to this value (in the loan’s currency) instead of deriving it from the closed-form annuity formula. The schedule’s final row absorbs whatever residual balance remains after term_months - 1 full payments — the final payment is balance_before_final + final_period_interest rounded once to cents. This unlocks the historical “given-payment, find-term” convention used by the FHLBB Federal Home Loan Bank Review of March 1935 (Direct-Reduction Plan A: $3,000 / 6% / $30 monthly / 138 full payments + 139th of $29.27). Defaults to None (use the closed-form value). Currently incompatible with non-empty rate_schedule.

  • interest_only_months (int) – Number of months at the start of the loan where only interest is paid. After this period, the loan recasts and amortizes over the remaining term. Defaults to 0.

  • currency_unit (Decimal) – The smallest monetary unit for quantization. Defaults to Decimal("0.01") (cents for USD, EUR, GBP, etc.). Set to Decimal("1") for zero-decimal currencies like JPY or KRW where the base unit is the smallest denomination. Must be a positive power of 10, at most 1.

  • fee_per_period (Decimal) – Flat amount added to each installment’s payment on top of the closed-form interest+principal value. Models the structure of the 1852 Crédit Foncier de France annuité (which embedded frais d’administration + fonds de réserve + impôt as a constant loading on top of the actuarial interest+amortissement) and the modern French tableau d’amortissement convention of pricing assurance emprunteur as taux_d_assurance * original_principal paid as a flat amount per period. Closed-form payment derivation, balance accounting, and rate-schedule recasts are all unaffected: the fee rides on top. Defaults to Decimal("0") (no loading; behavior identical to v0.6.0).

  • rate_schedule (tuple[RateChange, ...])

principal: Decimal
annual_rate: Decimal
term_months: int
day_count: DayCount
payment_rounding: PaymentRounding
interest_rounding: PaymentRounding
start_date: date | None
amortization_period_months: int | None
balance_tracking: BalanceTracking
compounding: Compounding
payment_frequency: PaymentFrequency
rate_schedule: tuple[RateChange, ...]
payment_override: Decimal | None
currency_unit: Decimal
interest_only_months: int
fee_per_period: Decimal
class mortgagemath.RateChange(effective_payment_number, new_annual_rate, recast=True, payment_cap_factor=None)[source]

Bases: object

A scheduled rate change for an Adjustable-Rate Mortgage (ARM).

Used in LoanParams.rate_schedule (a tuple of RateChange). Goldstein 12e §10.4 Example 13 5/1 ARM is the canonical fixture: RateChange(effective_payment_number=61, new_annual_rate=Decimal("7.2")) moves the rate from the initial 5.7% to 7.2% starting at payment 61 and recasts the level payment over the remaining 300 payments.

Parameters:
  • effective_payment_number (int) – 1-indexed payment ordinal at which the new rate first takes effect. Interest accrual for that payment uses the new rate. Must be >= 2 (a rate change at payment 1 is the same as constructing the loan with that initial rate).

  • new_annual_rate (Decimal) – The new annual rate in percent (e.g. Decimal("7.2")). Must be positive.

  • recast (bool) – When True (the default), recompute the level payment for the remaining payments using the new periodic rate applied to the current balance. When False, keep the prior level payment — the rate change still affects every subsequent period’s interest accrual, and any residual is absorbed by the final-row trueup.

  • payment_cap_factor (Decimal | None) – Optional bound on the recast payment as a multiple of the prior period’s payment (e.g. Decimal("1.075") for a 7.5% annual cap). Only meaningful when recast=True. When set, the new payment is min(closed_form_recast, prior_payment * cap_factor). If the cap binds and the new periodic interest exceeds the capped payment, the unpaid interest is capitalized into the balance (negative amortization) — the corresponding Installment.principal will be negative and the balance will grow. Validated against the ProEducate ARM payment- cap example ($65,000 / 10% Year 1 → 12% Year 2 / 7.5% cap; year-2 P&I $613.20, cumulative neg-am $420.90).

effective_payment_number: int
new_annual_rate: Decimal
recast: bool
payment_cap_factor: Decimal | None
class mortgagemath.Installment(number, payment, interest, principal, total_interest, balance, fee=Decimal('0.00'))[source]

Bases: object

A single payment in an amortization schedule.

Invariant: principal + interest + fee == payment for every installment. When LoanParams.fee_per_period is the default Decimal("0"), fee is also Decimal("0.00") and the invariant collapses to the historical principal + interest == payment form.

The fee field models the structure of fee-loaded annuités (Crédit Foncier de France 1852+; modern French tableau d’amortissement with assurance emprunteur loaded into the total échéance). Consumers that need the actuarially-pure interest+principal value can recover it as payment - fee; consumers that need the gross figure the borrower wrote on the check use payment directly.

Parameters:
number: int
payment: Decimal
interest: Decimal
principal: Decimal
total_interest: Decimal
balance: Decimal
fee: Decimal

Enums

class mortgagemath.DayCount(*values)[source]

Bases: Enum

Day-count convention for interest calculation.

US residential mortgages typically use 30/360. US commercial loans often use Actual/360.

THIRTY_360 = '30/360'
ACTUAL_360 = 'actual/360'
class mortgagemath.PaymentRounding(*values)[source]

Bases: Enum

Rounding convention for monetary amounts.

ROUND_UP (ceiling to nearest unit) is used by most US lenders for the monthly payment amount. ROUND_DOWN (truncate toward zero) matches published yen/won calculator schedules that floor monetary values to the currency unit. ROUND_HALF_UP (standard rounding) is used for monthly interest calculations. ROUND_HALF_EVEN (banker’s rounding) is included so fixtures from lenders or worked examples that use it can be modeled.

ROUND_UP = 'ROUND_UP'
ROUND_DOWN = 'ROUND_DOWN'
ROUND_HALF_UP = 'ROUND_HALF_UP'
ROUND_HALF_EVEN = 'ROUND_HALF_EVEN'
class mortgagemath.BalanceTracking(*values)[source]

Bases: Enum

How the running balance is tracked between schedule rows.

ROUND_EACH (the default) is how most US residential lender statements are computed: each month’s balance is rounded to cents, and next month’s interest is computed from that rounded balance. Validated against CFPB sample disclosures and most published bank statements.

CARRY_PRECISION carries the unrounded balance internally between rows and rounds only for display. This is the Excel-default behavior and the convention used by graduate-level CRE finance textbooks (Geltner et al., LibreTexts Olivier, eCampus Ontario). For long-horizon loans the two algorithms diverge by single-digit cents at each row, with the displayed total interest sometimes drifting by a few dollars over a 30-year term.

The ACTUAL_360 day-count always uses CARRY_PRECISION internally regardless of this field, because day-counted interest accrual is only meaningful with full balance precision.

ROUND_EACH = 'round_each'
CARRY_PRECISION = 'carry_precision'
class mortgagemath.Compounding(*values)[source]

Bases: Enum

How the annual rate compounds to a periodic rate.

MONTHLY (the default) is the US residential / commercial convention: the periodic rate is annual_rate / payments_per_year treated as a simple division. For monthly payments under monthly compounding this is exactly the v0.2.x behavior.

SEMI_ANNUAL is the Canadian convention (Interest Act §6): the quoted rate j_2 is per year compounded semi-annually, and the periodic rate per payment is (1 + j_2 / 200) ** (2 / payments_per_year) - 1. For a 5% Canadian mortgage with monthly payments this is (1.025) ** (1/6) - 1 0.41239...%, not 5/12.

ANNUAL treats the quoted rate as the effective annual rate; the periodic rate is (1 + annual / 100) ** (1 / payments_per_year) - 1. Included for completeness.

MONTHLY = 'monthly'
SEMI_ANNUAL = 'semi_annual'
ANNUAL = 'annual'
class mortgagemath.PaymentFrequency(*values)[source]

Bases: Enum

How often payments are made.

The payments_per_year mapping is the standard banking convention used by the worked-example sources the library validates against (e.g. the Canadian Olivier and eCampus Ontario texts use exactly 52 weekly / 26 biweekly / 24 semi-monthly periods per year).

MONTHLY = 'monthly'
SEMI_MONTHLY = 'semi_monthly'
BIWEEKLY = 'biweekly'
WEEKLY = 'weekly'
QUARTERLY = 'quarterly'
ANNUAL = 'annual'
property payments_per_year: int

Number of payments per calendar year.

Warnings

class mortgagemath.EarlyPayoffWarning[source]

Bases: UserWarning

Schedule terminated before term_months due to rounding overpayment.

For very small principals, the cent-rounded monthly payment can overpay the closed-form value by enough that the schedule fully amortizes before the requested term. When this happens, amortization_schedule truncates the schedule at the actual payoff month (with the final row trued up to land balance at exactly zero) and emits this warning.

Filter via the standard warnings machinery:

import warnings
from mortgagemath import EarlyPayoffWarning

warnings.filterwarnings("ignore", category=EarlyPayoffWarning)
# or, to promote it to an exception:
warnings.simplefilter("error", EarlyPayoffWarning)