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.
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 rate | Expected payment | FinancePy 1.1.2 | Final balance |
|---|---|---|---|
| 0 | 333.333333333333 | ZeroDivisionError | No completed schedule |
| 1e-15 | 333.333333333338 | ZeroDivisionError | No completed schedule |
| -1e-15 | 333.333333333328 | 250.199979298351 | 29,928.007452592137 |
| 1e-12 | 333.333333338347 | 333.599972407815 | -95.990065009659 |
| 0.035 (control) | 538.853625370589 | 538.853625370588 | 7.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
- Complete audit repository
- Submitted correction: FinancePy PR #257
- Released source
- Download the evidence ZIP (200,809 bytes)
Archive SHA-256: 02a53d8c6bd6f69127626bf8189dfbbd4086d89bba7807669179d51e5f183b8f. Source excerpts and patch retain GPL-3.0-or-later licensing.
