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 type { NormalizedObservation } from "@market-atlas/market-model";3import { pairObservations, planPair } from "../_shared/pairs.js";45const WS_URL = "wss://ws.bitstamp.net";6/** Bitstamp url_symbol → [base, quote]. Includes the two real fiat markets (EUR/USD, GBP/USD). */7const PAIRS: Record<string, [string, string]> = {8 btcusd: ["BTC", "USD"],9 ethusd: ["ETH", "USD"],10 xrpusd: ["XRP", "USD"],11 ltcusd: ["LTC", "USD"],12 solusd: ["SOL", "USD"],13 adausd: ["ADA", "USD"],14 linkusd: ["LINK", "USD"],15 dogeusd: ["DOGE", "USD"],16 btceur: ["BTC", "EUR"],17 etheur: ["ETH", "EUR"],18 btcusdt: ["BTC", "USDT"],19 eurusd: ["EUR", "USD"],20 gbpusd: ["GBP", "USD"],21};2223/** Bitstamp public WebSocket v2: live trades (price) + top of the order book (bid/ask) per pair. */24export const bitstampWs = defineConnector({25 metadata: {26 id: "bitstamp-ws",27 name: "Bitstamp — live trades & order book",28 version: "1.0.0",29 sourceId: "bitstamp",30 organization: "Bitstamp Ltd",31 sourceType: "WEBSOCKET",32 jurisdiction: "GB",33 rightsStatus: "PUBLIC_ATTRIBUTED",34 realtimeStatus: "REALTIME",35 expectedLatencyMs: 300,36 supportsStreaming: true,37 supportsHistorical: false,38 assetClasses: ["CRYPTO", "FOREX"],39 exchanges: ["bitstamp"],40 homepage: "https://www.bitstamp.net/websocket/v2/",41 description: "Public Bitstamp WebSocket v2: `live_trades_<pair>` (last trade, exchange microtimestamp) and `order_book_<pair>` (best bid/ask) for the major USD/EUR crypto markets and Bitstamp's fiat markets EUR/USD and GBP/USD.",42 rightsNotes: "Public market data displayed with attribution to Bitstamp.",43 termsUrl: "https://www.bitstamp.net/terms-of-use/",44 sourceFamily: "bitstamp",45 enabled: true,46 },47 seeds: Object.entries(PAIRS).map(([sym, [b, q]]) => ({ symbol: sym, hint: planPair(b, q, "bitstamp").hint, aliases: [`${b}-${q}`, `${b}/${q}`] })),48 defaultSymbols: Object.keys(PAIRS),49 async start(ctx) {50 const ws = ctx.openWebSocket(WS_URL, {51 label: "bitstamp",52 staleAfterMs: 120_000,53 heartbeat: { intervalMs: 25_000, message: JSON.stringify({ event: "bts:heartbeat" }) },54 onOpen: (sock) => {55 for (const p of ctx.watchedSymbols()) {56 sock.send({ event: "bts:subscribe", data: { channel: `live_trades_${p}` } });57 sock.send({ event: "bts:subscribe", data: { channel: `order_book_${p}` } });58 }59 },60 onMessage: (data) => {61 let msg: any;62 try {63 msg = JSON.parse(data);64 } catch {65 return;66 }67 if (msg?.event === "trade") ctx.emit(raw("bitstamp-ws", "bitstamp", "trade", msg));68 else if (msg?.event === "data" && typeof msg.channel === "string" && msg.channel.startsWith("order_book_")) ctx.emit(raw("bitstamp-ws", "bitstamp", "book", msg));69 else if (msg?.event === "bts:request_reconnect") ctx.logger.info("bitstamp asked to reconnect");70 },71 });72 ws.connect();73 },74 normalize(r): NormalizedBatch {75 const m = r.payload as { channel?: string; data?: Record<string, unknown> };76 const pairKey = typeof m.channel === "string" ? m.channel.replace(/^(live_trades_|order_book_)/, "") : "";77 const pair = PAIRS[pairKey];78 if (!pair || !m.data) return { observations: [] };79 const [base, quote] = pair;80 const micro = m.data.microtimestamp;81 const ts = typeof micro === "string" && /^\d+$/.test(micro) ? Math.floor(Number(micro) / 1000) : null;82 const meta = { sourceTimestamp: ts, timestampTrust: "EXCHANGE" as const, rightsStatus: "PUBLIC_ATTRIBUTED" as const, realtimeStatus: "REALTIME" as const };83 let observations: NormalizedObservation[] = [];84 if (r.kind === "trade") {85 observations = pairObservations(pairKey, base, quote, "bitstamp", { last: m.data.price }, { ...meta, sequence: typeof m.data.id === "number" ? m.data.id : null });86 } else if (r.kind === "book") {87 const bids = m.data.bids as unknown;88 const asks = m.data.asks as unknown;89 const bid = Array.isArray(bids) && Array.isArray(bids[0]) ? bids[0][0] : undefined;90 const ask = Array.isArray(asks) && Array.isArray(asks[0]) ? asks[0][0] : undefined;91 observations = pairObservations(pairKey, base, quote, "bitstamp", { bid, ask }, meta);92 }93 return { observations };94 },95 fixturesDir: "fixtures",96});97