This repository has been archived on 2025-10-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
gekko/exchange/wrappers/poloniex.js
2021-06-06 20:25:14 +02:00

534 lines
14 KiB
JavaScript

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;