Functional Weave
Code in Python

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),
    )