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%
3.5 KB · 80 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, cryptoSeeds } from "../_shared/crypto.js";4import { pairObservations } from "../_shared/pairs.js";56const WS_URL = "wss://ws-feed.exchange.coinbase.com";7const CRYPTO_PAIRS: Array<[string, string]> = [...MAJOR_BASES.map((b) => [b, "USD"] as [string, string]), ["BTC", "USDT"], ["ETH", "USDT"], ["BTC", "EUR"], ["ETH", "EUR"]];8/** Stablecoin/fiat markets → live FX proxies (USDC ≈ USD). */9const FX_PROXY_PAIRS: Array<[string, string]> = [["USDC", "EUR"], ["USDC", "GBP"], ["USDT", "EUR"], ["USDT", "GBP"]];10const PAIRS = [...CRYPTO_PAIRS, ...FX_PROXY_PAIRS];11const SYMBOLS = PAIRS.map(([b, q]) => `${b}-${q}`);1213/**14 * Coinbase Exchange public market-data feed, `ticker` channel: one multiplexed connection for all15 * products, best bid/ask, last trade price, 24h stats. Exchange-generated timestamps.16 */17export const coinbaseWs = defineConnector({18  metadata: {19    id: "coinbase-ws",20    name: "Coinbase Exchange — ticker feed",21    version: "1.0.0",22    sourceId: "coinbase",23    organization: "Coinbase Global, Inc.",24    sourceType: "WEBSOCKET",25    jurisdiction: "US",26    rightsStatus: "PUBLIC_ATTRIBUTED",27    realtimeStatus: "REALTIME",28    expectedLatencyMs: 300,29    supportsStreaming: true,30    supportsHistorical: false,31    assetClasses: ["CRYPTO", "FOREX"],32    exchanges: ["coinbase"],33    homepage: "https://docs.cdp.coinbase.com/exchange/docs/websocket-overview",34    description: "Public, unauthenticated WebSocket market-data feed of Coinbase Exchange (ticker channel). One connection carries every subscribed product.",35    rightsNotes: "Public market data; displayed with attribution to Coinbase Exchange. Not for commercial redistribution as a raw feed.",36    termsUrl: "https://www.coinbase.com/legal/market_data",37    sourceFamily: "coinbase",38    enabled: true,39  },40  seeds: cryptoSeeds(CRYPTO_PAIRS, "coinbase", (b, q) => `${b}-${q}`),41  defaultSymbols: SYMBOLS,42  async start(ctx) {43    const ws = ctx.openWebSocket(WS_URL, {44      label: "coinbase",45      staleAfterMs: 90_000,46      heartbeat: { intervalMs: 20_000 },47      onOpen: (sock) => {48        sock.send({ type: "subscribe", product_ids: ctx.watchedSymbols(), channels: ["ticker", "heartbeat"] });49      },50      onMessage: (data) => {51        let msg: any;52        try {53          msg = JSON.parse(data);54        } catch {55          return;56        }57        if (msg?.type === "ticker") ctx.emit(raw("coinbase-ws", "coinbase", "ticker", msg));58        else if (msg?.type === "error") ctx.reportError(new Error(String(msg.message ?? "coinbase error")), { reason: msg.reason });59      },60    });61    ws.connect();62  },63  normalize(r): NormalizedBatch {64    const m = r.payload as Record<string, unknown>;65    if (r.kind !== "ticker" || typeof m.product_id !== "string") return { observations: [] };66    const [base, quote] = m.product_id.split("-");67    if (!base || !quote) return { observations: [] };68    return {69      observations: pairObservations(m.product_id, base, quote, "coinbase", { last: m.price, open: m.open_24h, high: m.high_24h, low: m.low_24h, bid: m.best_bid, ask: m.best_ask, bidSize: m.best_bid_size, askSize: m.best_ask_size, volume: m.volume_24h }, {70        sourceTimestamp: parseTimestamp(m.time),71        timestampTrust: "EXCHANGE",72        sequence: typeof m.sequence === "number" ? m.sequence : null,73        rightsStatus: "PUBLIC_ATTRIBUTED",74        realtimeStatus: "REALTIME",75      }),76    };77  },78  fixturesDir: "fixtures",79});80