first commit
This commit is contained in:
commit
aa7869eb33
386 files changed
+69375
No files matched your search
@@ -0,0 +1,72 @@
|
||||
// Fake exchanges: used to test purposes to develop Gekko (works without internet).
|
||||
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const TREND_DURATION = 1000;
|
||||
|
||||
const Trader = function() {
|
||||
this.name = 'Exchange Simulator';
|
||||
this.at = moment().subtract(30, 'minutes');
|
||||
|
||||
|
||||
// fake data
|
||||
this.price = 100;
|
||||
this.trend = 'up';
|
||||
this.tid = 0;
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, cb) {
|
||||
const amount = moment().diff(this.at, 'seconds');
|
||||
|
||||
const trades = _.range(amount).map(() => {
|
||||
|
||||
this.tid++;
|
||||
|
||||
if(this.tid % TREND_DURATION === 0) {
|
||||
if(this.trend === 'up')
|
||||
this.trend = 'down';
|
||||
else
|
||||
this.trend = 'up';
|
||||
}
|
||||
|
||||
if(this.trend === 'up')
|
||||
this.price += Math.random();
|
||||
else
|
||||
this.price -= Math.random();
|
||||
|
||||
return {
|
||||
date: this.at.add(1, 'seconds').unix(),
|
||||
price: this.price,
|
||||
amount: Math.random() * 100,
|
||||
tid: this.tid
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[EXCHANGE SIMULATOR] emitted ${amount} fake trades, up until ${this.at.format('YYYY-MM-DD HH:mm:ss')}.`
|
||||
);
|
||||
|
||||
cb(null, trades);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Exchange Simulator',
|
||||
slug: 'DEBUG_exchange-simulator',
|
||||
currencies: ['USD'],
|
||||
assets: ['BTC', 'BTC'],
|
||||
maxTradesAge: 60,
|
||||
maxHistoryFetch: null,
|
||||
markets: [
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 5, unit: 'currency' } },
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
fetchTimespan: 60,
|
||||
tid: 'tid',
|
||||
tradable: false
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,530 @@
|
||||
const moment = require('moment');
|
||||
const _ = require('lodash');
|
||||
|
||||
const Errors = require('../exchangeErrors');
|
||||
const marketData = require('./binance-markets.json');
|
||||
const exchangeUtils = require('../exchangeUtils');
|
||||
const retry = exchangeUtils.retry;
|
||||
const scientificToDecimal = exchangeUtils.scientificToDecimal;
|
||||
|
||||
const Binance = require('binance');
|
||||
|
||||
const Trader = function(config) {
|
||||
_.bindAll(this, [
|
||||
'roundAmount',
|
||||
'roundPrice',
|
||||
'isValidPrice',
|
||||
'isValidLot'
|
||||
]);
|
||||
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency.toUpperCase();
|
||||
this.asset = config.asset.toUpperCase();
|
||||
}
|
||||
|
||||
let recvWindow = 6000;
|
||||
if(config.optimizedConnection) {
|
||||
// there is a bug in binance's API
|
||||
// where some requests randomly take
|
||||
// over a second, this tells binance
|
||||
// to bail out after 500ms.
|
||||
//
|
||||
// As discussed in binance API
|
||||
// telegram. TODO add link.
|
||||
recvWindow = 500;
|
||||
}
|
||||
|
||||
this.pair = this.asset + this.currency;
|
||||
this.name = 'binance';
|
||||
|
||||
this.market = _.find(Trader.getCapabilities().markets, (market) => {
|
||||
return market.pair[0] === this.currency && market.pair[1] === this.asset
|
||||
});
|
||||
|
||||
this.binance = new Binance.BinanceRest({
|
||||
key: this.key,
|
||||
secret: this.secret,
|
||||
timeout: 15000,
|
||||
recvWindow,
|
||||
disableBeautification: false,
|
||||
handleDrift: true,
|
||||
});
|
||||
|
||||
if(config.key && config.secret) {
|
||||
// Note non standard func:
|
||||
//
|
||||
// On binance we might pay fees in BNB
|
||||
// if we do we CANNOT calculate feePercent
|
||||
// since we don't track BNB price (when we
|
||||
// are not trading on a BNB market).
|
||||
//
|
||||
// Though we can deduce feePercent based
|
||||
// on user fee tracked through `this.getFee`.
|
||||
// Set default here, overwrite in getFee.
|
||||
this.fee = 0.001;
|
||||
// Set the proper fee asap.
|
||||
this.getFee(_.noop);
|
||||
|
||||
this.oldOrder = false;
|
||||
}
|
||||
};
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'Error -1021',
|
||||
'Response code 429',
|
||||
'Response code 5',
|
||||
'Response code 403',
|
||||
'ETIMEDOUT',
|
||||
'EHOSTUNREACH',
|
||||
// getaddrinfo EAI_AGAIN api.binance.com api.binance.com:443
|
||||
'EAI_AGAIN',
|
||||
'ENETUNREACH'
|
||||
];
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.handleResponse = function(funcName, callback) {
|
||||
return (error, body) => {
|
||||
if (body && body.code) {
|
||||
error = new Error(`Error ${body.code}: ${body.msg}`);
|
||||
}
|
||||
|
||||
if(error) {
|
||||
if(_.isString(error)) {
|
||||
error = new Error(error);
|
||||
}
|
||||
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
if(funcName === 'cancelOrder' && error.message.includes('UNKNOWN_ORDER')) {
|
||||
console.log(new Date, 'cancelOrder', 'UNKNOWN_ORDER');
|
||||
// order got filled in full before it could be
|
||||
// cancelled, meaning it was NOT cancelled.
|
||||
return callback(false, {filled: true});
|
||||
}
|
||||
|
||||
if(funcName === 'checkOrder' && error.message.includes('Order does not exist.')) {
|
||||
console.log(new Date, 'Binance doesnt know this order, retrying up to 10 times..');
|
||||
error.retry = 10;
|
||||
}
|
||||
|
||||
if(funcName === 'addOrder' && error.message.includes('Account has insufficient balance')) {
|
||||
console.log(new Date, 'insufficientFunds');
|
||||
error.type = 'insufficientFunds';
|
||||
}
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
return callback(undefined, body);
|
||||
}
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const processResults = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var parsedTrades = [];
|
||||
_.each(
|
||||
data,
|
||||
function(trade) {
|
||||
parsedTrades.push({
|
||||
tid: trade.aggTradeId,
|
||||
date: moment(trade.timestamp).unix(),
|
||||
price: parseFloat(trade.price),
|
||||
amount: parseFloat(trade.quantity),
|
||||
});
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
if (descending) callback(null, parsedTrades.reverse());
|
||||
else callback(undefined, parsedTrades);
|
||||
};
|
||||
|
||||
var reqData = {
|
||||
symbol: this.pair,
|
||||
};
|
||||
|
||||
if (since) {
|
||||
var endTs = moment(since)
|
||||
.add(1, 'h')
|
||||
.valueOf();
|
||||
var nowTs = moment().valueOf();
|
||||
|
||||
reqData.startTime = moment(since).valueOf();
|
||||
reqData.endTime = endTs > nowTs ? nowTs : endTs;
|
||||
}
|
||||
|
||||
const fetch = cb => this.binance.aggTrades(reqData, this.handleResponse('getTrades', cb));
|
||||
retry(undefined, fetch, processResults);
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const setBalance = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const findAsset = item => item.asset === this.asset;
|
||||
const assetAmount = parseFloat(_.find(data.balances, findAsset).free);
|
||||
|
||||
const findCurrency = item => item.asset === this.currency;
|
||||
const currencyAmount = parseFloat(_.find(data.balances, findCurrency).free);
|
||||
|
||||
if (!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if (!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount },
|
||||
];
|
||||
|
||||
return callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.binance.account({}, this.handleResponse('getPortfolio', cb));
|
||||
retry(undefined, fetch, setBalance);
|
||||
};
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
|
||||
// binance does NOT tell us whether the user is using BNB to pay
|
||||
// for fees, which means a discount (effectively lower fees)
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
const basepoints = data.makerCommission;
|
||||
|
||||
/** Binance raw response
|
||||
{ makerCommission: 10,
|
||||
takerCommission: 10,
|
||||
buyerCommission: 0,
|
||||
sellerCommission: 0,
|
||||
canTrade: true,
|
||||
canWithdraw: true,
|
||||
canDeposit: true,
|
||||
So to get decimal representation of fee we actually need to divide by 10000
|
||||
*/
|
||||
// note non standard func, see constructor
|
||||
this.fee = basepoints / 10000;
|
||||
|
||||
callback(undefined, this.fee);
|
||||
}
|
||||
|
||||
const fetch = cb => this.binance.account({}, this.handleResponse('getFee', cb));
|
||||
retry(undefined, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const setTicker = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
var result = _.find(data, ticker => ticker.symbol === this.pair);
|
||||
|
||||
if(!result)
|
||||
return callback(new Error(`Market ${this.pair} not found on Binance`));
|
||||
|
||||
var ticker = {
|
||||
ask: parseFloat(result.askPrice),
|
||||
bid: parseFloat(result.bidPrice),
|
||||
};
|
||||
|
||||
callback(undefined, ticker);
|
||||
};
|
||||
|
||||
const handler = cb => this.binance._makeRequest({}, this.handleResponse('getTicker', cb), 'api/v1/ticker/allBookTickers');
|
||||
retry(undefined, handler, setTicker);
|
||||
};
|
||||
|
||||
// Effectively counts the number of decimal places, so 0.001 or 0.234 results in 3
|
||||
Trader.prototype.getPrecision = function(tickSize) {
|
||||
if (!isFinite(tickSize)) return 0;
|
||||
var e = 1, p = 0;
|
||||
while (Math.round(tickSize * e) / e !== tickSize) { e *= 10; p++; }
|
||||
return p;
|
||||
};
|
||||
|
||||
Trader.prototype.round = function(amount, tickSize) {
|
||||
var precision = 100000000;
|
||||
var t = this.getPrecision(tickSize);
|
||||
|
||||
if(Number.isInteger(t))
|
||||
precision = Math.pow(10, t);
|
||||
|
||||
amount *= precision;
|
||||
amount = Math.floor(amount);
|
||||
amount /= precision;
|
||||
|
||||
// https://gist.github.com/jiggzson/b5f489af9ad931e3d186
|
||||
amount = scientificToDecimal(amount);
|
||||
|
||||
return amount;
|
||||
};
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return this.round(amount, this.market.minimalOrder.amount);
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return this.round(price, this.market.minimalOrder.price);
|
||||
}
|
||||
|
||||
Trader.prototype.isValidPrice = function(price) {
|
||||
return price >= this.market.minimalOrder.price;
|
||||
}
|
||||
|
||||
Trader.prototype.isValidLot = function(price, amount) {
|
||||
return amount * price >= this.market.minimalOrder.order;
|
||||
}
|
||||
|
||||
Trader.prototype.outbidPrice = function(price, isUp) {
|
||||
let newPrice;
|
||||
|
||||
if(isUp) {
|
||||
newPrice = price + this.market.minimalOrder.price;
|
||||
} else {
|
||||
newPrice = price - this.market.minimalOrder.price;
|
||||
}
|
||||
|
||||
return this.roundPrice(newPrice);
|
||||
}
|
||||
|
||||
Trader.prototype.addOrder = function(tradeType, amount, price, callback) {
|
||||
const setOrder = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const txid = data.orderId;
|
||||
|
||||
callback(undefined, txid);
|
||||
};
|
||||
|
||||
const reqData = {
|
||||
symbol: this.pair,
|
||||
side: tradeType.toUpperCase(),
|
||||
type: 'LIMIT',
|
||||
timeInForce: 'GTC',
|
||||
quantity: amount,
|
||||
price: price,
|
||||
timestamp: new Date().getTime()
|
||||
};
|
||||
|
||||
const handler = cb => this.binance.newOrder(reqData, this.handleResponse('addOrder', cb));
|
||||
retry(undefined, handler, setOrder);
|
||||
};
|
||||
|
||||
Trader.prototype.getOpenOrders = function(callback) {
|
||||
|
||||
const get = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, data.map(o => o.orderId));
|
||||
}
|
||||
|
||||
const reqData = {
|
||||
symbol: this.pair
|
||||
}
|
||||
|
||||
const handler = cb => this.binance.openOrders(reqData, this.handleResponse('getOpenOrders', cb));
|
||||
retry(undefined, handler, get);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
const get = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
let price = 0;
|
||||
let amount = 0;
|
||||
let date = moment(0);
|
||||
|
||||
const fees = {};
|
||||
|
||||
if(!data.length) {
|
||||
return callback(new Error('Binance did not return any trades'));
|
||||
}
|
||||
|
||||
const trades = _.filter(data, t => {
|
||||
// note: the API returns a string after creating
|
||||
return t.orderId == order;
|
||||
});
|
||||
|
||||
if(!trades.length) {
|
||||
console.log('cannot find trades!', { order, list: data.map(t => t.orderId).reverse() });
|
||||
|
||||
const reqData = {
|
||||
symbol: this.pair,
|
||||
orderId: order,
|
||||
};
|
||||
|
||||
this.binance.queryOrder(reqData, (err, resp) => {
|
||||
console.log('couldnt find any trade for order, here is order:', {err, resp});
|
||||
|
||||
callback(new Error('Trades not found'));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(trades, trade => {
|
||||
date = moment(trade.time);
|
||||
price = ((price * amount) + (+trade.price * trade.qty)) / (+trade.qty + amount);
|
||||
amount += +trade.qty;
|
||||
|
||||
if(fees[trade.commissionAsset])
|
||||
fees[trade.commissionAsset] += (+trade.commission);
|
||||
else
|
||||
fees[trade.commissionAsset] = (+trade.commission);
|
||||
});
|
||||
|
||||
let feePercent;
|
||||
if(_.keys(fees).length === 1) {
|
||||
if(fees.BNB && this.asset !== 'BNB' && this.currency !== 'BNB') {
|
||||
// we paid fees in BNB, right now that means the fee is always 75%
|
||||
// of base fee. We cannot calculate since we do not have the BNB rate.
|
||||
feePercent = this.fee * 0.75;
|
||||
} else {
|
||||
if(fees[this.asset]) {
|
||||
feePercent = fees[this.asset] / amount * 100;
|
||||
} else if(fees.currency) {
|
||||
feePercent = fees[this.currency] / price / amount * 100;
|
||||
} else {
|
||||
// use user fee of 10 basepoints
|
||||
feePercent = this.fee;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we paid fees in multiple currencies?
|
||||
// assume user fee
|
||||
feePercent = this.fee;
|
||||
}
|
||||
|
||||
callback(undefined, { price, amount, date, fees, feePercent });
|
||||
}
|
||||
|
||||
const reqData = {
|
||||
symbol: this.pair,
|
||||
// if this order was not part of the last 500 trades we won't find it..
|
||||
limit: 1000,
|
||||
};
|
||||
|
||||
const handler = cb => this.binance.myTrades(reqData, this.handleResponse('getOrder', cb));
|
||||
retry(undefined, handler, get);
|
||||
};
|
||||
|
||||
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.checkOrder = function(order, callback) {
|
||||
|
||||
const check = (err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(data.filled === true) {
|
||||
// binance responsed with order not found
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
}
|
||||
|
||||
const status = data.status;
|
||||
|
||||
if(
|
||||
status === 'CANCELED' ||
|
||||
status === 'REJECTED' ||
|
||||
// for good measure: GB does not
|
||||
// submit orders that can expire yet
|
||||
status === 'EXPIRED'
|
||||
) {
|
||||
return callback(undefined, { executed: false, open: false });
|
||||
} else if(
|
||||
status === 'NEW' ||
|
||||
status === 'PARTIALLY_FILLED'
|
||||
) {
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: +data.executedQty });
|
||||
} else if(status === 'FILLED') {
|
||||
return callback(undefined, { executed: true, open: false })
|
||||
}
|
||||
|
||||
console.log('what status?', status);
|
||||
throw status;
|
||||
};
|
||||
|
||||
const reqData = {
|
||||
symbol: this.pair,
|
||||
orderId: order,
|
||||
};
|
||||
|
||||
const fetcher = cb => this.binance.queryOrder(reqData, this.handleResponse('checkOrder', cb));
|
||||
retry(undefined, fetcher, check);
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
|
||||
const cancel = (err, data) => {
|
||||
|
||||
this.oldOrder = order;
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(data && data.filled) {
|
||||
return callback(undefined, true);
|
||||
}
|
||||
|
||||
return callback(undefined, false);
|
||||
};
|
||||
|
||||
let reqData = {
|
||||
symbol: this.pair,
|
||||
orderId: order,
|
||||
};
|
||||
|
||||
const fetcher = cb => this.binance.cancelOrder(reqData, this.handleResponse('cancelOrder', cb));
|
||||
retry(undefined, fetcher, cancel);
|
||||
};
|
||||
|
||||
Trader.getCapabilities = function() {
|
||||
return {
|
||||
name: 'Binance',
|
||||
slug: 'binance',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
gekkoBroker: 0.6,
|
||||
limitedCancelConfirmation: true
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,296 @@
|
||||
var Bitcoincoid = require('bitcoin-co-id-update'),
|
||||
_ = require('lodash'),
|
||||
moment = require('moment'),
|
||||
log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
this.currency = config.currency.toLowerCase();
|
||||
this.asset = config.asset.toLowerCase();
|
||||
this.pair = this.asset + '_' + this.currency;
|
||||
}
|
||||
this.name = 'Bitcoin-co-id';
|
||||
this.bitcoincoid = new Bitcoincoid(this.key, this.secret);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
var priceDivider = 100000000; // one hundred million;
|
||||
amount *= priceDivider;
|
||||
amount = Math.floor(amount);
|
||||
amount /= priceDivider;
|
||||
return amount;
|
||||
};
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args); },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = _.toArray(arguments),
|
||||
set = function(err, data) {
|
||||
if(err)
|
||||
return this.retry(this.getTicker, args);
|
||||
var ticker = {
|
||||
ask: this.roundAmount(data.ticker.buy),
|
||||
bid: this.roundAmount(data.ticker.sell),
|
||||
};
|
||||
callback(err, ticker);
|
||||
}.bind(this);
|
||||
this.bitcoincoid.getTicker(this.pair, set);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var functionName = 'Trader.getPortfolio()',
|
||||
args = _.toArray(arguments);
|
||||
set = function(err, data) {
|
||||
if(err)
|
||||
return this.retry(this.getPortfolio, args);
|
||||
var assetAmount = this.roundAmount( data.return.balance[this.asset] ),
|
||||
currencyAmount = this.roundAmount( data.return.balance[this.currency] ),
|
||||
assetHold = this.roundAmount( data.return.balance_hold[this.asset] ),
|
||||
currencyHold = this.roundAmount( data.return.balance_hold[this.currency] );
|
||||
|
||||
if(
|
||||
!_.isNumber(assetAmount) || _.isNaN(assetAmount) ||
|
||||
!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)
|
||||
) {
|
||||
return log.error('account balance error: Gekko is unable to trade with ',this.currency.toUpperCase(),':',currencyAmount,' or ',this.asset.toUpperCase(),':',assetAmount);
|
||||
}
|
||||
var portfolio = [
|
||||
{ name: this.currency.toUpperCase(), amount: currencyAmount - currencyHold},
|
||||
{ name: this.asset.toUpperCase(), amount: assetAmount - assetHold }
|
||||
];
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
this.bitcoincoid.getAccountBalances(set);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
if(since)
|
||||
since = 150; // ???
|
||||
|
||||
var process = function(err, data) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
var trades = _.map(data, function(trade) {
|
||||
return {
|
||||
price: +trade.price,
|
||||
amount: +trade.amount,
|
||||
tid: +trade.tid,
|
||||
date: trade.date
|
||||
};
|
||||
});
|
||||
callback(null, data.reverse());
|
||||
}.bind(this);
|
||||
|
||||
this.bitcoincoid.getTrades(this.pair, process);
|
||||
}
|
||||
|
||||
// bitcoin.co.id: Maker 0% - Taker 0.3%
|
||||
// Note: trading fee: 0.3% for IDR pairs and 0% for BTC pairs
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var fee = 0;
|
||||
if (this.currency.toUpperCase() === 'IDR')
|
||||
{
|
||||
fee = 0.003;
|
||||
}
|
||||
else if (this.currency.toUpperCase() === 'BTC')
|
||||
{
|
||||
fee = 0.000;
|
||||
}
|
||||
|
||||
callback(false, fee);
|
||||
};
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.type = 'buy';
|
||||
|
||||
// decrease purchase amount by 1% to avoid trying to buy more than balance
|
||||
amount -= amount / 100;
|
||||
amount = this.roundAmount(amount);
|
||||
|
||||
// decrease purchase price by 1% less than asking price
|
||||
// price -= price / 100;
|
||||
amount *= price;
|
||||
|
||||
var set = function(err, data) {
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err)
|
||||
return log.error('unable to buy', err);
|
||||
callback(null, data.return.order_id);
|
||||
}.bind(this);
|
||||
this.bitcoincoid.createOrders(
|
||||
this.pair + '',
|
||||
this.type,
|
||||
price,
|
||||
amount,
|
||||
set
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.type = 'sell';
|
||||
|
||||
// increase selling price by 1% more than bidding price
|
||||
// price += price / 100;
|
||||
|
||||
amount = this.roundAmount(amount);
|
||||
var set = function(err, data) {
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err)
|
||||
return log.error('unable to sell', err);
|
||||
callback(null, data.return.order_id);
|
||||
}.bind(this);
|
||||
this.bitcoincoid.createOrders(
|
||||
this.pair + '',
|
||||
this.type,
|
||||
price,
|
||||
amount,
|
||||
set
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
if (order === null) {
|
||||
return callback('no order_id', false);
|
||||
}
|
||||
var check = function(err, data) {
|
||||
if(err){
|
||||
return this.retry(this.checkOrder, arguments);
|
||||
}
|
||||
var status = data.return.order.status;
|
||||
if (status === 'filled') {
|
||||
return callback(err, true);
|
||||
} else if (status === 'open') {
|
||||
return callback(err, false);
|
||||
}
|
||||
callback(err, false);
|
||||
}.bind(this);
|
||||
|
||||
this.bitcoincoid.getOrderDetails(this.pair, order, check);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
if (order === null) {
|
||||
return callback('no order_id', false);
|
||||
}
|
||||
var get = function(data, err) {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
var price = 0;
|
||||
var amount = 0;
|
||||
var date = moment(0);
|
||||
|
||||
if(!data.success)
|
||||
return callback(null, {price, amount, date});
|
||||
|
||||
var result = data.return.order;
|
||||
var orderAmount = result.order+'_'+this.asset;
|
||||
price = result.Price;
|
||||
amount = result.orderAmount;
|
||||
|
||||
if(result.status === 'open') {
|
||||
date = moment(result.submit_time);
|
||||
} else {
|
||||
date = moment(result.finish_time);
|
||||
}
|
||||
callback(err, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.bitcoincoid.getOrderDetails(this.pair, order, get);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var cancel = function(err, data) {
|
||||
if(err) {
|
||||
return log.error('unable to cancel order: ', order, '(', err, '), retrying...');
|
||||
this.retry(this.cancelOrder, args);
|
||||
}
|
||||
callback();
|
||||
};
|
||||
this.bitcoincoid.cancelOrder(this.pair, order, this.type, cancel);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Bitcoin.co.id',
|
||||
slug: 'bitcoin-co-id',
|
||||
currencies: ['IDR', 'BTC'],
|
||||
assets: [
|
||||
"BTC", "BCH", "BTG", "ETH", "ETC", "LTC", "NXT", "WAVES", "STR", "XRP", "XZC", "BTS", "DRK", "DOGE", "NEM", "XZR", "DASH", "XLM", "XEM"
|
||||
],
|
||||
markets: [
|
||||
|
||||
// IDR <-> XXXX
|
||||
|
||||
{ pair: ['IDR', 'BTC'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'BCH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'BTG'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'ETC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'LTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'NXT'], minimalOrder: { amount: 5, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'WAVES'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'STR'], minimalOrder: { amount: 20, unit: 'asset' } }, // Listed as XLM
|
||||
{ pair: ['IDR', 'XRP'], minimalOrder: { amount: 10, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'XZC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
|
||||
|
||||
// BTC <-> XXXX
|
||||
|
||||
{ pair: ['BTC', 'BTS'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'DRK'], minimalOrder: { amount: 0.01, unit: 'asset' } }, // Listed as DASH
|
||||
{ pair: ['BTC', 'DOGE'], minimalOrder: { amount: 1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'NXT'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'STR'], minimalOrder: { amount: 0.01, unit: 'asset' } }, // Listed as XLM
|
||||
{ pair: ['BTC', 'NEM'], minimalOrder: { amount: 1, unit: 'asset' } }, // Listed as XEM
|
||||
{ pair: ['BTC', 'XRP'], minimalOrder: { amount: 0.01, unit: 'asset' } }
|
||||
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesHistory: true,
|
||||
providesFullHistory: false,
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,319 @@
|
||||
|
||||
const Bitfinex = require("bitfinex-api-node");
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const Errors = require('../exchangeErrors');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
|
||||
const marketData = require('./bitfinex-markets.json');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
}
|
||||
this.name = 'Bitfinex';
|
||||
this.balance;
|
||||
this.price;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
this.pair = this.asset + this.currency;
|
||||
this.bitfinex = new Bitfinex.RESTv1({apiKey: this.key, apiSecret: this.secret, transform: true});
|
||||
|
||||
this.interval = 4000;
|
||||
}
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'ESOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'443',
|
||||
'504',
|
||||
'503',
|
||||
'502',
|
||||
'Empty response',
|
||||
'Nonce is too small'
|
||||
];
|
||||
|
||||
Trader.prototype.handleResponse = function(funcName, callback) {
|
||||
return (error, data) => {
|
||||
|
||||
if(!error && _.isEmpty(data)) {
|
||||
error = new Error('Empty response');
|
||||
}
|
||||
|
||||
if(error) {
|
||||
const message = error.message;
|
||||
|
||||
console.log('handleResponse', funcName, message);
|
||||
|
||||
// in case we just cancelled our balances might not have
|
||||
// settled yet, retry.
|
||||
if(
|
||||
funcName === 'submitOrder' &&
|
||||
message.includes('not enough exchange balance')
|
||||
) {
|
||||
error.retry = 20;
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// most likely problem with v1 api
|
||||
if(
|
||||
funcName === 'submitOrder' &&
|
||||
message.includes('Cannot evaluate your available balance, please try again')
|
||||
) {
|
||||
error.retry = 10;
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// in some situations bfx returns 404 on
|
||||
// orders created recently
|
||||
if(
|
||||
funcName === 'checkOrder' &&
|
||||
message.includes('Not Found')
|
||||
) {
|
||||
error.retry = 5;
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if(includes(message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if(includes(message, 'Too Many Requests')) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 5000;
|
||||
}
|
||||
}
|
||||
|
||||
return callback(error, data);
|
||||
}
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// We are only interested in funds in the "exchange" wallet
|
||||
data = data.filter(c => c.type === 'exchange');
|
||||
|
||||
const asset = _.find(data, c => c.currency.toUpperCase() === this.asset);
|
||||
const currency = _.find(data, c => c.currency.toUpperCase() === this.currency);
|
||||
|
||||
let assetAmount, currencyAmount;
|
||||
|
||||
if(_.isObject(asset) && _.isNumber(+asset.available) && !_.isNaN(+asset.available))
|
||||
assetAmount = +asset.available;
|
||||
else {
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(_.isObject(currency) && _.isNumber(+currency.available) && !_.isNaN(+currency.available))
|
||||
currencyAmount = +currency.available;
|
||||
else {
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount },
|
||||
];
|
||||
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.bitfinex.wallet_balances(this.handleResponse('getPortfolio', cb));
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
callback(undefined, {bid: +data.bid, ask: +data.ask});
|
||||
};
|
||||
|
||||
const fetch = cb => this.bitfinex.ticker(this.pair, this.handleResponse('getTicker', cb));
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
const makerFee = 0.1;
|
||||
// const takerFee = 0.2;
|
||||
callback(undefined, makerFee / 100);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return Math.floor(amount*100000000)/100000000;
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
// todo: calc significant digits
|
||||
return price;
|
||||
}
|
||||
|
||||
Trader.prototype.submitOrder = function(type, amount, price, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
callback(null, data.order_id);
|
||||
}
|
||||
|
||||
const fetch = cb => this.bitfinex.new_order(this.pair,
|
||||
amount + '',
|
||||
price + '',
|
||||
this.name.toLowerCase(),
|
||||
type,
|
||||
'exchange limit',
|
||||
this.handleResponse('submitOrder', cb)
|
||||
);
|
||||
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.submitOrder('buy', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.submitOrder('sell', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) {
|
||||
console.log('this is after we have retried fetching it');
|
||||
// this is after we have retried fetching it
|
||||
// in this.handleResponse.
|
||||
if(err.message.includes('Not Found')) {
|
||||
return callback(undefined, {
|
||||
open: false,
|
||||
executed: true
|
||||
});
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(undefined, {
|
||||
open: data.is_live,
|
||||
executed: data.original_amount === data.executed_amount,
|
||||
filledAmount: +data.executed_amount
|
||||
});
|
||||
}
|
||||
|
||||
const fetcher = cb => this.bitfinex.order_status(order_id, this.handleResponse('checkOrder', cb));
|
||||
retry(null, fetcher, processResponse);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var price = parseFloat(data.avg_execution_price);
|
||||
var amount = parseFloat(data.executed_amount);
|
||||
var date = moment.unix(data.timestamp);
|
||||
|
||||
console.log('getOrder', data);
|
||||
|
||||
// TEMP: Thu May 31 14:49:34 CEST 2018
|
||||
// the `past_trades` call is not returning
|
||||
// any data.
|
||||
return callback(undefined, {price, amount, date});
|
||||
|
||||
const processPastTrade = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
console.log('processPastTrade', data);
|
||||
const trade = _.first(data);
|
||||
|
||||
const fees = {
|
||||
[trade.fee_currency]: trade.fee_amount
|
||||
}
|
||||
|
||||
callback(undefined, {price, amount, date, fees});
|
||||
}
|
||||
|
||||
// we need another API call to fetch the fees
|
||||
const feeFetcher = cb => this.bitfinex.past_trades(this.currency, {since: data.timestamp}, this.handleResponse('pastTrades', cb));
|
||||
retry(null, feeFetcher, processPastTrade);
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
};
|
||||
|
||||
const fetcher = cb => this.bitfinex.order_status(order_id, this.handleResponse('getOrder', cb));
|
||||
retry(null, fetcher, processResponse);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.cancelOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(undefined, false);
|
||||
}
|
||||
|
||||
const handler = cb => this.bitfinex.cancel_order(order_id, this.handleResponse('cancelOrder', cb));
|
||||
retry(null, handler, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var trades = _.map(data, function(trade) {
|
||||
return {
|
||||
tid: trade.tid,
|
||||
date: trade.timestamp,
|
||||
price: +trade.price,
|
||||
amount: +trade.amount
|
||||
}
|
||||
});
|
||||
|
||||
callback(undefined, descending ? trades : trades.reverse());
|
||||
};
|
||||
|
||||
var path = this.pair;
|
||||
if(since)
|
||||
path += '?limit_trades=2000';
|
||||
|
||||
const handler = cb => this.bitfinex.trades(path, this.handleResponse('getTrades', cb));
|
||||
retry(null, handler, processResponse);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Bitfinex',
|
||||
slug: 'bitfinex',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesFullHistory: true,
|
||||
providesHistory: 'date',
|
||||
tradable: true,
|
||||
forceReorderDelay: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,246 @@
|
||||
// NOT USED, see: https://github.com/bitfinexcom/bitfinex-api-node/issues/321
|
||||
|
||||
const Bitfinex = require("bitfinex-api-node");
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const Errors = require('../exchangeErrors');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
|
||||
const marketData = require('./bitfinex-markets.json');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
}
|
||||
this.name = 'Bitfinex';
|
||||
this.balance;
|
||||
this.price;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
this.pair = 't' + this.asset + this.currency;
|
||||
this.bitfinex = new Bitfinex.RESTv2({apiKey: this.key, apiSecret: this.secret, transform: true});
|
||||
}
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
// Probably we need to update these string
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'429',
|
||||
'443',
|
||||
'504',
|
||||
'503',
|
||||
'500',
|
||||
'502',
|
||||
'Empty response'
|
||||
];
|
||||
|
||||
Trader.prototype.handleResponse = function(funcName, callback) {
|
||||
return (error, data) => {
|
||||
if(!error && _.isEmpty(data)) {
|
||||
error = new Error('Empty response');
|
||||
}
|
||||
|
||||
if(error) {
|
||||
const message = error.message || error;
|
||||
|
||||
if(!includes(message, recoverableErrors)) {
|
||||
const error = new Error(message);
|
||||
error.notFatal = true;
|
||||
return callback(error);
|
||||
}
|
||||
}
|
||||
|
||||
return callback(error, data);
|
||||
}
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
console.log('processResponse', {err, data});
|
||||
if (err) return callback(err);
|
||||
|
||||
// We are only interested in funds in the "exchange" wallet
|
||||
data = data.filter(c => c.type === 'exchange');
|
||||
|
||||
const asset = _.find(data, c => c.currency.toUpperCase() === this.asset);
|
||||
const currency = _.find(data, c => c.currency.toUpperCase() === this.currency);
|
||||
|
||||
console.log(currency.balance);
|
||||
|
||||
let assetAmount, currencyAmount;
|
||||
|
||||
if(_.isObject(asset) && _.isNumber(+asset.balanceAvailable) && !_.isNaN(+asset.balanceAvailable))
|
||||
assetAmount = +asset.balanceAvailable;
|
||||
else {
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(_.isObject(currency) && _.isNumber(+currency.balanceAvailable) && !_.isNaN(+currency.balanceAvailable))
|
||||
currencyAmount = +currency.balanceAvailable;
|
||||
else {
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount },
|
||||
];
|
||||
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.bitfinex.wallets(this.handleResponse('getPortfolio', cb));
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
callback(undefined, {bid: +data.bid, ask: +data.ask});
|
||||
};
|
||||
|
||||
const fetch = cb => this.bitfinex.ticker(this.pair, this.handleResponse('getTicker', cb));
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
const makerFee = 0.1;
|
||||
// const takerFee = 0.2;
|
||||
callback(undefined, makerFee / 100);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return Math.floor(amount*100000000)/100000000;
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
// todo: calc significant digits
|
||||
return price;
|
||||
}
|
||||
|
||||
Trader.prototype.submit_order = function(type, amount, price, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
callback(err, data.order_id);
|
||||
}
|
||||
|
||||
const fetcher = cb => this.bitfinex.new_order(this.pair,
|
||||
amount + '',
|
||||
price + '',
|
||||
this.name.toLowerCase(),
|
||||
type,
|
||||
'exchange limit',
|
||||
this.handleResponse('submitOrder', cb)
|
||||
);
|
||||
|
||||
retry(retryCritical, fetcher, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.submit_order('buy', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.submit_order('sell', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
return callback(undefined, {
|
||||
open: data.is_live,
|
||||
executed: data.original_amount === data.executed_amount,
|
||||
filled: +data.executed_amount
|
||||
});
|
||||
}
|
||||
|
||||
const fetcher = cb => this.bitfinex.order_status(order_id, this.handleResponse('checkOrder', cb));
|
||||
retry(retryCritical, fetcher, processResponse);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var price = parseFloat(data.avg_execution_price);
|
||||
var amount = parseFloat(data.executed_amount);
|
||||
var date = moment.unix(data.timestamp);
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
};
|
||||
|
||||
const fetcher = cb => this.bitfinex.order_status(order_id, this.handleResponse('getOrder', cb));
|
||||
retry(retryCritical, fetcher, processResponse);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.cancelOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
return callback(undefined);
|
||||
}
|
||||
|
||||
const handler = cb => this.bitfinex.cancel_order(order_id, this.handleResponse('cancelOrder', cb));
|
||||
retry(retryForever, handler, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var trades = _.map(data, function(trade) {
|
||||
return {
|
||||
tid: trade.tid,
|
||||
date: trade.timestamp,
|
||||
price: +trade.price,
|
||||
amount: +trade.amount
|
||||
}
|
||||
});
|
||||
|
||||
callback(undefined, descending ? trades : trades.reverse());
|
||||
};
|
||||
|
||||
var path = this.pair;
|
||||
if(since)
|
||||
path += '?limit_trades=2000';
|
||||
|
||||
const handler = cb => this.bitfinex.trades(path, this.handleResponse('getTrades', cb));
|
||||
retry(retryForever, handler, processResponse);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Bitfinex',
|
||||
slug: 'bitfinex',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesFullHistory: true,
|
||||
providesHistory: 'date',
|
||||
tradable: true,
|
||||
forceReorderDelay: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,261 @@
|
||||
var Bitstamp = require("bitstamp");
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
var util = require('../core/util');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
this.asset = config.asset.toLowerCase();
|
||||
this.currency = config.currency.toLowerCase();
|
||||
this.market = this.asset + this.currency;
|
||||
}
|
||||
this.name = 'Bitstamp';
|
||||
|
||||
this.bitstamp = new Bitstamp(this.key, this.secret, this.clientID);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, data) {
|
||||
|
||||
if(data && data.error) {
|
||||
err = data.error;
|
||||
}
|
||||
|
||||
if(err) {
|
||||
if(err.meta && err.meta.reason === 'API key not found')
|
||||
util.die('Bitstamp says this API keys is invalid..');
|
||||
|
||||
log.error('BITSTAMP API ERROR:', err);
|
||||
return this.retry(this.getPortfolio, args);
|
||||
}
|
||||
|
||||
var portfolio = [];
|
||||
_.each(data, function(amount, asset) {
|
||||
if(asset.indexOf('available') !== -1) {
|
||||
asset = asset.substr(0, 3).toUpperCase();
|
||||
portfolio.push({name: asset, amount: parseFloat(amount)});
|
||||
}
|
||||
});
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
|
||||
this.bitstamp.balance(this.market, set);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
this.bitstamp.ticker(this.market, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
callback(err);
|
||||
|
||||
callback(false, data.fee / 100);
|
||||
}.bind(this);
|
||||
|
||||
this.bitstamp.balance(this.market, set);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
var findMarket = function(pair) {
|
||||
return pair.pair[0].toLowerCase() === this.currency && pair.pair[1].toLowerCase() === this.asset
|
||||
}
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, result) {
|
||||
if(err || result.status === "error") {
|
||||
log.error('unable to buy:', err, result.reason, 'retrying...');
|
||||
return this.retry(this.buy, args);
|
||||
}
|
||||
|
||||
callback(null, result.id);
|
||||
}.bind(this);
|
||||
|
||||
//Decrease amount by 1% to avoid trying to buy more than balance allows.
|
||||
amount = Number(amount * 0.99).toFixed(8);
|
||||
|
||||
// Use proper precision by currency pair
|
||||
var pair = _.find(Trader.getCapabilities().markets, _.bind(findMarket, this));
|
||||
price = Number(Number.parseFloat(price).toFixed(pair.precision));
|
||||
|
||||
this.bitstamp.buy(this.market, amount, price, undefined, set);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
var findMarket = function(pair) {
|
||||
return pair.pair[0].toLowerCase() === this.currency && pair.pair[1].toLowerCase() === this.asset
|
||||
}
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, result) {
|
||||
if(err || result.status === "error") {
|
||||
log.error('unable to sell:', err, result.reason, 'retrying...');
|
||||
return this.retry(this.sell, args);
|
||||
}
|
||||
|
||||
callback(null, result.id);
|
||||
}.bind(this);
|
||||
|
||||
// prevent:
|
||||
// 'Ensure that there are no more than 8 decimal places.'
|
||||
amount = Number(Number.parseFloat(amount)).toFixed(8);
|
||||
|
||||
// Use proper precision by currency pair
|
||||
var pair = _.find(Trader.getCapabilities().markets, _.bind(findMarket, this));
|
||||
price = Number(Number.parseFloat(price).toFixed(pair.precision));
|
||||
|
||||
this.bitstamp.sell(this.market, amount, price, undefined, set);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(id, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var get = function(err, data) {
|
||||
if(!err && _.isEmpty(data) && _.isEmpty(data.result))
|
||||
err = 'no data';
|
||||
|
||||
else if(!err && !_.isEmpty(data.error))
|
||||
err = data.error;
|
||||
|
||||
if(err) {
|
||||
log.error('Unable to get order', order, JSON.stringify(err));
|
||||
return this.retry(this.getOrder, args);
|
||||
}
|
||||
|
||||
var order = _.find(data, o => o.order_id === +id);
|
||||
|
||||
if(!order) {
|
||||
// if the order was cancelled we are unable
|
||||
// to retrieve it, assume that this is what
|
||||
// is happening.
|
||||
return callback(err, {
|
||||
price: 0,
|
||||
amount: 0,
|
||||
date: moment(0)
|
||||
});
|
||||
}
|
||||
|
||||
var price = parseFloat( order[`${this.asset}_${this.currency}`] );
|
||||
var amount = Math.abs(parseFloat( order[this.asset] ));
|
||||
var date = moment( order.datetime );
|
||||
|
||||
callback(err, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.bitstamp.user_transactions(this.market, {}, get);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var check = function(err, result) {
|
||||
var stillThere = _.find(result, function(o) { return o.id === order });
|
||||
callback(err, !stillThere);
|
||||
}.bind(this);
|
||||
|
||||
this.bitstamp.open_orders(this.market, check);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var cancel = function(err, result) {
|
||||
if(err || !result) {
|
||||
log.error('unable to cancel order', order, '(', err, result, ')');
|
||||
return this.retry(this.cancelOrder, args);
|
||||
}
|
||||
|
||||
callback();
|
||||
}.bind(this);
|
||||
|
||||
this.bitstamp.cancel_order(order, cancel);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, trades) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
var result = _.map(trades, t => {
|
||||
return {
|
||||
date: t.date,
|
||||
tid: +t.tid,
|
||||
price: +t.price,
|
||||
amount: +t.amount
|
||||
}
|
||||
})
|
||||
|
||||
callback(null, result.reverse());
|
||||
}.bind(this);
|
||||
|
||||
// NOTE: temporary disabled, see https://github.com/askmike/gekko/issues/794
|
||||
// if(since)
|
||||
// this.bitstamp.transactions(this.market, {time: 'day'}, process);
|
||||
// else
|
||||
this.bitstamp.transactions(this.market, process);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Bitstamp',
|
||||
slug: 'bitstamp',
|
||||
currencies: ['USD', 'EUR', 'BTC'],
|
||||
assets: ['BTC', 'BCH', 'EUR', 'LTC', 'ETH', 'XRP'],
|
||||
maxTradesAge: 60,
|
||||
maxHistoryFetch: null,
|
||||
markets: [
|
||||
{ pair: ['USD', 'EUR'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['EUR', 'BTC'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
|
||||
{ pair: ['USD', 'BCH'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['EUR', 'BCH'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['BTC', 'BCH'], minimalOrder: { amount: 0.001, unit: 'currency'}, precision: 8 },
|
||||
|
||||
{ pair: ['USD', 'XRP'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 5 },
|
||||
{ pair: ['EUR', 'XRP'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 5 },
|
||||
{ pair: ['BTC', 'XRP'], minimalOrder: { amount: 0.001, unit: 'currency'}, precision: 8 },
|
||||
|
||||
{ pair: ['USD', 'LTC'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['EUR', 'LTC'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.001, unit: 'currency'}, precision: 8 },
|
||||
|
||||
{ pair: ['USD', 'ETH'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['EUR', 'ETH'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.001, unit: 'currency'}, precision: 8 },
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
fetchTimespan: 60,
|
||||
tid: 'tid',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,633 @@
|
||||
const Bittrex = require('node.bittrex.api');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
|
||||
// Helper methods
|
||||
function joinCurrencies(currencyA, currencyB){
|
||||
return currencyA + '-' + currencyB;
|
||||
}
|
||||
|
||||
var Trader = function(config) {
|
||||
this.currency = config.currency;
|
||||
this.asset = config.asset;
|
||||
|
||||
if(!config.key) {
|
||||
// no api key defined -> we need to set a
|
||||
// dummy key, otherwise the Bittrex module
|
||||
// will not work even for public requests.
|
||||
this.key = 'dummyApiKey';
|
||||
this.secret = 'dummyApiKey';
|
||||
} else {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
}
|
||||
|
||||
this.name = 'Bittrex';
|
||||
this.balance;
|
||||
this.price;
|
||||
|
||||
this.pair = [this.currency, this.asset].join('-');
|
||||
|
||||
Bittrex.options({
|
||||
apikey: this.key,
|
||||
apisecret: this.secret,
|
||||
stream: false,
|
||||
verbose: false,
|
||||
cleartext: false,
|
||||
inverse_callback_arguments: true
|
||||
});
|
||||
|
||||
this.bittrexApi = Bittrex;
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
];
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.processResponse = function(callback) {
|
||||
return (err, response) => {
|
||||
|
||||
if(err) {
|
||||
if(!_.isError(err)) {
|
||||
err = new Error(err.message);
|
||||
}
|
||||
|
||||
if(err.message === 'APIKEY_INVALID') {
|
||||
console.log('APIKEY_INVALID');
|
||||
err.retry = 10;
|
||||
}
|
||||
|
||||
if(includes(err.message, recoverableErrors)) {
|
||||
err.notFatal = true;
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(undefined, response);
|
||||
}
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
data = data.result;
|
||||
|
||||
let assetEntry = _.find(data, i => i.Currency == this.asset);
|
||||
let currencyEntry = _.find(data, i => i.Currency == this.currency);
|
||||
|
||||
if(_.isUndefined(assetEntry)) {
|
||||
assetEntry = {
|
||||
Available: 0.0,
|
||||
Currency: this.asset
|
||||
}
|
||||
}
|
||||
|
||||
if(_.isUndefined(currencyEntry)) {
|
||||
currencyEntry = {
|
||||
Available: 0.0,
|
||||
Currency: this.currency
|
||||
}
|
||||
}
|
||||
|
||||
const assetAmount = parseFloat( assetEntry.Available );
|
||||
const currencyAmount = parseFloat( currencyEntry.Available );
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
|
||||
callback(undefined, portfolio);
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.getbalances(this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return _.round(price, 8);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(price) {
|
||||
return _.floor(price, 8);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const tick = data.result;
|
||||
|
||||
callback(null, {
|
||||
bid: parseFloat(tick.Bid),
|
||||
ask: parseFloat(tick.Ask),
|
||||
})
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.getticker({market: this.pair}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
callback(false, 0.00025);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
const handle = (err, result) => {
|
||||
console.log('[bittrex buy]', err, result);
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const id = _.get(result, 'result.uuid');
|
||||
if(!id) {
|
||||
console.log('[bittrex buy error]', result);
|
||||
return callback(new Error('Bad response'));
|
||||
}
|
||||
|
||||
callback(undefined, id);
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.buylimit({market: this.pair, quantity: amount, rate: price}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
const handle = (err, result) => {
|
||||
console.log('[bittrex sell]', err, result);
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const id = _.get(result, 'result.uuid');
|
||||
if(!id) {
|
||||
console.log('[bittrex sell error]', result);
|
||||
return callback(new Error('Bad response'));
|
||||
}
|
||||
|
||||
callback(undefined, id);
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.selllimit({market: this.pair, quantity: amount, rate: price}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
const handle = (err, result) => {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
console.log('checkOrder', result);
|
||||
throw 'a';
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.getopenorders({market: this.pair}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
|
||||
const handle = (err, result) => {
|
||||
console.log('getOrder', result);
|
||||
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
let price = 0;
|
||||
let amount = 0;
|
||||
let date = moment(0);
|
||||
|
||||
if(!result.success) {
|
||||
return callback(null, {price, amount, date});
|
||||
}
|
||||
|
||||
const resultOrder = result.result;
|
||||
|
||||
price = resultOrder.Price;
|
||||
amount = resultOrder.Quantity;
|
||||
|
||||
if(resultOrder.IsOpen) {
|
||||
date = moment(resultOrder.Opened);
|
||||
} else {
|
||||
date = moment(resultOrder.Closed);
|
||||
}
|
||||
|
||||
callback(err, {price, amount, date});
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.getorder({uuid: order}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
const handle = (err, result) => {
|
||||
console.log('cancelOrder', err, result);
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
throw 'a';
|
||||
|
||||
// if(!result.success && result.message === 'ORDER_NOT_OPEN') {
|
||||
// log.debug('getOrder', 'ORDER_NOT_OPEN: assuming already closed or executed');
|
||||
// }
|
||||
|
||||
callback(undefined, true);
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.cancel({uuid: order}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {;
|
||||
var firstFetch = !!since;
|
||||
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var result = data.result;
|
||||
|
||||
// Edge case, see here:
|
||||
// @link https://github.com/askmike/gekko/issues/479
|
||||
if(firstFetch && _.size(result) === 50000)
|
||||
util.die(
|
||||
[
|
||||
'Bittrex did not provide enough data. Read this:',
|
||||
'https://github.com/askmike/gekko/issues/479'
|
||||
].join('\n\n')
|
||||
);
|
||||
|
||||
result = _.map(result, function(trade) {
|
||||
var mr = {
|
||||
tid: trade.Id,
|
||||
amount: +trade.Quantity,
|
||||
date: moment.utc(trade.TimeStamp).unix(),
|
||||
timeStamp: trade.TimeStamp,
|
||||
price: +trade.Price
|
||||
};
|
||||
return mr;
|
||||
});
|
||||
|
||||
callback(null, result.reverse());
|
||||
}
|
||||
|
||||
var params = {
|
||||
currencyPair: joinCurrencies(this.currency, this.asset)
|
||||
}
|
||||
|
||||
if(since) {
|
||||
params.start = since.unix();
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.getmarkethistory({ market: params.currencyPair }, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function() {
|
||||
return {
|
||||
name: 'bittrex',
|
||||
slug: 'bittrex',
|
||||
currencies: ['BITCNY', 'BTC', 'ETH', 'USDT'],
|
||||
assets: [
|
||||
'1ST', '2GIVE', '8BIT', 'ABY', 'ADC', 'ADT', 'ADX', 'AEON', 'AGRS', 'AM',
|
||||
'AMP', 'AMS', 'ANS', 'ANT', 'APEX', 'APX', 'ARB', 'ARCH', 'ARDR', 'ARK',
|
||||
'AUR', 'BAT', 'BAY', 'BCC', 'BCY', 'BITB', 'BITCNY', 'BITS', 'BITZ', 'BLC',
|
||||
'BLITZ', 'BLK', 'BLOCK', 'BNT', 'BOB', 'BRK', 'BRX', 'BSD', 'BSTY', 'BTA',
|
||||
'BTC', 'BTCD', 'BTS', 'BURST', 'BYC', 'CANN', 'CCN', 'CFI', 'CLAM',
|
||||
'CLOAK', 'CLUB', 'COVAL', 'CPC', 'CRB', 'CRBIT', 'CRW', 'CRYPT', 'CURE',
|
||||
'CVC', 'DAR', 'DASH', 'DCR', 'DCT', 'DGB', 'DGC', 'DGD', 'DMD', 'DOGE',
|
||||
'DOPE', 'DRACO', 'DTB', 'DTC', 'DYN', 'EBST', 'EDG', 'EFL', 'EGC', 'EMC',
|
||||
'EMC2', 'ENRG', 'ERC', 'ETC', 'ETH', 'EXCL', 'EXP', 'FAIR', 'FC2', 'FCT',
|
||||
'FLDC', 'FLO', 'FRK', 'FSC2', 'FTC', 'FUN', 'GAM', 'GAME', 'GBG', 'GBYTE',
|
||||
'GCR', 'GEMZ', 'GEO', 'GHC', 'GLD', 'GNO', 'GNT', 'GOLOS', 'GP', 'GRC',
|
||||
'GRS', 'GRT', 'GUP', 'HKG', 'HMQ', 'HYPER', 'HZ', 'INCNT', 'INFX', 'IOC',
|
||||
'ION', 'IOP', 'J', 'JWL', 'KMD', 'KORE', 'KR', 'LBC', 'LGD', 'LMC', 'LSK',
|
||||
'LTC', 'LUN', 'LXC', 'MAID', 'MAX', 'MCO', 'MEC', 'MEME', 'METAL', 'MLN',
|
||||
'MND', 'MONA', 'MTL', 'MTR', 'MUE', 'MUSIC', 'MYR', 'MYST', 'MZC', 'NAUT',
|
||||
'NAV', 'NBT', 'NEO', 'NET', 'NEU', 'NLG', 'NMR', 'NTRN', 'NXC', 'NXS',
|
||||
'NXT', 'OC', 'OK', 'OMG', 'OMNI', 'ORB', 'PART', 'PAY', 'PDC', 'PINK',
|
||||
'PIVX', 'PKB', 'POT', 'PPC', 'PRIME', 'PTC', 'PTOY', 'PXI', 'QRL', 'QTL',
|
||||
'QTUM', 'QWARK', 'RADS', 'RBY', 'RDD', 'REP', 'RISE', 'RLC', 'ROOT',
|
||||
'SBD', 'SC', 'SCOT', 'SCRT', 'SDC', 'SEC', 'SEQ', 'SFR', 'SHIFT', 'SIB',
|
||||
'SLG', 'SLING', 'SLR', 'SLS', 'SNGLS', 'SNRG', 'SNT', 'SOON', 'SPHR',
|
||||
'SPR', 'SPRTS', 'SSD', 'START', 'STEEM', 'STEPS', 'STORJ', 'STRAT', 'STV',
|
||||
'SWIFT', 'SWING', 'SWT', 'SYNX', 'SYS', 'TES', 'THC', 'TIME', 'TIT',
|
||||
'TKN', 'TKS', 'TRI', 'TRIG', 'TRK', 'TROLL', 'TRST', 'TRUST', 'TX', 'U',
|
||||
'UBQ', 'UFO', 'UNB', 'UNIQ', 'UNIT', 'UNO', 'USDT', 'UTC', 'VIA', 'VIOR',
|
||||
'VIRAL', 'VOX', 'VPN', 'VRC', 'VRM', 'VTC', 'VTR', 'WARP', 'WAVES', 'WBB',
|
||||
'WINGS', 'XAUR', 'XBB', 'XC', 'XCO', 'XCP', 'XDN', 'XDQ', 'XEL', 'XEM',
|
||||
'XLM', 'XMG', 'XMR', 'XPY', 'XQN', 'XRP', 'XSEED', 'XST', 'XTC', 'XVC',
|
||||
'XVG', 'XWC', 'XZC', 'YBC', 'ZCL', 'ZEC', 'ZEN','BTG','OMG','ADA'
|
||||
],
|
||||
markets: [
|
||||
|
||||
// *** BTCNY <-> XXX
|
||||
{ pair: ['BITCNY','BTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
|
||||
// *** BTC <-> XXX
|
||||
{ pair: ['BTC','1ST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','2GIVE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ABY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ADT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ADX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','AEON'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','AGRS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','AMP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ANT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','APX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ARDR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ARK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','AUR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BAT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BAY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BCC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BCY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BITB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BLITZ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BLK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BLOCK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BRK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BRX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BSD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BTA'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BTCD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BTS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BURST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BYC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CANN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CFI'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CLAM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CLOAK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CLUB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','COVAL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CPC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CRB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CRW'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CURE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CVC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DAR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DASH'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DCR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DCT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DGB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DGD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DMD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DOGE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DOPE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DRACO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DTB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DYN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EBST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EDG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EFL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EGC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EMC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EMC2'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ENRG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ERC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ETC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ETH'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EXCL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EXP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FAIR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FCT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FLDC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FLO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FUN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GAM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GAME'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GBG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GBYTE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GCR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GEO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GLD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GNO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GOLOS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GRC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GRS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GUP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','HKG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','HMQ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','INCNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','INFX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','IOC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ION'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','IOP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','KMD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','KORE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LBC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LGD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LMC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LSK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LUN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MAID'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MCO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MEME'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MLN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MONA'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MTL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MUE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MUSIC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MYST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NAUT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NAV'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NBT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NEO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NLG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NMR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NXC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NXS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NXT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','OK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','OMG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','OMNI'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PART'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PAY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PDC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PINK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PIVX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PKB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','POT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PPC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PTOY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','QRL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','QTUM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','QWARK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','RADS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','RBY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','RDD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','REP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','RISE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','RLC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SAFEX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SBD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SEQ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SHIFT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SIB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SLR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SLS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SNGLS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SNRG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SPHR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SPR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','START'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','STEEM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','STORJ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','STRAT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SWIFT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SWT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SYNX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SYS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','THC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TIME'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TKN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TKS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TRIG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TRST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TRUST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','UBQ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','UNB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','UNO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VIA'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VOX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VRC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VRM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VTR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','WAVES'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','WINGS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XAUR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XBB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XCP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XDN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XEL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XEM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XLM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XMG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XMR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XMY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XRP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XVC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XVG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XWC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XZC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ZCL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ZEC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ZEN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
|
||||
// *** ETH <-> XXX
|
||||
{ pair: ['ETH','1ST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','ADT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','ADX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','ANT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','BAT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','BCC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','BNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','BTS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','CFI'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','CRB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','CVC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','DASH'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','DGB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','DGD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','ETC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','FCT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','FUN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','GNO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','GNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','GUP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','HMQ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','LGD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','LTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','LUN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','MCO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','MTL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','MYST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','NEO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','NMR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','OMG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','PAY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','PTOY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','QRL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','QTUM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','REP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','RLC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','SC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','SNGLS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','SNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','STORJ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','STRAT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','TIME'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','TKN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','TRST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','WAVES'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','WINGS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','XEM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','XLM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','XMR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','XRP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','ZEC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
|
||||
// *** USDT <-> XXX
|
||||
{ pair: ['USDT','BCC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','BTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','DASH'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','ETC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','ETH'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','LTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','NEO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','XMR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','XRP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','ZEC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','BTG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','OMG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','ADA'], minimalOrder: { amount: 0.00000001, unit: 'asset' }}
|
||||
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: false,
|
||||
tradable: false,
|
||||
forceReorderDelay: true,
|
||||
gekkoBroker: '0.6.0'
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,249 @@
|
||||
var BTCMarkets = require('btc-markets');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
this.currency = config.currency;
|
||||
this.asset = config.asset;
|
||||
}
|
||||
this.name = 'BTC Markets';
|
||||
this.priceDivider = 100000000; // one hundred million
|
||||
this.btcmakets = new BTCMarkets(this.key, this.secret);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var set = function(err, data) {
|
||||
|
||||
if(!_.isEmpty(data.errorMessage))
|
||||
return callback('BTC-MARKET API ERROR: ' + data.errorMessage);
|
||||
|
||||
var portfolio = _.map(data, function(balance) {
|
||||
return {
|
||||
name: balance.currency,
|
||||
amount: balance.balance / this.priceDivider
|
||||
}
|
||||
}, this);
|
||||
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
|
||||
this.btcmakets.getAccountBalances(set);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTicker, args);
|
||||
|
||||
callback(null, {
|
||||
bid: result.bestBid,
|
||||
ask: result.bestAsk
|
||||
});
|
||||
}.bind(this);
|
||||
this.btcmakets.getTick(this.asset, this.currency, set);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, data) {
|
||||
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err){
|
||||
log.error('unable to retrieve fee', err, ' retrying...');
|
||||
return this.retry(this.getFee, args);
|
||||
}
|
||||
data.tradingFeeRate /= this.priceDivider;
|
||||
callback(false, data.tradingFeeRate);
|
||||
}.bind(this);
|
||||
this.btcmakets.getTradingFee(this.asset, this.currency, set);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
|
||||
price *= this.priceDivider;
|
||||
amount = Math.floor(amount * this.priceDivider);
|
||||
var id = Math.random() + '';
|
||||
var set = function(err, data) {
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err)
|
||||
return log.error('unable to buy', err);
|
||||
callback(null, data.id);
|
||||
}.bind(this);
|
||||
this.btcmakets.createOrder(
|
||||
this.asset,
|
||||
this.currency,
|
||||
price,
|
||||
amount,
|
||||
'Bid',
|
||||
'Limit',
|
||||
id,
|
||||
set
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
price *= this.priceDivider;
|
||||
amount = Math.floor(amount * this.priceDivider);
|
||||
var id = Math.random() + ''
|
||||
var set = function(err, data) {
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err)
|
||||
return log.error('unable to sell', err)
|
||||
callback(null, data.id);
|
||||
}.bind(this);
|
||||
this.btcmakets.createOrder(
|
||||
this.asset,
|
||||
this.currency,
|
||||
price,
|
||||
amount,
|
||||
'Ask',
|
||||
'Limit',
|
||||
id,
|
||||
set
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
if (order == null) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
var check = function(err, data) {
|
||||
if(!err && _.isEmpty(data.orders))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err){
|
||||
return log.error('unable to check order: ', order, '(', err ,'), retrying...');
|
||||
this.retry(this.checkOrder, args);
|
||||
}
|
||||
var placed = !_.isEmpty(data.orders)
|
||||
callback(err, !placed);
|
||||
}.bind(this);
|
||||
|
||||
this.btcmakets.getOpenOrders(this.asset, this.currency, 10, null, check);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var get = function(err, data) {
|
||||
|
||||
if(!err && _.isEmpty(data.orders))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err){
|
||||
return log.error('unable to get order detail: ', order, '(', err ,'), retrying...');
|
||||
this.retry(this.getOrder, args);
|
||||
}
|
||||
var price = parseFloat(data.orders[0].price);
|
||||
var amount = parseFloat(data.orders[0].volumn);
|
||||
var date = moment.unix(data.orders[0].creationDate);
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.btcmakets.getOrderDetail([order], callback);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var get = function(err, data) {
|
||||
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err){
|
||||
return log.error('unable to cancel order: ',order, '(', err, '), retrying...');
|
||||
this.retry(this.cancelOrder, args);
|
||||
}
|
||||
callback();
|
||||
};
|
||||
this.btcmakets.cancelOrders([order], callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
callback(null, result.reverse());
|
||||
}.bind(this);
|
||||
|
||||
// supports `since` based on trade ID, Gekko can't work this atm..
|
||||
this.btcmakets.getTrades(this.asset, this.currency, process);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'BTC Markets',
|
||||
slug: 'btc-markets',
|
||||
currencies: ['AUD', 'BTC'],
|
||||
assets: [
|
||||
'BTC', 'LTC', 'ETH', 'ETC', 'BCH', 'XRP'
|
||||
],
|
||||
markets: [
|
||||
{ pair: ['AUD', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['AUD', 'LTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['AUD', 'ETH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['AUD', 'ETC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['AUD', 'BCH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['AUD', 'XRP'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'ETC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'BCH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'XRP'], minimalOrder: { amount: 0.001, unit: 'asset' } }
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesHistory: 'scan',
|
||||
providesFullHistory: false,
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,176 @@
|
||||
var BTCChina = require('btc-china-fork');
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
}
|
||||
this.name = 'BTCC';
|
||||
|
||||
this.pair = (config.asset + config.currency).toUpperCase();
|
||||
|
||||
this.btcc = new BTCChina(this.key, this.secret, this.clientID);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTicker, args);
|
||||
|
||||
callback(null, {
|
||||
bid: result.bids[0][0],
|
||||
ask: result.asks[0][0]
|
||||
});
|
||||
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.getOrderBook(process, this.pair, 1);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
if(descending)
|
||||
callback(null, result.reverse());
|
||||
else
|
||||
callback(null, result);
|
||||
}.bind(this);
|
||||
|
||||
|
||||
if(!since)
|
||||
since = 500;
|
||||
else
|
||||
since = 5000;
|
||||
|
||||
this.btcc.getHistoryData(process, {limit: since});
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
return this.retry(this.getPortfolio, args);
|
||||
|
||||
var portfolio = [];
|
||||
_.each(data.result.balance, function(obj) {
|
||||
portfolio.push({name: obj.currency, amount: parseFloat(obj.amount)});
|
||||
});
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.getAccountInfo(set, 'ALL');
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
this.retry(this.getFee, args);
|
||||
|
||||
callback(false, data.result.profile.trade_fee / 100);
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.getAccountInfo(set, 'ALL');
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
// TODO: do somewhere better..
|
||||
amount = Math.floor(amount * 10000) / 10000;
|
||||
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return log.error('unable to buy:', err, result);
|
||||
|
||||
callback(null, result.result);
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.createOrder2(set, 'buy', price, amount, this.pair);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
// TODO: do somewhere better..
|
||||
amount = Math.round(amount * 10000) / 10000;
|
||||
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return log.error('unable to sell:', err, result);
|
||||
|
||||
callback(null, result.result);
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.createOrder2(set, 'sell', price, amount, this.pair);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var check = function(err, result) {
|
||||
if(err)
|
||||
this.retry(this.checkOrder, args);
|
||||
|
||||
var done = result.result.order.status === 'closed';
|
||||
callback(err, done);
|
||||
};
|
||||
|
||||
this.btcc.getOrder(check, order, this.pair, true);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var cancel = function(err, result) {
|
||||
if(err)
|
||||
log.error('unable to cancel order', order, '(', err, result, ')');
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.cancelOrder(cancel, order, this.pair);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'BTCC',
|
||||
slug: 'btcc',
|
||||
currencies: ['BTC', 'CNY'],
|
||||
assets: ['BTC', 'LTC'],
|
||||
markets: [
|
||||
{ pair: ['CNY', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['CNY', 'LTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.001, unit: 'asset' } }
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesFullHistory: true,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,89 @@
|
||||
var BitexthaiAPI = require('bitexthai');
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
}
|
||||
this.name = 'BX.in.th';
|
||||
|
||||
this.pair = 1; // todo
|
||||
|
||||
this.bitexthai = new BitexthaiAPI(this.key, this.secret, this.clientID);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
var parsedTrades = [];
|
||||
_.each(result.trades, function(trade) {
|
||||
// We get trade time in local time, which is GMT+7
|
||||
var date = moment(trade.trade_date).subtract(7, 'hours').unix();
|
||||
|
||||
parsedTrades.push({
|
||||
date: date,
|
||||
price: parseFloat(trade.rate),
|
||||
amount: parseFloat(trade.amount),
|
||||
tid: trade.trade_id
|
||||
});
|
||||
}, this);
|
||||
|
||||
if(descending)
|
||||
callback(null, parsedTrades.reverse());
|
||||
else
|
||||
callback(null, parsedTrades);
|
||||
}.bind(this);
|
||||
|
||||
this.bitexthai.trades(this.pair, process);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'BX.in.th',
|
||||
slug: 'bx.in.th',
|
||||
currencies: ['THB'],
|
||||
assets: ['BTC'],
|
||||
markets: [
|
||||
{
|
||||
pair: ['THB', 'BTC'], minimalOrder: { amount: 0.0001, unit: 'asset' },
|
||||
}
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tradeError: 'NOT IMPLEMENTED YET',
|
||||
providesHistory: false
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,211 @@
|
||||
var CEXio = require('cexio'),
|
||||
moment = require('moment'),
|
||||
async = require('async'),
|
||||
_ = require('lodash'),
|
||||
util = require('../core/util'),
|
||||
log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
this.user = config.username;
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency.toUpperCase();
|
||||
this.asset = config.asset.toUpperCase();
|
||||
this.pair = this.asset + '_' + this.currency;
|
||||
this.name = 'cex.io';
|
||||
|
||||
this.cexio = new CEXio(
|
||||
this.pair,
|
||||
this.user,
|
||||
this.key,
|
||||
this.secret
|
||||
);
|
||||
|
||||
_.bindAll(this);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, trades) {
|
||||
if(err || !trades || trades.length === 0)
|
||||
return this.retry(this.getTrades, args, err);
|
||||
|
||||
if(descending)
|
||||
callback(null, trades);
|
||||
else
|
||||
callback(null, trades.reverse());
|
||||
}
|
||||
|
||||
this.cexio.trades({}, _.bind(process, this));
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
// Prevent "You incorrectly entered one of fields."
|
||||
// because of more than 8 decimals.
|
||||
amount *= 100000000;
|
||||
amount = Math.floor(amount);
|
||||
amount /= 100000000;
|
||||
|
||||
log.debug('BUY', amount, this.asset, '@', price, this.currency);
|
||||
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
return log.error('unable to buy:', err);
|
||||
if(data.error)
|
||||
return log.error('unable to buy:', data.error);
|
||||
|
||||
log.debug('BUY order placed. Order ID', data.id);
|
||||
callback(null, data.id);
|
||||
};
|
||||
|
||||
this.cexio.place_order('buy', amount, price, _.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
// Prevent "You incorrectly entered one of fields."
|
||||
// because of more than 8 decimals.
|
||||
amount *= 100000000;
|
||||
amount = Math.floor(amount);
|
||||
amount /= 100000000;
|
||||
|
||||
// test placing orders which will not be filled
|
||||
//price *= 10; price = price.toFixed(8);
|
||||
|
||||
log.debug('SELL', amount, this.asset, '@', price, this.currency);
|
||||
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
return log.error('unable to sell:', err);
|
||||
if(data.error)
|
||||
return log.error('unable to sell:', data.error);
|
||||
|
||||
log.debug('SELL order placed. Order ID', data.id);
|
||||
callback(null, data.id);
|
||||
};
|
||||
|
||||
this.cexio.place_order('sell', amount, price, _.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.retry = function(method, args, err) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..', err, 'waiting for', wait, 'ms');
|
||||
|
||||
if (!_.isFunction(method)) {
|
||||
log.error(this.name, 'failed to retry, no method supplied.');
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var calculate = function(err, data) {
|
||||
if(err)
|
||||
return this.retry(this.getPortfolio, args, err);
|
||||
|
||||
currency = parseFloat(data[this.currency].available)
|
||||
if(parseFloat(data[this.currency].orders)){
|
||||
currency -= parseFloat(data[this.currency].orders)
|
||||
}
|
||||
assets = parseFloat(data[this.asset].available);
|
||||
if( parseFloat(data[this.asset].orders)){
|
||||
assets -= parseFloat(data[this.asset].orders);
|
||||
}
|
||||
|
||||
var portfolio = [];
|
||||
portfolio.push({name: this.currency, amount: currency});
|
||||
portfolio.push({name: this.asset, amount: assets});
|
||||
callback(err, portfolio);
|
||||
}
|
||||
this.cexio.balance(calculate.bind(this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var set = function(err, data) {
|
||||
var ticker = {
|
||||
ask: data.ask,
|
||||
bid: data.bid
|
||||
};
|
||||
callback(err, ticker);
|
||||
}
|
||||
this.cexio.ticker(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
// cexio does currently don't take a fee on trades
|
||||
// TODO: isn't there an API call for this?
|
||||
callback(false, 0.002);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var check = function(err, result) {
|
||||
|
||||
if(err)
|
||||
return callback(false, true);
|
||||
if(result.error)
|
||||
return callback(false, true);
|
||||
|
||||
var exists = false;
|
||||
_.each(result, function(entry) {
|
||||
if(entry.id === order) {
|
||||
exists = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
callback(err, !exists);
|
||||
};
|
||||
|
||||
this.cexio.open_orders(_.bind(check, this));
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order) {
|
||||
var check= function(err, result) {
|
||||
if(err)
|
||||
log.error('cancel order failed:', err);
|
||||
if(typeof(result) !== 'undefined' && result.error)
|
||||
log.error('cancel order failed:', result.error);
|
||||
}
|
||||
this.cexio.cancel_order(order, check);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'CEX.io',
|
||||
slug: 'cexio',
|
||||
currencies: ['BTC', 'USD', 'EUR', 'RUB'],
|
||||
assets: ['GHS', 'BTC', 'ETH', 'LTC'],
|
||||
markets: [
|
||||
{ pair: ['BTC', 'GHS'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['EUR', 'LTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['USD', 'LTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['RUB', 'BTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['EUR', 'BTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['USD', 'ETH'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['EUR', 'ETH'], minimalOrder: { amount: 0.000001, unit: 'currency' } }
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
providesHistory: false,
|
||||
tid: 'tid'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
{
|
||||
"assets": [
|
||||
"ETC",
|
||||
"BTC",
|
||||
"BCH",
|
||||
"ETH",
|
||||
"LTC",
|
||||
"ZRX"
|
||||
],
|
||||
"currencies": [
|
||||
"EUR",
|
||||
"USD",
|
||||
"BTC",
|
||||
"GBP"
|
||||
],
|
||||
"markets": [
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"ETC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.00100000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.00100000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GBP",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.00100000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"ETC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ETC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"ZRX"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "1.00000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ZRX"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "1.00000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"ZRX"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "1.00000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GBP",
|
||||
"ETC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GBP",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GBP",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GBP",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"assets": [
|
||||
"IOT",
|
||||
"ETH",
|
||||
"LTC",
|
||||
"BTC",
|
||||
"NANO",
|
||||
"BCH",
|
||||
"KIN",
|
||||
"TRX",
|
||||
"CRPT",
|
||||
"GRLC",
|
||||
"ECA",
|
||||
"XRP",
|
||||
"ADA",
|
||||
"DOGE",
|
||||
"STQ",
|
||||
"DASC",
|
||||
"EOS",
|
||||
"DTX"
|
||||
],
|
||||
"currencies": [
|
||||
"BTC",
|
||||
"EUR",
|
||||
"ETH",
|
||||
"USDT"
|
||||
],
|
||||
"markets": [
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"IOT"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 0.00001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 0.00001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"NANO"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 0.00001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"KIN"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1,
|
||||
"price": 1e-10,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"ETH",
|
||||
"KIN"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"TRX"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"CRPT"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"GRLC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ECA"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"ETH",
|
||||
"ECA"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"XRP"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ADA"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.000001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"ETH",
|
||||
"ADA"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.000001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"DOGE"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.00001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"STQ"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"IOT"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.00001,
|
||||
"price": 0.0001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"DASC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.00001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"ETH",
|
||||
"DASC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.00001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"DASC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.00001,
|
||||
"price": 0.0001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"NANO"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.0001,
|
||||
"price": 0.0001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USDT",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USDT",
|
||||
"EOS"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.0001,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"DTX"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
const moment = require('moment');
|
||||
const _ = require('lodash');
|
||||
const marketData = require('./coinfalcon-markets.json');
|
||||
|
||||
const CoinFalcon = require('coinfalcon');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this, [
|
||||
'roundAmount',
|
||||
'roundPrice'
|
||||
]);
|
||||
|
||||
this.makerFee = 0;
|
||||
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency.toUpperCase();
|
||||
this.asset = config.asset.toUpperCase();
|
||||
}
|
||||
|
||||
this.pair = this.asset + '-' + this.currency;
|
||||
this.name = 'coinfalcon';
|
||||
|
||||
this.market = _.find(Trader.getCapabilities().markets, (market) => {
|
||||
return market.pair[0] === this.currency && market.pair[1] === this.asset
|
||||
});
|
||||
|
||||
this.coinfalcon = new CoinFalcon.Client(this.key, this.secret);
|
||||
|
||||
this.interval = 1500;
|
||||
};
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'ECONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'429',
|
||||
'522',
|
||||
'429',
|
||||
'504',
|
||||
'503',
|
||||
'500',
|
||||
'502',
|
||||
'408',
|
||||
// "The timestamp 1527996378 is invalid, current timestamp is 1527996441."
|
||||
'is invalid, current timestamp is',
|
||||
'EHOSTUNREACH',
|
||||
// https://github.com/askmike/gekko/issues/2407
|
||||
'We are fixing a few issues, be back shortly.',
|
||||
'Client network socket disconnected before secure TLS connection was established',
|
||||
'socket hang up',
|
||||
// getaddrinfo EAI_AGAIN coinfalcon.com coinfalcon.com:443
|
||||
'EAI_AGAIN'
|
||||
];
|
||||
|
||||
// errors that might mean
|
||||
// the API call succeeded.
|
||||
const unknownResultErrors = [
|
||||
'524',
|
||||
]
|
||||
|
||||
Trader.prototype.processResponse = function(method, args, next) {
|
||||
|
||||
const requestAt = moment();
|
||||
|
||||
const checkTime = () => {
|
||||
const diff = moment().diff(requestAt, 's');
|
||||
if(diff > 10) {
|
||||
console.log(new Date, '[CF] API CALL TOOK', diff, 'SECONDS!');
|
||||
console.log(new Date, '[CF]', {method, args, next});
|
||||
}
|
||||
}
|
||||
|
||||
const catcher = err => {
|
||||
if(!err || !err.message) {
|
||||
err = new Error(err || 'Empty error');
|
||||
}
|
||||
|
||||
if(includes(err.message, recoverableErrors)) {
|
||||
return this.retry(method, args);
|
||||
}
|
||||
|
||||
console.log(new Date, '[cf] big error!', err.message, method);
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return {
|
||||
failure: catcher,
|
||||
success: data => {
|
||||
if(!data)
|
||||
return catcher();
|
||||
|
||||
if(data.error)
|
||||
return catcher(data.error);
|
||||
|
||||
if(includes(data, ['Please complete the security check to proceed.']))
|
||||
return next(new Error(
|
||||
'Your IP has been flagged by CloudFlare. ' +
|
||||
'As such Gekko Broker cannot access Coinfalcon.'
|
||||
));
|
||||
|
||||
next(undefined, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(1, 'seconds');
|
||||
|
||||
// run the failed method again with the same arguments after wait
|
||||
setTimeout(() => {
|
||||
method.apply(this, args);
|
||||
}, wait);
|
||||
};
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const handle = this.processResponse(this.getTicker, [callback], (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
callback(null, {bid: +res.data.bids[0].price, ask: +res.data.asks[0].price})
|
||||
});
|
||||
|
||||
var url = "markets/" + this.pair + "/orders?level=1"
|
||||
|
||||
this.coinfalcon.get(url).then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
callback(false, this.makerFee / 100);
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const handle = this.processResponse(this.getPortfolio, [callback], (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
var portfolio = res.data.map(account => ({
|
||||
name: account.currency_code.toUpperCase(),
|
||||
amount: parseFloat(account.available_balance)
|
||||
}));
|
||||
|
||||
callback(null, portfolio);
|
||||
});
|
||||
|
||||
this.coinfalcon.get('user/accounts').then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.getOpenOrders = function(callback) {
|
||||
const handle = this.processResponse(this.getOpenOrders, [callback], (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
console.log(new Date, 'CF getOpenOrders', res.data);
|
||||
|
||||
callback(null, res.data.map(o => o.id));
|
||||
});
|
||||
|
||||
this.coinfalcon.get('user/orders').then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.addOrder = function(type, amount, price, callback) {
|
||||
const args = _.toArray(arguments);
|
||||
|
||||
const handle = this.processResponse(this.addOrder, args, (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
if(!res.data.id) {
|
||||
console.log(new Date, 'CF ERROR! CREATED ORDER BUT NO ID', res);
|
||||
}
|
||||
|
||||
callback(false, res.data.id);
|
||||
});
|
||||
|
||||
const payload = {
|
||||
order_type: type,
|
||||
operation_type: 'limit_order',
|
||||
market: this.pair,
|
||||
size: amount + '',
|
||||
price: price + ''
|
||||
}
|
||||
|
||||
this.coinfalcon.post('user/orders', payload).then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
['buy', 'sell'].map(function(type) {
|
||||
Trader.prototype[type] = function(amount, price, callback) {
|
||||
this.addOrder(type, amount, price, callback);
|
||||
};
|
||||
});
|
||||
|
||||
const round = function(number, precision) {
|
||||
var factor = Math.pow(10, precision);
|
||||
var tempNumber = number * factor;
|
||||
var roundedTempNumber = Math.round(tempNumber);
|
||||
return roundedTempNumber / factor;
|
||||
};
|
||||
|
||||
// ticksize 0.01 will yield: 2
|
||||
Trader.prototype.getPrecision = function(tickSize) {
|
||||
if (!isFinite(tickSize)) {
|
||||
return 0;
|
||||
}
|
||||
var e = 1;
|
||||
var p = 0;
|
||||
while (Math.round(tickSize * e) / e !== tickSize) {
|
||||
e *= 10; p++;
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return round(amount, this.getPrecision(this.market.minimalOrder.amount));
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return round(price, this.getPrecision(this.market.minimalOrder.price));
|
||||
}
|
||||
|
||||
Trader.prototype.outbidPrice = function(price, isUp) {
|
||||
let newPrice;
|
||||
|
||||
if(isUp) {
|
||||
newPrice = price + this.market.minimalOrder.price;
|
||||
} else {
|
||||
newPrice = price - this.market.minimalOrder.price;
|
||||
}
|
||||
|
||||
return this.roundPrice(newPrice);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
const args = _.toArray(arguments);
|
||||
const handle = this.processResponse(this.getOrder, args, (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
const price = parseFloat(res.data.price);
|
||||
const amount = parseFloat(res.data.size_filled);
|
||||
const date = moment(res.data.created_at);
|
||||
const fees = {};
|
||||
const feePercent = this.makerFee;
|
||||
callback(false, { price, amount, date, fees, feePercent });
|
||||
});
|
||||
|
||||
this.coinfalcon.get('user/orders/' + order).then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
const args = _.toArray(arguments);
|
||||
|
||||
const handle = this.processResponse(this.checkOrder, args, (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
// https://docs.coinfalcon.com/#list-orders
|
||||
const status = res.data.status;
|
||||
|
||||
if(status === 'canceled') {
|
||||
return callback(undefined, { executed: false, open: false });
|
||||
} if(status === 'fulfilled') {
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
} if(
|
||||
status === 'pending' ||
|
||||
status === 'partially_filled' ||
|
||||
status === 'open'
|
||||
) {
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: +res.data.size_filled });
|
||||
}
|
||||
|
||||
console.error(res.data);
|
||||
callback(new Error('Unknown status ' + status));
|
||||
});
|
||||
|
||||
this.coinfalcon.get('user/orders/' + order).then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
const args = _.toArray(arguments);
|
||||
|
||||
const handle = this.processResponse(this.cancelOrder, args, (err, res) => {
|
||||
|
||||
if(err) {
|
||||
if(err.message.includes('has wrong status.')) {
|
||||
|
||||
// see https://github.com/askmike/gekko/issues/2440
|
||||
return setTimeout(() => {
|
||||
this.checkOrder(order, (err, res) => {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(!res.open) {
|
||||
return callback(undefined, true);
|
||||
}
|
||||
|
||||
return setTimeout(
|
||||
() => this.cancelOrder(order, callback),
|
||||
this.interval
|
||||
);
|
||||
});
|
||||
}, this.interval);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(undefined, false, {
|
||||
filled: +res.data.size_filled
|
||||
});
|
||||
});
|
||||
|
||||
this.coinfalcon.delete('user/orders/' + order).then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
var success = function(res) {
|
||||
var parsedTrades = [];
|
||||
_.each(
|
||||
res.data,
|
||||
function(trade) {
|
||||
parsedTrades.push({
|
||||
tid: trade.id,
|
||||
date: moment(trade.created_at).unix(),
|
||||
price: parseFloat(trade.price),
|
||||
amount: parseFloat(trade.size),
|
||||
});
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
if (descending) {
|
||||
callback(null, parsedTrades);
|
||||
} else {
|
||||
callback(null, parsedTrades.reverse());
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
var failure = function (err) {
|
||||
err = new Error(err);
|
||||
return this.retry(this.getTrades, args, err);
|
||||
}.bind(this);
|
||||
|
||||
var url = "markets/" + this.pair + "/trades"
|
||||
|
||||
if (since) {
|
||||
url += '?since_time=' + (_.isString(since) ? since : since.format());
|
||||
}
|
||||
|
||||
this.coinfalcon.get(url).then(success).catch(failure);
|
||||
};
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'CoinFalcon',
|
||||
slug: 'coinfalcon',
|
||||
assets: marketData.assets,
|
||||
currencies: marketData.currencies,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
forceReorderDelay: false,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
Executable
+283
@@ -0,0 +1,283 @@
|
||||
var Coingi = require('coingi');
|
||||
var moment = require('moment');
|
||||
var _ = require('lodash');
|
||||
|
||||
var util = require('../core/util');
|
||||
var Errors = require('../core/error');
|
||||
var log = require('../core/log');
|
||||
|
||||
|
||||
var 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.pair = this.asset + "-" + this.currency;
|
||||
this.name = 'coingi';
|
||||
this.since = null;
|
||||
|
||||
this.coingi = new Coingi(
|
||||
this.key,
|
||||
this.secret,
|
||||
{timeout: +moment.duration(60, 'seconds')}
|
||||
);
|
||||
}
|
||||
|
||||
var retryCritical = {
|
||||
retries: 10,
|
||||
factor: 1.2,
|
||||
minTimeout: 1 * 1000,
|
||||
maxTimeout: 30 * 1000
|
||||
};
|
||||
|
||||
var retryForever = {
|
||||
forever: true,
|
||||
factor: 1.2,
|
||||
minTimeout: 10,
|
||||
maxTimeout: 30
|
||||
};
|
||||
|
||||
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)/)
|
||||
|
||||
Trader.prototype.processError = function (funcName, error) {
|
||||
if (!error)
|
||||
return undefined;
|
||||
|
||||
if (!error.message.match(recoverableErrors)) {
|
||||
log.error(`[coingi.js] (${funcName}) returned an irrecoverable error: ${error.message}`);
|
||||
return new Errors.AbortError('[coingi.js] ' + error.message);
|
||||
}
|
||||
|
||||
log.debug(`[coingi.js] (${funcName}) returned an error, retrying: ${error.message}`);
|
||||
return new Errors.RetryError('[coingi.js] ' + error.message);
|
||||
};
|
||||
|
||||
Trader.prototype.handleResponse = function (funcName, callback) {
|
||||
return (error, body) => {
|
||||
if (!error) {
|
||||
if (_.isEmpty(body))
|
||||
error = new Error('NO DATA WAS RETURNED');
|
||||
|
||||
else if (!_.isEmpty(body.error))
|
||||
error = new Error(body.error);
|
||||
}
|
||||
|
||||
return callback(this.processError(funcName, error), body);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/* PORTFOLIO MANAGER */
|
||||
|
||||
Trader.prototype.getTicker = function (callback) {
|
||||
var setTicker = function (err, data) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
var ticker = {
|
||||
ask: data.asks[0].price,
|
||||
bid: data.bids[0].price
|
||||
};
|
||||
callback(undefined, ticker);
|
||||
};
|
||||
|
||||
let handler = (cb) => this.coingi.api('order-book', "/" + this.pair + "/1/1/1", this.handleResponse('getTicker', cb));
|
||||
util.retryCustom(retryForever, _.bind(handler, this), _.bind(setTicker, this));
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getTrades = function (since, callback, ascending) {
|
||||
var startTs = since ? moment(since).valueOf() : null;
|
||||
|
||||
var processResults = function (err, trades) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var parsedTrades = [];
|
||||
_.each(trades, 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(trade.timestamp).valueOf()) {
|
||||
parsedTrades.push({
|
||||
type: trade.type === 0 ? "sell" : "buy",
|
||||
date: moment(trade.timestamp).unix(),
|
||||
amount: String(trade.amount),
|
||||
price: String(trade.price),
|
||||
tid: trade.timestamp
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
|
||||
if(ascending)
|
||||
callback(undefined, parsedTrades);
|
||||
else
|
||||
callback(undefined, parsedTrades.reverse());
|
||||
};
|
||||
|
||||
var optionalSince = "";
|
||||
if (since) {
|
||||
optionalSince = "/" + startTs;
|
||||
}
|
||||
|
||||
let handler = (cb) => this.coingi.api('transactions', "/" + this.pair + "/512" + optionalSince, this.handleResponse('getTrades', cb));
|
||||
util.retryCustom(retryForever, _.bind(handler, this), _.bind(processResults, this));
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function (callback) {
|
||||
var setBalance = function (err, data) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
log.debug('[coingi.js] entering "setBalance" callback after coingi-api call, data:', data);
|
||||
|
||||
const portfolio = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
portfolio.push({
|
||||
name: data[i].currency.name.toUpperCase(),
|
||||
amount: data[i].available
|
||||
});
|
||||
}
|
||||
|
||||
return callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
let handler = (cb) => this.coingi.api('balance', {currencies: this.asset + "," + this.currency}, this.handleResponse('getPortfolio', cb));
|
||||
util.retryCustom(retryForever, _.bind(handler, this), _.bind(setBalance, this));
|
||||
};
|
||||
|
||||
// Base fee is 0.2%
|
||||
Trader.prototype.getFee = function (callback) {
|
||||
const fee = 0.2;
|
||||
callback(undefined, fee / 100);
|
||||
};
|
||||
|
||||
Trader.prototype.addOrder = function (tradeType, amount, price, callback) {
|
||||
log.debug('[coingi.js] (add-order)', tradeType.toUpperCase(), amount, this.asset, '@', price, this.currency);
|
||||
|
||||
var setOrder = function (err, data) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
var uuid = data.result;
|
||||
log.debug('[coingi.js] (addOrder) added order with uuid:', uuid);
|
||||
|
||||
callback(undefined, uuid);
|
||||
};
|
||||
|
||||
let reqData = {
|
||||
currencyPair: this.pair,
|
||||
type: tradeType.toLowerCase() === "sell" ? 0 : 1,
|
||||
price: price,
|
||||
volume: amount.toString()
|
||||
};
|
||||
|
||||
let handler = (cb) => this.coingi.api('add-order', reqData, this.handleResponse('addOrder', cb));
|
||||
util.retryCustom(retryCritical, _.bind(handler, this), _.bind(setOrder, this));
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function (orderId, callback) {
|
||||
var getOrder = function (err, order) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
if (order !== null) {
|
||||
const price = parseFloat(order.price);
|
||||
const amount = parseFloat(order.baseAmount);
|
||||
const date = moment.unix(order.timestamp);
|
||||
callback(undefined, {price, amount, date});
|
||||
} else {
|
||||
log.error("Error! Order ID '" + orderId + "' couldn't be found in the result of get order!");
|
||||
}
|
||||
};
|
||||
|
||||
let reqData = {ordeId: orderId};
|
||||
let handler = (cb) => this.coingi.api('get-order', reqData, this.handleResponse('getOrder', cb));
|
||||
util.retryCustom(retryCritical, _.bind(handler, this), _.bind(getOrder, this));
|
||||
}
|
||||
|
||||
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.checkOrder = function (orderId, callback) {
|
||||
var check = function (err, order) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
if (order === null) {
|
||||
log.error("Error! Order ID '" + orderId + "' couldn't be found in the result of get order!");
|
||||
}
|
||||
// returns true when the order has been filled (processed in full), false otherwise
|
||||
callback(undefined, order !== null && order.status === 2);
|
||||
};
|
||||
|
||||
let reqData = {orderId: orderId};
|
||||
let handler = (cb) => this.coingi.api('get-order', reqData, this.handleResponse('checkOrder', cb));
|
||||
util.retryCustom(retryCritical, _.bind(handler, this), _.bind(check, this));
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function (order, callback) {
|
||||
let reqData = {orderId: order};
|
||||
let handler = (cb) => this.coingi.api('cancel-order', reqData, this.handleResponse('cancelOrder', cb));
|
||||
util.retryCustom(retryForever, _.bind(handler, this), callback);
|
||||
};
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Coingi',
|
||||
slug: 'coingi',
|
||||
currencies: ['EUR', 'USD', 'BTC'],
|
||||
assets: ['BTC', 'DASH', 'DOGE', 'EUR', 'LTC', 'NMC', 'PPC', 'VTC'],
|
||||
markets: [
|
||||
// Tradeable against BTC
|
||||
{pair: ['USD', 'BTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['EUR', 'BTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against DASH
|
||||
{pair: ['BTC', 'DASH'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against DOGE
|
||||
{pair: ['BTC', 'DOGE'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['USD', 'DOGE'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against EUR
|
||||
{pair: ['USD', 'EUR'], minimalOrder: {amount: 0.01, unit: 'asset'}, precision: 2},
|
||||
|
||||
// Tradeable against LTC
|
||||
{pair: ['BTC', 'LTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['EUR', 'LTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['USD', 'LTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against NMC
|
||||
{pair: ['BTC', 'NMC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against PPC
|
||||
{pair: ['USD', 'PPC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['EUR', 'PPC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['BTC', 'PPC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against VTC
|
||||
{pair: ['BTC', 'VTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8}
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
exchangeMaxHistoryAge: 30,
|
||||
tid: 'date',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"assets": [
|
||||
"BTC",
|
||||
"ETH"
|
||||
],
|
||||
"currencies": [
|
||||
"EUR",
|
||||
"USD",
|
||||
"LTC"
|
||||
],
|
||||
"markets": [
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.001",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.001",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"LTC",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01",
|
||||
"unit": "asset"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
|
||||
|
||||
const CryptoJS = require("crypto-js");
|
||||
const querystring = require('querystring');
|
||||
const request = require('request');
|
||||
|
||||
API_URL='https://api.exmo.com/v1/';
|
||||
|
||||
const marketData = require('./exmo-markets.json');
|
||||
|
||||
|
||||
const Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
this.key="";
|
||||
this.secret="";
|
||||
|
||||
if(_.isObject(config)) {
|
||||
if(_.isString(config.key)) this.key = config.key;
|
||||
if(_.isString(config.secret)) this.secret = config.secret;
|
||||
this.currency = config.currency;
|
||||
this.asset = config.asset;
|
||||
this.pair = this.asset + '_' + this.currency;
|
||||
};
|
||||
|
||||
this.name = 'EXMO';
|
||||
this.nonce = new Date() * 1000;
|
||||
}
|
||||
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'EHOSTUNREACH',
|
||||
];
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.api_query = function(method, params, callback){
|
||||
params.nonce = this.nonce++;
|
||||
var post_data = querystring.stringify(params);
|
||||
|
||||
var options = {
|
||||
url: API_URL + method,
|
||||
headers: {'Key': this.key,'Sign': CryptoJS.HmacSHA512(post_data, this.secret).toString(CryptoJS.enc.hex) },
|
||||
form: params
|
||||
};
|
||||
|
||||
request.post(options, function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
data=JSON.parse(body);
|
||||
if(data.error) error = { message: data.error }
|
||||
else if (data.result!=undefined && data.result==false) error = { message: '"result": false' } ;
|
||||
callback(error, data);
|
||||
} else {
|
||||
console.log('cb request error');
|
||||
console.log(body);
|
||||
|
||||
if(error) {
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
console.log(error);
|
||||
callback(error);
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const processResponse = (error, data) => {
|
||||
if (error) return callback(error);
|
||||
|
||||
data=data[this.pair];
|
||||
|
||||
var parsedTrades = _.map(data, function(trade) {
|
||||
return {
|
||||
tid: trade.trade_id,
|
||||
date: trade.date,
|
||||
price: +trade.price,
|
||||
amount: +trade.quantity
|
||||
};
|
||||
});
|
||||
|
||||
if (descending) callback(undefined, parsedTrades);
|
||||
else callback(undefined, parsedTrades.reverse());
|
||||
};
|
||||
|
||||
const fetch = cb => this.api_query("trades", { pair: this.pair} , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
data=data[this.pair];
|
||||
|
||||
callback(undefined, {bid: +data.sell_price, ask: +data.buy_price});
|
||||
};
|
||||
|
||||
const fetch = cb => this.api_query("ticker", { pair: this.pair} , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
callback(undefined, 0.002);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return _.floor(amount, 8);
|
||||
};
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return price;
|
||||
}
|
||||
|
||||
Trader.prototype.submitOrder = function(type, amount, price, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
callback(null, data.order_id);
|
||||
}
|
||||
|
||||
const fetch = cb => this.api_query("order_create", { pair: this.pair, quantity: amount, price: price, type: type } , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
//callback(null, 1177669837);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.submitOrder('buy', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.submitOrder('sell', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
data=data[this.pair];
|
||||
|
||||
if(data==undefined) return callback(new Error('Orders not found'));
|
||||
|
||||
const order = _.find(data, function(o) { return o.order_id === +order_id });
|
||||
|
||||
if(!order) return callback(new Error('Order not found'));
|
||||
|
||||
callback(undefined, { price: order.price, amount: order.amount, date: moment.unix(order.date) });
|
||||
}
|
||||
|
||||
const fetch = cb => this.api_query("user_trades", { pair: this.pair} , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
data=data[this.pair];
|
||||
|
||||
if(data==undefined) {
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
}
|
||||
|
||||
const order = _.find(data, function(o) { return o.order_id === +order_id });
|
||||
if(!order)
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
|
||||
callback(undefined, { executed: false, open: true /*, filledAmount: order.startingAmount - order.amount*/ });
|
||||
}
|
||||
|
||||
const fetch = cb => this.api_query("user_open_orders", { } , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(undefined, false);
|
||||
}
|
||||
|
||||
const fetch = cb => this.api_query("order_cancel", { order_id } , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
data=data["balances"];
|
||||
const balances = _.map(data, function (v,k){ return {name: k, amount: +v};});
|
||||
const portfolio = balances.filter(c => c.name===this.asset || c.name===this.currency)
|
||||
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.api_query("user_info", {}, cb);
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'EXMO',
|
||||
slug: 'exmo',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: false,
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,430 @@
|
||||
const Gdax = require('gdax');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const errors = require('../exchangeErrors');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
|
||||
const BATCH_SIZE = 100;
|
||||
const QUERY_DELAY = 350;
|
||||
|
||||
const marketData = require('./coinbase-markets.json');
|
||||
|
||||
const Trader = function(config) {
|
||||
this.post_only = true;
|
||||
this.use_sandbox = false;
|
||||
this.name = 'GDAX';
|
||||
this.scanback = false;
|
||||
this.scanbackTid = 0;
|
||||
this.scanbackResults = [];
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
|
||||
this.api_url = 'https://api.pro.coinbase.com';
|
||||
this.api_sandbox_url = 'https://api-public.sandbox.pro.coinbase.com';
|
||||
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.passphrase = config.passphrase;
|
||||
|
||||
this.pair = [config.asset, config.currency].join('-').toUpperCase();
|
||||
this.post_only =
|
||||
typeof config.post_only !== 'undefined' ? config.post_only : true;
|
||||
|
||||
if (config.sandbox) {
|
||||
this.use_sandbox = config.sandbox;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.gdax_public = new Gdax.PublicClient(
|
||||
this.use_sandbox ? this.api_sandbox_url : undefined
|
||||
);
|
||||
this.gdax = new Gdax.AuthenticatedClient(
|
||||
this.key,
|
||||
this.secret,
|
||||
this.passphrase,
|
||||
this.use_sandbox ? this.api_sandbox_url : undefined
|
||||
);
|
||||
};
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'Rate limit exceeded',
|
||||
'Response code 5',
|
||||
'GDAX is currently under maintenance.',
|
||||
'HTTP 408 Error',
|
||||
'HTTP 504 Error',
|
||||
'HTTP 503 Error',
|
||||
'socket hang up',
|
||||
'EHOSTUNREACH',
|
||||
'EAI_AGAIN',
|
||||
'ENETUNREACH'
|
||||
];
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.processResponse = function(method, next) {
|
||||
return (error, response, body) => {
|
||||
if(!error && body && !_.isEmpty(body.message)) {
|
||||
error = new Error(body.message);
|
||||
}
|
||||
|
||||
if(
|
||||
response &&
|
||||
response.statusCode < 200 &&
|
||||
response.statusCode >= 300
|
||||
) {
|
||||
error = new Error(`Response code ${response.statusCode}`);
|
||||
}
|
||||
|
||||
if(error) {
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 1000;
|
||||
}
|
||||
|
||||
if(
|
||||
['buy', 'sell'].includes(method) &&
|
||||
error.message.includes('Insufficient funds')
|
||||
) {
|
||||
error.retry = 10;
|
||||
}
|
||||
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return next(undefined, body);
|
||||
}
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var portfolio = data.map(function(account) {
|
||||
return {
|
||||
name: account.currency.toUpperCase(),
|
||||
amount: parseFloat(account.available),
|
||||
};
|
||||
});
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.getAccounts(this.processResponse('getPortfolio', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
callback(undefined, { bid: +data.bid, ask: +data.ask });
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax_public.getProductTicker(this.pair, this.processResponse('getTicker', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
//https://www.gdax.com/fees
|
||||
// const fee = this.asset == 'BTC' ? 0.0025 : 0.003;
|
||||
const fee = 0;
|
||||
|
||||
//There is no maker fee, not sure if we need taker fee here
|
||||
//If post only is enabled, gdax only does maker trades which are free
|
||||
callback(undefined, this.post_only ? 0 : fee);
|
||||
};
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return this.getMaxDecimalsNumber(amount);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
const buyParams = {
|
||||
price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
|
||||
size: this.getMaxDecimalsNumber(amount),
|
||||
product_id: this.pair,
|
||||
post_only: this.post_only,
|
||||
};
|
||||
|
||||
const result = (err, data) => {
|
||||
if (err) {
|
||||
console.log({buyParams}, err.message);
|
||||
return callback(err);
|
||||
}
|
||||
callback(undefined, data.id);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.buy(buyParams, this.processResponse('buy', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
const sellParams = {
|
||||
price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
|
||||
size: this.getMaxDecimalsNumber(amount),
|
||||
product_id: this.pair,
|
||||
post_only: this.post_only,
|
||||
};
|
||||
|
||||
const result = (err, data) => {
|
||||
if (err) {
|
||||
console.log({sellParams}, err.message);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(data.message && data.message.includes('Insufficient funds')) {
|
||||
err = new Error(data.message);
|
||||
err.retryOnce = true;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(undefined, data.id);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.sell(sellParams, this.processResponse('sell', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// @link:
|
||||
// https://stackoverflow.com/questions/48132078/available-gdax-order-statuses-and-meanings
|
||||
var status = data.status;
|
||||
if(status == 'pending') {
|
||||
// technically not open yet, but will be soon
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: 0 });
|
||||
} if (status === 'done' || status === 'settled') {
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
} else if (status === 'rejected') {
|
||||
return callback(undefined, { executed: false, open: false });
|
||||
} else if(status === 'open' || status === 'active') {
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.filled_size) });
|
||||
}
|
||||
|
||||
callback(new Error('Unknown status ' + status));
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.getOrder(order, this.processResponse('checkOrder', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const price = parseFloat(data.price);
|
||||
const amount = parseFloat(data.filled_size);
|
||||
const date = moment(data.done_at);
|
||||
const fees = {
|
||||
// you always pay fee in the base currency on gdax
|
||||
[this.currency]: +data.fill_fees
|
||||
}
|
||||
const feePercent = +data.fill_fees / price / amount * 100;
|
||||
|
||||
callback(undefined, { price, amount, date, fees, feePercent });
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.getOrder(order, this.processResponse('getOrder', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
// callback for cancelOrder should be true if the order was already filled, otherwise false
|
||||
const result = (err, data) => {
|
||||
if(err) {
|
||||
return callback(null, true); // need to catch the specific error but usually an error on cancel means it was filled
|
||||
}
|
||||
|
||||
return callback(null, false);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.cancelOrder(order, this.processResponse('cancelOrder', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var lastScan = 0;
|
||||
|
||||
const handle = function(err, data) {
|
||||
if (err) return callback(err);
|
||||
|
||||
var result = _.map(data, function(trade) {
|
||||
return {
|
||||
tid: trade.trade_id,
|
||||
amount: parseFloat(trade.size),
|
||||
date: moment.utc(trade.time).format('X'),
|
||||
price: parseFloat(trade.price),
|
||||
};
|
||||
});
|
||||
|
||||
if (this.scanback) {
|
||||
var last = _.last(data);
|
||||
var first = _.first(data);
|
||||
|
||||
// Try to find trade id matching the since date
|
||||
if (!this.scanbackTid) {
|
||||
// either scan for new ones or we found it.
|
||||
if (moment.utc(last.time) < moment.utc(since)) {
|
||||
this.scanbackTid = last.trade_id;
|
||||
} else {
|
||||
console.log('Scanning backwards...' + last.time);
|
||||
setTimeout(() => {
|
||||
let handler = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{
|
||||
after: last.trade_id - BATCH_SIZE * lastScan,
|
||||
limit: BATCH_SIZE,
|
||||
},
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(
|
||||
retryForever,
|
||||
_.bind(handler, this),
|
||||
_.bind(process, this)
|
||||
);
|
||||
}, QUERY_DELAY);
|
||||
lastScan++;
|
||||
if (lastScan > 100) {
|
||||
lastScan = 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.scanbackTid) {
|
||||
// if scanbackTid is set we need to move forward again
|
||||
console.log(
|
||||
'Backwards: ' +
|
||||
last.time +
|
||||
' (' +
|
||||
last.trade_id +
|
||||
') to ' +
|
||||
first.time +
|
||||
' (' +
|
||||
first.trade_id +
|
||||
')'
|
||||
);
|
||||
|
||||
this.scanbackResults = this.scanbackResults.concat(result.reverse());
|
||||
|
||||
if (this.scanbackTid != first.trade_id) {
|
||||
this.scanbackTid = first.trade_id;
|
||||
setTimeout(() => {
|
||||
let handler = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{ after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(
|
||||
retryForever,
|
||||
_.bind(handler, this),
|
||||
_.bind(process, this)
|
||||
);
|
||||
}, QUERY_DELAY);
|
||||
} else {
|
||||
this.scanback = false;
|
||||
this.scanbackTid = 0;
|
||||
|
||||
console.log('Scan finished: data found:' + this.scanbackResults.length);
|
||||
callback(null, this.scanbackResults);
|
||||
|
||||
this.scanbackResults = [];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
callback(null, result.reverse());
|
||||
}
|
||||
};
|
||||
|
||||
if (since || this.scanback) {
|
||||
this.scanback = true;
|
||||
if (this.scanbackTid) {
|
||||
let handler = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{ after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(
|
||||
retryForever,
|
||||
_.bind(handler, this),
|
||||
_.bind(process, this)
|
||||
);
|
||||
} else if (since) {
|
||||
console.log('Scanning back in the history needed...', since);
|
||||
}
|
||||
}
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{ limit: BATCH_SIZE },
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.getMaxDecimalsNumber = function(number, decimalLimit = 8) {
|
||||
var decimalNumber = parseFloat(number);
|
||||
|
||||
// The ^-?\d*\. strips off any sign, integer portion, and decimal point
|
||||
// leaving only the decimal fraction.
|
||||
// The 0+$ strips off any trailing zeroes.
|
||||
var decimalCount = (+decimalNumber).toString().replace(/^-?\d*\.?|0+$/g, '')
|
||||
.length;
|
||||
|
||||
var decimalMultiplier = 1;
|
||||
for (i = 0; i < decimalLimit; i++) {
|
||||
decimalMultiplier *= 10;
|
||||
}
|
||||
|
||||
return decimalCount <= decimalLimit
|
||||
? decimalNumber.toString()
|
||||
: (
|
||||
Math.floor(decimalNumber * decimalMultiplier) / decimalMultiplier
|
||||
).toFixed(decimalLimit);
|
||||
};
|
||||
|
||||
Trader.getCapabilities = function() {
|
||||
return {
|
||||
name: 'GDAX',
|
||||
slug: 'gdax',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret', 'passphrase'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
forceReorderDelay: false,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,228 @@
|
||||
var Gemini = require('gemini-exchange-coffee-api/lib/gemini');
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
}
|
||||
this.name = 'Gemini';
|
||||
this.balance;
|
||||
this.price;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
this.pair = this.asset + this.currency;
|
||||
this.gemini = new Gemini(this.key, this.secret);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
this.gemini.wallet_balances(function (err, data, body) {
|
||||
|
||||
if(err && err.message === '401') {
|
||||
let e = 'Gemini replied with an unauthorized error. ';
|
||||
e += 'Double check whether your API key is correct.';
|
||||
util.die(e);
|
||||
}
|
||||
|
||||
if(err || !data)
|
||||
return this.retry(this.getPortfolio, args);
|
||||
|
||||
// We are only interested in funds in the "exchange" wallet
|
||||
data = data.filter(c => c.type === 'exchange');
|
||||
|
||||
const asset = _.find(data, c => c.currency.toUpperCase() === this.asset);
|
||||
const currency = _.find(data, c => c.currency.toUpperCase() === this.currency);
|
||||
|
||||
let assetAmount, currencyAmount;
|
||||
|
||||
if(_.isObject(asset) && _.isNumber(+asset.available) && !_.isNaN(+asset.available))
|
||||
assetAmount = +asset.available;
|
||||
else {
|
||||
log.error(`Gemini did not provide ${this.asset} amount, assuming 0`);
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(_.isObject(currency) && _.isNumber(+currency.available) && !_.isNaN(+currency.available))
|
||||
currencyAmount = +currency.available;
|
||||
else {
|
||||
log.error(`Gemini did not provide ${this.currency} amount, assuming 0`);
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount },
|
||||
];
|
||||
|
||||
callback(err, portfolio);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
// the function that will handle the API callback
|
||||
var process = function(err, data, body) {
|
||||
if (err)
|
||||
return this.retry(this.getTicker(args));
|
||||
|
||||
// whenever we reach this point we have valid
|
||||
// data, the callback is still the same since
|
||||
// we are inside the same javascript scope.
|
||||
callback(err, {bid: +data.bid, ask: +data.ask})
|
||||
}.bind(this);
|
||||
this.gemini.ticker(this.pair, process);
|
||||
}
|
||||
|
||||
// This assumes that only limit orders are being placed, so fees are the
|
||||
// "maker fee" of 0.1%. It does not take into account volume discounts.
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var makerFee = 0.25;
|
||||
callback(false, makerFee / 100);
|
||||
}
|
||||
|
||||
Trader.prototype.submit_order = function(type, amount, price, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
amount = Math.floor(amount*100000000)/100000000;
|
||||
this.gemini.new_order(
|
||||
this.pair,
|
||||
amount + '',
|
||||
price + '',
|
||||
this.name.toLowerCase(),
|
||||
type,
|
||||
'exchange limit',
|
||||
function (err, data, body) {
|
||||
if (err) {
|
||||
log.error('unable to ' + type, err, body);
|
||||
return this.retry(this.submit_order, args);
|
||||
}
|
||||
|
||||
callback(err, data.order_id);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.submit_order('buy', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.submit_order('sell', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order_id, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
this.gemini.order_status(order_id, function (err, data, body) {
|
||||
|
||||
if(err || !data)
|
||||
return this.retry(this.checkOrder, arguments);
|
||||
|
||||
callback(err, !data.is_live);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var get = function(err, data) {
|
||||
if(err || !data)
|
||||
return this.retry(this.getOrder, arguments);
|
||||
|
||||
var price = parseFloat(data.avg_execution_price);
|
||||
var amount = parseFloat(data.executed_amount);
|
||||
var date = moment.unix(data.timestamp);
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.gemini.order_status(order, get);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.cancelOrder = function(order_id, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
this.gemini.cancel_order(order_id, function (err, data, body) {
|
||||
if (err || !data) {
|
||||
// gemini way of telling it was already cancelled..
|
||||
if(err.message === 'Order could not be cancelled.')
|
||||
return callback();
|
||||
|
||||
log.error('unable to cancel order', order_id, '(', err, data, '), retrying...');
|
||||
return this.retry(this.cancelOrder, args);
|
||||
}
|
||||
|
||||
return callback();
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
var path = this.pair;
|
||||
if(since)
|
||||
path += '?limit_trades=2000';
|
||||
|
||||
this.gemini.trades(path, function(err, data) {
|
||||
if (err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
var trades = _.map(data, function(trade) {
|
||||
return {
|
||||
tid: trade.tid,
|
||||
date: trade.timestamp,
|
||||
price: +trade.price,
|
||||
amount: +trade.amount
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, descending ? trades : trades.reverse());
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Gemini',
|
||||
slug: 'gemini',
|
||||
currencies: ['USD', 'BTC'],
|
||||
assets: ['BTC', 'ETH'],
|
||||
markets: [
|
||||
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['USD', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
var Lakebtc = require("lakebtc_nodejs");
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
}
|
||||
this.name = 'LakeBTC';
|
||||
this.balance;
|
||||
this.price;
|
||||
|
||||
this.lakebtc = new Lakebtc(this.key, this.secret);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var set = function(err, data) {
|
||||
var portfolio = [];
|
||||
_.map(data.balance, function(amount, asset) {
|
||||
portfolio.push({name: asset, amount: parseFloat(amount)});
|
||||
});
|
||||
callback(err, portfolio);
|
||||
}
|
||||
this.lakebtc.getAccountInfo(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
this.lakebtc.ticker(callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
callback(false, 0.002);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
var set = function(err, result) {
|
||||
if(err || result.error)
|
||||
return log.error('unable to buy:', err, result);
|
||||
|
||||
callback(null, result.id);
|
||||
};
|
||||
|
||||
// TODO: fees are hardcoded here?
|
||||
amount *= 0.998; // remove fees
|
||||
// prevent: Ensure that there are no more than 4 digits in total.
|
||||
amount *= 10000;
|
||||
amount = Math.floor(amount);
|
||||
amount /= 10000;
|
||||
this.lakebtc.buyOrder(_.bind(set, this), [price, amount, 'USD']);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
var set = function(err, result) {
|
||||
if(err || result.error)
|
||||
return log.error('unable to sell:', err, result);
|
||||
|
||||
callback(null, result.id);
|
||||
};
|
||||
|
||||
this.lakebtc.sell(_.bind(set, this), [price, amount, 'USD']);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var check = function(err, result) {
|
||||
var stillThere = _.find(result, function(o) { return o.id === order });
|
||||
callback(err, !stillThere);
|
||||
};
|
||||
|
||||
this.lakebtc.getOrders(_.bind(check, this));
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var cancel = function(err, result) {
|
||||
if(err || !result.result)
|
||||
log.error('unable to cancel order', order, '(', err, result, ')');
|
||||
};
|
||||
|
||||
this.lakebtc.cancelOrder(_.bind(cancel, this), [order]);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
callback(null, descending ? result.reverse() : result);
|
||||
};
|
||||
since = since ? since.unix() : moment().subtract(5, 'minutes').unix();
|
||||
this.lakebtc.bctrades( _.bind(process, this), since);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'LakeBTC',
|
||||
slug: 'lakebtc',
|
||||
currencies: ['USD'],
|
||||
assets: ['BTC'],
|
||||
markets: [
|
||||
{
|
||||
pair: ['USD', 'BTC'], minimalOrder: { amount: 1, unit: 'currency' }
|
||||
}
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: false,
|
||||
fetchTimespan: 60,
|
||||
tid: 'tid'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,154 @@
|
||||
var Mexbt = require("mexbt");
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.userId = config.username;
|
||||
this.pair = config.asset || this.currency;
|
||||
}
|
||||
this.name = 'meXBT';
|
||||
this.mexbt = new Mexbt(this.key, this.secret, this.userId);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var set = function(err, data) {
|
||||
var portfolio = [];
|
||||
_.each(data.currencies, function(balanceInfo) {
|
||||
portfolio.push({name: balanceInfo.name, amount: balanceInfo.balance});
|
||||
});
|
||||
callback(err, portfolio);
|
||||
}
|
||||
this.mexbt.accountBalance(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
this.mexbt.ticker(callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
callback(err);
|
||||
|
||||
callback(false, data.fee);
|
||||
}
|
||||
this.mexbt.getTradingFee({amount: 1, type: 'limit'}, _.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
var set = function(err, result) {
|
||||
if(err || result.error)
|
||||
return log.error('unable to buy:', err, result);
|
||||
|
||||
callback(null, result.serverOrderId);
|
||||
};
|
||||
|
||||
this.mexbt.createOrder({amount: amount, price: price, side: 'buy', type: 'limit'}, _.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
var set = function(err, result) {
|
||||
if(err || result.error)
|
||||
return log.error('unable to sell:', err, result);
|
||||
|
||||
callback(null, result.serverOrderId);
|
||||
};
|
||||
|
||||
this.mexbt.createOrder({amount: amount, price: price, side: 'sell', type: 'limit'}, _.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var currentPair = this.pair;
|
||||
|
||||
var check = function(err, result) {
|
||||
var ordersForPair = _.find(result, function(o) { return o.ins === currentPair});
|
||||
var stillThere = _.find(ordersForPair.openOrders, function(o) { return o.ServerOrderId === order });
|
||||
callback(err, !stillThere);
|
||||
};
|
||||
|
||||
this.mexbt.accountOrders(_.bind(check, this));
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var cancel = function(err, result) {
|
||||
if(err || !result)
|
||||
log.error('unable to cancel order', order, '(', err, result, ')');
|
||||
};
|
||||
|
||||
this.mexbt.cancelOrder({id: order}, _.bind(cancel, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
trades = _.map(result.trades, function (t) {
|
||||
return {tid: t.tid, price: t.px, date: t.unixtime, amount: t.qty};
|
||||
});
|
||||
if (descending) {
|
||||
trades = trades.reverse()
|
||||
}
|
||||
callback(null, trades);
|
||||
}.bind(this);
|
||||
|
||||
var endDate = moment().unix();
|
||||
|
||||
// FIXME: there is a bug in meXBT tradesByDate function, that it doesnt return all data
|
||||
// when trying to fetch all.
|
||||
// So if no since, we just fetch all via trades and giving a high count
|
||||
if (since) {
|
||||
this.mexbt.tradesByDate({startDate: since.unix(), endDate: endDate}, process);
|
||||
} else {
|
||||
// improvised
|
||||
this.mexbt.trades({count: 1000}, process);
|
||||
}
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'meXBT',
|
||||
slug: 'mexbt',
|
||||
currencies: ['MXN'],
|
||||
assets: ['BTC'],
|
||||
markets: [
|
||||
{
|
||||
pair: ['MXN', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' }
|
||||
}
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
providesHistory: 'date',
|
||||
tid: 'tid'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,214 @@
|
||||
// HERE FOR HISTORICAL PURPOSES
|
||||
|
||||
var MtGoxClient = require("mtgox-apiv2");
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var util = require('../core/util.js');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency || 'USD';
|
||||
|
||||
this.pair = 'BTC' + this.currency;
|
||||
}
|
||||
this.name = 'Mt. Gox';
|
||||
|
||||
_.bindAll(this);
|
||||
|
||||
this.mtgox = new MtGoxClient(this.key, this.secret, this.pair);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
var process = function(err, result) {
|
||||
this.checkUnauthorized(err);
|
||||
// if Mt. Gox is down or lagging
|
||||
if(err || result.result === 'error')
|
||||
log.error('unable to buy (', err, result, ')');
|
||||
|
||||
callback(null, result.data);
|
||||
};
|
||||
this.mtgox.add('bid', amount, price, _.bind(process, this));
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
var process = function(err, result) {
|
||||
this.checkUnauthorized(err);
|
||||
// if Mt. Gox is down or lagging
|
||||
if(err || result.result === 'error')
|
||||
log.error('unable to sell (', err, result, ')');
|
||||
|
||||
callback(null, result.data);
|
||||
};
|
||||
this.mtgox.add('ask', amount, price, _.bind(process, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
if(since && !_.isNumber(since))
|
||||
since = util.toMicro(since);
|
||||
|
||||
var args = _.toArray(arguments);
|
||||
this.mtgox.fetchTrades(since, _.bind(function(err, trades) {
|
||||
if (err || !trades)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
trades = trades.data;
|
||||
if (trades.length === 0)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
if(descending)
|
||||
callback(false, trades.reverse());
|
||||
else
|
||||
callback(false, trades);
|
||||
}, this));
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait.
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.checkUnauthorized = function(err) {
|
||||
if(err && err.message === 'Request failed with 403')
|
||||
util.die('It appears your ' + this.name + ' API key and secret are incorrect');
|
||||
}
|
||||
|
||||
// calls callback with the following data structure:
|
||||
//
|
||||
// [
|
||||
// { name: 'BTC', amount: 10.12413123},
|
||||
// { name: 'USD', amount: 500.0241},
|
||||
// { name: 'EUR', amount: 0},
|
||||
// ]
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var calculate = function(err, result) {
|
||||
this.checkUnauthorized(err);
|
||||
if(err)
|
||||
return this.retry(this.getPortfolio, args);
|
||||
|
||||
if(!('Wallets' in result.data))
|
||||
log.error('unable to get portfolio, do I have get_info rights?');
|
||||
|
||||
var assets = [];
|
||||
_.each(result.data.Wallets, function(wallet, name) {
|
||||
var amount = parseFloat(wallet.Balance.value);
|
||||
assets.push({name: name, amount: amount});
|
||||
});
|
||||
callback(null, assets);
|
||||
};
|
||||
|
||||
this.mtgox.info(_.bind(calculate, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var calculate = function(err, result) {
|
||||
this.checkUnauthorized(err);
|
||||
if(err)
|
||||
return this.retry(this.getFee, args);
|
||||
|
||||
// convert the %
|
||||
var fee = result.data.Trade_Fee / 100;
|
||||
callback(null, fee);
|
||||
};
|
||||
|
||||
this.mtgox.info(_.bind(calculate, this));
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
// we can't just request the order id
|
||||
// @link https://bitbucket.org/nitrous/mtgox-api/#markdown-header-moneyorderresult
|
||||
var check = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.checkOrder, args);
|
||||
|
||||
var stillThere = _.find(result.data, function(o) { return o.oid === order });
|
||||
callback(null, !stillThere);
|
||||
};
|
||||
|
||||
this.mtgox.orders(_.bind(check, this));
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order) {
|
||||
var cancel = function(err, result) {
|
||||
if(err || result.result !== 'succes')
|
||||
log.error('unable to cancel order', order, '(', err, result, ')');
|
||||
};
|
||||
|
||||
this.mtgox.cancel(order, _.bind(cancel, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTicker, args);
|
||||
|
||||
var ticker = {
|
||||
bid: result.data.buy.value,
|
||||
ask: result.data.sell.value
|
||||
}
|
||||
callback(err, ticker);
|
||||
};
|
||||
|
||||
this.mtgox.ticker(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {};
|
||||
// ---- Keeping this here for historical purposes. ----
|
||||
// {
|
||||
//
|
||||
// name: 'MtGox',
|
||||
// slug: 'mtgox',
|
||||
// direct: true,
|
||||
// infinityOrder: true,
|
||||
// currencies: [
|
||||
// 'USD', 'EUR', 'GBP', 'AUD', 'CAD', 'CHF', 'CNY',
|
||||
// 'DKK', 'HKD', 'PLN', 'RUB', 'SGD', 'THB'
|
||||
// ],
|
||||
// assets: ['BTC'],
|
||||
// markets: [
|
||||
// { pair: ['USD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['EUR', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['GBP', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['AUD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['CAD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['CHF', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['CNY', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['DKK', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['HKD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['PLN', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['RUB', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['SGD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['THB', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } }
|
||||
// ],
|
||||
// requires: ['key', 'secret'],
|
||||
// providesHistory: false
|
||||
// }
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
var OKCoin = require('okcoin-china');
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
}
|
||||
this.pair = [config.asset, config.currency].join('_').toLowerCase();
|
||||
this.name = 'okcoin';
|
||||
this.okcoin = new OKCoin(this.key, this.secret);
|
||||
this.lastTid = false;
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() {
|
||||
method.apply(self, args)
|
||||
},
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var calculate = function(err, data) {
|
||||
if(err) {
|
||||
if(err.message === 'invalid api key')
|
||||
util.die('Your ' + this.name + ' API keys are invalid');
|
||||
return this.retry(this.okcoin.getUserInfo, calculate);
|
||||
}
|
||||
|
||||
var portfolio = [];
|
||||
_.each(data.info.funds.free, function(amount, asset) {
|
||||
portfolio.push({name: asset.toUpperCase(), amount: +amount});
|
||||
});
|
||||
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
|
||||
this.okcoin.getUserInfo(calculate);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = [this.pair, process];
|
||||
var process = function(err, data) {
|
||||
if (err)
|
||||
return this.retry(this.okcoin.getTicker(args));
|
||||
|
||||
var ticker = _.extend(data.ticker, {
|
||||
bid: +data.ticker.sell,
|
||||
ask: +data.ticker.buy
|
||||
});
|
||||
|
||||
callback(err, ticker);
|
||||
}.bind(this);
|
||||
|
||||
this.okcoin.getTicker(process, args);
|
||||
}
|
||||
|
||||
// This assumes that only limit orders are being placed, so fees are the
|
||||
// "maker fee" of 0.1%. It does not take into account volume discounts.
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var makerFee = 0.1;
|
||||
callback(false, makerFee / 100);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(raw_amount, price, callback) {
|
||||
var amount = Math.floor(raw_amount * 10000) / 10000
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return log.error('unable to process order:', err, result);
|
||||
|
||||
callback(null, result.order_id);
|
||||
}.bind(this);
|
||||
|
||||
this.okcoin.addTrade(set, this.pair, 'buy', amount, price);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(raw_amount, price, callback) {
|
||||
var amount = Math.floor(raw_amount * 10000) / 10000
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return log.error('unable to process order:', err, result);
|
||||
|
||||
callback(null, result.order_id);
|
||||
}.bind(this);
|
||||
|
||||
this.okcoin.addTrade(set, this.pair, 'sell', amount, price);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.checkOrder = function(order_id, callback) {
|
||||
var check = function(err, result) {
|
||||
if(err || !result.result) {
|
||||
log.error('Perhaps the order already got filled?', '(', result, ')');
|
||||
callback(err, !result.result);
|
||||
} else {
|
||||
callback(err, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.okcoin.getOrderInfo(check, this.pair, order_id);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order_id, callback) {
|
||||
var cancel = function(err, result) {
|
||||
if(err || !result.result) {
|
||||
return log.error('unable to cancel order ', order_id, '(', result, ')');
|
||||
}
|
||||
|
||||
callback();
|
||||
}.bind(this);
|
||||
|
||||
this.okcoin.cancelOrder(cancel, this.pair, order_id);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order_id, callback) {
|
||||
|
||||
var handle = (err, resp) => {
|
||||
var order = _.first(resp.orders);
|
||||
|
||||
var amount = order.deal_amount;
|
||||
var price = order.price;
|
||||
var date = moment(order.date).utc();
|
||||
|
||||
callback(undefined, {amount, price, date});
|
||||
}
|
||||
|
||||
this.okcoin.getOrderInfo(handle, this.pair, order_id)
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
if(since)
|
||||
since = 600;
|
||||
|
||||
this.okcoin.getTrades(function(err, data) {
|
||||
if (err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
var trades = _.map(data, function(trade) {
|
||||
return {
|
||||
price: +trade.price,
|
||||
amount: +trade.amount,
|
||||
tid: +trade.tid,
|
||||
date: trade.date
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, trades.reverse());
|
||||
}.bind(this), this.pair, since);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'OkCoin',
|
||||
slug: 'okcoin',
|
||||
currencies: ['BTC', 'CNY'],
|
||||
assets: ['BTC', 'LTC'],
|
||||
markets: [
|
||||
{ pair: ['CNY', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['CNY', 'LTC'], minimalOrder: { amount: 0.01, unit: 'asset' } }
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
providesHistory: false,
|
||||
fetchTimespan: 60,
|
||||
tid: 'date',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"assets": [
|
||||
"XBT",
|
||||
"ETH",
|
||||
"LTC",
|
||||
"BCH",
|
||||
"BTG"
|
||||
],
|
||||
"currencies": [
|
||||
"CAD",
|
||||
"USD",
|
||||
"XBT"
|
||||
],
|
||||
"markets": [
|
||||
{
|
||||
"pair": [
|
||||
"CAD",
|
||||
"XBT"
|
||||
],
|
||||
"book": "btc_cad",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"XBT"
|
||||
],
|
||||
"book": "btc_usd",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"CAD",
|
||||
"BCH"
|
||||
],
|
||||
"book": "bch_cad",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XBT",
|
||||
"BCH"
|
||||
],
|
||||
"book": "bch_btc",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"CAD",
|
||||
"ETH"
|
||||
],
|
||||
"book": "eth_cad",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XBT",
|
||||
"ETH"
|
||||
],
|
||||
"book": "eth_btc",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"CAD",
|
||||
"LTC"
|
||||
],
|
||||
"book": "ltc_cad",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XBT",
|
||||
"LTC"
|
||||
],
|
||||
"book": "ltc_btc",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"CAD",
|
||||
"BTG"
|
||||
],
|
||||
"book": "btg_cad",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XBT",
|
||||
"BTG"
|
||||
],
|
||||
"book": "btg_btc",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
var QuadrigaCX = require('quadrigacx');
|
||||
var moment = require('moment');
|
||||
var util = require('../core/util');
|
||||
var _ = require('lodash');
|
||||
var log = require('../core/log');
|
||||
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientId = config.username;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
}
|
||||
|
||||
this.pair = this.asset.toLowerCase() + '_' + this.currency.toLowerCase();
|
||||
this.name = 'quadriga';
|
||||
this.since = null;
|
||||
|
||||
this.quadriga = new QuadrigaCX(
|
||||
this.clientId ? this.clientId : "1",
|
||||
this.key ? this.key : "",
|
||||
this.secret ? this.secret : "",
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.retry = function(method, warn, args, error) {
|
||||
var wait = +moment.duration(30, 'seconds');
|
||||
if (error.code === 200) {
|
||||
log.debug(`${this.name}: API rate limit exceeded! unable to call ${method}, will retry in 2 minutes`)
|
||||
wait = +moment.duration(120, 'seconds');
|
||||
}
|
||||
else {
|
||||
log.debug(JSON.stringify(error));
|
||||
log.debug(`${this.name}: ${warn}, will retry in 30 seconds`);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, trades) {
|
||||
if (trades && trades.error) return this.retry(this.getTrades, 'unable to get trades', args, trades.error);
|
||||
if (err) return this.retry(this.getTrades, 'unable to get trades', args, err);
|
||||
|
||||
var parsedTrades = [];
|
||||
_.each(trades, function(trade) {
|
||||
parsedTrades.push({
|
||||
tid: trade.tid,
|
||||
date: trade.date,
|
||||
price: parseFloat(trade.price),
|
||||
amount: parseFloat(trade.amount)
|
||||
});
|
||||
}, this);
|
||||
|
||||
if(descending)
|
||||
callback(null, parsedTrades);
|
||||
else
|
||||
callback(null, parsedTrades.reverse());
|
||||
};
|
||||
|
||||
var reqData = {
|
||||
book: this.pair,
|
||||
time: 'hour'
|
||||
};
|
||||
|
||||
this.quadriga.api('transactions', reqData, _.bind(process, this));
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, data) {
|
||||
|
||||
if (data && data.error) return this.retry(this.getPortfolio, 'unable to get balance', args, data.error);
|
||||
if (err) return this.retry(this.getPortfolio, 'unable to get balance', args, err);
|
||||
|
||||
var assetAmount = parseFloat( data[this.asset.toLowerCase() + '_available'] );
|
||||
var currencyAmount = parseFloat( data[this.currency.toLowerCase() + '_available'] );
|
||||
|
||||
if(!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
log.error(`Quadriga did not return balance for ${this.asset.toLowerCase()}, assuming 0.`);
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
log.error(`Quadriga did not return balance for ${this.currency.toLowerCase()}, assuming 0.`);
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
var portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
callback(err, portfolio);
|
||||
};
|
||||
|
||||
this.quadriga.api('balance', _.bind(set, this));
|
||||
};
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
callback(false, 0.005);
|
||||
};
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var set = function(err, data) {
|
||||
|
||||
if (data && data.error) return this.retry(this.getTicker, 'unable to get quote', args, data.error);
|
||||
if (err) return this.retry(this.getTicker, 'unable to get quote', args, err);
|
||||
|
||||
var ticker = {
|
||||
ask: data.ask,
|
||||
bid: data.bid
|
||||
};
|
||||
callback(err, ticker);
|
||||
};
|
||||
|
||||
this.quadriga.api('ticker', {book: this.pair}, _.bind(set, this));
|
||||
};
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
var precision = 100000000;
|
||||
var market = Trader.getCapabilities().markets.find(function(market){ return market.pair[0] === this.currency && market.pair[1] === this.asset });
|
||||
|
||||
if(Number.isInteger(market.precision))
|
||||
precision = 10 * market.precision;
|
||||
|
||||
amount *= precision;
|
||||
amount = Math.floor(amount);
|
||||
amount /= precision;
|
||||
return amount;
|
||||
};
|
||||
|
||||
Trader.prototype.addOrder = function(tradeType, amount, price, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
amount = this.roundAmount(amount);
|
||||
log.debug(tradeType.toUpperCase(), amount, this.asset, '@', price, this.currency);
|
||||
|
||||
var set = function(err, data) {
|
||||
|
||||
if (data && data.error) return this.retry(this.addOrder, 'unable to place order', args, data.error);
|
||||
if (err) return this.retry(this.addOrder, 'unable to place order', args, err);
|
||||
|
||||
var txid = data.id;
|
||||
log.debug('added order with txid:', txid);
|
||||
|
||||
callback(undefined, txid);
|
||||
};
|
||||
|
||||
this.quadriga.api(tradeType, {
|
||||
book: this.pair,
|
||||
price: price,
|
||||
amount: amount
|
||||
}, _.bind(set, this));
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
var get = function(err, data) {
|
||||
if (data && data.error) return this.retry(this.getOrder, 'unable to get order', args, data.error);
|
||||
if (err) return this.retry(this.getOrder, 'unable to get order', args, err);
|
||||
|
||||
var price = parseFloat( data[0].price );
|
||||
var amount = parseFloat( data[0].amount );
|
||||
var date = (data[0].updated) ? moment.unix( data[0].updated ) : false;
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.quadriga.api('lookup_oder', {id: order}, get);
|
||||
}
|
||||
|
||||
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.checkOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
var check = function(err, data) {
|
||||
|
||||
if (data && data.error) return this.retry(this.checkOrder, 'unable to get order', args, data.error);
|
||||
if (err) return this.retry(this.checkOrder, 'unable to get order', args, err);
|
||||
|
||||
var result = data[0];
|
||||
var stillThere = result.status === 0 || result.status === 1;
|
||||
callback(err, !stillThere);
|
||||
};
|
||||
|
||||
this.quadriga.api('lookup_order', {id: order}, _.bind(check, this));
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var cancel = function(err, data) {
|
||||
|
||||
if (data && data.error) return this.retry(this.cancelOrder, 'unable to cancel order', args, data.error);
|
||||
if (err) return this.retry(this.cancelOrder, 'unable to cancel order', args, err);
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
this.quadriga.api('cancel_order', {id: order}, _.bind(cancel, this));
|
||||
};
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Quadriga',
|
||||
slug: 'quadriga',
|
||||
currencies: ['CAD', 'USD', 'BTC'],
|
||||
assets: ['BTC', 'ETH', 'LTC', 'BCH'],
|
||||
markets: [
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
{ pair: ['CAD', 'ETH'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
{ pair: ['CAD', 'BTC'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
{ pair: ['CAD', 'LTC'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
{ pair: ['CAD', 'BCH'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
providesHistory: false,
|
||||
tid: 'tid',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"assets": [
|
||||
"ETH",
|
||||
"LTC",
|
||||
"BTC",
|
||||
"BCH",
|
||||
"EUR",
|
||||
"PPC",
|
||||
"ZEC",
|
||||
"NOKU",
|
||||
"FDZ"
|
||||
],
|
||||
"currencies": [
|
||||
"BTC",
|
||||
"EUR",
|
||||
"ETH",
|
||||
"GUSD",
|
||||
"XRP"
|
||||
],
|
||||
"markets": [
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.0005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"PPC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.05,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"ZEC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"GUSD"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"PPC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.05,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ZEC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GUSD",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.0005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XRP",
|
||||
"EUR"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XRP",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
const Therocktrading = require('therocktrading');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
const marketData = require("./therocktrading-markets.json");
|
||||
|
||||
const QUERY_DELAY = 350;
|
||||
|
||||
const Trader = function(config) {
|
||||
this.post_only = true;
|
||||
this.use_sandbox = false;
|
||||
this.name = 'Therocktrading';
|
||||
this.scanback = true;
|
||||
this.scanbackTid = 0;
|
||||
this.since = null;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
|
||||
this.api_url = 'https://api.therocktrading.com';
|
||||
this.api_sandbox_url = 'https://api-staging.therocktrading.com';
|
||||
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
|
||||
this.pair = [config.asset, config.currency].join('').toUpperCase();
|
||||
this.post_only =
|
||||
typeof config.post_only !== 'undefined' ? config.post_only : true;
|
||||
|
||||
if (config.sandbox) {
|
||||
this.use_sandbox = config.sandbox;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.therocktrading = new Therocktrading(this.key, this.secret, 'Gekko Broker v' + require('../package.json').version);
|
||||
};
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'Rate limit exceeded',
|
||||
'Response code 5',
|
||||
'Therocktrading is currently under maintenance.',
|
||||
'HTTP 408 Error',
|
||||
'HTTP 504 Error',
|
||||
'HTTP 503 Error',
|
||||
'EHOSTUNREACH',
|
||||
'EAI_AGAIN',
|
||||
'ENETUNREACH'
|
||||
];
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.processResponse = function(method, next) {
|
||||
return (error, body) => {
|
||||
if (body && body.code) {
|
||||
error = new Error(`Error ${body.code}: ${body.msg}`);
|
||||
}
|
||||
|
||||
if(!error && body && !_.isEmpty(body.message)) {
|
||||
error = new Error(body.message);
|
||||
}
|
||||
|
||||
/*if(
|
||||
response &&
|
||||
response.statusCode < 200 &&
|
||||
response.statusCode >= 300
|
||||
) {
|
||||
error = new Error(`Response code ${response.statusCode}`);
|
||||
}*/
|
||||
|
||||
if(error) {
|
||||
if(_.isString(error)) {
|
||||
error = new Error(error);
|
||||
}
|
||||
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return next(undefined, body);
|
||||
}
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const handle = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
var trades = [];
|
||||
if (_.isArray(data.trades)) {
|
||||
trades = _.map(data.trades, function(trade) {
|
||||
return {
|
||||
tid: trade.id,
|
||||
price: trade.price,
|
||||
amount: trade.amount,
|
||||
date: moment.utc(trade.date).format('X')
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
callback(null, descending ? trades : trades.reverse());
|
||||
};
|
||||
|
||||
if (moment.isMoment(since)) since = since.format();
|
||||
//if (moment.isMoment(to)) to = to.format();
|
||||
|
||||
var options = {
|
||||
order: "DESC" // which is default at therock
|
||||
}
|
||||
if (since)
|
||||
options.after = since;
|
||||
const fetch = cb => this.therocktrading.trades(this.pair, options, this.processResponse('getTrades', cb));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
callback(undefined, { bid: data.bid, ask: data.ask });
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.therocktrading.ticker(this.pair, this.processResponse('getTicker', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
const fee = 0.2;
|
||||
// should check for discounts ?
|
||||
callback(undefined, fee);
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
var portfolio = data.balances.map(function(balance) {
|
||||
return {
|
||||
name: balance.currency.toUpperCase(),
|
||||
amount: parseFloat(balance.trading_balance),
|
||||
};
|
||||
});
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.therocktrading.balances(this.processResponse('getPortfolio', cb));
|
||||
retry(undefined, fetch, result);
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(undefined, result.id);
|
||||
}
|
||||
const fetch = next => {
|
||||
this.therocktrading.buy(this.pair, amount.toString(), price.toString(), this.processResponse('order', next))
|
||||
};
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(undefined, result.id);
|
||||
}
|
||||
const fetch = next => {
|
||||
this.therocktrading.sell(this.pair, amount.toString(), price.toString(), this.processResponse('order', next))
|
||||
};
|
||||
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.amount === result.amount_unfilled) {
|
||||
return callback(null, {price, amount, date});
|
||||
}
|
||||
|
||||
_.each(result.trades, trade => {
|
||||
date = moment(trade.date);
|
||||
price = ((price * amount) + (+trade.price * trade.amount)) / (+trade.amount + amount);
|
||||
amount += +trade.amount;
|
||||
});
|
||||
|
||||
const fees = {};
|
||||
const feePercent = this.makerFee;
|
||||
|
||||
const fee = price * amount * feePercent;
|
||||
fees[this.currency] = fee;
|
||||
|
||||
callback(err, {price, amount, date, fees, feePercent});
|
||||
};
|
||||
|
||||
const fetch = cb => this.therocktrading.order_status(this.pair, order, this.processResponse('order_status', cb));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var status = data.status;
|
||||
if (status == 'executed') {
|
||||
return callback(undefined, { executed: true, open: false, filledAmount: parseFloat(data.amount) });
|
||||
} else if (status === 'deleted') {
|
||||
return callback(undefined, { executed: false, open: false, filledAmount: parseFloat(data.amount - data.amount_unfilled) });
|
||||
} else if (status === 'active') {
|
||||
// should check if not expired (data.close_on)?
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.amount - data.amount_unfilled) });
|
||||
}
|
||||
|
||||
callback(new Error('Unknown status ' + status));
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.therocktrading.order_status(this.pair, order, this.processResponse('order_status', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
// callback for cancelOrder should be true if the order was already filled, otherwise false
|
||||
const result = (err, data) => {
|
||||
if(err) {
|
||||
return callback(null, true); // need to catch the specific error but usually an error on cancel means it was filled
|
||||
}
|
||||
|
||||
return callback(null, false);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.therocktrading.cancel_order(this.pair, order, this.processResponse('cancel_order', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return _.floor(amount, 8);
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return +price;
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function() {
|
||||
return {
|
||||
name: 'Therocktrading',
|
||||
slug: 'therocktrading',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
// following tid, define type to use on trade to batch trades
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Trader;
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
var WEX = require('node-wex');
|
||||
|
||||
var moment = require('moment');
|
||||
var util = require('../core/util');
|
||||
var _ = require('lodash');
|
||||
var log = require('../core/log')
|
||||
|
||||
var Trader = function(config) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
this.pair = [config.asset, config.currency].join('_').toLowerCase();
|
||||
this.name = 'wex.nz';
|
||||
|
||||
_.bindAll(this);
|
||||
|
||||
this.wex = new WEX(this.key, this.secret);
|
||||
_.bindAll(this.wex, ['trade', 'trades', 'getInfo', 'ticker', 'orderList']);
|
||||
|
||||
// see @link https://github.com/askmike/gekko/issues/302
|
||||
this.wexHistorocial = new WEX(false, false, {public_url: 'https://wex.nz/api/3/'});
|
||||
_.bindAll(this.wexHistorocial, 'trades');
|
||||
}
|
||||
|
||||
Trader.prototype.round = function(amount) {
|
||||
// Prevent "You incorrectly entered one of fields."
|
||||
// because of more than 8 decimals.
|
||||
amount *= 100000000;
|
||||
amount = Math.floor(amount);
|
||||
amount /= 100000000;
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
amount = this.round(amount);
|
||||
|
||||
var set = function(err, data) {
|
||||
if(err || !data || !data.return)
|
||||
return log.error('unable to buy:', err);
|
||||
|
||||
callback(null, data.return.order_id);
|
||||
}.bind(this);
|
||||
|
||||
// workaround for nonce error
|
||||
setTimeout(function() {
|
||||
this.wex.trade(this.pair, 'buy', price, amount, _.bind(set, this));
|
||||
}.bind(this), 1000);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
amount = this.round(amount);
|
||||
|
||||
var set = function(err, data) {
|
||||
if(err || !data || !data.return)
|
||||
return log.error('unable to sell:\n\n', err);
|
||||
|
||||
callback(null, data.return.order_id);
|
||||
};
|
||||
|
||||
// workaround for nonce error
|
||||
setTimeout(function() {
|
||||
this.wex.trade(this.pair, 'sell', price, amount, _.bind(set, this));
|
||||
}.bind(this), 1000);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var calculate = function(err, data) {
|
||||
|
||||
if(err) {
|
||||
if(err.message === 'invalid api key')
|
||||
util.die('Your ' + this.name + ' API keys are invalid');
|
||||
|
||||
return this.retry(this.getPortfolio, args);
|
||||
}
|
||||
|
||||
if(_.isEmpty(data))
|
||||
err = 'no data';
|
||||
|
||||
if (err || !data.return || !data.return.funds)
|
||||
return this.retry(this.getPortfolio, args);
|
||||
|
||||
var assetAmount = parseFloat( data.return.funds[this.asset.toLowerCase()] );
|
||||
var currencyAmount = parseFloat( data.return.funds[this.currency.toLowerCase()] );
|
||||
|
||||
if(!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
log.error(`wex.nz did not return portfolio for ${this.asset}, assuming 0.`);
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
log.error(`wex.nz did not return portfolio for ${this.currency}, assuming 0.`);
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
var portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
|
||||
this.wex.getInfo(calculate);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
// wex.nz doesn't state asks and bids in its
|
||||
// ticker
|
||||
var set = function(err, data) {
|
||||
|
||||
if(err)
|
||||
return this.retry(this.wex.ticker, [this.pair, set]);
|
||||
|
||||
var ticker = {
|
||||
ask: data.ticker.buy,
|
||||
bid: data.ticker.sell
|
||||
};
|
||||
|
||||
callback(err, ticker);
|
||||
}.bind(this);
|
||||
|
||||
this.wex.ticker(this.pair, set);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
// wex.nz doesn't have different fees based on orders
|
||||
// at this moment it is always 0.2%
|
||||
callback(false, 0.002);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var check = function(err, result) {
|
||||
// wex returns an error when you have no open trades
|
||||
// right now we assume on every error that the order
|
||||
// was filled.
|
||||
//
|
||||
// TODO: check whether the error states that there are no
|
||||
// open trades or that there is something else.
|
||||
if(err)
|
||||
callback(false, true);
|
||||
else
|
||||
callback(err, !result[order]);
|
||||
}.bind(this);
|
||||
|
||||
this.wex.orderList({}, check);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(orderId, callback) {
|
||||
console.log('getOrder', orderId);
|
||||
var args = _.toArray(arguments);
|
||||
var check = function(err, result) {
|
||||
if(err) {
|
||||
log.error('error on getOrder', err);
|
||||
return this.retry(this.getOrder, args);
|
||||
}
|
||||
|
||||
var order = null;
|
||||
|
||||
_.each(result.return, o => {
|
||||
if(o.order_id === +orderId)
|
||||
order = o;
|
||||
});
|
||||
|
||||
if(!order)
|
||||
return log.error('wex.nz did not provide the order');
|
||||
|
||||
var price = parseFloat(order.rate);
|
||||
var amount = parseFloat(order.amount);
|
||||
var date = moment.unix(order.timestamp);
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.wex.tradeHistory({pair: this.pair}, check);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
this.wex.cancelOrder(order, (err, result) => {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, trades) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
if(descending)
|
||||
callback(false, trades);
|
||||
else
|
||||
callback(false, trades.reverse());
|
||||
}.bind(this);
|
||||
|
||||
// see @link https://github.com/askmike/gekko/issues/302
|
||||
if(since) {
|
||||
this.wexHistorocial.makePublicApiRequest(
|
||||
'trades',
|
||||
this.pair + '?limit=2000',
|
||||
this.processAPIv3Trades(process)
|
||||
)
|
||||
} else
|
||||
this.wex.trades(this.pair, process);
|
||||
}
|
||||
|
||||
Trader.prototype.processAPIv3Trades = function(cb) {
|
||||
return function(err, data) {
|
||||
var trades = _.map(data[this.pair], function(t) {
|
||||
return {
|
||||
price: t.price,
|
||||
amount: t.amount,
|
||||
tid: t.tid,
|
||||
date: t.timestamp
|
||||
}
|
||||
})
|
||||
cb(err, trades);
|
||||
}.bind(this)
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'wex.nz',
|
||||
slug: 'wex.nz',
|
||||
currencies: ['USD', 'RUR', 'EUR', 'BTC', 'LTC', 'ETH', 'NMC', 'NMC', 'NVC', 'PPC', 'DSH', 'BCH'],
|
||||
assets: [
|
||||
'BTC', 'LTC', 'NMC', 'NVC', 'USD', 'EUR', 'PPC', 'DSH', 'ETH',
|
||||
'USDET', 'RURET', 'EURET', 'BTCET', 'LTCET', 'ETHET', 'NMCET', 'NVCET', 'PPCET', 'DSHET', 'BCHET' // Token
|
||||
],
|
||||
markets: [
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['EUR', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['EUR', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'NMC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'NMC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'NVC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'NVC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'USD'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'EUR'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'EUR'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'PPC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'PPC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['EUR', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['LTC', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['ETH', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['EUR', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['LTC', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'BCH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'BCH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
|
||||
// Token pairs
|
||||
{ pair: ['USD', 'USDET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'RURET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['EUR', 'EURET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'BTCET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['LTC', 'LTCET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['ETH', 'ETHET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['NMC', 'NMCET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['NVC', 'NVCET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['PPC', 'PPCET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['DSH', 'DSHET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BCH', 'BCHET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: false,
|
||||
tid: 'tid',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,144 @@
|
||||
var Zaif = require("zaif.jp");
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
}
|
||||
this.name = 'Zaif';
|
||||
this.balance;
|
||||
this.price;
|
||||
|
||||
this.zaif = Zaif.createPrivateApi(this.key, this.secret, 'user agent is node-zaif');
|
||||
this.api = Zaif.PublicApi;
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var set = function(data) {
|
||||
var portfolio = [];
|
||||
_.each(data.funds, function(amount, asset) {
|
||||
// if(asset.indexOf('available') !== -1) {
|
||||
asset = asset.substr(0, 3).toUpperCase();
|
||||
portfolio.push({name: asset, amount: parseFloat(amount)});
|
||||
// }
|
||||
});
|
||||
callback(err, portfolio);
|
||||
}
|
||||
this.zaif.getInfo().then(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
this.api.ticker('btc_jpy').then(callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var makerFee = 0.0;
|
||||
callback(false, makerFee / 100);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
var set = function(result) {
|
||||
if(!result)
|
||||
return log.error('unable to buy:', result);
|
||||
|
||||
callback(null, result.order_id);
|
||||
};
|
||||
|
||||
// TODO: fees are hardcoded here?
|
||||
// amount *= 0.995; // remove fees
|
||||
// prevent: Ensure that there are no more than 8 digits in total.
|
||||
amount *= 100000000;
|
||||
amount = Math.floor(amount);
|
||||
amount /= 100000000;
|
||||
this.zaif.trade('bid', price, amount).then(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
var set = function(result) {
|
||||
if(!result)
|
||||
return log.error('unable to sell:', result);
|
||||
|
||||
callback(null, result.order_id);
|
||||
};
|
||||
|
||||
this.zaif.trade('ask', price, amount).then(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var check = function(result) {
|
||||
var stillThere = (order in result);
|
||||
callback(null, !stillThere);
|
||||
};
|
||||
|
||||
this.zaif.activeorders().then(_.bind(check, this));
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var cancel = function(result) {
|
||||
if(!result)
|
||||
log.error('unable to cancel order', order, '(', result, ')');
|
||||
};
|
||||
|
||||
this.zaif.cancelorder(order).then(_.bind(cancel, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(result) {
|
||||
if(!result)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
callback(null, result.reverse());
|
||||
};
|
||||
|
||||
this.api.trades('btc_jpy').then(_.bind(process, this));
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Zaif.jp',
|
||||
slug: 'zaif.jp',
|
||||
currencies: ['JPY'],
|
||||
assets: ['BTC'],
|
||||
markets: [
|
||||
{
|
||||
pair: ['JPY', 'BTC'], minimalOrder: { amount: 1, unit: 'currency' }
|
||||
}
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
providesHistory: false,
|
||||
fetchTimespan: 60,
|
||||
tid: 'tid'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
Reference in new issue
Block a user