Repository Public
1// Which symbols the market data engine ingests. Nothing is listed by hand: the set is derived from the token registry —2// every market symbol that a registry asset tracks on a network where swap execution is enabled, and that two of the3// three exchanges list against USD (static table packages/core/src/market-listings.ts) — plus the base set of V3.4//5// Budget: a new symbol costs about 60 requests per source for two years of 1h bars (Coinbase, 298 bars per call), about6// 12 for the daily history, and one request per interval and per source at each new bar; Kraken calls are 3 s apart, so7// N symbols take 3·N seconds of Kraken time at the top of each hour. Hence:8// - SPW_MARKET_MAX_SYMBOLS (default 40) bounds the set; symbols an agent trades are never left out and come first;9// - SPW_MARKET_SYMBOLS=BTC,ETH,… replaces the base and registry-derived part with an explicit list (agents' symbols still added);10// - 5-minute bars are ingested for the base set only (SPW_MARKET_5M_SYMBOLS to change): no agent decides on 5m.11import { getNetwork, hasMarketData, KYBER_ENABLED, tradableSymbols } from '@spw/core';12import { sql } from '../db.ts';13import { marketConfig, type MarketConfig } from './pipeline.ts';14import { SYMBOLS } from './types.ts';1516/** Market symbols tradable on at least one execution-enabled network, most widely available first. */17export function registrySymbols(networks: readonly string[] = Object.keys(KYBER_ENABLED)): string[] {18 const count = new Map<string, number>();19 for (const id of networks) for (const t of tradableSymbols(getNetwork(id))) count.set(t.symbol, (count.get(t.symbol) ?? 0) + 1);20 return [...count.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([s]) => s);21}2223const list = (v: string | undefined): string[] | null => (v === undefined || v.trim() === '' ? null : [...new Set(v.split(',').map((s) => s.trim()).filter(Boolean))]);24export interface SymbolSet { symbols: string[]; first: string[]; fiveMinute: string[]; dropped: string[] }25/** Order: symbols used by an agent, the base set, then the registry-derived ones; cut at the size limit (never an agent's symbol). */26export function configuredSymbols(agentSymbols: readonly string[] = [], env: Record<string, string | undefined> = process.env, registry: readonly string[] = registrySymbols()): SymbolSet {27 const first = [...new Set(agentSymbols)].filter((s) => hasMarketData(s));28 const explicit = list(env.SPW_MARKET_SYMBOLS);29 const rest = (explicit ?? [...SYMBOLS, ...registry]).filter((s) => hasMarketData(s));30 const max = Math.max(1, Math.floor(Number(env.SPW_MARKET_MAX_SYMBOLS ?? 40)) || 40);31 const ordered = [...new Set([...first, ...rest])];32 const symbols = ordered.filter((s, k) => k < max || first.includes(s));33 const five = (list(env.SPW_MARKET_5M_SYMBOLS) ?? [...SYMBOLS]).filter((s) => symbols.includes(s));34 return { symbols, first, fiveMinute: five, dropped: ordered.filter((s) => !symbols.includes(s)) };35}3637/** The configuration in force now: the registry-derived set, with the symbols of every agent that is not stopped first. */38export async function activeMarketConfig(): Promise<MarketConfig> {39 let used: string[] = [];40 try { used = (await sql<{ universe: string[] }[]>`select universe from agents where state not in ('STOPPED', 'FAILED') and kind = 'spot'`).flatMap((r) => r.universe); } catch { /* no agents table yet: the default set */ }41 return marketConfig(used);42}43