import { defineConnector, raw, type NormalizedBatch } from "@market-atlas/connector-sdk"; import { pairObservations, planPair } from "../_shared/pairs.js"; const WS_URL = "wss://api-pub.bitfinex.com/ws/2"; /** Bitfinex symbol → [base, quote] (UST = USDT on Bitfinex). */ const PAIRS: Record = { tBTCUSD: ["BTC", "USD"], tETHUSD: ["ETH", "USD"], tSOLUSD: ["SOL", "USD"], tXRPUSD: ["XRP", "USD"], tLTCUSD: ["LTC", "USD"], tADAUSD: ["ADA", "USD"], "tDOGE:USD": ["DOGE", "USD"], tDOTUSD: ["DOT", "USD"], "tAVAX:USD": ["AVAX", "USD"], "tLINK:USD": ["LINK", "USD"], tBTCUST: ["BTC", "USDT"], tETHUST: ["ETH", "USDT"], tBTCEUR: ["BTC", "EUR"], tEURUST: ["EUR", "USDT"], }; /** Bitfinex public WebSocket v2, `ticker` channel (array frames; channel id → symbol mapping kept per connection). */ export const bitfinexWs = defineConnector({ metadata: { id: "bitfinex-ws", name: "Bitfinex — ticker channel (WS v2)", version: "1.0.0", sourceId: "bitfinex", organization: "iFinex Inc.", sourceType: "WEBSOCKET", jurisdiction: null, rightsStatus: "PUBLIC_ATTRIBUTED", realtimeStatus: "REALTIME", expectedLatencyMs: 500, supportsStreaming: true, supportsHistorical: false, assetClasses: ["CRYPTO", "FOREX"], exchanges: ["bitfinex"], homepage: "https://docs.bitfinex.com/reference/ws-public-ticker", 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).", rightsNotes: "Public market data displayed with attribution to Bitfinex.", termsUrl: "https://www.bitfinex.com/legal/general/terms", sourceFamily: "bitfinex", enabled: true, }, seeds: Object.entries(PAIRS).map(([sym, [b, q]]) => ({ symbol: sym, hint: planPair(b, q, "bitfinex").hint, aliases: [`${b}-${q}`, `${b}/${q}`] })), defaultSymbols: Object.keys(PAIRS), async start(ctx) { const channels = new Map(); const ws = ctx.openWebSocket(WS_URL, { label: "bitfinex", staleAfterMs: 90_000, heartbeat: { intervalMs: 25_000, message: JSON.stringify({ event: "ping", cid: 1 }) }, onOpen: (sock) => { channels.clear(); for (const s of ctx.watchedSymbols()) sock.send({ event: "subscribe", channel: "ticker", symbol: s }); }, onMessage: (data) => { let msg: any; try { msg = JSON.parse(data); } catch { return; } if (Array.isArray(msg)) { const [chanId, body] = msg; if (body === "hb" || !Array.isArray(body)) return; const symbol = channels.get(chanId); if (symbol) ctx.emit(raw("bitfinex-ws", "bitfinex", "ticker", { symbol, ticker: body })); } else if (msg?.event === "subscribed" && msg.channel === "ticker") channels.set(msg.chanId, msg.symbol); else if (msg?.event === "error") ctx.reportError(new Error(String(msg.msg ?? "bitfinex error")), { code: msg.code, symbol: msg.symbol }); }, }); ws.connect(); }, normalize(r): NormalizedBatch { const m = r.payload as { symbol?: string; ticker?: unknown[] }; if (r.kind !== "ticker" || typeof m.symbol !== "string" || !Array.isArray(m.ticker) || m.ticker.length < 10) return { observations: [] }; const pair = PAIRS[m.symbol]; if (!pair) return { observations: [] }; // [BID, BID_SIZE, ASK, ASK_SIZE, DAILY_CHANGE, DAILY_CHANGE_RELATIVE, LAST_PRICE, VOLUME, HIGH, LOW] const [bid, bidSize, ask, askSize, , , last, volume, high, low] = m.ticker; return { observations: pairObservations(m.symbol, pair[0], pair[1], "bitfinex", { bid, bidSize, ask, askSize, last, volume, high, low }, { sourceTimestamp: null, timestampTrust: "CONNECTOR", rightsStatus: "PUBLIC_ATTRIBUTED", realtimeStatus: "REALTIME", }), }; }, fixturesDir: "fixtures", });