From fe3948f11e303ca0094810857a0388f151fc0ac7 Mon Sep 17 00:00:00 2001 From: emabo <3024205+emabo@users.noreply.github.com> Date: Sat, 11 Apr 2020 01:59:49 +0200 Subject: [PATCH] Notifier interaction: add events to notifiers, possibly fix #1550 (#2070) * stale[bot] (#1744) * fix: upgrade lint-staged from 10.1.1 to 10.1.2 (#2063) Snyk has created this PR to upgrade lint-staged from 10.1.1 to 10.1.2. See this package in NPM: https://www.npmjs.com/package/lint-staged See this project in Snyk: https://app.snyk.io/org/deviavir/project/14e19887-e219-40d4-89b6-6c657bf78942?utm_source=github&utm_medium=upgrade-pr * fix: upgrade ccxt from 1.25.80 to 1.25.81 (#2064) Snyk has created this PR to upgrade ccxt from 1.25.80 to 1.25.81. See this package in NPM: https://www.npmjs.com/package/ccxt See this project in Snyk: https://app.snyk.io/org/deviavir/project/14e19887-e219-40d4-89b6-6c657bf78942?utm_source=github&utm_medium=upgrade-pr * Exchanges: update-products 5838 * fix: upgrade css-loader from 3.4.2 to 3.5.0 (#2067) Snyk has created this PR to upgrade css-loader from 3.4.2 to 3.5.0. See this package in NPM: https://www.npmjs.com/package/css-loader See this project in Snyk: https://app.snyk.io/org/deviavir/project/14e19887-e219-40d4-89b6-6c657bf78942?utm_source=github&utm_medium=upgrade-pr * fix: upgrade ccxt from 1.25.81 to 1.25.83 (#2066) Snyk has created this PR to upgrade ccxt from 1.25.81 to 1.25.83. See this package in NPM: https://www.npmjs.com/package/ccxt See this project in Snyk: https://app.snyk.io/org/deviavir/project/14e19887-e219-40d4-89b6-6c657bf78942?utm_source=github&utm_medium=upgrade-pr * fix: upgrade semver from 7.1.3 to 7.2.1 (#2065) Snyk has created this PR to upgrade semver from 7.1.3 to 7.2.1. See this package in NPM: https://www.npmjs.com/package/semver See this project in Snyk: https://app.snyk.io/org/deviavir/project/14e19887-e219-40d4-89b6-6c657bf78942?utm_source=github&utm_medium=upgrade-pr * Exchanges: update-products 5852 * fix: upgrade css-loader from 3.5.0 to 3.5.1 (#2069) Snyk has created this PR to upgrade css-loader from 3.5.0 to 3.5.1. See this package in NPM: https://www.npmjs.com/package/css-loader See this project in Snyk: https://app.snyk.io/org/deviavir/project/14e19887-e219-40d4-89b6-6c657bf78942?utm_source=github&utm_medium=upgrade-pr * fix: upgrade ccxt from 1.25.83 to 1.25.86 (#2068) Snyk has created this PR to upgrade ccxt from 1.25.83 to 1.25.86. See this package in NPM: https://www.npmjs.com/package/ccxt See this project in Snyk: https://app.snyk.io/org/deviavir/project/14e19887-e219-40d4-89b6-6c657bf78942?utm_source=github&utm_medium=upgrade-pr * Exchanges: update-products 5862 * Notifier interaction: add events to notifiers in order to receive commands also from a connected notifier. Implemented only for Telegram at the moment. - list of accepted commands is the same used in command line - check the correct origin of the message looking at chat ID - accept the flag 'c.notifiers.telegram.interactive' (default = false) to enable/disable this feature This commit provides a possible solution for feature request #1550 Co-authored-by: Chase Co-authored-by: Snyk bot Co-authored-by: Travis CI --- commands/trade.js | 167 +++++++++++++++++-------------- conf-sample.js | 1 + extensions/notifiers/telegram.js | 24 ++++- lib/engine.js | 8 ++ lib/notify.js | 20 +++- 5 files changed, 143 insertions(+), 77 deletions(-) diff --git a/commands/trade.js b/commands/trade.js index b2aea91532..a24731570d 100644 --- a/commands/trade.js +++ b/commands/trade.js @@ -120,47 +120,44 @@ module.exports = function (program, conf) { keyMap.set('d', 'dump statistical output to HTML file'.grey) keyMap.set('D', 'toggle automatic HTML dump to file'.grey) + var pushStr = '' + function listKeys() { - console.log('\nAvailable command keys:') + printLog('Available command keys:', true) keyMap.forEach((value, key) => { - console.log(' ' + key + ' - ' + value) + printLog(' ' + key + ' - ' + value) }) } function listOptions () { - console.log() - console.log(s.exchange.name.toUpperCase() + ' exchange active trading options:'.grey) - console.log() - process.stdout.write(z(22, 'STRATEGY'.grey, ' ') + '\t' + so.strategy + '\t' + (require(`../extensions/strategies/${so.strategy}/strategy`).description).grey) - console.log('\n') - process.stdout.write([ + printLog(s.exchange.name.toUpperCase() + ' exchange active trading options:'.grey, true) + printLog(z(22, 'STRATEGY'.grey, ' ') + '\t' + so.strategy + '\t' + (require(`../extensions/strategies/${so.strategy}/strategy`).description).grey, true) + printLog([ z(24, (so.mode === 'paper' ? so.mode.toUpperCase() : so.mode.toUpperCase()) + ' MODE'.grey, ' '), z(26, 'PERIOD'.grey, ' '), z(30, 'ORDER TYPE'.grey, ' '), z(28, 'SLIPPAGE'.grey, ' '), z(33, 'EXCHANGE FEES'.grey, ' ') - ].join('') + '\n') - process.stdout.write([ + ].join(''), true) + printLog([ z(15, (so.mode === 'paper' ? ' ' : (so.mode === 'live' && (so.manual === false || typeof so.manual === 'undefined')) ? ' ' + 'AUTO'.black.bgRed + ' ' : ' ' + 'MANUAL'.black.bgGreen + ' '), ' '), z(13, so.period_length, ' '), z(29, (so.order_type === 'maker' ? so.order_type.toUpperCase().green : so.order_type.toUpperCase().red), ' '), z(31, (so.mode === 'paper' ? 'avg. '.grey + so.avg_slippage_pct + '%' : 'max '.grey + so.max_slippage_pct + '%'), ' '), z(20, (so.order_type === 'maker' ? so.order_type + ' ' + s.exchange.makerFee : so.order_type + ' ' + s.exchange.takerFee), ' ') - ].join('') + '\n') - process.stdout.write('') - process.stdout.write([ + ].join('')) + printLog([ z(19, 'BUY %'.grey, ' '), z(20, 'SELL %'.grey, ' '), z(35, 'TRAILING STOP %'.grey, ' '), z(33, 'TRAILING DISTANCE %'.grey, ' ') - ].join('') + '\n') - process.stdout.write([ + ].join('')) + printLog([ z(9, so.buy_pct + '%', ' '), z(9, so.sell_pct + '%', ' '), z(20, so.profit_stop_enable_pct + '%', ' '), z(20, so.profit_stop_pct + '%', ' ') - ].join('') + '\n') - process.stdout.write('') + ].join('')) } /* Implementing statistical Exit */ @@ -226,7 +223,7 @@ module.exports = function (program, conf) { } if (!statsonly) { output_lines.forEach(function (line) { - console.log(line) + printLog(line) }) } if (quit || dump) { @@ -275,9 +272,9 @@ module.exports = function (program, conf) { function toggleStats(){ shouldSaveStats = !shouldSaveStats if(shouldSaveStats) - console.log('Auto stats dump enabled') + printLog('Auto stats dump enabled') else - console.log('Auto stats dump disabled') + printLog('Auto stats dump disabled') } function saveStatsLoop(){ @@ -368,6 +365,79 @@ module.exports = function (program, conf) { } + function printLog(str, cr = false) { + if (str) { + console.log((cr?'\n':'') + str) + pushStr += str + '\n' + } + } + + function executeCommand(command) { + var info = { ctrl: false } + if (conf.debug) { + console.log('\nCommand received: ' + command) + } + executeKey(command, info) + } + + function executeKey(key, info) { + if (key === 'l') { + listKeys() + } else if (key === 'b' && !info.ctrl ) { + engine.executeSignal('buy') + printLog('manual'.grey + ' limit ' + 'BUY'.green + ' command executed'.grey, true) + } else if (key === 'B' && !info.ctrl) { + engine.executeSignal('buy', null, null, false, true) + printLog('manual'.grey + ' market ' + 'BUY'.green + ' command executed'.grey, true) + } else if (key === 's' && !info.ctrl) { + engine.executeSignal('sell') + printLog('manual'.grey + ' limit ' + 'SELL'.red + ' command executed'.grey, true) + } else if (key === 'S' && !info.ctrl) { + engine.executeSignal('sell', null, null, false, true) + printLog('manual'.grey + ' market ' + 'SELL'.red + ' command executed'.grey, true) + } else if ((key === 'c' || key === 'C') && !info.ctrl) { + delete s.buy_order + delete s.sell_order + printLog('manual'.grey + ' order cancel' + ' command executed'.grey, true) + } else if (key === 'm' && !info.ctrl && so.mode === 'live') { + so.manual = !so.manual + printLog('MANUAL trade in LIVE mode: ' + (so.manual ? 'ON'.green.inverse : 'OFF'.red.inverse), true) + } else if (key === 'T' && !info.ctrl) { + so.order_type = 'taker' + printLog('Taker fees activated'.bgRed, true) + } else if (key === 'M' && !info.ctrl) { + so.order_type = 'maker' + printLog('Maker fees activated'.black.bgGreen, true) + } else if (key === 'o' && !info.ctrl) { + listOptions() + } else if (key === 'O' && !info.ctrl) { + printLog(cliff.inspect(so), true) + } else if (key === 'P' && !info.ctrl) { + printLog('Writing statistics...'.grey, true) + printTrade(false) + } else if (key === 'X' && !info.ctrl) { + printLog('Exiting... ' + '\nWriting statistics...'.grey, true) + printTrade(true) + } else if (key === 'd' && !info.ctrl) { + printLog('Dumping statistics...'.grey, true) + printTrade(false, true) + } else if (key === 'D' && !info.ctrl) { + printLog('Dumping statistics...'.grey, true) + toggleStats() + } else if (key === 'L' && !info.ctrl) { + debug.flip() + printLog('DEBUG mode: ' + (debug.on ? 'ON'.green.inverse : 'OFF'.red.inverse), true) + } else if (info.name === 'c' && info.ctrl) { + // @todo: cancel open orders before exit + process.exit() + } + + if (pushStr) { + engine.pushMessage('Reply', colors.stripColors(pushStr)) + pushStr = '' + } + } + var order_types = ['maker', 'taker'] if (!order_types.includes(so.order_type)) { so.order_type = 'maker' @@ -483,62 +553,13 @@ module.exports = function (program, conf) { forwardScan() setInterval(forwardScan, so.poll_trades) + if (!so.non_interactive) { + engine.onMessage(executeCommand) + } readline.emitKeypressEvents(process.stdin) if (!so.non_interactive && process.stdin.setRawMode) { process.stdin.setRawMode(true) - process.stdin.on('keypress', function (key, info) { - if (key === 'l') { - listKeys() - } else if (key === 'b' && !info.ctrl ) { - engine.executeSignal('buy') - console.log('\nmanual'.grey + ' limit ' + 'BUY'.green + ' command executed'.grey) - } else if (key === 'B' && !info.ctrl) { - engine.executeSignal('buy', null, null, false, true) - console.log('\nmanual'.grey + ' market ' + 'BUY'.green + ' command executed'.grey) - } else if (key === 's' && !info.ctrl) { - engine.executeSignal('sell') - console.log('\nmanual'.grey + ' limit ' + 'SELL'.red + ' command executed'.grey) - } else if (key === 'S' && !info.ctrl) { - engine.executeSignal('sell', null, null, false, true) - console.log('\nmanual'.grey + ' market ' + 'SELL'.red + ' command executed'.grey) - } else if ((key === 'c' || key === 'C') && !info.ctrl) { - delete s.buy_order - delete s.sell_order - console.log('\nmanual'.grey + ' order cancel' + ' command executed'.grey) - } else if (key === 'm' && !info.ctrl && so.mode === 'live') { - so.manual = !so.manual - console.log('\nMANUAL trade in LIVE mode: ' + (so.manual ? 'ON'.green.inverse : 'OFF'.red.inverse)) - } else if (key === 'T' && !info.ctrl) { - so.order_type = 'taker' - console.log('\n' + 'Taker fees activated'.bgRed) - } else if (key === 'M' && !info.ctrl) { - so.order_type = 'maker' - console.log('\n' + 'Maker fees activated'.black.bgGreen) - } else if (key === 'o' && !info.ctrl) { - listOptions() - } else if (key === 'O' && !info.ctrl) { - console.log('\n' + cliff.inspect(so)) - } else if (key === 'P' && !info.ctrl) { - console.log('\nWriting statistics...'.grey) - printTrade(false) - } else if (key === 'X' && !info.ctrl) { - console.log('\nExiting... ' + '\nWriting statistics...'.grey) - printTrade(true) - } else if (key === 'd' && !info.ctrl) { - console.log('\nDumping statistics...'.grey) - printTrade(false, true) - } else if (key === 'D' && !info.ctrl) { - console.log('\nDumping statistics...'.grey) - toggleStats() - } else if (key === 'L' && !info.ctrl) { - debug.flip() - console.log('\nDEBUG mode: ' + (debug.on ? 'ON'.green.inverse : 'OFF'.red.inverse)) - } else if (info.name === 'c' && info.ctrl) { - // @todo: cancel open orders before exit - console.log() - process.exit() - } - }) + process.stdin.on('keypress', executeKey) } }) }) diff --git a/conf-sample.js b/conf-sample.js index e44f4ed04b..eaf9034fa6 100644 --- a/conf-sample.js +++ b/conf-sample.js @@ -262,6 +262,7 @@ c.notifiers.pushover.priority = '0' // choose a priority to send zenbot messages // telegram configs c.notifiers.telegram = {} c.notifiers.telegram.on = false // false telegram disabled; true telegram enabled (key should be correct) +c.notifiers.telegram.interactive = false // true telegram is interactive c.notifiers.telegram.bot_token = 'YOUR-BOT-TOKEN' c.notifiers.telegram.chat_id = 'YOUR-CHAT-ID' // the id of the chat the messages should be send in // end telegram configs diff --git a/extensions/notifiers/telegram.js b/extensions/notifiers/telegram.js index a37ed834a5..068e985d56 100644 --- a/extensions/notifiers/telegram.js +++ b/extensions/notifiers/telegram.js @@ -3,14 +3,34 @@ process.env['NTBA_FIX_319'] = 1 var TelegramBot = require('node-telegram-bot-api') module.exports = function telegram (config) { + var bot = new TelegramBot(config.bot_token, { polling: true }) + var wrapper = function(cb) { + return function(message) { + if (message.chat.id != config.chat_id) { + console.log('\nChat ID error: command coming from wrong chat: ' + message.chat.id) + return + } + cb(message.text) + } + } var telegram = { pushMessage: function(title, message) { - var bot = new TelegramBot(config.bot_token) - bot.sendMessage(config.chat_id, title + ': ' + message).catch(function (error) { console.error('\nerror: telegram notification') console.log(error.response.body) // => { ok: false, error_code: 400, description: 'Bad Request: chat not found' } }) + }, + onMessage: function (callback) { + bot.on('message', wrapper(callback)) + bot.on('webhook_error', (error) => { + console.log('\nwebhook error: telegram event ' + error.code) + }) + bot.on('polling_error', (error) => { + console.log('\npolling error: telegram event ' + error.code) + }) + bot.on('error', (error) => { + console.log('\nerror: telegram event ' + error.code) + }) } } return telegram diff --git a/lib/engine.js b/lib/engine.js index 85b6d6ccd2..1fc6376575 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -119,6 +119,12 @@ module.exports = function (s, conf) { } } + function onMessage(callback) { + if (so.mode === 'live' || so.mode === 'paper') { + notifier.onMessage(callback) + } + } + function isFiat() { return !s.currency.match(/^BTC|ETH|XMR|USDT$/) } @@ -996,6 +1002,8 @@ module.exports = function (s, conf) { executeSignal: executeSignal, writeReport: writeReport, syncBalance: syncBalance, + pushMessage: pushMessage, + onMessage: onMessage, } } diff --git a/lib/notify.js b/lib/notify.js index 1e1f81eab5..e0f0ce3140 100644 --- a/lib/notify.js +++ b/lib/notify.js @@ -1,8 +1,16 @@ module.exports = function notifier (conf) { var active_notifiers = [] + var interactive_notifiers = [] + for (var notifier in conf.notifiers) { if (conf.notifiers[notifier].on) { - active_notifiers.push(require(`../extensions/notifiers/${notifier}`)(conf.notifiers[notifier])) + var notif = require(`../extensions/notifiers/${notifier}`)(conf.notifiers[notifier]) + notif.notifier_name = notifier + + active_notifiers.push(notif) + if (conf.notifiers[notifier].interactive) { + interactive_notifiers.push(notif) + } } } @@ -14,10 +22,18 @@ module.exports = function notifier (conf) { active_notifiers.forEach((notifier) => { if (conf.debug) { - console.log(`Sending push message via ${notifier}`) + console.log(`Sending push message via ${notifier.notifier_name}`) } notifier.pushMessage(title, message) }) + }, + onMessage: function (callback) { + interactive_notifiers.forEach((notifier) => { + if (conf.debug) { + console.log(`Receiving message from ${notifier.notifier_name}`) + } + notifier.onMessage(callback) + }) } } }