tlab-trading-toolkit
Version:
A trading toolkit for building advanced trading bots on the GDAX platform
404 lines (403 loc) • 16.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ProductMap_1 = require("../ProductMap");
/***************************************************************************************************************************
* @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. *
***************************************************************************************************************************/
const GDAXExchangeAPI_1 = require("./GDAXExchangeAPI");
const types_1 = require("../../lib/types");
const ExchangeFeed_1 = require("../ExchangeFeed");
exports.GDAX_WS_FEED = 'wss://ws-feed.pro.coinbase.com';
const NO_BOOK = process.env.NO_BOOK;
var noBook = false;
if (NO_BOOK === 'true') {
noBook = true;
}
/**
* The GDAX message feed. Messages are created via a combination of WS and REST calls, which are then sent down the pipe.
* It handles automatically reconnects on errors and tracks the connection by monitoring a heartbeat.
* You can create the feeds from here, but it's preferable to use the `getFeed` or `FeedFactory` functions to get a
* connection from the pool
*/
class GDAXFeed extends ExchangeFeed_1.ExchangeFeed {
constructor(config) {
super(config);
this.queue = {};
this.queueing = {};
this.internalSequence = {};
this.products = new Set();
this.channels = ['matches', 'heartbeat'];
if (!noBook)
this.channels.push('level2');
this.gdaxAPI = new GDAXExchangeAPI_1.GDAXExchangeAPI(config);
this.connect();
}
get owner() {
return 'GDAX';
}
mapProduct(id) {
if (!id)
return "";
return ProductMap_1.ProductMap.ExchangeMap.get('GDAX').getGenericProduct(id);
}
/**
* Returns the Authenticated API instance if auth credentials were supplied in the constructor; null otherwise
*/
get authenticatedAPI() {
if (this.auth) {
return this.gdaxAPI;
}
return null;
}
/**
* Subscribe to the products given in the `products` array.
*
* `subscribe` returns a Promise that resolves to true if the subscription was successful.
*/
subscribe(products) {
if (!this.isConnected()) {
return Promise.reject(new Error('Socket is not connected. Have you called connect()? Otherwise the connection may have dropped and is in the process of reconnecting.'));
}
// To reset, we need to make a call with `product_ids` set
return new Promise((resolve, reject) => {
let subscribeMessage = {
type: 'subscribe',
product_ids: products // Use product_id to prevent clearing the other subscriptions
};
subscribeMessage.channels = this.channels;
// Add Signature
if (this.auth) {
subscribeMessage = this.signMessage(subscribeMessage);
}
this.send(subscribeMessage, (err) => {
if (err) {
this.log('error', `The subscription request to ${products.join(',')} on ${this.url} ${this.auth ? '(authenticated)' : ''} failed`, { error: err });
this.emit('error', err);
return reject(err);
}
this.products = new Set();
products.forEach((p) => {
this.products.add(p);
});
return resolve(true);
});
});
}
onClose(code, reason) {
// The feed has been closed by the other party. Wait a few seconds and then reconnect
this.log('info', `The websocket feed to ${this.url} ${this.auth ? '(authenticated)' : ''} has been closed by an external party. We will reconnect in 5 seconds`, {
code: code,
reason: reason
});
this.reconnect(5000);
}
validateAuth(auth) {
auth = super.validateAuth(auth);
return auth && auth.passphrase ? auth : undefined;
}
/**
* Converts a GDAX feed message into a GTT [[StreamMessage]] instance
*/
handleMessage(msg) {
try {
const feedMessage = JSON.parse(msg);
let message;
switch (feedMessage.type) {
case 'heartbeat':
this.confirmAlive();
return;
case 'ticker':
message = this.mapTicker(feedMessage);
break;
case 'l2update':
this.processUpdate(feedMessage);
return;
case 'snapshot':
this.processSnapshot(this.createSnapshotMessage(feedMessage));
return;
default:
message = this.mapFullFeed(feedMessage);
}
if (message) {
if (feedMessage.sequence) {
message.sourceSequence = feedMessage.sequence;
}
this.pushMessage(message);
}
}
catch (err) {
err.ws_msg = msg;
this.onError(err);
}
}
onOpen() {
// If we have any products (this might be a reconnect), then re-subscribe to them
if (this.products.size > 0) {
const products = Array.from(this.products);
this.log('debug', `Resubscribing to ${products.join(' ')}...`);
this.subscribe(products).then((result) => {
if (result) {
this.log('debug', `Reconnection to ${products.join(', ')} successful`);
}
else {
this.log('debug', `We were already connected to the feed it seems.`);
}
}, (err) => {
this.log('error', 'An error occurred while reconnecting. Trying again in 30s', { error: err });
this.reconnect(30000);
});
}
}
signMessage(msg) {
const headers = this.gdaxAPI.getSignature('GET', '/users/self', '');
msg.signature = headers['CB-ACCESS-SIGN'];
msg.key = headers['CB-ACCESS-KEY'];
msg.timestamp = headers['CB-ACCESS-TIMESTAMP'];
msg.passphrase = headers['CB-ACCESS-PASSPHRASE'];
return msg;
}
/**
* Returns the current message counter value for the given product. This does not correspond to the
* official sequence numbers of the message feeds (if they exist), but is purely an internal counter value
*/
getSequence(product) {
if (!this.internalSequence[product]) {
this.internalSequence[product] = 1;
}
return this.internalSequence[product];
}
/**
* Marked for deprecation
*/
pushMessage(message) {
const product = (message.productId);
const needsQueue = message.sequence && this.queueing[product];
// If we're waiting for a snapshot, and the message needs one (i.e. has a sequence number) queue it up, else send it straight on
if (!product || !needsQueue) {
this.push(message);
return;
}
this.queue[product].push(message);
}
createSnapshotMessage(snapshot) {
const product = this.mapProduct(snapshot.product_id);
const orders = {};
const snapshotMessage = {
type: 'snapshot',
time: new Date(),
productId: product,
sequence: this.getSequence(product),
asks: [],
bids: [],
orderPool: orders
};
['buy', 'sell'].forEach((side) => {
const levelArray = side === 'buy' ? 'bids' : 'asks';
snapshot[levelArray].forEach(([price, size]) => {
if (+size === 0) {
return;
}
const newOrder = {
id: price,
price: types_1.Big(price),
size: types_1.Big(size),
side: side
};
const level = {
price: types_1.Big(price),
totalSize: types_1.Big(size),
orders: [newOrder]
};
snapshotMessage[levelArray].push(level);
orders[newOrder.id] = newOrder;
});
});
return snapshotMessage;
}
processUpdate(update) {
const product = this.mapProduct(update.product_id);
update.changes.forEach(([side, price, newSize]) => {
this.internalSequence[product] = this.getSequence(product) + 1;
const message = {
type: 'level',
time: new Date(),
price: price,
size: newSize,
count: 1,
sequence: this.getSequence(product),
productId: product,
side: side
};
this.pushMessage(message);
});
}
mapTicker(ticker) {
return {
type: 'ticker',
time: new Date(ticker.time),
productId: this.mapProduct(ticker.product_id),
sequence: ticker.sequence,
price: types_1.Big(ticker.price),
bid: types_1.Big(ticker.best_bid),
ask: types_1.Big(ticker.best_ask),
trade_id: String(ticker.trade_id),
size: types_1.Big(ticker.last_size)
};
}
mapFullFeed(feedMessage) {
if (feedMessage.user_id) {
return this.mapAuthMessage(feedMessage);
}
const message = this.mapMessage(feedMessage);
return message;
}
processSnapshot(snapshot) {
this.push(snapshot);
this.emit('snapshot');
}
/**
* Converts GDAX messages into standardised GTT messages. Unknown messages are passed on as_is
* @param feedMessage
*/
mapMessage(feedMessage) {
switch (feedMessage.type) {
case 'open':
return {
type: 'newOrder',
time: new Date(feedMessage.time),
sequence: feedMessage.sequence,
productId: this.mapProduct(feedMessage.product_id),
orderId: feedMessage.order_id,
side: feedMessage.side,
price: feedMessage.price,
size: feedMessage.remaining_size
};
case 'done':
// remaining size is usually 0 -- and the corresponding match messages will have adjusted the orderbook
// There are cases when market orders are filled but remaining size is non-zero. This is as a result of STP
// or rounding, but the accounting is nevertheless correct. So if reason is 'filled' we can set the size
// to zero before removing the order. Otherwise if cancelled, remaining_size refers to the size
// that was on the order book
const size = feedMessage.reason === 'filled' ? '0' : feedMessage.remaining_size;
return {
type: 'orderDone',
time: new Date(feedMessage.time),
sequence: feedMessage.sequence,
productId: this.mapProduct(feedMessage.product_id),
orderId: feedMessage.order_id,
remainingSize: size,
price: feedMessage.price,
side: feedMessage.side,
reason: feedMessage.reason
};
case 'match':
return this.mapMatchMessage(feedMessage);
case 'change':
const change = feedMessage;
if (change.new_funds && !change.new_size) {
change.new_size = (types_1.Big(change.new_funds).div(change.price).toString());
}
return {
type: 'changedOrder',
time: new Date(change.time),
sequence: change.sequence,
productId: this.mapProduct(change.product_id),
orderId: change.order_id,
side: change.side,
price: change.price,
newSize: change.new_size
};
default:
console.dir("Unknown message for product id ", feedMessage.product_id, feedMessage.type);
console.log(feedMessage);
// return {
// type: 'unknown',
// time: new Date(),
// sequence: (feedMessage as any).sequence,
// productId: this.mapProduct(feedMessage.product_id),
// message: feedMessage
// } as UnknownMessage;
return null;
}
}
mapMatchMessage(msg) {
const takerSide = msg.side === 'buy' ? 'sell' : 'buy';
const trade = {
type: 'trade',
time: new Date(msg.time),
productId: this.mapProduct(msg.product_id),
tradeId: msg.trade_id,
side: takerSide,
price: msg.price,
size: msg.size
};
return trade;
}
/**
* When the user_id field is set, these are authenticated messages only we receive and so deserve special
* consideration
*/
mapAuthMessage(feedMessage) {
switch (feedMessage.type) {
case 'match':
const isTaker = !!feedMessage.taker_user_id;
const time = feedMessage.time ? new Date(feedMessage.time) : new Date();
let side;
if (!isTaker) {
side = feedMessage.side;
}
else {
side = feedMessage.side === 'buy' ? 'sell' : 'buy';
}
return {
type: 'tradeExecuted',
time: time,
productId: this.mapProduct(feedMessage.product_id),
orderId: isTaker ? feedMessage.taker_order_id : feedMessage.maker_order_id,
orderType: isTaker ? 'market' : 'limit',
side: side,
tradeSize: feedMessage.size,
remainingSize: null
};
case 'done':
return {
type: 'tradeFinalized',
time: time,
productId: this.mapProduct(feedMessage.product_id),
orderId: feedMessage.order_id,
reason: feedMessage.reason,
side: feedMessage.side,
filledSize: feedMessage.size,
remainingSize: feedMessage.remaining_size,
};
case 'open':
return {
type: 'myOrderPlaced',
time: time,
productId: this.mapProduct(feedMessage.product_id),
orderId: feedMessage.order_id,
side: feedMessage.side,
price: feedMessage.price,
orderType: feedMessage.type,
size: feedMessage.remaining_size,
sequence: feedMessage.sequence
};
default:
return {
type: 'unknown',
message: feedMessage
};
}
}
}
exports.GDAXFeed = GDAXFeed;