SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
12 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%
4.0 KB · 86 lines typescript
Raw Blame History
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