UNPKG

ccxws

Version:

Websocket client for 37 cryptocurrency exchanges

490 lines 16.7 kB
"use strict"; /* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/restrict-plus-operands */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BittrexClient = void 0; const moment = require("moment"); const BasicClient_1 = require("../BasicClient"); const Candle_1 = require("../Candle"); const CandlePeriod_1 = require("../CandlePeriod"); const Throttle_1 = require("../flowcontrol/Throttle"); const Level2Point_1 = require("../Level2Point"); const Level2Snapshots_1 = require("../Level2Snapshots"); const Level2Update_1 = require("../Level2Update"); const NotImplementedFn_1 = require("../NotImplementedFn"); const SmartWss_1 = require("../SmartWss"); const Ticker_1 = require("../Ticker"); const Trade_1 = require("../Trade"); const Util_1 = require("../Util"); const https = __importStar(require("../Https")); const zlib = __importStar(require("../ZlibUtils")); /** * Implements the v3 API: * https://bittrex.github.io/api/v3#topic-Synchronizing * https://bittrex.github.io/guides/v3/upgrade * * This client uses SignalR and requires a custom connection strategy to * obtain a socket. Otherwise, things are relatively the same vs a * standard client. */ class BittrexClient extends BasicClient_1.BasicClient { constructor({ wssPath, watcherMs = 15000, throttleL2Snapshot = 100 } = {}) { super(wssPath, "Bittrex", undefined, watcherMs); this._sendSubLevel2Snapshots = NotImplementedFn_1.NotImplementedFn; this._sendUnsubLevel2Snapshots = NotImplementedFn_1.NotImplementedFn; this._sendSubLevel3Snapshots = NotImplementedFn_1.NotImplementedFn; this._sendUnsubLevel3Snapshots = NotImplementedFn_1.NotImplementedFn; this._sendSubLevel3Updates = NotImplementedFn_1.NotImplementedFn; this._sendUnsubLevel3Updates = NotImplementedFn_1.NotImplementedFn; this.hasTickers = true; this.hasTrades = true; this.hasCandles = true; this.hasLevel2Snapshots = false; this.hasLevel2Updates = true; this.hasLevel3Snapshots = false; this.hasLevel3Updates = false; this.candlePeriod = CandlePeriod_1.CandlePeriod._1m; this.orderBookDepth = 500; this.connectInitTimeoutMs = 5000; this._subbedTickers = false; this._messageId = 0; this._processTickers = this._processTickers.bind(this); this._processTrades = this._processTrades.bind(this); this._processCandles = this._processCandles.bind(this); this._processLevel2Update = this._processLevel2Update.bind(this); this._requestLevel2Snapshot = (0, Throttle_1.throttle)(this.__requestLevel2Snapshot.bind(this), throttleL2Snapshot); } //////////////////////////////////// // PROTECTED _beforeConnect() { this._wss.on("connected", () => this._sendHeartbeat()); } _beforeClose() { this._subbedTickers = false; this._requestLevel2Snapshot.cancel(); } _sendHeartbeat() { this._wss.send(JSON.stringify({ H: "c3", M: "Subscribe", A: [["heartbeat"]], I: ++this._messageId, })); } _sendSubTicker() { if (this._subbedTickers) return; this._subbedTickers = true; this._wss.send(JSON.stringify({ H: "c3", M: "Subscribe", A: [["market_summaries"]], I: ++this._messageId, })); } _sendUnsubTicker() { // no-op } _sendSubTrades(remote_id) { this._wss.send(JSON.stringify({ H: "c3", M: "Subscribe", A: [[`trade_${remote_id}`]], I: ++this._messageId, })); } _sendUnsubTrades(remote_id) { this._wss.send(JSON.stringify({ H: "c3", M: "Unsubscribe", A: [[`trade_${remote_id}`]], I: ++this._messageId, })); } _sendSubCandles(remote_id) { this._wss.send(JSON.stringify({ H: "c3", M: "Subscribe", A: [[`candle_${remote_id}_${candlePeriod(this.candlePeriod)}`]], I: ++this._messageId, })); } _sendUnsubCandles(remote_id) { this._wss.send(JSON.stringify({ H: "c3", M: "Unsubscribe", A: [[`candle_${remote_id}_${candlePeriod(this.candlePeriod)}`]], I: ++this._messageId, })); } _sendSubLevel2Updates(remote_id, market) { this._requestLevel2Snapshot(market); this._wss.send(JSON.stringify({ H: "c3", M: "Subscribe", A: [[`orderbook_${remote_id}_${this.orderBookDepth}`]], I: ++this._messageId, })); } _sendUnsubLevel2Updates(remote_id) { this._wss.send(JSON.stringify({ H: "c3", M: "Subscribe", A: [[`orderbook_${remote_id}_${this.orderBookDepth}`]], I: ++this._messageId, })); } /** * Requires connecting to SignalR which has a whole BS negotiation * to obtain a token, similar to Kucoin actually. */ _connect() { if (!this._wss) { this._wss = { status: "connecting" }; this._connectAsync(); } } /** * Asynchronously connect to a socket. This method will retrieve a token * from an HTTP request and then construct a websocket. If the HTTP * request fails, it will retry until successful. */ async _connectAsync() { let wssPath = this.wssPath; // Retry HTTP requests until we are successful while (!wssPath) { try { const data = JSON.stringify([{ name: "c3" }]); const negotiations = await https.get(`https://socket-v3.bittrex.com/signalr/negotiate?connectionData=${data}&clientProtocol=1.5`); const token = encodeURIComponent(negotiations.ConnectionToken); wssPath = `wss://socket-v3.bittrex.com/signalr/connect?clientProtocol=1.5&transport=webSockets&connectionToken=${token}&connectionData=${data}&tid=10`; } catch (ex) { await (0, Util_1.wait)(this.connectInitTimeoutMs); this._onError(ex); } } // Construct a socket and bind all events const wss = new SmartWss_1.SmartWss(wssPath); this._wss = wss; this._wss.on("error", this._onError.bind(this)); this._wss.on("connecting", this._onConnecting.bind(this)); this._wss.on("connected", this._onConnected.bind(this)); this._wss.on("disconnected", this._onDisconnected.bind(this)); this._wss.on("closing", this._onClosing.bind(this)); this._wss.on("closed", this._onClosed.bind(this)); this._wss.on("message", msg => { try { this._onMessage(msg); } catch (ex) { this._onError(ex); } }); if (this._beforeConnect) this._beforeConnect(); this._wss.connect(); } _onMessage(raw) { const fullMsg = JSON.parse(raw); // Handle responses // {"R":[{"Success":true,"ErrorCode":null},{"Success":true,"ErrorCode":null}],"I":1} if (fullMsg.R) { for (const msg of fullMsg.R) { if (!msg.Success) { this.emit("error", new Error("Subscription failed with error " + msg.ErrorCode)); } } } // Handle messages if (!fullMsg.M) return; for (const msg of fullMsg.M) { if (msg.M === "heartbeat") { this._watcher.markAlive(); } if (msg.M === "marketSummaries") { for (const a of msg.A) { zlib.inflateRaw(Buffer.from(a, "base64"), this._processTickers); } } if (msg.M === "trade") { for (const a of msg.A) { zlib.inflateRaw(Buffer.from(a, "base64"), this._processTrades); } } if (msg.M === "candle") { for (const a of msg.A) { zlib.inflateRaw(Buffer.from(a, "base64"), this._processCandles); } } if (msg.M === "orderBook") { for (const a of msg.A) { zlib.inflateRaw(Buffer.from(a, "base64"), this._processLevel2Update); } } } } /** { "sequence": 3584000, "deltas": [ { symbol: 'BTC-USDT', high: '12448.02615735', low: '11773.32163568', volume: '640.86060471', quoteVolume: '7714634.67704918', percentChange: '3.98', updatedAt: '2020-08-17T20:16:27.617Z' } ] } */ _processTickers(err, raw) { if (err) { this.emit("error", err); return; } let msg; try { msg = JSON.parse(raw); } catch (ex) { this.emit("error", ex); return; } for (const datum of msg.deltas) { const market = this._tickerSubs.get(datum.symbol); if (!market) continue; const ticker = this._constructTicker(datum, market); this.emit("ticker", ticker, market); } } _constructTicker(msg, market) { const { high, low, volume, quoteVolume, percentChange, updatedAt } = msg; return new Ticker_1.Ticker({ exchange: this.name, base: market.base, quote: market.quote, timestamp: moment.utc(updatedAt).valueOf(), last: undefined, open: undefined, high: high, low: low, volume: volume, quoteVolume: quoteVolume, change: undefined, changePercent: percentChange, bid: undefined, ask: undefined, }); } /** { deltas: [ { id: 'edacd990-7c5f-4c75-8a66-ce0a71093b3c', executedAt: '2020-08-17T20:36:39.96Z', quantity: '0.00714818', rate: '12301.34800000', takerSide: 'BUY' } ], sequence: 18344, marketSymbol: 'BTC-USDT' } */ _processTrades(err, raw) { if (err) { this.emit("error", err); return; } let msg; try { msg = JSON.parse(raw); } catch (ex) { this.emit("error", ex); return; } const market = this._tradeSubs.get(msg.marketSymbol); if (!market) return; for (const datum of msg.deltas) { const trade = this._constructTrade(datum, market); this.emit("trade", trade, market); } } _constructTrade(msg, market) { const tradeId = msg.id; const unix = moment.utc(msg.executedAt).valueOf(); const price = msg.rate; const amount = msg.quantity; const side = msg.takerSide === "BUY" ? "buy" : "sell"; return new Trade_1.Trade({ exchange: this.name, base: market.base, quote: market.quote, tradeId, unix, side, price, amount, }); } /** { sequence: 10808, marketSymbol: 'BTC-USDT', interval: 'MINUTE_1', delta: { startsAt: '2020-08-17T20:47:00Z', open: '12311.59599999', high: '12311.59599999', low: '12301.57150000', close: '12301.57150000', volume: '1.65120614', quoteVolume: '20319.96359337' } } */ _processCandles(err, raw) { if (err) { this.emit("error", err); return; } let msg; try { msg = JSON.parse(raw); } catch (ex) { this.emit("error", ex); return; } const market = this._candleSubs.get(msg.marketSymbol); if (!market) return; const candle = this._constructCandle(msg.delta); this.emit("candle", candle, market); } _constructCandle(msg) { return new Candle_1.Candle(moment.utc(msg.startsAt).valueOf(), msg.open, msg.high, msg.low, msg.close, msg.volume); } /** { marketSymbol: 'BTC-USDT', depth: 500, sequence: 545851, bidDeltas: [ { quantity: '0', rate: '12338.47320003' }, { quantity: '0.01654433', rate: '10800.62000000' } ], askDeltas: [] } */ _processLevel2Update(err, raw) { if (err) { this.emit("error", err); return; } let msg; try { msg = JSON.parse(raw); } catch (ex) { this.emit("error", ex); return; } const market = this._level2UpdateSubs.get(msg.marketSymbol); if (!market) return; const update = this._constructLevel2Update(msg, market); this.emit("l2update", update, market); } _constructLevel2Update(msg, market) { const sequenceId = msg.sequence; const depth = msg.depth; const bids = msg.bidDeltas.map(p => new Level2Point_1.Level2Point(p.rate, p.quantity, undefined, { depth })); const asks = msg.askDeltas.map(p => new Level2Point_1.Level2Point(p.rate, p.quantity, undefined, { depth })); return new Level2Update_1.Level2Update({ exchange: this.name, base: market.base, quote: market.quote, sequenceId, asks, bids, }); } async __requestLevel2Snapshot(market) { let failed; try { const remote_id = market.id; const uri = `https://api.bittrex.com/v3/markets/${remote_id}/orderbook?depth=${this.orderBookDepth}`; const { data, response } = await https.getResponse(uri); const raw = data; const sequence = +response.headers.sequence; const asks = raw.ask.map(p => new Level2Point_1.Level2Point(p.rate, p.quantity)); const bids = raw.bid.map(p => new Level2Point_1.Level2Point(p.rate, p.quantity)); const snapshot = new Level2Snapshots_1.Level2Snapshot({ exchange: this.name, base: market.base, quote: market.quote, sequenceId: sequence, asks, bids, }); this.emit("l2snapshot", snapshot, market); } catch (ex) { const err = new Error("L2Snapshot failed"); err.inner = ex.message; err.market = market; this.emit("error", err); failed = err; } finally { if (failed && failed.inner.indexOf("MARKET_DOES_NOT_EXIST") === -1) { this._requestLevel2Snapshot(market); } } } } exports.BittrexClient = BittrexClient; function candlePeriod(period) { switch (period) { case CandlePeriod_1.CandlePeriod._1m: return "MINUTE_1"; case CandlePeriod_1.CandlePeriod._5m: return "MINUTE_5"; case CandlePeriod_1.CandlePeriod._1h: return "HOUR_1"; case CandlePeriod_1.CandlePeriod._1d: return "DAY_1"; } } //# sourceMappingURL=BittrexClient.js.map