code wiki / _hdl_build / nx_price_hist.nx
nx_price_hist.nx source
↩ module page · 143 lines · 9058 B
1// nx_price_hist.nx -- LIB: PRICE HISTORY + TRENDS (R-PRICE-R1). Turns single price observations into an
2// ADDITIVE time series (rule 13: history is sacred) so "how a store is doing" becomes "how prices are MOVING".
3// Each recorded observation is appended; the trend = latest minus earliest over the recorded series (signed:
4// rising/falling/flat), the basket trend = the sum across items. HONEST: trends are computed from RECORDED dated
5// observations, NEVER a prediction. Keeps nx_price's latest-observation in sync (pr_get_price still works).
6// Sovereign seg-store (knowledge/store/price-*), integer cents no-float. license_tier: ORIGINAL
7import "nx_price.nx"
8import "nx_food_science.nx"
9import "nx_seg_store.nx"
10import "nx_syscalls.nx"
11
12func ph_put(prefix: *u8, key: *u8, val: *u8) -> i64 {
13 let vl: i64 = as_len(val)
14 if fd_streq_store(prefix, key, val, vl) == 1 { return 0 }
15 let w: *i64 = ss_begin()
16 ss_add(w, 1, key, val, vl)
17 let seg: i64 = fd_seg_next(prefix)
18 ss_commit(prefix, w, seg)
19 return 1
20}
21func ph_sign(v: i64) -> i64 { if v > 0 { return 1 } if v < 0 { return 0 - 1 } return 0 }
22
23func ph_histn_key(sid: *u8, item: *u8, out: *u8) -> i64 { var o: i64 = 0; o = as_append(out, o, "price:histn:" as *u8); o = as_append(out, o, sid); out[o] = 58 as u8; o = o + 1; o = as_append(out, o, item); out[o] = 0 as u8; return o }
24func ph_hist_key(sid: *u8, item: *u8, seq: i64, out: *u8) -> i64 { var o: i64 = 0; o = as_append(out, o, "price:hist:" as *u8); o = as_append(out, o, sid); out[o] = 58 as u8; o = o + 1; o = as_append(out, o, item); out[o] = 58 as u8; o = o + 1; o = fd_apnum(out, o, seq); out[o] = 0 as u8; return o }
25
26func ph_count(prefix: *u8, sid: *u8, item: *u8) -> i64 {
27 let key: *u8 = sys_mmap(96); ph_histn_key(sid, item, key)
28 let pq: *i64 = sys_mmap(16) as *i64
29 let lq: *i64 = sys_mmap(16) as *i64
30 if ss_get(prefix, key, pq, lq) != 1 { return 0 }
31 return fd_atoi(pq[0] as *u8, lq[0])
32}
33// keep nx_price's latest observation in sync so pr_get_price / pr_cheapest still work after history records.
34func ph_set_latest(prefix: *u8, sid: *u8, item: *u8, price: i64, day: i64, source: *u8) -> i64 {
35 let key: *u8 = sys_mmap(96); pr_obs_key(sid, item, key)
36 let val: *u8 = sys_mmap(128); var v: i64 = 0
37 v = fd_apnum(val, v, price); val[v] = 9 as u8; v = v + 1
38 v = fd_apnum(val, v, day); val[v] = 9 as u8; v = v + 1
39 val[v] = 45 as u8; v = v + 1; val[v] = 9 as u8; v = v + 1 // unit "-"
40 v = as_append(val, v, source); val[v] = 0 as u8
41 return ph_put(prefix, key, val)
42}
43// append a dated observation to the series (ADDITIVE) + update the latest. Returns the new seq.
44func ph_record(prefix: *u8, sid: *u8, item: *u8, price: i64, day: i64, source: *u8) -> i64 {
45 let seq: i64 = ph_count(prefix, sid, item)
46 let key: *u8 = sys_mmap(96); ph_hist_key(sid, item, seq, key)
47 let val: *u8 = sys_mmap(128); var v: i64 = 0
48 v = fd_apnum(val, v, price); val[v] = 9 as u8; v = v + 1
49 v = fd_apnum(val, v, day); val[v] = 9 as u8; v = v + 1
50 v = as_append(val, v, source); val[v] = 0 as u8
51 ph_put(prefix, key, val)
52 let nk: *u8 = sys_mmap(96); ph_histn_key(sid, item, nk)
53 let nv: *u8 = sys_mmap(16); var no: i64 = fd_apnum(nv, 0, seq + 1); nv[no] = 0 as u8
54 ph_put(prefix, nk, nv)
55 ph_set_latest(prefix, sid, item, price, day, source)
56 return seq
57}
58func ph_at(prefix: *u8, sid: *u8, item: *u8, seq: i64) -> i64 {
59 let key: *u8 = sys_mmap(96); ph_hist_key(sid, item, seq, key)
60 let pq: *i64 = sys_mmap(16) as *i64
61 let lq: *i64 = sys_mmap(16) as *i64
62 if ss_get(prefix, key, pq, lq) != 1 { return 0 - 1 }
63 let fb: *u8 = sys_mmap(24)
64 let fl: i64 = fd_field(pq[0] as *u8, lq[0], 0, fb)
65 return fd_atoi(fb, fl)
66}
67func ph_earliest(prefix: *u8, sid: *u8, item: *u8) -> i64 { if ph_count(prefix, sid, item) == 0 { return 0 - 1 } return ph_at(prefix, sid, item, 0) }
68func ph_latest(prefix: *u8, sid: *u8, item: *u8) -> i64 { let n: i64 = ph_count(prefix, sid, item); if n == 0 { return 0 - 1 } return ph_at(prefix, sid, item, n - 1) }
69// net change over the recorded series (signed cents); 0 if fewer than 2 observations (can't trend).
70func ph_trend(prefix: *u8, sid: *u8, item: *u8) -> i64 {
71 let n: i64 = ph_count(prefix, sid, item)
72 if n < 2 { return 0 }
73 return ph_at(prefix, sid, item, n - 1) - ph_at(prefix, sid, item, 0)
74}
75func ph_trend_dir(prefix: *u8, sid: *u8, item: *u8) -> i64 { return ph_sign(ph_trend(prefix, sid, item)) }
76func ph_min(prefix: *u8, sid: *u8, item: *u8) -> i64 {
77 let n: i64 = ph_count(prefix, sid, item)
78 if n == 0 { return 0 - 1 }
79 var m: i64 = ph_at(prefix, sid, item, 0)
80 var i: i64 = 1
81 while i < n { let v: i64 = ph_at(prefix, sid, item, i); if v < m { m = v } i = i + 1 }
82 return m
83}
84func ph_max(prefix: *u8, sid: *u8, item: *u8) -> i64 {
85 let n: i64 = ph_count(prefix, sid, item)
86 if n == 0 { return 0 - 1 }
87 var m: i64 = ph_at(prefix, sid, item, 0)
88 var i: i64 = 1
89 while i < n { let v: i64 = ph_at(prefix, sid, item, i); if v > m { m = v } i = i + 1 }
90 return m
91}
92// whole-basket trend (sum of per-item trends): negative = the basket is getting cheaper over time.
93func ph_basket_trend(prefix: *u8, sid: *u8, items: *i64, n: i64) -> i64 {
94 var s: i64 = 0
95 var i: i64 = 0
96 while i < n { s = s + ph_trend(prefix, sid, items[i] as *u8); i = i + 1 }
97 return s
98}
99
100func ph_money_signed(dst: *u8, off: i64, cents: i64) -> i64 {
101 var o: i64 = off
102 var m: i64 = cents
103 if m < 0 { dst[o] = 45 as u8; o = o + 1; m = 0 - m } else { dst[o] = 43 as u8; o = o + 1 }
104 dst[o] = 36 as u8; o = o + 1
105 o = fd_apnum(dst, o, m / 100)
106 dst[o] = 46 as u8; o = o + 1
107 let r: i64 = m % 100
108 dst[o] = (48 + (r / 10)) as u8; o = o + 1
109 dst[o] = (48 + (r % 10)) as u8; o = o + 1
110 return o
111}
112func ph_trend_word(dir: i64) -> *u8 {
113 if dir > 0 { return "rising" as *u8 }
114 if dir < 0 { return "falling" as *u8 }
115 return "flat" as *u8
116}
117
118// render the price-trend page for a store + basket (now + movement + basket trajectory). Sovereign no-JS.
119func ph_render_trend(prefix: *u8, sid: *u8, items: *i64, n: i64, out: *u8) -> i64 {
120 let snm: *u8 = sys_mmap(96); pr_store_field(prefix, sid, 0, snm)
121 var o: i64 = 0
122 o = as_append(out, o, "<!doctype html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>Price trends</title><style>body{margin:0;font-family:system-ui,sans-serif;color:#1b2330;background:#eef2f7;line-height:1.5}header{background:#1f3a5f;color:#fff;padding:22px}header h1{margin:0;font-size:1.4rem}header p{margin:4px 0 0;color:#cfe0f5}main{max-width:680px;margin:0 auto;padding:16px}table{border-collapse:collapse;width:100%;background:#fff;border-radius:12px;overflow:hidden}th,td{padding:9px 12px;text-align:left;border-top:1px solid #eef3f8}th{background:#e4ecf6;color:#27405f}td.n{text-align:right;font-variant-numeric:tabular-nums}.up{color:#a23a2a;font-weight:700}.down{color:#1f7a44;font-weight:700}.flat{color:#5a6b80}.big{background:#fff;border-left:5px solid #1f3a5f;border-radius:8px;padding:14px;margin-top:14px;font-size:1.05rem}.muted{color:#5a6b80;font-size:.88rem}</style></head><body><header><h1>Price trends — " as *u8)
123 o = as_append_escaped(out, o, snm, as_len(snm))
124 o = as_append(out, o, "</h1><p>How prices are actually moving, from your recorded observations.</p></header><main><table><tr><th>Item</th><th>Now</th><th>Range</th><th>Trend</th></tr>" as *u8)
125 var i: i64 = 0
126 while i < n {
127 let item: *u8 = items[i] as *u8
128 let dir: i64 = ph_trend_dir(prefix, sid, item)
129 o = as_append(out, o, "<tr><td>" as *u8); o = as_append_escaped(out, o, item, as_len(item))
130 o = as_append(out, o, "</td><td class='n'>" as *u8); o = im_money_local(out, o, ph_latest(prefix, sid, item))
131 o = as_append(out, o, "</td><td class='n'>" as *u8); o = im_money_local(out, o, ph_min(prefix, sid, item)); o = as_append(out, o, "–" as *u8); o = im_money_local(out, o, ph_max(prefix, sid, item))
132 if dir > 0 { o = as_append(out, o, "</td><td class='up'>" as *u8) } else { if dir < 0 { o = as_append(out, o, "</td><td class='down'>" as *u8) } else { o = as_append(out, o, "</td><td class='flat'>" as *u8) } }
133 o = as_append(out, o, ph_trend_word(dir)); o = as_append(out, o, " " as *u8); o = ph_money_signed(out, o, ph_trend(prefix, sid, item))
134 o = as_append(out, o, "</td></tr>" as *u8)
135 i = i + 1
136 }
137 o = as_append(out, o, "</table><div class='big'>Your basket is " as *u8)
138 let bt: i64 = ph_basket_trend(prefix, sid, items, n)
139 o = as_append(out, o, ph_trend_word(ph_sign(bt))); o = as_append(out, o, " overall (" as *u8); o = ph_money_signed(out, o, bt); o = as_append(out, o, " vs the first reading).</div>" as *u8)
140 o = as_append(out, o, "<p class='muted'>Trends are computed from recorded, dated observations — what prices DID, not a prediction.</p></main></body></html>" as *u8)
141 out[o] = 0 as u8
142 return o
143}