first commit

This commit is contained in:
Francois Klieber committed 2021-06-06 20:25:14 +02:00
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
+530
View File
@@ -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;
+296
View File
@@ -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
+319
View File
@@ -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;
+246
View File
@@ -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;
+261
View File
@@ -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;
+633
View File
@@ -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;
+249
View File
@@ -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;
+176
View File
@@ -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;
+89
View File
@@ -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;
+211
View File
@@ -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;
+238
View File
@@ -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"
}
}
]
}
+316
View File
@@ -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
}
}
]
}
+385
View File
@@ -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;
+283
View File
@@ -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;
+43
View File
@@ -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"
}
}
]
}
+239
View File
@@ -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;
+430
View File
@@ -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;
+228
View File
@@ -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
+471
View File
@@ -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;
+138
View File
@@ -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;
+416
View File
@@ -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;
+154
View File
@@ -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;
+214
View File
@@ -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;
+194
View File
@@ -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
+533
View File
@@ -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;
+136
View File
@@ -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
}
]
}
+251
View File
@@ -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
}
}
]
}
+286
View File
@@ -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;
+301
View File
@@ -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;
+144
View File
@@ -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;