Skip to content
This repository has been archived by the owner on Feb 16, 2020. It is now read-only.

[KRAKEN] Normalizing the approach for retry handling #1411

Merged
merged 10 commits into from
Dec 8, 2017
25 changes: 25 additions & 0 deletions core/error.js
Original file line number Diff line number Diff line change
@@ -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
};

33 changes: 21 additions & 12 deletions core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
196 changes: 75 additions & 121 deletions exchanges/kraken.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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) {
Expand All @@ -154,7 +163,7 @@ Trader.prototype.getTrades = function(since, callback, descending) {
callback(undefined, parsedTrades);
};

var reqData = {
let reqData = {
pair: this.pair
};

Expand All @@ -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.
Expand All @@ -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 = {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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 () {
Expand Down
Loading