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.9 KB · 95 lines typescript
Raw Blame History
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