fix(api): fill-forward missing candle buckets

This commit is contained in:
u1
2026-02-02 23:14:18 +01:00
parent 144f6e7c86
commit cd4cbec7e0

View File

@@ -755,6 +755,62 @@ function parseTimeframeToSeconds(tf) {
return num * mult; 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) { function pickFlowPointBucketSeconds(bucketSeconds, rowsPerCandle) {
// We want a point step that is: // We want a point step that is:
// - small enough to capture intra-candle direction, // - small enough to capture intra-candle direction,
@@ -1014,7 +1070,9 @@ async function handler(cfg, req, res) {
rows = data?.[fn] || []; rows = data?.[fn] || [];
} }
const candles = rows const nowSec = Math.floor(Date.now() / 1000);
let candles = rows
.slice() .slice()
.reverse() .reverse()
.map((r) => { .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)); .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. // Flow = share of time spent moving up/down/flat inside each bucket.
// Used by the UI to render stacked volume bars describing microstructure. // Used by the UI to render stacked volume bars describing microstructure.
const nowSec = Math.floor(Date.now() / 1000);
const windowSeconds = bucketSeconds * candles.length; const windowSeconds = bucketSeconds * candles.length;
const canComputeFlow = candles.length > 0 && windowSeconds > 0 && windowSeconds <= 86_400; // cap at 24h const canComputeFlow = candles.length > 0 && windowSeconds > 0 && windowSeconds <= 86_400; // cap at 24h
const rowsPerCandle = Math.min(60, Math.max(12, Math.floor(bucketSeconds))); const rowsPerCandle = Math.min(60, Math.max(12, Math.floor(bucketSeconds)));