fix(api): fill-forward missing candle buckets
This commit is contained in:
@@ -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)));
|
||||||
|
|||||||
Reference in New Issue
Block a user