fix: Date.UTC spread operator and EMA for rolling sats chart

This commit is contained in:
Dione
2026-06-10 19:17:32 +00:00
parent 0ad8f03990
commit b659d26ad1

View File

@ -1571,24 +1571,24 @@ function setupCumulCard(cutoff) {
function setupSatsCard(cutoff) { function setupSatsCard(cutoff) {
const DAY_MS = 86400000; const DAY_MS = 86400000;
/* Derive daily balances (latest snapshot per day) */ /* Derive daily balances (latest snapshot per day) using UTC date strings */
const dailyBalances = {}; const dailyBalances = {};
const selectedAddr = getSelectedAddresses() || []; const selectedAddr = getSelectedAddresses() || [];
selectedAddr.forEach(addr => { selectedAddr.forEach(addr => {
const snaps = addressSnapshots[addr] || []; const snaps = addressSnapshots[addr] || [];
snaps.forEach(snap => { snaps.forEach(snap => {
const dateStr = snap.block_timestamp.slice(0, 10); const utcTs = new Date(snap.block_timestamp).getTime();
const dateStr = new Date(utcTs).toISOString().slice(0, 10);
const btcVal = getTokenAmount(snap?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(snap?.collateral?.cbBTC, 'cbBTC'); const btcVal = getTokenAmount(snap?.wallet?.cbBTC, 'cbBTC') + getTokenAmount(snap?.collateral?.cbBTC, 'cbBTC');
const ts = new Date(snap.block_timestamp).getTime(); if (!dailyBalances[dateStr] || utcTs > dailyBalances[dateStr].ts) {
if (!dailyBalances[dateStr] || ts > dailyBalances[dateStr].ts) { dailyBalances[dateStr] = { ts: utcTs, dateStr, btcVal };
dailyBalances[dateStr] = { ts, dateStr, btcVal };
} else { } else {
dailyBalances[dateStr].btcVal += btcVal; dailyBalances[dateStr].btcVal += btcVal;
} }
}); });
}); });
/* Sort by date, compute daily delta (acquisition), then fill continuous timeline */ /* Sort by date, compute daily delta (acquisition) */
const sortedDays = Object.values(dailyBalances).sort((a, b) => a.dateStr.localeCompare(b.dateStr)); const sortedDays = Object.values(dailyBalances).sort((a, b) => a.dateStr.localeCompare(b.dateStr));
const dailyDeltas = {}; const dailyDeltas = {};
sortedDays.forEach((d, i) => { sortedDays.forEach((d, i) => {
@ -1596,29 +1596,31 @@ function setupSatsCard(cutoff) {
dailyDeltas[d.dateStr] = d.btcVal - prevBalance; dailyDeltas[d.dateStr] = d.btcVal - prevBalance;
}); });
/* Fill continuous daily timeline so no days are skipped */ /* Fill continuous daily timeline — iterate every day from first to last */
const firstDate = new Date(sortedDays[0].dateStr).getTime(); const firstUTC = Date.UTC(new Date(sortedDays[0].dateStr).getUTCFullYear(), new Date(sortedDays[0].dateStr).getUTCMonth(), new Date(sortedDays[0].dateStr).getUTCDate());
const lastDate = new Date(sortedDays[sortedDays.length - 1].dateStr).getTime(); const lastUTC = Date.UTC(new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCFullYear(), new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCMonth(), new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCDate());
const continuousDays = []; const continuousDays = [];
for (let t = firstDate; t <= lastDate; t += DAY_MS) { for (let t = firstUTC; t <= lastUTC; t += DAY_MS) {
const ds = new Date(t).toISOString().slice(0, 10); const ds = new Date(t).toISOString().slice(0, 10);
continuousDays.push({ ts: t, dateStr: ds, delta: dailyDeltas[ds] || 0 }); continuousDays.push({ ts: t, dateStr: ds, delta: dailyDeltas[ds] || 0 });
} }
/* 30-day rolling average of daily deltas */ /* Exponentially weighted moving average — older days decay so average drops when there are no acquisitions */
const filtered = continuousDays.filter(d => d.ts >= cutoff); const filtered = continuousDays.filter(d => d.ts >= cutoff);
const rollingAvg = []; const halfLife = 7;
const decay = Math.pow(0.5, 1 / halfLife);
let ewmaVal = filtered[0]?.delta || 0;
const emaSats = [];
for (let i = 0; i < filtered.length; i++) { for (let i = 0; i < filtered.length; i++) {
const window = filtered.slice(Math.max(0, i - 29), i + 1); ewmaVal = filtered[i].delta + decay * ewmaVal;
const avgSats = Math.round(window.reduce((s, d) => s + d.delta, 0) / window.length * 1e8); emaSats.push([filtered[i].ts, Math.round(ewmaVal * 1e8)]);
rollingAvg.push([filtered[i].ts, avgSats]);
} }
const latestAvg = rollingAvg.length > 0 ? rollingAvg[rollingAvg.length - 1][1] : 0; const latestAvg = emaSats.length > 0 ? emaSats[emaSats.length - 1][1] : 0;
document.getElementById('avg-sats-val').innerText = new Intl.NumberFormat('en-US').format(Math.round(latestAvg)); document.getElementById('avg-sats-val').innerText = new Intl.NumberFormat('en-US').format(Math.round(latestAvg));
const options = { const options = {
chart: { id: 'instance-sats', type: 'area', height: 200, background: 'transparent', toolbar: { show: false }, sparkline: { enabled: false }, zoom: { enabled: true, type: 'x', autoScaleYaxis: true }, animations: { enabled: false } }, chart: { id: 'instance-sats', type: 'area', height: 200, background: 'transparent', toolbar: { show: false }, sparkline: { enabled: false }, zoom: { enabled: true, type: 'x', autoScaleYaxis: true }, animations: { enabled: false } },
series: [{ name: 'Avg Sats/Day', data: rollingAvg }], series: [{ name: 'Avg Sats/Day', data: emaSats }],
dataLabels: { enabled: false }, dataLabels: { enabled: false },
colors: [orangeBrandColor], colors: [orangeBrandColor],
stroke: { curve: 'smooth', width: 2 }, stroke: { curve: 'smooth', width: 2 },