tlab-trading-toolkit
Version:
A trading toolkit for building advanced trading bots on the GDAX platform
337 lines (336 loc) • 12.3 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 ExchangeFeed_1 = require("../ExchangeFeed");
const PoloniexCommon_1 = require("./PoloniexCommon");
const types_1 = require("../../lib/types");
const AUTH_CHANNEL = 1000;
const TROLL_BOX = 1001;
const TICKER_CHANNEL = 1002;
class PoloniexFeed extends ExchangeFeed_1.ExchangeFeed {
constructor(config) {
super(config);
this.pinger = null;
this.subscriptions = {};
this.tickerChannel = config.tickerChannel;
this.connect();
}
subscribe(channel) {
if (this.subscriptions[channel]) {
return this.resubscribe(channel);
}
this.subscriptions[channel] = {
id: channel,
connected: false,
sequence: -1
};
const message = {
command: 'subscribe',
channel: channel
};
this.send(message);
}
unsubscribe(channel) {
this.send({ command: 'unsubscribe', channel: channel });
}
resubscribe(channel) {
this.once(`unsubscribed-${channel}`, () => {
this.subscribe(channel);
});
this.unsubscribe(channel);
}
getSubscriptions() {
return Object.values(this.subscriptions);
}
get owner() {
return 'Poloniex';
}
handleMessage(message) {
this.confirmAlive();
let msg;
try {
msg = JSON.parse(message);
}
catch (e) {
this.log('error', 'Non-JSON WS message from Poloniex', { data: message });
return;
}
if ('error' in msg) {
this.log('error', 'An error message was received from Poloniex', { data: msg });
return;
}
if (!Array.isArray(msg)) {
this.log('warn', 'Poloniex WS message was not an array', { data: msg });
return;
}
if (msg.length === 0) {
return;
}
const channelId = msg[0];
switch (channelId) {
case AUTH_CHANNEL:
this.handle_user_message(msg);
return;
case TROLL_BOX:
this.handle_trollbox_message(msg);
return;
case TICKER_CHANNEL:
this.handle_ticker_message(msg);
return;
case 1003:
this.handle_total_volume_message(msg);
return;
case 1010:
return; // Ignore ping
default:
if (channelId > 0 && channelId < 1000) {
return this.handle_orderbook_message(msg);
}
return this.handle_unknown_system_message(msg);
}
}
clear_pinger() {
if (this.pinger) {
clearInterval(this.pinger);
this.pinger = null;
}
}
onOpen() {
// If this is due to a re-connect, resubscribe to channels
const oldSubscriptions = this.subscriptions;
this.subscriptions = {};
for (const key in oldSubscriptions) {
const channel = oldSubscriptions[key];
this.subscribe(channel.id);
}
this.pinger = setInterval(() => {
if (!this.isConnected()) {
this.clear_pinger();
return;
}
try {
this.send('.');
}
catch (err) {
this.log('warn', 'Failed to send PING to Poloniex. We should resubscribe');
this.reconnect(250);
}
}, 29000);
if (this.tickerChannel) {
this.subscribe(TICKER_CHANNEL);
}
if (this.auth) {
this.subscribe(AUTH_CHANNEL);
}
}
onClose(code, reason) {
super.onClose(code, reason);
this.clear_pinger();
this.reconnect(1000);
}
handle_user_message(msg) {
if (msg.length === 2 && msg[1] === 1) {
this.subscriptions[1000].connected = true;
return;
}
// I don't know what these messages are yet, so just log them for now
const message = {
type: 'poloniex-user',
time: new Date(),
productId: null,
sequence: null,
message: msg
};
this.push(message);
}
handle_trollbox_message(msg) {
if (msg.length === 2 && msg[1] === 1) {
this.subscriptions[1001].connected = true;
return;
}
const chat = {
sequence: +msg[1],
user: msg[2],
text: msg[3],
reputation: +msg[4]
};
const message = {
type: 'poloniex-trollbox',
time: new Date(),
productId: null,
sequence: null,
message: chat
};
this.push(message);
}
handle_total_volume_message(msg) {
const message = {
type: 'poloniex-volume',
time: new Date(msg[2][0]),
productId: null,
sequence: msg[1],
message: {
serverTime: msg[2][0],
usersOnline: msg[2][1],
volume: msg[2][2]
}
};
this.push(message);
}
handle_ticker_message(msg) {
if (msg.length === 2 && msg[1] === 1) {
this.subscriptions[1002].connected = true;
return;
}
const data = msg[2];
if (!Array.isArray(data) || data.length !== 10) {
this.log('warn', 'Unexpected ticker array in Poloniex WS message', { data: msg });
return;
}
PoloniexCommon_1.getProductInfo(data[0], false, this.logger).then((info) => {
const ticker = {
type: 'ticker',
time: new Date(),
productId: info.id,
price: types_1.Big(data[1]),
bid: types_1.Big(data[3]),
ask: types_1.Big(data[2]),
volume: types_1.Big(data[4])
};
this.push(ticker);
});
}
handle_unknown_system_message(msg) {
const message = {
type: 'poloniex-unknown',
time: new Date(),
productId: null,
sequence: null,
message: msg
};
this.push(message);
}
handle_orderbook_message(msg) {
const id = msg[0];
const self = this;
PoloniexCommon_1.getProductInfo(id, false, this.logger).then((info) => {
if (msg[1] === 0) {
this.log('debug', `Unsubscribed from ${info.id}`);
delete this.subscriptions[id];
setImmediate(() => {
this.emit(`unsubscribed-${id}`, id);
});
return;
}
const channelInfo = this.subscriptions[id];
if (!channelInfo) {
return;
}
if (msg[1] === 1) {
channelInfo.connected = true;
return;
}
const product = info.id;
const sequence = +msg[1];
const data = msg[2];
if (!Array.isArray(data)) {
this.log('warn', 'Unexpected order-book array in Poloniex WS message', { data: msg });
return;
}
channelInfo.sequence = sequence;
// Handle snapshot message
data.forEach((update, i) => {
send_orderbook_update(i, update);
});
function send_orderbook_update(index, update) {
if (!Array.isArray(update)) {
self.log('warn', `Unexpected order-book update message in ${product} #${sequence}.${index}`, { data: update });
return;
}
const type = update[0];
if (type === 'i') {
channelInfo.connected = true;
channelInfo.sequence = sequence;
const snapshot = self.createSnapshotMessage(product, sequence, update[1]);
self.push(snapshot);
return;
}
if (type === 'o') {
const message = {
type: 'level',
time: new Date(),
sequence: sequence,
sourceSequence: sequence,
productId: product,
side: update[1] === 0 ? 'sell' : 'buy',
price: update[2],
size: update[3],
count: 1
};
self.push(message);
return;
}
if (type === 't') {
const message = {
type: 'trade',
productId: product,
time: new Date(update[5] * 1000),
tradeId: update[1],
price: update[3],
size: update[4],
side: update[2] === 1 ? 'buy' : 'sell'
};
self.push(message);
return;
}
}
});
}
createSnapshotMessage(product, sequence, snapshot) {
const orders = {};
const snapshotMessage = {
type: 'snapshot',
time: new Date(),
productId: product,
sequence: sequence,
asks: [],
bids: [],
orderPool: orders
};
for (let i = 0; i <= 1; i++) {
const levelArray = snapshot.orderBook[i];
const sideArray = i === 0 ? snapshotMessage.asks : snapshotMessage.bids;
for (const price in snapshot.orderBook[i]) {
const side = i === 0 ? 'sell' : 'buy';
const size = types_1.Big(levelArray[price]);
const newOrder = {
id: String(price),
price: types_1.Big(price),
size: size,
side: side
};
const level = {
price: types_1.Big(price),
totalSize: types_1.Big(size),
orders: [newOrder]
};
sideArray.push(level);
orders[newOrder.id] = newOrder;
}
}
return snapshotMessage;
}
}
exports.PoloniexFeed = PoloniexFeed;