SPB Git

Repository Public

validate.ts 2.7 KB 50 lines typescript

1// Validation and normalisation of raw bars, and gap counting. Pure functions: no clock, no network, no database.2import { INTERVAL_MS, isClosed, type Candle, type Interval } from './types.ts';34export type RejectReason = 'not_finite' | 'non_positive_price' | 'negative_volume' | 'high_below_low' | 'open_out_of_range' | 'close_out_of_range'5  | 'off_grid' | 'duplicate' | 'future' | 'not_closed';6export interface Rejected { ts: number; reason: RejectReason }78function reasonFor(c: Candle, interval: Interval, now: number): RejectReason | null {9  const iv = INTERVAL_MS[interval];10  if (![c.ts, c.open, c.high, c.low, c.close, c.volume].every(Number.isFinite)) return 'not_finite';11  if (!Number.isInteger(c.ts) || c.ts % iv !== 0) return 'off_grid';12  if (c.ts > now) return 'future';13  if (!isClosed(c.ts, interval, now)) return 'not_closed';14  if (c.open <= 0 || c.high <= 0 || c.low <= 0 || c.close <= 0) return 'non_positive_price';15  if (c.volume < 0) return 'negative_volume';16  if (c.high < c.low) return 'high_below_low';17  if (c.open < c.low || c.open > c.high) return 'open_out_of_range';18  if (c.close < c.low || c.close > c.high) return 'close_out_of_range';19  return null;20}2122/** Keeps the possible bars. A timestamp that appears more than once is rejected entirely (every copy): there is no way23 *  to know which copy is right. */24export function validateBars(raw: Candle[], interval: Interval, now: number): { bars: Candle[]; rejected: Rejected[] } {25  const rejected: Rejected[] = [], seen = new Map<number, number>();26  for (const c of raw) seen.set(c.ts, (seen.get(c.ts) ?? 0) + 1);27  const bars: Candle[] = [];28  for (const c of raw) {29    const reason = reasonFor(c, interval, now) ?? ((seen.get(c.ts) ?? 0) > 1 ? 'duplicate' : null);30    if (reason) rejected.push({ ts: c.ts, reason });31    else bars.push(c);32  }33  return { bars, rejected };34}3536/** Canonical form stored and served: oldest first, exactly the six fields, bar-open time in UTC milliseconds. */37export const normalise = (bars: Candle[]): Candle[] => bars.map((c) => ({ ts: c.ts, open: c.open, high: c.high, low: c.low, close: c.close, volume: c.volume })).sort((x, y) => x.ts - y.ts);3839export interface GapRange { from: number; to: number; missing: number }40/** Missing grid slots between consecutive timestamps (sorted, oldest first). `from`/`to` are the first and last missing bar-open times. */41export function findGaps(ts: number[], interval: Interval): { gaps: number; ranges: GapRange[] } {42  const iv = INTERVAL_MS[interval], ranges: GapRange[] = [];43  let gaps = 0;44  for (let i = 1; i < ts.length; i++) {45    const missing = Math.round((ts[i]! - ts[i - 1]!) / iv) - 1;46    if (missing > 0) { gaps += missing; ranges.push({ from: ts[i - 1]! + iv, to: ts[i]! - iv, missing }); }47  }48  return { gaps, ranges };49}50
Files main
Menu

Theme