code wiki / (root) / nx_spend_analyze.nx

nx_spend_analyze.nx source

↩ module page · 75 lines · 3881 B

1// nx_spend_analyze.nx -- the SPEND-PATTERN analyzer (the operator's headline ask: "manage spend patterns to be 2// optimal for THEIR situation"). Categorizes spending into needs/wants/savings, benchmarks against 50/30/20, finds 3// recurring/subscription drains (+ their annual cost), and computes the situation-aware recommendation: exactly how 4// much to cut from discretionary to hit a savings target, and WHERE (the largest want). Integer-exact (cents/bps). 5// Reads a category array; pairs with nx_money_situation (income) to judge "optimal for THEM". license_tier: ORIGINAL 6import "nx_syscalls.nx" 7 8const SP_FIELDS: i64 = 3 // per category: [amount_cents, class, recurring] 9const SP_NEED: i64 = 0 // class: essential need 10const SP_WANT: i64 = 1 // class: discretionary want 11const SP_SAVE: i64 = 2 // class: savings / debt-paydown 12// 50/30/20 benchmark targets (documented budgeting standard), in basis points of total outflow: 13const B5030_NEEDS: i64 = 5000 14const B5030_WANTS: i64 = 3000 15const B5030_SAVE: i64 = 2000 16 17func sp_total(cats: *i64, n: i64) -> i64 { var s: i64 = 0; var i: i64 = 0; while i < n { s = s + cats[i*SP_FIELDS+0]; i = i + 1 } return s } 18func sp_total_by_class(cats: *i64, n: i64, class: i64) -> i64 { 19 var s: i64 = 0; var i: i64 = 0 20 while i < n { if cats[i*SP_FIELDS+1] == class { s = s + cats[i*SP_FIELDS+0] } i = i + 1 } 21 return s 22} 23func sp_class_pct_bps(cats: *i64, n: i64, class: i64) -> i64 { 24 let t: i64 = sp_total(cats, n) 25 if t <= 0 { return 0 } 26 return sp_total_by_class(cats, n, class) * 10000 / t 27} 28// signed gap vs the 50/30/20 target (positive = overspending this class relative to the benchmark). 29func sp_class_gap_bps(cats: *i64, n: i64, class: i64, target_bps: i64) -> i64 { 30 return sp_class_pct_bps(cats, n, class) - target_bps 31} 32 33// index of the largest discretionary (want) category -- the highest-leverage cut. -1 if none. 34func sp_largest_want(cats: *i64, n: i64) -> i64 { 35 var best: i64 = 0 - 1; var bestamt: i64 = 0; var i: i64 = 0 36 while i < n { 37 if cats[i*SP_FIELDS+1] == SP_WANT { 38 let a: i64 = cats[i*SP_FIELDS+0] 39 if best == (0 - 1) { best = i; bestamt = a } 40 else { if a > bestamt { bestamt = a; best = i } } 41 } 42 i = i + 1 43 } 44 return best 45} 46 47// recurring drains: all recurring, and the recurring-WANT subset (the cancellable subscriptions -- Rocket Money core). 48func sp_recurring_total(cats: *i64, n: i64) -> i64 { 49 var s: i64 = 0; var i: i64 = 0 50 while i < n { if cats[i*SP_FIELDS+2] == 1 { s = s + cats[i*SP_FIELDS+0] } i = i + 1 } 51 return s 52} 53func sp_recurring_want_total(cats: *i64, n: i64) -> i64 { 54 var s: i64 = 0; var i: i64 = 0 55 while i < n { if cats[i*SP_FIELDS+2] == 1 { if cats[i*SP_FIELDS+1] == SP_WANT { s = s + cats[i*SP_FIELDS+0] } } i = i + 1 } 56 return s 57} 58func sp_recurring_want_annual(cats: *i64, n: i64) -> i64 { return sp_recurring_want_total(cats, n) * 12 } 59 60// what cutting wants by cut_bps frees up (e.g. cut 30% -> wants_total * 3000/10000). 61func sp_potential_savings(cats: *i64, n: i64, cut_bps: i64) -> i64 { return sp_total_by_class(cats, n, SP_WANT) * cut_bps / 10000 } 62 63// the SITUATION-AWARE recommendation: to reach `target_save` from `current_save`, how much to cut from wants. 64// floored at 0, capped at wants_total (can't cut more discretionary than exists). 65func sp_required_want_cut(target_save: i64, current_save: i64, wants_total: i64) -> i64 { 66 var c: i64 = target_save - current_save 67 if c < 0 { return 0 } 68 if c > wants_total { return wants_total } 69 return c 70} 71// is the savings target reachable by cutting wants alone? 0 = no (needs income up or needs-cut / hardship review). 72func sp_target_reachable_by_wants(target_save: i64, current_save: i64, wants_total: i64) -> i64 { 73 if (target_save - current_save) <= wants_total { return 1 } 74 return 0 75}