Amortisation schedule (loans.amortisation_schedule)¶
Returns the amortisation schedule of a loan: for every period the instalment, the interest portion, the principal portion and the outstanding balance, with the totals and the final-period adjustment that brings the balance exactly to zero. Three amortisation types are available, with an optional initial interest-only period and an optional balloon amount. Use it to feed the interest of each year and the outstanding debt into a business plan and the net financial position, or to reconcile a schedule with the bank's.
Amortisation types¶
The names are those of the European credit reporting regulation (Regulation (EU) 2016/867, Annex IV, "amortisation type"):
| Type | What stays constant | Italian practice |
|---|---|---|
french |
the instalment: the total of principal and interest is the same in each instalment | ammortamento alla francese (a rata costante) |
fixed_amortisation_schedule |
the principal portion: the principal repaid in each instalment is the same | ammortamento all'italiana (a quota capitale costante) |
bullet |
nothing but the interest: the full principal is repaid with the last instalment | bullet |
Formula¶
With the periodic rate \(i\), \(n\) instalments (the full term), \(g\) interest-only periods at the start (principal grace period) and a balloon amount \(B\) repaid with the last instalment, each period \(k\) starts from the opening balance \(D_{k-1}\) (\(D_0 = P\)):
The instalment of period \(k\) is \(I_k + C_k\). For french, \(A\) is the constant instalment of
loans.instalment over the \(n - g\) periods after the grace period, with
the balloon amount (the same implementation):
Last period. The principal portion of period \(n\) is the opening balance \(D_{n-1}\), balloon
amount included, so the final outstanding balance is exactly zero. The difference between the
last instalment, less the balloon amount, and the regular instalment of that period is
reported as final_adjustment. Without rounding it is zero up to the last digits of the
arithmetic; with rounding it absorbs the accumulated rounding.
Rounding. When chosen, rounding applies to the amount the type keeps constant, so the type keeps its meaning: the instalment of a French schedule, the principal portion of a fixed amortisation schedule. Interest portions are never rounded. A bullet schedule has no constant amount to round other than its interest, so rounding does not change it.
Instalment \(k\) is paid at the end of period \(k\) (time \(k\)): this method version supports payments in arrears only.
Example¶
The worked example of a business loan in Watts, Business and Financial Mathematics, section 6.1: 50,000 at 5% compounded quarterly, repaid by six semi-annual instalments rounded to the cent. The periodic rate is \(1.0125^2 - 1 = 0.02515625\), computed through the series functions of the rate conversion, so it is exact to about \(10^{-27}\):
from decimal import Decimal
import fairmark as fm
loan = fm.loans.amortisation_schedule(
50000, 6, rate="0.05", compounding="quarterly", payment_frequency="semi_annual",
amortisation_type="french", timing="ordinary", decimal_places=2, rounding_mode="half_up",
)
assert round(loan.periodic_rate, 20) == Decimal("0.02515625") # series functions: 1e-27
first, last = loan.rows[0], loan.rows[-1]
assert first["instalment"] == Decimal("9082.24")
assert round(first["interest_portion"], 20) == Decimal("1257.8125") # not rounded
assert round(last["instalment"], 2) == Decimal("9082.26") # the book's final payment
assert last["outstanding_balance"] == 0
assert round(loan.final_adjustment, 4) == Decimal("0.0171")
assert round(loan.totals["interest"], 2) == Decimal("4493.46")
# Italian-style constant principal: 120,000 over 8 quarters at 6% compounded quarterly.
fixed = fm.loans.amortisation_schedule(
120000, 8, rate="0.06", compounding="quarterly", payment_frequency="quarterly",
amortisation_type="fixed_amortisation_schedule", timing="ordinary",
)
assert [row["principal_portion"] for row in fixed.rows] == [Decimal(15000)] * 8
assert fixed.totals["interest"] == Decimal(8100)
# Two interest-only months, then a French schedule with a balloon of 20% of the principal.
grace = fm.loans.amortisation_schedule(
100000, 60, rate="0.048", compounding="monthly", payment_frequency="monthly",
amortisation_type="french", timing="ordinary", principal_grace_periods=2,
balloon_amount=20000,
)
assert grace.rows[0]["instalment"] == Decimal(400)
assert grace.rows[-1]["balloon_amount"] == Decimal(20000)
assert abs(grace.final_adjustment) < Decimal("1e-18")
Inputs¶
| Field | Meaning | Default |
|---|---|---|
principal |
Amount borrowed, currency units, greater than 0 | required |
rate, compounding, payment_frequency |
Annual borrowing rate with its compounding and the instalments per year (form a) | one rate form required |
periodic_rate |
Rate per payment period, decimal fraction, greater than −1 (form b) | one rate form required |
number_of_instalments |
Full term, grace periods included, from 1 to 10000 | required |
timing |
ordinary (in arrears); due is not accepted by version 1 |
required |
amortisation_type |
french, fixed_amortisation_schedule or bullet |
required |
principal_grace_periods |
Interest-only periods at the start, lower than number_of_instalments; not with bullet |
0 |
balloon_amount |
Principal repaid with the last instalment, \(0 \le B < P\); not with bullet |
0 |
rounding |
decimal_places (0 to 6) and mode (half_up or half_even), both required |
none |
The rate forms are described in loans.instalment.
Outputs¶
| Field | Meaning |
|---|---|
periodic_rate |
Rate per payment period used |
rows |
One row per period: period, time, instalment, interest_portion, principal_portion, outstanding_balance, and balloon_amount in the last row |
totals |
instalments, interest and principal (the principal portions add up to the principal) |
final_adjustment |
Last instalment less the balloon amount less the regular instalment of the last period |
Conventions (explicit, reported in explain.conventions)¶
| Convention | Options | Default |
|---|---|---|
| Rate form | (a) annual rate with compounding and payment frequency, or (b) periodic rate | exactly one, required |
| Timing | in arrears: instalment \(k\) at time \(k\) | required, ordinary only |
| Amortisation type | french, fixed_amortisation_schedule, bullet |
required |
| Principal grace period | 0 to \(n - 1\) interest-only periods | 0, reported as default |
| Balloon amount | \(0 \le B < P\), repaid with the last instalment | 0, reported as default |
| Rounding | none, or 0 to 6 decimal places with half_up or half_even, on the instalment (french) or the principal portion (fixed_amortisation_schedule) |
none |
| Interest of a period | periodic rate × opening balance (compound interest), never rounded | fixed |
| Final-period adjustment | last principal portion = opening balance of the last period | always applied, reported |
The audit trail has a fixed number of steps whatever the term: the periodic rate, the regular amount and its rounding, the recurrence stated once, the first period (and the first period after the grace period) with the numbers substituted, the last period with the final adjustment, and the totals. The rows carry the numbers of every period.
Errors¶
| Code | When |
|---|---|
FM-E001 |
Timing due, principal 0 or negative, number_of_instalments 0 or above 10000, an incomplete or double rate form, rounding without a mode or with more than 6 decimal places, or a field missing or unknown |
FM-E003 |
The periodic rate is −100% or lower |
FM-E020 |
The balloon amount is negative, not lower than the principal, or given with bullet |
FM-E022 |
The grace period is not lower than the number of instalments, or is given with bullet |
FM-E023 |
With rounding, the balance would become negative before the last period, or the last principal portion or instalment would be negative (rounding too coarse for the loan) |
FM-E008 |
Arithmetic overflow (inputs far outside a realistic range) |
Accuracy¶
Each period uses one multiplication and two subtractions, so every row holds to the last of the engine's 28 significant digits: instalment = interest portion + principal portion, closing balance = opening balance − principal portion, and the principal portions add up to the principal. The final outstanding balance is always exactly zero, by construction.
The instalment itself is stored with 28 significant digits. The last period pays the balance actually left, so the final adjustment absorbs that storage error, amplified by \((1 + i)\) per period. Measured without rounding: about \(10^{-21}\) on 360 monthly periods; \(1.3 \times 10^{-17}\) at 10000 periods at 0.1%; \(2.9 \times 10^{-13}\) at 25% over 100 periods. It stays below \(10^{-18}\) of the principal while \((1 + i)^n \le 10^{10}\) (for example 1200 periods at 1.9%, or 470 periods at 5%); beyond that it grows with \((1 + i)^n\), to about 400 on a principal of 150,000 at 5% over 1200 periods, which is the consequence of the 28th digit of the instalment, not of the recurrence. In practice: a 30-year monthly loan at 15% a year has \((1 + i)^n\) of about 88, far inside the range. The present value of the instalments equals the principal within \(10^{-18}\) relative in every case. With rounding, the adjustment is dominated by the rounding itself. The golden cases are checked to \(10^{-20}\), or exactly where every value is a terminating decimal.
Verified against¶
- Two textbook worked examples, within half a cent: Valerie Watts, Business and Financial Mathematics (Fanshawe College Pressbooks), section 6.1, the 50,000 business loan; Jean-Paul Olivier, Business Math: A Step-by-Step Handbook (Lyryx Learning, 2018 Revision A), Example 13.3A.
- Golden cases in
tests/golden/loans.amortisation_schedule/: the ECMA-376 worked examples of the principal portion and an independent 50-digit reference (scripts/reference_loans.py), which is also compared row by row on random loans of the three types. - The interest-portion and principal-portion functions of
numpy_financialfor French schedules without rounding.