const Therocktrading = require('therocktrading'); const _ = require('lodash'); const moment = require('moment'); const retry = require('../exchangeUtils').retry; const marketData = require("./therocktrading-markets.json"); const QUERY_DELAY = 350; const Trader = function(config) { this.post_only = true; this.use_sandbox = false; this.name = 'Therocktrading'; this.scanback = true; this.scanbackTid = 0; this.since = null; this.asset = config.asset; this.currency = config.currency; this.api_url = 'https://api.therocktrading.com'; this.api_sandbox_url = 'https://api-staging.therocktrading.com'; if (_.isObject(config)) { this.key = config.key; this.secret = config.secret; this.pair = [config.asset, config.currency].join('').toUpperCase(); this.post_only = typeof config.post_only !== 'undefined' ? config.post_only : true; if (config.sandbox) { this.use_sandbox = config.sandbox; } } this.therocktrading = new Therocktrading(this.key, this.secret, 'Gekko Broker v' + require('../package.json').version); }; const recoverableErrors = [ 'SOCKETTIMEDOUT', 'TIMEDOUT', 'CONNRESET', 'CONNREFUSED', 'NOTFOUND', 'Rate limit exceeded', 'Response code 5', 'Therocktrading is currently under maintenance.', 'HTTP 408 Error', 'HTTP 504 Error', 'HTTP 503 Error', 'EHOSTUNREACH', 'EAI_AGAIN', 'ENETUNREACH' ]; const includes = (str, list) => { if(!_.isString(str)) return false; return _.some(list, item => str.includes(item)); } Trader.prototype.processResponse = function(method, next) { return (error, body) => { if (body && body.code) { error = new Error(`Error ${body.code}: ${body.msg}`); } if(!error && body && !_.isEmpty(body.message)) { error = new Error(body.message); } /*if( response && response.statusCode < 200 && response.statusCode >= 300 ) { error = new Error(`Response code ${response.statusCode}`); }*/ if(error) { if(_.isString(error)) { error = new Error(error); } if(includes(error.message, recoverableErrors)) { error.notFatal = true; } return next(error); } return next(undefined, body); } }; Trader.prototype.getTrades = function(since, callback, descending) { const handle = (err, data) => { if (err) return callback(err); var trades = []; if (_.isArray(data.trades)) { trades = _.map(data.trades, function(trade) { return { tid: trade.id, price: trade.price, amount: trade.amount, date: moment.utc(trade.date).format('X') }; }); } callback(null, descending ? trades : trades.reverse()); }; if (moment.isMoment(since)) since = since.format(); //if (moment.isMoment(to)) to = to.format(); var options = { order: "DESC" // which is default at therock } if (since) options.after = since; const fetch = cb => this.therocktrading.trades(this.pair, options, this.processResponse('getTrades', cb)); retry(null, fetch, handle); }; Trader.prototype.getTicker = function(callback) { const result = (err, data) => { if (err) return callback(err); callback(undefined, { bid: data.bid, ask: data.ask }); }; const fetch = cb => this.therocktrading.ticker(this.pair, this.processResponse('getTicker', cb)); retry(null, fetch, result); }; Trader.prototype.getFee = function(callback) { const fee = 0.2; // should check for discounts ? callback(undefined, fee); }; Trader.prototype.getPortfolio = function(callback) { const result = (err, data) => { if (err) return callback(err); var portfolio = data.balances.map(function(balance) { return { name: balance.currency.toUpperCase(), amount: parseFloat(balance.trading_balance), }; }); callback(undefined, portfolio); }; const fetch = cb => this.therocktrading.balances(this.processResponse('getPortfolio', cb)); retry(undefined, fetch, result); }; Trader.prototype.buy = function(amount, price, callback) { const handle = (err, result) => { if(err) { return callback(err); } callback(undefined, result.id); } const fetch = next => { this.therocktrading.buy(this.pair, amount.toString(), price.toString(), this.processResponse('order', next)) }; retry(null, fetch, handle); } Trader.prototype.sell = function(amount, price, callback) { const handle = (err, result) => { if(err) { return callback(err); } callback(undefined, result.id); } const fetch = next => { this.therocktrading.sell(this.pair, amount.toString(), price.toString(), this.processResponse('order', next)) }; retry(null, fetch, handle); } Trader.prototype.getOrder = function(order, callback) { const handle = (err, result) => { if(err) return callback(err); let price = 0; let amount = 0; let date = moment(0); if(result.amount === result.amount_unfilled) { return callback(null, {price, amount, date}); } _.each(result.trades, trade => { date = moment(trade.date); price = ((price * amount) + (+trade.price * trade.amount)) / (+trade.amount + amount); amount += +trade.amount; }); const fees = {}; const feePercent = this.makerFee; const fee = price * amount * feePercent; fees[this.currency] = fee; callback(err, {price, amount, date, fees, feePercent}); }; const fetch = cb => this.therocktrading.order_status(this.pair, order, this.processResponse('order_status', cb)); retry(null, fetch, handle); }; Trader.prototype.checkOrder = function(order, callback) { const result = (err, data) => { if (err) return callback(err); var status = data.status; if (status == 'executed') { return callback(undefined, { executed: true, open: false, filledAmount: parseFloat(data.amount) }); } else if (status === 'deleted') { return callback(undefined, { executed: false, open: false, filledAmount: parseFloat(data.amount - data.amount_unfilled) }); } else if (status === 'active') { // should check if not expired (data.close_on)? return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.amount - data.amount_unfilled) }); } callback(new Error('Unknown status ' + status)); }; const fetch = cb => this.therocktrading.order_status(this.pair, order, this.processResponse('order_status', cb)); retry(null, fetch, result); }; Trader.prototype.cancelOrder = function(order, callback) { // callback for cancelOrder should be true if the order was already filled, otherwise false const result = (err, data) => { if(err) { return callback(null, true); // need to catch the specific error but usually an error on cancel means it was filled } return callback(null, false); }; const fetch = cb => this.therocktrading.cancel_order(this.pair, order, this.processResponse('cancel_order', cb)); retry(null, fetch, result); }; Trader.prototype.roundAmount = function(amount) { return _.floor(amount, 8); } Trader.prototype.roundPrice = function(price) { return +price; } Trader.getCapabilities = function() { return { name: 'Therocktrading', slug: 'therocktrading', currencies: marketData.currencies, assets: marketData.assets, markets: marketData.markets, requires: ['key', 'secret'], providesHistory: 'date', providesFullHistory: true, // following tid, define type to use on trade to batch trades tid: 'tid', tradable: true, gekkoBroker: 0.6 }; }; module.exports = Trader;