功能说明
等额本息贷款每期还款额相同,但利息占比逐期下降、本金占比逐期上升。这个示例用 Decimal 而不是浮点数,避免分位舍入误差在几十期后累积成明显的金额偏差。输出结构包含期数、还款额、利息、本金和剩余余额,可直接渲染成表格或导出 CSV。
monthly_payment 使用标准年金公式;amortization_schedule 逐期计算利息并按分四舍五入,最后一期用剩余本金覆盖,确保余额精确归零。total_interest 汇总每一期的利息。
关键代码
月利率等于年利率除以十二。利率为零时直接本金除以期数,避免除零。growth 是 1 加月利率的期数次幂,还款额等于本金乘月利率乘 growth 再除以 growth 减一。构造还款表时先算利息,再用固定还款额减去利息得到本金;最后一期把本金改为剩余余额并重算还款额,这一行是消除尾差的关键。所有金额都经过 quantize 统一到两位小数。
使用注意
年利率必须以小数传入,例如百分之六点五写作 Decimal(“0.065”),不能传 6.5。期数必须为正整数,否则抛出 ValueError。提前还款、浮动利率和手续费不在模型内,需要另行扩展。总利息是各期利息之和,与还款总额减本金可能相差一分,报表中应统一采用同一口径。传入浮点数会污染 Decimal 精度,务必用字符串构造。
验证方式
两万五千元、年利率百分之六点五、二十四期的月供约为 1113.66 元,第一期利息约为 135.42 元。打印前三期可以看到利息递减、本金递增。最后一期余额应为 0.00,总利息为 1727.76 元;最后一期还款额因抹平尾差而略低于固定月供,总额等于本金加总利息。可以再测试零利率场景,确认每期本金相等。
代码示例
from dataclasses import dataclass
from decimal import ROUND_HALF_UP, Decimal
CENT = Decimal("0.01")
def quantize(value: Decimal) -> Decimal:
return value.quantize(CENT, rounding=ROUND_HALF_UP)
@dataclass(frozen=True)
class Payment:
period: int
amount: Decimal
interest: Decimal
principal: Decimal
balance: Decimal
def monthly_payment(principal: Decimal, annual_rate: Decimal, months: int) -> Decimal:
if months <= 0:
raise ValueError("months must be positive")
rate = annual_rate / Decimal(12)
if rate == 0:
return quantize(principal / months)
growth = (Decimal(1) + rate) ** months
return quantize(principal * rate * growth / (growth - 1))
def amortization_schedule(
principal: Decimal,
annual_rate: Decimal,
months: int,
) -> list[Payment]:
rate = annual_rate / Decimal(12)
payment = monthly_payment(principal, annual_rate, months)
balance = principal
schedule: list[Payment] = []
for period in range(1, months + 1):
interest = quantize(balance * rate)
principal_part = payment - interest
if period == months:
principal_part = balance
payment = principal_part + interest
balance = quantize(balance - principal_part)
schedule.append(Payment(period, payment, interest, principal_part, balance))
return schedule
def total_interest(schedule: list[Payment]) -> Decimal:
return quantize(sum((row.interest for row in schedule), Decimal(0)))
def main() -> None:
schedule = amortization_schedule(Decimal("25000"), Decimal("0.065"), 24)
print(f"payment: {schedule[0].amount}")
for row in schedule[:3]:
print(row)
print("final balance:", schedule[-1].balance)
print("total interest:", total_interest(schedule))
if __name__ == "__main__":
main()