tlab-trading-toolkit
Version:
A trading toolkit for building advanced trading bots on the GDAX platform
229 lines (228 loc) • 9.94 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 stream_1 = require("stream");
const BookBuilder_1 = require("../lib/BookBuilder");
const Messages_1 = require("./Messages");
const OrderbookDiff_1 = require("../lib/OrderbookDiff");
const types_1 = require("../lib/types");
/**
* The Trader class places orders on your behalf. The commands for placing the trades can either come from an attached
* stream, or directly via the API.
*
* One should have an *authenticated* feed piped into Trader so that it can keep track of the state of its own orderbook.
* Failing this, it is trading 'blind' and will have to rely on REST requests to update the state of the book.
*
* Emitted messages:
* Trader.outOfSyncWarning - The internal order pool and what's actually on the exchange may be out of sync
* Trader.trade-finalized - An order is complete (done)
* Trader.my-orders-cancelled - A call to cancel all orders in this orderbook has completed
* Trader.all-orders-cancelled - A call to cancel ALL of the user's orders (including those placed elsewhere) has been completed
* Trader.order-placed - Emitted after an order has been successfully placed
* Trader.order-cancelled - Emitted after an order has been cancelled
* Trader.trade-executed - emitted after a trade has been executed against my limit order
* Trader.place-order-failed - A REST order request returned with an error
* Trader.cancel-order-failed - A Cancel request returned with an error status
*/
class Trader extends stream_1.Writable {
constructor(config) {
super({ objectMode: true });
this._fitOrders = true;
this.api = config.exchangeAPI;
this.logger = config.logger;
this.myBook = new BookBuilder_1.BookBuilder(this.logger);
this._productId = config.productId;
this.sizePrecision = config.sizePrecision || 2;
this.pricePrecision = config.pricePrecision || 2;
if (!this.api) {
throw new Error('Trader cannot work without an exchange interface using valid credentials. Have you set the necessary ENVARS?');
}
}
get productId() {
return this._productId;
}
get fitOrders() {
return this._fitOrders;
}
set fitOrders(value) {
this._fitOrders = value;
}
placeOrder(req) {
if (this.fitOrders) {
req.size = types_1.Big(req.size).round(this.sizePrecision, 1).toString();
req.price = types_1.Big(req.price).round(this.pricePrecision, 2).toString();
}
return this.api.placeOrder(req).then((order) => {
this.myBook.add(order);
return order;
}).catch((err) => {
// Errors can fail if they're too precise, too small, or the API is down
// We pass the message along, but let the user decide what to do
this.emit('Trader.place-order-failed', err.message);
return Promise.resolve(null);
});
}
cancelOrder(orderId) {
return this.api.cancelOrder(orderId).then((id) => {
// To avoid race conditions, we only actually remove the order when the tradeFinalized message arrives
return id;
});
}
cancelMyOrders() {
if (!this.myBook.orderPool) {
return Promise.resolve([]);
}
const orderIds = Object.keys(this.myBook.orderPool);
const promises = orderIds.map((id) => {
return this.cancelOrder(id);
});
return Promise.all(promises).then((ids) => {
this.emit('Trader.my-orders-cancelled', ids);
return ids;
});
}
/**
* Cancel all, and we mean ALL orders (even those not placed by this Trader). To cancel only the messages
* listed in the in-memory orderbook, use `cancelMyOrders`
*/
cancelAllOrders() {
return this.api.cancelAllOrders(null).then((ids) => {
this.myBook.clear();
this.emit('Trader.all-orders-cancelled', ids);
return ids;
}, (err) => {
this.emit('error', err);
return [];
});
}
state() {
return this.myBook.state();
}
/**
* Compare the state of the in-memory orderbook with the one returned from a REST query of all my orders. The
* result is an `OrderbookState` object that represents the diff between the two states. Negative sizes represent
* orders in-memory that don't exist on the book and positive ones are vice versa
*/
checkState() {
return this.api.loadAllOrders(this.productId).then((actualOrders) => {
const book = new BookBuilder_1.BookBuilder(this.logger);
actualOrders.forEach((order) => {
book.add(order);
});
const diff = OrderbookDiff_1.OrderbookDiff.compareByOrder(this.myBook, book);
return Promise.resolve(diff);
});
}
executeMessage(msg) {
if (!Messages_1.isStreamMessage(msg)) {
return;
}
switch (msg.type) {
case 'placeOrder':
this.handleOrderRequest(msg);
break;
case 'cancelOrder':
this.handleCancelOrder(msg);
break;
case 'cancelAllOrders':
this.cancelAllOrders();
break;
case 'cancelMyOrders':
this.cancelMyOrders();
break;
case 'tradeExecuted':
this.handleTradeExecutedMessage(msg);
break;
case 'tradeFinalized':
this.handleTradeFinalized(msg);
break;
case 'myOrderPlaced':
this.handleOrderPlacedConfirmation(msg);
break;
}
}
_write(msg, encoding, callback) {
this.executeMessage(msg);
callback();
}
handleOrderRequest(request) {
if (request.productId !== this._productId) {
return;
}
this.placeOrder(request).then((result) => {
if (result) {
this.emit('Trader.order-placed', result);
}
});
}
handleCancelOrder(request) {
this.cancelOrder(request.orderId).then((result) => {
return this.emit('Trader.order-cancelled', result);
}, (err) => {
this.emit('Trader.cancel-order-failed', err);
});
}
handleTradeExecutedMessage(msg) {
this.emit('Trader.trade-executed', msg);
if (msg.orderType !== 'limit') {
return;
}
const order = this.myBook.getOrder(msg.orderId);
if (!order) {
this.logger.log('warn', 'Traded order not in my book', msg);
this.emit('Trader.outOfSyncWarning', 'Traded order not in my book');
return;
}
let newSize;
if (msg.tradeSize) {
newSize = order.size.minus(msg.tradeSize);
}
else {
newSize = types_1.Big(msg.remainingSize);
}
this.myBook.modify(order.id, newSize);
}
handleTradeFinalized(msg) {
const id = msg.orderId;
const order = this.myBook.remove(id);
if (!order) {
this.logger.log('warn', 'Trader: Cancelled order not in my book', id);
this.emit('Trader.outOfSyncWarning', 'Cancelled order not in my book');
return;
}
this.emit('Trader.trade-finalized', msg);
}
/**
* We should just confirm that we have the order, since we added when we placed it.
* Otherwise this Trader didn't place the order (or somehow missed the callback), but we should add
* it to our memory book anyway otherwise it will go out of sync
*/
handleOrderPlacedConfirmation(msg) {
const orderId = msg.orderId;
if (this.myBook.getOrder(orderId)) {
this.logger.log('debug', 'Order confirmed', msg);
return;
}
const order = {
id: orderId,
price: types_1.Big(msg.price),
side: msg.side,
size: types_1.Big(msg.size)
};
this.myBook.add(order);
this.emit('Trader.external-order-placement', msg);
}
}
exports.Trader = Trader;