UNPKG

tlab-trading-toolkit

Version:

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

352 lines (351 loc) 14.1 kB
"use strict"; 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 } }; }