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}