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;