diff --git a/kustomize/base/api/server.mjs b/kustomize/base/api/server.mjs index 9677b0e..3d964f2 100644 --- a/kustomize/base/api/server.mjs +++ b/kustomize/base/api/server.mjs @@ -755,6 +755,62 @@ function parseTimeframeToSeconds(tf) { return num * mult; } +function fillForwardCandles(candles, { bucketSeconds, limit, nowSec }) { + if (!Array.isArray(candles) || candles.length === 0) return []; + if (!Number.isFinite(bucketSeconds) || bucketSeconds <= 0) return candles; + if (!Number.isFinite(limit) || limit <= 0) return candles; + + // `candles` should be ascending by time. + const cleaned = candles + .filter((c) => c && Number.isFinite(c.time) && Number.isFinite(c.close)) + .slice() + .sort((a, b) => a.time - b.time); + + if (cleaned.length === 0) return []; + + const map = new Map(cleaned.map((c) => [c.time, c])); + const lastDataTime = cleaned[cleaned.length - 1].time; + const now = Number.isFinite(nowSec) ? nowSec : Math.floor(Date.now() / 1000); + const alignedNow = Math.floor(now / bucketSeconds) * bucketSeconds; + const endTime = Math.max(alignedNow, lastDataTime); + const startTime = endTime - bucketSeconds * (limit - 1); + + const baseline = cleaned[0]; + const out = []; + out.length = limit; + + let prev = null; + for (let i = 0; i < limit; i += 1) { + const t = startTime + i * bucketSeconds; + const hit = map.get(t); + if (hit) { + const c = { ...hit }; + c.volume = Number.isFinite(c.volume) ? c.volume : 0; + out[i] = c; + prev = c; + continue; + } + + const base = prev || baseline; + const close = Number(base.close); + const oracle = base.oracle == null ? null : Number(base.oracle); + + const filled = { + time: t, + open: close, + high: close, + low: close, + close, + volume: 0, + oracle: Number.isFinite(oracle) ? oracle : null, + }; + out[i] = filled; + prev = filled; + } + + return out.filter((c) => c && Number.isFinite(c.time) && Number.isFinite(c.open) && Number.isFinite(c.close)); +} + function pickFlowPointBucketSeconds(bucketSeconds, rowsPerCandle) { // We want a point step that is: // - small enough to capture intra-candle direction, @@ -1014,7 +1070,9 @@ async function handler(cfg, req, res) { rows = data?.[fn] || []; } - const candles = rows + const nowSec = Math.floor(Date.now() / 1000); + + let candles = rows .slice() .reverse() .map((r) => { @@ -1029,9 +1087,12 @@ async function handler(cfg, req, res) { }) .filter((c) => Number.isFinite(c.time) && Number.isFinite(c.open) && Number.isFinite(c.close)); + // Make candles continuous in time: if no tick happened in a bucket, emit a flat candle using last close. + // This keeps the chart stable for 1s/3s/... views and makes timeframe switching instant (cache + no gaps). + candles = fillForwardCandles(candles, { bucketSeconds, limit, nowSec }); + // Flow = share of time spent moving up/down/flat inside each bucket. // Used by the UI to render stacked volume bars describing microstructure. - const nowSec = Math.floor(Date.now() / 1000); const windowSeconds = bucketSeconds * candles.length; const canComputeFlow = candles.length > 0 && windowSeconds > 0 && windowSeconds <= 86_400; // cap at 24h const rowsPerCandle = Math.min(60, Math.max(12, Math.floor(bucketSeconds)));