Skip to content
markpaper

src/history/cache.ts

v0.3.0 · 2.9 KB

Download file
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 };
}
All files