spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import { defineConnector, raw, type NormalizedBatch } from "@market-atlas/connector-sdk";2import { parseTimestamp } from "@market-atlas/market-model";3import { MAJOR_BASES } from "../_shared/crypto.js";4import { pairObservations, planPair } from "../_shared/pairs.js";56const BULLET_URL = "https://api.kucoin.com/api/v1/bullet-public";7const SYMBOLS = [...MAJOR_BASES.filter((b) => b !== "PAXG").map((b) => `${b}-USDT`), "EUR-USDT"];89/**10 * KuCoin spot ticker (`/market/ticker:<symbols>`). The public WebSocket endpoint is obtained through a11 * keyless "bullet" token request (no account), then one connection carries every symbol.12 */13export const kucoinWs = defineConnector({14 metadata: {15 id: "kucoin-ws",16 name: "KuCoin — spot ticker",17 version: "1.0.0",18 sourceId: "kucoin",19 organization: "KuCoin",20 sourceType: "WEBSOCKET",21 jurisdiction: "SC",22 rightsStatus: "PUBLIC_ATTRIBUTED",23 realtimeStatus: "REALTIME",24 expectedLatencyMs: 500,25 supportsStreaming: true,26 supportsHistorical: false,27 assetClasses: ["CRYPTO", "FOREX"],28 exchanges: ["kucoin"],29 homepage: "https://www.kucoin.com/docs/websocket/spot-trading/public-channels/ticker",30 description: "Public KuCoin spot ticker topic (last price, best bid/ask with sizes) for USDT majors and the EUR/USDT stablecoin market. Endpoint discovered via the keyless public bullet token.",31 rightsNotes: "Public market data displayed with attribution to KuCoin.",32 termsUrl: "https://www.kucoin.com/legal/terms-of-use",33 sourceFamily: "kucoin",34 enabled: true,35 },36 seeds: SYMBOLS.map((s) => {37 const [b, q] = s.split("-") as [string, string];38 return { symbol: s, hint: planPair(b, q, "kucoin").hint };39 }),40 defaultSymbols: SYMBOLS,41 rateLimits: { "api.kucoin.com": 1 },42 async start(ctx) {43 const res = await ctx.http.request(BULLET_URL, { method: "POST", conditional: false, timeoutMs: 15_000, headers: { "content-type": "application/json" } });44 const bullet = JSON.parse(res.text) as { data?: { token?: string; instanceServers?: Array<{ endpoint: string; pingInterval?: number }> } };45 const server = bullet.data?.instanceServers?.[0];46 const token = bullet.data?.token;47 if (!server || !token) throw new Error("kucoin bullet token unavailable");48 const url = `${server.endpoint}?token=${encodeURIComponent(token)}&connectId=market-atlas`;49 const ws = ctx.openWebSocket(url, {50 label: "kucoin",51 staleAfterMs: 60_000,52 heartbeat: { intervalMs: Math.max(5000, Math.min(server.pingInterval ?? 18_000, 30_000) - 3000), message: JSON.stringify({ id: "hb", type: "ping" }) },53 onOpen: (sock) => sock.send({ id: "1", type: "subscribe", topic: `/market/ticker:${ctx.watchedSymbols().join(",")}`, response: true }),54 onMessage: (data) => {55 let msg: any;56 try {57 msg = JSON.parse(data);58 } catch {59 return;60 }61 if (msg?.type === "message" && typeof msg.topic === "string" && msg.topic.startsWith("/market/ticker:") && msg.data) ctx.emit(raw("kucoin-ws", "kucoin", "ticker", msg));62 else if (msg?.type === "error") ctx.reportError(new Error(String(msg.data ?? "kucoin error")));63 },64 });65 ws.connect();66 },67 normalize(r): NormalizedBatch {68 const m = r.payload as { topic?: string; data?: Record<string, unknown> };69 const d = m.data;70 if (r.kind !== "ticker" || typeof m.topic !== "string" || !d) return { observations: [] };71 const symbol = m.topic.slice("/market/ticker:".length);72 const [base, quote] = symbol.split("-");73 if (!base || !quote) return { observations: [] };74 return {75 observations: pairObservations(symbol, base, quote, "kucoin", { last: d.price, bid: d.bestBid, bidSize: d.bestBidSize, ask: d.bestAsk, askSize: d.bestAskSize }, {76 sourceTimestamp: parseTimestamp(d.time),77 timestampTrust: "EXCHANGE",78 sequence: typeof d.sequence === "string" ? d.sequence : null,79 rightsStatus: "PUBLIC_ATTRIBUTED",80 realtimeStatus: "REALTIME",81 }),82 };83 },84 fixturesDir: "fixtures",85});86