Repository Public
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