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 { MAJOR_BASES, cryptoSeeds } from "../_shared/crypto.js";3import { pairObservations, planPair } from "../_shared/pairs.js";4import type { ProposedInstrument } from "@market-atlas/connector-sdk";56const WS_URL = "wss://ws.kraken.com/v2";7const CRYPTO_PAIRS: Array<[string, string]> = [...MAJOR_BASES.filter((b) => b !== "PAXG").map((b) => [b, "USD"] as [string, string]), ["BTC", "USDT"], ["ETH", "USDT"], ["BTC", "EUR"], ["ETH", "EUR"]];8/** Kraken runs real fiat/fiat spot markets — a live, keyless FX source. */9const FIAT_PAIRS: Array<[string, string]> = [["EUR", "USD"], ["GBP", "USD"], ["USD", "JPY"], ["USD", "CAD"], ["USD", "CHF"], ["AUD", "USD"], ["EUR", "GBP"], ["EUR", "JPY"], ["EUR", "CHF"], ["EUR", "CAD"], ["EUR", "AUD"], ["AUD", "JPY"]];10const PAIRS = [...CRYPTO_PAIRS, ...FIAT_PAIRS];11const SYMBOLS = PAIRS.map(([b, q]) => `${b}/${q}`);12const fiatSeeds: ProposedInstrument[] = FIAT_PAIRS.map(([b, q]) => ({ symbol: `${b}/${q}`, hint: planPair(b, q, "kraken").hint }));1314/** Kraken WebSocket API v2, `ticker` channel (event trigger: trades). No per-message timestamp → connector receive time. */15export const krakenWs = defineConnector({16 metadata: {17 id: "kraken-ws",18 name: "Kraken — ticker feed (WS v2)",19 version: "1.0.0",20 sourceId: "kraken",21 organization: "Payward, Inc.",22 sourceType: "WEBSOCKET",23 jurisdiction: "US",24 rightsStatus: "PUBLIC_ATTRIBUTED",25 realtimeStatus: "REALTIME",26 expectedLatencyMs: 400,27 supportsStreaming: true,28 supportsHistorical: false,29 assetClasses: ["CRYPTO", "FOREX"],30 exchanges: ["kraken"],31 homepage: "https://docs.kraken.com/api/docs/websocket-v2/ticker",32 description: "Public Kraken WebSocket v2 ticker channel (best bid/ask, last trade, 24h volume/VWAP/high/low) for the major crypto spot pairs and Kraken's 12 fiat/fiat markets (EUR/USD, USD/JPY, GBP/USD, USD/CAD…) — a live, keyless FX source.",33 rightsNotes: "Public market data displayed with attribution to Kraken.",34 termsUrl: "https://www.kraken.com/legal",35 sourceFamily: "kraken",36 enabled: true,37 },38 seeds: [...cryptoSeeds(CRYPTO_PAIRS, "kraken", (b, q) => `${b}/${q}`), ...fiatSeeds],39 defaultSymbols: SYMBOLS,40 async start(ctx) {41 const ws = ctx.openWebSocket(WS_URL, {42 label: "kraken",43 staleAfterMs: 90_000,44 heartbeat: { intervalMs: 25_000, message: JSON.stringify({ method: "ping" }) },45 onOpen: (sock) => sock.send({ method: "subscribe", params: { channel: "ticker", symbol: ctx.watchedSymbols(), event_trigger: "trades", snapshot: true } }),46 onMessage: (data) => {47 let msg: any;48 try {49 msg = JSON.parse(data);50 } catch {51 return;52 }53 if (msg?.channel === "ticker" && Array.isArray(msg.data)) ctx.emit(raw("kraken-ws", "kraken", "ticker", msg));54 else if (msg?.method === "subscribe" && msg.success === false) ctx.reportError(new Error(String(msg.error ?? "subscribe failed")), { symbol: msg.result?.symbol });55 },56 });57 ws.connect();58 },59 normalize(r): NormalizedBatch {60 const m = r.payload as { channel?: string; data?: Array<Record<string, unknown>> };61 if (r.kind !== "ticker" || m.channel !== "ticker" || !Array.isArray(m.data)) return { observations: [] };62 const observations = [];63 for (const t of m.data) {64 if (typeof t.symbol !== "string") continue;65 const [base, quote] = t.symbol.split("/");66 if (!base || !quote) continue;67 observations.push(68 ...pairObservations(t.symbol, base, quote, "kraken", { last: t.last, high: t.high, low: t.low, bid: t.bid, ask: t.ask, bidSize: t.bid_qty, askSize: t.ask_qty, volume: t.volume, vwap: t.vwap }, {69 sourceTimestamp: null,70 timestampTrust: "CONNECTOR",71 rightsStatus: "PUBLIC_ATTRIBUTED",72 realtimeStatus: "REALTIME",73 }),74 );75 }76 return { observations };77 },78 fixturesDir: "fixtures",79});80