first commit
This commit is contained in:
commit
aa7869eb33
386 files changed
+69375
No files matched your search
@@ -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;
|
||||
Reference in new issue
Block a user