diff --git a/README.md b/README.md index fcd63721..da2e4704 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ -ADAMANT Trading & Market making bot is a software that allows to run trades on crypto exchanges or make fake volume. Trading is a mode when bot run orders according to some strategy. It can be profitable or not. -In Market making mode, the bot places orders and execute them by himself, making a trade volume. +ADAMANT Trading & Market making bot is a software that allows to run trades on crypto exchanges, make fake volume (wash trading) and build live-like dynamic order book. + +* In Market making mode, the bot places orders and execute them by himself, making a trade volume; and builds live-like dynamic order book. +* Trading is a mode when bot run orders according to some strategy. It can be profitable or not. Unavailable now—use [Zenbot](https://github.com/DeviaVir/zenbot). + Trade bots work in ADAMANT Messenger chats directly. Features: @@ -10,6 +13,7 @@ Features: * Fill order books * Place buy and sell limit or market orders * Market making +* Dynamic order book * Stores and displays statistics Supported exchanges (more in progress): @@ -18,8 +22,11 @@ Supported exchanges (more in progress): * [Bit-Z](https://u.bit-z.com/register?invite_code=2423317) * [CoinDeal](https://coindeal.com/ref/9WZN) * [Resfinex](https://trade.resfinex.com?ref=7ccb34d867&pair=ADM_USDT) +* [Atomars](https://atomars.com/refcode/kaba) + +# Usage -Available commands: ask a bot with `/help` command. Read more: [Crypto trading & Market making bot in ADAMANT](https://medium.com/adamant-im/crypto-trading-market-making-bot-in-adamant-82fa48b78f51). +Available commands: ask a bot with `/help` command. Read more how to use the bot: [Crypto trading & Market making bot in ADAMANT](https://medium.com/adamant-im/crypto-trading-market-making-bot-in-adamant-82fa48b78f51). # Installation @@ -50,8 +57,9 @@ Parameters: * `pair` Pair to with on the exchange. Obligatory. * `coin1Decimals` Meaningful decimals for output of coin1 amounts. Default is 8. * `coin2Decimals` Meaningful decimals for output of coin2 amounts. Default is 8. -* `apikey` Exchange's account API key for connection. Obligatory. -* `apisecret` Exchange's account API secret for connection. Obligatory. +* `clearAllOrdersInterval` Interval in minutes to clear all opened orders. Default is 0 (disabled). +* `apikey` Exchange's account API key (username/login for some exchanges) for connection. Obligatory. +* `apisecret` Exchange's account API secret (password for some exchanges) for connection. Obligatory. * `apipassword` Exchange's account trade password. If needed for exchange. * `passPhrase` The bot's secret phrase for accepting commands. Obligatory. Bot's ADM address will correspond this passPhrase. * `admin_accounts` ADAMANT accounts to accept commands from. Commands from other accounts will not be executed. At lease one account. diff --git a/app.js b/app.js index d506c5dc..32ebaa9f 100644 --- a/app.js +++ b/app.js @@ -31,6 +31,7 @@ function init() { } checker(); require('./trade/mm_trader').run(); + require('./trade/mm_orderbook_builder').run(); notify(`*${config.notifyName} started* for address _${Store.user.ADM.address}_ (ver. ${Store.version}).`, 'info'); }); } diff --git a/config.json b/config.json index 98ea0eca..cdbea628 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { - /** The bot's secret phrase for accepting commands. + /** The bot's secret passphrase. Create separate ADM account for the bot. Bot's ADM address will correspond this passPhrase. **/ "passPhrase": "distance expect praise frequent..", @@ -39,22 +39,32 @@ /** Notify non-admins that they are not admins. If false, bot will be silent. **/ "notify_non_admins": true, - /** Exchange to work with. Available values: "IDCM", "Bit-Z", "CoinDeal", "Resfinex". Case insensitive. **/ - "exchange": "IDCM", + /** Exchange to work with. Available values: "IDCM", "Bit-Z", "CoinDeal", "Resfinex", "Atomars". Case insensitive. **/ + "exchange": "Resfinex", /** Pair to trade **/ - "pair": "ADM/BTC", + "pair": "ADM/USDT", - /** Meaningful decimals for output of coin1 amounts **/ - "coin1Decimals": 0, + /** Meaningful decimals for output of coin1 amounts. Depends on coin and exchange **/ + "coin1Decimals": 2, // 0.12 ADM - /** Meaningful decimals for output of coin2 amounts **/ - "coin2Decimals": 8, + /** Meaningful decimals for output of coin2 amounts. Depends on coin and exchange **/ + "coin2Decimals": 6, // 0.123456 USDT - /** Exchange's account API key for connection **/ + /** Interval in minutes to clear all opened orders. + Some exchanges has API issues with closing or matching orders. + Because of this, your account can accumulate open orders and lead to freezed balances. + In such a case you can run "/clear all" command manually, or set this parameter for automatic clearing. + Note: this command cancels all of account orders, including ones which you set manually with "/fill" command. + If the exchange supports getting orders by pair param, only orders for the trade pair will be cleared. + 0 means disabled. + **/ + "clearAllOrdersInterval": 0, + + /** Exchange's account API key for connection. Or login for Atomars **/ "apikey": "YOUR-KEY..", - /** Exchange's account API secret for connection **/ + /** Exchange's account API secret for connection. Or password for Atomars **/ "apisecret": "YOUR-SECRET..", /** Exchange's account trade password. If needed. **/ @@ -72,7 +82,10 @@ /** Slack key for notifications and monitoring (if needed) **/ "slack": "https://hooks.slack.com/services/...", - /** Port for getting debug info. + /** If you don't want to receive "not enough balance" and "unable to execute cross-order" notifications, set this "true" **/ + "silent_mode": false, + + /** Port for getting debug info. Do not set for live bots, use only for debugging. Allows to get DBs records like http://ip:port/db?tb=incomingTxsDb **/ diff --git a/helpers/notify.js b/helpers/notify.js index 5a8365cb..1f16df2b 100644 --- a/helpers/notify.js +++ b/helpers/notify.js @@ -8,49 +8,52 @@ const { } = config; -module.exports = (message, type) => { +module.exports = (message, type, silent_mode = false) => { try { - log[type](message.replace(/\*/g, '').replace(/_/g, '')); - - if (!slack && !adamant_notify) { - return; - } - let color; - switch (type) { - case ('error'): - color = '#FF0000'; - break; - case ('warn'): - color = '#FFFF00'; - break; + log[type](message.replace(/\*/g, '').replace(/_/g, '')); - case ('info'): - color = '#00FF00'; - break; - case ('log'): - color = '#FFFFFF'; - break; - } - const opts = { - uri: slack, - method: 'POST', - json: true, - body: { - 'attachments': [{ - 'fallback': message, - 'color': color, - 'text': message, - 'mrkdwn_in': ['text'] - }] + if (!silent_mode) { + + if (!slack && !adamant_notify) { + return; + } + let color; + switch (type) { + case ('error'): + color = '#FF0000'; + break; + case ('warn'): + color = '#FFFF00'; + break; + case ('info'): + color = '#00FF00'; + break; + case ('log'): + color = '#FFFFFF'; + break; + } + const opts = { + uri: slack, + method: 'POST', + json: true, + body: { + 'attachments': [{ + 'fallback': message, + 'color': color, + 'text': message, + 'mrkdwn_in': ['text'] + }] + } + }; + if (slack && slack.length > 34) { + request(opts); + } + if (adamant_notify && adamant_notify.length > 5 && adamant_notify.startsWith('U') && config.passPhrase && config.passPhrase.length > 30) { + api.send(config.passPhrase, adamant_notify, `${type}| ${message.replace(/\*/g, '**')}`, 'message'); } - }; - if (slack && slack.length > 34) { - request(opts); - } - if (adamant_notify && adamant_notify.length > 5 && adamant_notify.startsWith('U') && config.passPhrase && config.passPhrase.length > 30) { - api.send(config.passPhrase, adamant_notify, `${type}| ${message.replace(/\*/g, '**')}`, 'message'); } + } catch (e) { log.error('Notifier error: ' + e); } diff --git a/helpers/utils/index.js b/helpers/utils/index.js index 68332efc..c8f20aa9 100644 --- a/helpers/utils/index.js +++ b/helpers/utils/index.js @@ -60,7 +60,7 @@ module.exports = { return null; } }, - async userDailiValue(senderId){ + async userDailyValue(senderId){ return (await db.paymentsDb.find({ transactionIsValid: true, senderId: senderId, @@ -68,7 +68,7 @@ module.exports = { inAmountMessageUsd: {$ne: null}, date: {$gt: (this.unix() - 24 * 3600 * 1000)} // last 24h })).reduce((r, c) => { - return r + c.inAmountMessageUsd; + return +r + +c.inAmountMessageUsd; }, 0); }, async updateAllBalances(){ diff --git a/modules/commandTxs.js b/modules/commandTxs.js index 5536714c..c4ed7b2f 100644 --- a/modules/commandTxs.js +++ b/modules/commandTxs.js @@ -57,9 +57,17 @@ function start(params) { if (type === "mm") { if (!tradeParams.mm_isActive) { tradeParams.mm_isActive = true; + if (tradeParams.mm_isOrderBookActive) { + msgNotify = `${config.notifyName} set to start market making & order book building for ${config.pair}.`; + msgSendBack = `Starting market making & order book building for ${config.pair} pair.`; + } else { + msgNotify = `${config.notifyName} set to start market making for ${config.pair}. Order book building is disabled.`; + msgSendBack = `Starting market making for ${config.pair} pair. Note, order book building is disabled. To enable, type */enable ob*.`; + } + return { - msgNotify: `${config.notifyName} set to start market making for ${config.pair}.`, - msgSendBack: `Starting market making for ${config.pair} pair..`, + msgNotify, + msgSendBack, notifyType: 'log' } } else { @@ -86,8 +94,8 @@ function stop(params) { if (tradeParams.mm_isActive) { tradeParams.mm_isActive = false; return { - msgNotify: `${config.notifyName} stopped market making for ${config.pair} pair.`, - msgSendBack: `Market making for ${config.pair} pair is disabled now.`, + msgNotify: `${config.notifyName} stopped market making & order book building for ${config.pair} pair.`, + msgSendBack: `Market making & order book building for ${config.pair} pair is disabled now.`, notifyType: 'log' } } else { @@ -101,6 +109,65 @@ function stop(params) { } } +function enable(params) { + const type = (params[0] || '').trim(); + if (!type || !type.length || !["ob"].includes(type)) { + return { + msgNotify: '', + msgSendBack: `Indicate option, _ob_ for order book building. Example: */enable ob 15*.`, + notifyType: 'log' + } + } + const value = +params[1]; + if (type === "ob") { + tradeParams.mm_isOrderBookActive = true; + if (value && value != Infinity) + tradeParams.mm_orderBookOrdersCount = value; + else if (!value.length && !tradeParams.mm_orderBookOrdersCount) + value = 15; // default for mm_orderBookOrdersCount + let msgNotify, msgSendBack; + if (tradeParams.mm_isActive) { + msgNotify = `${config.notifyName} enabled order book building for ${config.pair} pair with ${tradeParams.mm_orderBookOrdersCount} maximum number of orders.`; + msgSendBack = `Order book building is enabled for ${config.pair} pair with ${tradeParams.mm_orderBookOrdersCount} maximum number of orders.`; + } else { + msgNotify = `${config.notifyName} enabled order book building for ${config.pair} pair with ${tradeParams.mm_orderBookOrdersCount} maximum number of orders. Market making and order book building are not started yet.`; + msgSendBack = `Order book building is enabled for ${config.pair} pair with ${tradeParams.mm_orderBookOrdersCount} maximum number of orders. To start market making and order book building, type */start mm*.`; + } + return { + msgNotify, + msgSendBack, + notifyType: 'log' + } + } +} + +function disable(params) { + const type = (params[0] || '').trim(); + if (!type || !type.length || !["ob"].includes(type)) { + return { + msgNotify: '', + msgSendBack: `Indicate option, _ob_ for order book building. Example: */disable ob*.`, + notifyType: 'log' + } + } + if (type === "ob") { + tradeParams.mm_isOrderBookActive = false; + let msgNotify, msgSendBack; + if (tradeParams.mm_isActive) { + msgNotify = `${config.notifyName} disable order book building for ${config.pair} pair. Market making is still active.`; + msgSendBack = `Order book building is disabled for ${config.pair}. Market making is still active. To stop market making, type */stop mm*. To close current ob-orders, type */clear ob*.`; + } else { + msgNotify = `${config.notifyName} disabled order book building for ${config.pair}.`; + msgSendBack = `Order book building is disabled for ${config.pair}.`; + } + return { + msgNotify, + msgSendBack, + notifyType: 'log' + } + } +} + function buypercent(param) { const val = +((param[0] || '').trim()); if (!val || val === Infinity || val < 0 || val > 100) { @@ -113,8 +180,8 @@ function buypercent(param) { tradeParams.mm_buyPercent = val / 100; return { - msgNotify: `${config.notifyName} is set to make market with ${val}% of buy orders for ${config.pair} pair.`, - msgSendBack: `Set to make market with ${val}% of buy orders for ${config.pair} pair.`, + msgNotify: `${config.notifyName} is set to make market with ${val}% of buy orders for ${config.pair} pair. Order book building is set to ${100-val}% of buy orders.`, + msgSendBack: `Set to make market with ${val}% of buy orders for ${config.pair} pair. Order book building is set to ${100-val}% of buy orders.`, notifyType: 'log' } } @@ -196,46 +263,46 @@ function interval(param) { async function clear(params) { - param = params[0]; - if (!param || param.indexOf('/') === -1) { - param = config.pair; - } - let coin = (param || '').toUpperCase().trim(); - let output = ''; - let pair; - if (coin.indexOf('/') > -1) { - pair = coin; - coin = coin.substr(0, coin.indexOf('/')); + let pair = params[0].toUpperCase(); + if (!pair || pair.indexOf('/') === -1) { + pair = config.pair; } if (!pair || !pair.length) { - output = 'Please specify market to clear orders in. F. e., */clear DOGE/BTC*.'; return { msgNotify: ``, - msgSendBack: `${output}`, + msgSendBack: 'Please specify market to clear orders in. F. e., */clear DOGE/BTC mm*.', notifyType: 'log' } } let purposes; - let count = 0; let purposeString; params.forEach(param => { if (['all'].includes(param)) { - purposes = ['mm', 'tb']; + purposes = ['mm', 'tb', 'ob']; } if (['tb'].includes(param)) { purposes = ['tb']; purposeString = `trade bot`; } + if (['ob'].includes(param)) { + purposes = ['ob']; + purposeString = `order book`; + } }); if (!purposes) { purposes = ['mm']; purposeString = `market making`; } + let output = ''; + let count = 0; // console.log(purposes, count, output); + + // First, close orders which are in bot's database count = await orderCollector(purposes, pair); + if (purposeString) { if (count > 0) { output = `Clearing ${count} **${purposeString}** orders for ${pair} pair on ${config.exchangeName}..`; @@ -249,7 +316,8 @@ async function clear(params) { } } - if (pair) { + // Next, if need to clear all orders, close orders which are not closed yet + if (!purposeString) { // Need to clear all orders const openOrders = await traderapi.getOpenOrders(pair); if (openOrders) { if ((count + openOrders.length) > 0) { @@ -273,45 +341,6 @@ async function clear(params) { } -// function $u.getPairObj(param, letCoin1only = false) { - -// let pair = (param || '').toUpperCase().trim(); -// let coin1Decimals = 8; -// let coin2Decimals = 8; -// let isPairFromParam = true; -// let coin1, coin2; - -// if (!pair || pair.indexOf('/') === -1 || pair === config.pair) { // Set default pair -// if (pair != config.pair) -// isPairFromParam = false; -// if ((pair.indexOf('/') === -1) && letCoin1only) { // Not a pair, may be a coin only -// coin1 = pair; -// if (coin1 === config.coin1) -// coin1Decimals = config.coin1Decimals; -// pair = null; -// coin2 = null; -// } else { // A pair -// pair = config.pair; -// coin1Decimals = config.coin1Decimals; -// coin2Decimals = config.coin2Decimals; -// } -// } - -// if (pair) { -// coin1 = pair.substr(0, pair.indexOf('/')); -// coin2 = pair.substr(pair.indexOf('/') + 1, pair.length); -// } - -// return { -// pair, -// coin1, -// coin2, -// coin1Decimals, -// coin2Decimals, -// isPairFromParam -// } -// } - async function fill(params) { // default: low=bid, count=5, pair=ADM/BTC @@ -457,16 +486,30 @@ async function fill(params) { price += $u.randomDeviation(step, deviation); coin1Amount = $u.randomDeviation(orderAmount, deviation); total += coin1Amount; + + // Checks if total or price exceeded + if (total > amount) { + if (count === 1) + coin1Amount = amount + else + break; + } + if (price > high) { + if (count === 1) + price = high + else + break; + } + + // Count base and quote currency amounts if (type === 'buy') { - // console.log(price, coin1Amount, total); coin2Amount = coin1Amount; coin1Amount = coin1Amount / price; } else { coin1Amount = coin1Amount; coin2Amount = coin1Amount * price; } - if (price > high || total > amount) - break; + // console.log(price, coin1Amount, total); orderList.push({ price: price, amount: coin1Amount, @@ -489,11 +532,14 @@ async function fill(params) { } } - output = `${items} orders to ${type} ${$u.thousandSeparator(+total1.toFixed(coin1Decimals), false)} ${coin1} for ${$u.thousandSeparator(+total2.toFixed(coin2Decimals), false)} ${coin2}.`; + if (items > 0) + output = `${items} orders to ${type} ${$u.thousandSeparator(+total1.toFixed(coin1Decimals), false)} ${coin1} for ${$u.thousandSeparator(+total2.toFixed(coin2Decimals), false)} ${coin2}.`; + else + output = `No orders were placed. Check log file for details.`; return { - msgNotify: `${config.notifyName} placed ${output}`, - msgSendBack: `Placed ${output}`, + msgNotify: items > 0 ? `${config.notifyName} placed ${output}` : '', + msgSendBack: items > 0 ? `Placed ${output}` : output, notifyType: 'log' } @@ -581,8 +627,8 @@ function getBuySellParams(params, type) { } } - // when Market order, buy should follow quote, sell — amount - const allowBuyAmountExchanges = ['resfinex']; + // when Market order, buy should pass quote parameter, when sell — amount + const allowBuyAmountExchanges = ['resfinex', 'atomars']; if (price === 'market' && !allowBuyAmountExchanges.includes(config.exchange)) { if ((type === 'buy' && !quote) || ((type === 'sell' && !amount))) { output = `When placing Market order, buy should follow with _quote_, sell with _amount_. Command works like this: */sell ADM/BTC amount=200 price=market*.`; @@ -594,7 +640,7 @@ function getBuySellParams(params, type) { } } - const amountNecessaryExchanges = ['resfinex']; + const amountNecessaryExchanges = ['resfinex', 'atomars']; if (price === 'market' && amountNecessaryExchanges.includes(config.exchange)) { if (!amount) { output = `When placing Market order on ${config.exchangeName}, _amount_ is necessary. Command works like this: */sell ADM/BTC amount=200 price=market*.`; @@ -698,11 +744,15 @@ Commands: **/stop**: Stop trading (td) or market making (mm). F. e., /*stop mm*. +**/enable**: Enable option. F. e., /*enable ob 10* to enable order book building with 10 maximum number of orders. + +**/disable**: Disable option. F. e., /*disable ob* to disable order book building. + **/amount**: Set the amount range for market making orders. Example: */amount 0.1-20*. **/interval**: Set the frequency in [sec, *min*, hour] of transactions for market making. Example: */interval 1-5 min*. -**/buyPercent**: Set the percentage of buy orders for market making. Try */buyPercent 85*. +**/buyPercent**: Set the percentage of buy orders for market making, and 100-percentage of buy orders for order book building. Try */buyPercent 75*. **/fill**: Fill sell or buy order book. Works like this: */fill ADM/BTC buy amount=0.00200000 low=0.00000050 high=0.00000182 count=7*. @@ -1034,5 +1084,7 @@ const commands = { fill, params, buy, - sell + sell, + enable, + disable } \ No newline at end of file diff --git a/modules/configReader.js b/modules/configReader.js index 9f4bfbd1..da2b413a 100644 --- a/modules/configReader.js +++ b/modules/configReader.js @@ -35,6 +35,10 @@ const fields = { type: Number, default: 8 }, + clearAllOrdersInterval: { + type: Number, + default: 0 + }, apikey: { type: String, isRequired: true @@ -75,6 +79,10 @@ const fields = { type: String, default: null }, + silent_mode: { + type: Boolean, + default: false + }, welcome_string: { type: String, default: 'Hello 😊. This is a stub. I have nothing to say. Please check my config.' diff --git a/package.json b/package.json index e48bff9d..4c4f3f6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "adamant-tradebot", - "version": "1.0.0", + "version": "1.4.0", "description": "ADAMANT Trading and Market Making bot", "main": "index.js", "scripts": { @@ -18,13 +18,16 @@ "trade", "market making", "fake volumes", + "wash trading", + "order book", "exchange", "idcm", "crypto", "cryptocurrency", "bit-z", "coindeal", - "resfinex" + "resfinex", + "atomars" ], "author": "Aleksei Lebedev (https://adamant.im)", "license": "GPL-3.0", diff --git a/trade/atomars_api.js b/trade/atomars_api.js new file mode 100644 index 00000000..5a8d1195 --- /dev/null +++ b/trade/atomars_api.js @@ -0,0 +1,222 @@ +var CryptoJS = require('crypto-js'); +const request = require('request'); +const DEFAULT_HEADERS = { + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36" +} + +var WEB_BASE = ''; +var config = { + 'auth-token': '', + 'auth-secret': '' +}; + +function api(path, r_data, do_sign, type, do_stringify) { + var url = `${WEB_BASE}${path}`; + var pars = []; + for (let key in r_data) { + let v = r_data[key]; + pars.push(key + "=" + v); + } + var p = pars.join("&"); + if (p && type != 'post') + url = url + "?" + p; + let headersWithSign = DEFAULT_HEADERS; + if (do_sign) + headersWithSign = Object.assign({"login-token": config["auth-token"], "x-auth-sign": setSign(r_data)}, DEFAULT_HEADERS); + + return new Promise((resolve, reject) => { + try { + var httpOptions = { + url: url, + method: type, + timeout: 10000, + headers: headersWithSign, + form: do_stringify ? JSON.stringify(r_data) : {data: r_data} + } + + // console.log("api-httpOptions:", httpOptions); + request(httpOptions, function(err, res, data) { + if (err) { + reject(err); + } else { + // console.log(`api() request result: ${data}`) + resolve(data); + } + }).on('error', function(err) { + console.log(`api() request() error. url: ${url}, type: ${type}, headersWithSign: ${headersWithSign}, data: ${r_data}, httpOptions: ${httpOptions}`); + reject(null); + }); + } catch(err) { + console.log(`api() error. url: ${url}, type: ${type}, headersWithSign: ${headersWithSign}, data: ${r_data}, httpOptions: ${httpOptions}`); + reject(null); + } + }); +} + +function setSign(params) { + // console.log(params); + let keys = Object.keys(params); + let n = keys.length; + keys = keys.sort(); + let sign = ''; + for (let i = 0; i < n; i++) { + sign = sign + params[keys[i]]; + } + sign = sign + config["auth-secret"]; + sign = CryptoJS.SHA256(sign).toString(CryptoJS.enc.Hex); + return sign; +} + +var EXCHANGE_API = { + + setConfig: async function(apiServer, username, password) { + if (!WEB_BASE) { + WEB_BASE = apiServer; + var res = JSON.parse(await this.login(username, password)); + config = { + 'auth-token': res.token, + 'auth-secret': res.data.secret + }; + // console.log(config); + } + }, + + /** + * ------------------------------------------------------------------ + * Login for API access + * ------------------------------------------------------------------ + */ + login: function(username, password) { + var data = {}; + data.username = username; + data.password = password; + return api("/login", data, false, 'post', false); + }, + + /** + * ------------------------------------------------------------------ + * Get balances + * ------------------------------------------------------------------ + */ + getUserAssets: function() { + var data = {}; + data.request_id = Date.now().toString(); + return api("/private/balances", data, true, 'post', true); + }, + + /** + * ------------------------------------------------------------------ + * Get open orders + * ------------------------------------------------------------------ + */ + getUserNowEntrustSheet: function() { + var data = {}; + data.request_id = Date.now().toString(); + return api("/private/orders", data, true, 'post', true); + }, + + /** + * ------------------------------------------------------------------ + * Place an order + * @param pair string "BTCUSDT" + * @param volume amount, float + * @param rate price, float + * @param type Buy/Sell (0/1) + * @param type_trade Limit/Market/Stop Limit/Quick Market (0/1/2/3) + * ------------------------------------------------------------------ + */ + addEntrustSheet: function(pair, amount, price, type, type_trade) { + var data = {}; + data.request_id = Date.now().toString(); + data.pair = pair; + if (price) + data.rate = price + else + data.rate = 0; + data.volume = amount; + data.type = type; + data.type_trade = type_trade; + return api("/private/create-order", data, true, 'post', true); + }, + + /** + * ------------------------------------------------------------------ + * Get a deposit address + * @param coin string "ADM" + * @param createNew string, Flag to get new address (0 - old address, 1 - new address) + * ------------------------------------------------------------------ + */ + getDepositAddress: function(coin, createNew = 0) { + var data = {}; + data.request_id = Date.now().toString(); + data.iso = coin; + data.new = createNew; + return api("/private/get-address", data, true, 'post', true); + }, + + /** + * ------------------------------------------------------------------ + * Get details for an order + * @param orderId string + * https://docs.atomars.com/#api-Private_API-Get_Order + * ------------------------------------------------------------------ + */ + getEntrustSheetInfo: function(orderId) { + var data = {}; + data.request_id = Date.now().toString(); + data.order_id = orderId; + return api("/private/get-order", data, true, 'post', true); + }, + + /** + * ------------------------------------------------------------------ + * Cancel the order + * @param order_id string + * ------------------------------------------------------------------ + */ + cancelEntrustSheet: function(order_id) { + var data = {}; + data.request_id = Date.now().toString(); + data.order_id = order_id.toString(); + return api("/private/delete-order", data, true, 'post', true); + }, + + /** + * ------------------------------------------------------------------ + * Get pair data + * @param pair BTCUSDT + * ------------------------------------------------------------------ + */ + ticker: function(pair) { + var data = {}; + data.pair = pair; + return api("/public/ticker", data, false, 'get', false); + }, + + /** + * ------------------------------------------------------------------ + * Get all tickers + * https://docs.atomars.com/#api-Public_API-Ticker_List + * ------------------------------------------------------------------ + */ + tickerall: function() { + var data = {}; + return api("/public/symbols", data, false, 'get', false); + }, + + /** + * ------------------------------------------------------------------ + * Get depth data + * @param pair BTCUSDT + * ------------------------------------------------------------------ + */ + orderBook: function(pair) { + var data = {}; + data.pair = pair; + return api("/public/book", data, false, 'get', false); + } + +} + +module.exports = EXCHANGE_API; \ No newline at end of file diff --git a/trade/bit-z_api.js b/trade/bit-z_api.js index fda7abbd..3746ddd9 100644 --- a/trade/bit-z_api.js +++ b/trade/bit-z_api.js @@ -292,7 +292,7 @@ var EXCHANGE_API = { * @param symbol eth_btc * ------------------------------------------------------------------ */ - depth: function(symbol) { + orderBook: function(symbol) { var data = {}; data.symbol = symbol; return market_api("/Market/depth", data); diff --git a/trade/mm_orderbook_builder.js b/trade/mm_orderbook_builder.js new file mode 100644 index 00000000..3aaa7275 --- /dev/null +++ b/trade/mm_orderbook_builder.js @@ -0,0 +1,256 @@ +const $u = require('../helpers/utils'); +const config = require('../modules/configReader'); +const log = require('../helpers/log'); +const notify = require('../helpers/notify'); +const tradeParams = require('./tradeParams_' + config.exchange); +const traderapi = require('./trader_' + config.exchange)(config.apikey, config.apisecret, config.apipassword, log); +const db = require('../modules/DB'); + +let lastNotifyBalancesTimestamp = 0; +let lastNotifyPriceTimestamp = 0; +const hour = 1000 * 60 * 60; +const INTERVAL_MIN = 1000; +const INTERVAL_MAX = 3000; +const LIFETIME_MIN = 1000; +const LIFETIME_MAX = 20000; + +module.exports = { + run() { + this.iteration(); + }, + iteration() { + let interval = setPause(); + // console.log(interval); + if (interval && tradeParams.mm_isActive && tradeParams.mm_isOrderBookActive) { + this.buildOrderBook(); + setTimeout(() => {this.iteration()}, interval); + } else { + setTimeout(() => {this.iteration()}, 3000); // Check for config.mm_isActive every 3 seconds + } + }, + async buildOrderBook() { + const {ordersDb} = db; + const orderBookOrdersCount = (await ordersDb.find({ + isProcessed: false, + purpose: 'ob', // ob: dynamic order book order + pair: config.pair, + exchange: config.exchange + })).length; + +// console.log(orderBookOrdersCount); + if (orderBookOrdersCount < tradeParams.mm_orderBookOrdersCount) + this.placeOrderBookOrder(orderBookOrdersCount); + + this.closeOrderBookOrders(orderBookOrdersCount); + }, + async closeOrderBookOrders(orderBookOrdersCount) { + const {ordersDb} = db; + const ordersToClose = await ordersDb.find({ + isProcessed: false, + purpose: 'ob', // ob: dynamic order book order + pair: config.pair, + exchange: config.exchange, + dateTill: {$lt: $u.unix()} + }); + orderBookOrdersCount-= ordersToClose.length; + ordersToClose.forEach(async order => { + try { + traderapi.cancelOrder(order._id, order.type, order.pair); + order.update({ + isProcessed: true, + isClosed: true + }); + await order.save(); + log.info(`Closing ob-order with params: id=${order._id}, type=${order.targetType}, pair=${order.pair}, price=${order.price}, coin1Amount=${order.coin1Amount}, coin2Amount=${order.coin2Amount}. Open ob-orders: ~${orderBookOrdersCount}.`); + + } catch (e) { + log.error('Error in removeOrderBookOrders(): ' + e); + } + }); + }, + async placeOrderBookOrder(orderBookOrdersCount) { + const type = setType(); + const position = setPosition(); + const priceReq = await setPrice(type, config.pair, position); + const price = priceReq.price; + const coin1Amount = setAmount(); + const coin2Amount = coin1Amount * price; + const lifeTime = setLifeTime(position); + + let orderId; + let output = ''; + let orderParamsString = ''; + const pairObj = $u.getPairObj(config.pair); + + if (!price) { + if ((Date.now()-lastNotifyPriceTimestamp > hour) && priceReq.message) { + notify(priceReq.message, 'warn'); + lastNotifyPriceTimestamp = Date.now(); + } + return; + } + + orderParamsString = `type=${type}, pair=${config.pair}, price=${price}, coin1Amount=${coin1Amount}, coin2Amount=${coin2Amount}`; + if (!type || !price || !coin1Amount || !coin2Amount) { + notify(`${config.notifyName} unable to run ob-order with params: ${orderParamsString}.`, 'warn'); + return; + } + + // console.log(type, price.toFixed(8), coin1Amount.toFixed(0), coin2Amount.toFixed(0), 'position:', position, 'lifeTime:', lifeTime); + + // Check balances + const balances = await isEnoughCoins(config.coin1, config.coin2, coin1Amount, coin2Amount, type); + if (!balances.result) { + if ((Date.now()-lastNotifyBalancesTimestamp > hour) && balances.message) { + notify(balances.message, 'warn', config.silent_mode); + lastNotifyBalancesTimestamp = Date.now(); + } + return; + } + + orderId = (await traderapi.placeOrder(type, config.pair, price, coin1Amount, 1, null, pairObj)).orderid; + if (orderId) { + const {ordersDb} = db; + const order = new ordersDb({ + _id: orderId, + date: $u.unix(), + dateTill: $u.unix() + lifeTime, + purpose: 'ob', // ob: dynamic order book order + type: type, + targetType: type, + exchange: config.exchange, + pair: config.pair, + coin1: config.coin1, + coin2: config.coin2, + price: price, + coin1Amount: coin1Amount, + coin2Amount: coin2Amount, + LimitOrMarket: 1, // 1 for limit price. 0 for Market price. + isProcessed: false, + isExecuted: false, + isCancelled: false, + isClosed: false + }); + await order.save(); + output = `${type} ${coin1Amount.toFixed(config.coin1Decimals)} ${config.coin1} for ${coin2Amount.toFixed(config.coin2Decimals)} ${config.coin2}`; + log.info(`Successfully placed ob-order to ${output}. Open ob-orders: ~${orderBookOrdersCount+1}.`); + } else { + console.warn(`${config.notifyName} unable to execute ob-order with params: ${orderParamsString}. No order id returned.`); + } + + }, +}; + +function setType() { + if (!tradeParams || !tradeParams.mm_buyPercent) { + log.warn(`Param mm_buyPercent is not set. Check ${config.exchangeName} config.`); + return false; + } + let type = 'sell'; + if (Math.random() > tradeParams.mm_buyPercent) // 1 minus tradeParams.mm_buyPercent + type = 'buy'; + return type; +} + +async function isEnoughCoins(coin1, coin2, amount1, amount2, type) { + const balances = await traderapi.getBalances(false); + let balance1, balance2; + let isBalanceEnough = true; + let output = ''; + + if (balances) { + try { + balance1 = balances.filter(crypto => crypto.code === coin1)[0].free; + balance2 = balances.filter(crypto => crypto.code === coin2)[0].free; + + + if ((!balance1 || balance1 < amount1) && type === 'sell') { + output = `${config.notifyName}: Not enough ${coin1} for placing ${type} ob-order. Check balances.`; + isBalanceEnough = false; + } + if ((!balance2 || balance2 < amount2) && type === 'buy') { + output = `${config.notifyName}: Not enough ${coin2} for placing ${type} ob-order. Check balances.`; + isBalanceEnough = false; + } + + // console.log(balance1.toFixed(0), amount1.toFixed(0), balance2.toFixed(8), amount2.toFixed(8)); + return { + result: isBalanceEnough, + message: output + } + + } catch (e) { + log.warn(`Unable to process balances for placing ob-order.`); + return { + result: false + } + } + } else { + log.warn(`Unable to get balances for placing ob-order.`); + return { + result: false + } + } +} + +async function setPrice(type, pair, position) { + + let output = ''; + + let high, low; + const orderBook = await traderapi.getOrderBook(pair, tradeParams.mm_orderBookHeight + 1); + if (!orderBook) { + log.warn(`Unable to get order book for ${pair} to set a price while placing ob-order.`); + return { + price: false, + } + } + + const orderList = type === 'buy' ? orderBook.bids : orderBook.asks; + // console.log(); + // console.log(type); + // console.log(orderList); + + if (!orderList || !orderList[0] || !orderList[1]) { + output = `${config.notifyName}: Orders count of type ${type} is less then 2. Unable to set a price for ${pair} while placing ob-order.`; + return { + price: false, + message: output + } + } else { + if (orderList.length < position) + position = orderList.length; + if (type === 'sell') { + low = orderList[position-2].price; + high = orderList[position-1].price; + } else { + high = orderList[position-2].price; + low = orderList[position-1].price; + } + } + + return { + price: Math.random() * (high - low) + low + } + +} + +function setAmount() { + if (!tradeParams || !tradeParams.mm_maxAmount || !tradeParams.mm_minAmount) { + log.warn(`Params mm_maxAmount or mm_minAmount are not set. Check ${config.exchangeName} config.`); + return false; + } + return Math.random() * (tradeParams.mm_maxAmount - tradeParams.mm_minAmount) + tradeParams.mm_minAmount; +} + +function setPosition() { + return Math.round(Math.random() * (tradeParams.mm_orderBookHeight - 2) + 2); +} + +function setLifeTime(position) { + return Math.round((Math.random() * (LIFETIME_MAX - LIFETIME_MIN) + LIFETIME_MIN) * Math.sqrt(position)); +} + +function setPause() { + return Math.round(Math.random() * (INTERVAL_MAX - INTERVAL_MIN) + INTERVAL_MIN); +} diff --git a/trade/mm_trader.js b/trade/mm_trader.js index 7b753e3e..e27ac21f 100644 --- a/trade/mm_trader.js +++ b/trade/mm_trader.js @@ -56,7 +56,7 @@ module.exports = { const balances = await isEnoughCoins(config.coin1, config.coin2, coin1Amount, coin2Amount); if (!balances.result) { if ((Date.now()-lastNotifyBalancesTimestamp > hour) && balances.message) { - notify(balances.message, 'warn'); + notify(balances.message, 'warn', config.silent_mode); lastNotifyBalancesTimestamp = Date.now(); } return; @@ -97,7 +97,7 @@ module.exports = { await order.save(); } else { await order.save(); - notify(`${config.notifyName} unable to execute cross-order for mm-order with params: id=${order1}, ${orderParamsString}. Check balances. Running order collector now.`, 'warn'); + notify(`${config.notifyName} unable to execute cross-order for mm-order with params: id=${order1}, ${orderParamsString}. Check balances. Running order collector now.`, 'warn', config.silent_mode); orderCollector(['mm'], config.pair); } } else { // if order1 @@ -233,14 +233,14 @@ async function setPrice(type, pair) { const minPrice = +bid_low + +precision; const maxPrice = ask_high - precision; - // price = 0.009248977658650832; + // price = 0.009618977658650832; // console.log('low, high', bid_low, ask_high); // console.log('min, max', minPrice, maxPrice); // console.log('price1', price); if (price >= maxPrice) - price = price - precision; + price = ask_high - precision; if (price <= minPrice) - price = +price + +precision; + price = +bid_low + +precision; // console.log('price2', price); return { diff --git a/trade/orderCollector.js b/trade/orderCollector.js index d42a5f3a..06690d1f 100644 --- a/trade/orderCollector.js +++ b/trade/orderCollector.js @@ -3,20 +3,27 @@ const config = require('../modules/configReader'); const log = require('../helpers/log'); const traderapi = require('./trader_' + config.exchange)(config.apikey, config.apisecret, config.apipassword, log); +/** + * Purposes: + * mm: market making order + * ob: dynamic order book order + * tb: trade bot order +*/ + module.exports = async (purposes, pair) => { // log.info(`Order collector..`); - const {ordersDb} = db; + const {ordersDb} = db; let ordersToClear = await ordersDb.find({ isProcessed: false, purpose: {$in: purposes}, pair: pair || config.pair, exchange: config.exchange - }); + }); - ordersToClear.forEach(async order => { - try { + ordersToClear.forEach(async order => { + try { traderapi.cancelOrder(order._id, order.type, order.pair); order.update({ @@ -25,17 +32,36 @@ module.exports = async (purposes, pair) => { }); await order.save(); - log.info(`Cancelling mm-order with params: id=${order._id}, type=${order.targetType}, pair=${order.pair}, price=${order.price}, coin1Amount=${order.coin1Amount}, coin2Amount=${order.coin2Amount}.`); + log.info(`Cancelling ${order.purpose}-order with params: id=${order._id}, type=${order.targetType}, pair=${order.pair}, price=${order.price}, coin1Amount=${order.coin1Amount}, coin2Amount=${order.coin2Amount}.`); - } catch (e) { - log.error('Error in orderCollector module: ' + e); - } + } catch (e) { + log.error('Error in orderCollector module: ' + e); + } }); return ordersToClear.length; }; +async function clearAllOrders() { + // First, close orders which are in bot's database + let count = await module.exports(['mm', 'tb', 'ob'], config.pair); + // Next, if need to clear all orders, close orders which are not closed yet + const openOrders = await traderapi.getOpenOrders(config.pair); + log.info(`Clearing all opened orders every ${config.clearAllOrdersInterval} minutes.. Orders to close: ${count + openOrders.length}.`); + if (openOrders) { + openOrders.forEach(order => { + traderapi.cancelOrder(order.orderid, order.side, order.symbol); + }); + } +} + setInterval(() => { module.exports(['mm', 'tb'], config.pair); }, 15 * 1000); + +if (config.clearAllOrdersInterval) { + setInterval(() => { + clearAllOrders(); + }, config.clearAllOrdersInterval * 60 * 1000); +} diff --git a/trade/resfinex_api.js b/trade/resfinex_api.js index e1dfc2f6..a00f80a9 100644 --- a/trade/resfinex_api.js +++ b/trade/resfinex_api.js @@ -185,10 +185,10 @@ var EXCHANGE_API = { * @param symbol eth_btc * ------------------------------------------------------------------ */ - orderBook: function(symbol, size = 1) { + orderBook: function(symbol, size) { let data = {}; data.pair = symbol; - data.size = size; + if (size) data.size = size; return public_api(`/engine/depth`, data); } diff --git a/trade/tradeParams_atomars.js b/trade/tradeParams_atomars.js new file mode 100644 index 00000000..350a9971 --- /dev/null +++ b/trade/tradeParams_atomars.js @@ -0,0 +1,11 @@ +module.exports = { + "mm_buyPercent": 0.67, + "mm_minInterval": 3000, + "mm_maxInterval": 350000, + "mm_isActive": false, + "mm_minAmount": 14, + "mm_maxAmount": 1600, + "mm_isOrderBookActive": true, + "mm_orderBookHeight": 25, + "mm_orderBookOrdersCount": 20 +} diff --git a/trade/tradeParams_bit-z.js b/trade/tradeParams_bit-z.js index 3f0a9201..9c7dc444 100644 --- a/trade/tradeParams_bit-z.js +++ b/trade/tradeParams_bit-z.js @@ -1,8 +1,11 @@ module.exports = { - "mm_buyPercent": 0.77, + "mm_buyPercent": 0.67, "mm_minInterval": 60000, "mm_maxInterval": 120000, "mm_isActive": false, "mm_minAmount": 1, - "mm_maxAmount": 10 + "mm_maxAmount": 10, + "mm_isOrderBookActive": true, + "mm_orderBookHeight": 14, + "mm_orderBookOrdersCount": 12 } \ No newline at end of file diff --git a/trade/tradeParams_coindeal.js b/trade/tradeParams_coindeal.js index 85614a9e..8d485483 100644 --- a/trade/tradeParams_coindeal.js +++ b/trade/tradeParams_coindeal.js @@ -1,8 +1,11 @@ module.exports = { - "mm_buyPercent": 0.8, + "mm_buyPercent": 0.67, "mm_minInterval": 60000, "mm_maxInterval": 360000, "mm_isActive": false, "mm_minAmount": 0.1, - "mm_maxAmount": 202 + "mm_maxAmount": 202, + "mm_isOrderBookActive": true, + "mm_orderBookHeight": 10, + "mm_orderBookOrdersCount": 10 } \ No newline at end of file diff --git a/trade/tradeParams_idcm.js b/trade/tradeParams_idcm.js index 5d67454f..67c9da33 100644 --- a/trade/tradeParams_idcm.js +++ b/trade/tradeParams_idcm.js @@ -1,8 +1,12 @@ module.exports = { - "mm_buyPercent": 0.77, + "mm_buyPercent": 0.67, "mm_minInterval": 60000, "mm_maxInterval": 960000, "mm_isActive": false, "mm_minAmount": 1, - "mm_maxAmount": 30 + "mm_maxAmount": 30, + "mm_isOrderBookActive": true, + "mm_orderBookHeight": 10, + "mm_orderBookOrdersCount": 10 + } \ No newline at end of file diff --git a/trade/tradeParams_resfinex.js b/trade/tradeParams_resfinex.js index 027f7be5..11e6ca0e 100644 --- a/trade/tradeParams_resfinex.js +++ b/trade/tradeParams_resfinex.js @@ -1,8 +1,11 @@ module.exports = { - "mm_buyPercent": 0.77, + "mm_buyPercent": 0.67, "mm_minInterval": 3000, "mm_maxInterval": 350000, "mm_isActive": false, "mm_minAmount": 14, - "mm_maxAmount": 1600 + "mm_maxAmount": 1600, + "mm_isOrderBookActive": true, + "mm_orderBookHeight": 19, + "mm_orderBookOrdersCount": 15 } \ No newline at end of file diff --git a/trade/trader_atomars.js b/trade/trader_atomars.js new file mode 100644 index 00000000..ffb73195 --- /dev/null +++ b/trade/trader_atomars.js @@ -0,0 +1,345 @@ +const Atomars = require('./atomars_api'); +const apiServer = 'https://api.atomars.com/v1'; +const log = require('../helpers/log'); +const $u = require('../helpers/utils'); +const {SAT} = require('../helpers/const'); + +// API endpoints: +// https://api.atomars.com/v1 + +module.exports = (apiKey, secretKey, pwd) => { + + // apiKey = username + // secretKey = password + Atomars.setConfig(apiServer, apiKey, secretKey); + + return { + getBalances(nonzero = true) { + return new Promise((resolve, reject) => { + Atomars.getUserAssets().then(function (data) { + try { + // console.log(data); + let assets = JSON.parse(data).data.list; + if (!assets) + assets = []; + let result = []; + assets.forEach(crypto => { + result.push({ + code: crypto.currency.iso3.toUpperCase(), + free: +(crypto.balance_available / SAT).toFixed(8), + freezed: +((+crypto.balance - +crypto.balance_available) / SAT).toFixed(8) + }); + }) + if (nonzero) { + result = result.filter(crypto => crypto.free || crypto.freezed); + } + // console.log(result); + resolve(result); + } catch (e) { + resolve(false); + log.warn('Error while making getBalances() request: ' + e); + }; + }); + }); + }, + getOpenOrders(pair) { + pair_ = formatPairName(pair); + return new Promise((resolve, reject) => { + Atomars.getUserNowEntrustSheet().then(function (data) { + try { + // console.log(data); + // console.log(2); + + let openOrders = JSON.parse(data).data.list; + if (!openOrders) + openOrders = []; + + let result = []; + openOrders.forEach(order => { + // console.log(order); + if (order.pair === pair_.pair) + result.push({ + orderid: order.id, + symbol: order.pair, + price: order.rate, + side: order.type, // Buy/Sell (0/1) + type: order.type_trade, // Limit/Market (0/1) + timestamp: order.time_create, + amount: order.volume, + executedamount: order.volume_done, + status: order.status, + uid: order.id, + coin2Amount: order.price, + coinFrom: pair_.coin1, + coinTo: pair_.coin2 + }); + }) + // console.log(result); + // console.log(3); + + resolve(result); + + } catch (e) { + resolve(false); + log.warn('Error while making getOpenOrders() request: ' + e); + }; + }); + }); + }, + cancelOrder(orderId) { + return new Promise((resolve, reject) => { + Atomars.cancelEntrustSheet(orderId).then(function (data) { + try { + // console.log(data); + if (JSON.parse(data).status === true) { // it may be false, 401, etc. + log.info(`Cancelling order ${orderId}..`); + resolve(true); + } else { + log.info(`Order ${orderId} not found. Unable to cancel it.`); + resolve(false); + } + } catch (e) { + resolve(false); + log.warn('Error while making cancelOrder() request: ' + e); + }; + }); + }); + }, + getRates(pair) { + pair_ = formatPairName(pair); + return new Promise((resolve, reject) => { + Atomars.ticker(pair_.pair).then(function (data) { + data = JSON.parse(data).data; + // console.log(data); + try { + Atomars.orderBook(pair_.pair).then(function (data2) { + try { + // console.log(data2); + if (data2) { + data2 = parseOrderBook(data2); + resolve({ + last: +data.last, + ask: +data2.asks[0].price, + bid: +data2.bids[0].price, + volume: +data.volume_24H, // coin1 + high: +data.high, + low: +data.low, + }); + } else { + resolve(false); + } + } catch (e) { + resolve(false); + log.warn('Error while making getRates() orderBook() request: ' + e); + }; + }); + + } catch (e) { + resolve(false); + log.warn('Error while making getRates() ticker() request: ' + e); + }; + }); + }); + }, + placeOrder(orderType, pair, price, coin1Amount, limit = 1, coin2Amount, pairObj) { + + let pair_ = formatPairName(pair); + let output = ''; + let message; + let order = {}; + + let side = (orderType === 'sell') ? 1 : 0; // Buy/Sell (0/1) + + if (!coin1Amount && coin2Amount && price) { // both LIMIT and MARKET order amount are in coin1 + coin1Amount = coin2Amount / price; + } + + if (pairObj) { // Set precision (decimals) + if (coin1Amount) { + coin1Amount = (+coin1Amount).toFixed(pairObj.coin1Decimals); + } + if (coin2Amount) { + coin2Amount = (+coin2Amount).toFixed(pairObj.coin2Decimals) + } + if (price) + price = (+price).toFixed(pairObj.coin2Decimals); + } + + if (limit) { // Limit order Limit/Market/Stop Limit/Quick Market (0/1/2/3) + output = `${orderType} ${coin1Amount} ${pair_.coin1.toUpperCase()} at ${price} ${pair_.coin2.toUpperCase()}.`; + + return new Promise((resolve, reject) => { + Atomars.addEntrustSheet(pair_.pair, coin1Amount, price, side, 0).then(function (data) { + try { + // console.log(data); + let result = JSON.parse(data); + if (result.data && result.data.id) { + message = `Order placed to ${output} Order Id: ${result.data.id}.`; + log.info(message); + order.orderid = result.data.id; + order.message = message; + resolve(order); + } else { + message = `Unable to place order to ${output} Check parameters and balances.`; + log.warn(message); + order.orderid = false; + order.message = message; + resolve(order); + } + } catch (e) { + message = 'Error while making placeOrder() request: ' + e; + log.warn(message); + order.orderid = false; + order.message = message; + resolve(order); + }; + }); + }); + + } else { // Market order Limit/Market/Stop Limit/Quick Market (0/1/2/3) + // console.log(orderType, pair, price, coin1Amount, 'market', coin2Amount, pairObj); + let size = 0; + if (orderType === 'sell') { + if (coin1Amount) { + size = coin1Amount; + output = `${orderType} ${coin1Amount} ${pair_.coin1.toUpperCase()} at Market Price on ${pair} market.`; + } else { + message = `Unable to place order to ${orderType} ${pair_.coin1.toUpperCase()} at Market Price on ${pair} market. Set ${pair_.coin1.toUpperCase()} amount.`; + log.warn(message); + order.orderid = false; + order.message = message; + return order; + } + } else { // buy + if (coin1Amount) { + size = coin1Amount; + output = `${orderType} ${coin1Amount} ${pair_.coin1.toUpperCase()} at Market Price on ${pair} market.`; + } else { + message = `Unable to place order to ${orderType} ${pair_.coin1.toUpperCase()} at Market Price on ${pair} market. Set ${pair_.coin1.toUpperCase()} amount.`; + log.warn(message); + order.orderid = false; + order.message = message; + return order; + } + } + + return new Promise((resolve, reject) => { + Atomars.addEntrustSheet(pair_.pair, size, '', side, 1).then(function (data) { + try { + // console.log(data); + let result = JSON.parse(data); + if (result.data && result.data.id) { + message = `Order placed to ${output} Order Id: ${result.data.id}.`; + log.info(message); + order.orderid = result.data.id; + order.message = message; + resolve(order); + } else { + message = `Unable to place order to ${output} Check parameters and balances.`; + log.warn(message); + order.orderid = false; + order.message = message; + resolve(order); + } + } catch (e) { + message = 'Error while making placeOrder() request: ' + e; + log.warn(message); + order.orderid = false; + order.message = message; + resolve(order); + }; + }); + }); + } + }, // placeOrder() + getOrderBook(pair) { + + let pair_ = formatPairName(pair); + return new Promise((resolve, reject) => { + Atomars.orderBook(pair_.pair).then(function (data) { + try { + // console.log(data); + resolve(parseOrderBook(data)); + } catch (e) { + resolve(false); + log.warn('Error while making orderBook() request: ' + e); + }; + }); + }); + }, + getDepositAddress(coin) { + return new Promise((resolve, reject) => { + Atomars.getDepositAddress(coin, 0).then(function (data) { + try { + // console.log(data); + const address = JSON.parse(data).data.address; + if (address) { + resolve(address); + } else { + resolve(false); + } + } catch (e) { + resolve(false); + log.warn('Error while making getDepositAddress() request: ' + e); + }; + }); + }); + + } + } +} + +function parseOrderBook(data) { + let book = JSON.parse(data).data; + if (!book) + book = []; + // console.log(book); + let result = { + bids: new Array(), + asks: new Array() + }; + book.buy.forEach(crypto => { + result.bids.push({ + amount: crypto.volume, + price: crypto.rate, + count: crypto.count, + type: 'bid-buy-left' + }); + }) + result.bids.sort(function(a, b) { + return parseFloat(b.price) - parseFloat(a.price); + }); + book.sell.forEach(crypto => { + result.asks.push({ + amount: crypto.volume, + price: crypto.rate, + count: crypto.count, + type: 'ask-sell-right' + }); + }) + result.asks.sort(function(a, b) { + return parseFloat(a.price) - parseFloat(b.price); + }); + return result; +} + +function formatPairName(pair) { + let pair_, coin1, coin2; + if (pair.indexOf('-') > -1) { + pair_ = pair.replace('-', '').toUpperCase(); + [coin1, coin2] = pair.split('-'); + } else if (pair.indexOf('_') > -1) { + pair_ = pair.replace('_', '').toUpperCase(); + [coin1, coin2] = pair.split('_'); + } else { + pair_ = pair.replace('/', '').toUpperCase(); + [coin1, coin2] = pair.split('/'); + } + + return { + pair: pair_, + coin1: coin1.toUpperCase(), + coin2: coin2.toUpperCase() + }; +} + diff --git a/trade/trader_bit-z.js b/trade/trader_bit-z.js index b9c1004e..b53f177a 100644 --- a/trade/trader_bit-z.js +++ b/trade/trader_bit-z.js @@ -151,20 +151,20 @@ module.exports = (apiKey, secretKey, pwd) => { if (pairObj) { // Set precision (decimals) if (coin1Amount) { - coin1Amount = +coin1Amount.toFixed(pairObj.coin1Decimals); + coin1Amount = (+coin1Amount).toFixed(pairObj.coin1Decimals); } if (coin2Amount) { - coin2Amount = +coin2Amount.toFixed(pairObj.coin2Decimals) + coin2Amount = (+coin2Amount).toFixed(pairObj.coin2Decimals) } if (price) - price = +price.toFixed(pairObj.coin2Decimals); + price = (+price).toFixed(pairObj.coin2Decimals); } if (limit) { // Limit order output = `${orderType} ${coin1Amount} ${pair_.coin1.toUpperCase()} at ${price} ${pair_.coin2.toUpperCase()}.`; return new Promise((resolve, reject) => { - BITZ.addEntrustSheet(pair_.pair, +coin1Amount, +price, type).then(function (data) { + BITZ.addEntrustSheet(pair_.pair, coin1Amount, price, type).then(function (data) { try { // console.log(data); let result = JSON.parse(data).data; @@ -218,7 +218,7 @@ module.exports = (apiKey, secretKey, pwd) => { } return new Promise((resolve, reject) => { - BITZ.addMarketOrder(pair_.pair, +size, type).then(function (data) { + BITZ.addMarketOrder(pair_.pair, size, type).then(function (data) { try { // console.log(data); let result = JSON.parse(data).data; @@ -247,7 +247,7 @@ module.exports = (apiKey, secretKey, pwd) => { } }, // placeOrder() getOrderBook(pair) { - // depth(symbol) + // orderBook(symbol) }, getDepositAddress(coin) { diff --git a/trade/trader_coindeal.js b/trade/trader_coindeal.js index b6543d08..d9c777d5 100644 --- a/trade/trader_coindeal.js +++ b/trade/trader_coindeal.js @@ -138,33 +138,35 @@ module.exports = (apiKey, secretKey, pwd) => { let type = (orderType === 'sell') ? 'sell' : 'buy'; - if (pairObj) { // Set precision (decimals) + if (pairObj) { // Set precision (decimals) if (coin1Amount) { - coin1Amount = +coin1Amount.toFixed(pairObj.coin1Decimals); + coin1Amount = (+coin1Amount).toFixed(pairObj.coin1Decimals); } if (coin2Amount) { - coin2Amount = +coin2Amount.toFixed(pairObj.coin2Decimals) + coin2Amount = (+coin2Amount).toFixed(pairObj.coin2Decimals) } if (price) - price = +price.toFixed(pairObj.coin2Decimals); + price = (+price).toFixed(pairObj.coin2Decimals); } if (limit) { // Limit order output = `${orderType} ${coin1Amount} ${pair_.coin1.toUpperCase()} at ${price} ${pair_.coin2.toUpperCase()}.`; return new Promise((resolve, reject) => { - COINDEAL.addEntrustSheet(pair_.pair, +coin1Amount, +price, type).then(function (data) { + COINDEAL.addEntrustSheet(pair_.pair, coin1Amount, price, type).then(function (data) { try { // console.log(data); let result = JSON.parse(data); - if (result) { + if (result && result.id) { message = `Order placed to ${output} Order Id: ${result.id}.`; log.info(message); order.orderid = result.id; order.message = message; resolve(order); } else { - message = `Unable to place order to ${output} Check parameters and balances.`; + message = `Unable to place order to ${output} Check parameters and balances. Description: ${result.message}`; + if (result.errors && result.errors.errors) + message += `: ${result.errors.errors.join(', ')}`; log.warn(message); order.orderid = false; order.message = message; diff --git a/trade/trader_idcm.js b/trade/trader_idcm.js index 1dc74c9c..585f63e5 100644 --- a/trade/trader_idcm.js +++ b/trade/trader_idcm.js @@ -68,13 +68,13 @@ module.exports = (PubK, PrivK) => { if (pairObj) { // Set precision (decimals) if (coin1Amount) { - coin1Amount = +coin1Amount.toFixed(pairObj.coin1Decimals); + coin1Amount = (+coin1Amount).toFixed(pairObj.coin1Decimals); } if (coin2Amount) { - coin2Amount = +coin2Amount.toFixed(pairObj.coin2Decimals) + coin2Amount = (+coin2Amount).toFixed(pairObj.coin2Decimals) } if (price) - price = +price.toFixed(pairObj.coin2Decimals); + price = (+price).toFixed(pairObj.coin2Decimals); } if (limit) { // Limit order diff --git a/trade/trader_resfinex.js b/trade/trader_resfinex.js index 4f720cc4..86ebab21 100644 --- a/trade/trader_resfinex.js +++ b/trade/trader_resfinex.js @@ -109,7 +109,7 @@ module.exports = (apiKey, secretKey, pwd) => { data = data.filter(symbol => symbol.pair === pair_.pair)[0]; // console.log(data); try { - RESFINEX.orderBook(pair_.pair).then(function (data2) { + RESFINEX.orderBook(pair_.pair, 1).then(function (data2) { data2 = JSON.parse(data2).data; // console.log(data2); try { @@ -153,20 +153,20 @@ module.exports = (apiKey, secretKey, pwd) => { if (pairObj) { // Set precision (decimals) if (coin1Amount) { - coin1Amount = +coin1Amount.toFixed(pairObj.coin1Decimals); + coin1Amount = (+coin1Amount).toFixed(pairObj.coin1Decimals); } if (coin2Amount) { - coin2Amount = +coin2Amount.toFixed(pairObj.coin2Decimals) + coin2Amount = (+coin2Amount).toFixed(pairObj.coin2Decimals) } if (price) - price = +price.toFixed(pairObj.coin2Decimals); + price = (+price).toFixed(pairObj.coin2Decimals); } if (limit) { // Limit order output = `${orderType} ${coin1Amount} ${pair_.coin1.toUpperCase()} at ${price} ${pair_.coin2.toUpperCase()}.`; return new Promise((resolve, reject) => { - RESFINEX.addEntrustSheet(pair_.pair, +coin1Amount, +price, side, 'LIMIT').then(function (data) { + RESFINEX.addEntrustSheet(pair_.pair, coin1Amount, price, side, 'LIMIT').then(function (data) { try { // console.log(data); let result = JSON.parse(data); @@ -220,7 +220,7 @@ module.exports = (apiKey, secretKey, pwd) => { } return new Promise((resolve, reject) => { - RESFINEX.addEntrustSheet(pair_.pair, +coin1Amount, '', side, 'MARKET').then(function (data) { + RESFINEX.addEntrustSheet(pair_.pair, coin1Amount, '', side, 'MARKET').then(function (data) { try { // console.log(data); let result = JSON.parse(data); @@ -247,7 +247,54 @@ module.exports = (apiKey, secretKey, pwd) => { }); }); } - } // placeOrder() + }, // placeOrder() + getOrderBook(pair) { + + let pair_ = formatPairName(pair); + return new Promise((resolve, reject) => { + RESFINEX.orderBook(pair_.pair).then(function (data) { + try { + // console.log(data); + let book = JSON.parse(data).data; + if (!book) + book = []; + let result = { + bids: new Array(), + asks: new Array() + }; + book.asks.forEach(crypto => { + result.asks.push({ + amount: crypto.amount, + price: crypto.price, + count: 1, + type: 'ask-sell-right' + }); + }) + result.asks.sort(function(a, b) { + return parseFloat(a.price) - parseFloat(b.price); + }); + book.bids.forEach(crypto => { + result.bids.push({ + amount: crypto.amount, + price: crypto.price, + count: 1, + type: 'bid-buy-left' + }); + }) + result.bids.sort(function(a, b) { + return parseFloat(b.price) - parseFloat(a.price); + }); + resolve(result); + } catch (e) { + resolve(false); + log.warn('Error while making orderBook() request: ' + e); + }; + }); + }); + }, + getDepositAddress(coin) { + // Not available for Resfinex + } } }