first commit
This commit is contained in:
commit
aa7869eb33
386 files changed
+69375
No files matched your search
@@ -0,0 +1,416 @@
|
||||
const Luno = require("bitx");
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const node_util = require('util');
|
||||
const Errors = require('../exchangeErrors');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
const name = 'Luno';
|
||||
|
||||
var tradeAttempt = 0;
|
||||
|
||||
const Trader = function(config) {
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency;
|
||||
this.asset = config.asset;
|
||||
this.pair = config.asset + config.currency;
|
||||
}
|
||||
this.luno = new Luno(this.key, this.secret, {
|
||||
pair: this.pair
|
||||
});
|
||||
this.market = _.find(Trader.getCapabilities().markets, (market) => {
|
||||
return market.pair[0] === this.currency && market.pair[1] === this.asset;
|
||||
});
|
||||
this.interval = 2000;
|
||||
}
|
||||
|
||||
const log = function() {
|
||||
const message = node_util.format.apply(null, _.toArray(arguments));
|
||||
console.log(moment().format('YYYY-MM-DD HH:mm:ss') + ' (DEBUG): ' + message);
|
||||
}
|
||||
|
||||
Trader.prototype.inspect = function(obj) {
|
||||
return node_util.inspect(obj, {
|
||||
showhidden: false,
|
||||
depth: null,
|
||||
breakLength: Infinity,
|
||||
colors: true
|
||||
});
|
||||
};
|
||||
|
||||
const setPrecision = (amount, digits) => {
|
||||
let precision = 100000000;
|
||||
if (Number.isInteger(digits)) precision = Math.pow(10, digits);
|
||||
amount *= precision;
|
||||
amount = Math.floor(amount);
|
||||
amount /= precision;
|
||||
return amount;
|
||||
}
|
||||
|
||||
const round = (value, decimals = 14) => {
|
||||
return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
|
||||
}
|
||||
|
||||
const includes = (str, list) => {
|
||||
if (!_.isString(str))
|
||||
return false;
|
||||
let result = _.some(list, item => str.includes(item));
|
||||
return result;
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND'
|
||||
];
|
||||
|
||||
|
||||
const processResponse = function(funcName, callback) {
|
||||
return (error, body) => {
|
||||
/* BitX Error Codes
|
||||
Error: BitX error 400: invalid id
|
||||
Error: BitX error 401: "error":"API key not found","error_code":"ErrAPIKeyNotFound","error_action":
|
||||
Error: BitX error 429: -- API limit
|
||||
Error: BitX error 500: Something went wrong, we're looking into it.
|
||||
*/
|
||||
|
||||
if (!error && !body) {
|
||||
error = new Error('received empty response');
|
||||
}
|
||||
|
||||
if (error) {
|
||||
log(name, funcName + '() received error -->', error.message);
|
||||
|
||||
if (includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
if (includes(error.message, ['error 429'])) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 3000;
|
||||
}
|
||||
|
||||
if (includes(error.message, ['error 500'])) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 15000;
|
||||
}
|
||||
|
||||
if (includes(error.message, ['Insufficient balance'])) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 2000;
|
||||
tradeAttempt++;
|
||||
if (tradeAttempt >= 3) {
|
||||
error.notFatal = false;
|
||||
log(name, funcName + '() giving up after 3 failed attempts.');
|
||||
}
|
||||
}
|
||||
|
||||
return callback(error, undefined);
|
||||
}
|
||||
|
||||
return callback(undefined, body);
|
||||
}
|
||||
};
|
||||
|
||||
//------- Gekko Functions ---------//
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
// log(name, 'getTicker()');
|
||||
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
const ticker = {
|
||||
ask: data.ask,
|
||||
bid: data.bid,
|
||||
spread: round(data.ask - data.bid)
|
||||
};
|
||||
log(name, 'getTicker() -->', this.inspect(ticker));
|
||||
callback(undefined, ticker);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.getTicker(processResponse('getTicker', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
// log(name, 'getFee()');
|
||||
|
||||
if (this.pair === 'ETHXBT')
|
||||
return callback(undefined, 0.000025);
|
||||
else if (this.pair === 'XBTIDR')
|
||||
return callback(undefined, 0.00002);
|
||||
else
|
||||
return callback(undefined, 0.0001);
|
||||
|
||||
/*
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
// log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
// log(name, 'getFee() --> fee:', data.taker_fee / 100);
|
||||
callback(undefined, data.taker_fee / 100);
|
||||
};
|
||||
const handler = cb => this.luno.getFee(processResponse('getFee', cb));
|
||||
retry(null, handler, process);
|
||||
*/
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
// log(name, 'getPortfolio()');
|
||||
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
const assetProfile = _.find(data.balance, a => a.asset === this.asset);
|
||||
const currencyProfile = _.find(data.balance, a => a.asset === this.currency);
|
||||
let assetAmount = round(assetProfile.balance - assetProfile.reserved);
|
||||
let currencyAmount = round(currencyProfile.balance - currencyProfile.reserved);
|
||||
|
||||
if (!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if (!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [{
|
||||
name: this.asset.toUpperCase(),
|
||||
amount: assetAmount
|
||||
},
|
||||
{
|
||||
name: this.currency.toUpperCase(),
|
||||
amount: currencyAmount
|
||||
}
|
||||
];
|
||||
log(name, 'getPortfolio() --> ' + this.inspect(portfolio));
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.getBalance(processResponse('getPortfolio', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
log(name, 'buy() amount:', amount, 'price:', price);
|
||||
|
||||
tradeAttempt = 0;
|
||||
amount = round(amount);
|
||||
price = round(price);
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'unable to buy:', err.message);
|
||||
return callback(err);
|
||||
}
|
||||
log(name, 'buy() order id: -->', data.order_id);
|
||||
callback(undefined, data.order_id);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.postBuyOrder(amount, price, processResponse('buy', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
log(name, 'sell() amount:', amount, 'price:', price);
|
||||
|
||||
tradeAttempt = 0;
|
||||
amount = round(amount);
|
||||
price = round(price);
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'unable to sell:', err.message);
|
||||
return callback(err);
|
||||
}
|
||||
log(name, 'sell() order id: -->', data.order_id);
|
||||
callback(undefined, data.order_id);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.postSellOrder(amount, price, processResponse('sell', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
amount = setPrecision(amount, this.market.precision);
|
||||
return amount;
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return +price;
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
// log(name, 'getOrder() order id:', order);
|
||||
|
||||
if (!order) {
|
||||
return callback('invalid order_id', false);
|
||||
}
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
const price = parseFloat(data.limit_price);
|
||||
const amount = parseFloat(data.base);
|
||||
let date = moment();
|
||||
const fees = {
|
||||
[this.asset]: +data.fee_base,
|
||||
[this.currency]: +data.fee_counter
|
||||
};
|
||||
const feePercent = round(data.fee_base / data.base * 100, 2);
|
||||
if (data.state === 'PENDING') {
|
||||
date = moment(data.creation_timestamp);
|
||||
} else {
|
||||
date = moment(data.completed_timestamp);
|
||||
}
|
||||
const result = {
|
||||
price,
|
||||
amount,
|
||||
date,
|
||||
fees,
|
||||
feePercent
|
||||
};
|
||||
log(name, 'getOrder() -->', this.inspect(result));
|
||||
callback(undefined, result);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.getOrder(order, processResponse('getOrder', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
// log(name, 'checkOrder() order id:', order);
|
||||
|
||||
if (!order) {
|
||||
return callback('invalid order_id');
|
||||
}
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
const result = {
|
||||
open: data.state === 'PENDING',
|
||||
executed: data.limit_volume === data.base,
|
||||
filledAmount: +data.base,
|
||||
remaining: round(+data.limit_volume - +data.base)
|
||||
}
|
||||
log(name, 'checkOrder()', order, 'result:', this.inspect(result));
|
||||
callback(undefined, result);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.getOrder(order, processResponse('checkOrder', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
log(name, 'cancelOrder() order id:', order);
|
||||
|
||||
if (!order) {
|
||||
return callback('invalid order_id');
|
||||
}
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
if (_.includes(err.message, 'Cannot stop unknown')) {
|
||||
log(name, 'unable to cancel order:', order, '(' + err.message + ') assuming success...');
|
||||
} else {
|
||||
log(name, 'unable to cancel order:', order, '(' + err.message + ') aborting...');
|
||||
return callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (data && !data.success) {
|
||||
log('cancelOrder() --> status:', data.success);
|
||||
return callback(undefined, false);
|
||||
}
|
||||
|
||||
this.checkOrder(order, (error, orderStatus) => {
|
||||
if (error) {
|
||||
log(name, 'cancelOrder\'s checkOrder failed. What do i do here?');
|
||||
return callback(error, false);
|
||||
}
|
||||
|
||||
if (orderStatus.executed) {
|
||||
log(name, 'cancelOrder() -->', order, 'was fulfilled before cancelOrder was completed.');
|
||||
return callback(undefined, true);
|
||||
}
|
||||
const remaining = {
|
||||
remaining: orderStatus.remaining,
|
||||
filled: orderStatus.filledAmount
|
||||
}
|
||||
log(name, 'cancelOrder() --> status: false remaining:', this.inspect(remaining));
|
||||
return callback(undefined, false, remaining);
|
||||
});
|
||||
}
|
||||
|
||||
const handler = cb => this.luno.stopOrder(order, processResponse('cancelOrder', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
// log(name, 'getTrades() since:', since);
|
||||
|
||||
const process = (err, result) => {
|
||||
if (err) {
|
||||
log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
let trades = _.map(result.trades, (t) => {
|
||||
return {
|
||||
price: t.price,
|
||||
date: Math.round(t.timestamp / 1000),
|
||||
amount: t.volume,
|
||||
tid: t.timestamp
|
||||
}
|
||||
});
|
||||
if (!descending) {
|
||||
trades = trades.reverse()
|
||||
}
|
||||
callback(undefined, trades);
|
||||
};
|
||||
|
||||
if (moment.isMoment(since)) since = since.valueOf();
|
||||
(_.isNumber(since) && since > 0) ? since: since = 0;
|
||||
|
||||
const options = {
|
||||
pair: this.pair,
|
||||
since: since
|
||||
}
|
||||
|
||||
const handler = cb => this.luno.getTrades(options, processResponse('getTrades', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function() {
|
||||
return {
|
||||
name: 'Luno',
|
||||
slug: 'luno',
|
||||
currencies: ['MYR', 'KES', 'NGN', 'ZAR', 'XBT'],
|
||||
assets: ['ETH', 'XBT'],
|
||||
markets: [
|
||||
{ pair: ['XBT', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' }, precision: 2 },
|
||||
{ pair: ['MYR', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
|
||||
{ pair: ['KES', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
|
||||
{ pair: ['NGN', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
|
||||
{ pair: ['ZAR', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
providesFullHistory: true,
|
||||
providesHistory: 'date',
|
||||
// maxHistoryFetch: 100,
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
forceReorderDelay: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
Reference in new issue
Block a user