UNPKG

ccxws

Version:

Websocket client for 37 cryptocurrency exchanges

547 lines (494 loc) 16.4 kB
/* 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 */ import moment = require("moment"); import { BasicClient } from "../BasicClient"; import { Candle } from "../Candle"; import { CandlePeriod } from "../CandlePeriod"; import { ClientOptions } from "../ClientOptions"; import { CancelableFn } from "../flowcontrol/Fn"; import { throttle } from "../flowcontrol/Throttle"; import { Level2Point } from "../Level2Point"; import { Level2Snapshot } from "../Level2Snapshots"; import { Level2Update } from "../Level2Update"; import { NotImplementedFn } from "../NotImplementedFn"; import { SmartWss } from "../SmartWss"; import { Ticker } from "../Ticker"; import { Trade } from "../Trade"; import { wait } from "../Util"; import * as https from "../Https"; import * as zlib from "../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. */ export class BittrexClient extends BasicClient { public candlePeriod: CandlePeriod; public orderBookDepth: number; public connectInitTimeoutMs: number; protected _subbedTickers: boolean; protected _messageId: number; protected _requestLevel2Snapshot: CancelableFn; protected _sendSubLevel2Snapshots = NotImplementedFn; protected _sendUnsubLevel2Snapshots = NotImplementedFn; protected _sendSubLevel3Snapshots = NotImplementedFn; protected _sendUnsubLevel3Snapshots = NotImplementedFn; protected _sendSubLevel3Updates = NotImplementedFn; protected _sendUnsubLevel3Updates = NotImplementedFn; constructor({ wssPath, watcherMs = 15000, throttleL2Snapshot = 100 }: ClientOptions = {}) { super(wssPath, "Bittrex", undefined, watcherMs); this.hasTickers = true; this.hasTrades = true; this.hasCandles = true; this.hasLevel2Snapshots = false; this.hasLevel2Updates = true; this.hasLevel3Snapshots = false; this.hasLevel3Updates = false; this.candlePeriod = 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 = throttle( this.__requestLevel2Snapshot.bind(this), throttleL2Snapshot, ); } //////////////////////////////////// // PROTECTED protected _beforeConnect() { this._wss.on("connected", () => this._sendHeartbeat()); } protected _beforeClose() { this._subbedTickers = false; this._requestLevel2Snapshot.cancel(); } protected _sendHeartbeat() { this._wss.send( JSON.stringify({ H: "c3", M: "Subscribe", A: [["heartbeat"]], I: ++this._messageId, }), ); } protected _sendSubTicker() { if (this._subbedTickers) return; this._subbedTickers = true; this._wss.send( JSON.stringify({ H: "c3", M: "Subscribe", A: [["market_summaries"]], I: ++this._messageId, }), ); } protected _sendUnsubTicker() { // no-op } protected _sendSubTrades(remote_id) { this._wss.send( JSON.stringify({ H: "c3", M: "Subscribe", A: [[`trade_${remote_id}`]], I: ++this._messageId, }), ); } protected _sendUnsubTrades(remote_id) { this._wss.send( JSON.stringify({ H: "c3", M: "Unsubscribe", A: [[`trade_${remote_id}`]], I: ++this._messageId, }), ); } protected _sendSubCandles(remote_id) { this._wss.send( JSON.stringify({ H: "c3", M: "Subscribe", A: [[`candle_${remote_id}_${candlePeriod(this.candlePeriod)}`]], I: ++this._messageId, }), ); } protected _sendUnsubCandles(remote_id) { this._wss.send( JSON.stringify({ H: "c3", M: "Unsubscribe", A: [[`candle_${remote_id}_${candlePeriod(this.candlePeriod)}`]], I: ++this._messageId, }), ); } protected _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, }), ); } protected _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. */ protected _connect() { if (!this._wss) { this._wss = { status: "connecting" } as any; 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. */ protected async _connectAsync() { let wssPath = this.wssPath; // Retry HTTP requests until we are successful while (!wssPath) { try { const data = JSON.stringify([{ name: "c3" }]); const negotiations: any = 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 wait(this.connectInitTimeoutMs); this._onError(ex); } } // Construct a socket and bind all events const wss = new 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(); } protected _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' } ] } */ protected _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); } } protected _constructTicker(msg, market) { const { high, low, volume, quoteVolume, percentChange, updatedAt } = msg; return new 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' } */ protected _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); } } protected _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({ 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' } } */ protected _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); } protected _constructCandle(msg) { return new 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: [] } */ protected _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); } protected _constructLevel2Update(msg, market) { const sequenceId = msg.sequence; const depth = msg.depth; const bids = msg.bidDeltas.map( p => new Level2Point(p.rate, p.quantity, undefined, { depth }), ); const asks = msg.askDeltas.map( p => new Level2Point(p.rate, p.quantity, undefined, { depth }), ); return new Level2Update({ exchange: this.name, base: market.base, quote: market.quote, sequenceId, asks, bids, }); } protected async __requestLevel2Snapshot(market) { let failed: any; 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<any>(uri); const raw = data; const sequence = +response.headers.sequence; const asks = raw.ask.map(p => new Level2Point(p.rate, p.quantity)); const bids = raw.bid.map(p => new Level2Point(p.rate, p.quantity)); const snapshot = new 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") as any; 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); } } } } function candlePeriod(period) { switch (period) { case CandlePeriod._1m: return "MINUTE_1"; case CandlePeriod._5m: return "MINUTE_5"; case CandlePeriod._1h: return "HOUR_1"; case CandlePeriod._1d: return "DAY_1"; } }