diff --git a/core/error.js b/core/error.js new file mode 100644 index 000000000..aafdec49c --- /dev/null +++ b/core/error.js @@ -0,0 +1,25 @@ +const _ = require('lodash'); + +let RetryError = function(message) { + _.bindAll(this); + + this.name = "RetryError"; + this.message = message; +} + +RetryError.prototype = new Error(); + +let AbortError = function(message) { + _.bindAll(this); + + this.name = "AbortError"; + this.message = message; +} + +AbortError.prototype = new Error(); + +module.exports = { + 'RetryError': RetryError, + 'AbortError': AbortError +}; + diff --git a/core/util.js b/core/util.js index b34ae2505..0c92ae1ac 100644 --- a/core/util.js +++ b/core/util.js @@ -5,6 +5,7 @@ var fs = require('fs'); var semver = require('semver'); var program = require('commander'); var retry = require('retry'); +var Errors = require('./error'); var startTime = moment(); @@ -16,6 +17,19 @@ var _gekkoEnv = false; var _args = false; +var retryHelper = function(fn, options, callback) { + var operation = retry.operation(options); + operation.attempt(function(currentAttempt) { + fn(function(err, result) { + if (!(err instanceof Errors.AbortError) && operation.retry(err)) { + return; + } + + callback(err ? err.message : null, result); + }); + }); +} + // helper functions var util = { getConfig: function() { @@ -163,23 +177,18 @@ var util = { return startTime; }, retry: function(fn, callback) { - var operation = retry.operation({ + var operation = { retries: 5, factor: 1.2, minTimeout: 1 * 1000, maxTimeout: 3 * 1000 - }); + }; - operation.attempt(function(currentAttempt) { - fn(function(err, result) { - if (operation.retry(err)) { - return; - } - - callback(err ? operation.mainError() : null, result); - }); - }); - } + retryHelper(fn, options, callback); + }, + retryCustom: function(options, fn, callback) { + retryHelper(fn, options, callback); + }, } // NOTE: those options are only used diff --git a/exchanges/kraken.js b/exchanges/kraken.js index fc8e96842..eff3feb25 100644 --- a/exchanges/kraken.js +++ b/exchanges/kraken.js @@ -1,7 +1,9 @@ var Kraken = require('kraken-api-es5'); var moment = require('moment'); -var util = require('../core/util'); var _ = require('lodash'); + +var util = require('../core/util'); +var Errors = require('../core/error'); var log = require('../core/log'); var crypto_currencies = [ @@ -94,46 +96,53 @@ var Trader = function(config) { ); } -var recoverableErrors = new RegExp(/(SOCKETTIMEDOUT|TIMEDOUT|CONNRESET|CONNREFUSED|NOTFOUND|API:Invalid nonce|Service:Unavailable|Request timed out|Response code 525|Response code 520|Response code 504|Response code 502)/) +var retryCritical = { + retries: 10, + factor: 1.2, + minTimeout: 1 * 1000, + maxTimeout: 30 * 1000 +}; -Trader.prototype.retry = function(method, args, error, alwaysRetry) { - if (!alwaysRetry && (!error || !error.message.match(recoverableErrors))) { - log.error('[kraken.js] ', this.name, 'returned an irrecoverable error: ', error.message); - return false; - } +var retryForever = { + forever: true, + factor: 1.2, + minTimeout: 10, + maxTimeout: 30 +}; - // 5 -> 10s to avoid more rejection - var wait = +moment.duration(10, 'seconds'); - log.debug('[kraken.js] (retry) ', this.name, 'returned an error, retrying..'); +var recoverableErrors = new RegExp(/(SOCKETTIMEDOUT|TIMEDOUT|CONNRESET|CONNREFUSED|NOTFOUND|API:Invalid nonce|Service:Unavailable|Request timed out|Response code 520|Response code 504|Response code 502)/) - var self = this; +Trader.prototype.processError = function(funcName, error) { + if (!error) return undefined; - // make sure the callback (and any other fn) - // is bound to Trader - _.each(args, function(arg, i) { - if(_.isFunction(arg)) - args[i] = _.bind(arg, self); - }); + if (!error.message.match(recoverableErrors)) { + log.error(`[kraken.js] (${funcName}) returned an irrecoverable error: ${error.message}`); + return new Errors.AbortError('[kraken.js] ' + error.message); + } - // run the failed method again with the same - // arguments after wait - setTimeout( - function() { method.apply(self, args) }, - wait - ); + log.debug(`[kraken.js] (${funcName}) returned an error, retrying: ${error.message}`); + return new Errors.RetryError('[kraken.js] ' + error.message); +}; + +Trader.prototype.handleResponse = function(funcName, callback) { + return (error, body) => { + if(!error) { + if(_.isEmpty(body) || !body.result) + error = new Error('NO DATA WAS RETURNED'); + + else if(!_.isEmpty(body.error)) + error = new Error(body.error); + } - return true; + return callback(this.processError(funcName, error), body); + } }; Trader.prototype.getTrades = function(since, callback, descending) { - var args = _.toArray(arguments); var startTs = since ? moment(since).valueOf() : null; - var process = function(err, trades) { - if (err || !trades || trades.length === 0) { - log.error('error getting trades', err); - return this.retry(this.getTrades, args, err, true); - } + var processResults = function(err, trades) { + if (err) return callback(err); var parsedTrades = []; _.each(trades.result[this.pair], function(trade) { @@ -154,7 +163,7 @@ Trader.prototype.getTrades = function(since, callback, descending) { callback(undefined, parsedTrades); }; - var reqData = { + let reqData = { pair: this.pair }; @@ -163,25 +172,16 @@ Trader.prototype.getTrades = function(since, callback, descending) { reqData.since = startTs * 1000000; } - this.kraken.api('Trades', reqData, _.bind(process, this)); + let handler = (cb) => this.kraken.api('Trades', reqData, this.handleResponse('getTrades', cb)); + util.retryCustom(retryForever, _.bind(handler, this), _.bind(processResults, this)); }; Trader.prototype.getPortfolio = function(callback) { - var args = _.toArray(arguments); + console.log('getPortfolio'); var setBalance = function(err, data) { - log.debug('[kraken.js] entering "setBalance" callback after kraken-api call, err:', err, ' data:' , data); - - if(_.isEmpty(data)) - err = new Error('no data (getPortfolio)'); - - else if(!_.isEmpty(data.error)) - err = new Error(data.error); - - if (err || !data.result) { - log.error('[kraken.js] ' , err); - if (!this.retry(this.getPortfolio, args, err)) - return callback(err); - } + console.log('aa', arguments); + if(err) return callback(err); + log.debug('[kraken.js] entering "setBalance" callback after kraken-api call, data:' , data); // When using the prefix-less assets, you remove the prefix from the assset but leave // it on the curreny in this case. An undocumented Kraken quirk. @@ -207,30 +207,21 @@ Trader.prototype.getPortfolio = function(callback) { return callback(undefined, portfolio); }; - this.kraken.api('Balance', {}, _.bind(setBalance, this)); + let handler = (cb) => this.kraken.api('Balance', {}, this.handleResponse('getPortfolio', cb)); + util.retryCustom(retryForever, _.bind(handler, this), _.bind(setBalance, this)); }; // This assumes that only limit orders are being placed with standard assets pairs // It does not take into account volume discounts. // Base maker fee is 0.16%, taker fee is 0.26%. Trader.prototype.getFee = function(callback) { - var makerFee = 0.16; + const makerFee = 0.16; callback(undefined, makerFee / 100); }; Trader.prototype.getTicker = function(callback) { var setTicker = function(err, data) { - - if(!err && _.isEmpty(data)) - err = new Error('no data (getTicker)'); - - else if(!err && !_.isEmpty(data.error)) - err = new Error(data.error); - - if (err) { - log.error('unable to get ticker', JSON.stringify(err)); - return callback(err); - } + if (err) return callback(err); var result = data.result[this.pair]; var ticker = { @@ -240,7 +231,10 @@ Trader.prototype.getTicker = function(callback) { callback(undefined, ticker); }; - this.kraken.api('Ticker', {pair: this.pair}, _.bind(setTicker, this)); + let reqData = {pair: this.pair} + + let handler = (cb) => this.kraken.api('Ticker', reqData, this.handleResponse('getTicker', cb)); + util.retryCustom(retryForever, _.bind(handler, this), _.bind(setTicker, this)); }; Trader.prototype.roundAmount = function(amount) { @@ -262,66 +256,47 @@ Trader.prototype.roundAmount = function(amount) { }; Trader.prototype.addOrder = function(tradeType, amount, price, callback) { - var args = _.toArray(arguments); - amount = this.roundAmount(amount); price = this.roundAmount(price); // but the link talks about rounding price... And I had the bug log.debug('[kraken.js] (addOrder)', tradeType.toUpperCase(), amount, this.asset, '@', price, this.currency); var setOrder = function(err, data) { - - // console.log('blap', err, data); - - if(!err && _.isEmpty(data)) - err = new Error('no data (addOrder)'); - else if(!err && !_.isEmpty(data.error)) - err = new Error(data.error); - - if(err) { - log.error('unable to ' + tradeType.toLowerCase(), err); - if (!this.retry(this.addOrder, args, err)) - return callback(err); - } - + if(err) return callback(err); + var txid = data.result.txid[0]; - log.debug('added order with txid:', txid); + log.debug('[kraken.js] (addOrder) added order with txid:', txid); callback(undefined, txid); }; - this.kraken.api('AddOrder', { + let reqData = { pair: this.pair, type: tradeType.toLowerCase(), ordertype: 'limit', price: price, volume: amount.toString() - }, _.bind(setOrder, this)); + }; + + let handler = (cb) => this.kraken.api('AddOrder', reqData, this.handleResponse('addOrder', cb)); + util.retryCustom(retryCritical, _.bind(handler, this), _.bind(setOrder, this)); }; Trader.prototype.getOrder = function(order, callback) { - - var get = function(err, data) { - if(!err && _.isEmpty(data) && _.isEmpty(data.result)) - err = new Error('no data (getOrder)'); - - else if(!err && !_.isEmpty(data.error)) - err = new Error(data.error); - - if(err) { - log.error('Unable to get order', order, JSON.stringify(err)); - return callback(err); - } + var getOrder = function(err, data) { + if(err) return callback(err); var price = parseFloat( data.result[ order ].price ); var amount = parseFloat( data.result[ order ].vol_exec ); var date = moment.unix( data.result[ order ].closetm ); callback(undefined, {price, amount, date}); - }.bind(this); + }; - this.kraken.api('QueryOrders', {txid: order}, get); + let reqData = {txid: order}; + let handler = (cb) => this.kraken.api('QueryOrders', reqData, this.handleResponse('getOrder', cb)); + util.retryCustom(retryCritical, _.bind(handler, this), _.bind(getOrder, this)); } Trader.prototype.buy = function(amount, price, callback) { @@ -334,43 +309,22 @@ Trader.prototype.sell = function(amount, price, callback) { Trader.prototype.checkOrder = function(order, callback) { var check = function(err, data) { - if(_.isEmpty(data)) - err = new Error('no data (checkOrder)'); - - if(!_.isEmpty(data.error)) - err = new Error(data.error); - - if(err) { - log.error('Unable to check order', order, JSON.stringify(err)); - return callback(err); - } + if(err) return callback(err); var result = data.result[order]; var stillThere = result.status === 'open' || result.status === 'pending'; callback(undefined, !stillThere); }; - this.kraken.api('QueryOrders', {txid: order}, _.bind(check, this)); + let reqData = {txid: order}; + let handler = (cb) => this.kraken.api('QueryOrders', reqData, this.handleResponse('checkOrder', cb)); + util.retryCustom(retryCritical, _.bind(handler, this), _.bind(check, this)); }; Trader.prototype.cancelOrder = function(order, callback) { - var args = _.toArray(arguments); - var cancel = function(err, data) { - if(!err && _.isEmpty(data)) - err = new Error('no data (cancelOrder)'); - else if(!err && !_.isEmpty(data.error)) - err = new Error(data.error); - - if(err) { - log.error('unable to cancel order', order, '(', err, JSON.stringify(err), ')'); - if (!this.retry(this.cancelOrder, args, err)) - return callback(err); - } - - callback(); - }; - - this.kraken.api('CancelOrder', {txid: order}, _.bind(cancel, this)); + let reqData = {txid: order}; + let handler = (cb) => this.kraken.api('CancelOrder', reqData, this.handleResponse('cancelOrder', cb)); + util.retryCustom(retryForever, _.bind(handler, this), callback); }; Trader.getCapabilities = function () { diff --git a/importers/exchanges/kraken.js b/importers/exchanges/kraken.js index a5f6a1fec..2ba20cbf7 100644 --- a/importers/exchanges/kraken.js +++ b/importers/exchanges/kraken.js @@ -1,8 +1,10 @@ var KrakenClient = require('kraken-api-es5') -var util = require('../../core/util.js'); var _ = require('lodash'); var moment = require('moment'); + +var util = require('../../core/util.js'); var log = require('../../core/log'); +var Errors = require('../../core/error.js') var config = util.getConfig(); diff --git a/plugins/trader/portfolioManager.js b/plugins/trader/portfolioManager.js index c9669b8b8..b97a7fbb5 100644 --- a/plugins/trader/portfolioManager.js +++ b/plugins/trader/portfolioManager.js @@ -104,7 +104,7 @@ Manager.prototype.setPortfolio = function(callback) { }.bind(this); - util.retry(this.exchange.getPortfolio, set); + this.exchange.getPortfolio(set); }; Manager.prototype.setFee = function(callback) { @@ -117,7 +117,7 @@ Manager.prototype.setFee = function(callback) { if(_.isFunction(callback)) callback(); }.bind(this); - util.retry(this.exchange.getFee, set); + this.exchange.getFee(set); }; Manager.prototype.setTicker = function(callback) { @@ -130,7 +130,7 @@ Manager.prototype.setTicker = function(callback) { if(_.isFunction(callback)) callback(); }.bind(this); - util.retry(this.exchange.getTicker, set); + this.exchange.getTicker(set); }; // return the [fund] based on the data we have in memory