tlab-trading-toolkit
Version:
A trading toolkit for building advanced trading bots on the GDAX platform
281 lines (280 loc) • 11 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/***************************************************************************************************************************
* @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 types_1 = require("../lib/types");
const BookBuilder_1 = require("../lib/BookBuilder");
const Messages_1 = require("./Messages");
const stream_1 = require("stream");
class LiveBookConfig {
}
exports.LiveBookConfig = LiveBookConfig;
var SequenceStatus;
(function (SequenceStatus) {
SequenceStatus[SequenceStatus["OK"] = 0] = "OK";
SequenceStatus[SequenceStatus["ALREADY_PROCESSED"] = 1] = "ALREADY_PROCESSED";
SequenceStatus[SequenceStatus["SKIP_DETECTED"] = 2] = "SKIP_DETECTED";
})(SequenceStatus = exports.SequenceStatus || (exports.SequenceStatus = {}));
/**
* A live orderbook. This class maintains the state of an orderbook (using BookBuilder) in realtime by responding to
* messages from attached feeds.
*/
class LiveOrderbook extends stream_1.Writable {
constructor(config) {
super({ objectMode: true, highWaterMark: 1024 });
this.product = config.product;
[this.baseCurrency, this.quoteCurrency] = this.product.split('/');
this.logger = config.logger;
this._book = new BookBuilder_1.BookBuilder(this.logger);
this.liveTicker = {
productId: config.product,
price: undefined,
bid: undefined,
ask: undefined,
volume: types_1.ZERO,
time: undefined,
trade_id: undefined,
size: undefined
};
this.strictMode = !!config.strictMode;
this.snapshotReceived = false;
}
log(level, message, meta) {
if (!this.logger) {
return;
}
this.logger.log(level, message, meta);
}
get sourceSequence() {
return this._sourceSequence;
}
get numAsks() {
return this._book.numAsks;
}
get numBids() {
return this._book.numBids;
}
get bidsTotal() {
return this._book.bidsTotal;
}
get asksTotal() {
return this._book.asksTotal;
}
state() {
return this._book.state();
}
get book() {
return this._book;
}
get ticker() {
return this.liveTicker;
}
get sequence() {
return this._book.sequence;
}
/**
*/
get timeSinceTickerUpdate() {
const time = this.ticker.time ? this.ticker.time.valueOf() : 0;
return (Date.now() - time);
}
/**
* The time (in seconds) since the last orderbook update
*/
get timeSinceOrderbookUpdate() {
const time = this.lastBookUpdate ? this.lastBookUpdate.valueOf() : 0;
return (Date.now() - time);
}
/**
* Return an array of (aggregated) orders whose sum is equal to or greater than `value`. The side parameter is from
* the perspective of the purchaser, so 'buy' returns asks and 'sell' bids.
*/
ordersForValue(side, value, useQuote, startPrice) {
return this._book.ordersForValue(side, types_1.Big(value), useQuote, startPrice);
}
_read() { }
_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 'ticker':
this.updateTicker(msg);
// ticker is emitted in pvs method
break;
case 'snapshot':
this.processSnapshot(msg);
break;
case 'level':
this.processLevelChange(msg);
this.emit('LiveOrderbook.update', msg);
break;
case 'trade':
// Trade messages don't affect the orderbook
this.processTradeMessage(msg);
this.emit('LiveOrderbook.trade', msg);
break;
default:
console.log('Processing level3 message');
this.processLevel3Messages(msg);
this.emit('LiveOrderbook.update', msg);
break;
}
callback();
});
}
processTradeMessage(msg) {
return;
}
/**
* Checks the given sequence number against the expected number for a message and returns a status result
*/
checkSequence(sequence) {
if (sequence <= this.sequence) {
return SequenceStatus.ALREADY_PROCESSED;
}
if (sequence !== this.sequence + 1) {
// Dropped a message, restart the synchronising
this.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}`;
if (this.strictMode) {
throw new Error(msg);
}
this.emit('LiveOrderbook.skippedMessage', event);
return SequenceStatus.SKIP_DETECTED;
}
this.lastBookUpdate = new Date();
this._book.sequence = sequence;
return SequenceStatus.OK;
}
updateTicker(tickerMessage) {
const ticker = this.liveTicker;
ticker.price = tickerMessage.price;
ticker.bid = tickerMessage.bid;
ticker.ask = tickerMessage.ask;
ticker.volume = tickerMessage.volume;
ticker.time = tickerMessage.time;
ticker.trade_id = tickerMessage.trade_id;
ticker.size = tickerMessage.size;
this.emit('LiveOrderbook.ticker', ticker);
}
processSnapshot(snapshot) {
this._book.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 === SequenceStatus.ALREADY_PROCESSED) {
return;
}
const level = BookBuilder_1.AggregatedLevelFactory(msg.size, msg.price, msg.side);
this._book.setLevel(msg.side, level);
}
/**
* Processes order messages from order-level books.
*/
processLevel3Messages(message) {
// Can't do anything until we get a snapshot
if (!this.snapshotReceived || !message.sequence) {
return;
}
const sequenceStatus = this.checkSequence(message.sequence);
if (sequenceStatus === SequenceStatus.ALREADY_PROCESSED) {
return;
}
this._sourceSequence = message.sourceSequence;
switch (message.type) {
case 'newOrder':
this.processNewOrderMessage(message);
break;
case 'orderDone':
this.processDoneMessage(message);
break;
case 'changedOrder':
this.processChangedOrderMessage(message);
break;
default:
return;
}
}
processNewOrderMessage(msg) {
const order = {
id: msg.orderId,
size: types_1.Big(msg.size),
price: types_1.Big(msg.price),
side: msg.side
};
if (!(this._book.add(order))) {
this.emitError(msg);
}
}
processDoneMessage(msg) {
// If we're using an order pool, then we only remove orders that we're aware of. GDAX, for example might
// send a done message for a stop order that is cancelled (and was not previously known to us).
// Also filled orders will already have been removed by the time a GDAX done order reaches here
const book = this._book;
if (!book.hasOrder(msg.orderId)) {
return;
}
if (!(this._book.remove(msg.orderId))) {
this.emitError(msg);
}
}
processChangedOrderMessage(msg) {
if (!msg.newSize && !msg.changedAmount) {
return;
}
let newSize;
const newSide = msg.side;
if (msg.changedAmount) {
const order = this.book.getOrder(msg.orderId);
newSize = order.size.plus(msg.changedAmount);
}
else {
newSize = types_1.Big(msg.newSize);
}
if (!(this._book.modify(msg.orderId, newSize, newSide))) {
this.emitError(msg);
}
}
emitError(message) {
const err = new Error('An inconsistent orderbook state occurred');
err.msg = message;
this.log('error', err.message, { message: message });
this.emit('error', err);
}
}
exports.LiveOrderbook = LiveOrderbook;