UNPKG

ccxws

Version:

Websocket client for 37 cryptocurrency exchanges

540 lines (539 loc) 20.3 kB
"use strict"; /* eslint-disable prefer-const */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/restrict-plus-operands */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/require-await */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BiboxBasicClient = exports.BiboxClient = void 0; const zlib_1 = __importDefault(require("zlib")); const events_1 = require("events"); const Watcher_1 = require("../Watcher"); const BasicClient_1 = require("../BasicClient"); const Ticker_1 = require("../Ticker"); const Trade_1 = require("../Trade"); const Level2Point_1 = require("../Level2Point"); const Level2Snapshots_1 = require("../Level2Snapshots"); const Candle_1 = require("../Candle"); const SubscriptionType_1 = require("../SubscriptionType"); const CandlePeriod_1 = require("../CandlePeriod"); const Throttle_1 = require("../flowcontrol/Throttle"); const Util_1 = require("../Util"); const NotImplementedFn_1 = require("../NotImplementedFn"); class BiboxClient extends events_1.EventEmitter { /** Bibox allows listening to multiple markets on the same socket. Unfortunately, they throw errors if you subscribe to too more than 20 markets at a time re: https://github.com/Biboxcom/API_Docs_en/wiki/WS_request#1-access-to-the-url This makes like hard and we need to batch connections, which is why we can't use the BasicMultiClient. */ constructor(options) { super(); this.subscribeLevel2Updates = NotImplementedFn_1.NotImplementedFn; this.unsubscribeLevel2Updates = NotImplementedFn_1.NotImplementedAsyncFn; this.subscribeLevel3Snapshots = NotImplementedFn_1.NotImplementedFn; this.unsubscribeLevel3Snapshots = NotImplementedFn_1.NotImplementedAsyncFn; this.subscribeLevel3Updates = NotImplementedFn_1.NotImplementedFn; this.unsubscribeLevel3Updates = NotImplementedFn_1.NotImplementedFn; /** Stores the client used for each subscription request with teh key: remoteId_subType The value is the underlying client that is used. */ this._subClients = new Map(); /** List of all active clients. Clients will be removed when all subscriptions have vanished. */ this._clients = []; this.options = options; this.hasTickers = true; this.hasTrades = true; this.hasCandles = true; this.hasLevel2Snapshots = true; this.hasLevel2Updates = false; this.hasLevel3Snapshots = false; this.hasLevel3Updates = false; this.subsPerClient = 20; this.throttleMs = 200; this._subscribe = (0, Throttle_1.throttle)(this.__subscribe.bind(this), this.throttleMs); this.candlePeriod = CandlePeriod_1.CandlePeriod._1m; } subscribeTicker(market) { this._subscribe(market, SubscriptionType_1.SubscriptionType.ticker); } async unsubscribeTicker(market) { this._unsubscribe(market, SubscriptionType_1.SubscriptionType.ticker); } subscribeTrades(market) { this._subscribe(market, SubscriptionType_1.SubscriptionType.trade); } unsubscribeTrades(market) { this._unsubscribe(market, SubscriptionType_1.SubscriptionType.trade); } subscribeCandles(market) { this._subscribe(market, SubscriptionType_1.SubscriptionType.candle); } async unsubscribeCandles(market) { this._unsubscribe(market, SubscriptionType_1.SubscriptionType.candle); } async subscribeLevel2Snapshots(market) { this._subscribe(market, SubscriptionType_1.SubscriptionType.level2snapshot); } async unsubscribeLevel2Snapshots(market) { this._unsubscribe(market, SubscriptionType_1.SubscriptionType.level2snapshot); } close() { this._subscribe.cancel(); for (const client of this._clients) { client.close(); } } async reconnect() { for (const client of this._clients) { client.reconnect(); await (0, Util_1.wait)(this.timeoutMs); } } __subscribe(market, subscriptionType) { // construct the subscription key from the remote_id and the type // of subscription being performed const subKey = market.id + "_" + subscriptionType; // try to find the subscription client from the existing lookup let client = this._subClients.get(subKey); // if we haven't seen this market sub before first try // to find an available existing client if (!client) { // first try to find a client that has less than 20 subscriptions... client = this._clients.find(p => p.subCount < this.subsPerClient); // make sure we set the value this._subClients.set(subKey, client); } // if we were unable to find any avaialble clients, we will need // to create a new client. if (!client) { // construct a new client client = new BiboxBasicClient(this.options); // set properties client.parent = this; // wire up the events to pass through client.on("connecting", () => this.emit("connecting", market, subscriptionType)); client.on("connected", () => this.emit("connected", market, subscriptionType)); client.on("disconnected", () => this.emit("disconnected", market, subscriptionType)); client.on("reconnecting", () => this.emit("reconnecting", market, subscriptionType)); client.on("closing", () => this.emit("closing", market, subscriptionType)); client.on("closed", () => this.emit("closed", market, subscriptionType)); client.on("ticker", (ticker, market) => this.emit("ticker", ticker, market)); client.on("trade", (trade, market) => this.emit("trade", trade, market)); client.on("candle", (candle, market) => this.emit("candle", candle, market)); client.on("l2snapshot", (l2snapshot, market) => this.emit("l2snapshot", l2snapshot, market)); client.on("error", err => this.emit("error", err)); // push it into the list of clients this._clients.push(client); // make sure we set the value this._subClients.set(subKey, client); } // now that we have a client, call the sub method, which // should be an idempotent method, so no harm in calling it again switch (subscriptionType) { case SubscriptionType_1.SubscriptionType.ticker: client.subscribeTicker(market); break; case SubscriptionType_1.SubscriptionType.trade: client.subscribeTrades(market); break; case SubscriptionType_1.SubscriptionType.candle: client.subscribeCandles(market); break; case SubscriptionType_1.SubscriptionType.level2snapshot: client.subscribeLevel2Snapshots(market); break; } } _unsubscribe(market, subscriptionType) { // construct the subscription key from the remote_id and the type // of subscription being performed const subKey = market.id + "_" + subscriptionType; // find the client const client = this._subClients.get(subKey); // abort if nothign to do if (!client) return; // perform the unsubscribe operation switch (subscriptionType) { case SubscriptionType_1.SubscriptionType.ticker: client.unsubscribeTicker(market); break; case SubscriptionType_1.SubscriptionType.trade: client.unsubscribeTrades(market); break; case SubscriptionType_1.SubscriptionType.candle: client.unsubscribeCandles(market); break; case SubscriptionType_1.SubscriptionType.level2snapshot: client.unsubscribeLevel2Snapshots(market); break; } // remove the client if nothing left to do if (client.subCount === 0) { client.close(); const idx = this._clients.indexOf(client); this._clients.splice(idx, 1); } } } exports.BiboxClient = BiboxClient; class BiboxBasicClient extends BasicClient_1.BasicClient { /** Manages connections for a single market. A single socket is only allowed to work for 20 markets. */ constructor({ wssPath = "wss://push.bibox.com", watcherMs = 600 * 1000 } = {}) { super(wssPath, "Bibox"); this._sendSubLevel2Updates = NotImplementedFn_1.NotImplementedFn; this._sendUnsubLevel2Updates = NotImplementedFn_1.NotImplementedAsyncFn; this._sendSubLevel3Snapshots = NotImplementedFn_1.NotImplementedFn; this._sendUnsubLevel3Snapshots = NotImplementedFn_1.NotImplementedAsyncFn; this._sendSubLevel3Updates = NotImplementedFn_1.NotImplementedFn; this._sendUnsubLevel3Updates = NotImplementedFn_1.NotImplementedAsyncFn; this._watcher = new Watcher_1.Watcher(this, watcherMs); this.hasTickers = true; this.hasTrades = true; this.hasCandles = true; this.hasLevel2Snapshots = true; this.subCount = 0; } get candlePeriod() { return this.parent.candlePeriod; } /** Server will occassionally send ping messages. Client is expected to respond with a pong message that matches the identifier. If client fails to do this, server will abort connection after second attempt. */ _sendPong(id) { this._wss.send(JSON.stringify({ pong: id })); } _sendSubTicker(remote_id) { this.subCount++; this._wss.send(JSON.stringify({ event: "addChannel", channel: `bibox_sub_spot_${remote_id}_ticker`, })); } async _sendUnsubTicker(remote_id) { this.subCount--; this._wss.send(JSON.stringify({ event: "removeChannel", channel: `bibox_sub_spot_${remote_id}_ticker`, })); } async _sendSubTrades(remote_id) { this.subCount++; this._wss.send(JSON.stringify({ event: "addChannel", channel: `bibox_sub_spot_${remote_id}_deals`, })); } _sendUnsubTrades(remote_id) { this.subCount--; this._wss.send(JSON.stringify({ event: "removeChannel", channel: `bibox_sub_spot_${remote_id}_deals`, })); } _sendSubCandles(remote_id) { this.subCount++; this._wss.send(JSON.stringify({ event: "addChannel", channel: `bibox_sub_spot_${remote_id}_kline_${candlePeriod(this.candlePeriod)}`, })); } async _sendUnsubCandles(remote_id) { this.subCount--; this._wss.send(JSON.stringify({ event: "removeChannel", channel: `bibox_sub_spot_${remote_id}_kline_${candlePeriod(this.candlePeriod)}`, })); } async _sendSubLevel2Snapshots(remote_id) { this.subCount++; this._wss.send(JSON.stringify({ event: "addChannel", channel: `bibox_sub_spot_${remote_id}_depth`, })); } async _sendUnsubLevel2Snapshots(remote_id) { this.subCount--; this._wss.send(JSON.stringify({ event: "removeChannel", channel: `bibox_sub_spot_${remote_id}_depth`, })); } /** Message usually arives as a string, that must first be converted to JSON. Then we can process each message in the payload and perform gunzip on the data. */ _onMessage(raw) { const msgs = typeof raw == "string" ? JSON.parse(raw) : raw; if (Array.isArray(msgs)) { for (const msg of msgs) { this._processsMessage(msg); } } else { this._processsMessage(msgs); } } /** Process the individaul message that was sent from the server. Message will be informat: { channel: 'bibox_sub_spot_BTC_USDT_deals', binary: '1', data_type: 1, data: 'H4sIAAAAAAAA/xTLMQ6CUAyA4bv8c0Ne4RWeHdUbiJMxhghDB5QgTsa7Gw/wXT4sQ6w4+/5wO5+OPcIW84SrWdPtsllbrAjLGvcJJ6cmVZoNYZif78eGo1UqjSK8YvxLIUa8bjWnrtbyvf4CAAD//1PFt6BnAAAA' } */ _processsMessage(msg) { // if we detect gzip data, we need to process it if (msg.binary == 1) { const buffer = zlib_1.default.gunzipSync(Buffer.from(msg.data, "base64")); msg.data = JSON.parse(buffer.toString()); } // server will occassionally send a ping message and client // must respon with appropriate identifier if (msg.ping) { this._sendPong(msg.ping); return; } // watch for error messages if (msg.error) { const err = new Error(msg.error); err.message = msg; this.emit("error", err); return; } if (!msg.channel) { return; } if (msg.channel.endsWith("_deals")) { // trades are send in descendinging order // out library standardize to asc order so perform a reverse const data = msg.data.slice().reverse(); for (const datum of data) { const market = this._tradeSubs.get(datum.pair); if (!market) return; const trade = this._constructTradesFromMessage(datum, market); this.emit("trade", trade, market); } return; } // tickers if (msg.channel.endsWith("_ticker")) { const market = this._tickerSubs.get(msg.data.pair); if (!market) return; const ticker = this._constructTicker(msg, market); this.emit("ticker", ticker, market); return; } // l2 updates if (msg.channel.endsWith("depth")) { const remote_id = msg.data.pair; const market = this._level2SnapshotSubs.get(remote_id) || this._level2UpdateSubs.get(remote_id); if (!market) return; const snapshot = this._constructLevel2Snapshot(msg, market); this.emit("l2snapshot", snapshot, market); return; } // candle if (msg.channel.endsWith(`kline_${candlePeriod(this.candlePeriod)}`)) { // bibox_sub_spot_BTC_USDT_kline_1min const remote_id = msg.channel .replace("bibox_sub_spot_", "") .replace(`_kline_${candlePeriod(this.candlePeriod)}`, ""); const market = this._candleSubs.get(remote_id); if (!market) return; for (const datum of msg.data) { const candle = this._constructCandle(datum); this.emit("candle", candle, market); } } } /* Constructs a ticker from the source { channel: 'bibox_sub_spot_BIX_BTC_ticker', binary: 1, data_type: 1, data: { last: '0.00003573', buy: '0.00003554', sell: '0.00003589', base_last_cny: '0.86774973', last_cny: '0.86', buy_amount: '6.1867', percent: '-1.68%', pair: 'BIX_BTC', high: '0.00003700', vol: '737995', last_usd: '0.12', low: '0.00003535', sell_amount: '880.0475', timestamp: 1547546988399 } } */ _constructTicker(msg, market) { let { last, buy, sell, vol, percent, low, high, timestamp } = msg.data; percent = percent.replace(/%|\+/g, ""); const change = (parseFloat(last) * parseFloat(percent)) / 100; const open = parseFloat(last) - change; return new Ticker_1.Ticker({ exchange: "Bibox", base: market.base, quote: market.quote, timestamp, last, open: open.toFixed(8), high: high, low: low, volume: vol, change: change.toFixed(8), changePercent: percent, bid: buy, ask: sell, }); } /* Construct a trade { channel: 'bibox_sub_spot_BIX_BTC_deals', binary: '1', data_type: 1, data: [ { pair: 'BIX_BTC', time: 1547544945204, price: 0.0000359, amount: 6.1281, side: 2, id: 189765713 } ] } */ _constructTradesFromMessage(datum, market) { let { time, price, amount, side, id } = datum; side = side === 1 ? "buy" : "sell"; return new Trade_1.Trade({ exchange: "Bibox", base: market.base, quote: market.quote, tradeId: id, side, unix: time, price, amount, }); } /** { channel: 'bibox_sub_spot_BTC_USDT_kline_1min', binary: 1, data_type: 1, data: [ { time: 1597259460000, open: '11521.38000000', high: '11540.58990000', low: '11521.28990000', close: '11540.56990000', vol: '11.24330000' }, { time: 1597259520000, open: '11540.55990000', high: '11540.58990000', low: '11533.13000000', close: '11536.83990000', vol: '10.88200000' } ] } */ _constructCandle(datum) { return new Candle_1.Candle(datum.time, datum.open, datum.high, datum.low, datum.close, datum.vol); } /* Converts from a raw message { "binary": 0, "channel": "ok_sub_spot_bch_btc_depth", "data": { update_time: 1547549824601, asks: [ { volume: '433.588', price: '0.00003575' }, { volume: '1265.6753', price: '0.00003576' }, .. { volume: '69.5745', price: '0.000041' }, { volume: '5.277', price: '0.00004169' }, ... 100 more items ], bids: [ { volume: '6.1607', price: '0.00003571' }, { volume: '704.8954', price: '0.00003538' }, .. { volume: '155000', price: '2e-8' }, { volume: '8010000', price: '1e-8' } ], pair: 'BIX_BTC' } } */ _constructLevel2Snapshot(msg, market) { const asks = msg.data.asks.map(p => new Level2Point_1.Level2Point(p.price, p.volume)); const bids = msg.data.bids.map(p => new Level2Point_1.Level2Point(p.price, p.volume)); return new Level2Snapshots_1.Level2Snapshot({ exchange: "Bibox", base: market.base, quote: market.quote, timestampMs: msg.data.update_time, asks, bids, }); } } exports.BiboxBasicClient = BiboxBasicClient; function candlePeriod(period) { switch (period) { case CandlePeriod_1.CandlePeriod._1m: return "1min"; case CandlePeriod_1.CandlePeriod._5m: return "5min"; case CandlePeriod_1.CandlePeriod._15m: return "15min"; case CandlePeriod_1.CandlePeriod._30m: return "30min"; case CandlePeriod_1.CandlePeriod._1h: return "1hour"; case CandlePeriod_1.CandlePeriod._2h: return "2hour"; case CandlePeriod_1.CandlePeriod._4h: return "4hour"; case CandlePeriod_1.CandlePeriod._6h: return "6hour"; case CandlePeriod_1.CandlePeriod._12h: return "12hour"; case CandlePeriod_1.CandlePeriod._1d: return "day"; case CandlePeriod_1.CandlePeriod._1w: return "week"; } } //# sourceMappingURL=BiboxClient.js.map