lending.amortisation-schedule@1.0.1
impl/python.py
2,823 bytes · the Python implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import List
from .lending_amortisation_schedule_types import AmortisationRow, AmortisationSchedule
from .lending_loan_payment import loan_payment, payment_count, round_wide ← from lending.loan-payment ^1.0.0 · built alongside by fune
from .math_round_div import RoundingMode ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
def period_interest(balance: int, annual_rate_basis_points: int, payments_per_year: int) -> int:
"""Interest for one period on a positive balance: balance * b / D, rounded
half-up to the minor unit, which is what gets posted to the account."""
if balance <= 0 or annual_rate_basis_points == 0:
return 0
product = balance * annual_rate_basis_points
# Rust holds the product in an i64; refuse it everywhere rather than in one language.
if product > 9223372036854775807:
raise ValueError("balance too large for exact interest")
return round_wide(product, 10000 * payments_per_year, "half-up")
def amortisation_schedule(
principal: Money,
annual_rate_basis_points: int,
term_months: int,
payments_per_year: int,
mode: RoundingMode,
) -> AmortisationSchedule:
"""The schedule a lender's system produces: each period's interest is
computed on the opening balance and rounded to a whole minor unit, the
level payment repays interest first and principal with the rest, and the
last payment is whatever clears the balance, so it ends at exactly zero.
"""
payment = loan_payment(principal, annual_rate_basis_points, term_months, payments_per_year, mode)
n = payment_count(annual_rate_basis_points, term_months, payments_per_year)
currency = principal.currency
rows: List[AmortisationRow] = []
balance = principal.minor
total_interest = 0
total_paid = 0
period = 1
while period <= n and balance > 0:
interest = period_interest(balance, annual_rate_basis_points, payments_per_year)
# The last period, or one where the level payment would overshoot (a
# rounded-up payment can clear the loan a period early), pays exactly
# what is owed.
if period == n or balance + interest <= payment.minor:
due = balance + interest
else:
due = payment.minor
repaid = due - interest
balance -= repaid
total_interest += interest
total_paid += due
rows.append(
AmortisationRow(
period=period,
payment=money(due, currency),
interest=money(interest, currency),
principal=money(repaid, currency),
balance=money(balance, currency),
)
)
period += 1
return AmortisationSchedule(
payment=payment,
rows=rows,
total_interest=money(total_interest, currency),
total_paid=money(total_paid, currency),
)