UNPKG

bitmex-realtime-api

Version:

A library for interacting with BitMEX's websocket API.

244 lines (216 loc) 8.37 kB
const _ = require('lodash'); const EventEmitter = require('eventemitter2').EventEmitter2; const util = require('util'); const debug = require('debug')('BitMEX:realtime-api'); const createSocket = require('./lib/createSocket'); const deltaParser = require('./lib/deltaParser'); const getStreams = require('./lib/getStreams'); const DEFAULT_MAX_TABLE_LEN = 10000; const endpoints = { production: 'wss://ws.bitmex.com/realtime', testnet: 'wss://ws.testnet.bitmex.com/realtime' }; const httpEndpoints = { production: 'https://www.bitmex.com/api/v1', testnet: 'https://testnet.bitmex.com/api/v1' } const noSymbolTables = BitMEXClient.noSymbolTables = [ 'account', 'affiliate', 'funds', 'insurance', 'margin', 'transact', 'wallet', 'announcement', 'connected', 'chat', 'publicNotifications', 'privateNotifications' ]; module.exports = BitMEXClient; function BitMEXClient(options) { const emitter = this; // We inherit from EventEmitter2, which supports wildcards. EventEmitter.call(emitter, { wildcard: true, delimiter: ':', maxListeners: Infinity, newListener: true, }); if (!options) options = {}; this._data = {}; // internal data store keyed by [tableName][symbol]. Used by deltaParser. this._keys = {}; // keys store - populated by images on connect this._maxTableLen = typeof options.maxTableLen === 'number' ? options.maxTableLen : DEFAULT_MAX_TABLE_LEN; if (!options.endpoint) { options.endpoint = options.testnet ? endpoints.testnet : endpoints.production; } if (!options.httpEndpoint) { options.httpEndpoint = options.testnet ? httpEndpoints.testnet : httpEndpoints.production; } if (process.env.BITMEX_ENDPOINT) options.endpoint = process.env.BITMEX_ENDPOINT; debug(options) this._setupListenerTracking(); // Initialize the socket. this.socket = createSocket(options, emitter); if (options.apiKeyID) { this.authenticated = true; } // Get valid streams so we can validate our subscriptions. getStreams(options.httpEndpoint, function(err, streams) { if (err) throw err; emitter.initialized = true; emitter.streams = streams; emitter.emit('initialize'); }); } util.inherits(BitMEXClient, EventEmitter); /** * Simple data getter. Clones data on the way out so it can be safely modified. * @param {String} [symbol] Symbol of data to retrieve. * @param {String} [tableName] Table / stream name. * @return {Object} All current data. If no tableName is provided, will return an object keyed by * the table name. */ BitMEXClient.prototype.getData = function(symbol, tableName) { const tableUsesSymbol = noSymbolTables.indexOf(tableName) === -1; if (!tableUsesSymbol) symbol = '*'; let out; // Both filters specified, easy return if (symbol && tableName) { out = this._data[tableName][symbol] || []; } // Since we're keying by [table][symbol], we have to search deep else if (symbol && !tableName) { out = Object.keys(this._data).reduce((memo, tableKey) => { memo[tableKey] = this._data[tableKey][symbol] || []; return memo; }, {}); } // All table data else if (!symbol && tableName) { out = this._data[tableName] || {}; } else { throw new Error('Pass a symbol, tableName, or both to getData([symbol], [tableName]) - but one must be provided.'); } return _.cloneDeep(out); }; /** * Helper to get data for all symbols, by table. */ BitMEXClient.prototype.getTable = function(tableName) { return this.getData(null, tableName); }; /** * Helper to get data for all tables, by symbol. */ BitMEXClient.prototype.getSymbol = function(symbol) { return this.getData(symbol); }; /** * Add a stream to listen to. This function calls back with a full dataset with the arity * (data, symbol, tableName). * * To catch errors, attach an 'error' listener to the client itself. * * If a tableName of '*' is passed, it will subscribe to all public tables. * * @param {String} symbol Symbol to subscribe to. * @param {String} [tableName] Table to subscribe to. See README. * @param {Function} callback Data callback. */ BitMEXClient.prototype.addStream = function(symbol, tableName, callback) { const client = this; if (!this.initialized) { return this.once('initialize', () => client.addStream(symbol, tableName, callback)); } if (!this.socket.opened) { // Not open yet. Call this when open return this.socket.once('open', () => client.addStream(symbol, tableName, callback)) } // Massage arguments. if (typeof callback !== 'function') throw new Error('A callback must be passed to BitMEXClient#addStream.'); else if (client.streams.all.indexOf(tableName) === -1) { throw new Error('Unknown table for BitMEX subscription: ' + tableName + '. Available tables are ' + client.streams.all + '.'); } // TODO return async iterable instead addStreamHelper(client, symbol, tableName, callback); }; // Keep track of listeners in a tree. This helps us know what is still // subscribed to, so we can open & close connections as required. BitMEXClient.prototype._setupListenerTracking = function() { const listenerTree = this._listenerTree = {}; this.on('newListener', (eventName) => { const split = eventName.split(':'); if (split.length !== 3) return; // other events const [table, , symbol] = split; if (!listenerTree[table]) listenerTree[table] = {}; if (!listenerTree[table][symbol]) listenerTree[table][symbol] = 0; listenerTree[table][symbol]++; }); this.on('removeListener', (eventName) => { const split = eventName.split(':'); if (split.length !== 3) return; // other events const [table, , symbol] = split; listenerTree[table][symbol]--; }); } BitMEXClient.prototype.subscriptionCount = function(table, symbol) { return this._listenerTree[table] && this._listenerTree[table][symbol] || 0; }; BitMEXClient.prototype.sendSubscribeRequest = function(table, symbol) { const subscribePayload = {op: 'subscribe', args: `${table}:${symbol}`}; debug('sending: %j', subscribePayload) this.socket.send(JSON.stringify(subscribePayload)); }; function addStreamHelper(client, symbol, tableName, callback) { const tableUsesSymbol = noSymbolTables.indexOf(tableName) === -1; if (!tableUsesSymbol) symbol = '*'; // Tell BitMEX we want to subscribe to this data. If wildcard, sub to all tables. let toSubscribe; if (tableName === '*') { // This list comes from the getSymbols call, which hits // https://www.bitmex.com/api/v1/schema/websocketHelp toSubscribe = client.streams[client.authenticated ? 'all' : 'public']; } else { // Normal sub toSubscribe = [tableName]; } // For each subscription, toSubscribe.forEach(function(table) { // Create a subscription topic. const subscription = `${table}:*:${symbol}`; debug('Opening listener to %s.', subscription); // Add the listener for deltas before subscribing at BitMEX. // These events come from createSocket, which does minimal data parsing // to figure out what table and symbol the data is for. // // The emitter emits 'partial', 'update', 'insert', and 'delete' events, listen to them all. client.on(subscription, function(data) { const [table, action, symbol] = this.event.split(':'); try { const newData = deltaParser.onAction(action, table, symbol, client, data); // Shift oldest elements out of the table (FIFO queue) to prevent unbounded memory growth if (newData.length > client._maxTableLen) { newData.splice(0, newData.length - client._maxTableLen); } callback(newData, symbol, table); } catch(e) { client.emit('error', e); } }); // If this is the first sub, subscribe to bitmex adapter. if (client.subscriptionCount(table, symbol) === 1) { const openSubscription = () => client.sendSubscribeRequest(table, symbol); // If we reconnect, will need to reopen. client.on('open', openSubscription); // If we're already opened, prime the pump (I made that up) if (client.socket.opened) openSubscription(); } }); } if (require.main === module) { console.error('This module is not meant to be run directly. Try running example.js instead.'); process.exit(1); }