diff --git a/extensions/exchanges/cexio/exchange.js b/extensions/exchanges/cexio/exchange.js index a4d8db472e..19c96257b1 100644 --- a/extensions/exchanges/cexio/exchange.js +++ b/extensions/exchanges/cexio/exchange.js @@ -1,235 +1,472 @@ -const CEX = require('cexio-api-node') -var path = require('path') -var n = require('numbro') -var minimist = require('minimist') - -module.exports = function cexio (conf) { - var s = { - options: minimist(process.argv) - } - var so = s.options - - var public_client, authed_client - - function publicClient () { - if (!public_client) { - public_client = new CEX().rest - } - return public_client - } - - function authedClient () { - if (!authed_client) { - if (!conf.cexio || !conf.cexio.username || !conf.cexio.key || conf.cexio.key === 'YOUR-API-KEY') { - throw new Error('please configure your CEX.IO credentials in ' + path.resolve(__dirname, 'conf.js')) - } - authed_client = new CEX(conf.cexio.username, conf.cexio.key, conf.cexio.secret).rest - } - return authed_client - } - - function joinProduct (product_id) { - return product_id.split('-')[0] + '/' + product_id.split('-')[1] - } - - function retry (method, args) { - if (so.debug) { - console.error(('\nCEX.IO API is down! unable to call ' + method + ', retrying in 10s').red) - } - setTimeout(function () { - exchange[method].apply(exchange, args) - }, 10000) - } - - function refreshFees(args) { - var skew = 5000 // in ms - var now = new Date() - var nowUTC = new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds()) - var midnightUTC = new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds()).setHours(24,0,0,0) - var countdown = midnightUTC - nowUTC + skew - if (so.debug) { - var hours = parseInt((countdown/(1000*60*60))%24) - var minutes = parseInt((countdown/(1000*60))%60) - var seconds = parseInt((countdown/1000)%60) - console.log('\nRefreshing fees in ' + hours + ' hours ' + minutes + ' minutes ' + seconds + ' seconds') - } - setTimeout(function() { - exchange['setFees'].apply(exchange, args) - }, countdown) - } - - var orders = {} - var exchange = { - name: 'cexio', - historyScan: 'forward', - backfillRateLimit: 0, - makerFee: 0.16, - takerFee: 0.25, - dynamicFees: true, - makerBuy100Workaround: true, - - getProducts: function () { - return require('./products.json') - }, - - getTrades: function (opts, cb) { - var func_args = [].slice.call(arguments) - var args - if (typeof opts.from === 'undefined' && opts.product_id === 'BTC-USD') { - args = 2000000 - } else { - args = opts.from - } - var client = publicClient() - var pair = joinProduct(opts.product_id) - client.trade_history(pair, args, function (err, body) { - if (so.debug && typeof body === 'string' && body.match(/error/)) console.log(('\ngetTrades ' + body).red) - if (err || (typeof body === 'string' && body.match(/error/))) return cb(err) - var trades = body.map(function (trade) { - return { - trade_id: Number(trade.tid), - time: Number(trade.date) * 1000, - size: Number(trade.amount), - price: Number(trade.price), - side: trade.type - } - }) - cb(null, trades) - }) - }, - - getBalance: function (opts, cb) { - var func_args = [].slice.call(arguments) - var client = authedClient() - client.account_balance(function (err, body) { - if (so.debug && typeof body === 'string' && body.match(/error/)) console.log(('\ngetBalance ' + body).red) - if (err || (typeof body === 'string' && body.match(/error/))) return retry('getBalance', func_args) - var balance = { asset: 0, currency: 0 } - balance.currency = n(body[opts.currency].available).add(body[opts.currency].orders).format('0.00000000') - balance.currency_hold = n(body[opts.currency].orders).format('0.00000000') - balance.asset = n(body[opts.asset].available).add(body[opts.asset].orders).format('0.00000000') - balance.asset_hold = n(body[opts.asset].orders).format('0.00000000') - cb(null, balance) - }) - }, - - getQuote: function (opts, cb) { - var func_args = [].slice.call(arguments) - var client = publicClient() - var pair = joinProduct(opts.product_id) - client.ticker(pair, function (err, body) { - if (so.debug && typeof body === 'string' && body.match(/error/)) console.log(('\ngetQuote ' + body).red) - if (err || (typeof body === 'string' && body.match(/error/))) return retry('getQuote', func_args) - cb(null, { bid: String(body.bid), ask: String(body.ask) }) - }) - }, - - cancelOrder: function (opts, cb) { - var func_args = [].slice.call(arguments) - var client = authedClient() - client.cancel_order(opts.order_id, function (err, body) { - if (so.debug && typeof body === 'string' && body.match(/error/)) console.log(('\ncancelOrder ' + body).red) - if (err || (typeof body === 'string' && body.match(/error/) && body !== 'error: Error: Order not found')) return retry('cancelOrder', func_args) - cb() - }) - }, - - trade: function (action, opts, cb) { - var func_args = [].slice.call(arguments) - var client = authedClient() - var pair = joinProduct(opts.product_id) - if (opts.order_type === 'taker') { - delete opts.price - delete opts.post_only - if (action === 'buy') { - opts.size = n(opts.size).multiply(opts.orig_price).value() // CEXIO estimates asset size and uses free currency to performe margin buy - } - opts.type = 'market' - } - client.place_order(pair, action, opts.size, opts.price, opts.type, function (err, body) { - if (so.debug && typeof body === 'string' && body.match(/error/)) console.log(('\ntrade ' + body).red) - if (err || (typeof body === 'string' && body.match(/error/) && body !== 'error: Error: Place order error: Insufficient funds.')) return retry('trade', func_args) - if (body === 'error: Error: Place order error: Insufficient funds.') { - var order = { - status: 'rejected', - reject_reason: 'balance' - } - return cb(null, order) - } - if (err) return retry('trade', func_args, err) - order = { - id: body && (body.complete === false || body.message) ? body.id : null, - status: 'open', - price: opts.price, - size: opts.size, - post_only: !!opts.post_only, - created_at: new Date().getTime(), - filled_size: '0', - ordertype: opts.order_type - } - orders['~' + body.id] = order - cb(null, order) - }) - }, - - buy: function (opts, cb) { - exchange.trade('buy', opts, cb) - }, - - sell: function (opts, cb) { - exchange.trade('sell', opts, cb) - }, - - getOrder: function (opts, cb) { - var func_args = [].slice.call(arguments) - var order = orders['~' + opts.order_id] - var client = authedClient() - client.get_order_details(opts.order_id, function (err, body) { - if (so.debug && typeof body === 'string' && body.match(/error/)) console.log(('\ngetOrder ' + body).red) - if (err || (typeof body === 'string' && body.match(/error/))) return retry('getOrder', func_args) - if (body.status === 'c') { - order.status = 'rejected' - order.reject_reason = 'canceled' - } - if (body.status === 'd' || body.status === 'cd') { - order.status = 'done' - order.done_at = new Date().getTime() - order.filled_size = n(body.amount).subtract(body.remains).format('0.00000000') - } - cb(null, order) - }) - }, - - setFees: function(opts) { - var func_args = [].slice.call(arguments) - var client = authedClient() - client.get_my_fee(function (err, body) { - if (err || (typeof body === 'string' && body.match(/error/))) { - if (so.debug) { - console.log(('\nsetFees ' + body + ' - using fixed fees!').red) - } - return retry('setFees', func_args) - } else { - var pair = opts.asset + ':' + opts.currency - var makerFee = (parseFloat(body[pair].buyMaker) + parseFloat(body[pair].sellMaker)) / 2 - var takerFee = (parseFloat(body[pair].buy) + parseFloat(body[pair].sell)) / 2 - if (exchange.makerFee != makerFee) { - if (so.debug) console.log('\nMaker fee changed: ' + exchange.makerFee + '% -> ' + makerFee + '%') - exchange.makerFee = makerFee - } - if (exchange.takerFee != takerFee) { - if (so.debug) console.log('\nTaker fee changed: ' + exchange.takerFee + '% -> ' + takerFee + '%') - exchange.takerFee = takerFee - } - } - return refreshFees(func_args) - }) - }, - - // return the property used for range querying. - getCursor: function (trade) { - return trade.trade_id - } - } - return exchange -} +const CEX = require('cexio-api-node') +const path = require('path') +const n = require('numbro') +const minimist = require('minimist') +const _ = require('lodash') + +module.exports = function cexio (conf) { + let s = { + options: minimist(process.argv) + } + let so = s.options + + let public_client, authed_client, ws_client, ws_authed, ws_subscribed, amount_format + let ws_trades = [] + let orders = {} + + function publicClient () { + if (!public_client) { + public_client = new CEX().rest + } + return public_client + } + + function authedClient () { + if (!authed_client) { + if (!conf.cexio || !conf.cexio.username || !conf.cexio.key || conf.cexio.key === 'YOUR-API-KEY') { + throw new Error('please configure your CEX.IO credentials in ' + path.resolve(__dirname, 'conf.js')) + } + authed_client = new CEX(conf.cexio.username, conf.cexio.key, conf.cexio.secret).rest + } + return authed_client + } + + function joinProduct (product_id) { + return product_id.split('-')[0] + '/' + product_id.split('-')[1] + } + + function retry (method, args) { + if (so.debug) { + console.error(('\nCEX.IO API is down! unable to call ' + method + ', retrying in 10s').red) + } + setTimeout(function () { + exchange[method].apply(exchange, args) + }, 10000) + } + + function refreshFees(args) { + let skew = 5000 // in ms + let now = new Date() + let nowUTC = new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds()) + let midnightUTC = new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds()).setHours(24,0,0,0) + let countdown = midnightUTC - nowUTC + skew + if (so.debug) { + let hours = parseInt((countdown/(1000*60*60))%24) + let minutes = parseInt((countdown/(1000*60))%60) + let seconds = parseInt((countdown/1000)%60) + console.log('\nRefreshing fees in ' + hours + ' hours ' + minutes + ' minutes ' + seconds + ' seconds') + } + setTimeout(function() { + exchange['setFees'].apply(exchange, args) + }, countdown) + } + + function wsClient() { + return new Promise(function(resolve, reject) { + if (!ws_client) { + if (!conf.cexio || !conf.cexio.key || conf.cexio.key === 'YOUR-API-KEY') { + throw new Error('please configure your CEX.IO credentials in ' + path.resolve(__dirname, 'conf.js')) + } + ws_client = new CEX(conf.cexio.key, conf.cexio.secret).ws + ws_client.open() + ws_client.on('open', function() { + if (so.debug) console.log('WebSocket connected') + ws_client.auth() + ws_client.on('auth', function() { + if (so.debug) console.log('WebSocket authenticated') + ws_authed = true + resolve(ws_client) + }) + }) + ws_client.on('message', function(msg) { + switch (msg.e) { + case 'disconnecting': + if (so.debug) console.log('WebSocket disconnecting:', msg.reason) + break + case 'ping': + ws_client.send({ e: 'pong' }) // Heartbeat + break + case 'get-balance': + ws_client.emit('balance', msg.data) + break + case 'ticker': + ws_client.emit('ticker', msg.data) + break + case 'history': + ws_client.emit('history', msg.data) + break + case 'history-update': + msg.data.forEach(function(trade) { + ws_trades.push({ + trade_id: Number(trade[4]), + time: Number(trade[1]), + size: Number(n(trade[2]).divide(amount_format).format('0.00000000')), + price: Number(trade[3]), + side: trade[0] + }) + }) + break + case 'cancel-order': + ws_client.emit('cancelOrder', msg.data) + break + case 'place-order': + ws_client.emit('placeOrder', msg.data) + break + case 'get-order': + ws_client.emit('getOrder', msg.data) + break + } + }) + ws_client.on('error', function(err) { + console.error('WebSocket error:', err.Error) + }) + ws_client.on('close', function() { + ws_client = null + ws_authed = false + ws_subscribed = false + if (so.debug) console.log('WebSocket disconnected') + }) + } else { + switch (ws_client.ws.readyState) { + case 0: + reject('WebSocket connecting') + break + case 1: + if (ws_authed) { + if (so.debug) console.log('WebSocket open') + resolve(ws_client) + } else { + reject('WebSocket auth pending') + } + break + case 2: + reject('WebSocket closing') + break + case 3: + reject('WebSocket closed') + break + } + } + }) + } + + function wsTrades(pair) { + return new Promise(function(resolve, reject) { + wsClient().then(function(client) { + client.send({ + e: 'subscribe', + rooms: [ 'pair-' + pair ] + }) + client.once('history', function(trades) { + resolve(trades) + }) + }).catch(function(err) { + reject(err) + }) + }) + } + + function wsBalance() { + return new Promise(function(resolve, reject) { + wsClient().then(function(client) { + client.getBalance() + client.once('balance', function(balance) { + if (balance.error) { + reject(balance.error) + } else { + resolve(balance) + } + }) + }).catch(function(err) { + reject(err) + }) + }) + } + + function wsQuote(pair) { + return new Promise(function(resolve, reject) { + wsClient().then(function(client) { + client.authTicker(pair) + client.once('ticker', function(quote) { + if (quote.error) { + reject(quote.error) + } else { + resolve(quote) + } + }) + }).catch(function(err) { + reject(err) + }) + }) + } + + function wsCancelOrder(order_id) { + return new Promise(function(resolve, reject) { + wsClient().then(function(client) { + client.cancelOrder(order_id) + client.once('cancelOrder', function(order) { + if (order.error) { + reject(order.error) + } else { + resolve() + } + }) + }).catch(function(err) { + reject(err) + }) + }) + } + + function wsTrade(order) { + return new Promise(function(resolve, reject) { + wsClient().then(function(client) { + client.placeOrder(order.type, order.pair, order.size, order.price) + client.once('placeOrder', function(order) { + if (order.error) { + reject(order.error) + } else { + resolve(order) + } + }) + }).catch(function(err) { + reject(err) + }) + }) + } + + function wsGetOrder(order_id) { + return new Promise(function(resolve, reject) { + wsClient().then(function(client) { + client.getOrder(order_id) + client.once('getOrder', function(order) { + if (order.error) { + reject(order.error) + } else { + resolve(order) + } + }) + }).catch(function(err) { + reject(err) + }) + }) + } + + let exchange = { + name: 'cexio', + historyScan: 'forward', + backfillRateLimit: 0, + makerFee: 0.16, + takerFee: 0.25, + dynamicFees: true, + makerBuy100Workaround: true, + + getProducts: function () { + return require('./products.json') + }, + + getTrades: function (opts, cb) { + let func_args = [].slice.call(arguments) + if (so._[2] === 'backfill') { // Backfill using REST + let client = publicClient() + let pair = joinProduct(opts.product_id) + client.trade_history(pair, opts.from, function (err, body) { + if (err || (typeof body === 'string' && body.match(/error/))) { + if (so.debug) console.log(('\ngetTrades ' + (err ? err : body)).red) + return retry('getTrades', func_args) + } + let trades = body.map(function (trade) { + return { + trade_id: Number(trade.tid), + time: Number(trade.date) * 1000, + size: Number(trade.amount), + price: Number(trade.price), + side: trade.type + } + }) + cb(null, trades) + }) + } else { // WebSocket once Live + if (!ws_subscribed) wsTrades(opts.product_id).then(function(data) { + ws_subscribed = true + amount_format = opts.product_id.split('-')[0] === 'ETH' ? 1000000 : 100000000 // trade amount is an unformatted integer + data.forEach(function(trade) { + let t = trade.split(':') + ws_trades.push({ + trade_id: Number(t[4]), + time: Number(t[1]), + size: Number(n(t[2]).divide(amount_format).format('0.00000000')), + price: Number(t[3]), + side: t[0] + }) + }) + }).catch(function(err) { + if (so.debug) console.log(('\ngetTrades ' + err).red) + return retry('getTrades', func_args) + }) + _.remove(ws_trades, function(t) { + return t.trade_id <= opts.from + }) + cb(null, ws_trades) + } + }, + + getBalance: function (opts, cb) { + let func_args = [].slice.call(arguments) + wsBalance().then(function(data) { + ws_balance = { + currency: n(data.balance[opts.currency]).format('0.00000000'), + asset: n(data.balance[opts.asset]).format('0.00000000'), + currency_hold: n(data.obalance[opts.currency]).format('0.00000000'), + asset_hold: n(data.obalance[opts.asset]).format('0.00000000') + } + cb(null, ws_balance) + }).catch(function(err) { + if (so.debug) console.log(('\ngetBalance ' + err).red) + return retry('getBalance', func_args) + }) + }, + + getQuote: function (opts, cb) { + let func_args = [].slice.call(arguments) + wsQuote(opts.product_id).then(function(data) { + ws_ticker = { + ask: data.ask, + bid: data.bid + } + cb(null, ws_ticker) + }).catch(function(err) { + if (so.debug) console.log(('\ngetQuote ' + err).red) + return retry('getQuote', func_args) + }) + }, + + cancelOrder: function (opts, cb) { + let func_args = [].slice.call(arguments) + wsCancelOrder(opts.order_id).then(function() { + cb() + }).catch(function(err) { + if (so.debug) console.log(('\ncancelOrder ' + err).red) + if (err !== 'Error: Order not found') return retry('cancelOrder', func_args) + }) + }, + + trade: function (action, opts, cb) { + let func_args = [].slice.call(arguments) + if (opts.order_type === 'taker') { // Looks like WebSocket doesn't support taker/market orders (yet?) + delete opts.price + delete opts.post_only + if (action === 'buy') { + opts.size = n(opts.size).multiply(opts.orig_price).value() // CEXIO estimates asset size and uses free currency to performe margin buy + } + let client = authedClient() + client.place_order(joinProduct(opts.product_id), action, opts.size, opts.price, 'market', function (err, body) { + if (err || (typeof body === 'string' && body.match(/error/))) { + if (so.debug) console.log(('\ntrade ' + (err ? err : body)).red) + if (body === 'error: Error: Place order error: Insufficient funds.') { + let order = { + status: 'rejected', + reject_reason: 'balance' + } + return cb(null, order) + } else { + return retry('trade', func_args) + } + } else { + let order = { + id: body.id, + status: 'open', + price: opts.price, + size: opts.size, + post_only: !!opts.post_only, + created_at: body.time, + filled_size: '0', + ordertype: 'taker' + } + orders['~' + body.id] = order + cb(null, order) + } + }) + } else { + wsTrade({ + type: action, + pair: opts.product_id, + size: opts.size, + price: opts.price + }).then(function(data) { + let order = { + id: data.id, + status: 'open', + price: data.price, + size: data.amount, + post_only: !!opts.post_only, + created_at: data.time, + filled_size: data.amount - data.pending, + ordertype: 'maker' + } + orders['~' + data.id] = order + cb(null, order) + }).catch(function(err) { + if (so.debug) console.log(('\ntrade ' + err).red) + return retry('trade', func_args, err) + }) + } + }, + + buy: function (opts, cb) { + exchange.trade('buy', opts, cb) + }, + + sell: function (opts, cb) { + exchange.trade('sell', opts, cb) + }, + + getOrder: function (opts, cb) { + let func_args = [].slice.call(arguments) + let order = orders['~' + opts.order_id] + wsGetOrder(opts.order_id).then(function(data) { + if (data.status === 'c') { + order.status = 'rejected' + order.reject_reason = 'canceled' + } else if ( data.status === 'd' || data.status === 'cd') { + order.status = 'done' + order.done_at = new Date().getTime() + order.filled_size = n(data.amount).subtract(data.remains).format('0.00000000') + } + cb(null, order) + }).catch(function(err) { + if (so.debug) console.log(('\ngetOrder ' + err).red) + return retry('getOrder', func_args) + }) + }, + + setFees: function(opts) { + let func_args = [].slice.call(arguments) + let client = authedClient() + client.get_my_fee(function (err, body) { + if (err || (typeof body === 'string' && body.match(/error/))) { + if (so.debug) console.log(('\nsetFees ' + (err ? err : body) + ' - using fixed fees!').red) + return retry('setFees', func_args) + } else { + let pair = opts.asset + ':' + opts.currency + let makerFee = (parseFloat(body[pair].buyMaker) + parseFloat(body[pair].sellMaker)) / 2 + let takerFee = (parseFloat(body[pair].buy) + parseFloat(body[pair].sell)) / 2 + if (exchange.makerFee != makerFee) { + if (so.debug) console.log('\nMaker fee changed: ' + exchange.makerFee + '% -> ' + makerFee + '%') + exchange.makerFee = makerFee + } + if (exchange.takerFee != takerFee) { + if (so.debug) console.log('\nTaker fee changed: ' + exchange.takerFee + '% -> ' + takerFee + '%') + exchange.takerFee = takerFee + } + } + return refreshFees(func_args) + }) + }, + + // return the property used for range querying. + getCursor: function (trade) { + return trade.trade_id + } + } + return exchange +} \ No newline at end of file