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 { pairObservations, planPair } from "../_shared/pairs.js";34const WS_URL = "wss://api-pub.bitfinex.com/ws/2";5/** Bitfinex symbol → [base, quote] (UST = USDT on Bitfinex). */6const PAIRS: Record<string, [string, string]> = {7 tBTCUSD: ["BTC", "USD"],8 tETHUSD: ["ETH", "USD"],9 tSOLUSD: ["SOL", "USD"],10 tXRPUSD: ["XRP", "USD"],11 tLTCUSD: ["LTC", "USD"],12 tADAUSD: ["ADA", "USD"],13 "tDOGE:USD": ["DOGE", "USD"],14 tDOTUSD: ["DOT", "USD"],15 "tAVAX:USD": ["AVAX", "USD"],16 "tLINK:USD": ["LINK", "USD"],17 tBTCUST: ["BTC", "USDT"],18 tETHUST: ["ETH", "USDT"],19 tBTCEUR: ["BTC", "EUR"],20 tEURUST: ["EUR", "USDT"],21};2223/** Bitfinex public WebSocket v2, `ticker` channel (array frames; channel id → symbol mapping kept per connection). */24export const bitfinexWs = defineConnector({25 metadata: {26 id: "bitfinex-ws",27 name: "Bitfinex — ticker channel (WS v2)",28 version: "1.0.0",29 sourceId: "bitfinex",30 organization: "iFinex Inc.",31 sourceType: "WEBSOCKET",32 jurisdiction: null,33 rightsStatus: "PUBLIC_ATTRIBUTED",34 realtimeStatus: "REALTIME",35 expectedLatencyMs: 500,36 supportsStreaming: true,37 supportsHistorical: false,38 assetClasses: ["CRYPTO", "FOREX"],39 exchanges: ["bitfinex"],40 homepage: "https://docs.bitfinex.com/reference/ws-public-ticker",41 description: "Public Bitfinex WebSocket v2 ticker channel (bid/ask with sizes, last price, 24h volume/high/low) for major USD and USDT crypto pairs, plus the EUR/USDT stablecoin market (live FX proxy).",42 rightsNotes: "Public market data displayed with attribution to Bitfinex.",43 termsUrl: "https://www.bitfinex.com/legal/general/terms",44 sourceFamily: "bitfinex",45 enabled: true,46 },47 seeds: Object.entries(PAIRS).map(([sym, [b, q]]) => ({ symbol: sym, hint: planPair(b, q, "bitfinex").hint, aliases: [`${b}-${q}`, `${b}/${q}`] })),48 defaultSymbols: Object.keys(PAIRS),49 async start(ctx) {50 const channels = new Map<number, string>();51 const ws = ctx.openWebSocket(WS_URL, {52 label: "bitfinex",53 staleAfterMs: 90_000,54 heartbeat: { intervalMs: 25_000, message: JSON.stringify({ event: "ping", cid: 1 }) },55 onOpen: (sock) => {56 channels.clear();57 for (const s of ctx.watchedSymbols()) sock.send({ event: "subscribe", channel: "ticker", symbol: s });58 },59 onMessage: (data) => {60 let msg: any;61 try {62 msg = JSON.parse(data);63 } catch {64 return;65 }66 if (Array.isArray(msg)) {67 const [chanId, body] = msg;68 if (body === "hb" || !Array.isArray(body)) return;69 const symbol = channels.get(chanId);70 if (symbol) ctx.emit(raw("bitfinex-ws", "bitfinex", "ticker", { symbol, ticker: body }));71 } else if (msg?.event === "subscribed" && msg.channel === "ticker") channels.set(msg.chanId, msg.symbol);72 else if (msg?.event === "error") ctx.reportError(new Error(String(msg.msg ?? "bitfinex error")), { code: msg.code, symbol: msg.symbol });73 },74 });75 ws.connect();76 },77 normalize(r): NormalizedBatch {78 const m = r.payload as { symbol?: string; ticker?: unknown[] };79 if (r.kind !== "ticker" || typeof m.symbol !== "string" || !Array.isArray(m.ticker) || m.ticker.length < 10) return { observations: [] };80 const pair = PAIRS[m.symbol];81 if (!pair) return { observations: [] };82 // [BID, BID_SIZE, ASK, ASK_SIZE, DAILY_CHANGE, DAILY_CHANGE_RELATIVE, LAST_PRICE, VOLUME, HIGH, LOW]83 const [bid, bidSize, ask, askSize, , , last, volume, high, low] = m.ticker;84 return {85 observations: pairObservations(m.symbol, pair[0], pair[1], "bitfinex", { bid, bidSize, ask, askSize, last, volume, high, low }, {86 sourceTimestamp: null,87 timestampTrust: "CONNECTOR",88 rightsStatus: "PUBLIC_ATTRIBUTED",89 realtimeStatus: "REALTIME",90 }),91 };92 },93 fixturesDir: "fixtures",94});95