diff --git a/core/log.js b/core/log.js index cfd13fe50..bfbfe975a 100644 --- a/core/log.js +++ b/core/log.js @@ -23,6 +23,7 @@ var sendToParent = function() { error: send('error'), warn: send('warn'), info: send('info'), + status: send('status'), write: send('write') } } @@ -35,18 +36,39 @@ var Log = function() { this.output = console; else if(this.env === 'child-process') this.output = sendToParent(); + + this.remoteLoggers = []; }; Log.prototype = { _write: function(method, args, name) { + + var config = util.getConfig(); + var silent = config.silent; + + if(silent) + return; + if(!name) name = method.toUpperCase(); var message = moment().format('YYYY-MM-DD HH:mm:ss'); message += ' (' + name + '):\t'; - message += fmt.apply(null, args); + var rawMessage = fmt.apply(null, args); + message += rawMessage; - this.output[method](message); + if (method == 'remote') + { + // mirror remote output to info + this.output['info'](message); + _.each(this.remoteLoggers, function(logger) { + logger.logRemote(rawMessage); + }); + } + else + { + this.output[method](message); + } }, error: function() { this._write('error', arguments); @@ -57,10 +79,16 @@ Log.prototype = { info: function() { this._write('info', arguments); }, + remote: function() { + this._write('remote', arguments); + }, write: function() { var args = _.toArray(arguments); var message = fmt.apply(null, args); this.output.info(message); + }, + addRemoteLogger: function(logger) { + this.remoteLoggers.push(logger); } } @@ -77,6 +105,7 @@ if(silent) { Log.prototype.warn = _.noop; Log.prototype.error = _.noop; Log.prototype.write = _.noop; + Log.prototype.remote = _.noop; } module.exports = new Log; \ No newline at end of file diff --git a/plugins.js b/plugins.js index af5f8dca2..1b256dd5b 100644 --- a/plugins.js +++ b/plugins.js @@ -58,6 +58,7 @@ var plugins = [ slug: 'telegrambot', async: false, modes: ['realtime'], + emits: ['command'], dependencies: [{ module: 'node-telegram-bot-api', version: '0.24.0' diff --git a/plugins/telegrambot.js b/plugins/telegrambot.js index 058fbf673..377e897de 100644 --- a/plugins/telegrambot.js +++ b/plugins/telegrambot.js @@ -1,158 +1,103 @@ var log = require('../core/log'); var moment = require('moment'); var _ = require('lodash'); -var config = require('../core/util').getConfig(); -var telegrambot = config.telegrambot; +var util = require('../core/util'); +var config = util.getConfig(); var utc = moment.utc; -var telegram = require("node-telegram-bot-api"); +var telegramfancy = require("tgfancy"); -var Actor = function() { +var Actor = function () { _.bindAll(this); - this.advice = 'Dont got one yet :('; - this.adviceTime = utc(); - - this.price = 'Dont know yet :('; - this.priceTime = utc(); - - this.commands = { - '/advice': 'emitAdvice', - '/price': 'emitPrice', - '/donate': 'emitDonation', - '/realadvice': 'emitRealAdvice', //Without space - '/help': 'emitHelp' - }; - - this.rawCommands = _.keys(this.commands); - this.chatId = null; - this.bot = new telegram(telegrambot.token, { polling: true }); + this.bot = new telegramfancy(config.telegrambot.token, { polling: true }); this.bot.onText(/(.+)/, this.verifyQuestion); -} - -Actor.prototype.processCandle = function(candle, done) { - this.price = candle.close; - this.priceTime = candle.start; - - done(); -}; - -Actor.prototype.processAdvice = function(advice) { - if (telegrambot.muteSoft && advice.recommendation === 'soft') return; - this.advice = advice.recommendation; - this.adviceTime = utc(); - if (telegrambot.emitUpdates) - this.newAdvice(); -}; - -Actor.prototype.verifyQuestion = function(msg, text) { - this.chatId = msg.chat.id; - if (text[1].toLowerCase() in this.commands) - this[this.commands[text[1].toLowerCase()]](); - else - this.bot.sendMessage(this.chatId, "Hello!"); + log.addRemoteLogger(this); } -Actor.prototype.newAdvice = function() { +util.makeEventEmitter(Actor); + +Actor.prototype.processAdvice = function (advice) { if (this.chatId) { - this.bot.sendMessage(this.chatId, 'Important news!'); - this.emitAdvice(); + var message; + if (this.advice == 'long') { + message = "BUY order received, sending to exchange"; + } + else { + message = "SELL order received, sending to exchange"; + } + + this.bot.sendMessage(this.chatId, message); } } -// sent advice to the last chat -Actor.prototype.emitAdvice = function() { - var message = [ - 'Advice for ', - config.watch.exchange, - ' ', - config.watch.currency, - '/', - config.watch.asset, - ' using ', - config.tradingAdvisor.method, - ' at ', - config.tradingAdvisor.candleSize, - ' minute candles, is:\n', - this.advice, - ' ', - config.watch.asset, - ' (from ', - this.adviceTime.fromNow(), - ')' - ].join(''); - - if (this.chatId) - this.bot.sendMessage(this.chatId, message); -}; - -// sent price over to the last chat -Actor.prototype.emitPrice = function() { - var message = [ - 'Current price at ', - config.watch.exchange, - ' ', - config.watch.currency, - '/', - config.watch.asset, - ' is ', - this.price, - ' ', - config.watch.currency, - ' (from ', - this.priceTime.fromNow(), - ')' - ].join(''); - - if (this.chatId) - this.bot.sendMessage(this.chatId, message); -}; +Actor.prototype.processTrade = function (trade) { + //{ + // action: [either "buy" or "sell"], + // price: [number, price that was sold at], + // date: [moment object, exchange time trade completed at], + // portfolio: [object containing amount in currency and asset], + // balance: [number, total worth of portfolio] + //} -// sent donation info over to the IRC channel -Actor.prototype.emitDonation = function() { - var message = 'You want to donate? How nice of you! You can send your coins here:'; - message += '\nBTC:\t13r1jyivitShUiv9FJvjLH7Nh1ZZptumwW'; - - if (this.chatId) - this.bot.sendMessage(this.chatId, message); -}; + if (this.chatId) { + this.bot.sendMessage(this.chatId, "Trade completed!"); + this.bot.sendMessage(this.chatId, trade.date.toDate() + ": " + trade.action + " at " + trade.price.toFixed(2)); + // emit portfolio command to get results of trade + this.emit('command', { + command: 'portfolio', + arguments: [null], + handled: false, + response: null + }); + } +} -Actor.prototype.emitHelp = function() { - var message = _.reduce( - this.rawCommands, - function(message, command) { - return message + ' ' + command + ','; - }, - 'possible commands are:' - ); +Actor.prototype.verifyQuestion = function (msg, text) { + this.chatId = msg.chat.id; - message = message.substr(0, _.size(message) - 1) + '.'; + // simple parsing that supports a command and single argument + var tokens = text[1].split(" "); - if (this.chatId) - this.bot.sendMessage(this.chatId, message); + if (tokens.length == 1 || tokens.length == 2) { + var command = tokens[0].toLowerCase(); + var arg = tokens.length == 2 ? tokens[1].toLowerCase() : null; + this.emitCommand(command, arg); + } + else { + this.bot.sendMessage(this.chatId, "'" + text[1] + "' - syntax error"); + } } -Actor.prototype.emitRealAdvice = function() { - // http://www.examiner.com/article/uncaged-a-look-at-the-top-10-quotes-of-gordon-gekko - // http://elitedaily.com/money/memorable-gordon-gekko-quotes/ - var realAdvice = [ - 'I don\'t throw darts at a board. I bet on sure things. Read Sun-tzu, The Art of War. Every battle is won before it is ever fought.', - 'Ever wonder why fund managers can\'t beat the S&P 500? \'Cause they\'re sheep, and sheep get slaughtered.', - 'If you\'re not inside, you\'re outside!', - 'The most valuable commodity I know of is information.', - 'It\'s not a question of enough, pal. It\'s a zero sum game, somebody wins, somebody loses. Money itself isn\'t lost or made, it\'s simply transferred from one perception to another.', - 'What\'s worth doing is worth doing for money. (Wait, wasn\'t I a free and open source bot?)', - 'When I get a hold of the son of a bitch who leaked this, I\'m gonna tear his eyeballs out and I\'m gonna suck his fucking skull.' - ]; - - if (this.chatId) - this.bot.sendMessage(this.chatId, _.first(_.shuffle(realAdvice))); +Actor.prototype.emitCommand = function(command, arg) { + var cmd = { + command: command.replace('/',''), + arguments: [arg], + handled: false, + response: null + }; + + this.emit('command', cmd); + if (cmd.handled) { + if (cmd.response) { + this.bot.sendMessage(this.chatId, cmd.response); + } + } + else { + this.bot.sendMessage(this.chatId, "'" + cmd.command + "' - unrecognised command"); + } } -Actor.prototype.logError = function(message) { +Actor.prototype.logError = function (message) { log.error('Telegram ERROR:', message); }; +Actor.prototype.logRemote = function (message) { + if (this.chatId) { + this.bot.sendMessage(this.chatId, message); + } +} + module.exports = Actor; diff --git a/plugins/trader/portfolioManager.js b/plugins/trader/portfolioManager.js index ef7ebf296..0a7de5f38 100644 --- a/plugins/trader/portfolioManager.js +++ b/plugins/trader/portfolioManager.js @@ -11,12 +11,14 @@ var _ = require('lodash'); var util = require('../../core/util'); +var config = util.getConfig(); var dirs = util.dirs(); var events = require('events'); var log = require(dirs.core + 'log'); var async = require('async'); var checker = require(dirs.core + 'exchangeChecker.js'); var moment = require('moment'); +var utc = moment.utc; var Manager = function(conf) { _.bindAll(this); @@ -36,6 +38,9 @@ var Manager = function(conf) { this.fee; this.action; + this.candlePrice = 0; + this.candlePriceTime = utc(); + this.marketConfig = _.find(this.exchangeMeta.markets, function(p) { return _.first(p.pair) === conf.currency.toUpperCase() && _.last(p.pair) === conf.asset.toUpperCase(); }); @@ -74,6 +79,12 @@ Manager.prototype.init = function(callback) { this.setPortfolio, this.setFee ], _.bind(prepare, this)); + +} + +Manager.prototype.getPortfolio = function() +{ + return this.portfolio; } Manager.prototype.setPortfolio = function(callback) { @@ -394,4 +405,53 @@ Manager.prototype.logPortfolio = function() { }); }; +Manager.prototype.processCandle = function(candle) { + this.candlePrice = candle.close; + this.candlePriceTime = candle.start; +} + +Manager.prototype.processCommand = function (cmd) { + + if (cmd.command == 'portfolio') { + cmd.handled = true; + + this.setTicker(() => { + var message = "Portfolio:\n"; + var value = 0.0; + var price = this.ticker.bid; + _.each(this.portfolio, function (fund) { + var isAsset = fund.name == config.watch.asset; + var amount = parseFloat(fund.amount); + message += fund.name + ': ' + amount.toFixed(isAsset ? 6 : 2) + "\n"; + if (isAsset) { + value += amount * price; + } else { + value += amount; + } + }.bind(this)); + + message += "\nTotal value: " + value.toFixed(2); + log.remote(message); + + }); + } + else if (cmd.command == 'price') { + cmd.handled = true; + + var logPrice = function() { + var message = ['Current price at ', config.watch.exchange, ' ', + config.watch.currency, '/', config.watch.asset, ' is ', + this.ticker.bid, ' ', config.watch.currency, ' (bid) ', + this.ticker.ask, ' ', config.watch.currency, ' (ask) ', + this.candlePrice, ' ', config.watch.currency, ' (candle close, ', this.candlePriceTime.fromNow(), + ')' + ].join(''); + + log.remote(message); + }.bind(this); + + this.setTicker(logPrice); + } +} + module.exports = Manager; diff --git a/plugins/trader/trader.js b/plugins/trader/trader.js index 85a9962d3..d089a695d 100644 --- a/plugins/trader/trader.js +++ b/plugins/trader/trader.js @@ -32,7 +32,11 @@ var Trader = function(next) { // teach our trader events util.makeEventEmitter(Trader); -Trader.prototype.processCandle = (candle, done) => done(); +Trader.prototype.processCandle = function(candle, done) +{ + this.manager.processCandle(candle); + done(); +} Trader.prototype.processAdvice = function(advice) { if(advice.recommendation == 'long') { @@ -52,4 +56,8 @@ Trader.prototype.processAdvice = function(advice) { } } +Trader.prototype.processCommand = function(command) { + this.manager.processCommand(command); +} + module.exports = Trader; diff --git a/plugins/tradingAdvisor/baseTradingMethod.js b/plugins/tradingAdvisor/baseTradingMethod.js index 7dca2866c..e882afc2d 100644 --- a/plugins/tradingAdvisor/baseTradingMethod.js +++ b/plugins/tradingAdvisor/baseTradingMethod.js @@ -83,6 +83,9 @@ var Base = function(settings) { if(!this.onTrade) this.onTrade = function() {}; + if(!this.onCommand) + this.onCommand = function() {}; + // let's run the implemented starting point this.init(); @@ -262,6 +265,10 @@ Base.prototype.processTrade = function(trade) { this.onTrade(trade); } +Base.prototype.processCommand = function(command) { + this.onCommand(command); +} + Base.prototype.addTalibIndicator = function(name, type, parameters) { if(!talib) util.die('Talib is not enabled'); diff --git a/plugins/tradingAdvisor/tradingAdvisor.js b/plugins/tradingAdvisor/tradingAdvisor.js index 7d7597993..c468b98f5 100644 --- a/plugins/tradingAdvisor/tradingAdvisor.js +++ b/plugins/tradingAdvisor/tradingAdvisor.js @@ -86,6 +86,10 @@ Actor.prototype.processTrade = function(trade) { this.method.processTrade(trade); } +Actor.prototype.processCommand = function(command) { + this.method.processCommand(command); +} + // pass through shutdown handler Actor.prototype.finish = function(done) { this.method.finish(done); diff --git a/strategies/commands.js b/strategies/commands.js new file mode 100644 index 000000000..e9f675f72 --- /dev/null +++ b/strategies/commands.js @@ -0,0 +1,47 @@ +// example strategy that responds to commands send from remote agent + +var log = require('../core/log'); + +// Let's create our own strat +var strat = {}; + +// Prepare everything our method needs +strat.init = function() { + +} + +// What happens on every new candle? +strat.update = function(candle) { + +} + +// For debugging purposes. +strat.log = function() { + +} + +// Based on the newly calculated +// information, check if we should +// update or not. +strat.check = function() { + +} + +// command handling +strat.onCommand = function(cmd) { + var command = cmd.command; + if (command == 'help') { + cmd.handled = true; + cmd.response = "Supported commands are 'buy' and 'sell"; + } + else if (command == 'buy') { + cmd.handled = true; + this.advice('long'); + } + else if (command == 'sell') { + cmd.handled = true; + this.advice('short'); + } +} + +module.exports = strat; diff --git a/subscriptions.js b/subscriptions.js index 285dd3885..16c5f0b5f 100644 --- a/subscriptions.js +++ b/subscriptions.js @@ -29,6 +29,11 @@ var subscriptions = [ event: 'portfolioUpdate', handler: 'processPortfolioUpdate' }, + { + emitter: ['telegrambot'], + event: 'command', + handler: 'processCommand' + } ]; module.exports = subscriptions; \ No newline at end of file