import { MILLISECONDS_PER_DAY } from '../../../../common/time'; const FIRST_RANGE_END_INDEX = 1; export type CandleRange = { from: Date; to: Date }; export type CandleCoverage = CandleRange & { fetchedAt: Date; isComplete: boolean; }; export type SavedCandle = { close: string; isComplete: boolean; time: string; }; const windowDays = 365; const tailFreshForMs = 60_000; export function utcDay(value: Date): Date { return new Date( Math.floor(value.valueOf() / MILLISECONDS_PER_DAY) * MILLISECONDS_PER_DAY, ); } // A clock transition never turns an unconfirmed range into closed coverage. export function candleGaps( range: CandleRange, coverage: CandleCoverage[], now: Date, ): CandleRange[] { let gaps = [range]; for (const covered of coverage) { if ( !covered.isComplete && (now.valueOf() - covered.fetchedAt.valueOf() >= tailFreshForMs || utcDay(now).valueOf() !== utcDay(covered.fetchedAt).valueOf()) ) { continue; } gaps = gaps.flatMap((gap) => { if (covered.to <= gap.from || covered.from >= gap.to) return [gap]; return [ ...(covered.from > gap.from ? [{ from: gap.from, to: covered.from }] : []), ...(covered.to < gap.to ? [{ from: covered.to, to: gap.to }] : []), ]; }); } return gaps.filter((gap) => gap.from < gap.to); } export function candleWindows(range: CandleRange): CandleRange[] { const windows: CandleRange[] = []; for (let start = range.from.valueOf(); start < range.to.valueOf();) { const end = Math.min( start + windowDays * MILLISECONDS_PER_DAY, range.to.valueOf(), ); windows.push({ from: new Date(start), to: new Date(end) }); start = end; } return windows; } export function confirmedCandleCoverage( range: CandleRange, candles: SavedCandle[], fetchedAt: Date, ): CandleCoverage[] { const boundaries = new Set([range.from.valueOf(), range.to.valueOf()]); const midnight = utcDay(fetchedAt).valueOf(); const incompleteDays = new Set( candles .filter((candle) => !candle.isComplete) .map((candle) => utcDay(new Date(candle.time)).valueOf()), ); for (const boundary of [ midnight, ...[...incompleteDays].flatMap((day) => [day, day + MILLISECONDS_PER_DAY]), ]) { if (boundary > range.from.valueOf() && boundary < range.to.valueOf()) { boundaries.add(boundary); } } const sorted = [...boundaries].sort((a, b) => a - b); return sorted.slice(FIRST_RANGE_END_INDEX).map((to, index) => { // eslint-disable-next-line security/detect-object-injection -- The index comes from the sorted numeric boundary array. const from = sorted[index]; return { from: new Date(from), to: new Date(to), fetchedAt, isComplete: to <= midnight && !incompleteDays.has(utcDay(new Date(from)).valueOf()), }; }); }