first commit
This commit is contained in:
commit
aa7869eb33
386 files changed
+69375
No files matched your search
@@ -0,0 +1,533 @@
|
||||
const Poloniex = require("gekko-broker-poloniex");
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
const marketData = require('./poloniex-markets.json');
|
||||
|
||||
const Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency;
|
||||
this.asset = config.asset;
|
||||
}
|
||||
this.name = 'Poloniex';
|
||||
this.balance;
|
||||
this.price;
|
||||
|
||||
this.pair = this.currency + '_' + this.asset;
|
||||
|
||||
this.fillDataOnCancel = true;
|
||||
|
||||
this.poloniex = new Poloniex({
|
||||
key: this.key,
|
||||
secret: this.secret,
|
||||
userAgent: 'Gekko Broker v' + require('../package.json').version
|
||||
});
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'429',
|
||||
'522',
|
||||
'504',
|
||||
'503',
|
||||
'500',
|
||||
'502',
|
||||
'socket hang up',
|
||||
'Empty response',
|
||||
'Please try again in a few minutes.',
|
||||
'Nonce must be greater than',
|
||||
'Internal error. Please try again.',
|
||||
'Connection timed out. Please try again.',
|
||||
// getaddrinfo EAI_AGAIN poloniex.com poloniex.com:443
|
||||
'EAI_AGAIN',
|
||||
'ENETUNREACH',
|
||||
'socket hang up'
|
||||
];
|
||||
|
||||
// errors that might mean
|
||||
// the API call succeeded.
|
||||
const unknownResultErrors = [
|
||||
'ETIMEDOUT',
|
||||
]
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.outbidPrice = function(price, isUp) {
|
||||
let newPrice;
|
||||
|
||||
const tickSize = 0.00000001;
|
||||
|
||||
if(isUp) {
|
||||
newPrice = price + 0.00000001;
|
||||
} else {
|
||||
newPrice = price - tickSize;
|
||||
}
|
||||
|
||||
return this.roundPrice(newPrice);
|
||||
}
|
||||
|
||||
Trader.prototype.processResponse = function(next, fn, payload) {
|
||||
// TODO: in very rare cases the callback is
|
||||
// called twice (first on ETIMEDOUT later
|
||||
// without error). Temp workaround.
|
||||
next = _.once(next);
|
||||
|
||||
return (err, data) => {
|
||||
let error;
|
||||
|
||||
if(err) {
|
||||
if(err.message) {
|
||||
error = err;
|
||||
} else {
|
||||
console.log('err but no message,', typeof err, {err, data});
|
||||
error = new Error(err);
|
||||
}
|
||||
} else if(!data) {
|
||||
error = new Error('Empty response');
|
||||
} else if(data.error) {
|
||||
error = new Error(data.error);
|
||||
} else if(includes(data, ['Please complete the security check to proceed.'])) {
|
||||
error = new Error(
|
||||
'Your IP has been flagged by CloudFlare. ' +
|
||||
'As such Gekko Broker cannot access Poloniex.'
|
||||
);
|
||||
data = undefined;
|
||||
} else if(includes(data, ['Please try again in a few minutes.'])) {
|
||||
error = new Error('Please try again in a few minutes.');
|
||||
error.notFatal = true;
|
||||
data = undefined;
|
||||
} else if(includes(data, ['<!DOCTYPE html>'])) {
|
||||
error = new Error(data);
|
||||
data = undefined;
|
||||
}
|
||||
|
||||
if(error) {
|
||||
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
if(includes(error.message, ['Currently in maintenance mode.'])) {
|
||||
console.log(new Date, '[Poloniex] Currently in maintenance mode. Retrying...');
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 1000;
|
||||
}
|
||||
|
||||
// not actually an error, means order never executed against other trades
|
||||
if(fn === 'getOrder' &&
|
||||
error.message.includes('Order not found, or you are not the person who placed it.')
|
||||
) {
|
||||
error = undefined;
|
||||
data = { unfilled: true };
|
||||
console.log(new Date, 'UNKNOWN ORDER!', payload);
|
||||
}
|
||||
|
||||
if(fn === 'cancelOrder') {
|
||||
|
||||
// already filled
|
||||
if(includes(error.message, ['Invalid order number, or you are not the person who placed the order.'])) {
|
||||
console.log(new Date, 'cancelOrder invalid order');
|
||||
error = undefined;
|
||||
data = { filled: true };
|
||||
}
|
||||
|
||||
// it might be cancelled
|
||||
else if(includes(error.message, unknownResultErrors)) {
|
||||
setTimeout(() => {
|
||||
this.getRawOpenOrders((err, orders) => {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const order = _.find(orders, o => o.orderNumber == payload);
|
||||
|
||||
// the cancel did not work since the order still exists
|
||||
if(order) {
|
||||
error.notFatal = true;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
// it was cancelled, we need to check filled amount..
|
||||
console.log(new Date, '[CANCELFIX] process cancel response');
|
||||
console.log('[CANCELFIX] rechecking fill')
|
||||
setTimeout(() => {
|
||||
this.getOrder(payload, (error, order) => {
|
||||
if(error) {
|
||||
return next(error)
|
||||
}
|
||||
|
||||
console.log('[CANCELFIX] checked, got:', order);
|
||||
|
||||
return next(undefined, { filled: order.amount });
|
||||
});
|
||||
}, this.checkInterval);
|
||||
});
|
||||
|
||||
}, this.checkInterval);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(fn === 'order') {
|
||||
if(includes(error.message, ['Not enough'])) {
|
||||
error.retry = 2;
|
||||
}
|
||||
|
||||
// we need to check whether the order was actually created
|
||||
if(includes(error.message, unknownResultErrors)) {
|
||||
return setTimeout(() => {
|
||||
this.findLastOrder(10, payload, (err, lastTrade) => {
|
||||
if(lastTrade) {
|
||||
return next(undefined, lastTrade);
|
||||
}
|
||||
|
||||
next(error);
|
||||
});
|
||||
|
||||
}, this.checkInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next(error, data);
|
||||
}
|
||||
}
|
||||
|
||||
Trader.prototype.findLastOrder = function(since, side, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
result = result.filter(t => t.type === side);
|
||||
|
||||
if(!result.length) {
|
||||
return callback(undefined, undefined);
|
||||
}
|
||||
|
||||
let order;
|
||||
if(since) {
|
||||
const threshold = moment().subtract(since, 'm');
|
||||
order = _.find(result, o => moment.utc(o.date) > threshold);
|
||||
} else {
|
||||
order = _.last(result);
|
||||
}
|
||||
|
||||
callback(undefined, order);
|
||||
};
|
||||
|
||||
this.getRawOpenOrders(handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getRawOpenOrders = function(callback) {
|
||||
const fetch = next => this.poloniex.returnOpenOrders(this.currency, this.asset, this.processResponse(next));
|
||||
retry(null, fetch, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getOpenOrders = function(callback) {
|
||||
this.getRawOpenOrders((err, orders) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
console.log(orders);
|
||||
|
||||
const ids = orders.map(o => o.orderNumber);
|
||||
|
||||
return callback(undefined, ids);
|
||||
})
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var assetAmount = parseFloat( data[this.asset] );
|
||||
var currencyAmount = parseFloat( data[this.currency] );
|
||||
|
||||
if(
|
||||
!_.isNumber(assetAmount) || _.isNaN(assetAmount) ||
|
||||
!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)
|
||||
) {
|
||||
assetAmount = 0;
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
var portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
|
||||
callback(undefined, portfolio);
|
||||
}
|
||||
|
||||
const fetch = next => this.poloniex.myBalances(this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getFullPortfolio = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const asset = data[this.asset];
|
||||
const currency = data[this.currency];
|
||||
|
||||
let assetAmount = parseFloat(asset.available) + parseFloat(asset.onOrders);
|
||||
let currencyAmount = parseFloat(currency.available) + parseFloat(asset.onOrders);
|
||||
|
||||
if(!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
var portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
|
||||
callback(undefined, portfolio);
|
||||
}
|
||||
|
||||
const fetch = next => this.poloniex.returnCompleteBalances(this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
var tick = data[this.pair];
|
||||
|
||||
callback(null, {
|
||||
bid: parseFloat(tick.highestBid),
|
||||
ask: parseFloat(tick.lowestAsk),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const fetch = next => this.poloniex.getTicker(this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
callback(undefined, parseFloat(data.makerFee));
|
||||
}
|
||||
|
||||
const fetch = next => this.poloniex._private('returnFeeInfo', this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return _.floor(amount, 8);
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return +price;
|
||||
}
|
||||
|
||||
Trader.prototype.isValidLot = function(price, amount) {
|
||||
// Error: Total must be at least 0.0001.
|
||||
return amount * price >= 0.0001;
|
||||
}
|
||||
|
||||
Trader.prototype.createOrder = function(side, amount, price, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err) {
|
||||
console.log('createOrder', {side, amount, price});
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// console.log(new Date, 'PORDER created', result.orderNumber);
|
||||
|
||||
callback(undefined, result.orderNumber);
|
||||
}
|
||||
|
||||
const fetch = next => {
|
||||
this.poloniex[side](this.currency, this.asset, price, amount, this.processResponse(next, 'order', side))
|
||||
};
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.createOrder('buy', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.createOrder('sell', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(id, callback) {
|
||||
const handle = (err, result) => {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(result.completed) {
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
}
|
||||
|
||||
const order = _.find(result, function(o) { return o.orderNumber === id });
|
||||
if(!order) {
|
||||
console.log(new Date, 'order not open', id, result);
|
||||
// if the order is not open it's fully executed
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
}
|
||||
|
||||
callback(undefined, { executed: false, open: true, filledAmount: order.startingAmount - order.amount });
|
||||
}
|
||||
|
||||
const fetch = next => this.poloniex.myOpenOrders(this.currency, this.asset, this.processResponse(next, 'checkOrder'));
|
||||
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.unfilled) {
|
||||
return callback(null, {price, amount, date});
|
||||
}
|
||||
|
||||
_.each(result, trade => {
|
||||
date = moment(trade.date);
|
||||
price = ((price * amount) + (+trade.rate * trade.amount)) / (+trade.amount + amount);
|
||||
amount += +trade.amount;
|
||||
});
|
||||
|
||||
const fees = {};
|
||||
const feePercent = _.first(result).fee * 100;
|
||||
|
||||
if(_.first(result).type === 'sell') {
|
||||
const fee = price * amount * _.first(result).fee;
|
||||
fees[this.currency] = fee;
|
||||
} else {
|
||||
const fee = amount * _.first(result).fee;
|
||||
fees[this.asset] = fee;
|
||||
}
|
||||
|
||||
callback(err, {price, amount, date, fees, feePercent});
|
||||
};
|
||||
|
||||
const fetch = next => this.poloniex.returnOrderTrades(order, this.processResponse(next, 'getOrder', order));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// console.log(new Date, 'PORDER cancel', order);
|
||||
|
||||
if(result.filled) {
|
||||
console.log(new Date, 'result.filled', result);
|
||||
return callback(undefined, true);
|
||||
}
|
||||
|
||||
let data;
|
||||
|
||||
if(result.amount) {
|
||||
data = { remaining: result.amount };
|
||||
} else {
|
||||
console.log(new Date, 'not result.amount', result);
|
||||
}
|
||||
|
||||
|
||||
callback(undefined, false, data);
|
||||
};
|
||||
|
||||
const fetch = next => this.poloniex.cancelOrder(this.currency, this.asset, order, this.processResponse(next, 'cancelOrder', order));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
|
||||
const firstFetch = !!since;
|
||||
const args = _.toArray(arguments);
|
||||
|
||||
const handle = (err, result) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
// Edge case, see here:
|
||||
// @link https://github.com/askmike/gekko/issues/479
|
||||
if(firstFetch && _.size(result) === 50000)
|
||||
return callback(
|
||||
[
|
||||
'Poloniex did not provide enough data. Read this:',
|
||||
'https://github.com/askmike/gekko/issues/479'
|
||||
].join('\n\n')
|
||||
);
|
||||
|
||||
result = _.map(result, function(trade) {
|
||||
return {
|
||||
tid: trade.tradeID,
|
||||
amount: +trade.amount,
|
||||
date: moment.utc(trade.date).unix(),
|
||||
price: +trade.rate
|
||||
};
|
||||
});
|
||||
|
||||
callback(null, result.reverse());
|
||||
};
|
||||
|
||||
var params = {
|
||||
currencyPair: this.pair
|
||||
}
|
||||
|
||||
if(since)
|
||||
params.start = since.unix();
|
||||
|
||||
const fetch = next => this.poloniex._public('returnTradeHistory', params, this.processResponse(next, 'getTrades', since));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Poloniex',
|
||||
slug: 'poloniex',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
currencyMinimums: {BTC: 0.0001, ETH: 0.0001, XMR: 0.0001, USDT: 1.0},
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
tradable: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
Reference in new issue
Block a user