From 87b3b98dcd577e63305febb7720b9158de721f3d Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Sat, 15 Jul 2017 22:15:40 +0200 Subject: [PATCH 1/6] Added support for the BTCe exchange --- conf-sample.js | 5 + extensions/exchanges/btce/_codemap.js | 6 + extensions/exchanges/btce/exchange.js | 237 +++++++++++++++++++ extensions/exchanges/btce/products.json | 218 +++++++++++++++++ extensions/exchanges/btce/update-products.sh | 29 +++ package.json | 1 + 6 files changed, 496 insertions(+) create mode 100644 extensions/exchanges/btce/_codemap.js create mode 100644 extensions/exchanges/btce/exchange.js create mode 100644 extensions/exchanges/btce/products.json create mode 100755 extensions/exchanges/btce/update-products.sh diff --git a/conf-sample.js b/conf-sample.js index c0d0389fd5..0a23703c54 100644 --- a/conf-sample.js +++ b/conf-sample.js @@ -69,6 +69,11 @@ c.quadriga.secret = 'YOUR-SECRET'; // replace with the client id used at login, as a string, not number c.quadriga.client_id = 'YOUR-CLIENT-ID'; +// to enable BTC-e trading, enter your API credentials: +c.btce = {} +c.btce.key = 'YOUR-API-KEY' +c.btce.secret = 'YOUR-SECRET' + // Optional stop-order triggers: // sell if price drops below this % of bought price (0 to disable) diff --git a/extensions/exchanges/btce/_codemap.js b/extensions/exchanges/btce/_codemap.js new file mode 100644 index 0000000000..de021b736b --- /dev/null +++ b/extensions/exchanges/btce/_codemap.js @@ -0,0 +1,6 @@ +module.exports = { + _ns: 'zenbot', + + 'exchanges.btce': require('./exchange'), + 'exchanges.list[]': '#exchanges.btce' +} diff --git a/extensions/exchanges/btce/exchange.js b/extensions/exchanges/btce/exchange.js new file mode 100644 index 0000000000..431446dd4c --- /dev/null +++ b/extensions/exchanges/btce/exchange.js @@ -0,0 +1,237 @@ +var BTCE = require('btce') + , path = require('path') + , colors = require('colors') + , numbro = require('numbro') + +module.exports = function container (get, set, clear) { + var c = get('conf') + + var public_client, authed_client + + function publicClient () { + if (!public_client) { + public_client = new BTCE() + } + return public_client + } + + function authedClient () { + if (!authed_client) { + if (!c.btce || !c.btce.key || c.btce.key === 'YOUR-API-KEY') { + throw new Error('please configure your BTCe credentials in conf.js') + } + authed_client = new BTCE(c.btce.key, c.btce.secret) + } + return authed_client + } + + function joinProduct (product_id) { + return product_id.split('-')[0] + '_' + product_id.split('-')[1] + } + +/* + function statusErr (resp, body) { + if (resp.statusCode !== 200) { + var err = new Error('non-200 status: ' + resp.statusCode) + err.code = 'HTTP_STATUS' + err.body = body + return err + } + } +*/ + function statusErr (err, body) { + if (body === null) { + return new Error(err) + } else if (!body.success) { + if (body.error === 'invalid api key' || body.error === 'invalid sign') { + console.log(err) + throw new Error('please correct your BTCe credentials in conf.js') + } else if (err) { + return new Error('\nError: ' + err) + } + } else { + return body + } + } + + + function retry (method, args, err) { + if (method !== 'getTrades') { + console.error(('\nBTCe API is down! unable to call ' + method + ', retrying in 10s').red) + if (err) console.error(err) + console.error(args.slice(0, -1)) + } + setTimeout(function () { + exchange[method].apply(exchange, args) + }, 10000) + } + + var orders = {} + + var exchange = { + name: 'btce', + historyScan: 'false', + makerFee: 0.2, + takerFee: 0.2, + + getProducts: function () { + return require('./products.json') + }, + + getTrades: function (opts, cb) { + var func_args = [].slice.call(arguments) + var client = publicClient() + var pair = joinProduct(opts.product_id).toLowerCase() + var args = {} + if (opts.from) { + // move cursor into the future + args.before = opts.from + } + else if (opts.to) { + // move cursor into the past + args.after = opts.to + } + //var args = {pair: pair, count: 1000} + //client.trades(args, function (err, body) { + client.trades({ pair: pair, count: 1000 }, function (err, body) { +// body = statusErr(err, err) +//console.log('getTrades', body) + if (err) return retry('getTrades', func_args, err) + var trades = body.map(function (trade) { + return { + trade_id: trade.tid, + // Fix me + time: trade.date * 1000, // + //time: new Date(trade.date).getTime(), + size: trade.amount, + price: trade.price, + side: trade.trade_type + } + }) + cb(null, trades) + }) + }, + + getBalance: function (opts, cb) { + var args = { + currency: opts.currency.toLowerCase(), + asset: opts.asset.toLowerCase(), + wait: 10 + } + var func_args = [].slice.call(arguments) + var client = authedClient() + client.getInfo(function (err, body) { + body = statusErr(err, body) + if (err) { + return retry('getBalance', func_args, err) + } + if (body.success) { + var balance = {asset: 0, currency: 0} + var funds = body.return.funds + balance.currency = 999.450009 //funds[args.currency] + balance.asset = 0.450008 //funds[args.asset] + balance.currency_hold = 0 + balance.asset_hold = 0 + cb(null, balance) + } else { + } + }) + }, + + getQuote: function (opts, cb) { + var func_args = [].slice.call(arguments) + var client = publicClient() + var pair = joinProduct(opts.product_id).toLowerCase() + client.ticker({ pair: pair }, function (err, body) { + if (err) return retry('getQuote', func_args, err) + cb(null, { bid: body.ticker.buy, ask: body.ticker.sell }) + }) + }, + + cancelOrder: function (opts, cb) { + var func_args = [].slice.call(arguments) + var client = authedClient() + client.cancelOrder(opts.order_id, function (err, resp, body) { + body = statusErr(err, body) + // Fix me - Check return codes + if (body && (body.message === 'Order already done' || body.message === 'order not found')) return cb() + if (err) return retry('cancelOrder', func_args, err) + cb() + }) + }, + + trade: function (type, opts, cb) { + var func_args = [].slice.call(arguments) + var client = authed_client() + var pair = joinProduct(opts.product_id) + /* BTCe has no order type? + if (typeof opts.post_only === 'undefined') { + opts.post_only = true + } + if (opts.order_type === 'taker') { + delete opts.price + delete opts.post_only + opts.type = 'market' + } + */ + delete opts.order_type + client.trade({'pair': pair, 'type': type, 'rate': opts.price, 'amount': opts.size }, function(err, body) { + body = statusErr(err, body) + // Fix me - Check return codes from API + if (body && body.message === 'Insufficient funds') { + var order = { + status: 'rejected', + reject_reason: 'balance' + } + return cb(null, order) + } + if (err) return retry(type, func_args, err) + orders['~' + body.id] = body + cb(null,body) + //else console.log(err) + }) + }, + + 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 client = authedClient() + //client.getOrder(opts.order_id, function (err, resp, body) { + // Fix me - Check return result + var orderInfo = { + //from: opts.order_id, + count: 1, + from_id: opts.order_id, + //end_id: opts.order_id, + pair: opts.product_id + } + client.activeOrders(orderInfo, function (err, resp, body){ + body = statusErr(err, body) + if (err) return retry('getOrder', func_args, err) + if (resp.statusCode === 404) { + // order was cancelled. recall from cache + body = orders['~' + opts.order_id] + body.status = 'done' + body.done_reason = 'canceled' + } + // Fix me + body.filled_size = 0 + body.remaining_size = resp.return[opts.order_id].amount + cb(null, body) + }) + }, + + // return the property used for range querying. + getCursor: function (trade) { + return trade.trade_id + } + } + return exchange +} diff --git a/extensions/exchanges/btce/products.json b/extensions/exchanges/btce/products.json new file mode 100644 index 0000000000..1ca146ef55 --- /dev/null +++ b/extensions/exchanges/btce/products.json @@ -0,0 +1,218 @@ +[ + { + "asset": "BTC", + "currency": "USD", + "min_size": "0.001", + "max_size": "10000", + "increment": "0.0001", + "label": "BTC/USD" + }, + { + "asset": "BTC", + "currency": "RUR", + "min_size": "0.001", + "max_size": "1000000", + "increment": "0.0001", + "label": "BTC/RUR" + }, + { + "asset": "BTC", + "currency": "EUR", + "min_size": "0.001", + "max_size": "10000", + "increment": "0.0001", + "label": "BTC/EUR" + }, + { + "asset": "LTC", + "currency": "BTC", + "min_size": "0.01", + "max_size": "10", + "increment": "0.0001", + "label": "LTC/BTC" + }, + { + "asset": "LTC", + "currency": "USD", + "min_size": "0.1", + "max_size": "1000", + "increment": "0.0001", + "label": "LTC/USD" + }, + { + "asset": "LTC", + "currency": "RUR", + "min_size": "0.01", + "max_size": "100000", + "increment": "0.0001", + "label": "LTC/RUR" + }, + { + "asset": "LTC", + "currency": "EUR", + "min_size": "0.01", + "max_size": "1000", + "increment": "0.0001", + "label": "LTC/EUR" + }, + { + "asset": "NMC", + "currency": "BTC", + "min_size": "0.1", + "max_size": "10", + "increment": "0.0001", + "label": "NMC/BTC" + }, + { + "asset": "NMC", + "currency": "USD", + "min_size": "0.1", + "max_size": "100", + "increment": "0.0001", + "label": "NMC/USD" + }, + { + "asset": "NVC", + "currency": "BTC", + "min_size": "0.1", + "max_size": "10", + "increment": "0.0001", + "label": "NVC/BTC" + }, + { + "asset": "NVC", + "currency": "USD", + "min_size": "0.1", + "max_size": "1000", + "increment": "0.0001", + "label": "NVC/USD" + }, + { + "asset": "USD", + "currency": "RUR", + "min_size": "0.1", + "max_size": "150", + "increment": "0.0001", + "label": "USD/RUR" + }, + { + "asset": "EUR", + "currency": "USD", + "min_size": "0.1", + "max_size": "2", + "increment": "0.0001", + "label": "EUR/USD" + }, + { + "asset": "EUR", + "currency": "RUR", + "min_size": "0.1", + "max_size": "200", + "increment": "0.0001", + "label": "EUR/RUR" + }, + { + "asset": "PPC", + "currency": "BTC", + "min_size": "0.1", + "max_size": "10", + "increment": "0.0001", + "label": "PPC/BTC" + }, + { + "asset": "PPC", + "currency": "USD", + "min_size": "0.1", + "max_size": "100", + "increment": "0.0001", + "label": "PPC/USD" + }, + { + "asset": "DSH", + "currency": "BTC", + "min_size": "0.01", + "max_size": "10", + "increment": "0.0001", + "label": "DSH/BTC" + }, + { + "asset": "DSH", + "currency": "USD", + "min_size": "0.01", + "max_size": "1000", + "increment": "0.0001", + "label": "DSH/USD" + }, + { + "asset": "DSH", + "currency": "RUR", + "min_size": "0.01", + "max_size": "100000", + "increment": "0.0001", + "label": "DSH/RUR" + }, + { + "asset": "DSH", + "currency": "EUR", + "min_size": "0.01", + "max_size": "1000", + "increment": "0.0001", + "label": "DSH/EUR" + }, + { + "asset": "DSH", + "currency": "LTC", + "min_size": "0.01", + "max_size": "600", + "increment": "0.0001", + "label": "DSH/LTC" + }, + { + "asset": "DSH", + "currency": "ETH", + "min_size": "0.01", + "max_size": "600", + "increment": "0.0001", + "label": "DSH/ETH" + }, + { + "asset": "ETH", + "currency": "BTC", + "min_size": "0.01", + "max_size": "10", + "increment": "0.0001", + "label": "ETH/BTC" + }, + { + "asset": "ETH", + "currency": "USD", + "min_size": "0.01", + "max_size": "1000", + "increment": "0.0001", + "label": "ETH/USD" + }, + { + "asset": "ETH", + "currency": "EUR", + "min_size": "0.01", + "max_size": "1000", + "increment": "0.0001", + "label": "ETH/EUR" + }, + { + "asset": "ETH", + "currency": "LTC", + "min_size": "0.01", + "max_size": "1000", + "increment": "0.0001", + "label": "ETH/LTC" + }, + { + "asset": "ETH", + "currency": "RUR", + "min_size": "0.01", + "max_size": "100000", + "increment": "0.0001", + "label": "ETH/RUR" + } +] \ No newline at end of file diff --git a/extensions/exchanges/btce/update-products.sh b/extensions/exchanges/btce/update-products.sh new file mode 100755 index 0000000000..d2e9e9308d --- /dev/null +++ b/extensions/exchanges/btce/update-products.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env node +var request = require('micro-request') +request('https://btc-e.com/api/3/info', {headers: {'User-Agent': 'zenbot/4'}}, function (err, resp, body) { + if (err) throw err + if (resp.statusCode !== 200) { + var err = new Error('non-200 status: ' + resp.statusCode) + err.code = 'HTTP_STATUS' + err.body = body + console.error(err) + process.exit(1) + } + var products = [] + pairs = body.pairs + Object.keys(body.pairs).forEach(function (product) { + var min = pairs[product].min_amount + products.push({ + asset: product.split('_')[0].toUpperCase(), + currency: product.split('_')[1].toUpperCase(), + min_size: pairs[product].min_amount.toString(), + max_size: pairs[product].max_price.toString(), + increment: '0.0001', + label: (product.split('_')[0] + '/' + product.split('_')[1]).toUpperCase() + }) + }) + var target = require('path').resolve(__dirname, 'products.json') + require('fs').writeFileSync(target, JSON.stringify(products, null, 2)) + console.log('wrote', target) + process.exit() +}) diff --git a/package.json b/package.json index a6992a314f..5d3799823a 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "bitfinex-api-node": "^1.0.1", "bitstamp": "^1.0.1", "bl": "^1.2.1", + "btce": "^0.4.2", "codemap": "^1.3.1", "colors": "^1.1.2", "commander": "^2.9.0", From d55c28d988e79fd71854bc418d243aeda9782554 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Sat, 15 Jul 2017 22:20:34 +0200 Subject: [PATCH 2/6] A minor cleanup --- extensions/exchanges/btce/exchange.js | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/extensions/exchanges/btce/exchange.js b/extensions/exchanges/btce/exchange.js index 431446dd4c..3c554367b7 100644 --- a/extensions/exchanges/btce/exchange.js +++ b/extensions/exchanges/btce/exchange.js @@ -1,3 +1,9 @@ +// +// Warning - Some of the functions need testing +// by someone in posession of a BTCe account +// In particular this is the case for +// the buy, sell, cancelOrderand getOrderfunctions +// var BTCE = require('btce') , path = require('path') , colors = require('colors') @@ -29,16 +35,6 @@ module.exports = function container (get, set, clear) { return product_id.split('-')[0] + '_' + product_id.split('-')[1] } -/* - function statusErr (resp, body) { - if (resp.statusCode !== 200) { - var err = new Error('non-200 status: ' + resp.statusCode) - err.code = 'HTTP_STATUS' - err.body = body - return err - } - } -*/ function statusErr (err, body) { if (body === null) { return new Error(err) @@ -91,17 +87,12 @@ module.exports = function container (get, set, clear) { // move cursor into the past args.after = opts.to } - //var args = {pair: pair, count: 1000} - //client.trades(args, function (err, body) { client.trades({ pair: pair, count: 1000 }, function (err, body) { -// body = statusErr(err, err) -//console.log('getTrades', body) if (err) return retry('getTrades', func_args, err) var trades = body.map(function (trade) { return { trade_id: trade.tid, - // Fix me - time: trade.date * 1000, // + time: trade.date * 1000, //time: new Date(trade.date).getTime(), size: trade.amount, price: trade.price, @@ -203,7 +194,6 @@ module.exports = function container (get, set, clear) { getOrder: function (opts, cb) { var func_args = [].slice.call(arguments) var client = authedClient() - //client.getOrder(opts.order_id, function (err, resp, body) { // Fix me - Check return result var orderInfo = { //from: opts.order_id, From 4fc74bb8ad5c12e8cced6eb1499c212319388053 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Sat, 15 Jul 2017 22:36:07 +0200 Subject: [PATCH 3/6] Even more cleanup --- extensions/exchanges/btce/exchange.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/exchanges/btce/exchange.js b/extensions/exchanges/btce/exchange.js index 3c554367b7..80bf9fd26e 100644 --- a/extensions/exchanges/btce/exchange.js +++ b/extensions/exchanges/btce/exchange.js @@ -119,8 +119,8 @@ module.exports = function container (get, set, clear) { if (body.success) { var balance = {asset: 0, currency: 0} var funds = body.return.funds - balance.currency = 999.450009 //funds[args.currency] - balance.asset = 0.450008 //funds[args.asset] + balance.currency = funds[args.currency] + balance.asset = funds[args.asset] balance.currency_hold = 0 balance.asset_hold = 0 cb(null, balance) From 4ccdc40984318775210dd7fc718370d30b4fe968 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Mon, 17 Jul 2017 12:45:45 +0200 Subject: [PATCH 4/6] Visible RSI values in the free space along with the RSI graph and some minor cosmetic changes to make for a more pleasant formatting --- lib/engine.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/engine.js b/lib/engine.js index 1ac7b3587c..2c8f345dd6 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -79,20 +79,21 @@ module.exports = function container (get, set, clear) { } function isFiat () { - return !s.currency.match(/^BTC|ETH|XMR|USD|USDT$/) + return !s.currency.match(/^BTC|ETH|XMR|USD|EUR|USDT$/) } var max_fc_width = 0 function fc (amt, omit_currency, color_trick, do_pad) { var str - if (isFiat()) { - str = n(amt).format('0.00') - } - else { - str = n(amt).format('0.00000000') - if (str.split('.').length >= 2) - if (str.split('.')[1].length === 7) str += '0' - } + var fstr + amt > 999 ? fstr = '0.00' : + amt > 99 ? fstr = '0.000' : + amt > 9 ? fstr = '0.0000' : + amt > 0.9 ? fstr = '0.00000' : + amt > 0.09 ? fstr = '0.000000' : + amt > 0.009 ? fstr = '0.0000000' : + fstr = '0.00000000' + str = n(amt).format(fstr) if (do_pad) { max_fc_width = Math.max(max_fc_width, str.length) str = ' '.repeat(max_fc_width - str.length) + str @@ -226,7 +227,7 @@ module.exports = function container (get, set, clear) { msg('not enough balance for ' + type + ', aborting') return cb(null, false) } - var err = new Error('order rejected') + var err = new Error('\norder rejected') err.order = api_order return cb(err) } @@ -411,7 +412,7 @@ module.exports = function container (get, set, clear) { if (s.buy_order && so.max_slippage_pct) { var slippage = n(price).subtract(s.buy_order.orig_price).divide(s.buy_order.orig_price).multiply(100).value() if (so.max_slippage_pct && slippage > so.max_slippage_pct) { - var err = new Error('slippage protection') + var err = new Error('\nslippage protection') err.desc = 'refusing to buy at ' + fc(price) + ', slippage of ' + pct(slippage / 100) return cb(err) } @@ -444,7 +445,7 @@ module.exports = function container (get, set, clear) { } var sell_loss = s.last_buy_price ? (Number(price) - s.last_buy_price) / s.last_buy_price * -100 : null if (so.max_sell_loss_pct && sell_loss > so.max_sell_loss_pct) { - var err = new Error('loss protection') + var err = new Error('\nloss protection') err.desc = 'refusing to sell at ' + fc(price) + ', sell loss of ' + pct(sell_loss / 100) return cb(err) } @@ -452,7 +453,7 @@ module.exports = function container (get, set, clear) { if (s.sell_order && so.max_slippage_pct) { var slippage = n(s.sell_order.orig_price).subtract(price).divide(price).multiply(100).value() if (slippage > so.max_slippage_pct) { - var err = new Error('slippage protection') + var err = new Error('\nslippage protection') err.desc = 'refusing to sell at ' + fc(price) + ', slippage of ' + pct(slippage / 100) return cb(err) } @@ -665,8 +666,9 @@ module.exports = function container (get, set, clear) { var bar = '' var stars = 0 if (s.period.rsi >= 50) { - bar += ' '.repeat(half) stars = Math.min(Math.round(((s.period.rsi - 50) / 50) * half) + 1, half) + bar += ' '.repeat(half - 3) + bar += rsi.green + ' ' bar += '+'.repeat(stars).green.bgGreen bar += ' '.repeat(half - stars) } @@ -674,7 +676,9 @@ module.exports = function container (get, set, clear) { stars = Math.min(Math.round(((50 - s.period.rsi) / 50) * half) + 1, half) bar += ' '.repeat(half - stars) bar += '-'.repeat(stars).red.bgRed - bar += ' '.repeat(half) + bar += rsi.length > 1 ? ' ' : ' ' + bar += rsi.red + bar += ' '.repeat(half - 3) } process.stdout.write(' ' + bar) } From ec9f5539c6013fd19c3af9ebe50ff665bf962ba2 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Tue, 18 Jul 2017 09:09:52 +0200 Subject: [PATCH 5/6] Adjusted display for (RSI === 100) --- lib/engine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/engine.js b/lib/engine.js index 2c8f345dd6..a31e05bfb9 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -667,7 +667,7 @@ module.exports = function container (get, set, clear) { var stars = 0 if (s.period.rsi >= 50) { stars = Math.min(Math.round(((s.period.rsi - 50) / 50) * half) + 1, half) - bar += ' '.repeat(half - 3) + bar += ' '.repeat(half - (rsi < 100 ? 3 : 4)) bar += rsi.green + ' ' bar += '+'.repeat(stars).green.bgGreen bar += ' '.repeat(half - stars) From 96f4f47e35b90a078d68005f797d2f827fbeaa52 Mon Sep 17 00:00:00 2001 From: Eigil Bjorgum Date: Tue, 18 Jul 2017 19:28:17 +0200 Subject: [PATCH 6/6] Fixed a line that got lost in copy --- lib/engine.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/engine.js b/lib/engine.js index a31e05bfb9..7089200a47 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -665,6 +665,7 @@ module.exports = function container (get, set, clear) { var half = 5 var bar = '' var stars = 0 + var rsi = s.period.rsi.toString() if (s.period.rsi >= 50) { stars = Math.min(Math.round(((s.period.rsi - 50) / 50) * half) + 1, half) bar += ' '.repeat(half - (rsi < 100 ? 3 : 4))