UNPKG

tlab-trading-toolkit

Version:

A trading toolkit for building advanced trading bots on the GDAX platform

363 lines (362 loc) 15.1 kB
"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;