code wiki / (root) / nx_amort_lib.nx

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}