tlab-trading-toolkit
Version:
A trading toolkit for building advanced trading bots on the GDAX platform
223 lines (222 loc) • 9.56 kB
JavaScript
"use strict";
/***************************************************************************************************************************
* @license *
* Copyright 2017 Coinbase, Inc. *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on *
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the *
* License for the specific language governing permissions and limitations under the License. *
***************************************************************************************************************************/
Object.defineProperty(exports, "__esModule", { value: true });
const stream_1 = require("stream");
const crypto_1 = require("crypto");
const WebSocket = require("ws");
class ExchangeFeedConfig {
}
exports.ExchangeFeedConfig = ExchangeFeedConfig;
// hooks for replacing libraries if desired
exports.hooks = {
WebSocket: WebSocket
};
class ExchangeFeed extends stream_1.Readable {
constructor(config) {
super({ objectMode: true, highWaterMark: 1024 });
this.lastHeartBeat = -1;
this.connectionChecker = null;
this.multiSocket = false;
this.sockets = []; //only for multisockets
this._logger = config.logger;
this.url = config.wsUrl;
this.isConnecting = false;
this.auth = this.validateAuth(config.auth);
}
get logger() {
return this._logger;
}
log(level, message, meta) {
if (!this._logger) {
return;
}
this._logger.log(level, message, meta);
}
isConnected() {
return (this.socket && this.socket.readyState === 1) || (this.sockets.length > 0);
}
reconnect(delay) {
this._logger.log('debug', `Reconnecting to ${this.url} ${this.auth ? '(authenticated)' : ''} in ${delay * 0.001} seconds...`);
// If applicable, close the current socket first
if (this.socket && this.socket.readyState < 2) {
this._logger.log('debug', 'Closing existing socket prior to reconnecting to ' + this.url);
this.close();
}
setTimeout(() => {
// Force a reconnect
this.isConnecting = false;
this.connect();
}, delay);
}
disconnect() {
if (!this.isConnected()) {
return;
}
this.close();
}
connect(products) {
console.log('Is multi sockets : ', this.multiSocket);
console.log('Products list : ', products);
if (this.isConnecting || this.isConnected()) {
return;
}
this.isConnecting = true;
if (this.multiSocket && products && products.length > 0) {
products.forEach((product) => {
const socket = new exports.hooks.WebSocket(this.getWebsocketUrlForProduct(product));
socket.on('message', (msg) => {
this.handleMessage(msg, product);
});
socket.on('close', this.killProcess);
socket.on('error', () => this.killProcess(null, null));
this.sockets.push(socket);
this.lastHeartBeat = -1;
});
return;
}
const socket = new exports.hooks.WebSocket(this.url);
socket.on('message', (msg) => this.handleMessage(msg));
socket.on('open', () => this.onNewConnection());
socket.on('close', (code, reason) => this.onClose(code, reason));
socket.on('error', (err) => this.onError(err));
socket.on('pong', () => this.confirmAlive());
socket.on('connection', () => { this.emit('websocket-connection'); });
this.socket = socket;
this.lastHeartBeat = -1;
console.log('Setting up connection checker every 5 sec');
this.connectionChecker = setInterval(() => {
this.checkConnection(60 * 1000);
}, 5 * 1000);
}
ping() {
if (this.socket && this.socket.readyState === this.socket.OPEN) {
this.socket.ping();
}
}
getWebsocketUrlForProduct(product) {
throw ('implement in subclass');
}
killProcess(code, message) {
console.error("Socket error or close ");
console.error("code :", code);
console.error("Message :", message);
process.exit(1);
}
onClose(code, reason) {
this.emit('websocket-closed');
this.socket = null;
}
onError(err) {
if (err)
console.error(err);
this._logger.log('error', `The websocket feed to ${this.url} ${this.auth ? '(authenticated)' : ''} has reported an error. If necessary, we will reconnect.`);
``;
if (!this.socket || this.socket.readyState !== 1) {
this.reconnect(15000);
}
else {
this.resume();
}
}
/**
* Called by sub-classes to confirm that the connection is still alive
*/
confirmAlive() {
this.lastHeartBeat = Date.now();
}
close() {
// We're initiating the socket closure, so don't reconnect
this.socket.removeAllListeners('close');
this.socket.close();
}
onNewConnection() {
this.isConnecting = false;
this.log('debug', `Connection to ${this.url} ${this.auth ? '(authenticated)' : ''} has been established.`);
this.onOpen();
this.emit('websocket-open');
}
/**
* Check that we have received a heartbeat message within the last period ms
*/
checkConnection(period) {
if (this.lastHeartBeat < 0) {
return;
}
const diff = Date.now() - this.lastHeartBeat;
if (diff > period) {
this._logger.log('error', `No heartbeat has been received from ${this.url} ${this.auth ? '(authenticated)' : ''} in ${diff} ms. Assuming the connection is dead and reconnecting`);
clearInterval(this.connectionChecker);
this.reconnect(2500);
}
}
/**
* Checks that the auth object provided is fully populated and is valid. Subclasses can override this to provide
* additional validation steps.
*
* This function should return the auth object or `undefined` if it isn't valid.
*/
validateAuth(auth) {
return auth && auth.key && auth.secret ? auth : undefined;
}
send(msg, cb) {
try {
const msgString = typeof (msg) === 'string' ? msg : JSON.stringify(msg);
console.log(msgString);
this.log('debug', `Sending ${msgString} message to WS server`);
this.socket.send(msgString, cb);
}
catch (err) {
console.error(err);
// If there's an error just log and carry on
this.log('error', 'Could not send message to GDAX WS server because the message was invalid', { error: err, message: msg });
}
}
_read(size) {
// This is not an on-demand service. For that, I refer you to Netflix. Data gets pushed to the queue as it comes
// in from the websocket, so there's nothing to do here.
}
}
exports.ExchangeFeed = ExchangeFeed;
const feedSources = {};
/**
* Get or create a Websocket feed to a GDAX product. A single connection is maintained per URL + auth combination.
* Usually you'll connect to the main GDAX feed by passing in `GDAX_WS_FEED` as the first parameter, but you can create
* additional feeds to the public sandbox, for example by providing the relevant URL; or creating an authenticated and
* public feed (although the authenticated feed also carries public messages)
*/
function getFeed(type, config) {
const auth = config.auth && config.auth.key && config.auth.secret ? config.auth : undefined;
const key = getKey(config.wsUrl, auth);
const logger = config.logger;
let feed = feedSources[key];
if (!feed) {
logger.log('info', `Creating new Websocket connection to ${config.wsUrl} ${auth ? '(authenticated)' : ''}`);
feed = new type(config);
feedSources[key] = feed;
}
else {
logger.log('info', `Using existing GDAX Websocket connection to ${config.wsUrl} ${auth ? '(authenticated)' : ''}`);
}
return feed;
}
exports.getFeed = getFeed;
/**
* Create a unique key hash based on URL and credentials
*/
function getKey(wsUrl, config) {
const index = new Buffer(`${wsUrl}+${JSON.stringify(config)}`, 'base64');
return crypto_1.createHmac('sha256', index).digest('base64');
}
exports.getKey = getKey;