Backend security / security (push) Successful in 8s
Backend verify / verify (push) Failing after 2m17s
Conventional commits / commits (pull_request) Successful in 31s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 1m56s
97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
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()),
|
|
};
|
|
});
|
|
}
|