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 &mdash; " 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, "&ndash;" 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 &mdash; what prices DID, not a prediction.</p></main></body></html>" as *u8) 141 out[o] = 0 as u8 142 return o 143}