first commit
This commit is contained in:
commit
aa7869eb33
386 files changed
+69375
No files matched your search
@@ -0,0 +1,471 @@
|
||||
const Kraken = require('kraken-api');
|
||||
const moment = require('moment');
|
||||
const _ = require('lodash');
|
||||
const exchangeUtils = require('../exchangeUtils');
|
||||
const retry = exchangeUtils.retry;
|
||||
const scientificToDecimal = exchangeUtils.scientificToDecimal;
|
||||
|
||||
const marketData = require('./kraken-markets.json');
|
||||
|
||||
const Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency.toUpperCase()
|
||||
this.asset = config.asset.toUpperCase();
|
||||
}
|
||||
|
||||
this.name = 'kraken';
|
||||
this.since = null;
|
||||
|
||||
this.market = _.find(Trader.getCapabilities().markets, (market) => {
|
||||
return market.pair[0] === this.currency && market.pair[1] === this.asset
|
||||
});
|
||||
this.pair = this.market.book;
|
||||
|
||||
this.interval = 3100;
|
||||
|
||||
this.kraken = new Kraken(
|
||||
this.key,
|
||||
this.secret,
|
||||
{timeout: +moment.duration(60, 'seconds')}
|
||||
);
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'Service:Unavailable',
|
||||
'Request timed out',
|
||||
'Empty response',
|
||||
'API:Invalid nonce',
|
||||
'General:Temporary lockout',
|
||||
'Response code 525',
|
||||
'Service:Busy'
|
||||
];
|
||||
|
||||
// errors that might mean
|
||||
// the API call succeeded.
|
||||
const unknownResultErrors = [
|
||||
'Response code 502',
|
||||
'Response code 504',
|
||||
'Response code 522',
|
||||
'Response code 520',
|
||||
]
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.handleResponse = function(funcName, callback, nonMutating, payload) {
|
||||
return (error, body) => {
|
||||
|
||||
if(!error && !body) {
|
||||
error = new Error('Empty response');
|
||||
}
|
||||
|
||||
if(error) {
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
if(includes(error.message, ['Rate limit exceeded'])) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 2500;
|
||||
}
|
||||
|
||||
if(nonMutating && includes(error.message, unknownResultErrors)) {
|
||||
// this call only tried to retrieve data, safe to redo
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
if(funcName === 'addOrder' && includes(error.message, unknownResultErrors)) {
|
||||
|
||||
const { tradeType, amount, price } = payload;
|
||||
|
||||
return setTimeout(() => {
|
||||
this.getRawOpenOrders((err2, orders) => {
|
||||
if(err2) {
|
||||
console.log('err2', err2);
|
||||
return callback(err2);
|
||||
}
|
||||
|
||||
_.each(orders, (o, id) => {
|
||||
o.id = id;
|
||||
});
|
||||
|
||||
const order = _.find(orders, o => {
|
||||
if(o.descr.type !== tradeType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ts = moment.unix((o.opentm + '').split('.')[0])
|
||||
if(moment().diff(ts, 'm') > 10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// string vs float
|
||||
if(+o.descr.price != price) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// string vs float
|
||||
if(o.vol != amount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if(!order) {
|
||||
console.log('broken add order, appears not created:', {payload, orders: JSON.stringify(orders)});
|
||||
return this.addOrder(tradeType, amount, price, callback);
|
||||
}
|
||||
|
||||
return callback(undefined, { catched: true, id: order.id });
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
if(funcName === 'cancelOrder' && includes(error.message, unknownResultErrors)) {
|
||||
console.log('broken cancel');
|
||||
|
||||
return setTimeout(() => {
|
||||
const handle = (err2, data) => {
|
||||
if(err2) {
|
||||
console.log('err2', err2);
|
||||
return callback(err2);
|
||||
}
|
||||
|
||||
const order = _.get(data, `result["${payload}"]`);
|
||||
|
||||
if(!_.isObject(order)) {
|
||||
console.log('refetched broken cancel, cannot find order...', data);
|
||||
throw 'a';
|
||||
}
|
||||
|
||||
console.log(order);
|
||||
|
||||
if(order.status !== 'canceled') {
|
||||
console.log(new Date, 'it still exists, retrying cancel');
|
||||
return this.cancelOrder(payload, callback);
|
||||
}
|
||||
|
||||
console.log(new Date, 'it was canceled');
|
||||
return callback(undefined, true, { catched: true, filled: parseFloat(order.vol_exec) });
|
||||
|
||||
};
|
||||
|
||||
const reqData = {txid: payload};
|
||||
|
||||
const fetch = cb => this.kraken.api('QueryOrders', reqData, this.handleResponse('checkOrder', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
return callback(undefined, body);
|
||||
}
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const startTs = since ? moment(since).valueOf() : null;
|
||||
|
||||
const handle = (err, trades) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var parsedTrades = [];
|
||||
_.each(trades.result[this.pair], function(trade) {
|
||||
// Even when you supply 'since' you can still get more trades than you asked for, it needs to be filtered
|
||||
if (_.isNull(startTs) || startTs < moment.unix(trade[2]).valueOf()) {
|
||||
parsedTrades.push({
|
||||
tid: moment.unix(trade[2]).valueOf() * 1000000,
|
||||
date: parseInt(Math.round(trade[2]), 10),
|
||||
price: parseFloat(trade[0]),
|
||||
amount: parseFloat(trade[1])
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
|
||||
if(descending)
|
||||
callback(undefined, parsedTrades.reverse());
|
||||
else
|
||||
callback(undefined, parsedTrades);
|
||||
};
|
||||
|
||||
const reqData = {
|
||||
pair: this.pair
|
||||
};
|
||||
|
||||
if(since) {
|
||||
// Kraken wants a tid, which is found to be timestamp_ms * 1000000 in practice. No clear documentation on this though
|
||||
reqData.since = startTs * 1000000;
|
||||
}
|
||||
|
||||
const fetch = cb => this.kraken.api('Trades', reqData, this.handleResponse('getTrades', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) return callback(err);
|
||||
|
||||
let assetAmount = parseFloat( data.result[this.market.prefixed[1]] );
|
||||
let currencyAmount = parseFloat( data.result[this.market.prefixed[0]] );
|
||||
|
||||
if(!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
console.log(`Kraken did not return portfolio for ${this.asset}, assuming 0.`);
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
console.log(`Kraken did not return portfolio for ${this.currency}, assuming 0.`);
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
|
||||
return callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.kraken.api('Balance', {}, this.handleResponse('getPortfolio', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
// 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) {
|
||||
const makerFee = 0.16;
|
||||
callback(undefined, makerFee / 100);
|
||||
};
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const result = data.result[this.pair];
|
||||
const ticker = {
|
||||
ask: result.a[0],
|
||||
bid: result.b[0]
|
||||
};
|
||||
callback(undefined, ticker);
|
||||
};
|
||||
|
||||
const reqData = {pair: this.pair}
|
||||
const fetch = cb => this.kraken.api('Ticker', reqData, this.handleResponse('getTicker', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return _.floor(amount, this.market.amountPrecision);
|
||||
};
|
||||
|
||||
Trader.prototype.roundPrice = function(amount) {
|
||||
return scientificToDecimal(_.round(amount, this.market.pricePrecision));
|
||||
};
|
||||
|
||||
Trader.prototype.addOrder = function(tradeType, amount, price, callback) {
|
||||
price = this.roundPrice(price); // only round price, not amount
|
||||
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let txid;
|
||||
|
||||
if(data.catched) {
|
||||
// handled timeout, but order was created
|
||||
txid = data.id;
|
||||
} else if(_.isString(data)) {
|
||||
// handled timeout, order was NOT created
|
||||
txid = data;
|
||||
} else {
|
||||
// normal flow
|
||||
txid = data.result.txid[0];
|
||||
}
|
||||
|
||||
callback(undefined, txid);
|
||||
};
|
||||
|
||||
const reqData = {
|
||||
pair: this.pair,
|
||||
type: tradeType.toLowerCase(),
|
||||
ordertype: 'limit',
|
||||
price: price,
|
||||
volume: amount
|
||||
};
|
||||
|
||||
const fetch = cb => this.kraken.api('AddOrder', reqData, this.handleResponse('addOrder', cb, false, { tradeType, amount, price }));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.addOrder('buy', amount, price, callback);
|
||||
};
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.addOrder('sell', amount, price, callback);
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) return callback(err);
|
||||
|
||||
const price = parseFloat( data.result[ order ].price );
|
||||
const amount = parseFloat( data.result[ order ].vol_exec );
|
||||
const date = moment.unix( data.result[ order ].closetm );
|
||||
|
||||
// TODO: figure out fees, kraken is reporting 0 fees:
|
||||
// { 'OF6L2D-6LIKD-4OOHR7':
|
||||
// { refid: null,
|
||||
// userref: 0,
|
||||
// status: 'closed',
|
||||
// reason: null,
|
||||
// opentm: 1530694339.8116,
|
||||
// closetm: 1530694402.9572,
|
||||
// starttm: 0,
|
||||
// expiretm: 0,
|
||||
// descr: [Object],
|
||||
// vol: '0.00500000',
|
||||
// vol_exec: '0.00500000',
|
||||
// cost: '27.9',
|
||||
// fee: '0',
|
||||
// price: '5595.0',
|
||||
// stopprice: '0.00000',
|
||||
// limitprice: '0.00000',
|
||||
// misc: '',
|
||||
// oflags: 'fciq' } } }
|
||||
|
||||
callback(undefined, {
|
||||
price,
|
||||
amount,
|
||||
date,
|
||||
feePercent: 0.16 // default for now
|
||||
});
|
||||
};
|
||||
|
||||
const reqData = {txid: order};
|
||||
|
||||
const fetch = cb => this.kraken.api('QueryOrders', reqData, this.handleResponse('getOrder', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) return callback(err);
|
||||
|
||||
const result = data.result[order];
|
||||
|
||||
callback(undefined, {
|
||||
executed: result.vol === result.vol_exec,
|
||||
open: result.status === 'open',
|
||||
filledAmount: +result.vol_exec
|
||||
});
|
||||
};
|
||||
|
||||
const reqData = {txid: order};
|
||||
|
||||
const fetch = cb => this.kraken.api('QueryOrders', reqData, this.handleResponse('checkOrder', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
const reqData = {txid: order};
|
||||
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
if(err.message.includes('Unknown order')) {
|
||||
return callback(undefined, true);
|
||||
}
|
||||
|
||||
// catch race condition:
|
||||
// if we cancel, that request times out
|
||||
// we recheck: it's live BUT then the
|
||||
// timeout request goes through nonetheless
|
||||
// (only times out behind cloudflare)
|
||||
if(err.message.includes('Invalid order')) {
|
||||
return callback(undefined, true);
|
||||
}
|
||||
|
||||
return callback(err)
|
||||
}
|
||||
|
||||
if(data.filled) {
|
||||
callback(undefined, false, data);
|
||||
}
|
||||
|
||||
callback(undefined, false);
|
||||
}
|
||||
|
||||
const fetch = cb => this.kraken.api('CancelOrder', reqData, this.handleResponse('cancelOrder', cb, false, order));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getRawOpenOrders = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err)
|
||||
}
|
||||
|
||||
callback(undefined, data.result.open);
|
||||
}
|
||||
|
||||
const fetch = cb => this.kraken.api('OpenOrders', {}, this.handleResponse('getOpenOrders', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getOpenOrders = function(callback) {
|
||||
|
||||
this.getRawOpenOrders((err, allOrders) => {
|
||||
if(err) {
|
||||
console.log(err);
|
||||
|
||||
return callback(err)
|
||||
}
|
||||
|
||||
const orders = [];
|
||||
|
||||
_.each(allOrders, (o, id) => {
|
||||
if(o.descr.pair === this.pair) {
|
||||
orders.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
callback(undefined, orders);
|
||||
});
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Kraken',
|
||||
slug: 'kraken',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
tid: 'date',
|
||||
tradable: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
Reference in new issue
Block a user