tlab-trading-toolkit
Version:
A trading toolkit for building advanced trading bots on the GDAX platform
363 lines (362 loc) • 15.1 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const _1 = require("./");
const types_1 = require("./types");
const RedisConnector_1 = require("../core/RedisConnector");
const Messages_1 = require("../core/Messages");
const core_1 = require("../core");
const stream_1 = require("stream");
/**
* For cumulative order calculations, indicates at which price to start counting at and from which order size to start
* within that level
*/
/**
* BookBuilder is a convenience class for maintaining an in-memory Level 3 order book. Each
* side of the book is represented internally by a binary tree and a global order hash map
*
* The individual orders can be tracked globally via the orderPool set, or per level. The orderpool and the aggregated
* levels point to the same order objects, and not copies.
*
* Call #state to get a hierarchical object representation of the orderbook
*/
class RedisBookConfig extends core_1.LiveBookConfig {
}
exports.RedisBookConfig = RedisBookConfig;
class RedisBook extends stream_1.Writable {
constructor(config) {
super({ objectMode: true, highWaterMark: 1024 });
this._bidsTotal = types_1.ZERO;
this._bidsValueTotal = types_1.ZERO;
this._asksTotal = types_1.ZERO;
this._asksValueTotal = types_1.ZERO;
this.sequence = -1;
this.DELETE_LUA_SCRIPT = `local keysToDelete = redis.call('keys', ARGV[1])
for i,key in ipairs(keysToDelete)
do
return redis.call('del', key)
end`;
this.BEST_BID_LUA_SCRIPT = `
local bestBidSet = KEYS[1]..':BIDS';
local bestBidInfoMap = KEYS[1]..':BID:INFO:';
local bestBid = redis.call('ZREVRANGE', bestBidSet, 0, 0);
local bestBidLevel = redis.call('HGETALL', bestBidInfoMap..bestBid[1])
return bestBidLevel
`;
this.BEST_ASK_LUA_SCRIPT = `
local bestAskSet = KEYS[1]..':ASKS';
local bestAskInfoMap = KEYS[1]..':ASK:INFO:';
local bestAsk = redis.call('ZRANGE', bestAskSet, 0, 0);
local bestAskLevel = redis.call('HGETALL', bestAskInfoMap..bestAsk[1])
return bestAskLevel
`;
console.log('Using new redis book, no inmemory data');
this.product = config.product;
[this.baseCurrency, this.quoteCurrency] = this.product.split('/');
this.snapshotReceived = false;
this.redisclient = RedisConnector_1.getClient();
this.redisct = RedisConnector_1.getRedisct();
this.exchange = config.exchange;
this.symbol = `${this.exchange}:${this.product}`;
this.SET_KEY_BID = `{${this.exchange}:${this.product}}:BIDS`;
this.SET_KEY_ASK = `{${this.exchange}:${this.product}}:ASKS`;
this.KEY_BOOK_INFO = `{${this.exchange}:${this.product}}:BOOK:INFO`;
this.PARTIAL_KEY_BOOK_INFO_BID = `{${this.exchange}:${this.product}}:BID:INFO:`;
this.PARTIAL_KEY_BOOK_INFO_ASK = `{${this.exchange}:${this.product}}:ASK:INFO:`;
}
state() {
throw new Error("Method not implemented.");
}
fromState(state) {
var pipeline = this.redisclient.pipeline();
this.clear(pipeline);
pipeline.exec().then(() => { console.log('Cleared book for ', this.product); });
this.sequence = state.sequence;
var pipeline = this.redisclient.pipeline();
state.asks.forEach((priceLevel) => {
const level = _1.AggregatedLevelFromPriceLevel(priceLevel);
this.setLevel('sell', level, pipeline);
});
pipeline.exec().then(() => { console.log('Ask snapshot updated for ', this.product); });
var pipeline = this.redisclient.pipeline();
state.bids.forEach((priceLevel) => {
const level = _1.AggregatedLevelFromPriceLevel(priceLevel);
this.setLevel('buy', level, pipeline);
});
pipeline.exec().then(() => { console.log('Bid snapshot updated for ', this.product); });
}
clear(pipeline) {
if (this.redisclient) {
//SET containing the bids and asks
pipeline.del(this.SET_KEY_BID);
pipeline.del(this.SET_KEY_ASK);
// Trade History && Trade cum value
pipeline.del(`{${this.exchange}:${this.product}}:TRADES`);
pipeline.del(`{${this.exchange}:${this.product}}:T:T`);
pipeline.del(`{${this.exchange}:${this.product}}:T:B:S`);
pipeline.del(`{${this.exchange}:${this.product}}:T:B:C`);
pipeline.del(`{${this.exchange}:${this.product}}:T:B:V`);
pipeline.del(`{${this.exchange}:${this.product}}:T:S:S`);
pipeline.del(`{${this.exchange}:${this.product}}:T:S:V`);
pipeline.del(`{${this.exchange}:${this.product}}:T:S:C`);
//HASH containing the products totalbids, ask values
pipeline.del(this.KEY_BOOK_INFO);
pipeline.set(this.PARTIAL_KEY_BOOK_INFO_ASK + ':dummy', 'dummy');
pipeline.set(this.PARTIAL_KEY_BOOK_INFO_BID + ':dummy', 'dummy');
//HASH containing respective level bid and ask information size and value
pipeline.eval(this.DELETE_LUA_SCRIPT, 0, this.PARTIAL_KEY_BOOK_INFO_ASK + '*');
pipeline.eval(this.DELETE_LUA_SCRIPT, 0, this.PARTIAL_KEY_BOOK_INFO_BID + '*');
// level 3 orders not supported yet
// this.redisclient.del(`{${EXCHANGE}:${this.product}}:BIDS:*:ORDERS`)
// this.redisclient.del(`{${EXCHANGE}:${this.product}}:ASKS:*:ORDERS`)
}
}
_write(msg, encoding, callback) {
// Pass the msg on to downstream users
// this.push(msg);
// Process the message for the orderbook state
process.nextTick(() => {
if (!Messages_1.isStreamMessage(msg) || !msg.productId) {
return callback();
}
if (msg.productId !== this.product) {
return callback();
}
switch (msg.type) {
case 'snapshot':
this.processSnapshot(msg);
break;
case 'level':
this.processLevelChange(msg);
this.emit('LiveOrderbook.update', msg);
break;
default:
this.emit('LiveOrderbook.update', msg);
break;
}
callback();
});
}
processSnapshot(snapshot) {
this.fromState(snapshot);
this._sourceSequence = snapshot.sourceSequence;
this.snapshotReceived = true;
this.emit('LiveOrderbook.snapshot', snapshot);
}
/**
* Handles order messages from aggregated books
* @param msg
*/
processLevelChange(msg) {
if (!msg.sequence) {
return;
}
this._sourceSequence = msg.sourceSequence;
const sequenceStatus = this.checkSequence(msg.sequence);
if (sequenceStatus === core_1.SequenceStatus.ALREADY_PROCESSED) {
return;
}
const level = _1.AggregatedLevelFactory(msg.size, msg.price, msg.side);
var pipeline = this.redisclient.pipeline();
this.setLevel(msg.side, level, pipeline);
pipeline.exec();
}
checkSequence(sequence) {
if (sequence <= this.sequence) {
return core_1.SequenceStatus.ALREADY_PROCESSED;
}
if (sequence !== this.sequence + 1) {
// Dropped a message, restart the synchronising
console.log('info', `Dropped a message. Expected ${this.sequence + 1} but received ${sequence}.`);
const event = {
expected_sequence: this.sequence + 1,
sequence: sequence
};
const diff = event.expected_sequence - event.sequence;
const msg = `LiveOrderbook detected a skipped message. Expected ${event.expected_sequence}, but received ${event.sequence}. Diff = ${diff}`;
this.emit('LiveOrderbook.skippedMessage', event);
return core_1.SequenceStatus.SKIP_DETECTED;
}
this.lastBookUpdate = new Date();
this.sequence = sequence;
return core_1.SequenceStatus.OK;
}
emitError(message) {
const err = new Error('An inconsistent orderbook state occurred');
err.msg = message;
console.error(err.message, { message: message });
this.emit('error', err);
}
processTradeMessage(msg) {
// this._book.processTradeMessage(msg);
}
get bidsTotal() {
return this._bidsTotal;
}
get bidsValueTotal() {
return this._bidsValueTotal;
}
get asksTotal() {
return this._asksTotal;
}
get asksValueTotal() {
return this._asksValueTotal;
}
getNumAsks() {
throw new Error("Method not implemented.");
}
getNumBids() {
throw new Error("Method not implemented.");
}
getBidsTotal() {
throw new Error("Method not implemented.");
}
getAsksTotal() {
throw new Error("Method not implemented.");
}
getSequence() {
throw new Error("Method not implemented.");
}
getHighestBid() {
return __awaiter(this, void 0, void 0, function* () {
var level = yield this.redisclient.eval(this.BEST_BID_LUA_SCRIPT, 1, `{${this.symbol}}`);
level = _1.AggregatedLevelFactory(level[1], level[5], 'buy');
return level;
});
}
getLowestAsk() {
return __awaiter(this, void 0, void 0, function* () {
var level = yield this.redisclient.eval(this.BEST_ASK_LUA_SCRIPT, 1, `{${this.symbol}}`);
level = _1.AggregatedLevelFactory(level[1], level[5], 'sell');
return level;
});
}
getLevel(side, price) {
return __awaiter(this, void 0, void 0, function* () {
var level = null;
if (side === 'buy') {
level = yield this.redisclient.hgetall(this.PARTIAL_KEY_BOOK_INFO_BID + price);
level = _1.AggregatedLevelFactory(level[1], level[5], 'buy');
}
else {
level = yield this.redisclient.hgetall(this.PARTIAL_KEY_BOOK_INFO_BID + price);
level = _1.AggregatedLevelFactory(level[1], level[5], 'sell');
}
return level;
});
}
/**
* Add an order's information to the book
* @param order
*/
add(order) {
console.error('Level 3 orders not handled in redis');
return null;
}
// processTradeMessage(msg:TradeMessage) {
// let price = Big(msg.price);
// let size = Big(msg.size);
// let value = price.mul(size);
// let time = msg.time;
// let side = msg.side;
// let tradeId = msg.tradeId
// this.redisct.saveTradeMessage({ symbol : this.symbol, time, price, size, value, side, tradeId})
// }
updateRedis(side, level, pipeline) {
let infoKey = this.KEY_BOOK_INFO;
let totalValue = this._asksValueTotal.toString();
let totalSize = this._asksTotal.toString();
let totalValueKey = 'askTotalValue';
let totalSizeKey = 'askTotalSize';
if (side === 'buy') {
totalSize = this._bidsTotal.toString();
totalValue = this._bidsValueTotal.toString();
totalValueKey = 'bidTotaValue';
totalSizeKey = 'bidTotalSize';
}
pipeline.hmset(infoKey, totalSizeKey, totalSize, totalValueKey, totalValue, 'sequence', this.sequence);
}
// Add a complete price level with orders to the order book. If the price level already exists, throw an exception
addLevel(side, level, pipeline) {
this.addToTotal(level.totalSize, side, level.price);
// Add links to orders
let price = level.price.toString();
let bookKey = this.SET_KEY_ASK;
let specificKey = this.PARTIAL_KEY_BOOK_INFO_ASK + price;
;
if (side === 'buy') {
bookKey = this.SET_KEY_BID;
specificKey = this.PARTIAL_KEY_BOOK_INFO_BID + price;
;
}
pipeline.zadd(bookKey, price, price);
pipeline.hmset(specificKey, 'totalSize', level.totalSize, 'totalValue', level.totalValue, 'price', price);
this.updateRedis(side, level, pipeline);
}
/**
* Remove a complete level and links to orders in the order pool. If the price level doesn't exist, it returns
* false
*/
removeLevel(side, priceLevel, pipeline) {
const level = priceLevel;
this.subtractFromTotal(level.totalSize, side, level.price);
let bookKey = this.SET_KEY_ASK;
let price = level.price.toString();
let specificKey = this.PARTIAL_KEY_BOOK_INFO_ASK + price;
;
if (side === 'buy') {
bookKey = this.SET_KEY_BID;
specificKey = this.PARTIAL_KEY_BOOK_INFO_BID + price;
;
}
pipeline.zrem(bookKey, level.price.toString());
pipeline.del(specificKey);
this.updateRedis(side, level, pipeline);
return true;
}
/**
* Shortcut method for replacing a level. First removeLevel is called, and then addLevel
*/
setLevel(side, level, pipeline) {
this.removeLevel(side, level, pipeline);
if (level.numOrders > 0) {
this.addLevel(side, level, pipeline);
}
return true;
}
/**
* Remove the order from the orderbook If numOrders drops to zero, remove the level
*/
remove(orderId) {
console.error('Level 3 orders not handled in redis');
return null;
}
subtractFromTotal(amount, side, price) {
if (side === 'buy') {
this._bidsTotal = this._bidsTotal.minus(amount);
this._bidsValueTotal = this._bidsValueTotal.minus(amount.times(price));
}
else {
this._asksTotal = this._asksTotal.minus(amount);
this._asksValueTotal = this._asksValueTotal.minus(amount.times(price));
}
}
addToTotal(amount, side, price) {
if (side === 'buy') {
this._bidsTotal = this._bidsTotal.plus(amount);
this._bidsValueTotal = this._bidsValueTotal.plus(amount.times(price));
}
else {
this._asksTotal = this._asksTotal.plus(amount);
this._asksValueTotal = this._asksValueTotal.plus(amount.times(price));
}
}
}
exports.RedisBook = RedisBook;