import { MILLISECONDS_PER_DAY } from '../../../../common/time'; export type CandleRange = { from: Date; to: Date }; export type CandleCoverage = CandleRange & { isComplete: boolean; fetchedAt: Date; }; export type SavedCandle = { time: string; close: string; isComplete: boolean; }; 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(1).map((to, index) => { const from = sorted[index]; return { from: new Date(from), to: new Date(to), fetchedAt, isComplete: to <= midnight && !incompleteDays.has(utcDay(new Date(from)).valueOf()), }; }); }