UNPKG

ccxws

Version:

Websocket client for 37 cryptocurrency exchanges

554 lines (553 loc) 19.1 kB
"use strict"; 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; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OkexClient = void 0; /* eslint-disable @typescript-eslint/member-ordering */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-implied-eval */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ const moment_1 = __importDefault(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 Ticker_1 = require("../Ticker"); const Trade_1 = require("../Trade"); const zlib = __importStar(require("../ZlibUtils")); const pongBuffer = Buffer.from("pong"); /** * Implements OKEx V3 WebSocket API as defined in * https://www.okex.com/docs/en/#spot_ws-general * * Limits: * 1 connection / second * 240 subscriptions / hour * * Connection will disconnect after 30 seconds of silence * it is recommended to send a ping message that contains the * message "ping". * * Order book depth includes maintenance of a checksum for the * first 25 values in the orderbook. Each update includes a crc32 * checksum that can be run to validate that your order book * matches the server. If the order book does not match you should * issue a reconnect. * * Refer to: https://www.okex.com/docs/en/#spot_ws-checksum */ class OkexClient extends BasicClient_1.BasicClient { constructor({ wssPath = "wss://real.okex.com:8443/ws/v3", watcherMs, sendThrottleMs = 20, } = {}) { super(wssPath, "OKEx", undefined, watcherMs); this._sendSubLevel3Snapshots = NotImplementedFn_1.NotImplementedFn; this._sendUnsubLevel3Snapshots = NotImplementedFn_1.NotImplementedFn; this._sendSubLevel3Updates = NotImplementedFn_1.NotImplementedFn; this._sendUnsubLevel3Updates = NotImplementedFn_1.NotImplementedFn; this.candlePeriod = CandlePeriod_1.CandlePeriod._1m; this.hasTickers = true; this.hasTrades = true; this.hasCandles = true; this.hasLevel2Snapshots = true; this.hasLevel2Updates = true; this._sendMessage = (0, Throttle_1.throttle)(this.__sendMessage.bind(this), sendThrottleMs); } _beforeClose() { this._sendMessage.cancel(); } _beforeConnect() { this._wss.on("connected", this._startPing.bind(this)); this._wss.on("disconnected", this._stopPing.bind(this)); this._wss.on("closed", this._stopPing.bind(this)); } _startPing() { clearInterval(this._pingInterval); this._pingInterval = setInterval(this._sendPing.bind(this), 15000); } _stopPing() { clearInterval(this._pingInterval); } _sendPing() { if (this._wss) { this._wss.send("ping"); } } /** * Constructs a market argument in a backwards compatible manner where * the default is a spot market. */ _marketArg(method, market) { const type = (market.type || "spot").toLowerCase(); return `${type.toLowerCase()}/${method}:${market.id}`; } /** * Gets the exchanges interpretation of the candle period */ _candlePeriod(period) { switch (period) { case CandlePeriod_1.CandlePeriod._1m: return "60s"; case CandlePeriod_1.CandlePeriod._3m: return "180s"; case CandlePeriod_1.CandlePeriod._5m: return "300s"; case CandlePeriod_1.CandlePeriod._15m: return "900s"; case CandlePeriod_1.CandlePeriod._30m: return "1800s"; case CandlePeriod_1.CandlePeriod._1h: return "3600s"; case CandlePeriod_1.CandlePeriod._2h: return "7200s"; case CandlePeriod_1.CandlePeriod._4h: return "14400s"; case CandlePeriod_1.CandlePeriod._6h: return "21600s"; case CandlePeriod_1.CandlePeriod._12h: return "43200s"; case CandlePeriod_1.CandlePeriod._1d: return "86400s"; case CandlePeriod_1.CandlePeriod._1w: return "604800s"; } } __sendMessage(msg) { this._wss.send(msg); } _sendSubTicker(remote_id, market) { this._sendMessage(JSON.stringify({ op: "subscribe", args: [this._marketArg("ticker", market)], })); } _sendUnsubTicker(remote_id, market) { this._sendMessage(JSON.stringify({ op: "unsubscribe", args: [this._marketArg("ticker", market)], })); } _sendSubTrades(remote_id, market) { this._sendMessage(JSON.stringify({ op: "subscribe", args: [this._marketArg("trade", market)], })); } _sendUnsubTrades(remote_id, market) { this._sendMessage(JSON.stringify({ op: "unsubscribe", args: [this._marketArg("trade", market)], })); } _sendSubCandles(remote_id, market) { this._sendMessage(JSON.stringify({ op: "subscribe", args: [this._marketArg("candle" + this._candlePeriod(this.candlePeriod), market)], })); } _sendUnsubCandles(remote_id, market) { this._sendMessage(JSON.stringify({ op: "unsubscribe", args: [this._marketArg("candle" + this._candlePeriod(this.candlePeriod), market)], })); } _sendSubLevel2Snapshots(remote_id, market) { this._sendMessage(JSON.stringify({ op: "subscribe", args: [this._marketArg("depth5", market)], })); } _sendUnsubLevel2Snapshots(remote_id, market) { this._sendMessage(JSON.stringify({ op: "unsubscribe", args: [this._marketArg("depth5", market)], })); } _sendSubLevel2Updates(remote_id, market) { this._sendMessage(JSON.stringify({ op: "subscribe", args: [this._marketArg("depth_l2_tbt", market)], })); } _sendUnsubLevel2Updates(remote_id, market) { this._sendMessage(JSON.stringify({ op: "unsubscribe", args: [this._marketArg("depth_l2_tbt", market)], })); } _onMessage(compressed) { zlib.inflateRaw(compressed, (err, raw) => { if (err) { this.emit("error", err); return; } // ignore pongs if (raw.equals(pongBuffer)) { return; } // process JSON message try { const msg = JSON.parse(raw.toString()); this._processsMessage(msg); } catch (ex) { this.emit("error", ex); } }); } _processsMessage(msg) { // clear semaphore on subscription event reply if (msg.event === "subscribe") { return; } // ignore unsubscribe if (msg.event === "unsubscribe") { return; } // prevent failed messages from if (!msg.data) { // eslint-disable-next-line no-console console.warn("warn: failure response", JSON.stringify(msg)); return; } // tickers if (msg.table.match(/ticker/)) { this._processTicker(msg); return; } // trades if (msg.table.match(/trade/)) { this._processTrades(msg); return; } // candles if (msg.table.match(/candle/)) { this._processCandles(msg); return; } // l2 snapshots if (msg.table.match(/depth5/)) { this._processLevel2Snapshot(msg); return; } // l2 updates if (msg.table.match(/depth/)) { this._processLevel2Update(msg); return; } } /** * Process ticker messages in the format { table: 'spot/ticker', data: [ { instrument_id: 'ETH-BTC', last: '0.02181', best_bid: '0.0218', best_ask: '0.02181', open_24h: '0.02247', high_24h: '0.02262', low_24h: '0.02051', base_volume_24h: '379522.2418555', quote_volume_24h: '8243.729793336415', timestamp: '2019-07-15T17:10:55.671Z' } ] } */ _processTicker(msg) { for (const datum of msg.data) { // ensure market const remoteId = datum.instrument_id; const market = this._tickerSubs.get(remoteId); if (!market) continue; // construct and emit ticker const ticker = this._constructTicker(datum, market); this.emit("ticker", ticker, market); } } /** * Processes trade messages in the format { table: 'spot/trade', data: [ { instrument_id: 'ETH-BTC', price: '0.0218', side: 'sell', size: '1.1', timestamp: '2019-07-15T17:10:56.047Z', trade_id: '776432498' } ] } */ _processTrades(msg) { for (const datum of msg.data) { // ensure market const remoteId = datum.instrument_id; const market = this._tradeSubs.get(remoteId); if (!market) continue; // construct and emit trade const trade = this._constructTrade(datum, market); this.emit("trade", trade, market); } } /** * Processes a candle message { "table": "spot/candle60s", "data": [ { "candle": [ "2020-08-10T20:42:00.000Z", "0.03332", "0.03332", "0.03331", "0.03332", "44.058532" ], "instrument_id": "ETH-BTC" } ] } */ _processCandles(msg) { for (const datum of msg.data) { // ensure market const remoteId = datum.instrument_id; const market = this._candleSubs.get(remoteId); if (!market) continue; // construct and emit candle const candle = this._constructCandle(datum); this.emit("candle", candle, market); } } /** * Processes a level 2 snapshot message in the format: { table: 'spot/depth5', data: [{ asks: [ ['0.02192', '1.204054', '3' ] ], bids: [ ['0.02191', '15.117671', '3' ] ], instrument_id: 'ETH-BTC', timestamp: '2019-07-15T16:54:42.301Z' } ] } */ _processLevel2Snapshot(msg) { for (const datum of msg.data) { // ensure market const remote_id = datum.instrument_id; const market = this._level2SnapshotSubs.get(remote_id); if (!market) return; // construct snapshot const snapshot = this._constructLevel2Snapshot(datum, market); this.emit("l2snapshot", snapshot, market); } } /** * Processes a level 2 update message in one of two formats. * The first message received is the "partial" orderbook and contains * 200 records in it. * { table: 'spot/depth', action: 'partial', data: [ { instrument_id: 'ETH-BTC', asks: [Array], bids: [Array], timestamp: '2019-07-15T17:18:31.737Z', checksum: 723501244 } ] } * * Subsequent calls will include the updates stream for changes to * the order book: * { table: 'spot/depth', action: 'update', data: [ { instrument_id: 'ETH-BTC', asks: [Array], bids: [Array], timestamp: '2019-07-15T17:18:32.289Z', checksum: 680530848 } ] } */ _processLevel2Update(msg) { const action = msg.action; for (const datum of msg.data) { // ensure market const remote_id = datum.instrument_id; const market = this._level2UpdateSubs.get(remote_id); if (!market) continue; // handle updates if (action === "partial") { const snapshot = this._constructLevel2Snapshot(datum, market); this.emit("l2snapshot", snapshot, market); } else if (action === "update") { const update = this._constructLevel2Update(datum, market); this.emit("l2update", update, market); } else { // eslint-disable-next-line no-console console.error("Unknown action type", msg); } } } /** * Constructs a ticker from the datum in the format: { instrument_id: 'ETH-BTC', last: '0.02172', best_bid: '0.02172', best_ask: '0.02173', open_24h: '0.02254', high_24h: '0.02262', low_24h: '0.02051', base_volume_24h: '378400.064179', quote_volume_24h: '8226.4437921288', timestamp: '2019-07-15T16:10:40.193Z' } */ _constructTicker(data, market) { const { last, best_bid, best_bid_size, best_ask, best_ask_size, open_24h, high_24h, low_24h, base_volume_24h, volume_24h, // found in futures timestamp, } = data; const change = parseFloat(last) - parseFloat(open_24h); const changePercent = change / parseFloat(open_24h); const ts = moment_1.default.utc(timestamp).valueOf(); return new Ticker_1.Ticker({ exchange: this.name, base: market.base, quote: market.quote, timestamp: ts, last, open: open_24h, high: high_24h, low: low_24h, volume: base_volume_24h || volume_24h, change: change.toFixed(8), changePercent: changePercent.toFixed(2), bid: best_bid || "0", bidVolume: best_bid_size || "0", ask: best_ask || "0", askVolume: best_ask_size || "0", }); } /** * Constructs a trade from the message datum in format: { instrument_id: 'ETH-BTC', price: '0.02182', side: 'sell', size: '0.94', timestamp: '2019-07-15T16:38:02.169Z', trade_id: '776370532' } */ _constructTrade(datum, market) { const { price, side, size, timestamp, trade_id, qty } = datum; const ts = moment_1.default.utc(timestamp).valueOf(); return new Trade_1.Trade({ exchange: this.name, base: market.base, quote: market.quote, tradeId: trade_id, side, unix: ts, price, amount: size || qty, }); } /** * Constructs a candle for the market { "candle": [ "2020-08-10T20:42:00.000Z", "0.03332", "0.03332", "0.03331", "0.03332", "44.058532" ], "instrument_id": "ETH-BTC" } * @param {*} datum */ _constructCandle(datum) { const [datetime, open, high, low, close, volume] = datum.candle; const ts = moment_1.default.utc(datetime).valueOf(); return new Candle_1.Candle(ts, open, high, low, close, volume); } /** * Constructs a snapshot message from the datum in a * snapshot message data property. Datum in the format: * { instrument_id: 'ETH-BTC', asks: [ ['0.02192', '1.204054', '3' ] ], bids: [ ['0.02191', '15.117671', '3' ] ], timestamp: '2019-07-15T16:54:42.301Z' } * * The snapshot may also come from an update, in which case we need * to include the checksum * { instrument_id: 'ETH-BTC', asks: [ ['0.02192', '1.204054', '3' ] ], bids: [ ['0.02191', '15.117671', '3' ] ], timestamp: '2019-07-15T17:18:31.737Z', checksum: 723501244 } */ _constructLevel2Snapshot(datum, market) { const asks = datum.asks.map(p => new Level2Point_1.Level2Point(p[0], p[1], p[2])); const bids = datum.bids.map(p => new Level2Point_1.Level2Point(p[0], p[1], p[2])); const ts = moment_1.default.utc(datum.timestamp).valueOf(); const checksum = datum.checksum; return new Level2Snapshots_1.Level2Snapshot({ exchange: this.name, base: market.base, quote: market.quote, timestampMs: ts, asks, bids, checksum, }); } /** * Constructs an update message from the datum in the update * stream. Datum is in the format: { instrument_id: 'ETH-BTC', asks: [ ['0.02192', '1.204054', '3' ] ], bids: [ ['0.02191', '15.117671', '3' ] ], timestamp: '2019-07-15T17:18:32.289Z', checksum: 680530848 } */ // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility _constructLevel2Update(datum, market) { const asks = datum.asks.map(p => new Level2Point_1.Level2Point(p[0], p[1], p[3])); const bids = datum.bids.map(p => new Level2Point_1.Level2Point(p[0], p[1], p[3])); const ts = moment_1.default.utc(datum.timestamp).valueOf(); const checksum = datum.checksum; return new Level2Update_1.Level2Update({ exchange: this.name, base: market.base, quote: market.quote, timestampMs: ts, asks, bids, checksum, }); } } exports.OkexClient = OkexClient; //# sourceMappingURL=OkexClient.js.map