diff --git a/commands/trade.js b/commands/trade.js index d4bb9069c6..352171ca1b 100644 --- a/commands/trade.js +++ b/commands/trade.js @@ -114,9 +114,11 @@ module.exports = function container (get, set, clear) { limit: 1000 } if (db_cursor) { + trade_cursor = db_cursor opts.query.time = {$gt: db_cursor} } else { + trade_cursor = query_start opts.query.time = {$gte: query_start} } get('db.trades').select(opts, function (err, trades) { @@ -257,7 +259,9 @@ module.exports = function container (get, set, clear) { console.error('\n' + moment().format('YYYY-MM-DD HH:mm:ss') + ' - error saving session') console.error(err) } - engine.writeReport(true) + if (s.period) { + engine.writeReport(true) + } }) }) } diff --git a/extensions/exchanges/bitfinex/exchange.js b/extensions/exchanges/bitfinex/exchange.js index 5a25f2b2fa..1620d4edfe 100644 --- a/extensions/exchanges/bitfinex/exchange.js +++ b/extensions/exchanges/bitfinex/exchange.js @@ -1,39 +1,242 @@ const BFX = require('bitfinex-api-node') var _ = require('lodash') + , minimist = require('minimist') , path = require('path') , n = require('numbro') module.exports = function container (get, set, clear) { var c = get('conf') + var s = {options: minimist(process.argv)} + var so = s.options - var public_client, authed_client + var ws_connecting = false + var ws_connected = false + var ws_timeout = 60000 + var ws_retry = 10000 + var ws_wait_on_apikey_error = 60000 * 5 + var pair, public_client, ws_client + + var ws_trades = [] + var ws_balance = [] + var ws_orders = [] + var ws_ticker = [] + var ws_hb = [] + var ws_walletCalcDone + function publicClient () { if (!public_client) public_client = new BFX(null,null, {version: 2, transform: true}).rest return public_client } + + function wsUpdateTrades (pair, trades) { + if (trades[0] === "tu") { + trades = [trades[1]] + } else if (trades[0] === "te") { + return + } + + trades.forEach(function (trade) { + newTrade = { + trade_id: Number(trade.ID), + time: Number(trade.MTS), + size: Math.abs(trade.AMOUNT), + price: Number(trade.PRICE), + side: trade.AMOUNT > 0 ? 'buy' : 'sell' + } + ws_trades.push(newTrade) + }) + + if (ws_trades.length > 1010) + ws_trades.shift() + } + + function wsUpdateTicker (pair, ticker) { + ws_ticker = ticker + } + + function wsMessage (message) { + if (message.event == "auth" && message.status == "OK") { + if (so.debug) { console.log(('WebSockets: We are now fully connected and authenticated.').green) } + ws_connecting = false + ws_connected = true + } + + if (message[0] != "undefined") + ws_hb[message[0]] = Date.now() + } + + function wsUpdateOrder (ws_order) { + cid = ws_order[2] + + // https://bitfinex.readme.io/v2/reference#ws-auth-orders + var order = ws_orders['~' + cid] + if (!order) { + console.warn(("\nWarning: Order " + cid + ' not found in cache (manual order?).').red) + return + } + + if (ws_order[13] === 'ACTIVE' || ws_order[13].match(/^PARTIALLY FILLED/)) { + order.status = 'open' + } else if (ws_order[13].match(/^EXECUTED/)) { + order.status = 'done' + } else if (ws_order[13] === 'CANCELED') { + order.status = 'rejected' + } else if (ws_order[13] === 'POSTONLY CANCELED') { + order.status = 'rejected' + order.reject_reason = 'post only' + } + + order.bitfinex_id = ws_order[0] + order.created_at = ws_order[4] + order.filled_size = n(ws_order[7]).subtract(ws_order[6]).format('0.00000000') + order.bitfinex_status = ws_order[13] + order.price = ws_order[16] + order.price_avg = ws_order[17] + + ws_orders['~' + cid] = order + } - function authedClient () { - if (!authed_client) { - if (!c.bitfinex || !c.bitfinex.key || c.bitfinex.key === 'YOUR-API-KEY') { - throw new Error('please configure your Bitfinex credentials in ' + path.resolve(__dirname, 'conf.js')) + function wsUpdateOrderCancel (ws_order) { + cid = ws_order[2] + + if (ws_order[13].match(/^INSUFFICIENT MARGIN/)) { + ws_orders['~' + cid].status = 'rejected' + ws_orders['~' + cid].reject_reason = 'balance' + } + + if (ws_orders['~' + cid]) + { + setTimeout(function () { + delete(ws_orders['~' + cid]) + }, 60000 * 60 * 12) + } + + wsUpdateOrder(ws_order) + } + + function wsUpdateReqOrder (error) { + if (error[6] === 'ERROR' && error[7].match(/^Invalid order: not enough .* balance for/)) { + cid = error[4][2] + ws_orders['~' + cid].status = 'rejected' + ws_orders['~' + cid].reject_reason = 'balance' + } + } + + function updateWallet (wallets) { + if (typeof(wallets[0]) !== "object") wallets = [wallets] + + wallets.forEach(function (wallet) { + if (wallet[0] === c.bitfinex.wallet) { + ws_balance[wallet[1].toUpperCase()] = {} + ws_balance[wallet[1].toUpperCase()].balance = wallet[2] + ws_balance[wallet[1].toUpperCase()].available = wallet[4] ? wallet[4] : 0 + if (wallet[4] !== null) { ws_walletCalcDone[wallet[1]] = true } + } + }) + } + + function wsConnect () { + if (ws_connected || ws_connecting) return + ws_client.open() + } + + function wsOpen () { + ws_client.auth() + ws_client.subscribeTrades(pair) + ws_client.subscribeTicker(pair) + } + + function wsSubscribed (event) { + // We only use the 'trades' channel for heartbeats. That one should be most frequently updated. + if (event.channel === "trades") { + ws_hb[event.chanId] = Date.now() + + var intervalId = setInterval(function() { + if (ws_hb[event.chanId]) { + var timeoutThreshold = (Number(Date.now()) - ws_timeout) + if (timeoutThreshold > ws_hb[event.chanId]) { + console.warn(("\nWebSockets Warning: No message on channel 'trade' within " + ws_timeout / 1000 + ' seconds, reconnecting...').red) + clearInterval(intervalId) + ws_connecting = false + ws_connected = false + ws_client.close() + } + } + }, ws_timeout) + } + } + + function wsClose () { + ws_connecting = false + ws_connected = false + + console.error(("\nWebSockets Error: Connection closed.").red + " Retrying every " + (ws_retry / 1000 + ' seconds').yellow + '.') + } + + function wsError (e) { + if (e.event == "auth" && e.status == "FAILED") { + errorMessage = ('WebSockets Warning: Authentication ' + e.status + ' (Reason: "' + e.msg + '").').red + ' Retrying in ' + (ws_wait_on_apikey_error / 1000 + ' seconds').yellow + '.' + if (e.msg == 'apikey: invalid') errorMessage = errorMessage + "\nEither your API key is invalid or you tried reconnecting to quickly. Wait and/or check your API keys." + console.warn(errorMessage) + + setTimeout(function () { + ws_client.auth() + }, ws_wait_on_apikey_error) + } + else { + ws_connecting = false + ws_connected = false + + ws_client.close() } - authed_client = new BFX(c.bitfinex.key, c.bitfinex.secret, {version: 1}).rest } - return authed_client + + function wsClient () { + if (!ws_client) { + if (!c.bitfinex || !c.bitfinex.key || c.bitfinex.key === 'YOUR-API-KEY') { + throw new Error('please configure your Bitfinex credentials in ' + path.resolve(__dirname, 'conf.js')) + } + ws_connecting = true + ws_connected = false + + ws_client = new BFX(c.bitfinex.key, c.bitfinex.secret, {version: 2, transform: true}).ws + + ws_client + .on('open', wsOpen) + .on('close', wsClose) + .on('error', wsError) + .on('subscribed', wsSubscribed) + .on('message', wsMessage) + .on('trade', wsUpdateTrades) + .on('ticker', wsUpdateTicker) + .on('ws', updateWallet) + .on('wu', updateWallet) + .on('on', wsUpdateOrder) + .on('on-req', wsUpdateReqOrder) + .on('ou', wsUpdateOrder) + .on('oc', wsUpdateOrderCancel) + + setTimeout(function() { + wsConnect() + }, ws_retry) + } } function joinProduct (product_id) { return product_id.split('-')[0] + '' + product_id.split('-')[1] } - function retry (method, args) { - if (method !== 'getTrades') { - console.error(('\nBitfinex API is down! unable to call ' + method + ', retrying in 10s').red) - } + function retry (method, args, cb) { setTimeout(function () { - exchange[method].apply(exchange, args) - }, 10000) + exchange[method].call(exchange, args, cb) + }, ws_retry) + } + + function waitForCalc (method, args, cb) { + setTimeout(function () { + exchange[method].call(exchange, args, cb) + }, 50) } function encodeQueryData(data) { @@ -43,8 +246,6 @@ module.exports = function container (get, set, clear) { return ret.join('&') } - var orders = {} - var exchange = { name: 'bitfinex', historyScan: 'backward', @@ -56,201 +257,234 @@ module.exports = function container (get, set, clear) { }, getTrades: function (opts, cb) { - var func_args = [].slice.call(arguments) - var client = publicClient() - var args = {} - args.sort = -1 //backward - args.limit = 1000 - if (opts.from) { - args.start = opts.from - } - else if (opts.to) { - args.end = opts.to - } - else if (args.start && !args.end) { - args.end = args.start + 500000 - } - else if (args.end && !args.start) { - args.start = args.end - 500000 - } - var query = encodeQueryData(args) - var pair = 't' + joinProduct(opts.product_id) - client.makePublicRequest('trades/' + pair + '/hist?' + query, function (err, body) { - if (err) return retry('getTrades', func_args, err) - var trades = body.map(function(trade) { - return { - trade_id: trade.ID, - time: trade.MTS, - size: Math.abs(trade.AMOUNT), - price: trade.PRICE, - side: trade.AMOUNT > 0 ? 'buy' : 'sell' - } - }) + if (!pair) { pair = joinProduct(opts.product_id) } + + // Backfilling using the REST API + if (opts.to || opts.to === null) { + var func_args = [].slice.call(arguments) + var client = publicClient() + var args = {} + args.sort = -1 //backward + args.limit = 1000 + if (opts.from) { + args.start = opts.from + } + else if (opts.to) { + args.end = opts.to + } + else if (args.start && !args.end) { + args.end = args.start + 500000 + } + else if (args.end && !args.start) { + args.start = args.end - 500000 + } + var query = encodeQueryData(args) + var tpair = 't' + joinProduct(opts.product_id) + client.makePublicRequest('trades/' + tpair + '/hist?' + query, function (err, body) { + if (err) return retry('getTrades', opts, cb) + var trades = body.map(function(trade) { + return { + trade_id: trade.ID, + time: trade.MTS, + size: Math.abs(trade.AMOUNT), + price: trade.PRICE, + side: trade.AMOUNT > 0 ? 'buy' : 'sell' + } + }) + cb(null, trades) + }) + } else { + // We're live now (i.e. opts.from is set), use websockets + if (!ws_client) { wsClient() } + if (typeof(ws_trades) === "undefined") { return retry('getTrades', opts, cb) } + trades = ws_trades.filter(function (trade) { return trade.time >= opts.from }) cb(null, trades) - }) + } }, getBalance: function (opts, cb) { - var client = authedClient() - client.wallet_balances(function (err, body) { - if (err) return(err) - var balance = {asset: 0, currency: 0} - var accounts = _(body).filter(function (body) { return body.type === c.bitfinex.wallet }).forEach(function (account) { - if (account.currency.toUpperCase() === opts.currency) { - balance.currency = n(account.amount).format('0.00000000') - balance.currency_hold = n(account.amount).subtract(account.available).format('0.00000000') - } - else if (account.currency.toUpperCase() === opts.asset) { - balance.asset = n(account.amount).format('0.00000000') - balance.asset_hold = n(account.amount).subtract(account.available).format('0.00000000') + if (!pair) { pair = joinProduct(opts.asset + '-' + opts.currency) } + + if (pair && !ws_walletCalcDone) { + ws_walletCalcDone = {} + ws_walletCalcDone[opts.asset] = false + ws_walletCalcDone[opts.currency] = false + } + + if (!ws_client) { wsClient() } + if (Object.keys(ws_balance).length === 0) { + if (so.debug && ws_connected === true) { + console.warn(("WebSockets Warning: Waiting for initial websockets snapshot.").red + " Retrying in " + (ws_retry / 1000 + ' seconds').yellow + '.') + } + return retry('getBalance', opts, cb) + } + + if (ws_walletCalcDone[opts.asset] === false && ws_walletCalcDone[opts.currency] === false) { + var ws_update_wallet = [ + 0, + 'calc', + null, + [ + ["wallet_exchange_" + opts.currency], + ["wallet_exchange_" + opts.asset] + ] + ] + + try { + ws_walletCalcDone[opts.asset] = "inProgress" + ws_walletCalcDone[opts.currency] = "inProgress" + + ws_client.send(ws_update_wallet) + } + catch (e) { + if (so.debug) { + console.warn(e) + console.warn(("\nWebSockets Warning: Cannot send 'calc' for getBalance update (maybe connection not open?).").red + ' Waiting for reconnect.') } - }) + } + + return waitForCalc('getBalance', opts, cb) + } + else if ( + (ws_walletCalcDone[opts.asset] === false && ws_walletCalcDone[opts.currency] === true) || + (ws_walletCalcDone[opts.asset] === true && ws_walletCalcDone[opts.currency] === false) + ) { + return waitForCalc('getBalance', opts, cb) + } + else { + balance = {} + balance.currency = n(ws_balance[opts.currency].balance).format('0.00000000') + balance.asset = n(ws_balance[opts.asset].balance).format('0.00000000') + + balance.currency_hold = ws_balance[opts.currency].available ? n(ws_balance[opts.currency].balance).subtract(ws_balance[opts.currency].available).format('0.00000000') : n(0).format('0.00000000') + balance.asset_hold = ws_balance[opts.asset].available ? n(ws_balance[opts.asset].balance).subtract(ws_balance[opts.asset].available).format('0.00000000') : n(0).format('0.00000000') + + ws_walletCalcDone[opts.asset] = false + ws_walletCalcDone[opts.currency] = false + cb(null, balance) - }) + } }, getQuote: function (opts, cb) { - var func_args = [].slice.call(arguments) - var client = publicClient() - var pair = 't' + joinProduct(opts.product_id) - client.ticker(pair, function (err, body) { - if (err) return retry('getQuote', func_args, err) - cb(null, { bid : String(body.BID), ask : String(body.ASK) }) - }) + cb(null, { bid : String(ws_ticker.BID), ask : String(ws_ticker.ASK) }) }, cancelOrder: function (opts, cb) { - var client = authedClient() - client.cancel_order(opts.order_id, function (err, body) { - if (err) return(err) - cb() - }) - }, - - buy: function (opts, cb) { - var client = authedClient() - if (opts.order_type === 'maker' && typeof opts.type === 'undefined') { - opts.type = 'exchange limit' - } - else if (opts.order_type === 'taker' && typeof opts.type === 'undefined') { - opts.type = 'exchange market' - } - if (typeof opts.post_only === 'undefined') { - opts.post_only = true - } - var symbol = joinProduct(opts.product_id) - var amount = opts.size - var price = opts.price - var exchange = 'bitfinex' - var side = 'buy' - var type = opts.type - var is_hidden = false - var is_postonly = opts.post_only - var params = { - symbol, - amount, - price, - exchange, - side, - type, - is_hidden, - is_postonly + order = ws_orders['~' + opts.order_id] + ws_orders['~' + opts.order_id].reject_reason = "zenbot cancel" + + var ws_cancel_order = [ + 0, + 'oc', + null, + { + id: order.bitfinex_id + } + ] + + try { + ws_client.send(ws_cancel_order) } - client.make_request('order/new', params, function (err, body) { - var order = { - id: body && body.is_live === true ? body.order_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 - } - if (err && err.toString('Error: Invalid order: not enough exchange balance')) { - status: 'rejected' - reject_reason: 'balance' - return cb(null, order) + catch (e) { + if (so.debug) { + console.warn(e) + console.warn(("\nWebSockets Warning: Cannot send cancelOrder (maybe connection not open?).").red + " Retrying in " + (ws_retry / 1000 + ' seconds').yellow + '.') } - if (err) return(err) - orders['~' + body.id] = order - cb(null, order) - }) + return retry('cancelOrder', opts, cb) + } + cb() }, - sell: function (opts, cb) { - var client = authedClient() + trade: function (action, opts, cb) { + if (!pair) { pair = joinProduct(opts.product_id) } + var symbol = 't' + pair + + if (!ws_client) { wsClient() } + + var cid = Math.round(((new Date()).getTime()).toString() * Math.random()) + var amount = action === 'buy' ? opts.size : opts.size * -1 + var price = opts.price + if (opts.order_type === 'maker' && typeof opts.type === 'undefined') { - opts.type = 'exchange limit' + opts.type = 'EXCHANGE LIMIT' } else if (opts.order_type === 'taker' && typeof opts.type === 'undefined') { - opts.type = 'exchange market' + opts.type = 'EXCHANGE MARKET' } if (typeof opts.post_only === 'undefined') { opts.post_only = true } - var symbol = joinProduct(opts.product_id) - var amount = opts.size - var price = opts.price - var exchange = 'bitfinex' - var side = 'sell' var type = opts.type - var is_hidden = false var is_postonly = opts.post_only - var params = { - symbol, - amount, - price, - exchange, - side, - type, - is_hidden, - is_postonly + + var order = { + id: cid, + bitfinex_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 } - client.make_request('order/new', params, function (err, body) { - var order = { - id: body && body.is_live === true ? body.order_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 + + var ws_order = [ + 0, + 'on', + null, + { + cid: cid, + type: type, + symbol: symbol, + amount: String(amount), + price: price, + hidden: 0, + postonly: is_postonly ? 1 : 0 } - if (err && err.toString('Error: Invalid order: not enough exchange balance')) { - status: 'rejected' - reject_reason: 'balance' - return cb(null, order) + ] + + try { + ws_client.send(ws_order) + } + catch (e) { + if (so.debug) { + console.warn(e) + console.warn(("\nWebSockets Warning: Cannot send trade (maybe connection not open?).").red + (" Orders are sensitive, we're marking this one as rejected and will not just repeat the order automatically.").yellow) } - if (err) return(err) - orders['~' + body.id] = order - cb(null, order) - }) + + order.status = 'rejected' + order.reject_reason = 'could not send order over websockets' + } + ws_orders['~' + cid] = order + + return 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 order = orders['~' + opts.order_id] - var client = authedClient() - client.order_status(opts.order_id, function (err, body) { - if (err) return(err) - if (!body.id) { - return cb('Order not found') - } - if (body.is_cancelled === true && body.is_live === false) { - order.status = 'rejected' - order.reject_reason = 'post only' - order.done_at = new Date().getTime() - return cb(null, order) - } - if (body.is_live === false) { - order.status = 'done' - order.done_at = new Date().getTime() - order.filled_size = body.original_amount - body.executed_amount - return cb(null, order) - } - cb(null, order) - }) + var order = ws_orders['~' + opts.order_id] + + if (order.status === 'rejected' && order.reject_reason === 'post only') { + return cb(null, order) + } else if (order.status === 'rejected' && order.reject_reason === 'zenbot canceled') { + return cb(null, order) + } + + if (order.status == "done") { + order.done_at = new Date().getTime() + return cb(null, order) + } + + cb(null, order) }, // return the property used for range querying. diff --git a/extensions/exchanges/kraken/exchange.js b/extensions/exchanges/kraken/exchange.js index 38c212b449..f79b1aff22 100644 --- a/extensions/exchanges/kraken/exchange.js +++ b/extensions/exchanges/kraken/exchange.js @@ -7,7 +7,9 @@ var KrakenClient = require('kraken-api'), module.exports = function container(get, set, clear) { var c = get('conf') - var s = {options: minimist(process.argv)} + var s = { + options: minimist(process.argv) + } var so = s.options var public_client, authed_client @@ -52,7 +54,7 @@ module.exports = function container(get, set, clear) { } console.warn(('\nKraken API warning - unable to call ' + method + ' (' + errorMsg + '), retrying in ' + timeout / 1000 + 's').yellow) } - setTimeout(function () { + setTimeout(function() { exchange[method].apply(exchange, args) }, timeout) } @@ -65,23 +67,20 @@ module.exports = function container(get, set, clear) { makerFee: 0.16, takerFee: 0.26, // The limit for the public API is not documented, 1750 ms between getTrades in backfilling seems to do the trick to omit warning messages. - backfillRateLimit: 2000, + backfillRateLimit: 3500, - getProducts: function () { + getProducts: function() { return require('./products.json') }, - getTrades: function (opts, cb) { + getTrades: function(opts, cb) { var func_args = [].slice.call(arguments) var client = publicClient() var args = { pair: joinProduct(opts.product_id) } - if (opts.from) { - args.since = parseFloat(opts.from) * 1000000000 - } - client.api('Trades', args, function (error, data) { + client.api('Trades', args, function(error, data) { if (error && error.message.match(recoverableErrors)) { return retry('getTrades', func_args, error) } @@ -93,11 +92,14 @@ module.exports = function container(get, set, clear) { if (data.error.length) { return cb(data.error.join(',')) } + if (opts.from) { + args.since = Number(opts.from) * 1000000000 + } var trades = [] - Object.keys(data.result[args.pair]).forEach(function (i) { + Object.keys(data.result[args.pair]).forEach(function(i) { var trade = data.result[args.pair][i] - if (!opts.to || (parseFloat(opts.to) >= parseFloat(trade[2]))) { + if (!opts.from || (Number(opts.from) < moment.unix((trade[2]).valueOf()))) { trades.push({ trade_id: trade[2] + trade[1] + trade[0], time: moment.unix(trade[2]).valueOf(), @@ -107,17 +109,20 @@ module.exports = function container(get, set, clear) { }) } }) + cb(null, trades) }) }, - getBalance: function (opts, cb) { + getBalance: function(opts, cb) { var args = [].slice.call(arguments) var client = authedClient() - client.api('Balance', null, function (error, data) { + client.api('Balance', null, function(error, data) { var balance = { - asset: 0, - currency: 0 + asset: '0', + asset_hold: '0', + currency: '0', + currency_hold: '0' } if (error) { @@ -128,28 +133,32 @@ module.exports = function container(get, set, clear) { console.error(error) return cb(error) } + if (data.error.length) { return cb(data.error.join(',')) } + if (data.result[opts.currency]) { balance.currency = n(data.result[opts.currency]).format('0.00000000') - balance.currency_hold = 0 + balance.currency_hold = '0' } + if (data.result[opts.asset]) { balance.asset = n(data.result[opts.asset]).format('0.00000000') - balance.asset_hold = 0 + balance.asset_hold = '0' } + cb(null, balance) }) }, - getQuote: function (opts, cb) { + getQuote: function(opts, cb) { var args = [].slice.call(arguments) var client = publicClient() var pair = joinProduct(opts.product_id) client.api('Ticker', { pair: pair - }, function (error, data) { + }, function(error, data) { if (error) { if (error.message.match(recoverableErrors)) { return retry('getQuote', args, error) @@ -168,12 +177,12 @@ module.exports = function container(get, set, clear) { }) }, - cancelOrder: function (opts, cb) { + cancelOrder: function(opts, cb) { var args = [].slice.call(arguments) var client = authedClient() client.api('CancelOrder', { txid: opts.order_id - }, function (error, data) { + }, function(error, data) { if (error) { if (error.message.match(recoverableErrors)) { return retry('cancelOrder', args, error) @@ -193,7 +202,7 @@ module.exports = function container(get, set, clear) { }) }, - trade: function (type, opts, cb) { + trade: function(type, opts, cb) { var args = [].slice.call(arguments) var client = authedClient() var params = { @@ -213,7 +222,7 @@ module.exports = function container(get, set, clear) { console.log("trade") console.log(params) } - client.api('AddOrder', params, function (error, data) { + client.api('AddOrder', params, function(error, data) { if (error && error.message.match(recoverableErrors)) { return retry('trade', args, error) } @@ -264,15 +273,15 @@ module.exports = function container(get, set, clear) { }) }, - buy: function (opts, cb) { + buy: function(opts, cb) { exchange.trade('buy', opts, cb) }, - sell: function (opts, cb) { + sell: function(opts, cb) { exchange.trade('sell', opts, cb) }, - getOrder: function (opts, cb) { + getOrder: function(opts, cb) { var args = [].slice.call(arguments) var order = orders['~' + opts.order_id] if (!order) return cb(new Error('order not found in cache')) @@ -280,7 +289,7 @@ module.exports = function container(get, set, clear) { var params = { txid: opts.order_id } - client.api('QueryOrders', params, function (error, data) { + client.api('QueryOrders', params, function(error, data) { if (error) { if (error.message.match(recoverableErrors)) { return retry('getOrder', args, error) @@ -322,8 +331,8 @@ module.exports = function container(get, set, clear) { }, // return the property used for range querying. - getCursor: function (trade) { - return Math.floor((trade.time || trade) / 1000) + getCursor: function(trade) { + return (trade.time || trade) } } return exchange diff --git a/lib/engine.js b/lib/engine.js index ec672bfb45..e9dae0d7b7 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -282,6 +282,7 @@ module.exports = function container (get, set, clear) { if (err) return cb(err) s.api_order = api_order order.status = api_order.status + if (api_order.reject_reason) order.reject_reason = api_order.reject_reason msg('order status: ' + order.status) if (api_order.status === 'done') { order.time = new Date(api_order.done_at).getTime() @@ -292,7 +293,11 @@ module.exports = function container (get, set, clear) { } if (order.status === 'rejected' && (order.reject_reason === 'post only' || api_order.reject_reason === 'post only')) { msg('post-only ' + type + ' failed, re-ordering') - return cancelOrder(true) + return cb(null, null) + } + if (order.status === 'rejected' && order.reject_reason === 'balance') { + msg('not enough balance for ' + type + ', aborting') + return cb(null, null) } if (new Date().getTime() - order.local_time >= so.order_adjust_time) { getQuote(function (err, quote) {