code wiki / (root) / nx_debt_payoff.nx

nx_debt_payoff.nx source

↩ module page · 140 lines · 6535 B

1// nx_debt_payoff.nx -- R4 of THE NISHI FINANCIAL ECOSYSTEM: exact debt amortization + payoff strategy. 2// ⚠NAME HISTORY (seq240/seq121, resolved 2026-07-23): this module was ONCE runtime/nx_debt.nx and 3// COLLIDED with _hdl_build/nx_debt.nx (the debt-plane CLI). Because imports resolve dir-local and the 4// build probes _hdl_build FIRST, every `import "nx_debt.nx"` from runtime/ silently resolved to the CLI 5// -- which has no dbt_* symbols -- so the whole finance lane failed with `expand_imports failed`. 6// The file is now nx_debt_payoff.nx and `nx_debt` unambiguously means the CLI. Do NOT reintroduce the name. 7// Built on nx_money (no-float, exact i64 cents). This is the operator's MOST URGENT rung: HELOC + 8// high-interest debt. Everything here is deterministic integer arithmetic -- an amortization schedule 9// is just iterated (interest = round(balance * monthly_rate); principal = payment - interest) and is 10// therefore bit-exact and reproducible (floats would drift cent-by-cent over hundreds of months). 11// 12// CAPABILITIES: 13// dbt_payoff_months -- simulate one loan at a fixed payment -> months to payoff + total interest, 14// and DETECT the minimum-payment trap (payment <= interest -> never amortizes). 15// dbt_strategy -- multi-debt payoff under a fixed monthly budget, avalanche (highest-APR-first, 16// interest-optimal) vs snowball (lowest-balance-first). Returns total months + 17// total interest. The avalanche<=snowball interest fact is MEASURED by the gate. 18// 19// NEVER-BRICK FINANCIAL ANALOG (#26): no schedule can loop forever -- the minimum-payment trap is 20// detected and reported (-1), an under-funded budget is rejected (-1), and a hard month cap (-2) is a 21// defensive backstop. Nothing here mutates the caller's input arrays (strategy works on copies). 22// license_tier: ORIGINAL 23import "nx_syscalls.nx" 24import "nx_money.nx" 25const K_MAGIC_12000: i64 = 12000 26 27// Simulate paying down `principal` at `apr_scaled` (annual, fixed-point) with a fixed monthly 28// `payment`. Returns months-to-payoff (>=0); writes total interest to out_interest[0]. 29// -1 = minimum-payment trap (payment never covers interest -> balance never amortizes) 30// -2 = exceeded the safety month cap (defensive backstop; should not happen for valid input) 31func dbt_payoff_months(principal: i64, apr_scaled: i64, payment: i64, out_interest: *i64) -> i64 { 32 let mr: i64 = mny_rate_monthly(apr_scaled) 33 var bal: i64 = principal 34 var ti: i64 = 0 35 var months: i64 = 0 36 if bal <= 0 { out_interest[0] = 0; return 0 } 37 var done: i64 = 0 38 while done == 0 { 39 let intr: i64 = mny_apply_rate(bal, mr, RND_HALF_EVEN) 40 if payment <= intr { 41 out_interest[0] = ti 42 return 0 - 1 43 } 44 let pp: i64 = payment - intr 45 ti = mny_add(ti, intr) 46 months = months + 1 47 if pp >= bal { bal = 0; done = 1 } 48 else { bal = bal - pp } 49 if months > K_MAGIC_12000 { 50 if done == 0 { out_interest[0] = ti; return 0 - 2 } 51 } 52 } 53 out_interest[0] = ti 54 return months 55} 56 57// Multi-debt payoff under a fixed monthly `budget`. bal[]/apr[]/minp[] are parallel arrays of n debts 58// (balance cents, annual scaled APR, minimum payment cents). mode 0 = avalanche (target highest APR), 59// mode 1 = snowball (target lowest balance). Each month: accrue interest on every active debt, pay all 60// minimums, then cascade the remaining budget into the target until the budget is spent. Returns total 61// months (>=0); writes total interest paid to out_interest[0]. 62// -1 = budget does not cover the sum of minimum payments (a debt could grow without bound) 63// -2 = exceeded the safety month cap 64func dbt_strategy(bal: *i64, apr: *i64, minp: *i64, n: i64, budget: i64, mode: i64, out_interest: *i64) -> i64 { 65 let b: *i64 = sys_mmap(8 * n) as *i64 66 let mr: *i64 = sys_mmap(8 * n) as *i64 67 var i: i64 = 0 68 var summin: i64 = 0 69 while i < n { 70 b[i] = bal[i] 71 mr[i] = mny_rate_monthly(apr[i]) 72 summin = mny_add(summin, minp[i]) 73 i = i + 1 74 } 75 if budget < summin { out_interest[0] = 0; return 0 - 1 } 76 var total_int: i64 = 0 77 var months: i64 = 0 78 var running: i64 = 1 79 while running == 1 { 80 var any: i64 = 0 81 i = 0 82 while i < n { if b[i] > 0 { any = 1 } i = i + 1 } 83 if any == 0 { running = 0 } 84 else { 85 // accrue interest on every active debt 86 i = 0 87 while i < n { 88 if b[i] > 0 { 89 let intr: i64 = mny_apply_rate(b[i], mr[i], RND_HALF_EVEN) 90 b[i] = mny_add(b[i], intr) 91 total_int = mny_add(total_int, intr) 92 } 93 i = i + 1 94 } 95 var budget_left: i64 = budget 96 // pay every active debt its minimum (capped at balance and remaining budget) 97 i = 0 98 while i < n { 99 if b[i] > 0 { 100 var pay: i64 = minp[i] 101 if pay > b[i] { pay = b[i] } 102 if pay > budget_left { pay = budget_left } 103 b[i] = b[i] - pay 104 budget_left = budget_left - pay 105 } 106 i = i + 1 107 } 108 // cascade the remaining budget into the target debt(s) 109 var go: i64 = 1 110 while go == 1 { 111 if budget_left <= 0 { go = 0 } 112 else { 113 var tgt: i64 = 0 - 1 114 i = 0 115 while i < n { 116 if b[i] > 0 { 117 if tgt < 0 { tgt = i } 118 else { 119 if mode == 0 { if apr[i] > apr[tgt] { tgt = i } } 120 else { if b[i] < b[tgt] { tgt = i } } 121 } 122 } 123 i = i + 1 124 } 125 if tgt < 0 { go = 0 } 126 else { 127 var pay: i64 = budget_left 128 if pay > b[tgt] { pay = b[tgt] } 129 b[tgt] = b[tgt] - pay 130 budget_left = budget_left - pay 131 } 132 } 133 } 134 months = months + 1 135 if months > K_MAGIC_12000 { out_interest[0] = total_int; return 0 - 2 } 136 } 137 } 138 out_interest[0] = total_int 139 return months 140}