UNPKG

ccxws

Version:

Websocket client for 37 cryptocurrency exchanges

588 lines 21.1 kB
"use strict"; /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable prefer-const */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/restrict-plus-operands */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ Object.defineProperty(exports, "__esModule", { value: true }); exports.BitfinexClient = exports.BitfinexTradeMessageType = void 0; const BasicClient_1 = require("../BasicClient"); const Level2Point_1 = require("../Level2Point"); const Level2Snapshots_1 = require("../Level2Snapshots"); const Level2Update_1 = require("../Level2Update"); const Level3Point_1 = require("../Level3Point"); const Level3Snapshot_1 = require("../Level3Snapshot"); const Level3Update_1 = require("../Level3Update"); const NotImplementedFn_1 = require("../NotImplementedFn"); const Ticker_1 = require("../Ticker"); const Trade_1 = require("../Trade"); var BitfinexTradeMessageType; (function (BitfinexTradeMessageType) { /** * Receive both execution events and updates */ BitfinexTradeMessageType["All"] = "all"; /** * Receive trade events immediately at the time of execution. Events * do not include the database identifier, only the sequence identifier. */ BitfinexTradeMessageType["Execution"] = "te"; /** * Receive trade events that have been written to the database. These * events include both the sequence identifier as well as the database * identifier. These events are delayed by 1-2 seconds after the * trade event. */ BitfinexTradeMessageType["Update"] = "tu"; })(BitfinexTradeMessageType = exports.BitfinexTradeMessageType || (exports.BitfinexTradeMessageType = {})); class BitfinexClient extends BasicClient_1.BasicClient { constructor({ wssPath = "wss://api.bitfinex.com/ws/2", watcherMs, l2UpdateDepth = 250, enableEmptyHeartbeatEvents = false, tradeMessageType = BitfinexTradeMessageType.Update, } = {}) { super(wssPath, "Bitfinex", undefined, watcherMs); this._sendSubCandles = NotImplementedFn_1.NotImplementedFn; this._sendSubLevel2Snapshots = NotImplementedFn_1.NotImplementedFn; this._sendSubLevel3Snapshots = NotImplementedFn_1.NotImplementedFn; this._sendUnsubCandles = NotImplementedFn_1.NotImplementedAsyncFn; this._sendUnsubLevel2Snapshots = NotImplementedFn_1.NotImplementedAsyncFn; this._sendUnsubLevel3Snapshots = NotImplementedFn_1.NotImplementedAsyncFn; this._channels = {}; this.hasTickers = true; this.hasTrades = true; this.hasLevel2Updates = true; this.hasLevel3Updates = true; this.l2UpdateDepth = l2UpdateDepth; this.enableEmptyHeartbeatEvents = enableEmptyHeartbeatEvents; this.tradeMessageType = tradeMessageType; } _onConnected() { // immediately send the config event to include sequence IDs in every message this._sendConfiguration(); super._onConnected(); } /** * Override the default BasicClient _unsubscribe by deferring removal * of from the appropriate map until the unsubscribe event has been * received. * * If enableEmptyHeartbeatEvents (validating sequenceIds) we need to * keep receiving events from a channel after we sent the unsub event * until unsubscribe is confirmed. This is because every message's * sequenceId must be validated, and some may arrive between sending * unsub and it being confirmed. So we dont remove from the map and * will continue emitting events for this channel until they stop * arriving. */ _unsubscribe(market, map, sendFn) { const remote_id = market.id; if (map.has(remote_id)) { if (this._wss.isConnected) { sendFn(remote_id, market); } } } _sendConfiguration() { // see docs for "conf" flags. https://docs.bitfinex.com/docs/ws-general#configuration // combine multiple flags by summing their values // 65536 adds a sequence ID to each message // 32768 adds a Timestamp in milliseconds to each received event // 131072 Enable checksum for every book iteration. Checks the top 25 entries for each side of book. Checksum is a signed int. more info https://docs.bitfinex.com/docs/ws-websocket-checksum. it's sent in its own // separate event so we've disabled it this._wss.send(JSON.stringify({ event: "conf", flags: 65536 + 32768 })); } _sendSubTicker(remote_id) { this._wss.send(JSON.stringify({ event: "subscribe", channel: "ticker", pair: remote_id, })); } _sendUnsubTicker(remote_id) { this._sendUnsubscribe(remote_id); } _sendSubTrades(remote_id) { this._wss.send(JSON.stringify({ event: "subscribe", channel: "trades", pair: remote_id, })); } _sendUnsubTrades(remote_id) { const chanId = this._findChannel("trades", remote_id); this._sendUnsubscribe(chanId); } _sendSubLevel2Updates(remote_id) { this._wss.send(JSON.stringify({ event: "subscribe", channel: "book", pair: remote_id, len: String(this.l2UpdateDepth), // len must be of type string, even though it's a number })); } _sendUnsubLevel2Updates(remote_id) { const chanId = this._findChannel("level2updates", remote_id); this._sendUnsubscribe(chanId); } _sendSubLevel3Updates(remote_id) { this._wss.send(JSON.stringify({ event: "subscribe", channel: "book", pair: remote_id, prec: "R0", length: "100", })); } _sendUnsubLevel3Updates(remote_id) { const chanId = this._findChannel("level3updates", remote_id); this._sendUnsubscribe(chanId); } _sendUnsubscribe(chanId) { if (chanId) { this._wss.send(JSON.stringify({ event: "unsubscribe", chanId: chanId, })); } } _findChannel(type, remote_id) { for (const raw of Object.values(this._channels)) { const chan = raw; if (chan.pair === remote_id) { if (type === "trades" && chan.channel === "trades") return chan.chanId; if (type === "level2updates" && chan.channel === "book" && chan.prec !== "R0") return chan.chanId; if (type === "level3updates" && chan.channel === "book" && chan.prec === "R0") return chan.chanId; } } } /** * Handle heartbeat messages on each channel. */ _onHeartbeatMessage(msg, channel) { if (channel.channel === "ticker") { let market = this._tickerSubs.get(channel.pair); if (!market) return; this._onTickerHeartbeat(msg, market); return; } // trades if (channel.channel === "trades") { let market = this._tradeSubs.get(channel.pair); if (!market) return; this._onTradeMessageHeartbeat(msg, market); return; } // level3 if (channel.channel === "book" && channel.prec === "R0") { let market = this._level3UpdateSubs.get(channel.pair); if (!market) return; this._onLevel3UpdateHeartbeat(msg, market); return; } // level2 if (channel.channel === "book") { let market = this._level2UpdateSubs.get(channel.pair); if (!market) return; this._onLevel2UpdateHeartbeat(msg, market); return; } } _onMessage(raw) { const msg = JSON.parse(raw); // capture channel metadata if (msg.event === "subscribed") { this._channels[msg.chanId] = msg; return; } // process unsubscribe event if (msg.event === "unsubscribed") { this._onUnsubscribeMessage(msg); return; } // lookup channel const channel = this._channels[msg[0]]; if (!channel) return; // handle heartbeats if (msg[1] === "hb") { this._onHeartbeatMessage(msg, channel); return; } if (channel.channel === "ticker") { const market = this._tickerSubs.get(channel.pair); if (!market) return; this._onTicker(msg, market); return; } // trades if (channel.channel === "trades") { const market = this._tradeSubs.get(channel.pair); if (!market) return; // handle tradeMessageType (constructor param) filtering // example trade update msg: [ 359491, 'tu' or 'te', [ 560287312, 1609712228656, 0.005, 33432 ], 6 ] // note: "tu" means it's got the tradeId, this is delayed by 1-2 seconds and includes tradeId. // "te" is the same but available immediately and without the tradeId const tradeEventType = msg[1]; if (this.tradeMessageType === BitfinexTradeMessageType.All || tradeEventType === this.tradeMessageType) { this._onTradeMessage(msg, market); } return; } // level3 if (channel.channel === "book" && channel.prec === "R0") { const market = this._level3UpdateSubs.get(channel.pair); if (!market) return; if (Array.isArray(msg[1][0])) this._onLevel3Snapshot(msg, market); else this._onLevel3Update(msg, market); return; } // level2 if (channel.channel === "book") { const market = this._level2UpdateSubs.get(channel.pair); if (!market) return; if (Array.isArray(msg[1][0])) this._onLevel2Snapshot(msg, market); else this._onLevel2Update(msg, market); return; } } _onUnsubscribeMessage(msg) { const chanId = msg.chanId; const channel = this._channels[chanId]; if (!channel) return; const marketId = channel.pair; // remove channel metadata delete this._channels[chanId]; // remove from appropriate subscription map if (channel.channel === "ticker") { this._tickerSubs.delete(marketId); } else if (channel.channel === "trades") { this._tradeSubs.delete(marketId); } else if (channel.channel === "book" && channel.prec === "R0") { this._level3UpdateSubs.delete(marketId); } else if (channel.channel === "book") { this._level2UpdateSubs.delete(marketId); } } /** * Handle heartbeat events in the ticker channel. */ _onTickerHeartbeat(msg, market) { const sequenceId = Number(msg[2]); const timestampMs = msg[3]; if (this.enableEmptyHeartbeatEvents === false) return; // handle heartbeat by emitting empty update w/sequenceId. // heartbeat msg: [ 198655, 'hb', 3, 1610920929093 ] let ticker = new Ticker_1.Ticker({ exchange: "Bitfinex", base: market.base, quote: market.quote, timestamp: timestampMs, sequenceId, }); this.emit("ticker", ticker, market); return; } _onTicker(msg, market) { const msgBody = msg[1]; const sequenceId = Number(msg[2]); const [bid, bidSize, ask, askSize, change, changePercent, last, volume, high, low] = msgBody; const open = last + change; const ticker = new Ticker_1.Ticker({ exchange: "Bitfinex", base: market.base, quote: market.quote, timestamp: Date.now(), sequenceId, last: last.toFixed(8), open: open.toFixed(8), high: high.toFixed(8), low: low.toFixed(8), volume: volume.toFixed(8), change: change.toFixed(8), changePercent: changePercent.toFixed(2), bid: bid.toFixed(8), bidVolume: bidSize.toFixed(8), ask: ask.toFixed(8), askVolume: askSize.toFixed(8), }); this.emit("ticker", ticker, market); } /** * Handle heartbeat events in the trades channel. */ _onTradeMessageHeartbeat(msg, market) { const timestampMs = msg[3]; const sequenceId = Number(msg[2]); if (this.enableEmptyHeartbeatEvents === false) return; // handle heartbeat by emitting empty update w/sequenceId. // example trade heartbeat msg: [ 198655, 'hb', 3, 1610920929093 ] let trade = new Trade_1.Trade({ exchange: "Bitfinex", base: market.base, quote: market.quote, timestamp: timestampMs, sequenceId, }); this.emit("trade", trade, market); return; } /** * Handle the trade history payload received when initially subscribing, which includes recent trades history. * Each trade in history is emitted as its own trade event. */ _onTradeHistoryMessage(msg, market) { // handle the initial trades snapshot // trade snapshot example msg: /* [ CHANNEL_ID, [ [ ID, MTS, AMOUNT, PRICE ], ... ], sequenceId, timestampMs ] */ const sequenceId = Number(msg[2]); for (const thisTrade of msg[1]) { let [id, unix, amount, price] = thisTrade; let side = amount > 0 ? "buy" : "sell"; price = price.toFixed(8); amount = Math.abs(amount).toFixed(8); let trade = new Trade_1.Trade({ exchange: "Bitfinex", base: market.base, quote: market.quote, tradeId: id.toFixed(), sequenceId, unix: unix, side, price, amount, }); this.emit("trade", trade, market); } } _onTradeMessage(msg, market) { const isTradeHistory = Array.isArray(msg[1]); if (isTradeHistory) { this._onTradeHistoryMessage(msg, market); return; } // example msg: [ 359491, 'tu', [ 560287312, 1609712228656, 0.005, 33432 ], 6 ] let [id, unix, amount, price] = msg[2]; const sequenceId = Number(msg[3]); const side = amount > 0 ? "buy" : "sell"; price = price.toFixed(8); amount = Math.abs(amount).toFixed(8); const trade = new Trade_1.Trade({ exchange: "Bitfinex", base: market.base, quote: market.quote, tradeId: id.toFixed(), sequenceId, unix: unix, side, price, amount, }); this.emit("trade", trade, market); } _onLevel2Snapshot(msg, market) { /* example msg: [ 646750, [ [ 31115, 1, 1 ], [ 31114, 1, 0.31589592 ], ... ], 1, 1609794291015 ] */ const bids = []; const asks = []; const sequenceId = Number(msg[2]); const timestampMs = msg[3]; for (const [price, count, size] of msg[1]) { const isBid = size > 0; const result = new Level2Point_1.Level2Point(price.toFixed(8), Math.abs(size).toFixed(8), count.toFixed(0)); if (isBid) bids.push(result); else asks.push(result); } const result = new Level2Snapshots_1.Level2Snapshot({ exchange: "Bitfinex", base: market.base, quote: market.quote, sequenceId, timestampMs, bids, asks, }); this.emit("l2snapshot", result, market); } /** * Handle heartbeat events in the l2updatae channel */ _onLevel2UpdateHeartbeat(msg, market) { const sequenceId = Number(msg[2]); const timestampMs = msg[3]; if (this.enableEmptyHeartbeatEvents === false) return; // handle heartbeat by emitting empty update w/sequenceId. // heartbeat msg: [ 169546, 'hb', 17, 1610921150321 ] let update = new Level2Update_1.Level2Update({ exchange: "Bitfinex", base: market.base, quote: market.quote, sequenceId, timestampMs, asks: [], bids: [], }); this.emit("l2update", update, market); return; } _onLevel2Update(msg, market) { // example msg: [ 646750, [ 30927, 5, 0.0908 ], 19, 1609794565952 ] const [price, count, size] = msg[1]; const sequenceId = Number(msg[2]); const timestampMs = msg[3]; if (!price.toFixed) return; const point = new Level2Point_1.Level2Point(price.toFixed(8), Math.abs(size).toFixed(8), count.toFixed(0)); const asks = []; const bids = []; const isBid = size > 0; if (isBid) bids.push(point); else asks.push(point); const isDelete = count === 0; if (isDelete) point.size = (0).toFixed(8); // reset the size to 0, comes in as 1 or -1 to indicate bid/ask const update = new Level2Update_1.Level2Update({ exchange: "Bitfinex", base: market.base, quote: market.quote, sequenceId, timestampMs, asks, bids, }); this.emit("l2update", update, market); } _onLevel3Snapshot(msg, market) { /* example msg: [ 648087, [ [ 55888179267, 31111, 0.05 ], [ 55895806791, 31111, 0.989 ], ... ], 1, 1609794565952 ] */ const bids = []; const asks = []; const orders = msg[1]; const sequenceId = Number(msg[2]); const timestampMs = msg[3]; for (const [orderId, price, size] of orders) { const point = new Level3Point_1.Level3Point(orderId.toFixed(), price.toFixed(8), Math.abs(size).toFixed(8)); if (size > 0) bids.push(point); else asks.push(point); } const result = new Level3Snapshot_1.Level3Snapshot({ exchange: "Bitfinex", base: market.base, quote: market.quote, sequenceId, timestampMs, asks, bids, }); this.emit("l3snapshot", result, market); } /** * Handle heartbeat events in the l3updatae channel */ _onLevel3UpdateHeartbeat(msg, market) { const sequenceId = Number(msg[2]); const timestampMs = msg[3]; if (this.enableEmptyHeartbeatEvents === false) return; // handle heartbeat by emitting empty update w/sequenceId. // heartbeat msg: [ 169546, 'hb', 17, 1610921150321 ] let result = new Level3Update_1.Level3Update({ exchange: "Bitfinex", base: market.base, quote: market.quote, sequenceId, timestampMs, asks: [], bids: [], }); this.emit("l3update", result, market); } _onLevel3Update(msg, market) { // example msg: [ 648087, [ 55895794256, 31107, 0.07799627 ], 4, 1609794565952 ] const bids = []; const asks = []; const [orderId, price, size] = msg[1]; const sequenceId = Number(msg[2]); const timestampMs = msg[3]; const point = new Level3Point_1.Level3Point(orderId.toFixed(), price.toFixed(8), Math.abs(size).toFixed(8)); if (size > 0) bids.push(point); else asks.push(point); const result = new Level3Update_1.Level3Update({ exchange: "Bitfinex", base: market.base, quote: market.quote, sequenceId, timestampMs, asks, bids, }); this.emit("l3update", result, market); } } exports.BitfinexClient = BitfinexClient; //# sourceMappingURL=BitfinexClient.js.map