tlab-trading-toolkit
Version:
A trading toolkit for building advanced trading bots on the GDAX platform
352 lines (351 loc) • 14.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const ProductMap_1 = require("../ProductMap");
const BookBuilder_1 = require("../../lib/BookBuilder");
const types_1 = require("../../lib/types");
const Logger_1 = require("../../utils/Logger");
const request = require("superagent");
const querystring = require("querystring");
const Buffer = require("buffer");
const crypto = require("crypto");
exports.GDAX_API_URL = 'https://api.gdax.com';
class GDAXExchangeAPI {
constructor(options) {
this.owner = 'GDAX';
this._apiURL = options.apiUrl || exports.GDAX_API_URL;
this.auth = options.auth;
this.logger = options.logger || Logger_1.ConsoleLoggerFactory();
}
get apiURL() {
return this._apiURL;
}
static product(genericProduct) {
return ProductMap_1.ProductMap.ExchangeMap.get('GDAX').getExchangeProduct(genericProduct) || genericProduct;
}
static genericProduct(exchangeProduct) {
return ProductMap_1.ProductMap.ExchangeMap.get('GDAX').getGenericProduct(exchangeProduct) || exchangeProduct;
}
static getMarket(genericProduct) {
return ProductMap_1.ProductMap.ExchangeMap.get('GDAX').getMarket(genericProduct);
}
static getMarketForExchangeProduct(exchangeProduct) {
return ProductMap_1.ProductMap.ExchangeMap.get('GDAX').getMarket(GDAXExchangeAPI.genericProduct(exchangeProduct));
}
loadProducts() {
const url = `${this.apiURL}/products`;
return request.get(url)
.accept('application/json')
.then((res) => {
if (res.status !== 200) {
throw new Error('loadProducts did not get the expected response from the server. ' + res.body);
}
const products = res.body;
return products.map((prod) => {
let ccxtMarket = GDAXExchangeAPI.getMarketForExchangeProduct(prod.id);
return {
id: GDAXExchangeAPI.genericProduct(prod.id) || prod.id,
baseCurrency: ccxtMarket.base || prod.base_currency,
quoteCurrency: ccxtMarket.quote || prod.quote_currency,
baseMinSize: types_1.Big(prod.base_min_size),
baseMaxSize: types_1.Big(prod.base_max_size),
quoteIncrement: types_1.Big(prod.quote_increment)
};
});
});
}
loadMidMarketPrice(genericProduct) {
return this.loadTicker(genericProduct).then((ticker) => {
if (!ticker || !ticker.bid || !ticker.ask) {
throw new Error('Loading midmarket price failed because ticker data was incomplete or unavailable');
}
return ticker.ask.plus(ticker.bid).times(0.5);
});
}
loadOrderbook(genericProduct) {
return this.loadFullOrderbook(genericProduct);
}
loadFullOrderbook(genericProduct) {
let exchangeProduct = GDAXExchangeAPI.product(genericProduct);
return this.loadGDAXOrderbook({ product: exchangeProduct, level: 3 }).then((body) => {
return this.buildBook(body);
});
}
loadGDAXOrderbook(options) {
let exchangeProduct = options.product;
const url = `${this.apiURL}/products/${exchangeProduct}/book`;
return request.get(url)
.accept('application/json')
.query({ level: options.level })
.then((res) => {
if (res.status !== 200) {
throw new Error('loadOrderbook did not get the expected response from the server. ' + res.body);
}
const orders = res.body;
if (!(orders.bids && orders.asks)) {
throw new Error('loadOrderbook did not return an bids or asks: ' + res.body);
}
return res.body;
}, (err) => {
this.logger.log('error', `Error loading snapshot for ${exchangeProduct}`, err);
return Promise.resolve(null);
});
}
loadTicker(genericProduct) {
let exchangeProduct = GDAXExchangeAPI.product(genericProduct);
const url = `${this.apiURL}/products/${exchangeProduct}/ticker`;
return request.get(url)
.accept('application/json')
.then((res) => {
if (res.status !== 200) {
throw new Error('loadTicker did not get the expected response from the server. ' + res.body);
}
const ticker = res.body;
return {
productId: exchangeProduct,
ask: ticker.ask ? types_1.Big(ticker.ask) : undefined,
bid: ticker.bid ? types_1.Big(ticker.bid) : undefined,
price: types_1.Big(ticker.price || 0),
size: types_1.Big(ticker.size || 0),
volume: types_1.Big(ticker.volume || 0),
time: new Date(ticker.time || new Date()),
trade_id: ticker.trade_id ? ticker.trade_id.toString() : '0'
};
});
}
aggregateBook(body) {
const book = new BookBuilder_1.BookBuilder(this.logger);
book.sequence = parseInt(body.sequence, 10);
['bids', 'asks'].forEach((side) => {
let currentPrice;
let order;
const bookSide = side === 'bids' ? 'buy' : 'sell';
body[side].forEach((bid) => {
if (bid[0] !== currentPrice) {
// Set the price on the old level
if (order) {
book.add(order);
}
currentPrice = bid[0];
order = {
id: currentPrice,
price: types_1.Big(currentPrice),
side: bookSide,
size: types_1.ZERO
};
}
order.size = order.size.plus(bid[1]);
});
if (order) {
book.add(order);
}
});
return book;
}
// ----------------------------------- Authenticated API methods --------------------------------------------------//
placeOrder(order) {
let exchangeProduct = GDAXExchangeAPI.product(order.productId);
const gdaxOrder = {
product_id: exchangeProduct,
size: order.size,
price: order.price,
side: order.side,
type: order.orderType,
client_oid: order.clientId,
post_only: order.postOnly,
time_in_force: order.extra && order.extra.time_in_force,
cancel_after: order.extra && order.extra.cancel_after,
funds: order.funds
};
const apiCall = this.authCall('POST', '/orders', { body: gdaxOrder });
return this.handleResponse(apiCall, { order: order })
.then((result) => {
return GDAXOrderToOrder(result);
}, (err) => {
this.logger.log('error', 'Placing order failed', { order: order, reason: err.message });
return Promise.reject(err);
});
}
cancelOrder(id) {
const apiCall = this.authCall('DELETE', `/orders/${id}`, {});
return this.handleResponse(apiCall, { order_id: id }).then((ids) => {
return Promise.resolve(ids[0]);
});
}
cancelAllOrders(genericProduct) {
const apiCall = this.authCall('DELETE', `/orders`, {});
let exchangeProduct = GDAXExchangeAPI.product(genericProduct);
const options = exchangeProduct ? { product_id: exchangeProduct } : null;
return this.handleResponse(apiCall, options).then((ids) => {
return Promise.resolve(ids);
});
}
loadOrder(id) {
const apiCall = this.authCall('GET', `/orders/${id}`, {});
return this.handleResponse(apiCall, { order_id: id }).then((order) => {
return GDAXOrderToOrder(order);
});
}
loadAllOrders(genericProduct) {
let exchangeProduct = GDAXExchangeAPI.product(genericProduct);
const self = this;
let allOrders = [];
const loop = (after) => {
return self.loadNextOrders(exchangeProduct, after).then((result) => {
const liveOrders = result.orders.map(GDAXOrderToOrder);
allOrders = allOrders.concat(liveOrders);
if (result.after) {
return loop(result.after);
}
else {
return allOrders;
}
});
};
return new Promise((resolve, reject) => {
return loop(null).then((orders) => {
return resolve(orders);
}, reject);
});
}
loadBalances() {
const apiCall = this.authCall('GET', '/accounts', {});
return this.handleResponse(apiCall, {}).then((accounts) => {
const balances = {};
accounts.forEach((account) => {
if (!balances[account.profile_id]) {
balances[account.profile_id] = {};
}
balances[account.profile_id][account.currency] = {
balance: types_1.Big(account.balance),
available: types_1.Big(account.available)
};
});
return balances;
});
}
authCall(method, path, opts) {
return this.checkAuth().then(() => {
method = method.toUpperCase();
const url = `${this.apiURL}${path}`;
let body = '';
let req = request(method, url)
.accept('application/json')
.set('content-type', 'application/json');
if (opts.body) {
body = JSON.stringify(opts.body);
req.send(body);
}
else if (opts.qs && Object.keys(opts.qs).length !== 0) {
req.query(opts.qs);
body = '?' + querystring.stringify(opts.qs);
}
const signature = this.getSignature(method, path, body);
req.set(signature);
if (opts.headers) {
req = req.set(opts.headers);
}
return Promise.resolve(req);
});
}
getSignature(method, relativeURI, body) {
body = body || '';
const timestamp = (Date.now() / 1000).toFixed(3);
const what = timestamp + method + relativeURI + body;
const key = new Buffer.Buffer(this.auth.secret, 'base64');
const hmac = crypto.createHmac('sha256', key);
const signature = hmac.update(what).digest('base64');
return {
'CB-ACCESS-KEY': this.auth.key,
'CB-ACCESS-SIGN': signature,
'CB-ACCESS-TIMESTAMP': timestamp,
'CB-ACCESS-PASSPHRASE': this.auth.passphrase
};
}
handleResponse(req, meta) {
// then<T> is required to workaround bug in TS2.1 https://github.com/Microsoft/TypeScript/issues/10977
return req.then((res) => {
if (res.status >= 200 && res.status < 300) {
return Promise.resolve(res.body);
}
const err = new Error(res.body.message);
err.details = res.body;
return Promise.reject(err);
}).catch((err) => {
const reason = err.message;
const error = Object.assign(new Error('A GDAX API request failed. ' + reason), meta);
error.reason = reason;
return Promise.reject(error);
});
}
checkAuth() {
return new Promise((resolve, reject) => {
if (this.auth === null) {
return reject(new Error('You cannot make authenticated requests if a GDAXAuthConfig object was not provided to the GDAXExchangeAPI constructor'));
}
if (!(this.auth.key && this.auth.secret && this.auth.passphrase)) {
return reject(new Error('You cannot make authenticated requests without providing all API credentials'));
}
return resolve();
});
}
buildBook(body) {
const book = new BookBuilder_1.BookBuilder(this.logger);
book.sequence = parseInt(body.sequence, 10);
['bids', 'asks'].forEach((side) => {
const bookSide = side === 'bids' ? 'buy' : 'sell';
body[side].forEach((data) => {
const order = {
id: data[2],
price: types_1.Big(data[0]),
side: bookSide,
size: types_1.Big(data[1])
};
book.add(order);
});
});
return book;
}
loadNextOrders(genericProduct, after) {
let exchangeProduct = GDAXExchangeAPI.product(genericProduct);
const qs = {
status: ['open', 'pending', 'active']
};
if (exchangeProduct) {
qs.product_id = exchangeProduct;
}
if (after) {
qs.after = after;
}
return this.authCall('GET', '/orders', { qs: qs }).then((res) => {
const cbAfter = res.header['cb-after'];
const orders = res.body;
return {
after: cbAfter,
orders: orders
};
});
}
}
exports.GDAXExchangeAPI = GDAXExchangeAPI;
function GDAXOrderToOrder(order) {
let genericProduct = GDAXExchangeAPI.genericProduct(order.product_id);
return {
price: types_1.Big(order.price),
size: types_1.Big(order.size),
side: order.side,
id: order.id,
time: new Date(order.created_at),
productId: genericProduct,
status: order.status,
extra: {
post_only: order.post_only,
time_in_force: order.time_in_force,
settled: order.settled,
done_reason: order.done_reason,
filled_size: order.filled_size,
executed_value: order.executed_value,
fill_fees: order.fill_fees,
done_at: order.done_at
}
};
}