From 617f81923b2f71de31654a80d46a6fccc5dcd9a7 Mon Sep 17 00:00:00 2001 From: RJPGriffin Date: Sun, 2 Dec 2018 21:49:28 +0000 Subject: [PATCH] Pushbullet Plugin update round 2 (#2607) * Add functionality to pushbullet plugin. Now gives trade information. * Revert default plugin enebled to false * Improved time source * Merge branch 'develop' of https://github.com/askmike/gekko into pushbullet-update * Updated formatting, Added Round Trip PL to subject * Updated Start message and further formatting * Fixed Cost Percentage Calculation --- plugins/pushbullet.js | 249 +++++++++++++++++++++++++++++++----------- sample-config.js | 103 +++++++++-------- 2 files changed, 243 insertions(+), 109 deletions(-) diff --git a/plugins/pushbullet.js b/plugins/pushbullet.js index 5c5dc3128..4efb2d2f2 100644 --- a/plugins/pushbullet.js +++ b/plugins/pushbullet.js @@ -12,12 +12,12 @@ config.pushbullet = { enabled: true, // Send 'Gekko starting' message if true sendMessageOnStart: true, - // Send Message for advice? + // Send Message for advice? Recommend Flase for paper, true for live sendOnAdvice: true, // Send Message on Trade Completion? sendOnTrade: true, - // disable advice printout if it's soft - muteSoft: true, + // For Overall P/L calc. Pass in old balance if desired, else leave '0' + startingBalance: '0', // your pushbullet API key key: '', // your email @@ -32,19 +32,23 @@ config.pushbullet = { var pushbullet = require("pushbullet"); var _ = require('lodash'); const moment = require('moment'); +const request = require('request'); var log = require('../core/log.js'); var util = require('../core/util.js'); var config = util.getConfig(); -var pushbulletConfig = config.pushbullet; +var pbConf = config.pushbullet; var Pushbullet = function(done) { _.bindAll(this); this.pusher; this.price = 'N/A'; - + this.startingBalance = pbConf.startingBalance === undefined ? 0 : Number(pbConf.startingBalance); this.advicePrice = 0; this.adviceTime = moment(); + this.lastBuyTime = moment(); + this.lastBuyBalance = 0; + this.hasBought = 0; this.done = done; this.setup(); @@ -53,25 +57,26 @@ var Pushbullet = function(done) { Pushbullet.prototype.setup = function(done) { var setupPushBullet = function(err, result) { - if (pushbulletConfig.sendMessageOnStart) { - var title = pushbulletConfig.tag; + if (pbConf.sendMessageOnStart) { + var title = pbConf.tag; var exchange = config.watch.exchange; var currency = config.watch.currency; var asset = config.watch.asset; - var body = "Gekko has started watching " + - currency + - "/" + - asset + - " on " + - exchange + - "."; - + let tradeType = 'watching'; if (config.trader.enabled) { - body += "\nLive Trading is enabled" + tradeType = "Live Trading"; } if (config.paperTrader.enabled) { - body += "\nPaper Trading is enabled" + tradeType = "Paper Trading"; } + + var body = `Gekko has started ${tradeType} ${asset}/${currency} on ${exchange}.`; + + //If trading Advisor is enabled, add strategy and candle size information + if (config.tradingAdvisor.enabled) { + body += `\n\nUsing ${config.tradingAdvisor.method} strategy on M${config.tradingAdvisor.candleSize} candles.` + } + this.mail(title, body); } else { log.debug('Skipping Send message on startup') @@ -88,86 +93,206 @@ Pushbullet.prototype.processCandle = function(candle, done) { Pushbullet.prototype.processAdvice = function(advice) { - if (advice.recommendation == "soft" && pushbulletConfig.muteSoft) return; this.advicePrice = this.price; this.adviceTime = advice.date; - if (pushbulletConfig.sendOnAdvice) { + if (pbConf.sendOnAdvice) { var text = [ 'Gekko has new advice for ', - config.watch.exchange, + capF(config.watch.exchange), ', advice is to go ', - advice.recommendation, + capF(advice.recommendation), '.\n\nThe current ', config.watch.asset, ' price is ', this.advicePrice ].join(''); - var subject = pushbulletConfig.tag + ' New advice: go ' + advice.recommendation; + var subject = pbConf.tag + ' New advice: go ' + advice.recommendation; this.mail(subject, text); } }; Pushbullet.prototype.processTradeCompleted = function(trade) { - if (pushbulletConfig.sendOnTrade) { - var slip; - //Slip direction is opposite for buy and sell - if (trade.price === this.advicePrice) { - slip = 0; - } else if (trade.action === 'buy') { - slip = 100 * ((trade.price - this.advicePrice) / this.advicePrice); - } else if (trade.action === 'sell') { - slip = 100 * ((this.advicePrice - trade.price) / this.advicePrice); - } else { - slip = '1234'; - } - let tradeTime = trade.date; - let diff = tradeTime.diff(this.adviceTime); - let timeToComplete = moment.utc(diff).format("mm:ss"); + //Check Starting balance is initialized - if 0, initialize it + this.startingBalance = this.startingBalance ? this.startingBalance : trade.balance; + // If config variable doesn't exist (old config) defaults to send + let sendOnTrade = pbConf.sendOnTrade === undefined ? 1 : pbConf.sendOnTrade; - // timeToComplete = trade.date - this.adviceTime; - // timeToComplete.format('h:mm:ss'); + if (sendOnTrade) { - var text = [ - config.watch.exchange, - ' ', - config.watch.asset, - '/', - config.watch.currency, - '\nAdvice Price: ', - this.advicePrice, - '\nTrade Price: ', - trade.price, - '\nSlip: ', - slip.toFixed(2), '%', - '\nAdvice Time: ', - this.adviceTime.format("h:mm:ss"), - '\nTrade Time: ', - tradeTime.format("h:mm:ss"), - '\nTime to Fill: ', - timeToComplete + // Calculate exposure Time + let exposureTimeStr = ''; + let balanceChangeStr = '\n'; + let totBalanceChangeStr = ''; + let subject = `${pbConf.tag} ${capF(trade.action)} complete`; + + if (trade.action === 'buy') { + this.hasBought = 1; // Flag to ensure that the following variables have been filled + this.lastBuyTime = trade.date; + this.lastBuyBalance = trade.balance; + } else if (this.hasBought) { //if sell and we have previous buy data + exposureTimeStr = `\nExposure Time: ${moment.duration(trade.date.diff(this.lastBuyTime)).humanize()}`; + + //Calculate balance change + let oBal = this.lastBuyBalance; // Old Balance + let nBal = trade.balance; // New Balance + let diffBal = Math.abs(nBal - oBal); // Balance Difference + let percDiffBal = (diffBal / oBal) * 100; // Percentage difference - ].join(''); - var subject = ''; + if (nBal >= oBal) { // profit! + balanceChangeStr = `\n\nRound trip profit of: \n${getNumStr(diffBal)}${config.watch.currency} \n${getNumStr(percDiffBal,2)}%\n` + subject = `${subject}: +${getNumStr(percDiffBal,2)}%` + } else if (nBal < oBal) { // Loss :( + balanceChangeStr = `\n\nRound trip loss of: \n-${getNumStr(diffBal)}${config.watch.currency} \n-${getNumStr(percDiffBal,2)}%\n` + subject = `${subject}: -${getNumStr(percDiffBal,2)}%` + } + + //Calculate overall P/l + let sBal = this.startingBalance; + let tDiffBal = Math.abs(nBal - sBal); + let percDiffTotBal = (tDiffBal / sBal) * 100; + if (nBal >= sBal) { // profit! + totBalanceChangeStr = `\nOverall gain of: \n${getNumStr(tDiffBal)}${config.watch.currency} \n${getNumStr(percDiffTotBal,2)}%\n` + } else if (nBal < sBal) { // Loss :( + totBalanceChangeStr = `\nOverall loss of \n-${getNumStr(tDiffBal)}${config.watch.currency} \n-${getNumStr(percDiffTotBal,2)}%\n` + + } else if (trade.action === 'sell' && !this.hasBought) { + balanceChangeStr = `\n\nNot enough data for exposure time, round trip or overall performance yet. This will appear after bot has completed first round trip.` + } + } + let costOfTradeStr = `\nCost of Trade: ${getNumStr(trade.cost)}${config.watch.currency}, ${getNumStr(((trade.cost / (trade.amount*trade.price)) * 100), 2)}%`; - subject = pushbulletConfig.tag + ' ' + trade.action + ' Complete '; + //build strings that are only sent for Live trading, not paperTrader + let orderFillTimeStr = ''; + let slippageStr = ''; + + if (!config.paperTrader.enabled) { + let timeToComplete = moment.duration(trade.date.diff(this.adviceTime)).humanize(); + orderFillTimeStr = `\nOrder fill Time: ${timeToComplete}`; + + var slip; + //Slip direction is opposite for buy and sell + if (trade.price === this.advicePrice) { + slip = 0; + } else if (trade.action === 'buy') { + slip = 100 * ((trade.price - this.advicePrice) / this.advicePrice); + } else if (trade.action === 'sell') { + slip = 100 * ((this.advicePrice - trade.price) / this.advicePrice); + } + slippageStr = `\nSlipped ${getNumStr(slip,2)}% from advice @ ${getNumStr(this.advicePrice)}`; + } + + var text = [ + capF(config.watch.exchange), ' ', config.watch.asset, '/', config.watch.currency, + `\n\n${config.watch.asset} Trade Price: ${getNumStr(trade.price)}`, + `\n${getPastTense(trade.action)} ${getNumStr(trade.amount)} ${config.watch.asset}`, + orderFillTimeStr, + slippageStr, + costOfTradeStr, + exposureTimeStr, + balanceChangeStr, + totBalanceChangeStr, + '\nBalance: ', getNumStr(trade.balance), config.watch.currency, + ].join(''); + this.mail(subject, text); } }; + +// A long winded function to make sure numbers aren't displayed with too many decimal places +// and are a little humanized +function getNumStr(num, fixed = 4) { + let numStr = ''; + + if (typeof num != "number") { + num = Number(num); + if (isNaN(num)) { + // console.log("Pushbullet Plugin: Number Conversion Failed"); + return "Conversion Failure"; + } + } + + + if (Number.isInteger(num)) { + numStr = num.toString(); + } else { + + //Create modNum Max - Must be a better way... + let modNumMax = '1'; + for (let i = 1; i < fixed; i++) { + modNumMax = modNumMax + '0'; + } + modNumMax = Number(modNumMax); + + let i = 0; + if (num < 1) { + let modNum = num - Math.floor(num); + while (modNum < modNumMax && i < 8) { + modNum *= 10; + i += 1; + } + } else { + i = fixed; + } + numStr = num.toFixed(i); + //Remove any excess zeros + while (numStr.charAt(numStr.length - 1) === '0') { + numStr = numStr.substring(0, numStr.length - 1); + } + + //If last char remaining is a decimal point, remove it + if (numStr.charAt(numStr.length - 1) === '.') { + numStr = numStr.substring(0, numStr.length - 1); + } + + } + + //Add commas for thousands etc + let dp = numStr.indexOf('.'); //find deciaml point + if (dp < 0) { //no dp found + dp = numStr.length; + } + + let insPos = dp - 3; + insCount = 0; + while (insPos > 0) { + insCount++; + numStr = numStr.slice(0, insPos) + ',' + numStr.slice(insPos); + insPos -= 3; + } + + + return (numStr); +} + +function capF(inWord) { //Capitalise first letter of string + return (inWord.charAt(0).toUpperCase() + inWord.slice(1)); +} + +function getPastTense(action) { + let ret = ''; + if (action === 'buy') { + ret = 'Bought' + } else if (action === 'sell') { + ret = 'Sold' + } + return ret; +} + Pushbullet.prototype.mail = function(subject, content, done) { - var pusher = new pushbullet(pushbulletConfig.key); - pusher.note(pushbulletConfig.email, subject, content, function(error, response) { + var pusher = new pushbullet(pbConf.key); + pusher.note(pbConf.email, subject, content, function(error, response) { if (error || !response) { log.error('Pushbullet ERROR:', error) } else if (response && response.active) { diff --git a/sample-config.js b/sample-config.js index 8a0ab611d..36cdc31bb 100644 --- a/sample-config.js +++ b/sample-config.js @@ -114,10 +114,10 @@ config.pushover = { // want Gekko to send a mail on buy or sell advice? config.mailer = { - enabled: false, // Send Emails if true, false to turn off - sendMailOnStart: true, // Send 'Gekko starting' message if true, not if false + enabled: false, // Send Emails if true, false to turn off + sendMailOnStart: true, // Send 'Gekko starting' message if true, not if false - email: '', // Your Gmail address + email: '', // Your Gmail address muteSoft: true, // disable advice printout if it's soft // You don't have to set your password here, if you leave it blank we will ask it @@ -130,22 +130,22 @@ config.mailer = { // WARNING: If you have NOT downloaded Gekko from the github page above we CANNOT // guarantuee that your email address & password are safe! - password: '', // Your Gmail Password - if not supplied Gekko will prompt on startup. + password: '', // Your Gmail Password - if not supplied Gekko will prompt on startup. - tag: '[GEKKO] ', // Prefix all email subject lines with this + tag: '[GEKKO] ', // Prefix all email subject lines with this - // ADVANCED MAIL SETTINGS - // you can leave those as is if you - // just want to use Gmail + // ADVANCED MAIL SETTINGS + // you can leave those as is if you + // just want to use Gmail - server: 'smtp.gmail.com', // The name of YOUR outbound (SMTP) mail server. - smtpauth: true, // Does SMTP server require authentication (true for Gmail) - // The following 3 values default to the Email (above) if left blank - user: '', // Your Email server user name - usually your full Email address 'me@mydomain.com' - from: '', // 'me@mydomain.com' - to: '', // 'me@somedomain.com, me@someotherdomain.com' - ssl: true, // Use SSL (true for Gmail) - port: '', // Set if you don't want to use the default port + server: 'smtp.gmail.com', // The name of YOUR outbound (SMTP) mail server. + smtpauth: true, // Does SMTP server require authentication (true for Gmail) + // The following 3 values default to the Email (above) if left blank + user: '', // Your Email server user name - usually your full Email address 'me@mydomain.com' + from: '', // 'me@mydomain.com' + to: '', // 'me@somedomain.com, me@someotherdomain.com' + ssl: true, // Use SSL (true for Gmail) + port: '', // Set if you don't want to use the default port } config.pushbullet = { @@ -153,12 +153,12 @@ config.pushbullet = { enabled: false, // Send 'Gekko starting' message if true sendMessageOnStart: true, - // Send Message for advice? + // Send Message for advice? Recommend Flase for paper, true for live sendOnAdvice: true, // Send Message on Trade Completion? sendOnTrade: true, - // disable advice printout if it's soft - muteSoft: true, + // For Overall P/L calc. Pass in old balance if desired, else leave '0' + startingBalance: 0, // your pushbullet API key key: '', // your email @@ -192,20 +192,20 @@ config.telegrambot = { }; config.twitter = { - // sends pushbullets if true + // sends pushbullets if true enabled: false, - // Send 'Gekko starting' message if true + // Send 'Gekko starting' message if true sendMessageOnStart: false, - // disable advice printout if it's soft + // disable advice printout if it's soft muteSoft: false, tag: '[GEKKO]', - // twitter consumer key + // twitter consumer key consumer_key: '', - // twitter consumer secret + // twitter consumer secret consumer_secret: '', - // twitter access token key + // twitter access token key access_token_key: '', - // twitter access token secret + // twitter access token secret access_token_secret: '' }; @@ -233,11 +233,11 @@ config.redisBeacon = { enabled: false, port: 6379, // redis default host: '127.0.0.1', // localhost - // On default Gekko broadcasts - // events in the channel with - // the name of the event, set - // an optional prefix to the - // channel name. + // On default Gekko broadcasts + // events in the channel with + // the name of the event, set + // an optional prefix to the + // channel name. channelPrefix: '', broadcast: [ 'candle' @@ -298,7 +298,7 @@ config.sqlite = { dependencies: [] } - // Postgres adapter example config (please note: requires postgres >= 9.5): +// Postgres adapter example config (please note: requires postgres >= 9.5): config.postgresql = { path: 'plugins/postgresql', version: 0.1, @@ -331,10 +331,10 @@ config.mongodb = { config.backtest = { daterange: 'scan', -// daterange: { -// from: "2018-03-01", -// to: "2018-04-28" -//}, + // daterange: { + // from: "2018-03-01", + // to: "2018-04-28" + //}, batchSize: 50 } @@ -426,9 +426,18 @@ config.TSI = { // Ultimate Oscillator Settings config.UO = { - first: {weight: 4, period: 7}, - second: {weight: 2, period: 14}, - third: {weight: 1, period: 28}, + first: { + weight: 4, + period: 7 + }, + second: { + weight: 2, + period: 14 + }, + third: { + weight: 1, + period: 28 + }, thresholds: { low: 30, high: 70, @@ -440,13 +449,13 @@ config.UO = { // CCI Settings config.CCI = { - constant: 0.015, // constant multiplier. 0.015 gets to around 70% fit - history: 90, // history size, make same or smaller than history - thresholds: { - up: 100, // fixed values for overbuy upward trajectory - down: -100, // fixed value for downward trajectory - persistence: 0 // filter spikes by adding extra filters candles - } + constant: 0.015, // constant multiplier. 0.015 gets to around 70% fit + history: 90, // history size, make same or smaller than history + thresholds: { + up: 100, // fixed values for overbuy upward trajectory + down: -100, // fixed value for downward trajectory + persistence: 0 // filter spikes by adding extra filters candles + } }; // StochRSI settings @@ -513,4 +522,4 @@ config['tulip-adx'] = { // Not sure? Read this first: https://github.com/askmike/gekko/issues/201 config['I understand that Gekko only automates MY OWN trading strategies'] = false; -module.exports = config; +module.exports = config; \ No newline at end of file