nx_amort_lib.nx source
↩ module page · 97 lines · 4958 B
1// nx_amort_lib.nx -- LOAN AMORTIZATION with exact penny allocation. Integer-exact, conservation-proven.
2//
3// The companion to nx_fin_finance (which ranks a loan's TRUE cost): this produces the actual schedule. The
4// hard part is not the payment formula -- it is the PENNY: a level payment rarely divides the balance evenly,
5// and naive amortizers leave a residual cent on the last payment or, worse, create one. Here the schedule is
6// CONSERVATION-EXACT by construction -- sum of every month's principal reduction == the original principal,
7// and the final payment is computed to clear the balance to EXACTLY ZERO (remaining balance + its last
8// month's interest), never a lingering or invented cent.
9//
10// *THE LEVEL PAYMENT IS FOUND BY INTEGER BISECTION, not the pow((1+r),N) closed form (which needs floats and
11// rounds unpredictably): the remaining balance after N payments is monotone-decreasing in the payment, so we
12// bisect for the SMALLEST integer payment that clears the loan in exactly N months. Same technique as the
13// bond YTM. FAIL-CLOSED: non-positive principal/term or a negative rate returns AMORT_BAD -- no schedule is
14// produced on nonsense input.
15//
16// Money is minor units (cents); rate is annual basis points; monthly interest = balance * annual_bp / 120000
17// (=/12/10000), truncated down (borrower-favorable, absorbed by the final payment). SCALE ENVELOPE: balance
18// up to ~9e14/annual_bp fits i64. license_tier: ORIGINAL No hw writes (Rule 26). LIB.
19
20const AMORT_BP_MONTH_DIV: i64 = 120000 // annual bp -> monthly fraction denominator (12 * 10000)
21const AMORT_BAD: i64 = 0 - 2000000002 // invalid input
22
23// one month's interest on a balance at an annual bp rate, truncated down.
24func amort_monthly_interest(balance: i64, annual_bp: i64) -> i64 {
25 return balance * annual_bp / AMORT_BP_MONTH_DIV
26}
27
28// remaining balance after `months` level payments of `payment`. Monotone-decreasing in payment. Once the
29// balance reaches 0 it stops accruing (the loan is done); a too-small payment lets the balance grow.
30func amort_balance_after(principal: i64, annual_bp: i64, payment: i64, months: i64) -> i64 {
31 var bal: i64 = principal
32 var i: i64 = 0
33 while i < months {
34 if bal > 0 {
35 let intr: i64 = bal * annual_bp / AMORT_BP_MONTH_DIV
36 bal = bal + intr - payment
37 }
38 i = i + 1
39 }
40 return bal
41}
42
43// *the minimal integer level payment that clears the loan in exactly term_months, by bisection.
44// FAIL-CLOSED on non-positive principal/term or negative rate.
45func amort_level_payment(principal: i64, annual_bp: i64, term_months: i64) -> i64 {
46 if principal <= 0 { return AMORT_BAD }
47 if annual_bp < 0 { return AMORT_BAD }
48 if term_months <= 0 { return AMORT_BAD }
49 var lo: i64 = 0
50 var hi: i64 = principal + amort_monthly_interest(principal, annual_bp) + 1 // clears in 1 month -> upper bound
51 while (hi - lo) > 1 {
52 let mid: i64 = (lo + hi) / 2
53 let rem: i64 = amort_balance_after(principal, annual_bp, mid, term_months)
54 if rem <= 0 { hi = mid }
55 if rem > 0 { lo = mid }
56 }
57 return hi
58}
59
60// the adjusted final payment = balance remaining before the last month + that month's interest, so the
61// balance clears to exactly zero. Absorbs all the level-payment rounding.
62func amort_final_payment(principal: i64, annual_bp: i64, term_months: i64) -> i64 {
63 let m: i64 = amort_level_payment(principal, annual_bp, term_months)
64 if m == AMORT_BAD { return AMORT_BAD }
65 let b: i64 = amort_balance_after(principal, annual_bp, m, term_months - 1) // balance before last payment
66 let i: i64 = b * annual_bp / AMORT_BP_MONTH_DIV
67 return b + i
68}
69
70// *CONSERVATION: sum of every month's principal reduction. Must equal the original principal exactly, or the
71// schedule has lost/created a cent. The last month's principal reduction is the entire remaining balance.
72func amort_principal_sum(principal: i64, annual_bp: i64, term_months: i64) -> i64 {
73 let m: i64 = amort_level_payment(principal, annual_bp, term_months)
74 if m == AMORT_BAD { return AMORT_BAD }
75 var bal: i64 = principal
76 var princ_sum: i64 = 0
77 var i: i64 = 0
78 while i < term_months - 1 {
79 if bal > 0 {
80 let intr: i64 = bal * annual_bp / AMORT_BP_MONTH_DIV
81 let princ: i64 = m - intr
82 princ_sum = princ_sum + princ
83 bal = bal - princ
84 }
85 i = i + 1
86 }
87 princ_sum = princ_sum + bal // final month clears the remaining balance
88 return princ_sum
89}
90
91// total interest over the life of the loan = total paid minus principal.
92func amort_total_interest(principal: i64, annual_bp: i64, term_months: i64) -> i64 {
93 let m: i64 = amort_level_payment(principal, annual_bp, term_months)
94 if m == AMORT_BAD { return AMORT_BAD }
95 let fin: i64 = amort_final_payment(principal, annual_bp, term_months)
96 return m * (term_months - 1) + fin - principal
97}