PageSourceSearch

https://icafeforex.com/wp-content/mu-plugins/icafe-gold-assets/icafe-gold.js?ver=1.8.1

js icafeforex.com collected 2026-10-02 01:12:49 UTC 32,523 bytes, 637 lines download raw bytes

1/* iCafe Gold — front-end (Phase 1 baseline)
2   - auto-refresh board every 60s from REST (reads cache; never hits source from browser)
3   - calculator realtime
4   - SPDR holdings chart (Chart.js lazy-loaded), range tabs 1m/3m/1y */
5(function () {
6  "use strict";
7  var CFG = window.ICF_GOLD || {};
8
9  /* ---------- helpers ---------- */
10  function $(sel, ctx) { return (ctx || document).querySelector(sel); }
11  function $all(sel, ctx) { return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); }
12  function num(v) { return parseFloat(String(v == null ? "" : v).replace(/[^0-9.\-]/g, "")) || 0; }
13  function fmt(n, d) { d = d == null ? 2 : d; return n.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d }); }
14
15  /* ---------- auto-refresh board ---------- */
16  function setText(root, key, val) {
17    $all('[data-icf="' + key + '"]', root).forEach(function (el) { if (val != null && val !== "") el.textContent = val; });
18  }
19  function applyData(d) {
20    $all("[data-icf-gold-board]").forEach(function (root) {
21      var t = d.thai || {}, s = d.spdr || {};
22      setText(root, "bar_buy", t.bar_buy); setText(root, "bar_sell", t.bar_sell);
23      setText(root, "orn_buy", t.orn_buy); setText(root, "orn_sell", t.orn_sell);
24      setText(root, "stamp", t.date);
25      var chg = $('[data-icf="change"]', root);
26      if (chg && t.change != null) {
27        chg.textContent = "เปลี่ยนแปลงวันนี้: " + t.change + " บาท";
28        chg.classList.toggle("is-up", String(t.change).indexOf("-") !== 0);
29        chg.classList.toggle("is-down", String(t.change).indexOf("-") === 0);
30      }
31      setText(root, "spdr_current", s.current); setText(root, "spdr_d1", s.diff_1d_f);
32      setText(root, "spdr_d7", s.diff_7d_f); setText(root, "spdr_date", s.date);
33      setText(root, "spot", d.spot); setText(root, "fx", d.fx); setText(root, "spot_baht", d.spot_baht);
34    });
35    // keep calculators in sync with refreshed prices
36    if (d.thai) {
37      $all(".icf-gold-calc").forEach(function (c) {
38        if (d.thai.bar_buy)  c.dataset.barBuy  = num(d.thai.bar_buy);
39        if (d.thai.bar_sell) c.dataset.barSell = num(d.thai.bar_sell);
40        if (d.thai.orn_buy)  c.dataset.ornBuy  = num(d.thai.orn_buy);
41        if (d.thai.orn_sell) c.dataset.ornSell = num(d.thai.orn_sell);
42        recalc(c);
43      });
44    }
45  }
46  function refresh() {
47    if (!CFG.rest) return;
48    fetch(CFG.rest, { credentials: "same-origin" })
49      .then(function (r) { return r.ok ? r.json() : null; })
50      .then(function (d) { if (d) applyData(d); })
51      .catch(function () {});
52  }
53
54  /* ---------- calculator ---------- */
55  function recalc(c) {
56    var type = c.dataset.type || "bar";
57    var w = num(c.dataset.weight || (c.querySelector(".icf-gold-slider") ? c.querySelector(".icf-gold-slider").value : 1));
58    var buy  = type === "orn" ? num(c.dataset.ornBuy)  : num(c.dataset.barBuy);
59    var sell = type === "orn" ? num(c.dataset.ornSell) : num(c.dataset.barSell);
60    var outBuy = c.querySelector(".icf-gold-calc-buy");
61    var outSell = c.querySelector(".icf-gold-calc-sell");
62    if (outBuy)  outBuy.textContent  = buy  > 0 ? fmt(buy * w, 0) + " ฿" : "—";
63    if (outSell) outSell.textContent = sell > 0 ? fmt(sell * w, 0) + " ฿" : "—";
64  }
65  function initCalc(c) {
66    c.dataset.type = "bar";
67    var slider = c.querySelector(".icf-gold-slider");
68    var out = c.querySelector(".icf-gold-weight-out");
69    if (slider) {
70      slider.addEventListener("input", function () {
71        c.dataset.weight = slider.value;
72        if (out) out.textContent = parseFloat(slider.value).toFixed(2);
73        recalc(c);
74      });
75    }
76    $all(".icf-gold-type", c).forEach(function (btn) {
77      btn.addEventListener("click", function () {
78        $all(".icf-gold-type", c).forEach(function (b) { b.classList.remove("is-active"); });
79        btn.classList.add("is-active");
80        c.dataset.type = btn.dataset.type;
81        recalc(c);
82      });
83    });
84    // tab switching (now / retro) + retro calc
85    var tabs = $all(".icf-gold-calc-tab", c);
86    if (tabs.length) {
87      tabs.forEach(function (btn) {
88        btn.addEventListener("click", function () {
89          tabs.forEach(function (b) { b.classList.remove("is-active"); });
90          btn.classList.add("is-active");
91          var which = btn.dataset.calcTab;
92          $all(".icf-gold-calc-panel", c).forEach(function (p) {
93            p.hidden = (p.dataset.calcPanel !== which);
94          });
95        });
96      });
97      initAcalc(c);
98    }
99    recalc(c);
100  }
101
102  /* ---------- retro profit calculator (uses gold-close history) ---------- */
103  function initAcalc(c) {
104    if (!CFG.goldClose) return;
105    var amount = c.querySelector(".icf-gold-acalc-amount");
106    var dateIn = c.querySelector(".icf-gold-acalc-date");
107    var res = c.querySelector(".icf-gold-acalc-result");
108    var msg = c.querySelector(".icf-gold-acalc-msg");
109    if (!amount || !dateIn) return;
110    var series = null;
111
112    function nearestClose(dStr) {
113      // latest entry with date <= chosen; fallback earliest
114      var pick = null;
115      for (var i = 0; i < series.length; i++) {
116        if (series[i].date <= dStr) pick = series[i]; else break;
117      }
118      return pick || series[0];
119    }
120    function compute() {
121      if (!series || !series.length) return;
122      var amt = parseFloat(amount.value);
123      var d = dateIn.value;
124      if (!amt || amt <= 0 || !d) { if (res) res.hidden = true; return; }
125      var buy = nearestClose(d);
126      var now = series[series.length - 1];
127      if (!buy || !now || !buy.close) { if (res) res.hidden = true; return; }
128      var ratio = now.close / buy.close;
129      var valueNow = amt * ratio;
130      var pl = valueNow - amt;
131      var plPct = (ratio - 1) * 100;
132      if (msg) {
133        msg.hidden = (buy.date === d);
134        if (buy.date !== d) msg.textContent = "ใช้ราคาวันทำการใกล้เคียง: " + buy.date;
135      }
136      c.querySelector(".icf-gold-acalc-pbuy").textContent = fmt(buy.close, 2) + " USD (" + buy.date + ")";
137      c.querySelector(".icf-gold-acalc-pnow").textContent = fmt(now.close, 2) + " USD (" + now.date + ")";
138      c.querySelector(".icf-gold-acalc-value").textContent = fmt(valueNow, 0) + " ฿";
139      var plEl = c.querySelector(".icf-gold-acalc-pl");
140      var up = pl > 0, dn = pl < 0;
141      plEl.className = "icf-gold-acalc-pl " + (up ? "is-up" : dn ? "is-down" : "");
142      plEl.textContent = (up ? "▲ +" : dn ? "▼ " : "• ") + fmt(pl, 0) + " ฿ (" + (up ? "+" : "") + plPct.toFixed(2) + "%)";
143      if (res) res.hidden = false;
144    }
145    fetch(CFG.goldClose + "?range=1y", { credentials: "same-origin" })
146      .then(function (r) { return r.ok ? r.json() : null; })
147      .then(function (j) {
148        if (!j || !j.series || !j.series.length) { if (msg) { msg.hidden = false; msg.textContent = "ข้อมูลราคาย้อนหลังไม่พร้อม"; } return; }
149        series = j.series;
150        var first = series[0].date, last = series[series.length - 1].date;
151        dateIn.min = first; dateIn.max = last;
152        // default ~6 months back (or earliest)
153        if (!dateIn.value) {
154          var mid = series[Math.max(0, series.length - 126)];
155          dateIn.value = mid ? mid.date : first;
156        }
157        amount.addEventListener("input", compute);
158        dateIn.addEventListener("change", compute);
159        dateIn.addEventListener("input", compute);
160        compute();
161      })
162      .catch(function () { if (msg) { msg.hidden = false; msg.textContent = "โหลดข้อมูลราคาย้อนหลังไม่ได้"; } });
163  }
164
165  /* ---------- price vs 52-week high/low (gold-close) ---------- */
166  function initAth(box) {
167    if (!CFG.goldClose) return;
168    var loading = box.querySelector('[data-ath="loading"]');
169    var set = function (k, v) { var e = box.querySelector('[data-ath="' + k + '"]'); if (e) e.textContent = v; };
170    fetch(CFG.goldClose + "?range=1y", { credentials: "same-origin" })
171      .then(function (r) { return r.ok ? r.json() : null; })
172      .then(function (j) {
173        if (!j || !j.series || !j.series.length) { if (loading) loading.textContent = "ข้อมูลราคาไม่พร้อม"; return; }
174        var s = j.series, hi = s[0], lo = s[0], cur = s[s.length - 1];
175        s.forEach(function (p) { if (p.close > hi.close) hi = p; if (p.close < lo.close) lo = p; });
176        var range = hi.close - lo.close;
177        var pos = range > 0 ? ((cur.close - lo.close) / range) * 100 : 50;
178        pos = Math.max(0, Math.min(100, pos));
179        var fromHigh = hi.close > 0 ? ((hi.close - cur.close) / hi.close) * 100 : 0;
180        set("current", fmt(cur.close, 2));
181        set("low", fmt(lo.close, 2)); set("lowdate", "(" + lo.date + ")");
182        set("high", fmt(hi.close, 2)); set("highdate", "(" + hi.date + ")");
183        var frm = box.querySelector('[data-ath="fromhigh"]');
184        if (frm) {
185          if (fromHigh <= 0.05) { frm.textContent = "ที่จุดสูงสุด 52 สัปดาห์"; frm.className = "icf-gold-ath__frm is-up"; }
186          else { frm.textContent = "ต่ำกว่าจุดสูงสุด " + fromHigh.toFixed(1) + "%"; frm.className = "icf-gold-ath__frm is-down"; }
187        }
188        var dot = box.querySelector('[data-ath="dot"]');
189        if (dot) dot.style.left = pos.toFixed(1) + "%";
190        if (loading) loading.hidden = true;
191
192        // reuse for context card (no extra query)
193        var ctxA = document.querySelector('[data-ctx="ath"]');
194        if (ctxA) {
195          if (fromHigh <= 0.05) ctxA.innerHTML = '<em class="icf-gold-delta is-up">ATH</em>';
196          else ctxA.innerHTML = '<em class="icf-gold-delta is-down">▼ -' + fromHigh.toFixed(1) + '%</em>';
197        }
198      })
199      .catch(function () { if (loading) loading.textContent = "โหลดข้อมูลราคาไม่ได้"; });
200  }
201
202  /* ---------- chart (Chart.js lazy) ---------- */
203  var chartLib = null;
204  function loadChartLib() {
205    if (window.Chart) { return Promise.resolve(window.Chart); }
206    if (chartLib) { return chartLib; }
207    chartLib = new Promise(function (resolve, reject) {
208      var s = document.createElement("script");
209      s.src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js";
210      s.onload = function () { resolve(window.Chart); };
211      s.onerror = reject;
212      document.head.appendChild(s);
213    });
214    return chartLib;
215  }
216  function initChart(box) {
217    if (!CFG.spdr) return;
218    var canvas = box.querySelector(".icf-gold-chart-canvas");
219    var empty = box.querySelector(".icf-gold-chart-empty");
220    var inst = null, full = [];
221    function draw(range) {
222      loadChartLib().then(function (Chart) {
223        var data = full.slice(-range);
224        var labels = data.map(function (p) { return new Date(p.t * 1000).toLocaleDateString("th-TH", { day: "2-digit", month: "short" }); });
225        var vals = data.map(function (p) { return p.v; });
226        if (inst) { inst.data.labels = labels; inst.data.datasets[0].data = vals; inst.update(); return; }
227        inst = new Chart(canvas.getContext("2d"), {
228          type: "line",
229          data: { labels: labels, datasets: [{ label: "Holdings (t)", data: vals, borderColor: "#b8943f", backgroundColor: "rgba(184,148,63,.12)", fill: true, tension: .25, pointRadius: 0, borderWidth: 2 }] },
230          options: { responsive: true, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: "#9a927f", maxTicksLimit: 8 }, grid: { display: false } }, y: { ticks: { color: "#9a927f" }, grid: { color: "rgba(255,255,255,.05)" } } } }
231        });
232      });
233    }
234    if (empty) empty.hidden = false;
235    fetch(CFG.spdr, { credentials: "same-origin" })
236      .then(function (r) { return r.ok ? r.json() : null; })
237      .then(function (j) {
238        if (empty) empty.hidden = true;
239        if (!j || !j.series) return;
240        full = j.series;
241        draw(30);
242        $all(".icf-gold-range", box).forEach(function (btn) {
243          btn.addEventListener("click", function () {
244            $all(".icf-gold-range", box).forEach(function (b) { b.classList.remove("is-active"); });
245            btn.classList.add("is-active");
246            draw(parseInt(btn.dataset.range, 10) || 30);
247          });
248        });
249      })
250      .catch(function () { if (empty) { empty.hidden = false; empty.textContent = "โหลดข้อมูลกราฟไม่ได้"; } });
251  }
252
253  /* ---------- SPDR page chart (REST history, ranges 1m/3m/1y) ---------- */
254  function initSpdrChart(box) {
255    if (!CFG.spdrHistory) return;
256    var canvas = box.querySelector(".icf-gold-chart-canvas");
257    var empty = box.querySelector(".icf-gold-chart-empty");
258    var inst = null, cacheH = {}, cacheC = {};
259    // project close-price series onto SPDR dates (null where missing -> spanGaps)
260    function alignClose(series, closeSeries) {
261      if (!closeSeries || !closeSeries.length) return null;
262      var m = {}, any = false;
263      closeSeries.forEach(function (p) { m[p.date] = p.close; });
264      var out = series.map(function (p) {
265        var v = (m[p.date] != null) ? m[p.date] : null;
266        if (v != null) any = true;
267        return v;
268      });
269      return any ? out : null;
270    }
271    function render(series, closeSeries) {
272      loadChartLib().then(function (Chart) {
273        var labels = series.map(function (p) { return new Date(p.date + "T00:00:00").toLocaleDateString("th-TH", { day: "2-digit", month: "short", year: "2-digit" }); });
274        var vals = series.map(function (p) { return p.tonnes; });
275        var closeVals = alignClose(series, closeSeries);
276        var hasClose = !!closeVals;
277        // zoom left Y so daily holding variation is visible (values never near 0)
278        var mn = Math.min.apply(null, vals), mx = Math.max.apply(null, vals);
279        var pad = Math.max(5, (mx - mn) * 0.3);
280        var ymin = Math.max(0, Math.floor((mn - pad) / 10) * 10);
281        var ymax = Math.ceil((mx + pad) / 10) * 10;
282        if (inst) {
283          inst.data.labels = labels;
284          inst.data.datasets[0].data = vals;
285          inst.data.datasets[1].data = hasClose ? closeVals : [];
286          inst.data.datasets[1].hidden = !hasClose;
287          inst.options.scales.y.min = ymin;
288          inst.options.scales.y.max = ymax;
289          inst.options.scales.y1.display = hasClose;
290          inst.update();
291          return;
292        }
293        inst = new Chart(canvas.getContext("2d"), {
294          type: "bar",
295          data: {
296            labels: labels,
297            datasets: [
298              { type: "bar", label: "ถือครอง (ตัน)", data: vals, backgroundColor: "#b8943f", borderColor: "#b8943f", borderRadius: 3, borderSkipped: false, barPercentage: 0.62, categoryPercentage: 0.86, order: 2, yAxisID: "y" },
299              { type: "line", label: "ราคาทอง Close (USD)", data: hasClose ? closeVals : [], hidden: !hasClose, borderColor: "#f5edd6", backgroundColor: "transparent", borderWidth: 2, tension: 0.25, pointRadius: 0, spanGaps: true, fill: false, order: 1, yAxisID: "y1" }
300            ]
301          },
302          options: {
303            responsive: true, maintainAspectRatio: false,
304            interaction: { mode: "index", intersect: false },
305            plugins: {
306              legend: { display: true, position: "top", labels: { color: "#d8cfb6", usePointStyle: true, boxWidth: 10, padding: 14, font: { size: 12 } } },
307              tooltip: {
308                backgroundColor: "#141318", borderColor: "rgba(184,148,63,.5)", borderWidth: 1, titleColor: "#f5edd6", bodyColor: "#f5edd6", padding: 10,
309                callbacks: {
310                  label: function (ctx) {
311                    var v = ctx.parsed.y; if (v == null) return null;
312                    var num = Number(v).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
313                    return (ctx.dataset.label || "") + ": " + num + (ctx.dataset.yAxisID === "y1" ? " USD" : " ตัน");
314                  }
315                }
316              }
317            },
318            scales: {
319              x: { ticks: { color: "#9a927f", maxTicksLimit: 8, maxRotation: 0, autoSkip: true }, grid: { display: false } },
320              y: { position: "left", min: ymin, max: ymax, ticks: { color: "#c8a44f" }, grid: { color: "rgba(255,255,255,.05)" }, title: { display: true, text: "ถือครอง (ตัน)", color: "#c8a44f", font: { size: 11 } } },
321              y1: { position: "right", display: hasClose, beginAtZero: false, ticks: { color: "#e8e0c8" }, grid: { drawOnChartArea: false }, title: { display: true, text: "ราคาทอง Close (USD)", color: "#e8e0c8", font: { size: 11 } } }
322            }
323          }
324        });
325      });
326    }
327    function fetchSeries(url, range, store) {
328      if (store[range]) return Promise.resolve(store[range]);
329      return fetch(url + "?range=" + encodeURIComponent(range), { credentials: "same-origin" })
330        .then(function (r) { return r.ok ? r.json() : null; })
331        .then(function (j) { var s = (j && j.series) ? j.series : null; if (s) store[range] = s; return s; })
332        .catch(function () { return null; });
333    }
334    function load(range) {
335      if (empty) { empty.hidden = false; empty.textContent = "กำลังโหลดข้อมูลกราฟ…"; }
336      var pH = fetchSeries(CFG.spdrHistory, range, cacheH);
337      // close line is optional: missing/empty -> bars only (no error)
338      var pC = CFG.goldClose ? fetchSeries(CFG.goldClose, range, cacheC) : Promise.resolve(null);
339      Promise.all([pH, pC]).then(function (res) {
340        if (empty) empty.hidden = true;
341        if (!res[0]) { if (empty) { empty.hidden = false; empty.textContent = "โหลดข้อมูลกราฟไม่ได้"; } return; }
342        render(res[0], res[1]);
343      });
344    }
345    load("1m");
346    $all(".icf-gold-range", box).forEach(function (btn) {
347      btn.addEventListener("click", function () {
348        $all(".icf-gold-range", box).forEach(function (b) { b.classList.remove("is-active"); });
349        btn.classList.add("is-active");
350        load(btn.dataset.range || "1m");
351      });
352    });
353  }
354
355  function fmtThaiDelta(val, suffix) {
356    var n = parseInt(String(val == null ? "" : val).replace(/[^0-9-]/g, ""), 10) || 0;
357    var up = n > 0, dn = n < 0;
358    var arr = up ? "▲" : (dn ? "▼" : "▬");
359    var num = up ? "+" + n : String(n);
360    return { text: arr + num + (suffix || ""), cls: up ? "is-up" : (dn ? "is-down" : ""), n: n, arr: arr, num: num };
361  }
362
363  /* ---------- strip auto-refresh ---------- */
364  function refreshStrip() {
365    if (!CFG.rest) return;
366    fetch(CFG.rest, { credentials: "same-origin" })
367      .then(function (r) { return r.ok ? r.json() : null; })
368      .then(function (d) {
369        if (!d) return;
370        $all("[data-icf-gold-strip]").forEach(function (root) {
371          var t = d.thai || {};
372          setText(root, "bar_buy", t.bar_buy); setText(root, "bar_sell", t.bar_sell);
373          setText(root, "spot", d.spot);
374          var chEl = $('[data-icf="thai_change"]', root);
375          if (chEl && t.change != null && t.change !== "") {
376            var ch = fmtThaiDelta(t.change, " บาท");
377            chEl.className = "icf-gold-stripx__chg icf-gold-delta " + ch.cls;
378            chEl.textContent = ch.text;
379          }
380          var ctEl = $('[data-icf="thai_change_today"]', root);
381          if (ctEl && t.change_today != null) {
382            var ct = fmtThaiDelta(t.change_today, "");
383            ctEl.className = "icf-gold-stripx__chg icf-gold-delta " + ct.cls;
384            ctEl.textContent = " · วันนี้ " + ct.arr + ct.num;
385          }
386        });
387      }).catch(function () {});
388    if (!CFG.spdrLatest) return;
389    fetch(CFG.spdrLatest, { credentials: "same-origin" })
390      .then(function (r) { return r.ok ? r.json() : null; })
391      .then(function (s) {
392        if (!s) return;
393        $all("[data-icf-gold-strip]").forEach(function (root) {
394          if (s.tonnes != null) setText(root, "spdr_tonnes", fmt(s.tonnes, 2));
395          var dl = $('[data-icf="spdr_delta"]', root);
396          if (dl && s.change != null) {
397            var c = parseFloat(s.change) || 0, up = c > 0, dn = c < 0;
398            dl.innerHTML = '<em class="icf-gold-delta ' + (up ? "is-up" : dn ? "is-down" : "") + '">' + (up ? "▲ +" : dn ? "▼ " : "• ") + fmt(c, 2) + '</em>';
399          }
400        });
401      }).catch(function () {});
402  }
403
404  /* ---------- sidebar nav (in-page anchors) ---------- */
405  function initNav(nav) {
406    var toggle = nav.querySelector(".icf-gold-nav__toggle");
407    var links = $all(".icf-gold-nav__inner a", nav);
408    if (toggle) {
409      toggle.addEventListener("click", function () {
410        var open = nav.classList.toggle("is-open");
411        toggle.setAttribute("aria-expanded", open ? "true" : "false");
412      });
413    }
414    links.forEach(function (a) {
415      a.addEventListener("click", function () {
416        // close the collapsed menu on mobile after picking a section
417        if (window.matchMedia("(max-width: 760px)").matches) {
418          nav.classList.remove("is-open");
419          if (toggle) toggle.setAttribute("aria-expanded", "false");
420        }
421      });
422    });
423    // active link highlight on scroll
424    var targets = links
425      .map(function (a) { var id = a.getAttribute("href"); return id && id.charAt(0) === "#" ? document.getElementById(id.slice(1)) : null; })
426      .filter(Boolean);
427    if (targets.length && "IntersectionObserver" in window) {
428      var io = new IntersectionObserver(function (entries) {
429        entries.forEach(function (e) {
430          if (!e.isIntersecting) return;
431          links.forEach(function (a) {
432            a.classList.toggle("is-active", a.getAttribute("href") === "#" + e.target.id);
433          });
434        });
435      }, { rootMargin: "-40% 0px -55% 0px", threshold: 0 });
436      targets.forEach(function (t) { io.observe(t); });
437    }
438  }
439
440  /* ---------- SPDR sentiment gauge (computed from SPDR 30-day flow) ---------- */
441  function sentiClamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
442  function sentiPolar(cx, cy, r, deg) {
443    var a = deg * Math.PI / 180;
444    return { x: cx + r * Math.cos(a), y: cy - r * Math.sin(a) };
445  }
446  function sentiArc(cx, cy, r, a0, a1) {
447    var s = sentiPolar(cx, cy, r, a0), e = sentiPolar(cx, cy, r, a1);
448    var large = Math.abs(a1 - a0) > 180 ? 1 : 0;
449    return "M" + s.x.toFixed(2) + " " + s.y.toFixed(2) + " A " + r + " " + r + " 0 " + large + " 1 " + e.x.toFixed(2) + " " + e.y.toFixed(2);
450  }
451  function sentiZone(score) {
452    if (score <= 25) return { key: "fear2", label: "กลัวมาก", color: "#c0392b" };
453    if (score <= 45) return { key: "fear1", label: "ค่อนข้างกลัว", color: "#e2504a" };
454    if (score <= 55) return { key: "neutral", label: "เป็นกลาง", color: "#d4b86a" };
455    if (score <= 75) return { key: "greed1", label: "โลภ", color: "#9bcf6a" };
456    return { key: "greed2", label: "โลภมาก", color: "#5dca8a" };
457  }
458  function sentiSummary(zone) {
459    switch (zone.key) {
460      case "fear2": return "กองทุน SPDR เทขายทองหนัก — อารมณ์ตลาดสถาบันอยู่ในภาวะกลัวมาก";
461      case "fear1": return "กองทุน SPDR ทยอยขายสุทธิ — อารมณ์ตลาดเอนไปทางระมัดระวัง/กลัว";
462      case "neutral": return "กระแสเงินเข้า-ออกใกล้สมดุล — อารมณ์ตลาดเป็นกลาง";
463      case "greed1": return "กองทุน SPDR ทยอยสะสมทอง — อารมณ์ตลาดเอนไปทางโลภ";
464      default: return "เงินไหลเข้าทองหนัก — อารมณ์ตลาดสถาบันอยู่ในภาวะโลภมาก";
465    }
466  }
467  function buildGauge(host, score, zone) {
468    var NS = "http://www.w3.org/2000/svg";
469    var cx = 100, cy = 100, r = 80, sw = 16;
470    var bounds = [0, 25, 45, 55, 75, 100];
471    var colors = ["#c0392b", "#e2504a", "#d4b86a", "#9bcf6a", "#5dca8a"];
472    var ang = function (v) { return 180 - (v / 100) * 180; };
473    var svg = document.createElementNS(NS, "svg");
474    svg.setAttribute("viewBox", "0 0 200 124");
475    svg.setAttribute("class", "icf-gold-senti__svg");
476    for (var i = 0; i < colors.length; i++) {
477      var p = document.createElementNS(NS, "path");
478      p.setAttribute("d", sentiArc(cx, cy, r, ang(bounds[i]), ang(bounds[i + 1])));
479      p.setAttribute("fill", "none");
480      p.setAttribute("stroke", colors[i]);
481      p.setAttribute("stroke-width", sw);
482      svg.appendChild(p);
483    }
484    var na = ang(sentiClamp(score, 0, 100));
485    var tip = sentiPolar(cx, cy, r - 6, na);
486    var needle = document.createElementNS(NS, "line");
487    needle.setAttribute("x1", cx); needle.setAttribute("y1", cy);
488    needle.setAttribute("x2", tip.x.toFixed(2)); needle.setAttribute("y2", tip.y.toFixed(2));
489    needle.setAttribute("stroke", "#f5edd6"); needle.setAttribute("stroke-width", 3);
490    needle.setAttribute("stroke-linecap", "round");
491    svg.appendChild(needle);
492    var hub = document.createElementNS(NS, "circle");
493    hub.setAttribute("cx", cx); hub.setAttribute("cy", cy); hub.setAttribute("r", 6);
494    hub.setAttribute("fill", "#f5edd6");
495    svg.appendChild(hub);
496    var val = document.createElementNS(NS, "text");
497    val.setAttribute("x", cx); val.setAttribute("y", 90);
498    val.setAttribute("text-anchor", "middle"); val.setAttribute("class", "icf-gold-senti__val");
499    val.setAttribute("fill", zone.color); val.textContent = String(score);
500    svg.appendChild(val);
501    var lbl = document.createElementNS(NS, "text");
502    lbl.setAttribute("x", cx); lbl.setAttribute("y", 116);
503    lbl.setAttribute("text-anchor", "middle"); lbl.setAttribute("class", "icf-gold-senti__zone");
504    lbl.setAttribute("fill", zone.color); lbl.textContent = zone.label;
505    svg.appendChild(lbl);
506    host.innerHTML = "";
507    host.appendChild(svg);
508  }
509  function initSentiment(box) {
510    if (!CFG.spdrHistory) return;
511    var host = box.querySelector("[data-senti-gauge]");
512    fetch(CFG.spdrHistory + "?range=1m", { credentials: "same-origin" })
513      .then(function (r) { return r.ok ? r.json() : null; })
514      .then(function (j) {
515        if (!j || !j.series || !j.series.length) { if (host) host.innerHTML = '<div class="icf-gold-senti__loading">ข้อมูลไม่พร้อม</div>'; return; }
516        var s = j.series;
517        var valid = s.map(function (p) { return (p.change == null ? null : parseFloat(p.change)); })
518                     .filter(function (c) { return c != null && !isNaN(c); });
519        var net = valid.reduce(function (a, c) { return a + c; }, 0);
520        var up = valid.filter(function (c) { return c > 0; }).length;
521        var down = valid.filter(function (c) { return c < 0; }).length;
522        var start = parseFloat(s[0].tonnes) || 0;
523        var nChg = valid.length || 1;
524        var avg30 = net / nChg;
525        var last7 = valid.slice(-7);
526        var avg7 = last7.length ? last7.reduce(function (a, c) { return a + c; }, 0) / last7.length : avg30;
527        var momPerDay = avg7 - avg30;
528
529        // sentiment 0-100: 50% net flow · 30% buy:sell ratio · 20% momentum
530        var netPct = start > 0 ? (net / start * 100) : 0;
531        var netScore = sentiClamp(50 + (netPct / 6) * 50, 0, 100);        // ±6%/mo saturates
532        var dirTotal = up + down;
533        var ratioScore = dirTotal > 0 ? sentiClamp((up / dirTotal) * 100, 0, 100) : 50;
534        var momScore = sentiClamp(50 + (momPerDay / 3) * 50, 0, 100);     // ±3 t/day saturates
535        var score = sentiClamp(Math.round(0.50 * netScore + 0.30 * ratioScore + 0.20 * momScore), 0, 100);
536        var zone = sentiZone(score);
537
538        if (host) buildGauge(host, score, zone);
539
540        var netEl = box.querySelector('[data-senti="net"]');
541        if (netEl) {
542          var ncls = net > 0 ? "is-up" : (net < 0 ? "is-down" : "");
543          var narr = net > 0 ? "▲ +" : (net < 0 ? "▼ " : "• ");
544          netEl.innerHTML = '<em class="icf-gold-delta ' + ncls + '">' + narr + fmt(net, 2) + ' ตัน</em>';
545        }
546        var ratioEl = box.querySelector('[data-senti="ratio"]');
547        if (ratioEl) ratioEl.innerHTML = '<em class="icf-gold-delta is-up">' + up + '</em> : <em class="icf-gold-delta is-down">' + down + '</em>';
548        var momEl = box.querySelector('[data-senti="momentum"]');
549        if (momEl) {
550          var momTxt = momPerDay > 0.05 ? "▲ เริ่มชะลอ/ซื้อ" : (momPerDay < -0.05 ? "▼ เร่งขาย" : "• ทรงตัว");
551          var momCls = momPerDay > 0.05 ? "is-up" : (momPerDay < -0.05 ? "is-down" : "");
552          momEl.innerHTML = '<em class="icf-gold-delta ' + momCls + '">' + momTxt + ' (' + (momPerDay >= 0 ? "+" : "") + momPerDay.toFixed(2) + ' t/วัน)</em>';
553        }
554        var sumEl = box.querySelector('[data-senti="summary"]');
555        if (sumEl) sumEl.textContent = sentiSummary(zone);
556
557        // reuse for context card (no extra query)
558        var ctxS = document.querySelector('[data-ctx="senti"]');
559        if (ctxS) { ctxS.textContent = score + "/100"; ctxS.style.color = zone.color; }
560        var ctxSL = document.querySelector('[data-ctx="sentilabel"]');
561        if (ctxSL) ctxSL.textContent = zone.label;
562      })
563      .catch(function () { if (host) host.innerHTML = '<div class="icf-gold-senti__loading">คำนวณไม่ได้</div>'; });
564  }
565
566  /* ---------- share buttons (copy link) ---------- */
567  function initShare(box) {
568    var btn = box.querySelector(".icf-gold-share__copy");
569    if (!btn) return;
570    function fallback(text, cb) {
571      var ta = document.createElement("textarea");
572      ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
573      document.body.appendChild(ta); ta.focus(); ta.select();
574      try { document.execCommand("copy"); } catch (e) {}
575      document.body.removeChild(ta); cb();
576    }
577    btn.addEventListener("click", function () {
578      var url = btn.getAttribute("data-share-copy");
579      var label = btn.querySelector("span:last-child");
580      var done = function () {
581        if (!label) return;
582        var orig = label.textContent; label.textContent = "คัดลอกแล้ว ✓";
583        setTimeout(function () { label.textContent = orig; }, 1800);
584      };
585      if (navigator.clipboard && navigator.clipboard.writeText) {
586        navigator.clipboard.writeText(url).then(done).catch(function () { fallback(url, done); });
587      } else { fallback(url, done); }
588    });
589  }
590
591  /* ---------- stale-data badge (client-side, business-day aware) ---------- */
592  function initStale(box) {
593    var ds = box.getAttribute("data-spdr-date");
594    try {
595      var qp = new URLSearchParams(location.search).get("icf_asof");
596      if (qp) ds = qp; // debug/test override (badge reference date only)
597    } catch (e) {}
598    if (!ds) return;
599    var d = new Date(ds + "T00:00:00");
600    if (isNaN(d.getTime())) return;
601    var today = new Date(); today.setHours(0, 0, 0, 0);
602    var n = 0, cur = new Date(d);
603    while (cur < today) {
604      cur.setDate(cur.getDate() + 1);
605      var wd = cur.getDay();
606      if (wd !== 0 && wd !== 6) n++; // skip Sat/Sun
607    }
608    if (n > 2) {
609      var badge = box.querySelector("[data-icf-stale-badge]");
610      if (badge) badge.hidden = false;
611    }
612  }
613
614  /* ---------- boot ---------- */
615  function boot() {
616    $all(".icf-gold-calc").forEach(initCalc);
617    $all("[data-icf-gold-chart]").forEach(initChart);
618    $all("[data-icf-gold-spdr-chart]").forEach(initSpdrChart);
619    $all("[data-icf-gold-sentiment]").forEach(initSentiment);
620    $all("[data-icf-gold-ath]").forEach(initAth);
621    $all("[data-icf-gold-share]").forEach(initShare);
622    $all("[data-icf-stale]").forEach(initStale);
623    $all("[data-icf-gold-nav]").forEach(initNav);
624    if ($("[data-icf-gold-board]")) {
625      var every = (CFG.refresh || 60) * 1000;
626      setInterval(refresh, every);
627    }
628    if ($("[data-icf-gold-strip]")) {
629      setInterval(refreshStrip, (CFG.refresh || 60) * 1000);
630    }
631  }
632  if (document.readyState === "loading") {
633    document.addEventListener("DOMContentLoaded", boot);
634  } else {
635    boot();
636  }
637})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.