← Research index
INDEPENDENT NUMERICAL AUDIT13 September 2026

FinancePy mortgage payments fail at zero and very small rates

A zero-interest mortgage has a simple payment: principal divided by the number of periods. FinancePy 1.1.2 raises an exception instead. Near zero, cancellation in the annuity formula can also prevent complete repayment.

Xamit Kadirbekov
Xamit KadirbekovIndependent verification · GERO Research
Financial calculationsFinancePy 1.1.2Amortization
STATUS · CORRECTION SUBMITTEDReproduced from the released wheel and checked against pinned upstream source. PR #257 is the authoritative source for review and CI status.

A payment that leaves a balance

The synthetic mortgage starts on 1 January 2025, matures on 1 January 2055 and has principal 120,000. Its schedule contains 360 monthly payments. All rates below are annual fractions.

Annual rateExpected paymentFinancePy 1.1.2Final balance
0333.333333333333ZeroDivisionErrorNo completed schedule
1e-15333.333333333338ZeroDivisionErrorNo completed schedule
-1e-15333.333333333328250.19997929835129,928.007452592137
1e-12333.333333338347333.599972407815-95.990065009659
0.035 (control)538.853625370589538.8536253705887.90e-10

These are unrounded floating-point cash flows. The report does not impose a currency rounding policy. The independent oracle sums discounted unit payments at 80-digit Decimal precision, then divides principal by that sum.

Where the digits disappear

BondMortgage.repayment_amount() calculates p = (1 + rate/frequency)**periods, then divides by p - 1. At zero this is 0/0. Near zero, the addition and subtraction lose the information needed for an accurate payment. generate_flows(..., REPAYMENT) uses that payment throughout the amortization schedule.

from financepy.products.bonds.bond_mortgage import BondMortgage
from financepy.utils.date import Date

m = BondMortgage(Date(1, 1, 2025), Date(1, 1, 2055), 120000.0)
print(m.repayment_amount(0.0))
# Expected: 333.3333333333333
# Released 1.1.2: ZeroDivisionError

Correction and falsification checks

The correction uses the exact zero-rate limit and log1p/expm1 for nonzero rates. Equivalent positive- and negative-rate expressions avoid unnecessary exponential overflow. Dates, calendar rules and the interest-only path are preserved. Periodic rates at or below -1 are rejected because they do not define positive discount factors.

  • 144 native-API scenarios across four frequencies, three terms and twelve rates: 108 failures before, zero after.
  • 64 focused tests pass, including the existing mortgage tests and independent payment/amortization checks.
  • 1020 local unit tests pass, with four warnings.
  • Restoring the original implementation makes 40 new repayment checks fail; the 16 ordinary-rate controls pass. The corrected source was restored and verified.

All numerical work ran sequentially on CPU with one numerical thread. GPU behavior and performance were not benchmarked.

Evidence boundaries

The ordinary 3.5% mortgage rate passes in the released version. Small nonzero rates isolate the instability; zero interest gives an exact boundary condition. The examples establish a library calculation defect, without establishing deployed lender exposure, customer losses or security impact.

Released version 1.1.2 and upstream commit 2b9227fea9d832c4033421d6cd53a54316414fca contain the identical mortgage source file. Public issue and pull-request searches found no matching report within the searched title/body scope; that is bounded duplicate screening, not proof of worldwide priority.

Source and reproducible package

Archive SHA-256: 02a53d8c6bd6f69127626bf8189dfbbd4086d89bba7807669179d51e5f183b8f. Source excerpts and patch retain GPL-3.0-or-later licensing.