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}