tlab-trading-toolkit
Version:
A trading toolkit for building advanced trading bots on the GDAX platform
435 lines (434 loc) • 16.1 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 Orderbook_1 = require("./Orderbook");
const types_1 = require("./types");
const events_1 = require("events");
const Logger_1 = require("../utils/Logger");
const assert = require("assert");
function AggregatedLevelFactory(totalSize, price, side) {
const level = new AggregatedLevelWithOrders(types_1.Big(price));
const size = types_1.Big(totalSize);
if (!size.eq(types_1.ZERO)) {
const order = {
id: price.toString(),
price: types_1.Big(price),
size: types_1.Big(totalSize),
side: side
};
level.addOrder(order);
}
return level;
}
exports.AggregatedLevelFactory = AggregatedLevelFactory;
function AggregatedLevelFromPriceLevel(priceLevel) {
priceLevel.price = types_1.Big(priceLevel.price);
priceLevel.totalSize = types_1.Big(priceLevel.totalSize);
const level = new AggregatedLevelWithOrders(priceLevel.price);
level.totalSize = priceLevel.totalSize;
level.totalValue = priceLevel.price.times(priceLevel.totalSize);
level._orders = priceLevel.orders;
return level;
}
exports.AggregatedLevelFromPriceLevel = AggregatedLevelFromPriceLevel;
class AggregatedLevel {
constructor(price) {
this._numOrders = 0;
this.totalSize = types_1.ZERO;
this.totalValue = types_1.ZERO;
this.price = price;
}
get numOrders() {
return this._numOrders;
}
isEmpty() {
return this._numOrders === 0;
}
equivalent(level) {
return this.price.eq(level.price) && this.totalSize.eq(level.totalSize);
}
add(amount) {
this.totalSize = this.totalSize.plus(amount);
this.totalValue = this.totalValue.plus(amount.times(this.price));
}
subtract(amount) {
this.totalSize = this.totalSize.minus(amount);
this.totalValue = this.totalValue.minus(amount.times(this.price));
}
}
exports.AggregatedLevel = AggregatedLevel;
class AggregatedLevelWithOrders extends AggregatedLevel {
constructor(price) {
super(price);
this._orders = [];
}
get orders() {
return this._orders;
}
get numOrders() {
return this._orders.length;
}
findOrder(id) {
return this._orders.find((order) => order.id === id);
}
addOrder(order) {
if (!this.price.eq(order.price)) {
throw new Error(`Tried to add order with price ${order.price.toString()} to level with price ${this.price.toString()}`);
}
if (this.findOrder(order.id)) {
return false;
}
this.add(order.size);
this._orders.push(order);
return true;
}
removeOrder(id) {
const order = this.findOrder(id);
if (!order) {
return false;
}
this.subtract(order.size);
const i = this._orders.indexOf(order);
this._orders.splice(i, 1);
return true;
}
}
exports.AggregatedLevelWithOrders = AggregatedLevelWithOrders;
/**
* 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 BookBuilder extends events_1.EventEmitter {
constructor(logger) {
super();
this.sequence = -1;
this._bidsTotal = types_1.ZERO;
this._bidsValueTotal = types_1.ZERO;
this._asksTotal = types_1.ZERO;
this._asksValueTotal = types_1.ZERO;
this._orderPool = {};
this.logger = logger || Logger_1.ConsoleLoggerFactory();
this.clear();
}
clear() {
this.bids = Orderbook_1.PriceTreeFactory();
this.asks = Orderbook_1.PriceTreeFactory();
this._bidsTotal = types_1.ZERO;
this._asksTotal = types_1.ZERO;
this._bidsValueTotal = types_1.ZERO;
this._asksValueTotal = types_1.ZERO;
this._orderPool = {};
this.sequence = -1;
}
get bidsTotal() {
return this._bidsTotal;
}
get bidsValueTotal() {
return this._bidsValueTotal;
}
get asksTotal() {
return this._asksTotal;
}
get asksValueTotal() {
return this._asksValueTotal;
}
get numAsks() {
return this.asks.size;
}
get numBids() {
return this.bids.size;
}
get orderPool() {
return this._orderPool;
}
set orderPool(value) {
this._orderPool = value;
}
get highestBid() {
return this.bids.max();
}
get lowestAsk() {
return this.asks.min();
}
getOrder(id) {
return this._orderPool[id];
}
hasOrder(orderId) {
return this._orderPool.hasOwnProperty(orderId);
}
getLevel(side, price) {
const tree = this.getTree(side);
return tree.find({ price: price });
}
/**
* Add an order's information to the book
* @param order
*/
add(order) {
const side = order.side;
const tree = this.getTree(side);
let level = new AggregatedLevelWithOrders(order.price);
const existing = tree.find(level);
if (existing) {
level = existing;
}
else {
if (!tree.insert(level)) {
return false;
}
}
// Add order to the aggregated level
if (!level.addOrder(order)) {
return false;
}
// Update global order pool stats
this._orderPool[order.id] = order;
this.addToTotal(order.size, order.side, order.price);
return true;
}
/**
* Changes the size of an existing order to newSize. If the order doesn't exist, returns false.
* If the newSize is zero, the order is removed.
* If newSize is negative, an error is thrown.
* It is possible for an order to switch sides, in which case the newSide parameter determines the new side.
*/
modify(id, newSize, newSide) {
if (newSize.lt(types_1.ZERO)) {
throw new Error('Cannot set an order size to a negative number');
}
const order = this.getOrder(id);
if (!order) {
return false;
}
if (!this.remove(id)) {
return false;
}
if (newSize.gt(types_1.ZERO)) {
order.size = newSize;
order.side = newSide || order.side;
this.add(order);
}
return true;
}
// Add a complete price level with orders to the order book. If the price level already exists, throw an exception
addLevel(side, level) {
const tree = this.getTree(side);
if (tree.find(level)) {
throw new Error(`cannot add a new level to orderbook since the level already exists at price ${level.price.toString()}`);
}
tree.insert(level);
this.addToTotal(level.totalSize, side, level.price);
// Add links to orders
level.orders.forEach((order) => {
this._orderPool[order.id] = order;
});
}
/**
* 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) {
const tree = this.getTree(side);
const level = tree.find(priceLevel);
if (!level) {
return false;
}
assert(tree.remove(level));
level.orders.forEach((order) => {
delete this.orderPool[order.id];
});
this.subtractFromTotal(level.totalSize, side, level.price);
return true;
}
/**
* Shortcut method for replacing a level. First removeLevel is called, and then addLevel
*/
setLevel(side, level) {
this.removeLevel(side, level);
if (level.numOrders > 0) {
this.addLevel(side, level);
}
return true;
}
/**
* Remove the order from the orderbook If numOrders drops to zero, remove the level
*/
remove(orderId) {
const order = this.getOrder(orderId);
if (!order) {
return null;
}
const side = order.side;
const tree = this.getTree(side);
let level = new AggregatedLevelWithOrders(order.price);
level = tree.find(level);
if (!level) {
// If a market order has filled, we can carry on
if (order.size.eq(types_1.ZERO)) {
return order;
}
this.logger.log('error', `There should have been orders at price level ${order.price} for at least ${order.size}, but there were none`);
return null;
}
if (this.removeFromPool(order.id)) {
this.subtractFromTotal(order.size, order.side, order.price);
}
level.removeOrder(order.id);
if (level.numOrders === 0) {
if (!(level.totalSize.eq(types_1.ZERO))) {
this.logger.log('error', `Total size should be zero at level $${level.price} but was ${level.totalSize}.`);
return null;
}
tree.remove(level);
}
return order;
}
getTree(side) {
return side === 'buy' ? this.bids : this.asks;
}
/**
* Returns a book object that has all the bids and asks at this current moment. For performance reasons, this method
* returns a shallow copy of the underlying orders, so modifying the state object may break the orderbook generally.
* For deep copies, call #stateCopy instead
*/
state() {
const book = {
sequence: this.sequence,
asks: [],
bids: [],
orderPool: this.orderPool
};
this.bids.reach((bid) => {
book.bids.push(bid);
});
this.asks.each((ask) => {
book.asks.push(ask);
});
return book;
}
/**
* Returns a deep copy of the orderbook state.
*/
stateCopy() {
const shallowBook = this.state();
return Object.assign({}, shallowBook);
}
fromState(state) {
this.clear();
this.sequence = state.sequence;
// The order pool gets set up in setLevel
state.asks.forEach((priceLevel) => {
const level = AggregatedLevelFromPriceLevel(priceLevel);
this.setLevel('sell', level);
});
state.bids.forEach((priceLevel) => {
const level = AggregatedLevelFromPriceLevel(priceLevel);
this.setLevel('buy', level);
});
}
/**
* 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.
* If useQuote is true, value is assumed to represent price * size, otherwise just size is used
* startPrice sets the first price to start counting from (inclusive). The default is undefined, which starts at the best bid/by
*/
ordersForValue(side, value, useQuote, start) {
const source = side === 'buy' ? this.asks : this.bids;
const iter = source.iterator();
let totalSize = types_1.ZERO;
let totalValue = types_1.ZERO;
const orders = [];
let level;
// Find start order with price >= startPrice (for buys)
if (start) {
if (side === 'buy') {
do {
level = iter.next();
} while (level && level.price.lt(start.price));
}
else {
do {
level = iter.prev();
} while (level && level.price.gt(start.price));
}
level = Object.assign({}, level);
level.totalSize = level.totalSize.minus(start.size);
}
else {
level = side === 'buy' ? iter.next() : iter.prev();
}
while (level !== null && ((useQuote && totalValue.lt(value)) || (!useQuote && totalSize.lt(value)))) {
let levelValue = level.price.times(level.totalSize);
let levelSize = level.totalSize;
if (useQuote && levelValue.plus(totalValue).gte(value)) {
levelValue = value.minus(totalValue);
levelSize = levelValue.div(level.price);
}
else if (!useQuote && levelSize.plus(totalSize).gte(value)) {
levelSize = value.minus(totalSize);
levelValue = levelSize.times(level.price);
}
else {
levelSize = level.totalSize;
levelValue = levelSize.times(level.price);
}
totalSize = totalSize.plus(levelSize);
totalValue = totalValue.plus(levelValue);
orders.push({
totalSize: levelSize,
value: levelValue,
price: level.price,
cumSize: totalSize,
cumValue: totalValue,
orders: level.orders
});
level = side === 'buy' ? iter.next() : iter.prev();
}
return orders;
}
removeFromPool(orderId) {
const exists = this.hasOrder(orderId);
if (exists) {
delete this._orderPool[orderId];
}
return exists;
}
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.BookBuilder = BookBuilder;