src/history/cache.ts
v0.3.0 · 2.9 KB
import type { TimeWindow } from './types.js';
/** One hour: the recommended quantum for candle cache keys. */
export const HOUR_MS = 3_600_000;
/**
* Floors both ends of a window to a multiple of `quantumMs`.
*
* Why: a cache keyed by `endTime = Date.now()` never hits - the key changes
* every millisecond, so every run refetches (and a disk cache named after the
* window never gets reused). Quantize the window, or pin a fixed period end.
* The freshest partial quantum is excluded by design; bars in it are not
* closed anyway.
*/
export function quantizeWindow(window: TimeWindow, quantumMs: number = HOUR_MS): TimeWindow {
if (!Number.isFinite(quantumMs) || quantumMs <= 0) throw new RangeError('history: quantumMs must be positive');
const q = (ms: number): number => Math.floor(ms / quantumMs) * quantumMs;
const startTime = q(window.startTime);
const endTime = Math.max(startTime, q(window.endTime));
return { startTime, endTime };
}
/**
* Window ending at `endMs` quantized, starting `lookbackMs` earlier:
* `periodStart = periodEnd - lookback`. Use it for "last N days" requests so
* repeated runs within one quantum share a cache key.
*/
export function lookbackWindow(endMs: number, lookbackMs: number, quantumMs: number = HOUR_MS): TimeWindow {
const q = quantizeWindow({ startTime: endMs, endTime: endMs }, quantumMs);
return { startTime: Math.max(0, q.endTime - lookbackMs), endTime: q.endTime };
}
export interface WindowCacheKey {
key: string;
/** The quantized window. Request exactly this window so the key matches the data. */
window: TimeWindow;
}
/** Cache key `coin|interval|start|end` over a quantized window. */
export function candleCacheKey(
params: { coin: string; interval: string } & TimeWindow,
quantumMs: number = HOUR_MS,
): WindowCacheKey {
const window = quantizeWindow(params, quantumMs);
return { key: `candles|${params.coin}|${params.interval}|${window.startTime}|${window.endTime}`, window };
}
/**
* Cache key `user|start|end` over a quantized window. The address is
* lowercased. There is no dex in the key: fills of all dexes come in one request.
*/
export function fillsCacheKey(user: string, window: TimeWindow, quantumMs: number = HOUR_MS): WindowCacheKey {
const q = quantizeWindow(window, quantumMs);
return { key: `fills|${user.toLowerCase()}|${q.startTime}|${q.endTime}`, window: q };
}
/** One UTC day in ms. */
export const DAY_MS = 86_400_000;
/**
* The UTC day containing `ms` as an inclusive window for `userFillsByTime`:
* `start = Date.UTC(y, m, d)`, `end = start + 86_400_000 - 1`. `endTime` is
* inclusive on HL, so `start + 86_400_000` would pull in the first millisecond
* of the next day.
*/
export function utcDayWindow(ms: number): TimeWindow {
if (!Number.isFinite(ms)) throw new RangeError('history: ms must be finite');
const startTime = Math.floor(ms / DAY_MS) * DAY_MS;
return { startTime, endTime: startTime + DAY_MS - 1 };
}