SPB Git forge

spb/market-atlas

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