fix: Date.UTC spread operator and EMA for rolling sats chart
This commit is contained in:
36
index.html
36
index.html
@ -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 },
|
||||||
|
|||||||
Reference in New Issue
Block a user