first commit
This commit is contained in:
commit
aa7869eb33
386 files changed
+69375
No files matched your search
@@ -0,0 +1 @@
|
||||
nusadua
|
||||
@@ -0,0 +1,3 @@
|
||||
# Gekko Broker
|
||||
|
||||
see [the docs](https://gekko.wizb.it/docs/gekko-broker/introduction.html).
|
||||
@@ -0,0 +1,23 @@
|
||||
const deps = require('./package.json').dependencies;
|
||||
|
||||
const missing = [];
|
||||
|
||||
Object.keys(deps).forEach(dep => {
|
||||
try {
|
||||
require(dep);
|
||||
} catch(e) {
|
||||
if(e.code === 'MODULE_NOT_FOUND') {
|
||||
missing.push(dep);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(missing.length) {
|
||||
console.error(
|
||||
'\nThe following Gekko Broker dependencies are not installed: [',
|
||||
missing.join(', '),
|
||||
'].\n\nYou need to install them first, read here how:',
|
||||
'https://gekko.wizb.it/docs/installation/installing_gekko.html#Installing-Gekko-39-s-dependencies\n'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const moment = require('moment');
|
||||
const errors = require('./exchangeErrors');
|
||||
|
||||
const Checker = function() {
|
||||
_.bindAll(this);
|
||||
}
|
||||
|
||||
Checker.prototype.getExchangeCapabilities = function(slug) {
|
||||
if(!fs.existsSync(__dirname + '/wrappers/' + slug + '.js'))
|
||||
throw new errors.ExchangeError(`Gekko does not know the exchange "${slug}"`);
|
||||
|
||||
return require('./wrappers/' + slug).getCapabilities();
|
||||
}
|
||||
|
||||
// check if the exchange is configured correctly for monitoring
|
||||
Checker.prototype.cantMonitor = function(conf) {
|
||||
var slug = conf.exchange.toLowerCase();
|
||||
var exchange = this.getExchangeCapabilities(slug);
|
||||
|
||||
if(!exchange)
|
||||
return 'Gekko does not support the exchange ' + slug;
|
||||
|
||||
var name = exchange.name;
|
||||
|
||||
if('monitorError' in exchange)
|
||||
return 'At this moment Gekko can\'t monitor ' + name + ', find out more info here:\n\n' + exchange.monitorError;
|
||||
|
||||
var name = exchange.name;
|
||||
|
||||
if(!_.includes(exchange.currencies, conf.currency))
|
||||
return 'Gekko only supports the currencies [ ' + exchange.currencies.join(', ') + ' ] at ' + name + ' (not ' + conf.currency + ')';
|
||||
|
||||
if(!_.includes(exchange.assets, conf.asset))
|
||||
return 'Gekko only supports the assets [ ' + exchange.assets.join(', ') + ' ] at ' + name + ' (not ' + conf.asset + ')';
|
||||
|
||||
var pair = _.find(exchange.markets, function(p) {
|
||||
return p.pair[0] === conf.currency && p.pair[1] === conf.asset;
|
||||
});
|
||||
|
||||
if(!pair)
|
||||
return 'Gekko does not support this currency/assets pair at ' + name;
|
||||
|
||||
// everything is okay
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the exchange is configured correctly for fetching
|
||||
// full history
|
||||
Checker.prototype.cantFetchFullHistory = function(conf) {
|
||||
var slug = conf.exchange.toLowerCase();
|
||||
var exchange = this.getExchangeCapabilities(slug);
|
||||
|
||||
if(this.cantMonitor(conf))
|
||||
return this.cantMonitor(conf);
|
||||
|
||||
var name = exchange.name;
|
||||
|
||||
if(!exchange.providesFullHistory)
|
||||
return 'The exchange ' + name + ' does not provide full history (or Gekko doesn\'t support importing it)';
|
||||
|
||||
if ("exchangeMaxHistoryAge" in exchange) {
|
||||
if (moment(config.importer.daterange.from) < moment().subtract(exchange.exchangeMaxHistoryAge, "days")) {
|
||||
return 'Unsupported date from! ' + exchange.name + ' supports history of max ' + exchange.exchangeMaxHistoryAge + ' days..';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if the exchange if configured correctly for real trading
|
||||
Checker.prototype.cantTrade = function(conf) {
|
||||
var cantMonitor = this.cantMonitor(conf);
|
||||
if(cantMonitor)
|
||||
return cantMonitor;
|
||||
|
||||
var slug = conf.exchange.toLowerCase();
|
||||
var exchange = this.getExchangeCapabilities(slug);
|
||||
var name = exchange.name;
|
||||
|
||||
if(!exchange.tradable)
|
||||
return 'At this moment Gekko can\'t trade at ' + name + '.';
|
||||
|
||||
if(conf.key === 'your-key')
|
||||
return '"your-key" is not a valid API key';
|
||||
|
||||
if(conf.secret === 'your-secret')
|
||||
return '"your-secret" is not a valid API secret';
|
||||
|
||||
var error = false;
|
||||
_.each(exchange.requires, function(req) {
|
||||
if(!conf[req])
|
||||
error = name + ' requires "' + req + '" to be set in the config';
|
||||
}, this);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
Checker.prototype.settings = function(conf) {
|
||||
var slug = conf.exchange.toLowerCase();
|
||||
return this.getExchangeCapabilities(slug);
|
||||
|
||||
}
|
||||
|
||||
module.exports = new Checker();
|
||||
@@ -0,0 +1,42 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const ExchangeError = function(message) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.name = "ExchangeError";
|
||||
this.message = message;
|
||||
}
|
||||
ExchangeError.prototype = new Error();
|
||||
|
||||
const ExchangeAuthenticationError = function(message) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.name = "ExchangeAuthenticationError";
|
||||
this.message = message;
|
||||
}
|
||||
ExchangeAuthenticationError.prototype = new Error();
|
||||
|
||||
const RetryError = function(message) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.name = "RetryError";
|
||||
this.retry = 5;
|
||||
this.message = message;
|
||||
}
|
||||
RetryError.prototype = new Error();
|
||||
|
||||
const AbortError = function(message) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.name = "AbortError";
|
||||
this.message = message;
|
||||
}
|
||||
AbortError.prototype = new Error();
|
||||
|
||||
module.exports = {
|
||||
ExchangeError,
|
||||
ExchangeAuthenticationError,
|
||||
RetryError,
|
||||
AbortError
|
||||
};
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// generic low level reusuable utils for interacting with exchanges.
|
||||
|
||||
const retry = require('retry');
|
||||
const errors = require('./exchangeErrors');
|
||||
const _ = require('lodash');
|
||||
|
||||
const retryInstance = (options, checkFn, callback, e) => {
|
||||
if(!options) {
|
||||
options = {
|
||||
retries: 100,
|
||||
factor: 1.2,
|
||||
minTimeout: 1 * 1000,
|
||||
maxTimeout: 4 * 1000
|
||||
};
|
||||
}
|
||||
|
||||
let attempt = 0;
|
||||
|
||||
const operation = retry.operation(options);
|
||||
operation.attempt(function(currentAttempt) {
|
||||
checkFn((err, result) => {
|
||||
|
||||
if(!err) {
|
||||
return callback(undefined, result);
|
||||
}
|
||||
|
||||
console.log(new Date, err.message);
|
||||
|
||||
let maxAttempts = err.retry;
|
||||
if(maxAttempts === true)
|
||||
maxAttempts = 10;
|
||||
|
||||
if(err.retry && attempt++ < maxAttempts) {
|
||||
return operation.retry(err);
|
||||
}
|
||||
|
||||
if(err.notFatal) {
|
||||
if(err.backoffDelay) {
|
||||
return setTimeout(() => operation.retry(err), err.backoffDelay);
|
||||
}
|
||||
|
||||
return operation.retry(err);
|
||||
}
|
||||
|
||||
callback(err, result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// es6 bind all: https://github.com/posrix/es6-class-bind-all/blob/master/lib/es6ClassBindAll.js
|
||||
const allMethods = targetClass => {
|
||||
const propertys = Object.getOwnPropertyNames(Object.getPrototypeOf(targetClass))
|
||||
propertys.splice(propertys.indexOf('constructor'), 1)
|
||||
return propertys
|
||||
}
|
||||
|
||||
const bindAll = (targetClass, methodNames = []) => {
|
||||
for (const name of !methodNames.length ? allMethods(targetClass) : methodNames) {
|
||||
targetClass[name] = targetClass[name].bind(targetClass)
|
||||
}
|
||||
}
|
||||
|
||||
const isValidOrder = ({api, market, amount, price}) => {
|
||||
let reason = false;
|
||||
|
||||
// Check amount
|
||||
if(amount < market.minimalOrder.amount) {
|
||||
reason = 'Amount is too small';
|
||||
}
|
||||
|
||||
// Some exchanges have restrictions on prices
|
||||
if(
|
||||
_.isFunction(api.isValidPrice) &&
|
||||
!api.isValidPrice(price)
|
||||
) {
|
||||
reason = 'Price is not valid';
|
||||
}
|
||||
|
||||
if(
|
||||
_.isFunction(api.isValidLot) &&
|
||||
!api.isValidLot(price, amount)
|
||||
) {
|
||||
reason = 'Lot size is too small';
|
||||
}
|
||||
|
||||
return {
|
||||
reason,
|
||||
valid: !reason
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// https://gist.github.com/jiggzson/b5f489af9ad931e3d186
|
||||
const scientificToDecimal = num => {
|
||||
if(/\d+\.?\d*e[\+\-]*\d+/i.test(num)) {
|
||||
const zero = '0';
|
||||
const parts = String(num).toLowerCase().split('e'); // split into coeff and exponent
|
||||
const e = parts.pop(); // store the exponential part
|
||||
const l = Math.abs(e); // get the number of zeros
|
||||
const sign = e/l;
|
||||
const coeff_array = parts[0].split('.');
|
||||
if(sign === -1) {
|
||||
num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
|
||||
} else {
|
||||
const dec = coeff_array[1];
|
||||
if(dec) {
|
||||
l = l - dec.length;
|
||||
}
|
||||
num = coeff_array.join('') + new Array(l+1).join(zero);
|
||||
}
|
||||
} else {
|
||||
// make sure we always cast to string
|
||||
num = num + '';
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
// TEMP until we have proper scheduling
|
||||
const cacheFn = (fn, timeout) => {
|
||||
let nextCall = false;
|
||||
let cache = false;
|
||||
let inflight = false;
|
||||
let callbackQueue = [];
|
||||
|
||||
return next => {
|
||||
if(inflight) {
|
||||
return callbackQueue.push(next);
|
||||
}
|
||||
|
||||
const now = +new Date;
|
||||
if(cache && now >= nextCall) {
|
||||
return next(res.error, res.result);
|
||||
}
|
||||
|
||||
inflight = true;
|
||||
fn((error, result) => {
|
||||
cache = {error, result};
|
||||
nextCall = now + timeout;
|
||||
next(error, result);
|
||||
callbackQueue.forEach(cb => cb(error, result));
|
||||
callbackQueue = [];
|
||||
inflight = false;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
retry: retryInstance,
|
||||
bindAll,
|
||||
isValidOrder,
|
||||
scientificToDecimal,
|
||||
cacheFn
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
The broker manages all communicatinn with the exchange, delegating:
|
||||
|
||||
- the management of the portfolio to the portfolioManager
|
||||
- the management of actual trades to "orders".
|
||||
*/
|
||||
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const events = require('events');
|
||||
const moment = require('moment');
|
||||
const checker = require('./exchangeChecker');
|
||||
const errors = require('./exchangeErrors');
|
||||
const Portfolio = require('./portfolioManager');
|
||||
// const Market = require('./market');
|
||||
const orders = require('./orders');
|
||||
const Trigger = require('./trigger');
|
||||
const exchangeUtils = require('./exchangeUtils');
|
||||
const bindAll = exchangeUtils.bindAll;
|
||||
const isValidOrder = exchangeUtils.isValidOrder;
|
||||
|
||||
|
||||
|
||||
class Broker {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
|
||||
if(config.private) {
|
||||
if(this.cantTrade()) {
|
||||
throw new Error(this.cantTrade());
|
||||
}
|
||||
} else {
|
||||
if(this.cantMonitor()) {
|
||||
throw new Error(this.cantMonitor());
|
||||
}
|
||||
}
|
||||
|
||||
this.orders = {
|
||||
// contains current open orders
|
||||
open: [],
|
||||
// contains all closed orders
|
||||
closed: []
|
||||
}
|
||||
|
||||
const slug = config.exchange.toLowerCase();
|
||||
|
||||
const API = require('./wrappers/' + slug);
|
||||
|
||||
this.api = new API(config);
|
||||
|
||||
this.capabilities = API.getCapabilities();
|
||||
|
||||
this.marketConfig = _.find(this.capabilities.markets, (p) => {
|
||||
return _.first(p.pair) === config.currency.toUpperCase() &&
|
||||
_.last(p.pair) === config.asset.toUpperCase();
|
||||
});
|
||||
|
||||
if(config.customInterval) {
|
||||
this.interval = config.customInterval;
|
||||
this.api.interval = config.customInterval;
|
||||
console.log(new Date, '[GB] setting custom interval to', config.customInterval);
|
||||
} else {
|
||||
this.interval = this.api.interval || 1500;
|
||||
}
|
||||
|
||||
this.market = config.currency.toUpperCase() + config.asset.toUpperCase();
|
||||
|
||||
if(config.private) {
|
||||
this.portfolio = new Portfolio(config, this.api);
|
||||
}
|
||||
|
||||
bindAll(this);
|
||||
}
|
||||
|
||||
cantTrade() {
|
||||
return checker.cantTrade(this.config);
|
||||
}
|
||||
|
||||
cantMonitor() {
|
||||
return checker.cantMonitor(this.config);
|
||||
}
|
||||
|
||||
sync(callback) {
|
||||
if(!this.private) {
|
||||
this.setTicker();
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.cantTrade()) {
|
||||
throw new errors.ExchangeError(this.cantTrade());
|
||||
}
|
||||
|
||||
this.syncPrivateData();
|
||||
}
|
||||
|
||||
syncPrivateData(callback) {
|
||||
async.series([
|
||||
this.setTicker,
|
||||
next => setTimeout(next, this.interval),
|
||||
this.portfolio.setFee.bind(this.portfolio),
|
||||
next => setTimeout(next, this.interval),
|
||||
this.portfolio.setBalances.bind(this.portfolio),
|
||||
next => setTimeout(next, this.interval)
|
||||
], callback);
|
||||
}
|
||||
|
||||
setTicker(callback) {
|
||||
this.api.getTicker((err, ticker) => {
|
||||
|
||||
if(err) {
|
||||
if(err.message) {
|
||||
console.log(this.api.name, err.message);
|
||||
throw err;
|
||||
} else {
|
||||
console.log('err not wrapped in error:', err);
|
||||
throw new errors.ExchangeError(err);
|
||||
}
|
||||
}
|
||||
|
||||
this.ticker = ticker;
|
||||
|
||||
if(_.isFunction(callback))
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
isValidOrder(amount, price) {
|
||||
return isValidOrder({
|
||||
market: this.marketConfig,
|
||||
api: this.api,
|
||||
amount,
|
||||
price
|
||||
});
|
||||
}
|
||||
|
||||
createOrder(type, side, amount, parameters, handler) {
|
||||
if(!this.config.private)
|
||||
throw new Error('Client not authenticated');
|
||||
|
||||
if(side !== 'buy' && side !== 'sell')
|
||||
throw new Error('Unknown side ' + side);
|
||||
|
||||
if(!orders[type])
|
||||
throw new Error('Unknown order type');
|
||||
|
||||
const order = new orders[type]({
|
||||
api: this.api,
|
||||
marketConfig: this.marketConfig,
|
||||
capabilities: this.capabilities
|
||||
});
|
||||
|
||||
// todo: figure out a smarter generic way
|
||||
this.syncPrivateData(() => {
|
||||
order.setData({
|
||||
balances: this.portfolio.balances,
|
||||
ticker: this.ticker,
|
||||
});
|
||||
|
||||
order.create(side, amount, parameters);
|
||||
});
|
||||
|
||||
order.on('completed', summary => {
|
||||
_.remove(this.orders.open, order);
|
||||
this.orders.closed.push(summary);
|
||||
});
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
createTrigger({type, onTrigger, props}) {
|
||||
return new Trigger({
|
||||
api: this.api,
|
||||
type,
|
||||
onTrigger,
|
||||
props
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Broker;
|
||||
@@ -0,0 +1,5 @@
|
||||
const sticky = require('./sticky');
|
||||
|
||||
module.exports = {
|
||||
sticky
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// NOTE: this is currently broken, see
|
||||
// @link https://github.com/askmike/gekko/issues/2398
|
||||
|
||||
throw ':(';
|
||||
|
||||
/*
|
||||
The limit order is a simple order:
|
||||
- It is created at the specified price
|
||||
- If it were to cross it will throw instead (only if postOnly is specified)
|
||||
- It can be moved
|
||||
|
||||
*/
|
||||
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const events = require('events');
|
||||
const moment = require('moment');
|
||||
const errors = require('../exchangeErrors');
|
||||
const BaseOrder = require('./order');
|
||||
const states = require('./states');
|
||||
|
||||
class LimitOrder extends BaseOrder {
|
||||
constructor(api) {
|
||||
super(api);
|
||||
}
|
||||
|
||||
create(side, amount, params) {
|
||||
this.side = side;
|
||||
|
||||
this.postOnly = params.postOnly;
|
||||
|
||||
this.status = states.SUBMITTED;
|
||||
this.emitStatus();
|
||||
|
||||
this.createOrder(price, amount);
|
||||
}
|
||||
|
||||
createOrder(price, amount) {
|
||||
this.amount = this.api.roundAmount(amount);
|
||||
this.price = this.api.roundPrice(price);
|
||||
|
||||
// note: this assumes ticker data to be up to date
|
||||
if(this.postOnly) {
|
||||
if(side === 'buy' && this.price > this.data.ticker.ask)
|
||||
throw new Error('Order crosses the book');
|
||||
else if(side === 'sell' && this.price < this.data.ticker.bid)
|
||||
throw new Error('Order crosses the book');
|
||||
}
|
||||
|
||||
this.submit({
|
||||
side: this.side,
|
||||
amount: this.api.roundAmount(this.amount - alreadyFilled),
|
||||
price: this.price,
|
||||
alreadyFilled: this.filled
|
||||
});
|
||||
}
|
||||
|
||||
handleCreate(err, id) {
|
||||
if(err)
|
||||
throw err;
|
||||
|
||||
this.status = states.OPEN;
|
||||
this.emitStatus();
|
||||
|
||||
this.id = id;
|
||||
|
||||
if(this.cancelling)
|
||||
return this.cancel();
|
||||
|
||||
if(this.movingAmount)
|
||||
return this.moveAmount();
|
||||
|
||||
if(this.movingPrice)
|
||||
return this.movePrice();
|
||||
|
||||
this.timeout = setTimeout(this.checkOrder, this.checkInterval)
|
||||
}
|
||||
|
||||
checkOrder() {
|
||||
this.checking = true;
|
||||
this.api.checkOrder(this.id, this.handleCheck);
|
||||
}
|
||||
|
||||
handleCheck(err, result) {
|
||||
if(this.cancelling || this.status === states.CANCELLED)
|
||||
return;
|
||||
|
||||
this.checking = false;
|
||||
|
||||
if(err)
|
||||
throw err;
|
||||
|
||||
if(result.open) {
|
||||
if(result.filledAmount !== this.filledAmount) {
|
||||
this.filledAmount = result.filledAmount;
|
||||
|
||||
// note: doc event API
|
||||
this.emit('fill', this.filledAmount);
|
||||
}
|
||||
|
||||
if(this.cancelling)
|
||||
return this.cancel();
|
||||
|
||||
if(this.movingAmount)
|
||||
return this.moveAmount();
|
||||
|
||||
if(this.movingPrice)
|
||||
return this.movePrice();
|
||||
|
||||
this.timeout = setTimeout(this.checkOrder, this.checkInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!result.executed) {
|
||||
// not open and not executed means it never hit the book
|
||||
this.rejected();
|
||||
return;
|
||||
}
|
||||
|
||||
this.filled(this.price);
|
||||
}
|
||||
|
||||
movePrice(price) {
|
||||
if(this.completed)
|
||||
return;
|
||||
|
||||
if(!price)
|
||||
price = this.movePriceTo;
|
||||
|
||||
if(this.price === this.api.roundPrice(price))
|
||||
// effectively nothing changed
|
||||
return;
|
||||
|
||||
if(
|
||||
this.status === states.SUBMITTED ||
|
||||
this.status === states.MOVING ||
|
||||
this.checking
|
||||
) {
|
||||
this.movePriceTo = price;
|
||||
this.movingPrice = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.movingPrice = false;
|
||||
|
||||
this.price = this.api.roundPrice(price);
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
this.status = states.MOVING;
|
||||
|
||||
this.api.cancelOrder(this.id, (err, filled) => {
|
||||
if(err)
|
||||
throw err;
|
||||
|
||||
if(filled)
|
||||
return this.filled(this.price);
|
||||
|
||||
this.submit({
|
||||
side: this.side,
|
||||
amount: this.amount,
|
||||
price: this.price,
|
||||
alreadyFilled: this.filled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
moveAmount(amount) {
|
||||
if(this.completed)
|
||||
return;
|
||||
|
||||
if(!amount)
|
||||
amount = this.moveAmountTo;
|
||||
|
||||
if(this.amount === this.api.roundAmount(amount))
|
||||
// effectively nothing changed
|
||||
return;
|
||||
|
||||
if(
|
||||
this.status === states.SUBMITTED ||
|
||||
this.status === states.MOVING ||
|
||||
this.checking
|
||||
) {
|
||||
this.moveAmountTo = amount;
|
||||
this.movingAmount = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.movingAmount = false;
|
||||
this.amount = this.api.roundAmount(amount);
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
this.status = states.MOVING;
|
||||
this.emitStatus();
|
||||
|
||||
this.api.cancelOrder(this.id, (err, filled) => {
|
||||
if(err)
|
||||
throw err;
|
||||
|
||||
if(filled)
|
||||
return this.filled(this.price);
|
||||
|
||||
this.submit({
|
||||
side: this.side,
|
||||
amount: this.amount,
|
||||
price: this.price,
|
||||
alreadyFilled: this.filled
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if(this.completed)
|
||||
return;
|
||||
|
||||
if(
|
||||
this.status === states.SUBMITTED ||
|
||||
this.status === states.MOVING ||
|
||||
this.checking
|
||||
) {
|
||||
this.cancelling = true;
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
this.api.cancelOrder(this.id, (err, filled) => {
|
||||
if(err)
|
||||
throw err;
|
||||
|
||||
this.cancelling = false;
|
||||
|
||||
if(filled)
|
||||
return this.filled(this.price);
|
||||
|
||||
this.status = states.CANCELLED;
|
||||
this.emitStatus();
|
||||
this.finish(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LimitOrder;
|
||||
@@ -0,0 +1,88 @@
|
||||
const EventEmitter = require('events');
|
||||
const _ = require('lodash');
|
||||
|
||||
const exchangeUtils = require('../exchangeUtils');
|
||||
const bindAll = exchangeUtils.bindAll;
|
||||
const isValidOrder = exchangeUtils.isValidOrder;
|
||||
const states = require('./states');
|
||||
|
||||
// base order
|
||||
|
||||
class BaseOrder extends EventEmitter {
|
||||
constructor(api) {
|
||||
super();
|
||||
|
||||
this.api = api;
|
||||
|
||||
this.checkInterval = api.interval || 1500;
|
||||
this.status = states.INITIALIZING;
|
||||
|
||||
this.completed = false;
|
||||
this.completing = false;
|
||||
|
||||
bindAll(this);
|
||||
}
|
||||
|
||||
submit({side, amount, price, alreadyFilled}) {
|
||||
const check = isValidOrder({
|
||||
market: this.market,
|
||||
api: this.api,
|
||||
amount,
|
||||
price
|
||||
});
|
||||
|
||||
if(!check.valid) {
|
||||
if(alreadyFilled) {
|
||||
// partially filled, but the remainder is too
|
||||
// small.
|
||||
return this.filled();
|
||||
}
|
||||
|
||||
this.emit('invalidOrder', check.reason);
|
||||
this.rejected(check.reason);
|
||||
}
|
||||
|
||||
this.api[this.side](amount, this.price, this.handleCreate);
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
emitStatus() {
|
||||
this.emit('statusChange', this.status);
|
||||
}
|
||||
|
||||
cancelled() {
|
||||
this.status = states.CANCELLED;
|
||||
this.emitStatus();
|
||||
this.completed = true;
|
||||
this.finish();
|
||||
}
|
||||
|
||||
rejected(reason) {
|
||||
this.rejectedReason = reason;
|
||||
this.status = states.REJECTED;
|
||||
this.emitStatus();
|
||||
console.log(new Date, 'sticky rejected', reason)
|
||||
this.finish();
|
||||
}
|
||||
|
||||
filled(price) {
|
||||
this.status = states.FILLED;
|
||||
this.emitStatus();
|
||||
this.completed = true;
|
||||
console.log(new Date, 'sticky filled')
|
||||
this.finish(true);
|
||||
}
|
||||
|
||||
finish(filled) {
|
||||
this.completed = true;
|
||||
this.emit('completed', {
|
||||
status: this.status,
|
||||
filled
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseOrder;
|
||||
@@ -0,0 +1,34 @@
|
||||
const states = {
|
||||
// Not created
|
||||
INITIALIZING: 'INITIALIZING',
|
||||
|
||||
// Created and send to the exchange, but no acknowledgement received yet
|
||||
SUBMITTED: 'SUBMITTED',
|
||||
|
||||
// In the process of moving the order
|
||||
MOVING: 'MOVING',
|
||||
|
||||
// Order is open on the exchange
|
||||
OPEN: 'OPEN',
|
||||
|
||||
|
||||
CHECKING: 'CHECKING',
|
||||
|
||||
CHECKED: 'CHECKED',
|
||||
|
||||
// the orders below indicate a fully completed order
|
||||
|
||||
|
||||
// Order is completely filled
|
||||
FILLED: 'FILLED',
|
||||
|
||||
// Order was succesfully cancelled
|
||||
CANCELLED: 'CANCELLED',
|
||||
|
||||
// Order was rejected by the exchange
|
||||
REJECTED: 'REJECTED',
|
||||
|
||||
ERROR: 'ERROR'
|
||||
}
|
||||
|
||||
module.exports = states;
|
||||
@@ -0,0 +1,754 @@
|
||||
/*
|
||||
The sticky order is an advanced order:
|
||||
- It is created at a limit price of X
|
||||
- if limit is not specified always at bbo.
|
||||
- if limit is specified the price is either limit or the bbo (whichever is more favorable)
|
||||
- it will readjust the order:
|
||||
- if outbid is true it will outbid the current bbo (on supported exchanges)
|
||||
- if outbid is false it will stick to current bbo when this moves
|
||||
- If the price moves away from the order it will "stick to" the top
|
||||
|
||||
TODO:
|
||||
- native move
|
||||
*/
|
||||
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const events = require('events');
|
||||
const moment = require('moment');
|
||||
const errors = require('../exchangeErrors');
|
||||
const BaseOrder = require('./order');
|
||||
const states = require('./states');
|
||||
|
||||
class StickyOrder extends BaseOrder {
|
||||
constructor({api, marketConfig, capabilities}) {
|
||||
super(api);
|
||||
|
||||
this.market = marketConfig;
|
||||
this.capabilities = capabilities;
|
||||
|
||||
// global async lock
|
||||
this.sticking = false;
|
||||
|
||||
// bound helpers
|
||||
this.roundPrice = this.api.roundPrice.bind(this.api);
|
||||
this.roundAmount = this.api.roundAmount.bind(this.api);
|
||||
if(_.isFunction(this.api.outbidPrice)) {
|
||||
this.outbidPrice = this.api.outbidPrice.bind(this.api);
|
||||
}
|
||||
|
||||
this.debug = this.api.name === 'Deribit2';
|
||||
this.log = m => this.debug && console.log(new Date, m);
|
||||
}
|
||||
|
||||
create(side, rawAmount, params = {}) {
|
||||
if(this.completed || this.completing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(new Date, 'sticky create', side);
|
||||
|
||||
this.side = side;
|
||||
|
||||
this.amount = this.roundAmount(rawAmount);
|
||||
|
||||
this.initialLimit = params.initialLimit;
|
||||
|
||||
if(side === 'buy') {
|
||||
if(params.limit) {
|
||||
this.limit = this.roundPrice(params.limit);
|
||||
} else {
|
||||
this.noLimit = true;
|
||||
this.limit = Infinity;
|
||||
}
|
||||
} else {
|
||||
if(params.limit) {
|
||||
this.limit = this.roundPrice(params.limit);
|
||||
} else {
|
||||
this.noLimit = true;
|
||||
this.limit = -Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
this.status = states.SUBMITTED;
|
||||
this.emitStatus();
|
||||
|
||||
this.orders = {};
|
||||
|
||||
this.outbid = params.outbid && _.isFunction(this.outbidPrice);
|
||||
|
||||
if(this.data && this.data.ticker) {
|
||||
this.price = this.calculatePrice(this.data.ticker);
|
||||
this.createOrder();
|
||||
} else {
|
||||
this.api.getTicker((err, ticker) => {
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'error get ticker');
|
||||
return;
|
||||
}
|
||||
|
||||
this.price = this.calculatePrice(ticker);
|
||||
this.createOrder();
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
calculatePrice(ticker) {
|
||||
|
||||
const r = this.roundPrice;
|
||||
|
||||
if(this.initialLimit && !this.id) {
|
||||
console.log('passing initial limit of:', this.limit);
|
||||
return r(this.limit);
|
||||
}
|
||||
|
||||
if(this.side === 'buy') {
|
||||
|
||||
if(!this.noLimit && ticker.bid >= this.limit) {
|
||||
return r(this.limit);
|
||||
}
|
||||
|
||||
if(!this.outbid) {
|
||||
return r(ticker.bid);
|
||||
}
|
||||
|
||||
const outbidPrice = this.outbidPrice(ticker.bid, true);
|
||||
|
||||
if(outbidPrice <= this.limit && outbidPrice < ticker.ask) {
|
||||
return r(outbidPrice);
|
||||
} else {
|
||||
return r(this.limit);
|
||||
}
|
||||
|
||||
} else if(this.side === 'sell') {
|
||||
|
||||
if(!this.noLimit && ticker.ask <= this.limit) {
|
||||
return r(this.limit);
|
||||
}
|
||||
|
||||
if(!this.outbid) {
|
||||
return r(ticker.ask);
|
||||
}
|
||||
|
||||
const outbidPrice = this.outbidPrice(ticker.ask, false);
|
||||
|
||||
if(outbidPrice >= this.limit && outbidPrice > ticker.bid) {
|
||||
return r(outbidPrice);
|
||||
} else {
|
||||
return r(this.limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createOrder() {
|
||||
if(this.completed || this.completing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const alreadyFilled = this.calculateFilled();
|
||||
|
||||
this.submit({
|
||||
side: this.side,
|
||||
amount: this.roundAmount(this.amount - alreadyFilled),
|
||||
price: this.price,
|
||||
alreadyFilled
|
||||
});
|
||||
}
|
||||
|
||||
// check if the last order was partially filled
|
||||
// on an exchange that does not pass fill data on cancel
|
||||
// see https://github.com/askmike/gekko/pull/2450
|
||||
handleInsufficientFundsError(err) {
|
||||
if(
|
||||
!err ||
|
||||
err.type !== 'insufficientFunds' ||
|
||||
!this.capabilities.limitedCancelConfirmation ||
|
||||
!this.id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const id = this.id;
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
this.api.getOrder(id, (innerError, res) => {
|
||||
if(this.handleError(innerError)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = res.amount;
|
||||
|
||||
if(this.orders[id].filled === amount) {
|
||||
// handle original error
|
||||
return this.handleError(err);
|
||||
}
|
||||
|
||||
this.orders[id].filled = amount;
|
||||
this.emit('fill', this.calculateFilled());
|
||||
if(this.calculateFilled() >= this.amount) {
|
||||
return this.filled(this.price);
|
||||
}
|
||||
|
||||
setTimeout(this.createOrder, this.checkInterval);
|
||||
});
|
||||
},
|
||||
this.checkInterval
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
handleCreate(err, id) {
|
||||
|
||||
if(this.handleInsufficientFundsError(err)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'handleCreate error');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!id) {
|
||||
console.log('BLUP! no id...');
|
||||
}
|
||||
|
||||
// potentailly clean up old order
|
||||
if(
|
||||
this.id &&
|
||||
this.orders[this.id] &&
|
||||
this.orders[this.id].filled === 0
|
||||
)
|
||||
delete this.orders[this.id];
|
||||
|
||||
// register new order
|
||||
this.log(`${this.side} old id: ${this.id} new id: ${id}`);
|
||||
this.id = id;
|
||||
|
||||
this.orders[id] = {
|
||||
price: this.price,
|
||||
filled: 0
|
||||
}
|
||||
|
||||
this.emit('new order', this.id);
|
||||
this.emit('movelimit handled', new Date);
|
||||
|
||||
this.status = states.OPEN;
|
||||
this.emitStatus();
|
||||
|
||||
this.scheduleNextCheck();
|
||||
}
|
||||
|
||||
initiateDefferedAction() {
|
||||
// check whether we had an action pending
|
||||
if(this.cancelling) {
|
||||
this.cancel();
|
||||
return true;
|
||||
}
|
||||
|
||||
if(this.movingLimit && this.moveLimit()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(this.movingAmount) {
|
||||
this.moveAmount();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
scheduleNextCheck() {
|
||||
|
||||
// remove lock
|
||||
this.sticking = false;
|
||||
|
||||
if(this.initiateDefferedAction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// register check
|
||||
this.timeout = setTimeout(this.checkOrder, this.checkInterval);
|
||||
|
||||
}
|
||||
|
||||
checkOrder() {
|
||||
|
||||
if(this.completed || this.completing) {
|
||||
return console.log(new Date, this.side, 'checkOrder called on completed/completing order..', this.completed, this.completing);
|
||||
}
|
||||
|
||||
if(this.status === states.MOVING) {
|
||||
return console.log(new Date, this.side, 'refusing to check, in the middle of move');
|
||||
}
|
||||
|
||||
if(this.initiateDefferedAction()) {
|
||||
console.log(new Date, this.side, 'skipping check logic, better things to do - 0');
|
||||
return;
|
||||
}
|
||||
|
||||
this.sticking = true;
|
||||
const checkId = this.id;
|
||||
|
||||
this.api.checkOrder(this.id, (err, result) => {
|
||||
|
||||
if(!this.debug) {
|
||||
if(this.ignoreCheckResult) {
|
||||
this.log(this.side + ' debug ignoring check result');
|
||||
this.ignoreCheckResult = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(this.status === states.MOVING) {
|
||||
this.log(`${this.side} ${this.id} debug ignoring check result - in the middle of move`);
|
||||
this.ignoreCheckResult = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if(checkId !== this.id) {
|
||||
this.log(this.side + ' debug got check on old id ' + checkId + ', ' + this.id);
|
||||
this.ignoreCheckResult = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = states.CHECKED;
|
||||
this.emitStatus();
|
||||
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'checkOrder error');
|
||||
return;
|
||||
}
|
||||
|
||||
if(result.open) {
|
||||
if(result.filledAmount !== this.orders[this.id].filled) {
|
||||
this.orders[this.id].filled = result.filledAmount;
|
||||
this.emit('fill', this.calculateFilled());
|
||||
}
|
||||
|
||||
// if we are already at limit we dont care where the top is
|
||||
// note: might be string VS float
|
||||
if(this.price == this.limit) {
|
||||
this.scheduleNextCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.initiateDefferedAction()) {
|
||||
console.log(new Date, this.side, 'skipping check ticker logic, better things to do 1');
|
||||
return;
|
||||
}
|
||||
|
||||
this.api.getTicker((err, ticker) => {
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'getTicker error');
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.initiateDefferedAction()) {
|
||||
console.log(new Date, this.side, 'skipping check ticker logic, better things to do 2');
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.status === states.MOVING) {
|
||||
this.log(`${this.side} ${this.id} debug ignoring check result - in the middle of move ---- 2`);
|
||||
this.ignoreCheckResult = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.ticker = ticker;
|
||||
this.emit('ticker', ticker);
|
||||
|
||||
const bookSide = this.side === 'buy' ? 'bid' : 'ask';
|
||||
// note: might be string VS float
|
||||
if(ticker[bookSide] != this.price) {
|
||||
return this.move(this.calculatePrice(ticker));
|
||||
} else {
|
||||
this.scheduleNextCheck();
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// it's not open right now
|
||||
// meaning we are done
|
||||
this.sticking = false;
|
||||
|
||||
if(!result.executed) {
|
||||
// not open and not executed means it never hit the book
|
||||
console.log(this.side, this.status, this.id, 'not open not executed!', result);
|
||||
this.rejected();
|
||||
throw 'a';
|
||||
return;
|
||||
}
|
||||
|
||||
// order got filled!
|
||||
this.orders[this.id].filled = this.amount;
|
||||
this.emit('fill', this.amount);
|
||||
this.filled(this.price);
|
||||
});
|
||||
|
||||
|
||||
this.status = states.CHECKING;
|
||||
this.emitStatus();
|
||||
}
|
||||
|
||||
// global error handler
|
||||
handleError(error) {
|
||||
if(!error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(new Date, '[sticky order] FATAL ERROR', error.message);
|
||||
console.log(new Date, error);
|
||||
this.status = states.ERROR;
|
||||
this.emitStatus();
|
||||
this.error = error;
|
||||
|
||||
this.emit('error', error);
|
||||
return true;
|
||||
}
|
||||
|
||||
// returns true if the order was fully filled
|
||||
// handles partial fills on cancels calls
|
||||
// on exchanges that support it.
|
||||
handleCancel(filled, data) {
|
||||
// it got filled before we could cancel
|
||||
if(filled) {
|
||||
this.orders[this.id].filled = this.amount;
|
||||
this.emit('fill', this.amount);
|
||||
this.filled(this.price);
|
||||
return;
|
||||
}
|
||||
|
||||
// if we have data on partial fills
|
||||
// check whether we had a partial fill
|
||||
if(_.isObject(data)) {
|
||||
let amountFilled = data.filled;
|
||||
|
||||
if(!amountFilled && data.remaining) {
|
||||
const alreadyFilled = this.calculateFilled();
|
||||
const orderAmount = this.roundAmount(this.amount - alreadyFilled);
|
||||
amountFilled = this.roundAmount(orderAmount - data.remaining);
|
||||
}
|
||||
|
||||
if(amountFilled > this.orders[this.id].filled) {
|
||||
this.orders[this.id].filled = amountFilled;
|
||||
this.emit('fill', this.calculateFilled());
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
move(price) {
|
||||
if(this.completed || this.completing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.status = states.MOVING;
|
||||
this.emitStatus();
|
||||
|
||||
this.log(`${this.side} ${this.id} this.move 1`);
|
||||
|
||||
const cancelId = this.id;
|
||||
|
||||
this.api.cancelOrder(cancelId, (err, filled, data) => {
|
||||
this.log(`${this.side} ${this.id} this.move 2`);
|
||||
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'error move');
|
||||
return;
|
||||
}
|
||||
|
||||
// it got filled before we could cancel
|
||||
if(cancelId === this.id && this.handleCancel(filled, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update to new price
|
||||
this.price = this.roundPrice(price);
|
||||
|
||||
this.createOrder();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
calculateFilled() {
|
||||
let totalFilled = 0;
|
||||
_.each(this.orders, (order, id) => totalFilled += order.filled);
|
||||
|
||||
return totalFilled;
|
||||
}
|
||||
|
||||
moveLimit(limit) {
|
||||
if(this.completed || this.completing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!limit) {
|
||||
limit = this.moveLimitTo;
|
||||
// console.log(new Date, this.side, 'debug resuming move!');
|
||||
} else {
|
||||
this.limitRequested = new Date;
|
||||
this.log(this.side + ' received move limit');
|
||||
}
|
||||
|
||||
if(this.limit === this.roundPrice(limit)) {
|
||||
// effectively nothing changed
|
||||
return false;
|
||||
}
|
||||
|
||||
if(this.cancelling) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.emit('movelimit', new Date);
|
||||
|
||||
if(
|
||||
this.status === states.INITIALIZING ||
|
||||
this.status === states.SUBMITTED ||
|
||||
this.status === states.MOVING ||
|
||||
this.status === states.CHECKING ||
|
||||
this.sticking
|
||||
) {
|
||||
|
||||
if(
|
||||
!this.api.fillDataOnCancel ||
|
||||
this.status === states.MOVING ||
|
||||
this.status === states.SUBMITTED
|
||||
) {
|
||||
// console.log(new Date, '[sticky]', this.side, 'skipping because:', this.status);
|
||||
this.moveLimitTo = limit;
|
||||
this.movingLimit = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.limit = this.roundPrice(limit);
|
||||
this.movingLimit = false;
|
||||
|
||||
const moveBuy = this.side === 'buy' && this.limit < this.price;
|
||||
const moveSell = this.side === 'sell' && this.limit > this.price;
|
||||
|
||||
if(moveBuy || moveSell) {
|
||||
this.sticking = true;
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
if(this.api.fillDataOnCancel && this.status === states.CHECKING) {
|
||||
this.log(this.side + 'debug move overwrites running check ' + this.status + ' ' + this.id);
|
||||
this.ignoreCheckResult = true;
|
||||
}
|
||||
|
||||
this.log(this.side + ' moving ' + this.id);
|
||||
|
||||
const timeToMove = new Date - this.limitRequested;
|
||||
if(timeToMove > 300) {
|
||||
console.log(new Date, this.side, 'long time to move:', timeToMove);
|
||||
}
|
||||
this.move(this.limit);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.emit('movelimit handled', new Date);
|
||||
return false;
|
||||
}
|
||||
|
||||
moveAmount(amount) {
|
||||
if(this.completed || this.completing)
|
||||
return false;
|
||||
|
||||
if(!amount)
|
||||
amount = this.moveAmountTo;
|
||||
|
||||
if(this.amount === this.roundAmount(amount))
|
||||
// effectively nothing changed
|
||||
return true;
|
||||
|
||||
if(this.calculateFilled() > this.roundAmount(amount)) {
|
||||
// the amount is now below how much we have
|
||||
// already filled.
|
||||
this.filled();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(
|
||||
this.status === states.INITIALIZING ||
|
||||
this.status === states.SUBMITTED ||
|
||||
this.status === states.MOVING ||
|
||||
this.sticking
|
||||
) {
|
||||
this.moveAmountTo = amount;
|
||||
this.movingAmount = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.amount = this.roundAmount(amount - this.calculateFilled());
|
||||
|
||||
if(this.amount < this.market.minimalOrder.amount) {
|
||||
if(this.calculateFilled()) {
|
||||
// we already filled enough of the order!
|
||||
this.filled();
|
||||
return false;
|
||||
} else {
|
||||
this.handleError(new Error("The amount " + this.amount + " is too small."));
|
||||
}
|
||||
}
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
this.movingAmount = false;
|
||||
this.sticking = true;
|
||||
|
||||
this.api.cancelOrder(this.id, (err, filled, data) => {
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'error cancel');
|
||||
return;
|
||||
}
|
||||
|
||||
// it got filled before we could cancel
|
||||
if(this.handleCancel(filled, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.createOrder();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if(this.completed)
|
||||
return;
|
||||
|
||||
if(
|
||||
this.status === states.SUBMITTED ||
|
||||
this.status === states.MOVING ||
|
||||
this.sticking
|
||||
) {
|
||||
this.cancelling = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.completing = true;
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
this.api.cancelOrder(this.id, (err, filled, data) => {
|
||||
if(this.handleError(err)) {
|
||||
console.log(new Date, 'error cancel');
|
||||
return;
|
||||
}
|
||||
|
||||
this.cancelling = false;
|
||||
|
||||
// it got filled before we could cancel
|
||||
if(this.handleCancel(filled, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.status = states.CANCELLED;
|
||||
this.emitStatus();
|
||||
|
||||
this.finish(false);
|
||||
})
|
||||
}
|
||||
|
||||
createSummary(next) {
|
||||
if(!this.completed)
|
||||
console.log(new Date, 'createSummary BUT ORDER NOT COMPLETED!');
|
||||
|
||||
if(!next)
|
||||
next = _.noop;
|
||||
|
||||
const checkOrders = _.keys(this.orders)
|
||||
.map(id => next => {
|
||||
|
||||
if(!this.orders[id].filled) {
|
||||
return next();
|
||||
}
|
||||
|
||||
setTimeout(() => this.api.getOrder(id, next), this.checkInterval);
|
||||
});
|
||||
|
||||
async.series(checkOrders, (err, trades) => {
|
||||
// note this is a standalone function after the order is
|
||||
// completed, as such we do not use the handleError flow.
|
||||
if(err) {
|
||||
console.log(new Date, 'error createSummary (checkOrder)')
|
||||
return next(err);
|
||||
}
|
||||
|
||||
let price = 0;
|
||||
let amount = 0;
|
||||
let date = moment(0);
|
||||
|
||||
_.each(trades, trade => {
|
||||
if(!trade) {
|
||||
return;
|
||||
}
|
||||
|
||||
// last fill counts
|
||||
date = moment(trade.date);
|
||||
price = ((price * amount) + (+trade.price * trade.amount)) / (+trade.amount + amount);
|
||||
amount += +trade.amount;
|
||||
});
|
||||
|
||||
const summary = {
|
||||
price,
|
||||
amount,
|
||||
date,
|
||||
side: this.side,
|
||||
orders: trades.length
|
||||
}
|
||||
|
||||
const first = _.first(trades);
|
||||
|
||||
if(first && first.fees) {
|
||||
summary.fees = {};
|
||||
|
||||
_.each(trades, trade => {
|
||||
if(!trade) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(trade.fees, (amount, currency) => {
|
||||
if(!_.isNumber(summary.fees[currency])) {
|
||||
summary.fees[currency] = amount;
|
||||
} else {
|
||||
summary.fees[currency] += amount;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(first && !_.isUndefined(first.feePercent)) {
|
||||
summary.feePercent = 0;
|
||||
let amount = 0;
|
||||
|
||||
_.each(trades, trade => {
|
||||
if(!trade || _.isUndefined(trade.feePercent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(trade.feePercent === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
summary.feePercent = ((summary.feePercent * amount) + (+trade.feePercent * trade.amount)) / (+trade.amount + amount);
|
||||
amount += +trade.amount;
|
||||
});
|
||||
}
|
||||
|
||||
this.emit('summary', summary);
|
||||
next(undefined, summary);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = StickyOrder;
|
||||
Generated
+823
@@ -0,0 +1,823 @@
|
||||
{
|
||||
"name": "gekko-broker",
|
||||
"version": "0.6.8",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@types/caseless": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz",
|
||||
"integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A=="
|
||||
},
|
||||
"@types/form-data": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz",
|
||||
"integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.5.7.tgz",
|
||||
"integrity": "sha512-VkKcfuitP+Nc/TaTFH0B8qNmn+6NbI6crLkQonbedViVz7O2w8QV/GERPlkJ4bg42VGHiEWa31CoTOPs1q6z1w=="
|
||||
},
|
||||
"@types/request": {
|
||||
"version": "2.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.0.tgz",
|
||||
"integrity": "sha512-/KXM5oev+nNCLIgBjkwbk8VqxmzI56woD4VUxn95O+YeQ8hJzcSmIZ1IN3WexiqBb6srzDo2bdMbsXxgXNkz5Q==",
|
||||
"requires": {
|
||||
"@types/caseless": "*",
|
||||
"@types/form-data": "*",
|
||||
"@types/node": "*",
|
||||
"@types/tough-cookie": "*"
|
||||
}
|
||||
},
|
||||
"@types/tough-cookie": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.3.tgz",
|
||||
"integrity": "sha512-MDQLxNFRLasqS4UlkWMSACMKeSm1x4Q3TxzUC7KQUsh6RK1ZrQ0VEyE3yzXcBu+K8ejVj4wuX32eUG02yNp+YQ=="
|
||||
},
|
||||
"ajv": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
|
||||
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
|
||||
"requires": {
|
||||
"co": "^4.6.0",
|
||||
"fast-deep-equal": "^1.0.0",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
|
||||
"requires": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
|
||||
"integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.10"
|
||||
}
|
||||
},
|
||||
"async-limiter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
|
||||
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
|
||||
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
|
||||
},
|
||||
"babel-runtime": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
||||
"requires": {
|
||||
"core-js": "^2.4.0",
|
||||
"regenerator-runtime": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"bignumber.js": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-6.0.0.tgz",
|
||||
"integrity": "sha512-x247jIuy60/+FtMRvscqfxtVHQf8AGx2hm9c6btkgC0x/hp9yt+teISNhvF8WlwRkCc5yF2fDECH8SIMe8j+GA=="
|
||||
},
|
||||
"binance": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/binance/-/binance-1.3.3.tgz",
|
||||
"integrity": "sha512-1eV2QUoH/Z0FZPiGjigJg4udXV9Uu6Clr0Sg1xsX3xStgPfzXz0juA3mllQIiIaHx7dmfAQgEiZIyeJLx5ajag==",
|
||||
"requires": {
|
||||
"request": "^2.81.0",
|
||||
"underscore": "^1.8.3",
|
||||
"ws": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"bintrees": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
|
||||
"integrity": "sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg="
|
||||
},
|
||||
"bitfinex-api-node": {
|
||||
"version": "2.0.0-beta.1",
|
||||
"resolved": "https://registry.npmjs.org/bitfinex-api-node/-/bitfinex-api-node-2.0.0-beta.1.tgz",
|
||||
"integrity": "sha512-UVKh4PAaijplm/VWs3ZLONQo8mhF0bemDZjEQCKtJteNlB+crmqRp/fPuWC2ZTYX2ZQ//J0zriOhEUiuwwGSDA==",
|
||||
"requires": {
|
||||
"bignumber.js": "^6.0.0",
|
||||
"cbq": "0.0.1",
|
||||
"crc-32": "^1.2.0",
|
||||
"debug": "^2.2.0",
|
||||
"lodash": "^4.17.4",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"request": "^2.67.0",
|
||||
"request-promise": "^4.2.0",
|
||||
"ws": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"bitx": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bitx/-/bitx-1.5.1.tgz",
|
||||
"integrity": "sha1-+h9cYuhIEFse1Pj2tIIwd4jmMq0="
|
||||
},
|
||||
"bluebird": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
|
||||
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
|
||||
},
|
||||
"caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||
},
|
||||
"cbq": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cbq/-/cbq-0.0.1.tgz",
|
||||
"integrity": "sha512-MCLjfpHAcI3gPdx26xoQx2JqRdNMf68ovhJkqNSiZIx/yQP6yNYYKbf8ww2J6kgNTPGIrXyugFlNz5jmGtg8BQ=="
|
||||
},
|
||||
"cloudscraper": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cloudscraper/-/cloudscraper-1.5.0.tgz",
|
||||
"integrity": "sha512-bZagLhj59+N6Z6lD9zRksYu87GthLwXdKARULi4RZ6UVpotH39ruSFN3UQmw3uuqoj00iDxkGrapAvxeurmlQA==",
|
||||
"requires": {
|
||||
"request": "^2.49.0"
|
||||
}
|
||||
},
|
||||
"co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
|
||||
},
|
||||
"coinfalcon": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/coinfalcon/-/coinfalcon-1.0.5.tgz",
|
||||
"integrity": "sha512-TyzLmcE2Vmll6oCyZSdEkla/thVZPjLxhDwlPyYKB5/Uv5wf/dzwox3Q2DrnJcOKRAQ1bKcHWmR2dFQKaEoK+A==",
|
||||
"requires": {
|
||||
"babel-runtime": "^6.26.0"
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
|
||||
"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.5.7",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz",
|
||||
"integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw=="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"crc-32": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
|
||||
"integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
|
||||
"requires": {
|
||||
"exit-on-epipe": "~1.0.1",
|
||||
"printj": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"crypto-js": {
|
||||
"version": "3.1.9-1",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz",
|
||||
"integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg="
|
||||
},
|
||||
"dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
|
||||
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
|
||||
"requires": {
|
||||
"mimic-response": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"duplexer3": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
|
||||
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"exit-on-epipe": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
|
||||
"integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw=="
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
|
||||
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
|
||||
"integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"gdax": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/gdax/-/gdax-0.7.0.tgz",
|
||||
"integrity": "sha512-lJHXlGchJVNtql8VWH+Idalehl5T5N6O9g5MUEW7IOuPtsbb7D15vgz6MOx1NgSyZe0fSIINv9s0HxujYB3sqg==",
|
||||
"requires": {
|
||||
"@types/request": "2.47.0",
|
||||
"bignumber.js": "^5.0.0",
|
||||
"bintrees": "^1.0.1",
|
||||
"request": "^2.81.0",
|
||||
"ws": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bignumber.js": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-5.0.0.tgz",
|
||||
"integrity": "sha512-KWTu6ZMVk9sxlDJQh2YH1UOnfDP8O8TpxUxgQG/vKASoSnEjK9aVuOueFaPcQEYQ5fyNXNTOYwYw3099RYebWg=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz",
|
||||
"integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==",
|
||||
"requires": {
|
||||
"async-limiter": "~1.0.0",
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gekko-bittrex": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/gekko-bittrex/-/gekko-bittrex-0.8.5.tgz",
|
||||
"integrity": "sha512-yA4FV1MjVXIX5/vpKGtZnh28OG9Mj+R2Dcb9v71OBPan6uHw4HYfX8gM6RBwlLmiVGxhyg5uDsHBiYab9yr86A==",
|
||||
"requires": {
|
||||
"cloudscraper": "^1.5.0",
|
||||
"jsonic": "^0.3.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"request": "^2.87.0",
|
||||
"signalr-client": "0.0.17"
|
||||
}
|
||||
},
|
||||
"gekko-broker-poloniex": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/gekko-broker-poloniex/-/gekko-broker-poloniex-0.0.12.tgz",
|
||||
"integrity": "sha512-fyR+zljTvRmKJoPoBl8oQl91sLmZy6xoitIUjjF+xV1vUKzwYJ3uHaG8VPQ3JP4sHcKZ6pZfbkftCepPEOph3g==",
|
||||
"requires": {
|
||||
"nonce": "~1.0.4"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
|
||||
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
|
||||
},
|
||||
"getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"got": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz",
|
||||
"integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==",
|
||||
"requires": {
|
||||
"decompress-response": "^3.2.0",
|
||||
"duplexer3": "^0.1.4",
|
||||
"get-stream": "^3.0.0",
|
||||
"is-plain-obj": "^1.1.0",
|
||||
"is-retry-allowed": "^1.0.0",
|
||||
"is-stream": "^1.0.0",
|
||||
"isurl": "^1.0.0-alpha5",
|
||||
"lowercase-keys": "^1.0.0",
|
||||
"p-cancelable": "^0.3.0",
|
||||
"p-timeout": "^1.1.1",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"timed-out": "^4.0.0",
|
||||
"url-parse-lax": "^1.0.0",
|
||||
"url-to-options": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
|
||||
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
|
||||
"requires": {
|
||||
"ajv": "^5.1.0",
|
||||
"har-schema": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"has-symbol-support-x": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz",
|
||||
"integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw=="
|
||||
},
|
||||
"has-to-string-tag-x": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz",
|
||||
"integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==",
|
||||
"requires": {
|
||||
"has-symbol-support-x": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"jsprim": "^1.2.2",
|
||||
"sshpk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"is-object": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz",
|
||||
"integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA="
|
||||
},
|
||||
"is-plain-obj": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
|
||||
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
|
||||
},
|
||||
"is-retry-allowed": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
|
||||
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ="
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
|
||||
},
|
||||
"isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
|
||||
},
|
||||
"isurl": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz",
|
||||
"integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==",
|
||||
"requires": {
|
||||
"has-to-string-tag-x": "^1.2.0",
|
||||
"is-object": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
|
||||
"optional": true
|
||||
},
|
||||
"json-schema": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
|
||||
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
|
||||
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||
},
|
||||
"jsonic": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonic/-/jsonic-0.3.0.tgz",
|
||||
"integrity": "sha1-tUXalfVDkuWLPdoF9fLjd6bJ0b8="
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"extsprintf": "1.3.0",
|
||||
"json-schema": "0.2.3",
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"kraken-api": {
|
||||
"version": "github:askmike/npm-kraken-api#a69dfb3eb296b9c795cfa48dd31edcdc1b3d5398",
|
||||
"from": "github:askmike/npm-kraken-api#a69dfb3eb296b9c795cfa48dd31edcdc1b3d5398",
|
||||
"requires": {
|
||||
"got": "^7.1.0",
|
||||
"qs": ">=6.4.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.10",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
|
||||
"integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg=="
|
||||
},
|
||||
"lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
|
||||
"integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.35.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz",
|
||||
"integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.19",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz",
|
||||
"integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==",
|
||||
"requires": {
|
||||
"mime-db": "~1.35.0"
|
||||
}
|
||||
},
|
||||
"mimic-response": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.22.2",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
|
||||
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
|
||||
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
|
||||
},
|
||||
"nonce": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/nonce/-/nonce-1.0.4.tgz",
|
||||
"integrity": "sha1-7nMCrejBvvR28wG4yR9cxRpIdhI="
|
||||
},
|
||||
"oauth-sign": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
|
||||
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM="
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
},
|
||||
"p-cancelable": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz",
|
||||
"integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw=="
|
||||
},
|
||||
"p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"p-timeout": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz",
|
||||
"integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=",
|
||||
"requires": {
|
||||
"p-finally": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
},
|
||||
"prepend-http": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
|
||||
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
|
||||
},
|
||||
"printj": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
|
||||
"integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ=="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||
},
|
||||
"request": {
|
||||
"version": "2.87.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz",
|
||||
"integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==",
|
||||
"requires": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.6.0",
|
||||
"caseless": "~0.12.0",
|
||||
"combined-stream": "~1.0.5",
|
||||
"extend": "~3.0.1",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.3.1",
|
||||
"har-validator": "~5.0.3",
|
||||
"http-signature": "~1.2.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.17",
|
||||
"oauth-sign": "~0.8.2",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.1",
|
||||
"safe-buffer": "^5.1.1",
|
||||
"tough-cookie": "~2.3.3",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"request-promise": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz",
|
||||
"integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=",
|
||||
"requires": {
|
||||
"bluebird": "^3.5.0",
|
||||
"request-promise-core": "1.1.1",
|
||||
"stealthy-require": "^1.1.0",
|
||||
"tough-cookie": ">=2.3.3"
|
||||
}
|
||||
},
|
||||
"request-promise-core": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz",
|
||||
"integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=",
|
||||
"requires": {
|
||||
"lodash": "^4.13.1"
|
||||
}
|
||||
},
|
||||
"retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"signalr-client": {
|
||||
"version": "0.0.17",
|
||||
"resolved": "https://registry.npmjs.org/signalr-client/-/signalr-client-0.0.17.tgz",
|
||||
"integrity": "sha1-pSF383ziSOzIcibdEDxB/3DIKbE=",
|
||||
"requires": {
|
||||
"websocket": "^1.0.17"
|
||||
}
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.14.2",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
|
||||
"integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
|
||||
"requires": {
|
||||
"asn1": "~0.2.3",
|
||||
"assert-plus": "^1.0.0",
|
||||
"bcrypt-pbkdf": "^1.0.0",
|
||||
"dashdash": "^1.12.0",
|
||||
"ecc-jsbn": "~0.1.1",
|
||||
"getpass": "^0.1.1",
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
}
|
||||
},
|
||||
"stealthy-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
||||
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
|
||||
},
|
||||
"therocktrading": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/therocktrading/-/therocktrading-0.9.0.tgz",
|
||||
"integrity": "sha512-yHYefoQ05GJsytI6QsAbm3G+cnQta9Q0wC+4etmxDiesWvtSeH87PN3JIpq56UYsUxjn37mgiK2c5oSTK4yRpQ==",
|
||||
"requires": {
|
||||
"underscore": "1.4.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"underscore": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz",
|
||||
"integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"timed-out": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
|
||||
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8="
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
|
||||
"integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
|
||||
"requires": {
|
||||
"punycode": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
|
||||
"optional": true
|
||||
},
|
||||
"typedarray-to-buffer": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
|
||||
"requires": {
|
||||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"ultron": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
|
||||
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
|
||||
},
|
||||
"underscore": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz",
|
||||
"integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg=="
|
||||
},
|
||||
"url-parse-lax": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
|
||||
"integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=",
|
||||
"requires": {
|
||||
"prepend-http": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"url-to-options": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz",
|
||||
"integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"websocket": {
|
||||
"version": "1.0.26",
|
||||
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.26.tgz",
|
||||
"integrity": "sha512-fjcrYDPIQxpTnqFQ9JjxUQcdvR89MFAOjPBlF+vjOt49w/XW4fJknUoMz/mDIn2eK1AdslVojcaOxOqyZZV8rw==",
|
||||
"requires": {
|
||||
"debug": "^2.2.0",
|
||||
"nan": "^2.3.3",
|
||||
"typedarray-to-buffer": "^3.1.2",
|
||||
"yaeti": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
|
||||
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
|
||||
"requires": {
|
||||
"async-limiter": "~1.0.0",
|
||||
"safe-buffer": "~5.1.0",
|
||||
"ultron": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"yaeti": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
|
||||
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "gekko-broker",
|
||||
"version": "0.6.8",
|
||||
"description": "Gekko's order execution library for bitcoin & crypto exchanges",
|
||||
"main": "gekkoBroker.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "github.com/askmike/gekko"
|
||||
},
|
||||
"keywords": [
|
||||
"crypto",
|
||||
"bitcoin",
|
||||
"exchange",
|
||||
"execution",
|
||||
"trade"
|
||||
],
|
||||
"author": "Mike van Rossum <[email protected]>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"async": "^2.6.0",
|
||||
"binance": "^1.3.3",
|
||||
"bitfinex-api-node": "^2.0.0-beta",
|
||||
"bitx": "^1.5.0",
|
||||
"bluebird": "^3.5.1",
|
||||
"coinfalcon": "^1.0.3",
|
||||
"crypto-js": "^3.1.9-1",
|
||||
"gdax": "^0.7.0",
|
||||
"gekko-bittrex": "^0.8.5",
|
||||
"gekko-broker-poloniex": "^0.0.12",
|
||||
"kraken-api": "askmike/npm-kraken-api#a69dfb3eb296b9c795cfa48dd31edcdc1b3d5398",
|
||||
"lodash": "^4.17.5",
|
||||
"moment": "^2.22.1",
|
||||
"request-promise": "^4.2.2",
|
||||
"retry": "^0.12.0",
|
||||
"therocktrading": "^0.9.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
The Portfolio class holds data about the portfolio
|
||||
*/
|
||||
|
||||
const _ = require('lodash');
|
||||
const async = require('async');
|
||||
const errors = require('./exchangeErrors');
|
||||
// const EventEmitter = require('events');
|
||||
|
||||
class Portfolio {
|
||||
constructor(config, api) {
|
||||
_.bindAll(this);
|
||||
this.config = config;
|
||||
this.api = api;
|
||||
this.balances = {};
|
||||
this.fee = null;
|
||||
}
|
||||
|
||||
getBalance(fund) {
|
||||
return this.getFund(fund).amount;
|
||||
}
|
||||
|
||||
// return the [fund] based on the data we have in memory
|
||||
getFund(fund) {
|
||||
return _.find(this.balances, function(f) { return f.name === fund});
|
||||
}
|
||||
|
||||
// convert into the portfolio expected by the performanceAnalyzer
|
||||
convertBalances(asset,currency) { // rename?
|
||||
var asset = _.find(this.balances, a => a.name === this.config.asset).amount;
|
||||
var currency = _.find(this.balances, a => a.name === this.config.currency).amount;
|
||||
|
||||
return {
|
||||
currency,
|
||||
asset,
|
||||
balance: currency + (asset * this.ticker.bid)
|
||||
}
|
||||
}
|
||||
|
||||
setBalances(callback) {
|
||||
let set = (err, fullPortfolio) => {
|
||||
if(err) {
|
||||
console.log(err);
|
||||
throw new errors.ExchangeError(err);
|
||||
}
|
||||
|
||||
// only include the currency/asset of this market
|
||||
const balances = [ this.config.currency, this.config.asset ]
|
||||
.map(name => {
|
||||
let item = _.find(fullPortfolio, {name});
|
||||
|
||||
if(!item) {
|
||||
// assume we have 0
|
||||
item = { name, amount: 0 };
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
this.balances = balances;
|
||||
|
||||
if(_.isFunction(callback))
|
||||
callback();
|
||||
}
|
||||
|
||||
this.api.getPortfolio(set);
|
||||
}
|
||||
|
||||
setFee(callback) {
|
||||
this.api.getFee((err, fee) => {
|
||||
if(err)
|
||||
throw new errors.ExchangeError(err);
|
||||
|
||||
this.fee = fee;
|
||||
|
||||
if(_.isFunction(callback))
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
setTicker(ticker) {
|
||||
this.ticker = ticker;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Portfolio
|
||||
@@ -0,0 +1,80 @@
|
||||
// wraps around a low level trigger and feeds
|
||||
// it live market data.
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const exchangeUtils = require('./exchangeUtils');
|
||||
const bindAll = exchangeUtils.bindAll;
|
||||
|
||||
const triggers = require('./triggers');
|
||||
|
||||
// @param api: a gekko broker wrapper instance
|
||||
// @param type: type of trigger to wrap
|
||||
// @param props: properties to feed to trigger
|
||||
class Trigger {
|
||||
constructor({api, type, props, onTrigger}) {
|
||||
this.onTrigger = onTrigger;
|
||||
this.api = api;
|
||||
|
||||
this.isLive = true;
|
||||
|
||||
// note: we stay on the safe side and trigger
|
||||
// as soon as the bid goes below trail.
|
||||
this.tickerProp = 'bid';
|
||||
|
||||
if(!_.has(triggers, type)) {
|
||||
throw new Error('Gekko Broker does not know trigger ' + type);
|
||||
}
|
||||
|
||||
this.CHECK_INTERVAL = this.api.interval * 10;
|
||||
|
||||
bindAll(this);
|
||||
this.trigger = new triggers[type]({
|
||||
onTrigger: this.propogateTrigger,
|
||||
...props
|
||||
})
|
||||
|
||||
this.scheduleFetch();
|
||||
}
|
||||
|
||||
scheduleFetch() {
|
||||
this.timout = setTimeout(this.fetch, this.CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
fetch() {
|
||||
if(!this.isLive) {
|
||||
return;
|
||||
}
|
||||
this.api.getTicker(this.processTicker)
|
||||
}
|
||||
|
||||
processTicker(err, ticker) {
|
||||
if(!this.isLive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(err) {
|
||||
return console.log('[GB/trigger] failed to fetch ticker:', err);
|
||||
}
|
||||
|
||||
this.price = ticker[this.tickerProp];
|
||||
|
||||
this.trigger.updatePrice(this.price);
|
||||
this.scheduleFetch();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.isLive = false;
|
||||
clearTimeout(this.timout);
|
||||
}
|
||||
|
||||
propogateTrigger(payload) {
|
||||
if(!this.isLive) {
|
||||
return;
|
||||
}
|
||||
this.isLive = false;
|
||||
this.onTrigger(payload);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Trigger;
|
||||
@@ -0,0 +1,3 @@
|
||||
const trailingStop = require('./trailingStop');
|
||||
|
||||
module.exports = { trailingStop };
|
||||
@@ -0,0 +1,62 @@
|
||||
const EventEmitter = require('events');
|
||||
|
||||
// Note: as of now only supports trailing the price going up (after
|
||||
// a buy), on trigger (when the price moves down) you should sell.
|
||||
|
||||
|
||||
// @param initialPrice: initial price, preferably buy price
|
||||
// @param trail: fixed offset from the price
|
||||
// @param onTrigger: fn to call when the stop triggers
|
||||
class TrailingStop extends EventEmitter {
|
||||
constructor({trail, initialPrice, onTrigger}) {
|
||||
super();
|
||||
|
||||
this.trail = trail;
|
||||
this.isLive = true;
|
||||
this.onTrigger = onTrigger;
|
||||
|
||||
this.previousPrice = initialPrice;
|
||||
this.trailingPoint = initialPrice - this.trail;
|
||||
}
|
||||
|
||||
updatePrice(price) {
|
||||
if(!this.isLive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(price > this.trailingPoint + this.trail) {
|
||||
this.trailingPoint = price - this.trail;
|
||||
}
|
||||
|
||||
this.previousPrice = price;
|
||||
|
||||
if(price <= this.trailingPoint) {
|
||||
this.trigger();
|
||||
}
|
||||
}
|
||||
|
||||
updateTrail(trail) {
|
||||
if(!this.isLive) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.trail = trail;
|
||||
this.trailingPoint = this.previousPrice - this.trail;
|
||||
// recheck whether moving the trail triggered.
|
||||
this.updatePrice(this.previousPrice);
|
||||
}
|
||||
|
||||
trigger() {
|
||||
if(!this.isLive) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLive = false;
|
||||
if(this.onTrigger) {
|
||||
this.onTrigger(this.previousPrice);
|
||||
}
|
||||
this.emit('trigger', this.previousPrice);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TrailingStop;
|
||||
@@ -0,0 +1,51 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const request = require('request-promise');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
|
||||
let getOrderMinSize = currency => {
|
||||
if (currency === 'BTC') return 0.001;
|
||||
else if (currency === 'ETH') return 0.01;
|
||||
else if (currency === 'USDT') return 10;
|
||||
else return 1;
|
||||
};
|
||||
|
||||
const options = {
|
||||
url: 'https://www.binance.com/exchange/public/product',
|
||||
headers: {
|
||||
Connection: 'keep-alive',
|
||||
'User-Agent': 'Request-Promise',
|
||||
},
|
||||
json: true,
|
||||
};
|
||||
|
||||
request(options)
|
||||
.then(body => {
|
||||
if (!body && !body.data) {
|
||||
throw new Error('Unable to fetch product list, response was empty');
|
||||
}
|
||||
|
||||
let assets = _.uniqBy(_.map(body.data, market => market.baseAsset));
|
||||
let currencies = _.uniqBy(_.map(body.data, market => market.quoteAsset));
|
||||
let pairs = _.map(body.data, market => {
|
||||
return {
|
||||
pair: [market.quoteAsset, market.baseAsset],
|
||||
minimalOrder: {
|
||||
amount: parseFloat(market.minTrade),
|
||||
price: parseFloat(market.tickSize),
|
||||
order: getOrderMinSize(market.quoteAsset),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return { assets: assets, currencies: currencies, markets: pairs };
|
||||
})
|
||||
.then(markets => {
|
||||
fs.writeFileSync('../../wrappers/binance-markets.json', JSON.stringify(markets, null, 2));
|
||||
console.log(`Done writing Binance market data`);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(`Couldn't import products from Binance`);
|
||||
console.log(err);
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const request = require('request-promise');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
request({
|
||||
url: 'https://api.bitfinex.com/v1/symbols_details',
|
||||
headers: {
|
||||
Connection: 'keep-alive',
|
||||
'User-Agent': 'Request-Promise',
|
||||
},
|
||||
json: true,
|
||||
})
|
||||
.then(body => {
|
||||
if (!body) {
|
||||
throw new Error('Unable to fetch list of assets, response was empty');
|
||||
}
|
||||
|
||||
return body;
|
||||
})
|
||||
.then(results => {
|
||||
let assets = _.uniq(_.map(results, market => {
|
||||
return market.pair.substring(0, 3).toUpperCase();
|
||||
}));
|
||||
|
||||
let currencies = _.uniq(_.map(results, market => {
|
||||
return market.pair.substring(3, 6).toUpperCase();
|
||||
}));
|
||||
|
||||
let markets = _.map(results, market => {
|
||||
return {
|
||||
pair: [
|
||||
market.pair.substring(3, 6).toUpperCase(),
|
||||
market.pair.substring(0, 3).toUpperCase()
|
||||
],
|
||||
minimalOrder: {
|
||||
amount: market.minimum_order_size,
|
||||
unit: 'asset',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return { assets: assets, currencies: currencies, markets: markets };
|
||||
})
|
||||
.then(markets => {
|
||||
fs.writeFileSync('../../wrappers/bitfinex-markets.json', JSON.stringify(markets, null, 2));
|
||||
console.log(`Done writing Bitfinex market data`);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(`Couldn't import products from Bitfinex`);
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const request = require('request-promise');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
request({
|
||||
url: 'https://api.pro.coinbase.com/products',
|
||||
headers: {
|
||||
Connection: 'keep-alive',
|
||||
'User-Agent': 'Request-Promise',
|
||||
},
|
||||
json: true,
|
||||
})
|
||||
.then(body => {
|
||||
if (!body) {
|
||||
throw new Error('Unable to fetch list of assets, response was empty');
|
||||
}
|
||||
|
||||
return body;
|
||||
})
|
||||
.then(results => {
|
||||
let assets = _.uniq(_.map(results, market => {
|
||||
return market.base_currency.toUpperCase();
|
||||
}));
|
||||
|
||||
let currencies = _.uniq(_.map(results, market => {
|
||||
return market.quote_currency.toUpperCase();
|
||||
}));
|
||||
|
||||
let markets = _.map(results, market => {
|
||||
return {
|
||||
pair: [
|
||||
market.quote_currency.toUpperCase(),
|
||||
market.base_currency.toUpperCase()
|
||||
],
|
||||
minimalOrder: {
|
||||
amount: market.base_min_size,
|
||||
unit: 'asset',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return { assets: assets, currencies: currencies, markets: markets };
|
||||
})
|
||||
.then(markets => {
|
||||
fs.writeFileSync('../../wrappers/coinbase-markets.json', JSON.stringify(markets, null, 2));
|
||||
console.log(`Done writing Coinbase market data`);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(`Couldn't import products from Coinbase`);
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const request = require('request-promise');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
const options = {
|
||||
url: 'https://coinfalcon.com/api/v1/markets',
|
||||
headers: {
|
||||
Connection: 'keep-alive',
|
||||
'User-Agent': 'Request-Promise',
|
||||
},
|
||||
json: true,
|
||||
};
|
||||
|
||||
request(options)
|
||||
.then(body => {
|
||||
if (!body && !body.data) {
|
||||
throw new Error('Unable to fetch product list, response was empty');
|
||||
}
|
||||
|
||||
let assets = _.uniq(_.map(body.data, market => market.name.split('-')[0]));
|
||||
let currencies = _.uniq(_.map(body.data, market => market.name.split('-')[1]));
|
||||
let pairs = _.map(body.data, market => {
|
||||
var currency = market.name.split('-')[1];
|
||||
var asset = market.name.split('-')[0];
|
||||
return {
|
||||
pair: [currency, asset],
|
||||
minimalOrder: {
|
||||
amount: parseFloat(market.min_volume),
|
||||
price: parseFloat(market.min_price),
|
||||
order: 0.0
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return { assets: assets, currencies: currencies, markets: pairs };
|
||||
})
|
||||
.then(markets => {
|
||||
fs.writeFileSync('../../wrappers/coinfalcon-markets.json', JSON.stringify(markets, null, 2));
|
||||
console.log(`Done writing CoinFalcon market data`);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(`Couldn't import products from CoinFalcon`);
|
||||
console.log(err);
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
const _ = require('lodash');
|
||||
const fs = require('fs');
|
||||
const request = require('request-promise');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
// Minimum amounts are not queryable, get them here
|
||||
// https://support.kraken.com/hc/en-us/articles/205893708-What-is-the-minimum-order-size-
|
||||
|
||||
let getMinTradeSize = asset => {
|
||||
let minTradeSize = 0.01;
|
||||
switch (asset) {
|
||||
case 'XREP':
|
||||
minTradeSize = '0.3'
|
||||
break;
|
||||
case 'XXBT':
|
||||
minTradeSize = '0.002'
|
||||
break;
|
||||
case 'BCH':
|
||||
minTradeSize = '0.002'
|
||||
break;
|
||||
case 'ADA':
|
||||
minTradeSize = '1'
|
||||
break;
|
||||
case 'DASH':
|
||||
minTradeSize = '0.03'
|
||||
break;
|
||||
case 'XXDG':
|
||||
minTradeSize = '3000'
|
||||
break;
|
||||
case 'EOS':
|
||||
minTradeSize = '3.0'
|
||||
break;
|
||||
case 'XETH':
|
||||
minTradeSize = '0.02'
|
||||
break;
|
||||
case 'XETC':
|
||||
minTradeSize = '0.3'
|
||||
break;
|
||||
case 'GNO':
|
||||
minTradeSize = '0.03'
|
||||
break;
|
||||
case 'XICN':
|
||||
minTradeSize = '2'
|
||||
break;
|
||||
case 'XLTC':
|
||||
minTradeSize = '0.1'
|
||||
break;
|
||||
case 'XMLN':
|
||||
minTradeSize = '0.1'
|
||||
break;
|
||||
case 'XXMR':
|
||||
minTradeSize = '0.1'
|
||||
break;
|
||||
case 'QTUM':
|
||||
minTradeSize = '0.1'
|
||||
break;
|
||||
case 'XXRP':
|
||||
minTradeSize = '30'
|
||||
break;
|
||||
case 'XXLM':
|
||||
minTradeSize = '30'
|
||||
break;
|
||||
case 'USDT':
|
||||
minTradeSize = '5'
|
||||
break;
|
||||
case 'XTZ':
|
||||
minTradeSize = '1'
|
||||
break;
|
||||
case 'XZEC':
|
||||
minTradeSize = '0.03'
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return minTradeSize;
|
||||
}
|
||||
|
||||
let assetPromise = request({
|
||||
url: 'https://api.kraken.com/0/public/Assets',
|
||||
headers: {
|
||||
Connection: 'keep-alive',
|
||||
'User-Agent': 'Request-Promise',
|
||||
},
|
||||
json: true,
|
||||
}).then(body => {
|
||||
if (!body || !body.result) {
|
||||
throw new Error('Unable to fetch list of assets, response was empty')
|
||||
} else if (!_.isEmpty(body.error)) {
|
||||
throw new Error(`Unable to fetch list of assets: ${body.error}`);
|
||||
}
|
||||
|
||||
return body.result;
|
||||
});
|
||||
|
||||
let assetPairsPromise = request({
|
||||
url: 'https://api.kraken.com/0/public/AssetPairs',
|
||||
headers: {
|
||||
Connection: 'keep-alive',
|
||||
'User-Agent': 'Request-Promise',
|
||||
},
|
||||
json: true,
|
||||
}).then(body => {
|
||||
if (!body || !body.result) {
|
||||
throw new Error('Unable to fetch list of assets, response was empty')
|
||||
} else if (!_.isEmpty(body.error)) {
|
||||
throw new Error(`Unable to fetch list of assets: ${body.error}`);
|
||||
}
|
||||
|
||||
return body.result;
|
||||
});
|
||||
|
||||
Promise.all([assetPromise, assetPairsPromise])
|
||||
.then(results => {
|
||||
let assets = _.uniq(_.map(results[1], market => {
|
||||
return results[0][market.base].altname;
|
||||
}));
|
||||
|
||||
let currencies = _.uniq(_.map(results[1], market => {
|
||||
return results[0][market.quote].altname;
|
||||
}));
|
||||
|
||||
let marketKeys = _.filter(_.keys(results[1]), k => { return !k.endsWith('.d'); });
|
||||
let markets = _.map(marketKeys, k => {
|
||||
let market = results[1][k];
|
||||
let asset = results[0][market.base];
|
||||
let currency = results[0][market.quote];
|
||||
return {
|
||||
pair: [currency.altname, asset.altname],
|
||||
prefixed: [market.quote, market.base],
|
||||
book: k,
|
||||
minimalOrder: {
|
||||
amount: getMinTradeSize(market.base),
|
||||
unit: 'asset',
|
||||
},
|
||||
pricePrecision: market.pair_decimals,
|
||||
amountPrecision: market.lot_decimals
|
||||
};
|
||||
});
|
||||
|
||||
return { assets: assets, currencies: currencies, markets: markets };
|
||||
})
|
||||
.then(markets => {
|
||||
fs.writeFileSync('../../wrappers/kraken-markets.json', JSON.stringify(markets, null, 2));
|
||||
console.log(`Done writing Kraken market data`);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(`Couldn't import products from Kraken`);
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Fake exchanges: used to test purposes to develop Gekko (works without internet).
|
||||
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const TREND_DURATION = 1000;
|
||||
|
||||
const Trader = function() {
|
||||
this.name = 'Exchange Simulator';
|
||||
this.at = moment().subtract(30, 'minutes');
|
||||
|
||||
|
||||
// fake data
|
||||
this.price = 100;
|
||||
this.trend = 'up';
|
||||
this.tid = 0;
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, cb) {
|
||||
const amount = moment().diff(this.at, 'seconds');
|
||||
|
||||
const trades = _.range(amount).map(() => {
|
||||
|
||||
this.tid++;
|
||||
|
||||
if(this.tid % TREND_DURATION === 0) {
|
||||
if(this.trend === 'up')
|
||||
this.trend = 'down';
|
||||
else
|
||||
this.trend = 'up';
|
||||
}
|
||||
|
||||
if(this.trend === 'up')
|
||||
this.price += Math.random();
|
||||
else
|
||||
this.price -= Math.random();
|
||||
|
||||
return {
|
||||
date: this.at.add(1, 'seconds').unix(),
|
||||
price: this.price,
|
||||
amount: Math.random() * 100,
|
||||
tid: this.tid
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[EXCHANGE SIMULATOR] emitted ${amount} fake trades, up until ${this.at.format('YYYY-MM-DD HH:mm:ss')}.`
|
||||
);
|
||||
|
||||
cb(null, trades);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Exchange Simulator',
|
||||
slug: 'DEBUG_exchange-simulator',
|
||||
currencies: ['USD'],
|
||||
assets: ['BTC', 'BTC'],
|
||||
maxTradesAge: 60,
|
||||
maxHistoryFetch: null,
|
||||
markets: [
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 5, unit: 'currency' } },
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
fetchTimespan: 60,
|
||||
tid: 'tid',
|
||||
tradable: false
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,530 @@
|
||||
const moment = require('moment');
|
||||
const _ = require('lodash');
|
||||
|
||||
const Errors = require('../exchangeErrors');
|
||||
const marketData = require('./binance-markets.json');
|
||||
const exchangeUtils = require('../exchangeUtils');
|
||||
const retry = exchangeUtils.retry;
|
||||
const scientificToDecimal = exchangeUtils.scientificToDecimal;
|
||||
|
||||
const Binance = require('binance');
|
||||
|
||||
const Trader = function(config) {
|
||||
_.bindAll(this, [
|
||||
'roundAmount',
|
||||
'roundPrice',
|
||||
'isValidPrice',
|
||||
'isValidLot'
|
||||
]);
|
||||
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency.toUpperCase();
|
||||
this.asset = config.asset.toUpperCase();
|
||||
}
|
||||
|
||||
let recvWindow = 6000;
|
||||
if(config.optimizedConnection) {
|
||||
// there is a bug in binance's API
|
||||
// where some requests randomly take
|
||||
// over a second, this tells binance
|
||||
// to bail out after 500ms.
|
||||
//
|
||||
// As discussed in binance API
|
||||
// telegram. TODO add link.
|
||||
recvWindow = 500;
|
||||
}
|
||||
|
||||
this.pair = this.asset + this.currency;
|
||||
this.name = 'binance';
|
||||
|
||||
this.market = _.find(Trader.getCapabilities().markets, (market) => {
|
||||
return market.pair[0] === this.currency && market.pair[1] === this.asset
|
||||
});
|
||||
|
||||
this.binance = new Binance.BinanceRest({
|
||||
key: this.key,
|
||||
secret: this.secret,
|
||||
timeout: 15000,
|
||||
recvWindow,
|
||||
disableBeautification: false,
|
||||
handleDrift: true,
|
||||
});
|
||||
|
||||
if(config.key && config.secret) {
|
||||
// Note non standard func:
|
||||
//
|
||||
// On binance we might pay fees in BNB
|
||||
// if we do we CANNOT calculate feePercent
|
||||
// since we don't track BNB price (when we
|
||||
// are not trading on a BNB market).
|
||||
//
|
||||
// Though we can deduce feePercent based
|
||||
// on user fee tracked through `this.getFee`.
|
||||
// Set default here, overwrite in getFee.
|
||||
this.fee = 0.001;
|
||||
// Set the proper fee asap.
|
||||
this.getFee(_.noop);
|
||||
|
||||
this.oldOrder = false;
|
||||
}
|
||||
};
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'Error -1021',
|
||||
'Response code 429',
|
||||
'Response code 5',
|
||||
'Response code 403',
|
||||
'ETIMEDOUT',
|
||||
'EHOSTUNREACH',
|
||||
// getaddrinfo EAI_AGAIN api.binance.com api.binance.com:443
|
||||
'EAI_AGAIN',
|
||||
'ENETUNREACH'
|
||||
];
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.handleResponse = function(funcName, callback) {
|
||||
return (error, body) => {
|
||||
if (body && body.code) {
|
||||
error = new Error(`Error ${body.code}: ${body.msg}`);
|
||||
}
|
||||
|
||||
if(error) {
|
||||
if(_.isString(error)) {
|
||||
error = new Error(error);
|
||||
}
|
||||
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
if(funcName === 'cancelOrder' && error.message.includes('UNKNOWN_ORDER')) {
|
||||
console.log(new Date, 'cancelOrder', 'UNKNOWN_ORDER');
|
||||
// order got filled in full before it could be
|
||||
// cancelled, meaning it was NOT cancelled.
|
||||
return callback(false, {filled: true});
|
||||
}
|
||||
|
||||
if(funcName === 'checkOrder' && error.message.includes('Order does not exist.')) {
|
||||
console.log(new Date, 'Binance doesnt know this order, retrying up to 10 times..');
|
||||
error.retry = 10;
|
||||
}
|
||||
|
||||
if(funcName === 'addOrder' && error.message.includes('Account has insufficient balance')) {
|
||||
console.log(new Date, 'insufficientFunds');
|
||||
error.type = 'insufficientFunds';
|
||||
}
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
return callback(undefined, body);
|
||||
}
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const processResults = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var parsedTrades = [];
|
||||
_.each(
|
||||
data,
|
||||
function(trade) {
|
||||
parsedTrades.push({
|
||||
tid: trade.aggTradeId,
|
||||
date: moment(trade.timestamp).unix(),
|
||||
price: parseFloat(trade.price),
|
||||
amount: parseFloat(trade.quantity),
|
||||
});
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
if (descending) callback(null, parsedTrades.reverse());
|
||||
else callback(undefined, parsedTrades);
|
||||
};
|
||||
|
||||
var reqData = {
|
||||
symbol: this.pair,
|
||||
};
|
||||
|
||||
if (since) {
|
||||
var endTs = moment(since)
|
||||
.add(1, 'h')
|
||||
.valueOf();
|
||||
var nowTs = moment().valueOf();
|
||||
|
||||
reqData.startTime = moment(since).valueOf();
|
||||
reqData.endTime = endTs > nowTs ? nowTs : endTs;
|
||||
}
|
||||
|
||||
const fetch = cb => this.binance.aggTrades(reqData, this.handleResponse('getTrades', cb));
|
||||
retry(undefined, fetch, processResults);
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const setBalance = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const findAsset = item => item.asset === this.asset;
|
||||
const assetAmount = parseFloat(_.find(data.balances, findAsset).free);
|
||||
|
||||
const findCurrency = item => item.asset === this.currency;
|
||||
const currencyAmount = parseFloat(_.find(data.balances, findCurrency).free);
|
||||
|
||||
if (!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if (!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount },
|
||||
];
|
||||
|
||||
return callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.binance.account({}, this.handleResponse('getPortfolio', cb));
|
||||
retry(undefined, fetch, setBalance);
|
||||
};
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
|
||||
// binance does NOT tell us whether the user is using BNB to pay
|
||||
// for fees, which means a discount (effectively lower fees)
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
const basepoints = data.makerCommission;
|
||||
|
||||
/** Binance raw response
|
||||
{ makerCommission: 10,
|
||||
takerCommission: 10,
|
||||
buyerCommission: 0,
|
||||
sellerCommission: 0,
|
||||
canTrade: true,
|
||||
canWithdraw: true,
|
||||
canDeposit: true,
|
||||
So to get decimal representation of fee we actually need to divide by 10000
|
||||
*/
|
||||
// note non standard func, see constructor
|
||||
this.fee = basepoints / 10000;
|
||||
|
||||
callback(undefined, this.fee);
|
||||
}
|
||||
|
||||
const fetch = cb => this.binance.account({}, this.handleResponse('getFee', cb));
|
||||
retry(undefined, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const setTicker = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
var result = _.find(data, ticker => ticker.symbol === this.pair);
|
||||
|
||||
if(!result)
|
||||
return callback(new Error(`Market ${this.pair} not found on Binance`));
|
||||
|
||||
var ticker = {
|
||||
ask: parseFloat(result.askPrice),
|
||||
bid: parseFloat(result.bidPrice),
|
||||
};
|
||||
|
||||
callback(undefined, ticker);
|
||||
};
|
||||
|
||||
const handler = cb => this.binance._makeRequest({}, this.handleResponse('getTicker', cb), 'api/v1/ticker/allBookTickers');
|
||||
retry(undefined, handler, setTicker);
|
||||
};
|
||||
|
||||
// Effectively counts the number of decimal places, so 0.001 or 0.234 results in 3
|
||||
Trader.prototype.getPrecision = function(tickSize) {
|
||||
if (!isFinite(tickSize)) return 0;
|
||||
var e = 1, p = 0;
|
||||
while (Math.round(tickSize * e) / e !== tickSize) { e *= 10; p++; }
|
||||
return p;
|
||||
};
|
||||
|
||||
Trader.prototype.round = function(amount, tickSize) {
|
||||
var precision = 100000000;
|
||||
var t = this.getPrecision(tickSize);
|
||||
|
||||
if(Number.isInteger(t))
|
||||
precision = Math.pow(10, t);
|
||||
|
||||
amount *= precision;
|
||||
amount = Math.floor(amount);
|
||||
amount /= precision;
|
||||
|
||||
// https://gist.github.com/jiggzson/b5f489af9ad931e3d186
|
||||
amount = scientificToDecimal(amount);
|
||||
|
||||
return amount;
|
||||
};
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return this.round(amount, this.market.minimalOrder.amount);
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return this.round(price, this.market.minimalOrder.price);
|
||||
}
|
||||
|
||||
Trader.prototype.isValidPrice = function(price) {
|
||||
return price >= this.market.minimalOrder.price;
|
||||
}
|
||||
|
||||
Trader.prototype.isValidLot = function(price, amount) {
|
||||
return amount * price >= this.market.minimalOrder.order;
|
||||
}
|
||||
|
||||
Trader.prototype.outbidPrice = function(price, isUp) {
|
||||
let newPrice;
|
||||
|
||||
if(isUp) {
|
||||
newPrice = price + this.market.minimalOrder.price;
|
||||
} else {
|
||||
newPrice = price - this.market.minimalOrder.price;
|
||||
}
|
||||
|
||||
return this.roundPrice(newPrice);
|
||||
}
|
||||
|
||||
Trader.prototype.addOrder = function(tradeType, amount, price, callback) {
|
||||
const setOrder = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const txid = data.orderId;
|
||||
|
||||
callback(undefined, txid);
|
||||
};
|
||||
|
||||
const reqData = {
|
||||
symbol: this.pair,
|
||||
side: tradeType.toUpperCase(),
|
||||
type: 'LIMIT',
|
||||
timeInForce: 'GTC',
|
||||
quantity: amount,
|
||||
price: price,
|
||||
timestamp: new Date().getTime()
|
||||
};
|
||||
|
||||
const handler = cb => this.binance.newOrder(reqData, this.handleResponse('addOrder', cb));
|
||||
retry(undefined, handler, setOrder);
|
||||
};
|
||||
|
||||
Trader.prototype.getOpenOrders = function(callback) {
|
||||
|
||||
const get = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, data.map(o => o.orderId));
|
||||
}
|
||||
|
||||
const reqData = {
|
||||
symbol: this.pair
|
||||
}
|
||||
|
||||
const handler = cb => this.binance.openOrders(reqData, this.handleResponse('getOpenOrders', cb));
|
||||
retry(undefined, handler, get);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
const get = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
let price = 0;
|
||||
let amount = 0;
|
||||
let date = moment(0);
|
||||
|
||||
const fees = {};
|
||||
|
||||
if(!data.length) {
|
||||
return callback(new Error('Binance did not return any trades'));
|
||||
}
|
||||
|
||||
const trades = _.filter(data, t => {
|
||||
// note: the API returns a string after creating
|
||||
return t.orderId == order;
|
||||
});
|
||||
|
||||
if(!trades.length) {
|
||||
console.log('cannot find trades!', { order, list: data.map(t => t.orderId).reverse() });
|
||||
|
||||
const reqData = {
|
||||
symbol: this.pair,
|
||||
orderId: order,
|
||||
};
|
||||
|
||||
this.binance.queryOrder(reqData, (err, resp) => {
|
||||
console.log('couldnt find any trade for order, here is order:', {err, resp});
|
||||
|
||||
callback(new Error('Trades not found'));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(trades, trade => {
|
||||
date = moment(trade.time);
|
||||
price = ((price * amount) + (+trade.price * trade.qty)) / (+trade.qty + amount);
|
||||
amount += +trade.qty;
|
||||
|
||||
if(fees[trade.commissionAsset])
|
||||
fees[trade.commissionAsset] += (+trade.commission);
|
||||
else
|
||||
fees[trade.commissionAsset] = (+trade.commission);
|
||||
});
|
||||
|
||||
let feePercent;
|
||||
if(_.keys(fees).length === 1) {
|
||||
if(fees.BNB && this.asset !== 'BNB' && this.currency !== 'BNB') {
|
||||
// we paid fees in BNB, right now that means the fee is always 75%
|
||||
// of base fee. We cannot calculate since we do not have the BNB rate.
|
||||
feePercent = this.fee * 0.75;
|
||||
} else {
|
||||
if(fees[this.asset]) {
|
||||
feePercent = fees[this.asset] / amount * 100;
|
||||
} else if(fees.currency) {
|
||||
feePercent = fees[this.currency] / price / amount * 100;
|
||||
} else {
|
||||
// use user fee of 10 basepoints
|
||||
feePercent = this.fee;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we paid fees in multiple currencies?
|
||||
// assume user fee
|
||||
feePercent = this.fee;
|
||||
}
|
||||
|
||||
callback(undefined, { price, amount, date, fees, feePercent });
|
||||
}
|
||||
|
||||
const reqData = {
|
||||
symbol: this.pair,
|
||||
// if this order was not part of the last 500 trades we won't find it..
|
||||
limit: 1000,
|
||||
};
|
||||
|
||||
const handler = cb => this.binance.myTrades(reqData, this.handleResponse('getOrder', cb));
|
||||
retry(undefined, handler, get);
|
||||
};
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.addOrder('buy', amount, price, callback);
|
||||
};
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.addOrder('sell', amount, price, callback);
|
||||
};
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
|
||||
const check = (err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(data.filled === true) {
|
||||
// binance responsed with order not found
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
}
|
||||
|
||||
const status = data.status;
|
||||
|
||||
if(
|
||||
status === 'CANCELED' ||
|
||||
status === 'REJECTED' ||
|
||||
// for good measure: GB does not
|
||||
// submit orders that can expire yet
|
||||
status === 'EXPIRED'
|
||||
) {
|
||||
return callback(undefined, { executed: false, open: false });
|
||||
} else if(
|
||||
status === 'NEW' ||
|
||||
status === 'PARTIALLY_FILLED'
|
||||
) {
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: +data.executedQty });
|
||||
} else if(status === 'FILLED') {
|
||||
return callback(undefined, { executed: true, open: false })
|
||||
}
|
||||
|
||||
console.log('what status?', status);
|
||||
throw status;
|
||||
};
|
||||
|
||||
const reqData = {
|
||||
symbol: this.pair,
|
||||
orderId: order,
|
||||
};
|
||||
|
||||
const fetcher = cb => this.binance.queryOrder(reqData, this.handleResponse('checkOrder', cb));
|
||||
retry(undefined, fetcher, check);
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
|
||||
const cancel = (err, data) => {
|
||||
|
||||
this.oldOrder = order;
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(data && data.filled) {
|
||||
return callback(undefined, true);
|
||||
}
|
||||
|
||||
return callback(undefined, false);
|
||||
};
|
||||
|
||||
let reqData = {
|
||||
symbol: this.pair,
|
||||
orderId: order,
|
||||
};
|
||||
|
||||
const fetcher = cb => this.binance.cancelOrder(reqData, this.handleResponse('cancelOrder', cb));
|
||||
retry(undefined, fetcher, cancel);
|
||||
};
|
||||
|
||||
Trader.getCapabilities = function() {
|
||||
return {
|
||||
name: 'Binance',
|
||||
slug: 'binance',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
gekkoBroker: 0.6,
|
||||
limitedCancelConfirmation: true
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,296 @@
|
||||
var Bitcoincoid = require('bitcoin-co-id-update'),
|
||||
_ = require('lodash'),
|
||||
moment = require('moment'),
|
||||
log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
this.currency = config.currency.toLowerCase();
|
||||
this.asset = config.asset.toLowerCase();
|
||||
this.pair = this.asset + '_' + this.currency;
|
||||
}
|
||||
this.name = 'Bitcoin-co-id';
|
||||
this.bitcoincoid = new Bitcoincoid(this.key, this.secret);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
var priceDivider = 100000000; // one hundred million;
|
||||
amount *= priceDivider;
|
||||
amount = Math.floor(amount);
|
||||
amount /= priceDivider;
|
||||
return amount;
|
||||
};
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args); },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = _.toArray(arguments),
|
||||
set = function(err, data) {
|
||||
if(err)
|
||||
return this.retry(this.getTicker, args);
|
||||
var ticker = {
|
||||
ask: this.roundAmount(data.ticker.buy),
|
||||
bid: this.roundAmount(data.ticker.sell),
|
||||
};
|
||||
callback(err, ticker);
|
||||
}.bind(this);
|
||||
this.bitcoincoid.getTicker(this.pair, set);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var functionName = 'Trader.getPortfolio()',
|
||||
args = _.toArray(arguments);
|
||||
set = function(err, data) {
|
||||
if(err)
|
||||
return this.retry(this.getPortfolio, args);
|
||||
var assetAmount = this.roundAmount( data.return.balance[this.asset] ),
|
||||
currencyAmount = this.roundAmount( data.return.balance[this.currency] ),
|
||||
assetHold = this.roundAmount( data.return.balance_hold[this.asset] ),
|
||||
currencyHold = this.roundAmount( data.return.balance_hold[this.currency] );
|
||||
|
||||
if(
|
||||
!_.isNumber(assetAmount) || _.isNaN(assetAmount) ||
|
||||
!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)
|
||||
) {
|
||||
return log.error('account balance error: Gekko is unable to trade with ',this.currency.toUpperCase(),':',currencyAmount,' or ',this.asset.toUpperCase(),':',assetAmount);
|
||||
}
|
||||
var portfolio = [
|
||||
{ name: this.currency.toUpperCase(), amount: currencyAmount - currencyHold},
|
||||
{ name: this.asset.toUpperCase(), amount: assetAmount - assetHold }
|
||||
];
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
this.bitcoincoid.getAccountBalances(set);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
if(since)
|
||||
since = 150; // ???
|
||||
|
||||
var process = function(err, data) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
var trades = _.map(data, function(trade) {
|
||||
return {
|
||||
price: +trade.price,
|
||||
amount: +trade.amount,
|
||||
tid: +trade.tid,
|
||||
date: trade.date
|
||||
};
|
||||
});
|
||||
callback(null, data.reverse());
|
||||
}.bind(this);
|
||||
|
||||
this.bitcoincoid.getTrades(this.pair, process);
|
||||
}
|
||||
|
||||
// bitcoin.co.id: Maker 0% - Taker 0.3%
|
||||
// Note: trading fee: 0.3% for IDR pairs and 0% for BTC pairs
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var fee = 0;
|
||||
if (this.currency.toUpperCase() === 'IDR')
|
||||
{
|
||||
fee = 0.003;
|
||||
}
|
||||
else if (this.currency.toUpperCase() === 'BTC')
|
||||
{
|
||||
fee = 0.000;
|
||||
}
|
||||
|
||||
callback(false, fee);
|
||||
};
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.type = 'buy';
|
||||
|
||||
// decrease purchase amount by 1% to avoid trying to buy more than balance
|
||||
amount -= amount / 100;
|
||||
amount = this.roundAmount(amount);
|
||||
|
||||
// decrease purchase price by 1% less than asking price
|
||||
// price -= price / 100;
|
||||
amount *= price;
|
||||
|
||||
var set = function(err, data) {
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err)
|
||||
return log.error('unable to buy', err);
|
||||
callback(null, data.return.order_id);
|
||||
}.bind(this);
|
||||
this.bitcoincoid.createOrders(
|
||||
this.pair + '',
|
||||
this.type,
|
||||
price,
|
||||
amount,
|
||||
set
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.type = 'sell';
|
||||
|
||||
// increase selling price by 1% more than bidding price
|
||||
// price += price / 100;
|
||||
|
||||
amount = this.roundAmount(amount);
|
||||
var set = function(err, data) {
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err)
|
||||
return log.error('unable to sell', err);
|
||||
callback(null, data.return.order_id);
|
||||
}.bind(this);
|
||||
this.bitcoincoid.createOrders(
|
||||
this.pair + '',
|
||||
this.type,
|
||||
price,
|
||||
amount,
|
||||
set
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
if (order === null) {
|
||||
return callback('no order_id', false);
|
||||
}
|
||||
var check = function(err, data) {
|
||||
if(err){
|
||||
return this.retry(this.checkOrder, arguments);
|
||||
}
|
||||
var status = data.return.order.status;
|
||||
if (status === 'filled') {
|
||||
return callback(err, true);
|
||||
} else if (status === 'open') {
|
||||
return callback(err, false);
|
||||
}
|
||||
callback(err, false);
|
||||
}.bind(this);
|
||||
|
||||
this.bitcoincoid.getOrderDetails(this.pair, order, check);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
if (order === null) {
|
||||
return callback('no order_id', false);
|
||||
}
|
||||
var get = function(data, err) {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
var price = 0;
|
||||
var amount = 0;
|
||||
var date = moment(0);
|
||||
|
||||
if(!data.success)
|
||||
return callback(null, {price, amount, date});
|
||||
|
||||
var result = data.return.order;
|
||||
var orderAmount = result.order+'_'+this.asset;
|
||||
price = result.Price;
|
||||
amount = result.orderAmount;
|
||||
|
||||
if(result.status === 'open') {
|
||||
date = moment(result.submit_time);
|
||||
} else {
|
||||
date = moment(result.finish_time);
|
||||
}
|
||||
callback(err, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.bitcoincoid.getOrderDetails(this.pair, order, get);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var cancel = function(err, data) {
|
||||
if(err) {
|
||||
return log.error('unable to cancel order: ', order, '(', err, '), retrying...');
|
||||
this.retry(this.cancelOrder, args);
|
||||
}
|
||||
callback();
|
||||
};
|
||||
this.bitcoincoid.cancelOrder(this.pair, order, this.type, cancel);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Bitcoin.co.id',
|
||||
slug: 'bitcoin-co-id',
|
||||
currencies: ['IDR', 'BTC'],
|
||||
assets: [
|
||||
"BTC", "BCH", "BTG", "ETH", "ETC", "LTC", "NXT", "WAVES", "STR", "XRP", "XZC", "BTS", "DRK", "DOGE", "NEM", "XZR", "DASH", "XLM", "XEM"
|
||||
],
|
||||
markets: [
|
||||
|
||||
// IDR <-> XXXX
|
||||
|
||||
{ pair: ['IDR', 'BTC'], minimalOrder: { amount: 0.0001, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'BCH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'BTG'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'ETC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'LTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'NXT'], minimalOrder: { amount: 5, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'WAVES'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'STR'], minimalOrder: { amount: 20, unit: 'asset' } }, // Listed as XLM
|
||||
{ pair: ['IDR', 'XRP'], minimalOrder: { amount: 10, unit: 'asset' } },
|
||||
{ pair: ['IDR', 'XZC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
|
||||
|
||||
// BTC <-> XXXX
|
||||
|
||||
{ pair: ['BTC', 'BTS'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'DRK'], minimalOrder: { amount: 0.01, unit: 'asset' } }, // Listed as DASH
|
||||
{ pair: ['BTC', 'DOGE'], minimalOrder: { amount: 1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'NXT'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'STR'], minimalOrder: { amount: 0.01, unit: 'asset' } }, // Listed as XLM
|
||||
{ pair: ['BTC', 'NEM'], minimalOrder: { amount: 1, unit: 'asset' } }, // Listed as XEM
|
||||
{ pair: ['BTC', 'XRP'], minimalOrder: { amount: 0.01, unit: 'asset' } }
|
||||
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesHistory: true,
|
||||
providesFullHistory: false,
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,319 @@
|
||||
|
||||
const Bitfinex = require("bitfinex-api-node");
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const Errors = require('../exchangeErrors');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
|
||||
const marketData = require('./bitfinex-markets.json');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
}
|
||||
this.name = 'Bitfinex';
|
||||
this.balance;
|
||||
this.price;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
this.pair = this.asset + this.currency;
|
||||
this.bitfinex = new Bitfinex.RESTv1({apiKey: this.key, apiSecret: this.secret, transform: true});
|
||||
|
||||
this.interval = 4000;
|
||||
}
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'ESOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'443',
|
||||
'504',
|
||||
'503',
|
||||
'502',
|
||||
'Empty response',
|
||||
'Nonce is too small'
|
||||
];
|
||||
|
||||
Trader.prototype.handleResponse = function(funcName, callback) {
|
||||
return (error, data) => {
|
||||
|
||||
if(!error && _.isEmpty(data)) {
|
||||
error = new Error('Empty response');
|
||||
}
|
||||
|
||||
if(error) {
|
||||
const message = error.message;
|
||||
|
||||
console.log('handleResponse', funcName, message);
|
||||
|
||||
// in case we just cancelled our balances might not have
|
||||
// settled yet, retry.
|
||||
if(
|
||||
funcName === 'submitOrder' &&
|
||||
message.includes('not enough exchange balance')
|
||||
) {
|
||||
error.retry = 20;
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// most likely problem with v1 api
|
||||
if(
|
||||
funcName === 'submitOrder' &&
|
||||
message.includes('Cannot evaluate your available balance, please try again')
|
||||
) {
|
||||
error.retry = 10;
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// in some situations bfx returns 404 on
|
||||
// orders created recently
|
||||
if(
|
||||
funcName === 'checkOrder' &&
|
||||
message.includes('Not Found')
|
||||
) {
|
||||
error.retry = 5;
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if(includes(message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if(includes(message, 'Too Many Requests')) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 5000;
|
||||
}
|
||||
}
|
||||
|
||||
return callback(error, data);
|
||||
}
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// We are only interested in funds in the "exchange" wallet
|
||||
data = data.filter(c => c.type === 'exchange');
|
||||
|
||||
const asset = _.find(data, c => c.currency.toUpperCase() === this.asset);
|
||||
const currency = _.find(data, c => c.currency.toUpperCase() === this.currency);
|
||||
|
||||
let assetAmount, currencyAmount;
|
||||
|
||||
if(_.isObject(asset) && _.isNumber(+asset.available) && !_.isNaN(+asset.available))
|
||||
assetAmount = +asset.available;
|
||||
else {
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(_.isObject(currency) && _.isNumber(+currency.available) && !_.isNaN(+currency.available))
|
||||
currencyAmount = +currency.available;
|
||||
else {
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount },
|
||||
];
|
||||
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.bitfinex.wallet_balances(this.handleResponse('getPortfolio', cb));
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
callback(undefined, {bid: +data.bid, ask: +data.ask});
|
||||
};
|
||||
|
||||
const fetch = cb => this.bitfinex.ticker(this.pair, this.handleResponse('getTicker', cb));
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
const makerFee = 0.1;
|
||||
// const takerFee = 0.2;
|
||||
callback(undefined, makerFee / 100);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return Math.floor(amount*100000000)/100000000;
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
// todo: calc significant digits
|
||||
return price;
|
||||
}
|
||||
|
||||
Trader.prototype.submitOrder = function(type, amount, price, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
callback(null, data.order_id);
|
||||
}
|
||||
|
||||
const fetch = cb => this.bitfinex.new_order(this.pair,
|
||||
amount + '',
|
||||
price + '',
|
||||
this.name.toLowerCase(),
|
||||
type,
|
||||
'exchange limit',
|
||||
this.handleResponse('submitOrder', cb)
|
||||
);
|
||||
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.submitOrder('buy', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.submitOrder('sell', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) {
|
||||
console.log('this is after we have retried fetching it');
|
||||
// this is after we have retried fetching it
|
||||
// in this.handleResponse.
|
||||
if(err.message.includes('Not Found')) {
|
||||
return callback(undefined, {
|
||||
open: false,
|
||||
executed: true
|
||||
});
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(undefined, {
|
||||
open: data.is_live,
|
||||
executed: data.original_amount === data.executed_amount,
|
||||
filledAmount: +data.executed_amount
|
||||
});
|
||||
}
|
||||
|
||||
const fetcher = cb => this.bitfinex.order_status(order_id, this.handleResponse('checkOrder', cb));
|
||||
retry(null, fetcher, processResponse);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var price = parseFloat(data.avg_execution_price);
|
||||
var amount = parseFloat(data.executed_amount);
|
||||
var date = moment.unix(data.timestamp);
|
||||
|
||||
console.log('getOrder', data);
|
||||
|
||||
// TEMP: Thu May 31 14:49:34 CEST 2018
|
||||
// the `past_trades` call is not returning
|
||||
// any data.
|
||||
return callback(undefined, {price, amount, date});
|
||||
|
||||
const processPastTrade = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
console.log('processPastTrade', data);
|
||||
const trade = _.first(data);
|
||||
|
||||
const fees = {
|
||||
[trade.fee_currency]: trade.fee_amount
|
||||
}
|
||||
|
||||
callback(undefined, {price, amount, date, fees});
|
||||
}
|
||||
|
||||
// we need another API call to fetch the fees
|
||||
const feeFetcher = cb => this.bitfinex.past_trades(this.currency, {since: data.timestamp}, this.handleResponse('pastTrades', cb));
|
||||
retry(null, feeFetcher, processPastTrade);
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
};
|
||||
|
||||
const fetcher = cb => this.bitfinex.order_status(order_id, this.handleResponse('getOrder', cb));
|
||||
retry(null, fetcher, processResponse);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.cancelOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(undefined, false);
|
||||
}
|
||||
|
||||
const handler = cb => this.bitfinex.cancel_order(order_id, this.handleResponse('cancelOrder', cb));
|
||||
retry(null, handler, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var trades = _.map(data, function(trade) {
|
||||
return {
|
||||
tid: trade.tid,
|
||||
date: trade.timestamp,
|
||||
price: +trade.price,
|
||||
amount: +trade.amount
|
||||
}
|
||||
});
|
||||
|
||||
callback(undefined, descending ? trades : trades.reverse());
|
||||
};
|
||||
|
||||
var path = this.pair;
|
||||
if(since)
|
||||
path += '?limit_trades=2000';
|
||||
|
||||
const handler = cb => this.bitfinex.trades(path, this.handleResponse('getTrades', cb));
|
||||
retry(null, handler, processResponse);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Bitfinex',
|
||||
slug: 'bitfinex',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesFullHistory: true,
|
||||
providesHistory: 'date',
|
||||
tradable: true,
|
||||
forceReorderDelay: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,246 @@
|
||||
// NOT USED, see: https://github.com/bitfinexcom/bitfinex-api-node/issues/321
|
||||
|
||||
const Bitfinex = require("bitfinex-api-node");
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const Errors = require('../exchangeErrors');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
|
||||
const marketData = require('./bitfinex-markets.json');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
}
|
||||
this.name = 'Bitfinex';
|
||||
this.balance;
|
||||
this.price;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
this.pair = 't' + this.asset + this.currency;
|
||||
this.bitfinex = new Bitfinex.RESTv2({apiKey: this.key, apiSecret: this.secret, transform: true});
|
||||
}
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
// Probably we need to update these string
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'429',
|
||||
'443',
|
||||
'504',
|
||||
'503',
|
||||
'500',
|
||||
'502',
|
||||
'Empty response'
|
||||
];
|
||||
|
||||
Trader.prototype.handleResponse = function(funcName, callback) {
|
||||
return (error, data) => {
|
||||
if(!error && _.isEmpty(data)) {
|
||||
error = new Error('Empty response');
|
||||
}
|
||||
|
||||
if(error) {
|
||||
const message = error.message || error;
|
||||
|
||||
if(!includes(message, recoverableErrors)) {
|
||||
const error = new Error(message);
|
||||
error.notFatal = true;
|
||||
return callback(error);
|
||||
}
|
||||
}
|
||||
|
||||
return callback(error, data);
|
||||
}
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
console.log('processResponse', {err, data});
|
||||
if (err) return callback(err);
|
||||
|
||||
// We are only interested in funds in the "exchange" wallet
|
||||
data = data.filter(c => c.type === 'exchange');
|
||||
|
||||
const asset = _.find(data, c => c.currency.toUpperCase() === this.asset);
|
||||
const currency = _.find(data, c => c.currency.toUpperCase() === this.currency);
|
||||
|
||||
console.log(currency.balance);
|
||||
|
||||
let assetAmount, currencyAmount;
|
||||
|
||||
if(_.isObject(asset) && _.isNumber(+asset.balanceAvailable) && !_.isNaN(+asset.balanceAvailable))
|
||||
assetAmount = +asset.balanceAvailable;
|
||||
else {
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(_.isObject(currency) && _.isNumber(+currency.balanceAvailable) && !_.isNaN(+currency.balanceAvailable))
|
||||
currencyAmount = +currency.balanceAvailable;
|
||||
else {
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount },
|
||||
];
|
||||
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.bitfinex.wallets(this.handleResponse('getPortfolio', cb));
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
callback(undefined, {bid: +data.bid, ask: +data.ask});
|
||||
};
|
||||
|
||||
const fetch = cb => this.bitfinex.ticker(this.pair, this.handleResponse('getTicker', cb));
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
const makerFee = 0.1;
|
||||
// const takerFee = 0.2;
|
||||
callback(undefined, makerFee / 100);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return Math.floor(amount*100000000)/100000000;
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
// todo: calc significant digits
|
||||
return price;
|
||||
}
|
||||
|
||||
Trader.prototype.submit_order = function(type, amount, price, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
callback(err, data.order_id);
|
||||
}
|
||||
|
||||
const fetcher = cb => this.bitfinex.new_order(this.pair,
|
||||
amount + '',
|
||||
price + '',
|
||||
this.name.toLowerCase(),
|
||||
type,
|
||||
'exchange limit',
|
||||
this.handleResponse('submitOrder', cb)
|
||||
);
|
||||
|
||||
retry(retryCritical, fetcher, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.submit_order('buy', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.submit_order('sell', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
return callback(undefined, {
|
||||
open: data.is_live,
|
||||
executed: data.original_amount === data.executed_amount,
|
||||
filled: +data.executed_amount
|
||||
});
|
||||
}
|
||||
|
||||
const fetcher = cb => this.bitfinex.order_status(order_id, this.handleResponse('checkOrder', cb));
|
||||
retry(retryCritical, fetcher, processResponse);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var price = parseFloat(data.avg_execution_price);
|
||||
var amount = parseFloat(data.executed_amount);
|
||||
var date = moment.unix(data.timestamp);
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
};
|
||||
|
||||
const fetcher = cb => this.bitfinex.order_status(order_id, this.handleResponse('getOrder', cb));
|
||||
retry(retryCritical, fetcher, processResponse);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.cancelOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
return callback(undefined);
|
||||
}
|
||||
|
||||
const handler = cb => this.bitfinex.cancel_order(order_id, this.handleResponse('cancelOrder', cb));
|
||||
retry(retryForever, handler, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var trades = _.map(data, function(trade) {
|
||||
return {
|
||||
tid: trade.tid,
|
||||
date: trade.timestamp,
|
||||
price: +trade.price,
|
||||
amount: +trade.amount
|
||||
}
|
||||
});
|
||||
|
||||
callback(undefined, descending ? trades : trades.reverse());
|
||||
};
|
||||
|
||||
var path = this.pair;
|
||||
if(since)
|
||||
path += '?limit_trades=2000';
|
||||
|
||||
const handler = cb => this.bitfinex.trades(path, this.handleResponse('getTrades', cb));
|
||||
retry(retryForever, handler, processResponse);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Bitfinex',
|
||||
slug: 'bitfinex',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesFullHistory: true,
|
||||
providesHistory: 'date',
|
||||
tradable: true,
|
||||
forceReorderDelay: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,261 @@
|
||||
var Bitstamp = require("bitstamp");
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
var util = require('../core/util');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
this.asset = config.asset.toLowerCase();
|
||||
this.currency = config.currency.toLowerCase();
|
||||
this.market = this.asset + this.currency;
|
||||
}
|
||||
this.name = 'Bitstamp';
|
||||
|
||||
this.bitstamp = new Bitstamp(this.key, this.secret, this.clientID);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, data) {
|
||||
|
||||
if(data && data.error) {
|
||||
err = data.error;
|
||||
}
|
||||
|
||||
if(err) {
|
||||
if(err.meta && err.meta.reason === 'API key not found')
|
||||
util.die('Bitstamp says this API keys is invalid..');
|
||||
|
||||
log.error('BITSTAMP API ERROR:', err);
|
||||
return this.retry(this.getPortfolio, args);
|
||||
}
|
||||
|
||||
var portfolio = [];
|
||||
_.each(data, function(amount, asset) {
|
||||
if(asset.indexOf('available') !== -1) {
|
||||
asset = asset.substr(0, 3).toUpperCase();
|
||||
portfolio.push({name: asset, amount: parseFloat(amount)});
|
||||
}
|
||||
});
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
|
||||
this.bitstamp.balance(this.market, set);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
this.bitstamp.ticker(this.market, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
callback(err);
|
||||
|
||||
callback(false, data.fee / 100);
|
||||
}.bind(this);
|
||||
|
||||
this.bitstamp.balance(this.market, set);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
var findMarket = function(pair) {
|
||||
return pair.pair[0].toLowerCase() === this.currency && pair.pair[1].toLowerCase() === this.asset
|
||||
}
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, result) {
|
||||
if(err || result.status === "error") {
|
||||
log.error('unable to buy:', err, result.reason, 'retrying...');
|
||||
return this.retry(this.buy, args);
|
||||
}
|
||||
|
||||
callback(null, result.id);
|
||||
}.bind(this);
|
||||
|
||||
//Decrease amount by 1% to avoid trying to buy more than balance allows.
|
||||
amount = Number(amount * 0.99).toFixed(8);
|
||||
|
||||
// Use proper precision by currency pair
|
||||
var pair = _.find(Trader.getCapabilities().markets, _.bind(findMarket, this));
|
||||
price = Number(Number.parseFloat(price).toFixed(pair.precision));
|
||||
|
||||
this.bitstamp.buy(this.market, amount, price, undefined, set);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
var findMarket = function(pair) {
|
||||
return pair.pair[0].toLowerCase() === this.currency && pair.pair[1].toLowerCase() === this.asset
|
||||
}
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, result) {
|
||||
if(err || result.status === "error") {
|
||||
log.error('unable to sell:', err, result.reason, 'retrying...');
|
||||
return this.retry(this.sell, args);
|
||||
}
|
||||
|
||||
callback(null, result.id);
|
||||
}.bind(this);
|
||||
|
||||
// prevent:
|
||||
// 'Ensure that there are no more than 8 decimal places.'
|
||||
amount = Number(Number.parseFloat(amount)).toFixed(8);
|
||||
|
||||
// Use proper precision by currency pair
|
||||
var pair = _.find(Trader.getCapabilities().markets, _.bind(findMarket, this));
|
||||
price = Number(Number.parseFloat(price).toFixed(pair.precision));
|
||||
|
||||
this.bitstamp.sell(this.market, amount, price, undefined, set);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(id, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var get = function(err, data) {
|
||||
if(!err && _.isEmpty(data) && _.isEmpty(data.result))
|
||||
err = 'no data';
|
||||
|
||||
else if(!err && !_.isEmpty(data.error))
|
||||
err = data.error;
|
||||
|
||||
if(err) {
|
||||
log.error('Unable to get order', order, JSON.stringify(err));
|
||||
return this.retry(this.getOrder, args);
|
||||
}
|
||||
|
||||
var order = _.find(data, o => o.order_id === +id);
|
||||
|
||||
if(!order) {
|
||||
// if the order was cancelled we are unable
|
||||
// to retrieve it, assume that this is what
|
||||
// is happening.
|
||||
return callback(err, {
|
||||
price: 0,
|
||||
amount: 0,
|
||||
date: moment(0)
|
||||
});
|
||||
}
|
||||
|
||||
var price = parseFloat( order[`${this.asset}_${this.currency}`] );
|
||||
var amount = Math.abs(parseFloat( order[this.asset] ));
|
||||
var date = moment( order.datetime );
|
||||
|
||||
callback(err, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.bitstamp.user_transactions(this.market, {}, get);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var check = function(err, result) {
|
||||
var stillThere = _.find(result, function(o) { return o.id === order });
|
||||
callback(err, !stillThere);
|
||||
}.bind(this);
|
||||
|
||||
this.bitstamp.open_orders(this.market, check);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var cancel = function(err, result) {
|
||||
if(err || !result) {
|
||||
log.error('unable to cancel order', order, '(', err, result, ')');
|
||||
return this.retry(this.cancelOrder, args);
|
||||
}
|
||||
|
||||
callback();
|
||||
}.bind(this);
|
||||
|
||||
this.bitstamp.cancel_order(order, cancel);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, trades) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
var result = _.map(trades, t => {
|
||||
return {
|
||||
date: t.date,
|
||||
tid: +t.tid,
|
||||
price: +t.price,
|
||||
amount: +t.amount
|
||||
}
|
||||
})
|
||||
|
||||
callback(null, result.reverse());
|
||||
}.bind(this);
|
||||
|
||||
// NOTE: temporary disabled, see https://github.com/askmike/gekko/issues/794
|
||||
// if(since)
|
||||
// this.bitstamp.transactions(this.market, {time: 'day'}, process);
|
||||
// else
|
||||
this.bitstamp.transactions(this.market, process);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Bitstamp',
|
||||
slug: 'bitstamp',
|
||||
currencies: ['USD', 'EUR', 'BTC'],
|
||||
assets: ['BTC', 'BCH', 'EUR', 'LTC', 'ETH', 'XRP'],
|
||||
maxTradesAge: 60,
|
||||
maxHistoryFetch: null,
|
||||
markets: [
|
||||
{ pair: ['USD', 'EUR'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['EUR', 'BTC'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
|
||||
{ pair: ['USD', 'BCH'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['EUR', 'BCH'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['BTC', 'BCH'], minimalOrder: { amount: 0.001, unit: 'currency'}, precision: 8 },
|
||||
|
||||
{ pair: ['USD', 'XRP'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 5 },
|
||||
{ pair: ['EUR', 'XRP'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 5 },
|
||||
{ pair: ['BTC', 'XRP'], minimalOrder: { amount: 0.001, unit: 'currency'}, precision: 8 },
|
||||
|
||||
{ pair: ['USD', 'LTC'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['EUR', 'LTC'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.001, unit: 'currency'}, precision: 8 },
|
||||
|
||||
{ pair: ['USD', 'ETH'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['EUR', 'ETH'], minimalOrder: { amount: 5, unit: 'currency' }, precision: 2 },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.001, unit: 'currency'}, precision: 8 },
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
fetchTimespan: 60,
|
||||
tid: 'tid',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,633 @@
|
||||
const Bittrex = require('node.bittrex.api');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
|
||||
// Helper methods
|
||||
function joinCurrencies(currencyA, currencyB){
|
||||
return currencyA + '-' + currencyB;
|
||||
}
|
||||
|
||||
var Trader = function(config) {
|
||||
this.currency = config.currency;
|
||||
this.asset = config.asset;
|
||||
|
||||
if(!config.key) {
|
||||
// no api key defined -> we need to set a
|
||||
// dummy key, otherwise the Bittrex module
|
||||
// will not work even for public requests.
|
||||
this.key = 'dummyApiKey';
|
||||
this.secret = 'dummyApiKey';
|
||||
} else {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
}
|
||||
|
||||
this.name = 'Bittrex';
|
||||
this.balance;
|
||||
this.price;
|
||||
|
||||
this.pair = [this.currency, this.asset].join('-');
|
||||
|
||||
Bittrex.options({
|
||||
apikey: this.key,
|
||||
apisecret: this.secret,
|
||||
stream: false,
|
||||
verbose: false,
|
||||
cleartext: false,
|
||||
inverse_callback_arguments: true
|
||||
});
|
||||
|
||||
this.bittrexApi = Bittrex;
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
];
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.processResponse = function(callback) {
|
||||
return (err, response) => {
|
||||
|
||||
if(err) {
|
||||
if(!_.isError(err)) {
|
||||
err = new Error(err.message);
|
||||
}
|
||||
|
||||
if(err.message === 'APIKEY_INVALID') {
|
||||
console.log('APIKEY_INVALID');
|
||||
err.retry = 10;
|
||||
}
|
||||
|
||||
if(includes(err.message, recoverableErrors)) {
|
||||
err.notFatal = true;
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(undefined, response);
|
||||
}
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
data = data.result;
|
||||
|
||||
let assetEntry = _.find(data, i => i.Currency == this.asset);
|
||||
let currencyEntry = _.find(data, i => i.Currency == this.currency);
|
||||
|
||||
if(_.isUndefined(assetEntry)) {
|
||||
assetEntry = {
|
||||
Available: 0.0,
|
||||
Currency: this.asset
|
||||
}
|
||||
}
|
||||
|
||||
if(_.isUndefined(currencyEntry)) {
|
||||
currencyEntry = {
|
||||
Available: 0.0,
|
||||
Currency: this.currency
|
||||
}
|
||||
}
|
||||
|
||||
const assetAmount = parseFloat( assetEntry.Available );
|
||||
const currencyAmount = parseFloat( currencyEntry.Available );
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
|
||||
callback(undefined, portfolio);
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.getbalances(this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return _.round(price, 8);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(price) {
|
||||
return _.floor(price, 8);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const tick = data.result;
|
||||
|
||||
callback(null, {
|
||||
bid: parseFloat(tick.Bid),
|
||||
ask: parseFloat(tick.Ask),
|
||||
})
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.getticker({market: this.pair}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
callback(false, 0.00025);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
const handle = (err, result) => {
|
||||
console.log('[bittrex buy]', err, result);
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const id = _.get(result, 'result.uuid');
|
||||
if(!id) {
|
||||
console.log('[bittrex buy error]', result);
|
||||
return callback(new Error('Bad response'));
|
||||
}
|
||||
|
||||
callback(undefined, id);
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.buylimit({market: this.pair, quantity: amount, rate: price}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
const handle = (err, result) => {
|
||||
console.log('[bittrex sell]', err, result);
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const id = _.get(result, 'result.uuid');
|
||||
if(!id) {
|
||||
console.log('[bittrex sell error]', result);
|
||||
return callback(new Error('Bad response'));
|
||||
}
|
||||
|
||||
callback(undefined, id);
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.selllimit({market: this.pair, quantity: amount, rate: price}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
const handle = (err, result) => {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
console.log('checkOrder', result);
|
||||
throw 'a';
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.getopenorders({market: this.pair}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
|
||||
const handle = (err, result) => {
|
||||
console.log('getOrder', result);
|
||||
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
let price = 0;
|
||||
let amount = 0;
|
||||
let date = moment(0);
|
||||
|
||||
if(!result.success) {
|
||||
return callback(null, {price, amount, date});
|
||||
}
|
||||
|
||||
const resultOrder = result.result;
|
||||
|
||||
price = resultOrder.Price;
|
||||
amount = resultOrder.Quantity;
|
||||
|
||||
if(resultOrder.IsOpen) {
|
||||
date = moment(resultOrder.Opened);
|
||||
} else {
|
||||
date = moment(resultOrder.Closed);
|
||||
}
|
||||
|
||||
callback(err, {price, amount, date});
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.getorder({uuid: order}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
const handle = (err, result) => {
|
||||
console.log('cancelOrder', err, result);
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
throw 'a';
|
||||
|
||||
// if(!result.success && result.message === 'ORDER_NOT_OPEN') {
|
||||
// log.debug('getOrder', 'ORDER_NOT_OPEN: assuming already closed or executed');
|
||||
// }
|
||||
|
||||
callback(undefined, true);
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.cancel({uuid: order}, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {;
|
||||
var firstFetch = !!since;
|
||||
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var result = data.result;
|
||||
|
||||
// Edge case, see here:
|
||||
// @link https://github.com/askmike/gekko/issues/479
|
||||
if(firstFetch && _.size(result) === 50000)
|
||||
util.die(
|
||||
[
|
||||
'Bittrex did not provide enough data. Read this:',
|
||||
'https://github.com/askmike/gekko/issues/479'
|
||||
].join('\n\n')
|
||||
);
|
||||
|
||||
result = _.map(result, function(trade) {
|
||||
var mr = {
|
||||
tid: trade.Id,
|
||||
amount: +trade.Quantity,
|
||||
date: moment.utc(trade.TimeStamp).unix(),
|
||||
timeStamp: trade.TimeStamp,
|
||||
price: +trade.Price
|
||||
};
|
||||
return mr;
|
||||
});
|
||||
|
||||
callback(null, result.reverse());
|
||||
}
|
||||
|
||||
var params = {
|
||||
currencyPair: joinCurrencies(this.currency, this.asset)
|
||||
}
|
||||
|
||||
if(since) {
|
||||
params.start = since.unix();
|
||||
}
|
||||
|
||||
const fetch = next => this.bittrexApi.getmarkethistory({ market: params.currencyPair }, this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function() {
|
||||
return {
|
||||
name: 'bittrex',
|
||||
slug: 'bittrex',
|
||||
currencies: ['BITCNY', 'BTC', 'ETH', 'USDT'],
|
||||
assets: [
|
||||
'1ST', '2GIVE', '8BIT', 'ABY', 'ADC', 'ADT', 'ADX', 'AEON', 'AGRS', 'AM',
|
||||
'AMP', 'AMS', 'ANS', 'ANT', 'APEX', 'APX', 'ARB', 'ARCH', 'ARDR', 'ARK',
|
||||
'AUR', 'BAT', 'BAY', 'BCC', 'BCY', 'BITB', 'BITCNY', 'BITS', 'BITZ', 'BLC',
|
||||
'BLITZ', 'BLK', 'BLOCK', 'BNT', 'BOB', 'BRK', 'BRX', 'BSD', 'BSTY', 'BTA',
|
||||
'BTC', 'BTCD', 'BTS', 'BURST', 'BYC', 'CANN', 'CCN', 'CFI', 'CLAM',
|
||||
'CLOAK', 'CLUB', 'COVAL', 'CPC', 'CRB', 'CRBIT', 'CRW', 'CRYPT', 'CURE',
|
||||
'CVC', 'DAR', 'DASH', 'DCR', 'DCT', 'DGB', 'DGC', 'DGD', 'DMD', 'DOGE',
|
||||
'DOPE', 'DRACO', 'DTB', 'DTC', 'DYN', 'EBST', 'EDG', 'EFL', 'EGC', 'EMC',
|
||||
'EMC2', 'ENRG', 'ERC', 'ETC', 'ETH', 'EXCL', 'EXP', 'FAIR', 'FC2', 'FCT',
|
||||
'FLDC', 'FLO', 'FRK', 'FSC2', 'FTC', 'FUN', 'GAM', 'GAME', 'GBG', 'GBYTE',
|
||||
'GCR', 'GEMZ', 'GEO', 'GHC', 'GLD', 'GNO', 'GNT', 'GOLOS', 'GP', 'GRC',
|
||||
'GRS', 'GRT', 'GUP', 'HKG', 'HMQ', 'HYPER', 'HZ', 'INCNT', 'INFX', 'IOC',
|
||||
'ION', 'IOP', 'J', 'JWL', 'KMD', 'KORE', 'KR', 'LBC', 'LGD', 'LMC', 'LSK',
|
||||
'LTC', 'LUN', 'LXC', 'MAID', 'MAX', 'MCO', 'MEC', 'MEME', 'METAL', 'MLN',
|
||||
'MND', 'MONA', 'MTL', 'MTR', 'MUE', 'MUSIC', 'MYR', 'MYST', 'MZC', 'NAUT',
|
||||
'NAV', 'NBT', 'NEO', 'NET', 'NEU', 'NLG', 'NMR', 'NTRN', 'NXC', 'NXS',
|
||||
'NXT', 'OC', 'OK', 'OMG', 'OMNI', 'ORB', 'PART', 'PAY', 'PDC', 'PINK',
|
||||
'PIVX', 'PKB', 'POT', 'PPC', 'PRIME', 'PTC', 'PTOY', 'PXI', 'QRL', 'QTL',
|
||||
'QTUM', 'QWARK', 'RADS', 'RBY', 'RDD', 'REP', 'RISE', 'RLC', 'ROOT',
|
||||
'SBD', 'SC', 'SCOT', 'SCRT', 'SDC', 'SEC', 'SEQ', 'SFR', 'SHIFT', 'SIB',
|
||||
'SLG', 'SLING', 'SLR', 'SLS', 'SNGLS', 'SNRG', 'SNT', 'SOON', 'SPHR',
|
||||
'SPR', 'SPRTS', 'SSD', 'START', 'STEEM', 'STEPS', 'STORJ', 'STRAT', 'STV',
|
||||
'SWIFT', 'SWING', 'SWT', 'SYNX', 'SYS', 'TES', 'THC', 'TIME', 'TIT',
|
||||
'TKN', 'TKS', 'TRI', 'TRIG', 'TRK', 'TROLL', 'TRST', 'TRUST', 'TX', 'U',
|
||||
'UBQ', 'UFO', 'UNB', 'UNIQ', 'UNIT', 'UNO', 'USDT', 'UTC', 'VIA', 'VIOR',
|
||||
'VIRAL', 'VOX', 'VPN', 'VRC', 'VRM', 'VTC', 'VTR', 'WARP', 'WAVES', 'WBB',
|
||||
'WINGS', 'XAUR', 'XBB', 'XC', 'XCO', 'XCP', 'XDN', 'XDQ', 'XEL', 'XEM',
|
||||
'XLM', 'XMG', 'XMR', 'XPY', 'XQN', 'XRP', 'XSEED', 'XST', 'XTC', 'XVC',
|
||||
'XVG', 'XWC', 'XZC', 'YBC', 'ZCL', 'ZEC', 'ZEN','BTG','OMG','ADA'
|
||||
],
|
||||
markets: [
|
||||
|
||||
// *** BTCNY <-> XXX
|
||||
{ pair: ['BITCNY','BTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
|
||||
// *** BTC <-> XXX
|
||||
{ pair: ['BTC','1ST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','2GIVE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ABY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ADT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ADX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','AEON'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','AGRS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','AMP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ANT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','APX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ARDR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ARK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','AUR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BAT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BAY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BCC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BCY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BITB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BLITZ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BLK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BLOCK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BRK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BRX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BSD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BTA'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BTCD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BTS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BURST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','BYC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CANN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CFI'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CLAM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CLOAK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CLUB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','COVAL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CPC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CRB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CRW'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CURE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','CVC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DAR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DASH'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DCR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DCT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DGB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DGD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DMD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DOGE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DOPE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DRACO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DTB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','DYN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EBST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EDG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EFL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EGC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EMC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EMC2'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ENRG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ERC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ETC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ETH'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EXCL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','EXP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FAIR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FCT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FLDC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FLO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','FUN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GAM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GAME'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GBG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GBYTE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GCR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GEO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GLD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GNO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GOLOS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GRC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GRS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','GUP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','HKG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','HMQ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','INCNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','INFX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','IOC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ION'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','IOP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','KMD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','KORE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LBC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LGD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LMC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LSK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','LUN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MAID'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MCO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MEME'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MLN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MONA'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MTL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MUE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MUSIC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','MYST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NAUT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NAV'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NBT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NEO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NLG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NMR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NXC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NXS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','NXT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','OK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','OMG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','OMNI'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PART'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PAY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PDC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PINK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PIVX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PKB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','POT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PPC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','PTOY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','QRL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','QTUM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','QWARK'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','RADS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','RBY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','RDD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','REP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','RISE'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','RLC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SAFEX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SBD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SEQ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SHIFT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SIB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SLR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SLS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SNGLS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SNRG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SPHR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SPR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','START'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','STEEM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','STORJ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','STRAT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SWIFT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SWT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SYNX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','SYS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','THC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TIME'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TKN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TKS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TRIG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TRST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TRUST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','TX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','UBQ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','UNB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','UNO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VIA'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VOX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VRC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VRM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','VTR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','WAVES'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','WINGS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XAUR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XBB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XCP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XDN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XEL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XEM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XLM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XMG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XMR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XMY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XRP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XVC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XVG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XWC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','XZC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ZCL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ZEC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['BTC','ZEN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
|
||||
// *** ETH <-> XXX
|
||||
{ pair: ['ETH','1ST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','ADT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','ADX'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','ANT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','BAT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','BCC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','BNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','BTS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','CFI'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','CRB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','CVC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','DASH'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','DGB'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','DGD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','ETC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','FCT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','FUN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','GNO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','GNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','GUP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','HMQ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','LGD'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','LTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','LUN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','MCO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','MTL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','MYST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','NEO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','NMR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','OMG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','PAY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','PTOY'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','QRL'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','QTUM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','REP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','RLC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','SC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','SNGLS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','SNT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','STORJ'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','STRAT'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','TIME'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','TKN'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','TRST'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','WAVES'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','WINGS'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','XEM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','XLM'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','XMR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','XRP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['ETH','ZEC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
|
||||
// *** USDT <-> XXX
|
||||
{ pair: ['USDT','BCC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','BTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','DASH'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','ETC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','ETH'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','LTC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','NEO'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','XMR'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','XRP'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','ZEC'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','BTG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','OMG'], minimalOrder: { amount: 0.00000001, unit: 'asset' }},
|
||||
{ pair: ['USDT','ADA'], minimalOrder: { amount: 0.00000001, unit: 'asset' }}
|
||||
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: false,
|
||||
tradable: false,
|
||||
forceReorderDelay: true,
|
||||
gekkoBroker: '0.6.0'
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,249 @@
|
||||
var BTCMarkets = require('btc-markets');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
this.currency = config.currency;
|
||||
this.asset = config.asset;
|
||||
}
|
||||
this.name = 'BTC Markets';
|
||||
this.priceDivider = 100000000; // one hundred million
|
||||
this.btcmakets = new BTCMarkets(this.key, this.secret);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var set = function(err, data) {
|
||||
|
||||
if(!_.isEmpty(data.errorMessage))
|
||||
return callback('BTC-MARKET API ERROR: ' + data.errorMessage);
|
||||
|
||||
var portfolio = _.map(data, function(balance) {
|
||||
return {
|
||||
name: balance.currency,
|
||||
amount: balance.balance / this.priceDivider
|
||||
}
|
||||
}, this);
|
||||
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
|
||||
this.btcmakets.getAccountBalances(set);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTicker, args);
|
||||
|
||||
callback(null, {
|
||||
bid: result.bestBid,
|
||||
ask: result.bestAsk
|
||||
});
|
||||
}.bind(this);
|
||||
this.btcmakets.getTick(this.asset, this.currency, set);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, data) {
|
||||
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err){
|
||||
log.error('unable to retrieve fee', err, ' retrying...');
|
||||
return this.retry(this.getFee, args);
|
||||
}
|
||||
data.tradingFeeRate /= this.priceDivider;
|
||||
callback(false, data.tradingFeeRate);
|
||||
}.bind(this);
|
||||
this.btcmakets.getTradingFee(this.asset, this.currency, set);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
|
||||
price *= this.priceDivider;
|
||||
amount = Math.floor(amount * this.priceDivider);
|
||||
var id = Math.random() + '';
|
||||
var set = function(err, data) {
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err)
|
||||
return log.error('unable to buy', err);
|
||||
callback(null, data.id);
|
||||
}.bind(this);
|
||||
this.btcmakets.createOrder(
|
||||
this.asset,
|
||||
this.currency,
|
||||
price,
|
||||
amount,
|
||||
'Bid',
|
||||
'Limit',
|
||||
id,
|
||||
set
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
price *= this.priceDivider;
|
||||
amount = Math.floor(amount * this.priceDivider);
|
||||
var id = Math.random() + ''
|
||||
var set = function(err, data) {
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err)
|
||||
return log.error('unable to sell', err)
|
||||
callback(null, data.id);
|
||||
}.bind(this);
|
||||
this.btcmakets.createOrder(
|
||||
this.asset,
|
||||
this.currency,
|
||||
price,
|
||||
amount,
|
||||
'Ask',
|
||||
'Limit',
|
||||
id,
|
||||
set
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
if (order == null) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
var check = function(err, data) {
|
||||
if(!err && _.isEmpty(data.orders))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err){
|
||||
return log.error('unable to check order: ', order, '(', err ,'), retrying...');
|
||||
this.retry(this.checkOrder, args);
|
||||
}
|
||||
var placed = !_.isEmpty(data.orders)
|
||||
callback(err, !placed);
|
||||
}.bind(this);
|
||||
|
||||
this.btcmakets.getOpenOrders(this.asset, this.currency, 10, null, check);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var get = function(err, data) {
|
||||
|
||||
if(!err && _.isEmpty(data.orders))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err){
|
||||
return log.error('unable to get order detail: ', order, '(', err ,'), retrying...');
|
||||
this.retry(this.getOrder, args);
|
||||
}
|
||||
var price = parseFloat(data.orders[0].price);
|
||||
var amount = parseFloat(data.orders[0].volumn);
|
||||
var date = moment.unix(data.orders[0].creationDate);
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.btcmakets.getOrderDetail([order], callback);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var get = function(err, data) {
|
||||
|
||||
if(!err && _.isEmpty(data))
|
||||
err = 'no data';
|
||||
else if(!err && !_.isEmpty(data.errorMessage))
|
||||
err = data.errorMessage;
|
||||
if(err){
|
||||
return log.error('unable to cancel order: ',order, '(', err, '), retrying...');
|
||||
this.retry(this.cancelOrder, args);
|
||||
}
|
||||
callback();
|
||||
};
|
||||
this.btcmakets.cancelOrders([order], callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
callback(null, result.reverse());
|
||||
}.bind(this);
|
||||
|
||||
// supports `since` based on trade ID, Gekko can't work this atm..
|
||||
this.btcmakets.getTrades(this.asset, this.currency, process);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'BTC Markets',
|
||||
slug: 'btc-markets',
|
||||
currencies: ['AUD', 'BTC'],
|
||||
assets: [
|
||||
'BTC', 'LTC', 'ETH', 'ETC', 'BCH', 'XRP'
|
||||
],
|
||||
markets: [
|
||||
{ pair: ['AUD', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['AUD', 'LTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['AUD', 'ETH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['AUD', 'ETC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['AUD', 'BCH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['AUD', 'XRP'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'ETC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'BCH'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'XRP'], minimalOrder: { amount: 0.001, unit: 'asset' } }
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesHistory: 'scan',
|
||||
providesFullHistory: false,
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,176 @@
|
||||
var BTCChina = require('btc-china-fork');
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
}
|
||||
this.name = 'BTCC';
|
||||
|
||||
this.pair = (config.asset + config.currency).toUpperCase();
|
||||
|
||||
this.btcc = new BTCChina(this.key, this.secret, this.clientID);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTicker, args);
|
||||
|
||||
callback(null, {
|
||||
bid: result.bids[0][0],
|
||||
ask: result.asks[0][0]
|
||||
});
|
||||
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.getOrderBook(process, this.pair, 1);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
if(descending)
|
||||
callback(null, result.reverse());
|
||||
else
|
||||
callback(null, result);
|
||||
}.bind(this);
|
||||
|
||||
|
||||
if(!since)
|
||||
since = 500;
|
||||
else
|
||||
since = 5000;
|
||||
|
||||
this.btcc.getHistoryData(process, {limit: since});
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
return this.retry(this.getPortfolio, args);
|
||||
|
||||
var portfolio = [];
|
||||
_.each(data.result.balance, function(obj) {
|
||||
portfolio.push({name: obj.currency, amount: parseFloat(obj.amount)});
|
||||
});
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.getAccountInfo(set, 'ALL');
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
this.retry(this.getFee, args);
|
||||
|
||||
callback(false, data.result.profile.trade_fee / 100);
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.getAccountInfo(set, 'ALL');
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
// TODO: do somewhere better..
|
||||
amount = Math.floor(amount * 10000) / 10000;
|
||||
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return log.error('unable to buy:', err, result);
|
||||
|
||||
callback(null, result.result);
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.createOrder2(set, 'buy', price, amount, this.pair);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
// TODO: do somewhere better..
|
||||
amount = Math.round(amount * 10000) / 10000;
|
||||
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return log.error('unable to sell:', err, result);
|
||||
|
||||
callback(null, result.result);
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.createOrder2(set, 'sell', price, amount, this.pair);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var check = function(err, result) {
|
||||
if(err)
|
||||
this.retry(this.checkOrder, args);
|
||||
|
||||
var done = result.result.order.status === 'closed';
|
||||
callback(err, done);
|
||||
};
|
||||
|
||||
this.btcc.getOrder(check, order, this.pair, true);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var cancel = function(err, result) {
|
||||
if(err)
|
||||
log.error('unable to cancel order', order, '(', err, result, ')');
|
||||
}.bind(this);
|
||||
|
||||
this.btcc.cancelOrder(cancel, order, this.pair);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'BTCC',
|
||||
slug: 'btcc',
|
||||
currencies: ['BTC', 'CNY'],
|
||||
assets: ['BTC', 'LTC'],
|
||||
markets: [
|
||||
{ pair: ['CNY', 'BTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['CNY', 'LTC'], minimalOrder: { amount: 0.001, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.001, unit: 'asset' } }
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesFullHistory: true,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,89 @@
|
||||
var BitexthaiAPI = require('bitexthai');
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
}
|
||||
this.name = 'BX.in.th';
|
||||
|
||||
this.pair = 1; // todo
|
||||
|
||||
this.bitexthai = new BitexthaiAPI(this.key, this.secret, this.clientID);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
var parsedTrades = [];
|
||||
_.each(result.trades, function(trade) {
|
||||
// We get trade time in local time, which is GMT+7
|
||||
var date = moment(trade.trade_date).subtract(7, 'hours').unix();
|
||||
|
||||
parsedTrades.push({
|
||||
date: date,
|
||||
price: parseFloat(trade.rate),
|
||||
amount: parseFloat(trade.amount),
|
||||
tid: trade.trade_id
|
||||
});
|
||||
}, this);
|
||||
|
||||
if(descending)
|
||||
callback(null, parsedTrades.reverse());
|
||||
else
|
||||
callback(null, parsedTrades);
|
||||
}.bind(this);
|
||||
|
||||
this.bitexthai.trades(this.pair, process);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'BX.in.th',
|
||||
slug: 'bx.in.th',
|
||||
currencies: ['THB'],
|
||||
assets: ['BTC'],
|
||||
markets: [
|
||||
{
|
||||
pair: ['THB', 'BTC'], minimalOrder: { amount: 0.0001, unit: 'asset' },
|
||||
}
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tradeError: 'NOT IMPLEMENTED YET',
|
||||
providesHistory: false
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,211 @@
|
||||
var CEXio = require('cexio'),
|
||||
moment = require('moment'),
|
||||
async = require('async'),
|
||||
_ = require('lodash'),
|
||||
util = require('../core/util'),
|
||||
log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
this.user = config.username;
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency.toUpperCase();
|
||||
this.asset = config.asset.toUpperCase();
|
||||
this.pair = this.asset + '_' + this.currency;
|
||||
this.name = 'cex.io';
|
||||
|
||||
this.cexio = new CEXio(
|
||||
this.pair,
|
||||
this.user,
|
||||
this.key,
|
||||
this.secret
|
||||
);
|
||||
|
||||
_.bindAll(this);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, trades) {
|
||||
if(err || !trades || trades.length === 0)
|
||||
return this.retry(this.getTrades, args, err);
|
||||
|
||||
if(descending)
|
||||
callback(null, trades);
|
||||
else
|
||||
callback(null, trades.reverse());
|
||||
}
|
||||
|
||||
this.cexio.trades({}, _.bind(process, this));
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
// Prevent "You incorrectly entered one of fields."
|
||||
// because of more than 8 decimals.
|
||||
amount *= 100000000;
|
||||
amount = Math.floor(amount);
|
||||
amount /= 100000000;
|
||||
|
||||
log.debug('BUY', amount, this.asset, '@', price, this.currency);
|
||||
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
return log.error('unable to buy:', err);
|
||||
if(data.error)
|
||||
return log.error('unable to buy:', data.error);
|
||||
|
||||
log.debug('BUY order placed. Order ID', data.id);
|
||||
callback(null, data.id);
|
||||
};
|
||||
|
||||
this.cexio.place_order('buy', amount, price, _.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
// Prevent "You incorrectly entered one of fields."
|
||||
// because of more than 8 decimals.
|
||||
amount *= 100000000;
|
||||
amount = Math.floor(amount);
|
||||
amount /= 100000000;
|
||||
|
||||
// test placing orders which will not be filled
|
||||
//price *= 10; price = price.toFixed(8);
|
||||
|
||||
log.debug('SELL', amount, this.asset, '@', price, this.currency);
|
||||
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
return log.error('unable to sell:', err);
|
||||
if(data.error)
|
||||
return log.error('unable to sell:', data.error);
|
||||
|
||||
log.debug('SELL order placed. Order ID', data.id);
|
||||
callback(null, data.id);
|
||||
};
|
||||
|
||||
this.cexio.place_order('sell', amount, price, _.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.retry = function(method, args, err) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..', err, 'waiting for', wait, 'ms');
|
||||
|
||||
if (!_.isFunction(method)) {
|
||||
log.error(this.name, 'failed to retry, no method supplied.');
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var calculate = function(err, data) {
|
||||
if(err)
|
||||
return this.retry(this.getPortfolio, args, err);
|
||||
|
||||
currency = parseFloat(data[this.currency].available)
|
||||
if(parseFloat(data[this.currency].orders)){
|
||||
currency -= parseFloat(data[this.currency].orders)
|
||||
}
|
||||
assets = parseFloat(data[this.asset].available);
|
||||
if( parseFloat(data[this.asset].orders)){
|
||||
assets -= parseFloat(data[this.asset].orders);
|
||||
}
|
||||
|
||||
var portfolio = [];
|
||||
portfolio.push({name: this.currency, amount: currency});
|
||||
portfolio.push({name: this.asset, amount: assets});
|
||||
callback(err, portfolio);
|
||||
}
|
||||
this.cexio.balance(calculate.bind(this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var set = function(err, data) {
|
||||
var ticker = {
|
||||
ask: data.ask,
|
||||
bid: data.bid
|
||||
};
|
||||
callback(err, ticker);
|
||||
}
|
||||
this.cexio.ticker(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
// cexio does currently don't take a fee on trades
|
||||
// TODO: isn't there an API call for this?
|
||||
callback(false, 0.002);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var check = function(err, result) {
|
||||
|
||||
if(err)
|
||||
return callback(false, true);
|
||||
if(result.error)
|
||||
return callback(false, true);
|
||||
|
||||
var exists = false;
|
||||
_.each(result, function(entry) {
|
||||
if(entry.id === order) {
|
||||
exists = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
callback(err, !exists);
|
||||
};
|
||||
|
||||
this.cexio.open_orders(_.bind(check, this));
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order) {
|
||||
var check= function(err, result) {
|
||||
if(err)
|
||||
log.error('cancel order failed:', err);
|
||||
if(typeof(result) !== 'undefined' && result.error)
|
||||
log.error('cancel order failed:', result.error);
|
||||
}
|
||||
this.cexio.cancel_order(order, check);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'CEX.io',
|
||||
slug: 'cexio',
|
||||
currencies: ['BTC', 'USD', 'EUR', 'RUB'],
|
||||
assets: ['GHS', 'BTC', 'ETH', 'LTC'],
|
||||
markets: [
|
||||
{ pair: ['BTC', 'GHS'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['EUR', 'LTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['USD', 'LTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['RUB', 'BTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['EUR', 'BTC'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['USD', 'ETH'], minimalOrder: { amount: 0.000001, unit: 'currency' } },
|
||||
{ pair: ['EUR', 'ETH'], minimalOrder: { amount: 0.000001, unit: 'currency' } }
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
providesHistory: false,
|
||||
tid: 'tid'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
{
|
||||
"assets": [
|
||||
"ETC",
|
||||
"BTC",
|
||||
"BCH",
|
||||
"ETH",
|
||||
"LTC",
|
||||
"ZRX"
|
||||
],
|
||||
"currencies": [
|
||||
"EUR",
|
||||
"USD",
|
||||
"BTC",
|
||||
"GBP"
|
||||
],
|
||||
"markets": [
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"ETC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.00100000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.00100000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GBP",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.00100000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"ETC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ETC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"ZRX"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "1.00000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ZRX"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "1.00000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"ZRX"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "1.00000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GBP",
|
||||
"ETC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GBP",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GBP",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.10000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GBP",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01000000",
|
||||
"unit": "asset"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"assets": [
|
||||
"IOT",
|
||||
"ETH",
|
||||
"LTC",
|
||||
"BTC",
|
||||
"NANO",
|
||||
"BCH",
|
||||
"KIN",
|
||||
"TRX",
|
||||
"CRPT",
|
||||
"GRLC",
|
||||
"ECA",
|
||||
"XRP",
|
||||
"ADA",
|
||||
"DOGE",
|
||||
"STQ",
|
||||
"DASC",
|
||||
"EOS",
|
||||
"DTX"
|
||||
],
|
||||
"currencies": [
|
||||
"BTC",
|
||||
"EUR",
|
||||
"ETH",
|
||||
"USDT"
|
||||
],
|
||||
"markets": [
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"IOT"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 0.00001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 0.00001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"NANO"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 0.00001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"KIN"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1,
|
||||
"price": 1e-10,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"ETH",
|
||||
"KIN"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"TRX"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"CRPT"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"GRLC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ECA"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"ETH",
|
||||
"ECA"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"XRP"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ADA"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.000001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"ETH",
|
||||
"ADA"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.000001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"DOGE"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.00001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"STQ"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"IOT"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.00001,
|
||||
"price": 0.0001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"DASC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.00001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"ETH",
|
||||
"DASC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.00001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"DASC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.00001,
|
||||
"price": 0.0001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"NANO"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.0001,
|
||||
"price": 0.0001,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USDT",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 1e-8,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USDT",
|
||||
"EOS"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.0001,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"DTX"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
const moment = require('moment');
|
||||
const _ = require('lodash');
|
||||
const marketData = require('./coinfalcon-markets.json');
|
||||
|
||||
const CoinFalcon = require('coinfalcon');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this, [
|
||||
'roundAmount',
|
||||
'roundPrice'
|
||||
]);
|
||||
|
||||
this.makerFee = 0;
|
||||
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency.toUpperCase();
|
||||
this.asset = config.asset.toUpperCase();
|
||||
}
|
||||
|
||||
this.pair = this.asset + '-' + this.currency;
|
||||
this.name = 'coinfalcon';
|
||||
|
||||
this.market = _.find(Trader.getCapabilities().markets, (market) => {
|
||||
return market.pair[0] === this.currency && market.pair[1] === this.asset
|
||||
});
|
||||
|
||||
this.coinfalcon = new CoinFalcon.Client(this.key, this.secret);
|
||||
|
||||
this.interval = 1500;
|
||||
};
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'ECONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'429',
|
||||
'522',
|
||||
'429',
|
||||
'504',
|
||||
'503',
|
||||
'500',
|
||||
'502',
|
||||
'408',
|
||||
// "The timestamp 1527996378 is invalid, current timestamp is 1527996441."
|
||||
'is invalid, current timestamp is',
|
||||
'EHOSTUNREACH',
|
||||
// https://github.com/askmike/gekko/issues/2407
|
||||
'We are fixing a few issues, be back shortly.',
|
||||
'Client network socket disconnected before secure TLS connection was established',
|
||||
'socket hang up',
|
||||
// getaddrinfo EAI_AGAIN coinfalcon.com coinfalcon.com:443
|
||||
'EAI_AGAIN'
|
||||
];
|
||||
|
||||
// errors that might mean
|
||||
// the API call succeeded.
|
||||
const unknownResultErrors = [
|
||||
'524',
|
||||
]
|
||||
|
||||
Trader.prototype.processResponse = function(method, args, next) {
|
||||
|
||||
const requestAt = moment();
|
||||
|
||||
const checkTime = () => {
|
||||
const diff = moment().diff(requestAt, 's');
|
||||
if(diff > 10) {
|
||||
console.log(new Date, '[CF] API CALL TOOK', diff, 'SECONDS!');
|
||||
console.log(new Date, '[CF]', {method, args, next});
|
||||
}
|
||||
}
|
||||
|
||||
const catcher = err => {
|
||||
if(!err || !err.message) {
|
||||
err = new Error(err || 'Empty error');
|
||||
}
|
||||
|
||||
if(includes(err.message, recoverableErrors)) {
|
||||
return this.retry(method, args);
|
||||
}
|
||||
|
||||
console.log(new Date, '[cf] big error!', err.message, method);
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return {
|
||||
failure: catcher,
|
||||
success: data => {
|
||||
if(!data)
|
||||
return catcher();
|
||||
|
||||
if(data.error)
|
||||
return catcher(data.error);
|
||||
|
||||
if(includes(data, ['Please complete the security check to proceed.']))
|
||||
return next(new Error(
|
||||
'Your IP has been flagged by CloudFlare. ' +
|
||||
'As such Gekko Broker cannot access Coinfalcon.'
|
||||
));
|
||||
|
||||
next(undefined, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(1, 'seconds');
|
||||
|
||||
// run the failed method again with the same arguments after wait
|
||||
setTimeout(() => {
|
||||
method.apply(this, args);
|
||||
}, wait);
|
||||
};
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const handle = this.processResponse(this.getTicker, [callback], (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
callback(null, {bid: +res.data.bids[0].price, ask: +res.data.asks[0].price})
|
||||
});
|
||||
|
||||
var url = "markets/" + this.pair + "/orders?level=1"
|
||||
|
||||
this.coinfalcon.get(url).then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
callback(false, this.makerFee / 100);
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const handle = this.processResponse(this.getPortfolio, [callback], (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
var portfolio = res.data.map(account => ({
|
||||
name: account.currency_code.toUpperCase(),
|
||||
amount: parseFloat(account.available_balance)
|
||||
}));
|
||||
|
||||
callback(null, portfolio);
|
||||
});
|
||||
|
||||
this.coinfalcon.get('user/accounts').then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.getOpenOrders = function(callback) {
|
||||
const handle = this.processResponse(this.getOpenOrders, [callback], (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
console.log(new Date, 'CF getOpenOrders', res.data);
|
||||
|
||||
callback(null, res.data.map(o => o.id));
|
||||
});
|
||||
|
||||
this.coinfalcon.get('user/orders').then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.addOrder = function(type, amount, price, callback) {
|
||||
const args = _.toArray(arguments);
|
||||
|
||||
const handle = this.processResponse(this.addOrder, args, (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
if(!res.data.id) {
|
||||
console.log(new Date, 'CF ERROR! CREATED ORDER BUT NO ID', res);
|
||||
}
|
||||
|
||||
callback(false, res.data.id);
|
||||
});
|
||||
|
||||
const payload = {
|
||||
order_type: type,
|
||||
operation_type: 'limit_order',
|
||||
market: this.pair,
|
||||
size: amount + '',
|
||||
price: price + ''
|
||||
}
|
||||
|
||||
this.coinfalcon.post('user/orders', payload).then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
['buy', 'sell'].map(function(type) {
|
||||
Trader.prototype[type] = function(amount, price, callback) {
|
||||
this.addOrder(type, amount, price, callback);
|
||||
};
|
||||
});
|
||||
|
||||
const round = function(number, precision) {
|
||||
var factor = Math.pow(10, precision);
|
||||
var tempNumber = number * factor;
|
||||
var roundedTempNumber = Math.round(tempNumber);
|
||||
return roundedTempNumber / factor;
|
||||
};
|
||||
|
||||
// ticksize 0.01 will yield: 2
|
||||
Trader.prototype.getPrecision = function(tickSize) {
|
||||
if (!isFinite(tickSize)) {
|
||||
return 0;
|
||||
}
|
||||
var e = 1;
|
||||
var p = 0;
|
||||
while (Math.round(tickSize * e) / e !== tickSize) {
|
||||
e *= 10; p++;
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return round(amount, this.getPrecision(this.market.minimalOrder.amount));
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return round(price, this.getPrecision(this.market.minimalOrder.price));
|
||||
}
|
||||
|
||||
Trader.prototype.outbidPrice = function(price, isUp) {
|
||||
let newPrice;
|
||||
|
||||
if(isUp) {
|
||||
newPrice = price + this.market.minimalOrder.price;
|
||||
} else {
|
||||
newPrice = price - this.market.minimalOrder.price;
|
||||
}
|
||||
|
||||
return this.roundPrice(newPrice);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
const args = _.toArray(arguments);
|
||||
const handle = this.processResponse(this.getOrder, args, (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
const price = parseFloat(res.data.price);
|
||||
const amount = parseFloat(res.data.size_filled);
|
||||
const date = moment(res.data.created_at);
|
||||
const fees = {};
|
||||
const feePercent = this.makerFee;
|
||||
callback(false, { price, amount, date, fees, feePercent });
|
||||
});
|
||||
|
||||
this.coinfalcon.get('user/orders/' + order).then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
const args = _.toArray(arguments);
|
||||
|
||||
const handle = this.processResponse(this.checkOrder, args, (err, res) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
// https://docs.coinfalcon.com/#list-orders
|
||||
const status = res.data.status;
|
||||
|
||||
if(status === 'canceled') {
|
||||
return callback(undefined, { executed: false, open: false });
|
||||
} if(status === 'fulfilled') {
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
} if(
|
||||
status === 'pending' ||
|
||||
status === 'partially_filled' ||
|
||||
status === 'open'
|
||||
) {
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: +res.data.size_filled });
|
||||
}
|
||||
|
||||
console.error(res.data);
|
||||
callback(new Error('Unknown status ' + status));
|
||||
});
|
||||
|
||||
this.coinfalcon.get('user/orders/' + order).then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
const args = _.toArray(arguments);
|
||||
|
||||
const handle = this.processResponse(this.cancelOrder, args, (err, res) => {
|
||||
|
||||
if(err) {
|
||||
if(err.message.includes('has wrong status.')) {
|
||||
|
||||
// see https://github.com/askmike/gekko/issues/2440
|
||||
return setTimeout(() => {
|
||||
this.checkOrder(order, (err, res) => {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(!res.open) {
|
||||
return callback(undefined, true);
|
||||
}
|
||||
|
||||
return setTimeout(
|
||||
() => this.cancelOrder(order, callback),
|
||||
this.interval
|
||||
);
|
||||
});
|
||||
}, this.interval);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(undefined, false, {
|
||||
filled: +res.data.size_filled
|
||||
});
|
||||
});
|
||||
|
||||
this.coinfalcon.delete('user/orders/' + order).then(handle.success).catch(handle.failure);
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
var success = function(res) {
|
||||
var parsedTrades = [];
|
||||
_.each(
|
||||
res.data,
|
||||
function(trade) {
|
||||
parsedTrades.push({
|
||||
tid: trade.id,
|
||||
date: moment(trade.created_at).unix(),
|
||||
price: parseFloat(trade.price),
|
||||
amount: parseFloat(trade.size),
|
||||
});
|
||||
},
|
||||
this
|
||||
);
|
||||
|
||||
if (descending) {
|
||||
callback(null, parsedTrades);
|
||||
} else {
|
||||
callback(null, parsedTrades.reverse());
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
var failure = function (err) {
|
||||
err = new Error(err);
|
||||
return this.retry(this.getTrades, args, err);
|
||||
}.bind(this);
|
||||
|
||||
var url = "markets/" + this.pair + "/trades"
|
||||
|
||||
if (since) {
|
||||
url += '?since_time=' + (_.isString(since) ? since : since.format());
|
||||
}
|
||||
|
||||
this.coinfalcon.get(url).then(success).catch(failure);
|
||||
};
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'CoinFalcon',
|
||||
slug: 'coinfalcon',
|
||||
assets: marketData.assets,
|
||||
currencies: marketData.currencies,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
forceReorderDelay: false,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
Executable
+283
@@ -0,0 +1,283 @@
|
||||
var Coingi = require('coingi');
|
||||
var moment = require('moment');
|
||||
var _ = require('lodash');
|
||||
|
||||
var util = require('../core/util');
|
||||
var Errors = require('../core/error');
|
||||
var log = require('../core/log');
|
||||
|
||||
|
||||
var Trader = function (config) {
|
||||
_.bindAll(this);
|
||||
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency.toUpperCase()
|
||||
this.asset = config.asset.toUpperCase();
|
||||
}
|
||||
|
||||
this.pair = this.asset + "-" + this.currency;
|
||||
this.name = 'coingi';
|
||||
this.since = null;
|
||||
|
||||
this.coingi = new Coingi(
|
||||
this.key,
|
||||
this.secret,
|
||||
{timeout: +moment.duration(60, 'seconds')}
|
||||
);
|
||||
}
|
||||
|
||||
var retryCritical = {
|
||||
retries: 10,
|
||||
factor: 1.2,
|
||||
minTimeout: 1 * 1000,
|
||||
maxTimeout: 30 * 1000
|
||||
};
|
||||
|
||||
var retryForever = {
|
||||
forever: true,
|
||||
factor: 1.2,
|
||||
minTimeout: 10,
|
||||
maxTimeout: 30
|
||||
};
|
||||
|
||||
var recoverableErrors = new RegExp(/(SOCKETTIMEDOUT|TIMEDOUT|CONNRESET|CONNREFUSED|NOTFOUND|API:Invalid nonce|Service:Unavailable|Request timed out|Response code 520|Response code 504|Response code 502)/)
|
||||
|
||||
Trader.prototype.processError = function (funcName, error) {
|
||||
if (!error)
|
||||
return undefined;
|
||||
|
||||
if (!error.message.match(recoverableErrors)) {
|
||||
log.error(`[coingi.js] (${funcName}) returned an irrecoverable error: ${error.message}`);
|
||||
return new Errors.AbortError('[coingi.js] ' + error.message);
|
||||
}
|
||||
|
||||
log.debug(`[coingi.js] (${funcName}) returned an error, retrying: ${error.message}`);
|
||||
return new Errors.RetryError('[coingi.js] ' + error.message);
|
||||
};
|
||||
|
||||
Trader.prototype.handleResponse = function (funcName, callback) {
|
||||
return (error, body) => {
|
||||
if (!error) {
|
||||
if (_.isEmpty(body))
|
||||
error = new Error('NO DATA WAS RETURNED');
|
||||
|
||||
else if (!_.isEmpty(body.error))
|
||||
error = new Error(body.error);
|
||||
}
|
||||
|
||||
return callback(this.processError(funcName, error), body);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/* PORTFOLIO MANAGER */
|
||||
|
||||
Trader.prototype.getTicker = function (callback) {
|
||||
var setTicker = function (err, data) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
var ticker = {
|
||||
ask: data.asks[0].price,
|
||||
bid: data.bids[0].price
|
||||
};
|
||||
callback(undefined, ticker);
|
||||
};
|
||||
|
||||
let handler = (cb) => this.coingi.api('order-book', "/" + this.pair + "/1/1/1", this.handleResponse('getTicker', cb));
|
||||
util.retryCustom(retryForever, _.bind(handler, this), _.bind(setTicker, this));
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getTrades = function (since, callback, ascending) {
|
||||
var startTs = since ? moment(since).valueOf() : null;
|
||||
|
||||
var processResults = function (err, trades) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var parsedTrades = [];
|
||||
_.each(trades, function (trade) {
|
||||
// Even when you supply 'since' you can still get more trades than you asked for, it needs to be filtered
|
||||
if (_.isNull(startTs) || startTs < moment(trade.timestamp).valueOf()) {
|
||||
parsedTrades.push({
|
||||
type: trade.type === 0 ? "sell" : "buy",
|
||||
date: moment(trade.timestamp).unix(),
|
||||
amount: String(trade.amount),
|
||||
price: String(trade.price),
|
||||
tid: trade.timestamp
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
|
||||
if(ascending)
|
||||
callback(undefined, parsedTrades);
|
||||
else
|
||||
callback(undefined, parsedTrades.reverse());
|
||||
};
|
||||
|
||||
var optionalSince = "";
|
||||
if (since) {
|
||||
optionalSince = "/" + startTs;
|
||||
}
|
||||
|
||||
let handler = (cb) => this.coingi.api('transactions', "/" + this.pair + "/512" + optionalSince, this.handleResponse('getTrades', cb));
|
||||
util.retryCustom(retryForever, _.bind(handler, this), _.bind(processResults, this));
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function (callback) {
|
||||
var setBalance = function (err, data) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
log.debug('[coingi.js] entering "setBalance" callback after coingi-api call, data:', data);
|
||||
|
||||
const portfolio = [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
portfolio.push({
|
||||
name: data[i].currency.name.toUpperCase(),
|
||||
amount: data[i].available
|
||||
});
|
||||
}
|
||||
|
||||
return callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
let handler = (cb) => this.coingi.api('balance', {currencies: this.asset + "," + this.currency}, this.handleResponse('getPortfolio', cb));
|
||||
util.retryCustom(retryForever, _.bind(handler, this), _.bind(setBalance, this));
|
||||
};
|
||||
|
||||
// Base fee is 0.2%
|
||||
Trader.prototype.getFee = function (callback) {
|
||||
const fee = 0.2;
|
||||
callback(undefined, fee / 100);
|
||||
};
|
||||
|
||||
Trader.prototype.addOrder = function (tradeType, amount, price, callback) {
|
||||
log.debug('[coingi.js] (add-order)', tradeType.toUpperCase(), amount, this.asset, '@', price, this.currency);
|
||||
|
||||
var setOrder = function (err, data) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
var uuid = data.result;
|
||||
log.debug('[coingi.js] (addOrder) added order with uuid:', uuid);
|
||||
|
||||
callback(undefined, uuid);
|
||||
};
|
||||
|
||||
let reqData = {
|
||||
currencyPair: this.pair,
|
||||
type: tradeType.toLowerCase() === "sell" ? 0 : 1,
|
||||
price: price,
|
||||
volume: amount.toString()
|
||||
};
|
||||
|
||||
let handler = (cb) => this.coingi.api('add-order', reqData, this.handleResponse('addOrder', cb));
|
||||
util.retryCustom(retryCritical, _.bind(handler, this), _.bind(setOrder, this));
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function (orderId, callback) {
|
||||
var getOrder = function (err, order) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
if (order !== null) {
|
||||
const price = parseFloat(order.price);
|
||||
const amount = parseFloat(order.baseAmount);
|
||||
const date = moment.unix(order.timestamp);
|
||||
callback(undefined, {price, amount, date});
|
||||
} else {
|
||||
log.error("Error! Order ID '" + orderId + "' couldn't be found in the result of get order!");
|
||||
}
|
||||
};
|
||||
|
||||
let reqData = {ordeId: orderId};
|
||||
let handler = (cb) => this.coingi.api('get-order', reqData, this.handleResponse('getOrder', cb));
|
||||
util.retryCustom(retryCritical, _.bind(handler, this), _.bind(getOrder, this));
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function (amount, price, callback) {
|
||||
this.addOrder('buy', amount, price, callback);
|
||||
};
|
||||
|
||||
Trader.prototype.sell = function (amount, price, callback) {
|
||||
this.addOrder('sell', amount, price, callback);
|
||||
};
|
||||
|
||||
Trader.prototype.checkOrder = function (orderId, callback) {
|
||||
var check = function (err, order) {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
if (order === null) {
|
||||
log.error("Error! Order ID '" + orderId + "' couldn't be found in the result of get order!");
|
||||
}
|
||||
// returns true when the order has been filled (processed in full), false otherwise
|
||||
callback(undefined, order !== null && order.status === 2);
|
||||
};
|
||||
|
||||
let reqData = {orderId: orderId};
|
||||
let handler = (cb) => this.coingi.api('get-order', reqData, this.handleResponse('checkOrder', cb));
|
||||
util.retryCustom(retryCritical, _.bind(handler, this), _.bind(check, this));
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function (order, callback) {
|
||||
let reqData = {orderId: order};
|
||||
let handler = (cb) => this.coingi.api('cancel-order', reqData, this.handleResponse('cancelOrder', cb));
|
||||
util.retryCustom(retryForever, _.bind(handler, this), callback);
|
||||
};
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Coingi',
|
||||
slug: 'coingi',
|
||||
currencies: ['EUR', 'USD', 'BTC'],
|
||||
assets: ['BTC', 'DASH', 'DOGE', 'EUR', 'LTC', 'NMC', 'PPC', 'VTC'],
|
||||
markets: [
|
||||
// Tradeable against BTC
|
||||
{pair: ['USD', 'BTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['EUR', 'BTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against DASH
|
||||
{pair: ['BTC', 'DASH'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against DOGE
|
||||
{pair: ['BTC', 'DOGE'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['USD', 'DOGE'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against EUR
|
||||
{pair: ['USD', 'EUR'], minimalOrder: {amount: 0.01, unit: 'asset'}, precision: 2},
|
||||
|
||||
// Tradeable against LTC
|
||||
{pair: ['BTC', 'LTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['EUR', 'LTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['USD', 'LTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against NMC
|
||||
{pair: ['BTC', 'NMC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against PPC
|
||||
{pair: ['USD', 'PPC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['EUR', 'PPC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
{pair: ['BTC', 'PPC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8},
|
||||
|
||||
// Tradeable against VTC
|
||||
{pair: ['BTC', 'VTC'], minimalOrder: {amount: 0.00001, unit: 'asset'}, precision: 8}
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
exchangeMaxHistoryAge: 30,
|
||||
tid: 'date',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"assets": [
|
||||
"BTC",
|
||||
"ETH"
|
||||
],
|
||||
"currencies": [
|
||||
"EUR",
|
||||
"USD",
|
||||
"LTC"
|
||||
],
|
||||
"markets": [
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.001",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.001",
|
||||
"unit": "asset"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"LTC",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": "0.01",
|
||||
"unit": "asset"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
|
||||
|
||||
const CryptoJS = require("crypto-js");
|
||||
const querystring = require('querystring');
|
||||
const request = require('request');
|
||||
|
||||
API_URL='https://api.exmo.com/v1/';
|
||||
|
||||
const marketData = require('./exmo-markets.json');
|
||||
|
||||
|
||||
const Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
this.key="";
|
||||
this.secret="";
|
||||
|
||||
if(_.isObject(config)) {
|
||||
if(_.isString(config.key)) this.key = config.key;
|
||||
if(_.isString(config.secret)) this.secret = config.secret;
|
||||
this.currency = config.currency;
|
||||
this.asset = config.asset;
|
||||
this.pair = this.asset + '_' + this.currency;
|
||||
};
|
||||
|
||||
this.name = 'EXMO';
|
||||
this.nonce = new Date() * 1000;
|
||||
}
|
||||
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'EHOSTUNREACH',
|
||||
];
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.api_query = function(method, params, callback){
|
||||
params.nonce = this.nonce++;
|
||||
var post_data = querystring.stringify(params);
|
||||
|
||||
var options = {
|
||||
url: API_URL + method,
|
||||
headers: {'Key': this.key,'Sign': CryptoJS.HmacSHA512(post_data, this.secret).toString(CryptoJS.enc.hex) },
|
||||
form: params
|
||||
};
|
||||
|
||||
request.post(options, function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
data=JSON.parse(body);
|
||||
if(data.error) error = { message: data.error }
|
||||
else if (data.result!=undefined && data.result==false) error = { message: '"result": false' } ;
|
||||
callback(error, data);
|
||||
} else {
|
||||
console.log('cb request error');
|
||||
console.log(body);
|
||||
|
||||
if(error) {
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
console.log(error);
|
||||
callback(error);
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const processResponse = (error, data) => {
|
||||
if (error) return callback(error);
|
||||
|
||||
data=data[this.pair];
|
||||
|
||||
var parsedTrades = _.map(data, function(trade) {
|
||||
return {
|
||||
tid: trade.trade_id,
|
||||
date: trade.date,
|
||||
price: +trade.price,
|
||||
amount: +trade.quantity
|
||||
};
|
||||
});
|
||||
|
||||
if (descending) callback(undefined, parsedTrades);
|
||||
else callback(undefined, parsedTrades.reverse());
|
||||
};
|
||||
|
||||
const fetch = cb => this.api_query("trades", { pair: this.pair} , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
data=data[this.pair];
|
||||
|
||||
callback(undefined, {bid: +data.sell_price, ask: +data.buy_price});
|
||||
};
|
||||
|
||||
const fetch = cb => this.api_query("ticker", { pair: this.pair} , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
callback(undefined, 0.002);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return _.floor(amount, 8);
|
||||
};
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return price;
|
||||
}
|
||||
|
||||
Trader.prototype.submitOrder = function(type, amount, price, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err)
|
||||
return callback(err);
|
||||
|
||||
callback(null, data.order_id);
|
||||
}
|
||||
|
||||
const fetch = cb => this.api_query("order_create", { pair: this.pair, quantity: amount, price: price, type: type } , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
//callback(null, 1177669837);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.submitOrder('buy', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.submitOrder('sell', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
data=data[this.pair];
|
||||
|
||||
if(data==undefined) return callback(new Error('Orders not found'));
|
||||
|
||||
const order = _.find(data, function(o) { return o.order_id === +order_id });
|
||||
|
||||
if(!order) return callback(new Error('Order not found'));
|
||||
|
||||
callback(undefined, { price: order.price, amount: order.amount, date: moment.unix(order.date) });
|
||||
}
|
||||
|
||||
const fetch = cb => this.api_query("user_trades", { pair: this.pair} , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
data=data[this.pair];
|
||||
|
||||
if(data==undefined) {
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
}
|
||||
|
||||
const order = _.find(data, function(o) { return o.order_id === +order_id });
|
||||
if(!order)
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
|
||||
callback(undefined, { executed: false, open: true /*, filledAmount: order.startingAmount - order.amount*/ });
|
||||
}
|
||||
|
||||
const fetch = cb => this.api_query("user_open_orders", { } , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order_id, callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(undefined, false);
|
||||
}
|
||||
|
||||
const fetch = cb => this.api_query("order_cancel", { order_id } , cb);
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const processResponse = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
data=data["balances"];
|
||||
const balances = _.map(data, function (v,k){ return {name: k, amount: +v};});
|
||||
const portfolio = balances.filter(c => c.name===this.asset || c.name===this.currency)
|
||||
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.api_query("user_info", {}, cb);
|
||||
retry(null, fetch, processResponse);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'EXMO',
|
||||
slug: 'exmo',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: false,
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,430 @@
|
||||
const Gdax = require('gdax');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const errors = require('../exchangeErrors');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
|
||||
const BATCH_SIZE = 100;
|
||||
const QUERY_DELAY = 350;
|
||||
|
||||
const marketData = require('./coinbase-markets.json');
|
||||
|
||||
const Trader = function(config) {
|
||||
this.post_only = true;
|
||||
this.use_sandbox = false;
|
||||
this.name = 'GDAX';
|
||||
this.scanback = false;
|
||||
this.scanbackTid = 0;
|
||||
this.scanbackResults = [];
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
|
||||
this.api_url = 'https://api.pro.coinbase.com';
|
||||
this.api_sandbox_url = 'https://api-public.sandbox.pro.coinbase.com';
|
||||
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.passphrase = config.passphrase;
|
||||
|
||||
this.pair = [config.asset, config.currency].join('-').toUpperCase();
|
||||
this.post_only =
|
||||
typeof config.post_only !== 'undefined' ? config.post_only : true;
|
||||
|
||||
if (config.sandbox) {
|
||||
this.use_sandbox = config.sandbox;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.gdax_public = new Gdax.PublicClient(
|
||||
this.use_sandbox ? this.api_sandbox_url : undefined
|
||||
);
|
||||
this.gdax = new Gdax.AuthenticatedClient(
|
||||
this.key,
|
||||
this.secret,
|
||||
this.passphrase,
|
||||
this.use_sandbox ? this.api_sandbox_url : undefined
|
||||
);
|
||||
};
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'Rate limit exceeded',
|
||||
'Response code 5',
|
||||
'GDAX is currently under maintenance.',
|
||||
'HTTP 408 Error',
|
||||
'HTTP 504 Error',
|
||||
'HTTP 503 Error',
|
||||
'socket hang up',
|
||||
'EHOSTUNREACH',
|
||||
'EAI_AGAIN',
|
||||
'ENETUNREACH'
|
||||
];
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.processResponse = function(method, next) {
|
||||
return (error, response, body) => {
|
||||
if(!error && body && !_.isEmpty(body.message)) {
|
||||
error = new Error(body.message);
|
||||
}
|
||||
|
||||
if(
|
||||
response &&
|
||||
response.statusCode < 200 &&
|
||||
response.statusCode >= 300
|
||||
) {
|
||||
error = new Error(`Response code ${response.statusCode}`);
|
||||
}
|
||||
|
||||
if(error) {
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 1000;
|
||||
}
|
||||
|
||||
if(
|
||||
['buy', 'sell'].includes(method) &&
|
||||
error.message.includes('Insufficient funds')
|
||||
) {
|
||||
error.retry = 10;
|
||||
}
|
||||
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return next(undefined, body);
|
||||
}
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var portfolio = data.map(function(account) {
|
||||
return {
|
||||
name: account.currency.toUpperCase(),
|
||||
amount: parseFloat(account.available),
|
||||
};
|
||||
});
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.getAccounts(this.processResponse('getPortfolio', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
callback(undefined, { bid: +data.bid, ask: +data.ask });
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax_public.getProductTicker(this.pair, this.processResponse('getTicker', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
//https://www.gdax.com/fees
|
||||
// const fee = this.asset == 'BTC' ? 0.0025 : 0.003;
|
||||
const fee = 0;
|
||||
|
||||
//There is no maker fee, not sure if we need taker fee here
|
||||
//If post only is enabled, gdax only does maker trades which are free
|
||||
callback(undefined, this.post_only ? 0 : fee);
|
||||
};
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return this.getMaxDecimalsNumber(amount);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
const buyParams = {
|
||||
price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
|
||||
size: this.getMaxDecimalsNumber(amount),
|
||||
product_id: this.pair,
|
||||
post_only: this.post_only,
|
||||
};
|
||||
|
||||
const result = (err, data) => {
|
||||
if (err) {
|
||||
console.log({buyParams}, err.message);
|
||||
return callback(err);
|
||||
}
|
||||
callback(undefined, data.id);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.buy(buyParams, this.processResponse('buy', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
const sellParams = {
|
||||
price: this.getMaxDecimalsNumber(price, this.currency == 'BTC' ? 5 : 2),
|
||||
size: this.getMaxDecimalsNumber(amount),
|
||||
product_id: this.pair,
|
||||
post_only: this.post_only,
|
||||
};
|
||||
|
||||
const result = (err, data) => {
|
||||
if (err) {
|
||||
console.log({sellParams}, err.message);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(data.message && data.message.includes('Insufficient funds')) {
|
||||
err = new Error(data.message);
|
||||
err.retryOnce = true;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(undefined, data.id);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.sell(sellParams, this.processResponse('sell', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// @link:
|
||||
// https://stackoverflow.com/questions/48132078/available-gdax-order-statuses-and-meanings
|
||||
var status = data.status;
|
||||
if(status == 'pending') {
|
||||
// technically not open yet, but will be soon
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: 0 });
|
||||
} if (status === 'done' || status === 'settled') {
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
} else if (status === 'rejected') {
|
||||
return callback(undefined, { executed: false, open: false });
|
||||
} else if(status === 'open' || status === 'active') {
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.filled_size) });
|
||||
}
|
||||
|
||||
callback(new Error('Unknown status ' + status));
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.getOrder(order, this.processResponse('checkOrder', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const price = parseFloat(data.price);
|
||||
const amount = parseFloat(data.filled_size);
|
||||
const date = moment(data.done_at);
|
||||
const fees = {
|
||||
// you always pay fee in the base currency on gdax
|
||||
[this.currency]: +data.fill_fees
|
||||
}
|
||||
const feePercent = +data.fill_fees / price / amount * 100;
|
||||
|
||||
callback(undefined, { price, amount, date, fees, feePercent });
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.getOrder(order, this.processResponse('getOrder', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
// callback for cancelOrder should be true if the order was already filled, otherwise false
|
||||
const result = (err, data) => {
|
||||
if(err) {
|
||||
return callback(null, true); // need to catch the specific error but usually an error on cancel means it was filled
|
||||
}
|
||||
|
||||
return callback(null, false);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax.cancelOrder(order, this.processResponse('cancelOrder', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var lastScan = 0;
|
||||
|
||||
const handle = function(err, data) {
|
||||
if (err) return callback(err);
|
||||
|
||||
var result = _.map(data, function(trade) {
|
||||
return {
|
||||
tid: trade.trade_id,
|
||||
amount: parseFloat(trade.size),
|
||||
date: moment.utc(trade.time).format('X'),
|
||||
price: parseFloat(trade.price),
|
||||
};
|
||||
});
|
||||
|
||||
if (this.scanback) {
|
||||
var last = _.last(data);
|
||||
var first = _.first(data);
|
||||
|
||||
// Try to find trade id matching the since date
|
||||
if (!this.scanbackTid) {
|
||||
// either scan for new ones or we found it.
|
||||
if (moment.utc(last.time) < moment.utc(since)) {
|
||||
this.scanbackTid = last.trade_id;
|
||||
} else {
|
||||
console.log('Scanning backwards...' + last.time);
|
||||
setTimeout(() => {
|
||||
let handler = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{
|
||||
after: last.trade_id - BATCH_SIZE * lastScan,
|
||||
limit: BATCH_SIZE,
|
||||
},
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(
|
||||
retryForever,
|
||||
_.bind(handler, this),
|
||||
_.bind(process, this)
|
||||
);
|
||||
}, QUERY_DELAY);
|
||||
lastScan++;
|
||||
if (lastScan > 100) {
|
||||
lastScan = 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.scanbackTid) {
|
||||
// if scanbackTid is set we need to move forward again
|
||||
console.log(
|
||||
'Backwards: ' +
|
||||
last.time +
|
||||
' (' +
|
||||
last.trade_id +
|
||||
') to ' +
|
||||
first.time +
|
||||
' (' +
|
||||
first.trade_id +
|
||||
')'
|
||||
);
|
||||
|
||||
this.scanbackResults = this.scanbackResults.concat(result.reverse());
|
||||
|
||||
if (this.scanbackTid != first.trade_id) {
|
||||
this.scanbackTid = first.trade_id;
|
||||
setTimeout(() => {
|
||||
let handler = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{ after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(
|
||||
retryForever,
|
||||
_.bind(handler, this),
|
||||
_.bind(process, this)
|
||||
);
|
||||
}, QUERY_DELAY);
|
||||
} else {
|
||||
this.scanback = false;
|
||||
this.scanbackTid = 0;
|
||||
|
||||
console.log('Scan finished: data found:' + this.scanbackResults.length);
|
||||
callback(null, this.scanbackResults);
|
||||
|
||||
this.scanbackResults = [];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
callback(null, result.reverse());
|
||||
}
|
||||
};
|
||||
|
||||
if (since || this.scanback) {
|
||||
this.scanback = true;
|
||||
if (this.scanbackTid) {
|
||||
let handler = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{ after: this.scanbackTid + BATCH_SIZE + 1, limit: BATCH_SIZE },
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(
|
||||
retryForever,
|
||||
_.bind(handler, this),
|
||||
_.bind(process, this)
|
||||
);
|
||||
} else if (since) {
|
||||
console.log('Scanning back in the history needed...', since);
|
||||
}
|
||||
}
|
||||
|
||||
const fetch = cb =>
|
||||
this.gdax_public.getProductTrades(
|
||||
this.pair,
|
||||
{ limit: BATCH_SIZE },
|
||||
this.processResponse('getTrades', cb)
|
||||
);
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.getMaxDecimalsNumber = function(number, decimalLimit = 8) {
|
||||
var decimalNumber = parseFloat(number);
|
||||
|
||||
// The ^-?\d*\. strips off any sign, integer portion, and decimal point
|
||||
// leaving only the decimal fraction.
|
||||
// The 0+$ strips off any trailing zeroes.
|
||||
var decimalCount = (+decimalNumber).toString().replace(/^-?\d*\.?|0+$/g, '')
|
||||
.length;
|
||||
|
||||
var decimalMultiplier = 1;
|
||||
for (i = 0; i < decimalLimit; i++) {
|
||||
decimalMultiplier *= 10;
|
||||
}
|
||||
|
||||
return decimalCount <= decimalLimit
|
||||
? decimalNumber.toString()
|
||||
: (
|
||||
Math.floor(decimalNumber * decimalMultiplier) / decimalMultiplier
|
||||
).toFixed(decimalLimit);
|
||||
};
|
||||
|
||||
Trader.getCapabilities = function() {
|
||||
return {
|
||||
name: 'GDAX',
|
||||
slug: 'gdax',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret', 'passphrase'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
forceReorderDelay: false,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,228 @@
|
||||
var Gemini = require('gemini-exchange-coffee-api/lib/gemini');
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
}
|
||||
this.name = 'Gemini';
|
||||
this.balance;
|
||||
this.price;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
this.pair = this.asset + this.currency;
|
||||
this.gemini = new Gemini(this.key, this.secret);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
this.gemini.wallet_balances(function (err, data, body) {
|
||||
|
||||
if(err && err.message === '401') {
|
||||
let e = 'Gemini replied with an unauthorized error. ';
|
||||
e += 'Double check whether your API key is correct.';
|
||||
util.die(e);
|
||||
}
|
||||
|
||||
if(err || !data)
|
||||
return this.retry(this.getPortfolio, args);
|
||||
|
||||
// We are only interested in funds in the "exchange" wallet
|
||||
data = data.filter(c => c.type === 'exchange');
|
||||
|
||||
const asset = _.find(data, c => c.currency.toUpperCase() === this.asset);
|
||||
const currency = _.find(data, c => c.currency.toUpperCase() === this.currency);
|
||||
|
||||
let assetAmount, currencyAmount;
|
||||
|
||||
if(_.isObject(asset) && _.isNumber(+asset.available) && !_.isNaN(+asset.available))
|
||||
assetAmount = +asset.available;
|
||||
else {
|
||||
log.error(`Gemini did not provide ${this.asset} amount, assuming 0`);
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(_.isObject(currency) && _.isNumber(+currency.available) && !_.isNaN(+currency.available))
|
||||
currencyAmount = +currency.available;
|
||||
else {
|
||||
log.error(`Gemini did not provide ${this.currency} amount, assuming 0`);
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount },
|
||||
];
|
||||
|
||||
callback(err, portfolio);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
// the function that will handle the API callback
|
||||
var process = function(err, data, body) {
|
||||
if (err)
|
||||
return this.retry(this.getTicker(args));
|
||||
|
||||
// whenever we reach this point we have valid
|
||||
// data, the callback is still the same since
|
||||
// we are inside the same javascript scope.
|
||||
callback(err, {bid: +data.bid, ask: +data.ask})
|
||||
}.bind(this);
|
||||
this.gemini.ticker(this.pair, process);
|
||||
}
|
||||
|
||||
// This assumes that only limit orders are being placed, so fees are the
|
||||
// "maker fee" of 0.1%. It does not take into account volume discounts.
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var makerFee = 0.25;
|
||||
callback(false, makerFee / 100);
|
||||
}
|
||||
|
||||
Trader.prototype.submit_order = function(type, amount, price, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
amount = Math.floor(amount*100000000)/100000000;
|
||||
this.gemini.new_order(
|
||||
this.pair,
|
||||
amount + '',
|
||||
price + '',
|
||||
this.name.toLowerCase(),
|
||||
type,
|
||||
'exchange limit',
|
||||
function (err, data, body) {
|
||||
if (err) {
|
||||
log.error('unable to ' + type, err, body);
|
||||
return this.retry(this.submit_order, args);
|
||||
}
|
||||
|
||||
callback(err, data.order_id);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.submit_order('buy', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.submit_order('sell', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order_id, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
this.gemini.order_status(order_id, function (err, data, body) {
|
||||
|
||||
if(err || !data)
|
||||
return this.retry(this.checkOrder, arguments);
|
||||
|
||||
callback(err, !data.is_live);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var get = function(err, data) {
|
||||
if(err || !data)
|
||||
return this.retry(this.getOrder, arguments);
|
||||
|
||||
var price = parseFloat(data.avg_execution_price);
|
||||
var amount = parseFloat(data.executed_amount);
|
||||
var date = moment.unix(data.timestamp);
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.gemini.order_status(order, get);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.cancelOrder = function(order_id, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
this.gemini.cancel_order(order_id, function (err, data, body) {
|
||||
if (err || !data) {
|
||||
// gemini way of telling it was already cancelled..
|
||||
if(err.message === 'Order could not be cancelled.')
|
||||
return callback();
|
||||
|
||||
log.error('unable to cancel order', order_id, '(', err, data, '), retrying...');
|
||||
return this.retry(this.cancelOrder, args);
|
||||
}
|
||||
|
||||
return callback();
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
var path = this.pair;
|
||||
if(since)
|
||||
path += '?limit_trades=2000';
|
||||
|
||||
this.gemini.trades(path, function(err, data) {
|
||||
if (err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
var trades = _.map(data, function(trade) {
|
||||
return {
|
||||
tid: trade.tid,
|
||||
date: trade.timestamp,
|
||||
price: +trade.price,
|
||||
amount: +trade.amount
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, descending ? trades : trades.reverse());
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Gemini',
|
||||
slug: 'gemini',
|
||||
currencies: ['USD', 'BTC'],
|
||||
assets: ['BTC', 'ETH'],
|
||||
markets: [
|
||||
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['USD', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,471 @@
|
||||
const Kraken = require('kraken-api');
|
||||
const moment = require('moment');
|
||||
const _ = require('lodash');
|
||||
const exchangeUtils = require('../exchangeUtils');
|
||||
const retry = exchangeUtils.retry;
|
||||
const scientificToDecimal = exchangeUtils.scientificToDecimal;
|
||||
|
||||
const marketData = require('./kraken-markets.json');
|
||||
|
||||
const Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency.toUpperCase()
|
||||
this.asset = config.asset.toUpperCase();
|
||||
}
|
||||
|
||||
this.name = 'kraken';
|
||||
this.since = null;
|
||||
|
||||
this.market = _.find(Trader.getCapabilities().markets, (market) => {
|
||||
return market.pair[0] === this.currency && market.pair[1] === this.asset
|
||||
});
|
||||
this.pair = this.market.book;
|
||||
|
||||
this.interval = 3100;
|
||||
|
||||
this.kraken = new Kraken(
|
||||
this.key,
|
||||
this.secret,
|
||||
{timeout: +moment.duration(60, 'seconds')}
|
||||
);
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'Service:Unavailable',
|
||||
'Request timed out',
|
||||
'Empty response',
|
||||
'API:Invalid nonce',
|
||||
'General:Temporary lockout',
|
||||
'Response code 525',
|
||||
'Service:Busy'
|
||||
];
|
||||
|
||||
// errors that might mean
|
||||
// the API call succeeded.
|
||||
const unknownResultErrors = [
|
||||
'Response code 502',
|
||||
'Response code 504',
|
||||
'Response code 522',
|
||||
'Response code 520',
|
||||
]
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.handleResponse = function(funcName, callback, nonMutating, payload) {
|
||||
return (error, body) => {
|
||||
|
||||
if(!error && !body) {
|
||||
error = new Error('Empty response');
|
||||
}
|
||||
|
||||
if(error) {
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
if(includes(error.message, ['Rate limit exceeded'])) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 2500;
|
||||
}
|
||||
|
||||
if(nonMutating && includes(error.message, unknownResultErrors)) {
|
||||
// this call only tried to retrieve data, safe to redo
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
if(funcName === 'addOrder' && includes(error.message, unknownResultErrors)) {
|
||||
|
||||
const { tradeType, amount, price } = payload;
|
||||
|
||||
return setTimeout(() => {
|
||||
this.getRawOpenOrders((err2, orders) => {
|
||||
if(err2) {
|
||||
console.log('err2', err2);
|
||||
return callback(err2);
|
||||
}
|
||||
|
||||
_.each(orders, (o, id) => {
|
||||
o.id = id;
|
||||
});
|
||||
|
||||
const order = _.find(orders, o => {
|
||||
if(o.descr.type !== tradeType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ts = moment.unix((o.opentm + '').split('.')[0])
|
||||
if(moment().diff(ts, 'm') > 10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// string vs float
|
||||
if(+o.descr.price != price) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// string vs float
|
||||
if(o.vol != amount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if(!order) {
|
||||
console.log('broken add order, appears not created:', {payload, orders: JSON.stringify(orders)});
|
||||
return this.addOrder(tradeType, amount, price, callback);
|
||||
}
|
||||
|
||||
return callback(undefined, { catched: true, id: order.id });
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
if(funcName === 'cancelOrder' && includes(error.message, unknownResultErrors)) {
|
||||
console.log('broken cancel');
|
||||
|
||||
return setTimeout(() => {
|
||||
const handle = (err2, data) => {
|
||||
if(err2) {
|
||||
console.log('err2', err2);
|
||||
return callback(err2);
|
||||
}
|
||||
|
||||
const order = _.get(data, `result["${payload}"]`);
|
||||
|
||||
if(!_.isObject(order)) {
|
||||
console.log('refetched broken cancel, cannot find order...', data);
|
||||
throw 'a';
|
||||
}
|
||||
|
||||
console.log(order);
|
||||
|
||||
if(order.status !== 'canceled') {
|
||||
console.log(new Date, 'it still exists, retrying cancel');
|
||||
return this.cancelOrder(payload, callback);
|
||||
}
|
||||
|
||||
console.log(new Date, 'it was canceled');
|
||||
return callback(undefined, true, { catched: true, filled: parseFloat(order.vol_exec) });
|
||||
|
||||
};
|
||||
|
||||
const reqData = {txid: payload};
|
||||
|
||||
const fetch = cb => this.kraken.api('QueryOrders', reqData, this.handleResponse('checkOrder', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
return callback(undefined, body);
|
||||
}
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const startTs = since ? moment(since).valueOf() : null;
|
||||
|
||||
const handle = (err, trades) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var parsedTrades = [];
|
||||
_.each(trades.result[this.pair], function(trade) {
|
||||
// Even when you supply 'since' you can still get more trades than you asked for, it needs to be filtered
|
||||
if (_.isNull(startTs) || startTs < moment.unix(trade[2]).valueOf()) {
|
||||
parsedTrades.push({
|
||||
tid: moment.unix(trade[2]).valueOf() * 1000000,
|
||||
date: parseInt(Math.round(trade[2]), 10),
|
||||
price: parseFloat(trade[0]),
|
||||
amount: parseFloat(trade[1])
|
||||
});
|
||||
}
|
||||
}, this);
|
||||
|
||||
if(descending)
|
||||
callback(undefined, parsedTrades.reverse());
|
||||
else
|
||||
callback(undefined, parsedTrades);
|
||||
};
|
||||
|
||||
const reqData = {
|
||||
pair: this.pair
|
||||
};
|
||||
|
||||
if(since) {
|
||||
// Kraken wants a tid, which is found to be timestamp_ms * 1000000 in practice. No clear documentation on this though
|
||||
reqData.since = startTs * 1000000;
|
||||
}
|
||||
|
||||
const fetch = cb => this.kraken.api('Trades', reqData, this.handleResponse('getTrades', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) return callback(err);
|
||||
|
||||
let assetAmount = parseFloat( data.result[this.market.prefixed[1]] );
|
||||
let currencyAmount = parseFloat( data.result[this.market.prefixed[0]] );
|
||||
|
||||
if(!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
console.log(`Kraken did not return portfolio for ${this.asset}, assuming 0.`);
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
console.log(`Kraken did not return portfolio for ${this.currency}, assuming 0.`);
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
|
||||
return callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.kraken.api('Balance', {}, this.handleResponse('getPortfolio', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
// This assumes that only limit orders are being placed with standard assets pairs
|
||||
// It does not take into account volume discounts.
|
||||
// Base maker fee is 0.16%, taker fee is 0.26%.
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
const makerFee = 0.16;
|
||||
callback(undefined, makerFee / 100);
|
||||
};
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
const result = data.result[this.pair];
|
||||
const ticker = {
|
||||
ask: result.a[0],
|
||||
bid: result.b[0]
|
||||
};
|
||||
callback(undefined, ticker);
|
||||
};
|
||||
|
||||
const reqData = {pair: this.pair}
|
||||
const fetch = cb => this.kraken.api('Ticker', reqData, this.handleResponse('getTicker', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return _.floor(amount, this.market.amountPrecision);
|
||||
};
|
||||
|
||||
Trader.prototype.roundPrice = function(amount) {
|
||||
return scientificToDecimal(_.round(amount, this.market.pricePrecision));
|
||||
};
|
||||
|
||||
Trader.prototype.addOrder = function(tradeType, amount, price, callback) {
|
||||
price = this.roundPrice(price); // only round price, not amount
|
||||
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let txid;
|
||||
|
||||
if(data.catched) {
|
||||
// handled timeout, but order was created
|
||||
txid = data.id;
|
||||
} else if(_.isString(data)) {
|
||||
// handled timeout, order was NOT created
|
||||
txid = data;
|
||||
} else {
|
||||
// normal flow
|
||||
txid = data.result.txid[0];
|
||||
}
|
||||
|
||||
callback(undefined, txid);
|
||||
};
|
||||
|
||||
const reqData = {
|
||||
pair: this.pair,
|
||||
type: tradeType.toLowerCase(),
|
||||
ordertype: 'limit',
|
||||
price: price,
|
||||
volume: amount
|
||||
};
|
||||
|
||||
const fetch = cb => this.kraken.api('AddOrder', reqData, this.handleResponse('addOrder', cb, false, { tradeType, amount, price }));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.addOrder('buy', amount, price, callback);
|
||||
};
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.addOrder('sell', amount, price, callback);
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) return callback(err);
|
||||
|
||||
const price = parseFloat( data.result[ order ].price );
|
||||
const amount = parseFloat( data.result[ order ].vol_exec );
|
||||
const date = moment.unix( data.result[ order ].closetm );
|
||||
|
||||
// TODO: figure out fees, kraken is reporting 0 fees:
|
||||
// { 'OF6L2D-6LIKD-4OOHR7':
|
||||
// { refid: null,
|
||||
// userref: 0,
|
||||
// status: 'closed',
|
||||
// reason: null,
|
||||
// opentm: 1530694339.8116,
|
||||
// closetm: 1530694402.9572,
|
||||
// starttm: 0,
|
||||
// expiretm: 0,
|
||||
// descr: [Object],
|
||||
// vol: '0.00500000',
|
||||
// vol_exec: '0.00500000',
|
||||
// cost: '27.9',
|
||||
// fee: '0',
|
||||
// price: '5595.0',
|
||||
// stopprice: '0.00000',
|
||||
// limitprice: '0.00000',
|
||||
// misc: '',
|
||||
// oflags: 'fciq' } } }
|
||||
|
||||
callback(undefined, {
|
||||
price,
|
||||
amount,
|
||||
date,
|
||||
feePercent: 0.16 // default for now
|
||||
});
|
||||
};
|
||||
|
||||
const reqData = {txid: order};
|
||||
|
||||
const fetch = cb => this.kraken.api('QueryOrders', reqData, this.handleResponse('getOrder', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) return callback(err);
|
||||
|
||||
const result = data.result[order];
|
||||
|
||||
callback(undefined, {
|
||||
executed: result.vol === result.vol_exec,
|
||||
open: result.status === 'open',
|
||||
filledAmount: +result.vol_exec
|
||||
});
|
||||
};
|
||||
|
||||
const reqData = {txid: order};
|
||||
|
||||
const fetch = cb => this.kraken.api('QueryOrders', reqData, this.handleResponse('checkOrder', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
const reqData = {txid: order};
|
||||
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
if(err.message.includes('Unknown order')) {
|
||||
return callback(undefined, true);
|
||||
}
|
||||
|
||||
// catch race condition:
|
||||
// if we cancel, that request times out
|
||||
// we recheck: it's live BUT then the
|
||||
// timeout request goes through nonetheless
|
||||
// (only times out behind cloudflare)
|
||||
if(err.message.includes('Invalid order')) {
|
||||
return callback(undefined, true);
|
||||
}
|
||||
|
||||
return callback(err)
|
||||
}
|
||||
|
||||
if(data.filled) {
|
||||
callback(undefined, false, data);
|
||||
}
|
||||
|
||||
callback(undefined, false);
|
||||
}
|
||||
|
||||
const fetch = cb => this.kraken.api('CancelOrder', reqData, this.handleResponse('cancelOrder', cb, false, order));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getRawOpenOrders = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err)
|
||||
}
|
||||
|
||||
callback(undefined, data.result.open);
|
||||
}
|
||||
|
||||
const fetch = cb => this.kraken.api('OpenOrders', {}, this.handleResponse('getOpenOrders', cb, true));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getOpenOrders = function(callback) {
|
||||
|
||||
this.getRawOpenOrders((err, allOrders) => {
|
||||
if(err) {
|
||||
console.log(err);
|
||||
|
||||
return callback(err)
|
||||
}
|
||||
|
||||
const orders = [];
|
||||
|
||||
_.each(allOrders, (o, id) => {
|
||||
if(o.descr.pair === this.pair) {
|
||||
orders.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
callback(undefined, orders);
|
||||
});
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Kraken',
|
||||
slug: 'kraken',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
tid: 'date',
|
||||
tradable: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
var Lakebtc = require("lakebtc_nodejs");
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
}
|
||||
this.name = 'LakeBTC';
|
||||
this.balance;
|
||||
this.price;
|
||||
|
||||
this.lakebtc = new Lakebtc(this.key, this.secret);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var set = function(err, data) {
|
||||
var portfolio = [];
|
||||
_.map(data.balance, function(amount, asset) {
|
||||
portfolio.push({name: asset, amount: parseFloat(amount)});
|
||||
});
|
||||
callback(err, portfolio);
|
||||
}
|
||||
this.lakebtc.getAccountInfo(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
this.lakebtc.ticker(callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
callback(false, 0.002);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
var set = function(err, result) {
|
||||
if(err || result.error)
|
||||
return log.error('unable to buy:', err, result);
|
||||
|
||||
callback(null, result.id);
|
||||
};
|
||||
|
||||
// TODO: fees are hardcoded here?
|
||||
amount *= 0.998; // remove fees
|
||||
// prevent: Ensure that there are no more than 4 digits in total.
|
||||
amount *= 10000;
|
||||
amount = Math.floor(amount);
|
||||
amount /= 10000;
|
||||
this.lakebtc.buyOrder(_.bind(set, this), [price, amount, 'USD']);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
var set = function(err, result) {
|
||||
if(err || result.error)
|
||||
return log.error('unable to sell:', err, result);
|
||||
|
||||
callback(null, result.id);
|
||||
};
|
||||
|
||||
this.lakebtc.sell(_.bind(set, this), [price, amount, 'USD']);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var check = function(err, result) {
|
||||
var stillThere = _.find(result, function(o) { return o.id === order });
|
||||
callback(err, !stillThere);
|
||||
};
|
||||
|
||||
this.lakebtc.getOrders(_.bind(check, this));
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var cancel = function(err, result) {
|
||||
if(err || !result.result)
|
||||
log.error('unable to cancel order', order, '(', err, result, ')');
|
||||
};
|
||||
|
||||
this.lakebtc.cancelOrder(_.bind(cancel, this), [order]);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
callback(null, descending ? result.reverse() : result);
|
||||
};
|
||||
since = since ? since.unix() : moment().subtract(5, 'minutes').unix();
|
||||
this.lakebtc.bctrades( _.bind(process, this), since);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'LakeBTC',
|
||||
slug: 'lakebtc',
|
||||
currencies: ['USD'],
|
||||
assets: ['BTC'],
|
||||
markets: [
|
||||
{
|
||||
pair: ['USD', 'BTC'], minimalOrder: { amount: 1, unit: 'currency' }
|
||||
}
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: false,
|
||||
fetchTimespan: 60,
|
||||
tid: 'tid'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,416 @@
|
||||
const Luno = require("bitx");
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const node_util = require('util');
|
||||
const Errors = require('../exchangeErrors');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
const name = 'Luno';
|
||||
|
||||
var tradeAttempt = 0;
|
||||
|
||||
const Trader = function(config) {
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency;
|
||||
this.asset = config.asset;
|
||||
this.pair = config.asset + config.currency;
|
||||
}
|
||||
this.luno = new Luno(this.key, this.secret, {
|
||||
pair: this.pair
|
||||
});
|
||||
this.market = _.find(Trader.getCapabilities().markets, (market) => {
|
||||
return market.pair[0] === this.currency && market.pair[1] === this.asset;
|
||||
});
|
||||
this.interval = 2000;
|
||||
}
|
||||
|
||||
const log = function() {
|
||||
const message = node_util.format.apply(null, _.toArray(arguments));
|
||||
console.log(moment().format('YYYY-MM-DD HH:mm:ss') + ' (DEBUG): ' + message);
|
||||
}
|
||||
|
||||
Trader.prototype.inspect = function(obj) {
|
||||
return node_util.inspect(obj, {
|
||||
showhidden: false,
|
||||
depth: null,
|
||||
breakLength: Infinity,
|
||||
colors: true
|
||||
});
|
||||
};
|
||||
|
||||
const setPrecision = (amount, digits) => {
|
||||
let precision = 100000000;
|
||||
if (Number.isInteger(digits)) precision = Math.pow(10, digits);
|
||||
amount *= precision;
|
||||
amount = Math.floor(amount);
|
||||
amount /= precision;
|
||||
return amount;
|
||||
}
|
||||
|
||||
const round = (value, decimals = 14) => {
|
||||
return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
|
||||
}
|
||||
|
||||
const includes = (str, list) => {
|
||||
if (!_.isString(str))
|
||||
return false;
|
||||
let result = _.some(list, item => str.includes(item));
|
||||
return result;
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND'
|
||||
];
|
||||
|
||||
|
||||
const processResponse = function(funcName, callback) {
|
||||
return (error, body) => {
|
||||
/* BitX Error Codes
|
||||
Error: BitX error 400: invalid id
|
||||
Error: BitX error 401: "error":"API key not found","error_code":"ErrAPIKeyNotFound","error_action":
|
||||
Error: BitX error 429: -- API limit
|
||||
Error: BitX error 500: Something went wrong, we're looking into it.
|
||||
*/
|
||||
|
||||
if (!error && !body) {
|
||||
error = new Error('received empty response');
|
||||
}
|
||||
|
||||
if (error) {
|
||||
log(name, funcName + '() received error -->', error.message);
|
||||
|
||||
if (includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
if (includes(error.message, ['error 429'])) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 3000;
|
||||
}
|
||||
|
||||
if (includes(error.message, ['error 500'])) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 15000;
|
||||
}
|
||||
|
||||
if (includes(error.message, ['Insufficient balance'])) {
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 2000;
|
||||
tradeAttempt++;
|
||||
if (tradeAttempt >= 3) {
|
||||
error.notFatal = false;
|
||||
log(name, funcName + '() giving up after 3 failed attempts.');
|
||||
}
|
||||
}
|
||||
|
||||
return callback(error, undefined);
|
||||
}
|
||||
|
||||
return callback(undefined, body);
|
||||
}
|
||||
};
|
||||
|
||||
//------- Gekko Functions ---------//
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
// log(name, 'getTicker()');
|
||||
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
const ticker = {
|
||||
ask: data.ask,
|
||||
bid: data.bid,
|
||||
spread: round(data.ask - data.bid)
|
||||
};
|
||||
log(name, 'getTicker() -->', this.inspect(ticker));
|
||||
callback(undefined, ticker);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.getTicker(processResponse('getTicker', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
// log(name, 'getFee()');
|
||||
|
||||
if (this.pair === 'ETHXBT')
|
||||
return callback(undefined, 0.000025);
|
||||
else if (this.pair === 'XBTIDR')
|
||||
return callback(undefined, 0.00002);
|
||||
else
|
||||
return callback(undefined, 0.0001);
|
||||
|
||||
/*
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
// log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
// log(name, 'getFee() --> fee:', data.taker_fee / 100);
|
||||
callback(undefined, data.taker_fee / 100);
|
||||
};
|
||||
const handler = cb => this.luno.getFee(processResponse('getFee', cb));
|
||||
retry(null, handler, process);
|
||||
*/
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
// log(name, 'getPortfolio()');
|
||||
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
const assetProfile = _.find(data.balance, a => a.asset === this.asset);
|
||||
const currencyProfile = _.find(data.balance, a => a.asset === this.currency);
|
||||
let assetAmount = round(assetProfile.balance - assetProfile.reserved);
|
||||
let currencyAmount = round(currencyProfile.balance - currencyProfile.reserved);
|
||||
|
||||
if (!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if (!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
const portfolio = [{
|
||||
name: this.asset.toUpperCase(),
|
||||
amount: assetAmount
|
||||
},
|
||||
{
|
||||
name: this.currency.toUpperCase(),
|
||||
amount: currencyAmount
|
||||
}
|
||||
];
|
||||
log(name, 'getPortfolio() --> ' + this.inspect(portfolio));
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.getBalance(processResponse('getPortfolio', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
log(name, 'buy() amount:', amount, 'price:', price);
|
||||
|
||||
tradeAttempt = 0;
|
||||
amount = round(amount);
|
||||
price = round(price);
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'unable to buy:', err.message);
|
||||
return callback(err);
|
||||
}
|
||||
log(name, 'buy() order id: -->', data.order_id);
|
||||
callback(undefined, data.order_id);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.postBuyOrder(amount, price, processResponse('buy', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
log(name, 'sell() amount:', amount, 'price:', price);
|
||||
|
||||
tradeAttempt = 0;
|
||||
amount = round(amount);
|
||||
price = round(price);
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'unable to sell:', err.message);
|
||||
return callback(err);
|
||||
}
|
||||
log(name, 'sell() order id: -->', data.order_id);
|
||||
callback(undefined, data.order_id);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.postSellOrder(amount, price, processResponse('sell', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
amount = setPrecision(amount, this.market.precision);
|
||||
return amount;
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return +price;
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
// log(name, 'getOrder() order id:', order);
|
||||
|
||||
if (!order) {
|
||||
return callback('invalid order_id', false);
|
||||
}
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
const price = parseFloat(data.limit_price);
|
||||
const amount = parseFloat(data.base);
|
||||
let date = moment();
|
||||
const fees = {
|
||||
[this.asset]: +data.fee_base,
|
||||
[this.currency]: +data.fee_counter
|
||||
};
|
||||
const feePercent = round(data.fee_base / data.base * 100, 2);
|
||||
if (data.state === 'PENDING') {
|
||||
date = moment(data.creation_timestamp);
|
||||
} else {
|
||||
date = moment(data.completed_timestamp);
|
||||
}
|
||||
const result = {
|
||||
price,
|
||||
amount,
|
||||
date,
|
||||
fees,
|
||||
feePercent
|
||||
};
|
||||
log(name, 'getOrder() -->', this.inspect(result));
|
||||
callback(undefined, result);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.getOrder(order, processResponse('getOrder', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
// log(name, 'checkOrder() order id:', order);
|
||||
|
||||
if (!order) {
|
||||
return callback('invalid order_id');
|
||||
}
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
const result = {
|
||||
open: data.state === 'PENDING',
|
||||
executed: data.limit_volume === data.base,
|
||||
filledAmount: +data.base,
|
||||
remaining: round(+data.limit_volume - +data.base)
|
||||
}
|
||||
log(name, 'checkOrder()', order, 'result:', this.inspect(result));
|
||||
callback(undefined, result);
|
||||
};
|
||||
|
||||
const handler = cb => this.luno.getOrder(order, processResponse('checkOrder', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
log(name, 'cancelOrder() order id:', order);
|
||||
|
||||
if (!order) {
|
||||
return callback('invalid order_id');
|
||||
}
|
||||
const process = (err, data) => {
|
||||
if (err) {
|
||||
if (_.includes(err.message, 'Cannot stop unknown')) {
|
||||
log(name, 'unable to cancel order:', order, '(' + err.message + ') assuming success...');
|
||||
} else {
|
||||
log(name, 'unable to cancel order:', order, '(' + err.message + ') aborting...');
|
||||
return callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (data && !data.success) {
|
||||
log('cancelOrder() --> status:', data.success);
|
||||
return callback(undefined, false);
|
||||
}
|
||||
|
||||
this.checkOrder(order, (error, orderStatus) => {
|
||||
if (error) {
|
||||
log(name, 'cancelOrder\'s checkOrder failed. What do i do here?');
|
||||
return callback(error, false);
|
||||
}
|
||||
|
||||
if (orderStatus.executed) {
|
||||
log(name, 'cancelOrder() -->', order, 'was fulfilled before cancelOrder was completed.');
|
||||
return callback(undefined, true);
|
||||
}
|
||||
const remaining = {
|
||||
remaining: orderStatus.remaining,
|
||||
filled: orderStatus.filledAmount
|
||||
}
|
||||
log(name, 'cancelOrder() --> status: false remaining:', this.inspect(remaining));
|
||||
return callback(undefined, false, remaining);
|
||||
});
|
||||
}
|
||||
|
||||
const handler = cb => this.luno.stopOrder(order, processResponse('cancelOrder', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
// log(name, 'getTrades() since:', since);
|
||||
|
||||
const process = (err, result) => {
|
||||
if (err) {
|
||||
log(name, 'Error: -->', err);
|
||||
return callback(err);
|
||||
}
|
||||
let trades = _.map(result.trades, (t) => {
|
||||
return {
|
||||
price: t.price,
|
||||
date: Math.round(t.timestamp / 1000),
|
||||
amount: t.volume,
|
||||
tid: t.timestamp
|
||||
}
|
||||
});
|
||||
if (!descending) {
|
||||
trades = trades.reverse()
|
||||
}
|
||||
callback(undefined, trades);
|
||||
};
|
||||
|
||||
if (moment.isMoment(since)) since = since.valueOf();
|
||||
(_.isNumber(since) && since > 0) ? since: since = 0;
|
||||
|
||||
const options = {
|
||||
pair: this.pair,
|
||||
since: since
|
||||
}
|
||||
|
||||
const handler = cb => this.luno.getTrades(options, processResponse('getTrades', cb));
|
||||
retry(null, handler, process);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function() {
|
||||
return {
|
||||
name: 'Luno',
|
||||
slug: 'luno',
|
||||
currencies: ['MYR', 'KES', 'NGN', 'ZAR', 'XBT'],
|
||||
assets: ['ETH', 'XBT'],
|
||||
markets: [
|
||||
{ pair: ['XBT', 'ETH'], minimalOrder: { amount: 0.01, unit: 'asset' }, precision: 2 },
|
||||
{ pair: ['MYR', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
|
||||
{ pair: ['KES', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
|
||||
{ pair: ['NGN', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
|
||||
{ pair: ['ZAR', 'XBT'], minimalOrder: { amount: 0.0005, unit: 'asset' }, precision: 6 },
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
providesFullHistory: true,
|
||||
providesHistory: 'date',
|
||||
// maxHistoryFetch: 100,
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
forceReorderDelay: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,154 @@
|
||||
var Mexbt = require("mexbt");
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.userId = config.username;
|
||||
this.pair = config.asset || this.currency;
|
||||
}
|
||||
this.name = 'meXBT';
|
||||
this.mexbt = new Mexbt(this.key, this.secret, this.userId);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var set = function(err, data) {
|
||||
var portfolio = [];
|
||||
_.each(data.currencies, function(balanceInfo) {
|
||||
portfolio.push({name: balanceInfo.name, amount: balanceInfo.balance});
|
||||
});
|
||||
callback(err, portfolio);
|
||||
}
|
||||
this.mexbt.accountBalance(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
this.mexbt.ticker(callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var set = function(err, data) {
|
||||
if(err)
|
||||
callback(err);
|
||||
|
||||
callback(false, data.fee);
|
||||
}
|
||||
this.mexbt.getTradingFee({amount: 1, type: 'limit'}, _.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
var set = function(err, result) {
|
||||
if(err || result.error)
|
||||
return log.error('unable to buy:', err, result);
|
||||
|
||||
callback(null, result.serverOrderId);
|
||||
};
|
||||
|
||||
this.mexbt.createOrder({amount: amount, price: price, side: 'buy', type: 'limit'}, _.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
var set = function(err, result) {
|
||||
if(err || result.error)
|
||||
return log.error('unable to sell:', err, result);
|
||||
|
||||
callback(null, result.serverOrderId);
|
||||
};
|
||||
|
||||
this.mexbt.createOrder({amount: amount, price: price, side: 'sell', type: 'limit'}, _.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var currentPair = this.pair;
|
||||
|
||||
var check = function(err, result) {
|
||||
var ordersForPair = _.find(result, function(o) { return o.ins === currentPair});
|
||||
var stillThere = _.find(ordersForPair.openOrders, function(o) { return o.ServerOrderId === order });
|
||||
callback(err, !stillThere);
|
||||
};
|
||||
|
||||
this.mexbt.accountOrders(_.bind(check, this));
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var cancel = function(err, result) {
|
||||
if(err || !result)
|
||||
log.error('unable to cancel order', order, '(', err, result, ')');
|
||||
};
|
||||
|
||||
this.mexbt.cancelOrder({id: order}, _.bind(cancel, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
trades = _.map(result.trades, function (t) {
|
||||
return {tid: t.tid, price: t.px, date: t.unixtime, amount: t.qty};
|
||||
});
|
||||
if (descending) {
|
||||
trades = trades.reverse()
|
||||
}
|
||||
callback(null, trades);
|
||||
}.bind(this);
|
||||
|
||||
var endDate = moment().unix();
|
||||
|
||||
// FIXME: there is a bug in meXBT tradesByDate function, that it doesnt return all data
|
||||
// when trying to fetch all.
|
||||
// So if no since, we just fetch all via trades and giving a high count
|
||||
if (since) {
|
||||
this.mexbt.tradesByDate({startDate: since.unix(), endDate: endDate}, process);
|
||||
} else {
|
||||
// improvised
|
||||
this.mexbt.trades({count: 1000}, process);
|
||||
}
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'meXBT',
|
||||
slug: 'mexbt',
|
||||
currencies: ['MXN'],
|
||||
assets: ['BTC'],
|
||||
markets: [
|
||||
{
|
||||
pair: ['MXN', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' }
|
||||
}
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
providesHistory: 'date',
|
||||
tid: 'tid'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,214 @@
|
||||
// HERE FOR HISTORICAL PURPOSES
|
||||
|
||||
var MtGoxClient = require("mtgox-apiv2");
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var util = require('../core/util.js');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency || 'USD';
|
||||
|
||||
this.pair = 'BTC' + this.currency;
|
||||
}
|
||||
this.name = 'Mt. Gox';
|
||||
|
||||
_.bindAll(this);
|
||||
|
||||
this.mtgox = new MtGoxClient(this.key, this.secret, this.pair);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
var process = function(err, result) {
|
||||
this.checkUnauthorized(err);
|
||||
// if Mt. Gox is down or lagging
|
||||
if(err || result.result === 'error')
|
||||
log.error('unable to buy (', err, result, ')');
|
||||
|
||||
callback(null, result.data);
|
||||
};
|
||||
this.mtgox.add('bid', amount, price, _.bind(process, this));
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
var process = function(err, result) {
|
||||
this.checkUnauthorized(err);
|
||||
// if Mt. Gox is down or lagging
|
||||
if(err || result.result === 'error')
|
||||
log.error('unable to sell (', err, result, ')');
|
||||
|
||||
callback(null, result.data);
|
||||
};
|
||||
this.mtgox.add('ask', amount, price, _.bind(process, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
if(since && !_.isNumber(since))
|
||||
since = util.toMicro(since);
|
||||
|
||||
var args = _.toArray(arguments);
|
||||
this.mtgox.fetchTrades(since, _.bind(function(err, trades) {
|
||||
if (err || !trades)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
trades = trades.data;
|
||||
if (trades.length === 0)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
if(descending)
|
||||
callback(false, trades.reverse());
|
||||
else
|
||||
callback(false, trades);
|
||||
}, this));
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait.
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.checkUnauthorized = function(err) {
|
||||
if(err && err.message === 'Request failed with 403')
|
||||
util.die('It appears your ' + this.name + ' API key and secret are incorrect');
|
||||
}
|
||||
|
||||
// calls callback with the following data structure:
|
||||
//
|
||||
// [
|
||||
// { name: 'BTC', amount: 10.12413123},
|
||||
// { name: 'USD', amount: 500.0241},
|
||||
// { name: 'EUR', amount: 0},
|
||||
// ]
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var calculate = function(err, result) {
|
||||
this.checkUnauthorized(err);
|
||||
if(err)
|
||||
return this.retry(this.getPortfolio, args);
|
||||
|
||||
if(!('Wallets' in result.data))
|
||||
log.error('unable to get portfolio, do I have get_info rights?');
|
||||
|
||||
var assets = [];
|
||||
_.each(result.data.Wallets, function(wallet, name) {
|
||||
var amount = parseFloat(wallet.Balance.value);
|
||||
assets.push({name: name, amount: amount});
|
||||
});
|
||||
callback(null, assets);
|
||||
};
|
||||
|
||||
this.mtgox.info(_.bind(calculate, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var calculate = function(err, result) {
|
||||
this.checkUnauthorized(err);
|
||||
if(err)
|
||||
return this.retry(this.getFee, args);
|
||||
|
||||
// convert the %
|
||||
var fee = result.data.Trade_Fee / 100;
|
||||
callback(null, fee);
|
||||
};
|
||||
|
||||
this.mtgox.info(_.bind(calculate, this));
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
// we can't just request the order id
|
||||
// @link https://bitbucket.org/nitrous/mtgox-api/#markdown-header-moneyorderresult
|
||||
var check = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.checkOrder, args);
|
||||
|
||||
var stillThere = _.find(result.data, function(o) { return o.oid === order });
|
||||
callback(null, !stillThere);
|
||||
};
|
||||
|
||||
this.mtgox.orders(_.bind(check, this));
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order) {
|
||||
var cancel = function(err, result) {
|
||||
if(err || result.result !== 'succes')
|
||||
log.error('unable to cancel order', order, '(', err, result, ')');
|
||||
};
|
||||
|
||||
this.mtgox.cancel(order, _.bind(cancel, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return this.retry(this.getTicker, args);
|
||||
|
||||
var ticker = {
|
||||
bid: result.data.buy.value,
|
||||
ask: result.data.sell.value
|
||||
}
|
||||
callback(err, ticker);
|
||||
};
|
||||
|
||||
this.mtgox.ticker(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {};
|
||||
// ---- Keeping this here for historical purposes. ----
|
||||
// {
|
||||
//
|
||||
// name: 'MtGox',
|
||||
// slug: 'mtgox',
|
||||
// direct: true,
|
||||
// infinityOrder: true,
|
||||
// currencies: [
|
||||
// 'USD', 'EUR', 'GBP', 'AUD', 'CAD', 'CHF', 'CNY',
|
||||
// 'DKK', 'HKD', 'PLN', 'RUB', 'SGD', 'THB'
|
||||
// ],
|
||||
// assets: ['BTC'],
|
||||
// markets: [
|
||||
// { pair: ['USD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['EUR', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['GBP', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['AUD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['CAD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['CHF', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['CNY', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['DKK', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['HKD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['PLN', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['RUB', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['SGD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
// { pair: ['THB', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } }
|
||||
// ],
|
||||
// requires: ['key', 'secret'],
|
||||
// providesHistory: false
|
||||
// }
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
var OKCoin = require('okcoin-china');
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
}
|
||||
this.pair = [config.asset, config.currency].join('_').toLowerCase();
|
||||
this.name = 'okcoin';
|
||||
this.okcoin = new OKCoin(this.key, this.secret);
|
||||
this.lastTid = false;
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if (_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() {
|
||||
method.apply(self, args)
|
||||
},
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var calculate = function(err, data) {
|
||||
if(err) {
|
||||
if(err.message === 'invalid api key')
|
||||
util.die('Your ' + this.name + ' API keys are invalid');
|
||||
return this.retry(this.okcoin.getUserInfo, calculate);
|
||||
}
|
||||
|
||||
var portfolio = [];
|
||||
_.each(data.info.funds.free, function(amount, asset) {
|
||||
portfolio.push({name: asset.toUpperCase(), amount: +amount});
|
||||
});
|
||||
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
|
||||
this.okcoin.getUserInfo(calculate);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var args = [this.pair, process];
|
||||
var process = function(err, data) {
|
||||
if (err)
|
||||
return this.retry(this.okcoin.getTicker(args));
|
||||
|
||||
var ticker = _.extend(data.ticker, {
|
||||
bid: +data.ticker.sell,
|
||||
ask: +data.ticker.buy
|
||||
});
|
||||
|
||||
callback(err, ticker);
|
||||
}.bind(this);
|
||||
|
||||
this.okcoin.getTicker(process, args);
|
||||
}
|
||||
|
||||
// This assumes that only limit orders are being placed, so fees are the
|
||||
// "maker fee" of 0.1%. It does not take into account volume discounts.
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var makerFee = 0.1;
|
||||
callback(false, makerFee / 100);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(raw_amount, price, callback) {
|
||||
var amount = Math.floor(raw_amount * 10000) / 10000
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return log.error('unable to process order:', err, result);
|
||||
|
||||
callback(null, result.order_id);
|
||||
}.bind(this);
|
||||
|
||||
this.okcoin.addTrade(set, this.pair, 'buy', amount, price);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(raw_amount, price, callback) {
|
||||
var amount = Math.floor(raw_amount * 10000) / 10000
|
||||
var set = function(err, result) {
|
||||
if(err)
|
||||
return log.error('unable to process order:', err, result);
|
||||
|
||||
callback(null, result.order_id);
|
||||
}.bind(this);
|
||||
|
||||
this.okcoin.addTrade(set, this.pair, 'sell', amount, price);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.checkOrder = function(order_id, callback) {
|
||||
var check = function(err, result) {
|
||||
if(err || !result.result) {
|
||||
log.error('Perhaps the order already got filled?', '(', result, ')');
|
||||
callback(err, !result.result);
|
||||
} else {
|
||||
callback(err, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.okcoin.getOrderInfo(check, this.pair, order_id);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order_id, callback) {
|
||||
var cancel = function(err, result) {
|
||||
if(err || !result.result) {
|
||||
return log.error('unable to cancel order ', order_id, '(', result, ')');
|
||||
}
|
||||
|
||||
callback();
|
||||
}.bind(this);
|
||||
|
||||
this.okcoin.cancelOrder(cancel, this.pair, order_id);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order_id, callback) {
|
||||
|
||||
var handle = (err, resp) => {
|
||||
var order = _.first(resp.orders);
|
||||
|
||||
var amount = order.deal_amount;
|
||||
var price = order.price;
|
||||
var date = moment(order.date).utc();
|
||||
|
||||
callback(undefined, {amount, price, date});
|
||||
}
|
||||
|
||||
this.okcoin.getOrderInfo(handle, this.pair, order_id)
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
if(since)
|
||||
since = 600;
|
||||
|
||||
this.okcoin.getTrades(function(err, data) {
|
||||
if (err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
var trades = _.map(data, function(trade) {
|
||||
return {
|
||||
price: +trade.price,
|
||||
amount: +trade.amount,
|
||||
tid: +trade.tid,
|
||||
date: trade.date
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, trades.reverse());
|
||||
}.bind(this), this.pair, since);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'OkCoin',
|
||||
slug: 'okcoin',
|
||||
currencies: ['BTC', 'CNY'],
|
||||
assets: ['BTC', 'LTC'],
|
||||
markets: [
|
||||
{ pair: ['CNY', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['CNY', 'LTC'], minimalOrder: { amount: 0.01, unit: 'asset' } }
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
providesHistory: false,
|
||||
fetchTimespan: 60,
|
||||
tid: 'date',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,533 @@
|
||||
const Poloniex = require("gekko-broker-poloniex");
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
const marketData = require('./poloniex-markets.json');
|
||||
|
||||
const Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.currency = config.currency;
|
||||
this.asset = config.asset;
|
||||
}
|
||||
this.name = 'Poloniex';
|
||||
this.balance;
|
||||
this.price;
|
||||
|
||||
this.pair = this.currency + '_' + this.asset;
|
||||
|
||||
this.fillDataOnCancel = true;
|
||||
|
||||
this.poloniex = new Poloniex({
|
||||
key: this.key,
|
||||
secret: this.secret,
|
||||
userAgent: 'Gekko Broker v' + require('../package.json').version
|
||||
});
|
||||
}
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'429',
|
||||
'522',
|
||||
'504',
|
||||
'503',
|
||||
'500',
|
||||
'502',
|
||||
'socket hang up',
|
||||
'Empty response',
|
||||
'Please try again in a few minutes.',
|
||||
'Nonce must be greater than',
|
||||
'Internal error. Please try again.',
|
||||
'Connection timed out. Please try again.',
|
||||
// getaddrinfo EAI_AGAIN poloniex.com poloniex.com:443
|
||||
'EAI_AGAIN',
|
||||
'ENETUNREACH',
|
||||
'socket hang up'
|
||||
];
|
||||
|
||||
// errors that might mean
|
||||
// the API call succeeded.
|
||||
const unknownResultErrors = [
|
||||
'ETIMEDOUT',
|
||||
]
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.outbidPrice = function(price, isUp) {
|
||||
let newPrice;
|
||||
|
||||
const tickSize = 0.00000001;
|
||||
|
||||
if(isUp) {
|
||||
newPrice = price + 0.00000001;
|
||||
} else {
|
||||
newPrice = price - tickSize;
|
||||
}
|
||||
|
||||
return this.roundPrice(newPrice);
|
||||
}
|
||||
|
||||
Trader.prototype.processResponse = function(next, fn, payload) {
|
||||
// TODO: in very rare cases the callback is
|
||||
// called twice (first on ETIMEDOUT later
|
||||
// without error). Temp workaround.
|
||||
next = _.once(next);
|
||||
|
||||
return (err, data) => {
|
||||
let error;
|
||||
|
||||
if(err) {
|
||||
if(err.message) {
|
||||
error = err;
|
||||
} else {
|
||||
console.log('err but no message,', typeof err, {err, data});
|
||||
error = new Error(err);
|
||||
}
|
||||
} else if(!data) {
|
||||
error = new Error('Empty response');
|
||||
} else if(data.error) {
|
||||
error = new Error(data.error);
|
||||
} else if(includes(data, ['Please complete the security check to proceed.'])) {
|
||||
error = new Error(
|
||||
'Your IP has been flagged by CloudFlare. ' +
|
||||
'As such Gekko Broker cannot access Poloniex.'
|
||||
);
|
||||
data = undefined;
|
||||
} else if(includes(data, ['Please try again in a few minutes.'])) {
|
||||
error = new Error('Please try again in a few minutes.');
|
||||
error.notFatal = true;
|
||||
data = undefined;
|
||||
} else if(includes(data, ['<!DOCTYPE html>'])) {
|
||||
error = new Error(data);
|
||||
data = undefined;
|
||||
}
|
||||
|
||||
if(error) {
|
||||
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
if(includes(error.message, ['Currently in maintenance mode.'])) {
|
||||
console.log(new Date, '[Poloniex] Currently in maintenance mode. Retrying...');
|
||||
error.notFatal = true;
|
||||
error.backoffDelay = 1000;
|
||||
}
|
||||
|
||||
// not actually an error, means order never executed against other trades
|
||||
if(fn === 'getOrder' &&
|
||||
error.message.includes('Order not found, or you are not the person who placed it.')
|
||||
) {
|
||||
error = undefined;
|
||||
data = { unfilled: true };
|
||||
console.log(new Date, 'UNKNOWN ORDER!', payload);
|
||||
}
|
||||
|
||||
if(fn === 'cancelOrder') {
|
||||
|
||||
// already filled
|
||||
if(includes(error.message, ['Invalid order number, or you are not the person who placed the order.'])) {
|
||||
console.log(new Date, 'cancelOrder invalid order');
|
||||
error = undefined;
|
||||
data = { filled: true };
|
||||
}
|
||||
|
||||
// it might be cancelled
|
||||
else if(includes(error.message, unknownResultErrors)) {
|
||||
setTimeout(() => {
|
||||
this.getRawOpenOrders((err, orders) => {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const order = _.find(orders, o => o.orderNumber == payload);
|
||||
|
||||
// the cancel did not work since the order still exists
|
||||
if(order) {
|
||||
error.notFatal = true;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
// it was cancelled, we need to check filled amount..
|
||||
console.log(new Date, '[CANCELFIX] process cancel response');
|
||||
console.log('[CANCELFIX] rechecking fill')
|
||||
setTimeout(() => {
|
||||
this.getOrder(payload, (error, order) => {
|
||||
if(error) {
|
||||
return next(error)
|
||||
}
|
||||
|
||||
console.log('[CANCELFIX] checked, got:', order);
|
||||
|
||||
return next(undefined, { filled: order.amount });
|
||||
});
|
||||
}, this.checkInterval);
|
||||
});
|
||||
|
||||
}, this.checkInterval);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(fn === 'order') {
|
||||
if(includes(error.message, ['Not enough'])) {
|
||||
error.retry = 2;
|
||||
}
|
||||
|
||||
// we need to check whether the order was actually created
|
||||
if(includes(error.message, unknownResultErrors)) {
|
||||
return setTimeout(() => {
|
||||
this.findLastOrder(10, payload, (err, lastTrade) => {
|
||||
if(lastTrade) {
|
||||
return next(undefined, lastTrade);
|
||||
}
|
||||
|
||||
next(error);
|
||||
});
|
||||
|
||||
}, this.checkInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next(error, data);
|
||||
}
|
||||
}
|
||||
|
||||
Trader.prototype.findLastOrder = function(since, side, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
result = result.filter(t => t.type === side);
|
||||
|
||||
if(!result.length) {
|
||||
return callback(undefined, undefined);
|
||||
}
|
||||
|
||||
let order;
|
||||
if(since) {
|
||||
const threshold = moment().subtract(since, 'm');
|
||||
order = _.find(result, o => moment.utc(o.date) > threshold);
|
||||
} else {
|
||||
order = _.last(result);
|
||||
}
|
||||
|
||||
callback(undefined, order);
|
||||
};
|
||||
|
||||
this.getRawOpenOrders(handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getRawOpenOrders = function(callback) {
|
||||
const fetch = next => this.poloniex.returnOpenOrders(this.currency, this.asset, this.processResponse(next));
|
||||
retry(null, fetch, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getOpenOrders = function(callback) {
|
||||
this.getRawOpenOrders((err, orders) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
console.log(orders);
|
||||
|
||||
const ids = orders.map(o => o.orderNumber);
|
||||
|
||||
return callback(undefined, ids);
|
||||
})
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var assetAmount = parseFloat( data[this.asset] );
|
||||
var currencyAmount = parseFloat( data[this.currency] );
|
||||
|
||||
if(
|
||||
!_.isNumber(assetAmount) || _.isNaN(assetAmount) ||
|
||||
!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)
|
||||
) {
|
||||
assetAmount = 0;
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
var portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
|
||||
callback(undefined, portfolio);
|
||||
}
|
||||
|
||||
const fetch = next => this.poloniex.myBalances(this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getFullPortfolio = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const asset = data[this.asset];
|
||||
const currency = data[this.currency];
|
||||
|
||||
let assetAmount = parseFloat(asset.available) + parseFloat(asset.onOrders);
|
||||
let currencyAmount = parseFloat(currency.available) + parseFloat(asset.onOrders);
|
||||
|
||||
if(!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
var portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
|
||||
callback(undefined, portfolio);
|
||||
}
|
||||
|
||||
const fetch = next => this.poloniex.returnCompleteBalances(this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
var tick = data[this.pair];
|
||||
|
||||
callback(null, {
|
||||
bid: parseFloat(tick.highestBid),
|
||||
ask: parseFloat(tick.lowestAsk),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const fetch = next => this.poloniex.getTicker(this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
const handle = (err, data) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
callback(undefined, parseFloat(data.makerFee));
|
||||
}
|
||||
|
||||
const fetch = next => this.poloniex._private('returnFeeInfo', this.processResponse(next));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return _.floor(amount, 8);
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return +price;
|
||||
}
|
||||
|
||||
Trader.prototype.isValidLot = function(price, amount) {
|
||||
// Error: Total must be at least 0.0001.
|
||||
return amount * price >= 0.0001;
|
||||
}
|
||||
|
||||
Trader.prototype.createOrder = function(side, amount, price, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err) {
|
||||
console.log('createOrder', {side, amount, price});
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// console.log(new Date, 'PORDER created', result.orderNumber);
|
||||
|
||||
callback(undefined, result.orderNumber);
|
||||
}
|
||||
|
||||
const fetch = next => {
|
||||
this.poloniex[side](this.currency, this.asset, price, amount, this.processResponse(next, 'order', side))
|
||||
};
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.createOrder('buy', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.createOrder('sell', amount, price, callback);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(id, callback) {
|
||||
const handle = (err, result) => {
|
||||
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if(result.completed) {
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
}
|
||||
|
||||
const order = _.find(result, function(o) { return o.orderNumber === id });
|
||||
if(!order) {
|
||||
console.log(new Date, 'order not open', id, result);
|
||||
// if the order is not open it's fully executed
|
||||
return callback(undefined, { executed: true, open: false });
|
||||
}
|
||||
|
||||
callback(undefined, { executed: false, open: true, filledAmount: order.startingAmount - order.amount });
|
||||
}
|
||||
|
||||
const fetch = next => this.poloniex.myOpenOrders(this.currency, this.asset, this.processResponse(next, 'checkOrder'));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
let price = 0;
|
||||
let amount = 0;
|
||||
let date = moment(0);
|
||||
|
||||
if(result.unfilled) {
|
||||
return callback(null, {price, amount, date});
|
||||
}
|
||||
|
||||
_.each(result, trade => {
|
||||
date = moment(trade.date);
|
||||
price = ((price * amount) + (+trade.rate * trade.amount)) / (+trade.amount + amount);
|
||||
amount += +trade.amount;
|
||||
});
|
||||
|
||||
const fees = {};
|
||||
const feePercent = _.first(result).fee * 100;
|
||||
|
||||
if(_.first(result).type === 'sell') {
|
||||
const fee = price * amount * _.first(result).fee;
|
||||
fees[this.currency] = fee;
|
||||
} else {
|
||||
const fee = amount * _.first(result).fee;
|
||||
fees[this.asset] = fee;
|
||||
}
|
||||
|
||||
callback(err, {price, amount, date, fees, feePercent});
|
||||
};
|
||||
|
||||
const fetch = next => this.poloniex.returnOrderTrades(order, this.processResponse(next, 'getOrder', order));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// console.log(new Date, 'PORDER cancel', order);
|
||||
|
||||
if(result.filled) {
|
||||
console.log(new Date, 'result.filled', result);
|
||||
return callback(undefined, true);
|
||||
}
|
||||
|
||||
let data;
|
||||
|
||||
if(result.amount) {
|
||||
data = { remaining: result.amount };
|
||||
} else {
|
||||
console.log(new Date, 'not result.amount', result);
|
||||
}
|
||||
|
||||
|
||||
callback(undefined, false, data);
|
||||
};
|
||||
|
||||
const fetch = next => this.poloniex.cancelOrder(this.currency, this.asset, order, this.processResponse(next, 'cancelOrder', order));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
|
||||
const firstFetch = !!since;
|
||||
const args = _.toArray(arguments);
|
||||
|
||||
const handle = (err, result) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
// Edge case, see here:
|
||||
// @link https://github.com/askmike/gekko/issues/479
|
||||
if(firstFetch && _.size(result) === 50000)
|
||||
return callback(
|
||||
[
|
||||
'Poloniex did not provide enough data. Read this:',
|
||||
'https://github.com/askmike/gekko/issues/479'
|
||||
].join('\n\n')
|
||||
);
|
||||
|
||||
result = _.map(result, function(trade) {
|
||||
return {
|
||||
tid: trade.tradeID,
|
||||
amount: +trade.amount,
|
||||
date: moment.utc(trade.date).unix(),
|
||||
price: +trade.rate
|
||||
};
|
||||
});
|
||||
|
||||
callback(null, result.reverse());
|
||||
};
|
||||
|
||||
var params = {
|
||||
currencyPair: this.pair
|
||||
}
|
||||
|
||||
if(since)
|
||||
params.start = since.unix();
|
||||
|
||||
const fetch = next => this.poloniex._public('returnTradeHistory', params, this.processResponse(next, 'getTrades', since));
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Poloniex',
|
||||
slug: 'poloniex',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
currencyMinimums: {BTC: 0.0001, ETH: 0.0001, XMR: 0.0001, USDT: 1.0},
|
||||
requires: ['key', 'secret'],
|
||||
tid: 'tid',
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
tradable: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"assets": [
|
||||
"XBT",
|
||||
"ETH",
|
||||
"LTC",
|
||||
"BCH",
|
||||
"BTG"
|
||||
],
|
||||
"currencies": [
|
||||
"CAD",
|
||||
"USD",
|
||||
"XBT"
|
||||
],
|
||||
"markets": [
|
||||
{
|
||||
"pair": [
|
||||
"CAD",
|
||||
"XBT"
|
||||
],
|
||||
"book": "btc_cad",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"USD",
|
||||
"XBT"
|
||||
],
|
||||
"book": "btc_usd",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"CAD",
|
||||
"BCH"
|
||||
],
|
||||
"book": "bch_cad",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XBT",
|
||||
"BCH"
|
||||
],
|
||||
"book": "bch_btc",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"CAD",
|
||||
"ETH"
|
||||
],
|
||||
"book": "eth_cad",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XBT",
|
||||
"ETH"
|
||||
],
|
||||
"book": "eth_btc",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"CAD",
|
||||
"LTC"
|
||||
],
|
||||
"book": "ltc_cad",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XBT",
|
||||
"LTC"
|
||||
],
|
||||
"book": "ltc_btc",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"CAD",
|
||||
"BTG"
|
||||
],
|
||||
"book": "btg_cad",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XBT",
|
||||
"BTG"
|
||||
],
|
||||
"book": "btg_btc",
|
||||
"minimalOrder": {
|
||||
"amount": "0.0005",
|
||||
"unit": "asset"
|
||||
},
|
||||
"precision": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
var QuadrigaCX = require('quadrigacx');
|
||||
var moment = require('moment');
|
||||
var util = require('../core/util');
|
||||
var _ = require('lodash');
|
||||
var log = require('../core/log');
|
||||
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientId = config.username;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
}
|
||||
|
||||
this.pair = this.asset.toLowerCase() + '_' + this.currency.toLowerCase();
|
||||
this.name = 'quadriga';
|
||||
this.since = null;
|
||||
|
||||
this.quadriga = new QuadrigaCX(
|
||||
this.clientId ? this.clientId : "1",
|
||||
this.key ? this.key : "",
|
||||
this.secret ? this.secret : "",
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.retry = function(method, warn, args, error) {
|
||||
var wait = +moment.duration(30, 'seconds');
|
||||
if (error.code === 200) {
|
||||
log.debug(`${this.name}: API rate limit exceeded! unable to call ${method}, will retry in 2 minutes`)
|
||||
wait = +moment.duration(120, 'seconds');
|
||||
}
|
||||
else {
|
||||
log.debug(JSON.stringify(error));
|
||||
log.debug(`${this.name}: ${warn}, will retry in 30 seconds`);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, trades) {
|
||||
if (trades && trades.error) return this.retry(this.getTrades, 'unable to get trades', args, trades.error);
|
||||
if (err) return this.retry(this.getTrades, 'unable to get trades', args, err);
|
||||
|
||||
var parsedTrades = [];
|
||||
_.each(trades, function(trade) {
|
||||
parsedTrades.push({
|
||||
tid: trade.tid,
|
||||
date: trade.date,
|
||||
price: parseFloat(trade.price),
|
||||
amount: parseFloat(trade.amount)
|
||||
});
|
||||
}, this);
|
||||
|
||||
if(descending)
|
||||
callback(null, parsedTrades);
|
||||
else
|
||||
callback(null, parsedTrades.reverse());
|
||||
};
|
||||
|
||||
var reqData = {
|
||||
book: this.pair,
|
||||
time: 'hour'
|
||||
};
|
||||
|
||||
this.quadriga.api('transactions', reqData, _.bind(process, this));
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var set = function(err, data) {
|
||||
|
||||
if (data && data.error) return this.retry(this.getPortfolio, 'unable to get balance', args, data.error);
|
||||
if (err) return this.retry(this.getPortfolio, 'unable to get balance', args, err);
|
||||
|
||||
var assetAmount = parseFloat( data[this.asset.toLowerCase() + '_available'] );
|
||||
var currencyAmount = parseFloat( data[this.currency.toLowerCase() + '_available'] );
|
||||
|
||||
if(!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
log.error(`Quadriga did not return balance for ${this.asset.toLowerCase()}, assuming 0.`);
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
log.error(`Quadriga did not return balance for ${this.currency.toLowerCase()}, assuming 0.`);
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
var portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
callback(err, portfolio);
|
||||
};
|
||||
|
||||
this.quadriga.api('balance', _.bind(set, this));
|
||||
};
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
callback(false, 0.005);
|
||||
};
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
var set = function(err, data) {
|
||||
|
||||
if (data && data.error) return this.retry(this.getTicker, 'unable to get quote', args, data.error);
|
||||
if (err) return this.retry(this.getTicker, 'unable to get quote', args, err);
|
||||
|
||||
var ticker = {
|
||||
ask: data.ask,
|
||||
bid: data.bid
|
||||
};
|
||||
callback(err, ticker);
|
||||
};
|
||||
|
||||
this.quadriga.api('ticker', {book: this.pair}, _.bind(set, this));
|
||||
};
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
var precision = 100000000;
|
||||
var market = Trader.getCapabilities().markets.find(function(market){ return market.pair[0] === this.currency && market.pair[1] === this.asset });
|
||||
|
||||
if(Number.isInteger(market.precision))
|
||||
precision = 10 * market.precision;
|
||||
|
||||
amount *= precision;
|
||||
amount = Math.floor(amount);
|
||||
amount /= precision;
|
||||
return amount;
|
||||
};
|
||||
|
||||
Trader.prototype.addOrder = function(tradeType, amount, price, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
amount = this.roundAmount(amount);
|
||||
log.debug(tradeType.toUpperCase(), amount, this.asset, '@', price, this.currency);
|
||||
|
||||
var set = function(err, data) {
|
||||
|
||||
if (data && data.error) return this.retry(this.addOrder, 'unable to place order', args, data.error);
|
||||
if (err) return this.retry(this.addOrder, 'unable to place order', args, err);
|
||||
|
||||
var txid = data.id;
|
||||
log.debug('added order with txid:', txid);
|
||||
|
||||
callback(undefined, txid);
|
||||
};
|
||||
|
||||
this.quadriga.api(tradeType, {
|
||||
book: this.pair,
|
||||
price: price,
|
||||
amount: amount
|
||||
}, _.bind(set, this));
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
var get = function(err, data) {
|
||||
if (data && data.error) return this.retry(this.getOrder, 'unable to get order', args, data.error);
|
||||
if (err) return this.retry(this.getOrder, 'unable to get order', args, err);
|
||||
|
||||
var price = parseFloat( data[0].price );
|
||||
var amount = parseFloat( data[0].amount );
|
||||
var date = (data[0].updated) ? moment.unix( data[0].updated ) : false;
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.quadriga.api('lookup_oder', {id: order}, get);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
this.addOrder('buy', amount, price, callback);
|
||||
};
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
this.addOrder('sell', amount, price, callback);
|
||||
};
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
|
||||
var check = function(err, data) {
|
||||
|
||||
if (data && data.error) return this.retry(this.checkOrder, 'unable to get order', args, data.error);
|
||||
if (err) return this.retry(this.checkOrder, 'unable to get order', args, err);
|
||||
|
||||
var result = data[0];
|
||||
var stillThere = result.status === 0 || result.status === 1;
|
||||
callback(err, !stillThere);
|
||||
};
|
||||
|
||||
this.quadriga.api('lookup_order', {id: order}, _.bind(check, this));
|
||||
};
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var cancel = function(err, data) {
|
||||
|
||||
if (data && data.error) return this.retry(this.cancelOrder, 'unable to cancel order', args, data.error);
|
||||
if (err) return this.retry(this.cancelOrder, 'unable to cancel order', args, err);
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
this.quadriga.api('cancel_order', {id: order}, _.bind(cancel, this));
|
||||
};
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Quadriga',
|
||||
slug: 'quadriga',
|
||||
currencies: ['CAD', 'USD', 'BTC'],
|
||||
assets: ['BTC', 'ETH', 'LTC', 'BCH'],
|
||||
markets: [
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
{ pair: ['CAD', 'ETH'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
{ pair: ['CAD', 'BTC'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
{ pair: ['CAD', 'LTC'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
{ pair: ['CAD', 'BCH'], minimalOrder: { amount: 0.00001, unit: 'asset' }, precision: 8 },
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
providesHistory: false,
|
||||
tid: 'tid',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"assets": [
|
||||
"ETH",
|
||||
"LTC",
|
||||
"BTC",
|
||||
"BCH",
|
||||
"EUR",
|
||||
"PPC",
|
||||
"ZEC",
|
||||
"NOKU",
|
||||
"FDZ"
|
||||
],
|
||||
"currencies": [
|
||||
"BTC",
|
||||
"EUR",
|
||||
"ETH",
|
||||
"GUSD",
|
||||
"XRP"
|
||||
],
|
||||
"markets": [
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.0005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"PPC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.05,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"ZEC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"EUR",
|
||||
"GUSD"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ETH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.001,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"PPC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.05,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"LTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"BCH"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"BTC",
|
||||
"ZEC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 1e-8,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"GUSD",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.0005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XRP",
|
||||
"EUR"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.01,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pair": [
|
||||
"XRP",
|
||||
"BTC"
|
||||
],
|
||||
"minimalOrder": {
|
||||
"amount": 0.005,
|
||||
"price": 0.01,
|
||||
"order": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
const Therocktrading = require('therocktrading');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
const retry = require('../exchangeUtils').retry;
|
||||
const marketData = require("./therocktrading-markets.json");
|
||||
|
||||
const QUERY_DELAY = 350;
|
||||
|
||||
const Trader = function(config) {
|
||||
this.post_only = true;
|
||||
this.use_sandbox = false;
|
||||
this.name = 'Therocktrading';
|
||||
this.scanback = true;
|
||||
this.scanbackTid = 0;
|
||||
this.since = null;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
|
||||
this.api_url = 'https://api.therocktrading.com';
|
||||
this.api_sandbox_url = 'https://api-staging.therocktrading.com';
|
||||
|
||||
if (_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
|
||||
this.pair = [config.asset, config.currency].join('').toUpperCase();
|
||||
this.post_only =
|
||||
typeof config.post_only !== 'undefined' ? config.post_only : true;
|
||||
|
||||
if (config.sandbox) {
|
||||
this.use_sandbox = config.sandbox;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.therocktrading = new Therocktrading(this.key, this.secret, 'Gekko Broker v' + require('../package.json').version);
|
||||
};
|
||||
|
||||
const recoverableErrors = [
|
||||
'SOCKETTIMEDOUT',
|
||||
'TIMEDOUT',
|
||||
'CONNRESET',
|
||||
'CONNREFUSED',
|
||||
'NOTFOUND',
|
||||
'Rate limit exceeded',
|
||||
'Response code 5',
|
||||
'Therocktrading is currently under maintenance.',
|
||||
'HTTP 408 Error',
|
||||
'HTTP 504 Error',
|
||||
'HTTP 503 Error',
|
||||
'EHOSTUNREACH',
|
||||
'EAI_AGAIN',
|
||||
'ENETUNREACH'
|
||||
];
|
||||
|
||||
const includes = (str, list) => {
|
||||
if(!_.isString(str))
|
||||
return false;
|
||||
|
||||
return _.some(list, item => str.includes(item));
|
||||
}
|
||||
|
||||
Trader.prototype.processResponse = function(method, next) {
|
||||
return (error, body) => {
|
||||
if (body && body.code) {
|
||||
error = new Error(`Error ${body.code}: ${body.msg}`);
|
||||
}
|
||||
|
||||
if(!error && body && !_.isEmpty(body.message)) {
|
||||
error = new Error(body.message);
|
||||
}
|
||||
|
||||
/*if(
|
||||
response &&
|
||||
response.statusCode < 200 &&
|
||||
response.statusCode >= 300
|
||||
) {
|
||||
error = new Error(`Response code ${response.statusCode}`);
|
||||
}*/
|
||||
|
||||
if(error) {
|
||||
if(_.isString(error)) {
|
||||
error = new Error(error);
|
||||
}
|
||||
|
||||
if(includes(error.message, recoverableErrors)) {
|
||||
error.notFatal = true;
|
||||
}
|
||||
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return next(undefined, body);
|
||||
}
|
||||
};
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
const handle = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
var trades = [];
|
||||
if (_.isArray(data.trades)) {
|
||||
trades = _.map(data.trades, function(trade) {
|
||||
return {
|
||||
tid: trade.id,
|
||||
price: trade.price,
|
||||
amount: trade.amount,
|
||||
date: moment.utc(trade.date).format('X')
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
callback(null, descending ? trades : trades.reverse());
|
||||
};
|
||||
|
||||
if (moment.isMoment(since)) since = since.format();
|
||||
//if (moment.isMoment(to)) to = to.format();
|
||||
|
||||
var options = {
|
||||
order: "DESC" // which is default at therock
|
||||
}
|
||||
if (since)
|
||||
options.after = since;
|
||||
const fetch = cb => this.therocktrading.trades(this.pair, options, this.processResponse('getTrades', cb));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
callback(undefined, { bid: data.bid, ask: data.ask });
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.therocktrading.ticker(this.pair, this.processResponse('getTicker', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
const fee = 0.2;
|
||||
// should check for discounts ?
|
||||
callback(undefined, fee);
|
||||
};
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
var portfolio = data.balances.map(function(balance) {
|
||||
return {
|
||||
name: balance.currency.toUpperCase(),
|
||||
amount: parseFloat(balance.trading_balance),
|
||||
};
|
||||
});
|
||||
callback(undefined, portfolio);
|
||||
};
|
||||
|
||||
const fetch = cb => this.therocktrading.balances(this.processResponse('getPortfolio', cb));
|
||||
retry(undefined, fetch, result);
|
||||
};
|
||||
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(undefined, result.id);
|
||||
}
|
||||
const fetch = next => {
|
||||
this.therocktrading.buy(this.pair, amount.toString(), price.toString(), this.processResponse('order', next))
|
||||
};
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(undefined, result.id);
|
||||
}
|
||||
const fetch = next => {
|
||||
this.therocktrading.sell(this.pair, amount.toString(), price.toString(), this.processResponse('order', next))
|
||||
};
|
||||
retry(null, fetch, handle);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.getOrder = function(order, callback) {
|
||||
const handle = (err, result) => {
|
||||
if(err)
|
||||
return callback(err);
|
||||
|
||||
let price = 0;
|
||||
let amount = 0;
|
||||
let date = moment(0);
|
||||
|
||||
if(result.amount === result.amount_unfilled) {
|
||||
return callback(null, {price, amount, date});
|
||||
}
|
||||
|
||||
_.each(result.trades, trade => {
|
||||
date = moment(trade.date);
|
||||
price = ((price * amount) + (+trade.price * trade.amount)) / (+trade.amount + amount);
|
||||
amount += +trade.amount;
|
||||
});
|
||||
|
||||
const fees = {};
|
||||
const feePercent = this.makerFee;
|
||||
|
||||
const fee = price * amount * feePercent;
|
||||
fees[this.currency] = fee;
|
||||
|
||||
callback(err, {price, amount, date, fees, feePercent});
|
||||
};
|
||||
|
||||
const fetch = cb => this.therocktrading.order_status(this.pair, order, this.processResponse('order_status', cb));
|
||||
retry(null, fetch, handle);
|
||||
};
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
const result = (err, data) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
var status = data.status;
|
||||
if (status == 'executed') {
|
||||
return callback(undefined, { executed: true, open: false, filledAmount: parseFloat(data.amount) });
|
||||
} else if (status === 'deleted') {
|
||||
return callback(undefined, { executed: false, open: false, filledAmount: parseFloat(data.amount - data.amount_unfilled) });
|
||||
} else if (status === 'active') {
|
||||
// should check if not expired (data.close_on)?
|
||||
return callback(undefined, { executed: false, open: true, filledAmount: parseFloat(data.amount - data.amount_unfilled) });
|
||||
}
|
||||
|
||||
callback(new Error('Unknown status ' + status));
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.therocktrading.order_status(this.pair, order, this.processResponse('order_status', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
// callback for cancelOrder should be true if the order was already filled, otherwise false
|
||||
const result = (err, data) => {
|
||||
if(err) {
|
||||
return callback(null, true); // need to catch the specific error but usually an error on cancel means it was filled
|
||||
}
|
||||
|
||||
return callback(null, false);
|
||||
};
|
||||
|
||||
const fetch = cb =>
|
||||
this.therocktrading.cancel_order(this.pair, order, this.processResponse('cancel_order', cb));
|
||||
retry(null, fetch, result);
|
||||
};
|
||||
|
||||
Trader.prototype.roundAmount = function(amount) {
|
||||
return _.floor(amount, 8);
|
||||
}
|
||||
|
||||
Trader.prototype.roundPrice = function(price) {
|
||||
return +price;
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function() {
|
||||
return {
|
||||
name: 'Therocktrading',
|
||||
slug: 'therocktrading',
|
||||
currencies: marketData.currencies,
|
||||
assets: marketData.assets,
|
||||
markets: marketData.markets,
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: 'date',
|
||||
providesFullHistory: true,
|
||||
// following tid, define type to use on trade to batch trades
|
||||
tid: 'tid',
|
||||
tradable: true,
|
||||
gekkoBroker: 0.6
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Trader;
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
var WEX = require('node-wex');
|
||||
|
||||
var moment = require('moment');
|
||||
var util = require('../core/util');
|
||||
var _ = require('lodash');
|
||||
var log = require('../core/log')
|
||||
|
||||
var Trader = function(config) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.asset = config.asset;
|
||||
this.currency = config.currency;
|
||||
this.pair = [config.asset, config.currency].join('_').toLowerCase();
|
||||
this.name = 'wex.nz';
|
||||
|
||||
_.bindAll(this);
|
||||
|
||||
this.wex = new WEX(this.key, this.secret);
|
||||
_.bindAll(this.wex, ['trade', 'trades', 'getInfo', 'ticker', 'orderList']);
|
||||
|
||||
// see @link https://github.com/askmike/gekko/issues/302
|
||||
this.wexHistorocial = new WEX(false, false, {public_url: 'https://wex.nz/api/3/'});
|
||||
_.bindAll(this.wexHistorocial, 'trades');
|
||||
}
|
||||
|
||||
Trader.prototype.round = function(amount) {
|
||||
// Prevent "You incorrectly entered one of fields."
|
||||
// because of more than 8 decimals.
|
||||
amount *= 100000000;
|
||||
amount = Math.floor(amount);
|
||||
amount /= 100000000;
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
amount = this.round(amount);
|
||||
|
||||
var set = function(err, data) {
|
||||
if(err || !data || !data.return)
|
||||
return log.error('unable to buy:', err);
|
||||
|
||||
callback(null, data.return.order_id);
|
||||
}.bind(this);
|
||||
|
||||
// workaround for nonce error
|
||||
setTimeout(function() {
|
||||
this.wex.trade(this.pair, 'buy', price, amount, _.bind(set, this));
|
||||
}.bind(this), 1000);
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
amount = this.round(amount);
|
||||
|
||||
var set = function(err, data) {
|
||||
if(err || !data || !data.return)
|
||||
return log.error('unable to sell:\n\n', err);
|
||||
|
||||
callback(null, data.return.order_id);
|
||||
};
|
||||
|
||||
// workaround for nonce error
|
||||
setTimeout(function() {
|
||||
this.wex.trade(this.pair, 'sell', price, amount, _.bind(set, this));
|
||||
}.bind(this), 1000);
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var args = _.toArray(arguments);
|
||||
var calculate = function(err, data) {
|
||||
|
||||
if(err) {
|
||||
if(err.message === 'invalid api key')
|
||||
util.die('Your ' + this.name + ' API keys are invalid');
|
||||
|
||||
return this.retry(this.getPortfolio, args);
|
||||
}
|
||||
|
||||
if(_.isEmpty(data))
|
||||
err = 'no data';
|
||||
|
||||
if (err || !data.return || !data.return.funds)
|
||||
return this.retry(this.getPortfolio, args);
|
||||
|
||||
var assetAmount = parseFloat( data.return.funds[this.asset.toLowerCase()] );
|
||||
var currencyAmount = parseFloat( data.return.funds[this.currency.toLowerCase()] );
|
||||
|
||||
if(!_.isNumber(assetAmount) || _.isNaN(assetAmount)) {
|
||||
log.error(`wex.nz did not return portfolio for ${this.asset}, assuming 0.`);
|
||||
assetAmount = 0;
|
||||
}
|
||||
|
||||
if(!_.isNumber(currencyAmount) || _.isNaN(currencyAmount)) {
|
||||
log.error(`wex.nz did not return portfolio for ${this.currency}, assuming 0.`);
|
||||
currencyAmount = 0;
|
||||
}
|
||||
|
||||
var portfolio = [
|
||||
{ name: this.asset, amount: assetAmount },
|
||||
{ name: this.currency, amount: currencyAmount }
|
||||
];
|
||||
|
||||
callback(err, portfolio);
|
||||
}.bind(this);
|
||||
|
||||
this.wex.getInfo(calculate);
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
// wex.nz doesn't state asks and bids in its
|
||||
// ticker
|
||||
var set = function(err, data) {
|
||||
|
||||
if(err)
|
||||
return this.retry(this.wex.ticker, [this.pair, set]);
|
||||
|
||||
var ticker = {
|
||||
ask: data.ticker.buy,
|
||||
bid: data.ticker.sell
|
||||
};
|
||||
|
||||
callback(err, ticker);
|
||||
}.bind(this);
|
||||
|
||||
this.wex.ticker(this.pair, set);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
// wex.nz doesn't have different fees based on orders
|
||||
// at this moment it is always 0.2%
|
||||
callback(false, 0.002);
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var check = function(err, result) {
|
||||
// wex returns an error when you have no open trades
|
||||
// right now we assume on every error that the order
|
||||
// was filled.
|
||||
//
|
||||
// TODO: check whether the error states that there are no
|
||||
// open trades or that there is something else.
|
||||
if(err)
|
||||
callback(false, true);
|
||||
else
|
||||
callback(err, !result[order]);
|
||||
}.bind(this);
|
||||
|
||||
this.wex.orderList({}, check);
|
||||
}
|
||||
|
||||
Trader.prototype.getOrder = function(orderId, callback) {
|
||||
console.log('getOrder', orderId);
|
||||
var args = _.toArray(arguments);
|
||||
var check = function(err, result) {
|
||||
if(err) {
|
||||
log.error('error on getOrder', err);
|
||||
return this.retry(this.getOrder, args);
|
||||
}
|
||||
|
||||
var order = null;
|
||||
|
||||
_.each(result.return, o => {
|
||||
if(o.order_id === +orderId)
|
||||
order = o;
|
||||
});
|
||||
|
||||
if(!order)
|
||||
return log.error('wex.nz did not provide the order');
|
||||
|
||||
var price = parseFloat(order.rate);
|
||||
var amount = parseFloat(order.amount);
|
||||
var date = moment.unix(order.timestamp);
|
||||
|
||||
callback(undefined, {price, amount, date});
|
||||
}.bind(this);
|
||||
|
||||
this.wex.tradeHistory({pair: this.pair}, check);
|
||||
}
|
||||
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
this.wex.cancelOrder(order, (err, result) => {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(err, trades) {
|
||||
if(err)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
if(descending)
|
||||
callback(false, trades);
|
||||
else
|
||||
callback(false, trades.reverse());
|
||||
}.bind(this);
|
||||
|
||||
// see @link https://github.com/askmike/gekko/issues/302
|
||||
if(since) {
|
||||
this.wexHistorocial.makePublicApiRequest(
|
||||
'trades',
|
||||
this.pair + '?limit=2000',
|
||||
this.processAPIv3Trades(process)
|
||||
)
|
||||
} else
|
||||
this.wex.trades(this.pair, process);
|
||||
}
|
||||
|
||||
Trader.prototype.processAPIv3Trades = function(cb) {
|
||||
return function(err, data) {
|
||||
var trades = _.map(data[this.pair], function(t) {
|
||||
return {
|
||||
price: t.price,
|
||||
amount: t.amount,
|
||||
tid: t.tid,
|
||||
date: t.timestamp
|
||||
}
|
||||
})
|
||||
cb(err, trades);
|
||||
}.bind(this)
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'wex.nz',
|
||||
slug: 'wex.nz',
|
||||
currencies: ['USD', 'RUR', 'EUR', 'BTC', 'LTC', 'ETH', 'NMC', 'NMC', 'NVC', 'PPC', 'DSH', 'BCH'],
|
||||
assets: [
|
||||
'BTC', 'LTC', 'NMC', 'NVC', 'USD', 'EUR', 'PPC', 'DSH', 'ETH',
|
||||
'USDET', 'RURET', 'EURET', 'BTCET', 'LTCET', 'ETHET', 'NMCET', 'NVCET', 'PPCET', 'DSHET', 'BCHET' // Token
|
||||
],
|
||||
markets: [
|
||||
{ pair: ['USD', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['EUR', 'BTC'], minimalOrder: { amount: 0.01, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['EUR', 'LTC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'NMC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'NMC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'NVC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'NVC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'USD'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'EUR'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'EUR'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'PPC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'PPC'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['EUR', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['LTC', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['ETH', 'DSH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['EUR', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['LTC', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'ETH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['USD', 'BCH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'BCH'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
|
||||
// Token pairs
|
||||
{ pair: ['USD', 'USDET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['RUR', 'RURET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['EUR', 'EURET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BTC', 'BTCET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['LTC', 'LTCET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['ETH', 'ETHET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['NMC', 'NMCET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['NVC', 'NVCET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['PPC', 'PPCET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['DSH', 'DSHET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
{ pair: ['BCH', 'BCHET'], minimalOrder: { amount: 0.1, unit: 'asset' } },
|
||||
|
||||
],
|
||||
requires: ['key', 'secret'],
|
||||
providesHistory: false,
|
||||
tid: 'tid',
|
||||
tradable: true
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
@@ -0,0 +1,144 @@
|
||||
var Zaif = require("zaif.jp");
|
||||
var util = require('../core/util.js');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var log = require('../core/log');
|
||||
|
||||
var Trader = function(config) {
|
||||
_.bindAll(this);
|
||||
if(_.isObject(config)) {
|
||||
this.key = config.key;
|
||||
this.secret = config.secret;
|
||||
this.clientID = config.username;
|
||||
}
|
||||
this.name = 'Zaif';
|
||||
this.balance;
|
||||
this.price;
|
||||
|
||||
this.zaif = Zaif.createPrivateApi(this.key, this.secret, 'user agent is node-zaif');
|
||||
this.api = Zaif.PublicApi;
|
||||
}
|
||||
|
||||
// if the exchange errors we try the same call again after
|
||||
// waiting 10 seconds
|
||||
Trader.prototype.retry = function(method, args) {
|
||||
var wait = +moment.duration(10, 'seconds');
|
||||
log.debug(this.name, 'returned an error, retrying..');
|
||||
|
||||
var self = this;
|
||||
|
||||
// make sure the callback (and any other fn)
|
||||
// is bound to Trader
|
||||
_.each(args, function(arg, i) {
|
||||
if(_.isFunction(arg))
|
||||
args[i] = _.bind(arg, self);
|
||||
});
|
||||
|
||||
// run the failed method again with the same
|
||||
// arguments after wait
|
||||
setTimeout(
|
||||
function() { method.apply(self, args) },
|
||||
wait
|
||||
);
|
||||
}
|
||||
|
||||
Trader.prototype.getPortfolio = function(callback) {
|
||||
var set = function(data) {
|
||||
var portfolio = [];
|
||||
_.each(data.funds, function(amount, asset) {
|
||||
// if(asset.indexOf('available') !== -1) {
|
||||
asset = asset.substr(0, 3).toUpperCase();
|
||||
portfolio.push({name: asset, amount: parseFloat(amount)});
|
||||
// }
|
||||
});
|
||||
callback(err, portfolio);
|
||||
}
|
||||
this.zaif.getInfo().then(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTicker = function(callback) {
|
||||
this.api.ticker('btc_jpy').then(callback);
|
||||
}
|
||||
|
||||
Trader.prototype.getFee = function(callback) {
|
||||
var makerFee = 0.0;
|
||||
callback(false, makerFee / 100);
|
||||
}
|
||||
|
||||
Trader.prototype.buy = function(amount, price, callback) {
|
||||
var set = function(result) {
|
||||
if(!result)
|
||||
return log.error('unable to buy:', result);
|
||||
|
||||
callback(null, result.order_id);
|
||||
};
|
||||
|
||||
// TODO: fees are hardcoded here?
|
||||
// amount *= 0.995; // remove fees
|
||||
// prevent: Ensure that there are no more than 8 digits in total.
|
||||
amount *= 100000000;
|
||||
amount = Math.floor(amount);
|
||||
amount /= 100000000;
|
||||
this.zaif.trade('bid', price, amount).then(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.sell = function(amount, price, callback) {
|
||||
var set = function(result) {
|
||||
if(!result)
|
||||
return log.error('unable to sell:', result);
|
||||
|
||||
callback(null, result.order_id);
|
||||
};
|
||||
|
||||
this.zaif.trade('ask', price, amount).then(_.bind(set, this));
|
||||
}
|
||||
|
||||
Trader.prototype.checkOrder = function(order, callback) {
|
||||
var check = function(result) {
|
||||
var stillThere = (order in result);
|
||||
callback(null, !stillThere);
|
||||
};
|
||||
|
||||
this.zaif.activeorders().then(_.bind(check, this));
|
||||
}
|
||||
|
||||
Trader.prototype.cancelOrder = function(order, callback) {
|
||||
var cancel = function(result) {
|
||||
if(!result)
|
||||
log.error('unable to cancel order', order, '(', result, ')');
|
||||
};
|
||||
|
||||
this.zaif.cancelorder(order).then(_.bind(cancel, this));
|
||||
}
|
||||
|
||||
Trader.prototype.getTrades = function(since, callback, descending) {
|
||||
var args = _.toArray(arguments);
|
||||
var process = function(result) {
|
||||
if(!result)
|
||||
return this.retry(this.getTrades, args);
|
||||
|
||||
callback(null, result.reverse());
|
||||
};
|
||||
|
||||
this.api.trades('btc_jpy').then(_.bind(process, this));
|
||||
}
|
||||
|
||||
Trader.getCapabilities = function () {
|
||||
return {
|
||||
name: 'Zaif.jp',
|
||||
slug: 'zaif.jp',
|
||||
currencies: ['JPY'],
|
||||
assets: ['BTC'],
|
||||
markets: [
|
||||
{
|
||||
pair: ['JPY', 'BTC'], minimalOrder: { amount: 1, unit: 'currency' }
|
||||
}
|
||||
],
|
||||
requires: ['key', 'secret', 'username'],
|
||||
providesHistory: false,
|
||||
fetchTimespan: 60,
|
||||
tid: 'tid'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Trader;
|
||||
Reference in new issue
Block a user