57 lines
2.2 KiB
Python
57 lines
2.2 KiB
Python
import calendar
|
|
from dataclasses import dataclass
|
|
from datetime import date
|
|
from decimal import ROUND_HALF_UP, Decimal
|
|
|
|
CENT = Decimal("0.01")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Installment:
|
|
due: date
|
|
payment: Decimal
|
|
interest: Decimal
|
|
principal: Decimal
|
|
remaining: Decimal
|
|
|
|
|
|
def add_months(d: date, months: int) -> date:
|
|
y, m0 = divmod(d.year * 12 + d.month - 1 + months, 12)
|
|
m = m0 + 1
|
|
return date(y, m, min(d.day, calendar.monthrange(y, m)[1]))
|
|
|
|
|
|
def annuity_payment(principal: Decimal, annual_rate_pct: Decimal, term_months: int) -> Decimal:
|
|
i = annual_rate_pct / Decimal(100) / Decimal(12)
|
|
if i == 0:
|
|
return (principal / term_months).quantize(CENT, ROUND_HALF_UP)
|
|
q = (Decimal(1) + i) ** term_months
|
|
return (principal * i * q / (q - Decimal(1))).quantize(CENT, ROUND_HALF_UP)
|
|
|
|
|
|
def loan_schedule(principal: Decimal, annual_rate_pct: Decimal, term_months: int,
|
|
payout_date: date, repayment_type: str) -> list[Installment]:
|
|
i = annual_rate_pct / Decimal(100) / Decimal(12)
|
|
plan: list[Installment] = []
|
|
remaining = principal
|
|
if repayment_type == "bullet":
|
|
for n in range(1, term_months + 1):
|
|
interest = (remaining * i).quantize(CENT, ROUND_HALF_UP)
|
|
principal_part = remaining if n == term_months else Decimal("0")
|
|
remaining = remaining - principal_part
|
|
plan.append(Installment(add_months(payout_date, n),
|
|
-(interest + principal_part), interest,
|
|
principal_part, remaining.quantize(CENT)))
|
|
return plan
|
|
rate = annuity_payment(principal, annual_rate_pct, term_months)
|
|
for n in range(1, term_months + 1):
|
|
interest = (remaining * i).quantize(CENT, ROUND_HALF_UP)
|
|
principal_part = rate - interest
|
|
if n == term_months or principal_part > remaining:
|
|
principal_part = remaining
|
|
remaining = remaining - principal_part
|
|
plan.append(Installment(add_months(payout_date, n),
|
|
-(interest + principal_part), interest,
|
|
principal_part, remaining.quantize(CENT)))
|
|
return plan
|