← 中文资源

简体中文版本

贷款等额本息还款计划

用 Decimal 精确计算固定月供并生成完整还款表,处理末期尾差并汇总总利息。

阅读英文原文

功能说明

等额本息贷款每期还款额相同,但利息占比逐期下降、本金占比逐期上升。这个示例用 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()