SPB Git

Repository Public

routes.ts 2.8 KB 40 lines typescript

1// Read-only routes of the market data engine (authentication is the global hook of server.ts).2import type { FastifyInstance } from 'fastify';3import { z } from 'zod';4import { QUALITY_WINDOW, rejectedCount } from './pipeline.ts';5import { activeMarketConfig } from './symbols.ts';6import { DEFAULT_PRIORITY, DEFAULT_QUALITY } from './quality.ts';7import { getSeries } from './series.ts';8import { defaultSources } from './sources.ts';9import { coverage, listQuality } from './store.ts';10import { INTERVAL_MS, isSymbol, type Interval } from './types.ts';1112const RULE = 'for each bar-open time: the whole bar of the first source, in priority order, that has it; never a blend of sources';13let cached: { at: number; v: unknown } | null = null;1415export function marketDataRoutes(app: FastifyInstance): void {16  app.get('/api/v1/market-data/status', async () => {17    if (cached && Date.now() - cached.at < 30_000) return cached.v;18    const cfg = await activeMarketConfig(), sources = defaultSources();19    const [cov, quality] = await Promise.all([coverage(), listQuality()]);20    const v = {21      symbols: cfg.symbols, intervals: cfg.intervals, only: cfg.only ?? {}, first: cfg.first ?? [], dropped: cfg.dropped ?? [], priority: DEFAULT_PRIORITY, rule: RULE,22      thresholds: { maxDeviationBps: DEFAULT_QUALITY.maxDeviationBps, agreeBars: DEFAULT_QUALITY.agreeBars, minSources: DEFAULT_QUALITY.minSources, maxLagBars: DEFAULT_QUALITY.maxLagBars, qualityWindow: QUALITY_WINDOW },23      sources: sources.map((s) => ({ id: s.id, maxBars: s.maxBars, historyBars: s.historyBars, pairs: Object.fromEntries(cfg.symbols.flatMap((x) => (s.pairs[x] ? [[x, s.pairs[x]]] : []))) })),24      coverage: cov.map((c) => ({ source: c.source, symbol: c.symbol, interval: c.interval, bars: c.bars, first: c.first_ts, last: c.last_ts })),25      quality: quality.map((q) => ({ symbol: q.symbol, interval: q.interval, status: q.status, fresh: q.fresh, checkedAt: q.checked_at, lastTs: q.last_ts, gaps: q.gaps,26        maxDeviationBps: q.max_deviation_bps, failures: q.detail.failures, window: q.detail.window, provided: q.detail.provided, sources: q.detail.sources })),27      rejectedSinceStart: rejectedCount,28    };29    cached = { at: Date.now(), v };30    return v;31  });3233  app.get('/api/v1/market-data/candles', async (req) => {34    const q = z.object({ symbol: z.string().max(12).refine(isSymbol, 'no market data for this symbol'), interval: z.enum(Object.keys(INTERVAL_MS) as [Interval, ...Interval[]]),35      limit: z.coerce.number().int().min(1).max(5000).default(500), until: z.coerce.number().int().positive().optional() }).parse(req.query);36    const s = await getSeries(q.symbol, q.interval, { limit: q.limit, ...(q.until === undefined ? {} : { until: q.until }) });37    return { symbol: s.symbol, interval: s.interval, rule: RULE, priority: s.priority, gaps: s.gaps, provided: s.provided, bars: s.bars };38  });39}40
Files main
Menu

Theme