├── dir
├── node.js
├── f_e.png
├── logo.jpg
├── ss.jpg
├── logwatch.png
├── presets.png
├── sspreset.png
└── logo_small.png
├── src
├── handlers
│ ├── registrationHandler.js
│ ├── ptShortcutsHandler.js
│ ├── botSettingsHandler.js
│ ├── presetsHandler.js
│ ├── ptfSettingsHandler.js
│ ├── poloniexHandler.js
│ ├── bittrexHandler.js
│ ├── binanceHandler.js
│ ├── ptSettingsHandler.js
│ ├── balanceHandler.js
│ └── ptJsonDataHandler.js
├── constants.js
├── model
│ ├── market.js
│ ├── order.js
│ └── balance.js
├── moonbotLogger.js
├── filesUtil.js
├── keyboards.js
├── PTProperties.js
├── moonbotProperties.js
├── ptSettingsParser.js
├── engines
│ ├── bittrexEngine.js
│ ├── poloniexEngine.js
│ ├── binanceEngine.js
│ ├── engine.js
│ └── ptJsonEngine.js
├── ptfJsonParser.js
├── propertiesValidator.js
└── monitor.js
├── package.json
├── README.md
└── moonbot.js
/dir/node.js:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dir/f_e.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulihub/PT-MoonBot/HEAD/dir/f_e.png
--------------------------------------------------------------------------------
/dir/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulihub/PT-MoonBot/HEAD/dir/logo.jpg
--------------------------------------------------------------------------------
/dir/ss.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulihub/PT-MoonBot/HEAD/dir/ss.jpg
--------------------------------------------------------------------------------
/dir/logwatch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulihub/PT-MoonBot/HEAD/dir/logwatch.png
--------------------------------------------------------------------------------
/dir/presets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulihub/PT-MoonBot/HEAD/dir/presets.png
--------------------------------------------------------------------------------
/dir/sspreset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulihub/PT-MoonBot/HEAD/dir/sspreset.png
--------------------------------------------------------------------------------
/dir/logo_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tulihub/PT-MoonBot/HEAD/dir/logo_small.png
--------------------------------------------------------------------------------
/src/handlers/registrationHandler.js:
--------------------------------------------------------------------------------
1 | class RegistrationHandler {
2 |
3 | async checkLicence() {
4 | return {state: "VALID"};
5 | }
6 |
7 | }
8 |
9 | module.exports = RegistrationHandler;
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | module.exports = Object.freeze({
2 | BITTREX_PREFIX_URI: 'https://bittrex.com/api/v1.1/',
3 | BALANCES_URI: 'account/getbalances',
4 | VERSION: 'Clash Royal | 7.0.0'
5 | });
6 |
--------------------------------------------------------------------------------
/src/model/market.js:
--------------------------------------------------------------------------------
1 |
2 | class Market {
3 |
4 | constructor(marketName) {
5 | this.marketName = marketName;
6 | }
7 |
8 | setBid(bid) {
9 | this.bid = bid;
10 | return this;
11 | }
12 |
13 | setAsk(ask) {
14 | this.ask = ask;
15 | return this;
16 | }
17 |
18 | setPervDay(prevDay) {
19 | this.prevDay = prevDay;
20 | return this;
21 | }
22 | }
23 |
24 | module.exports = Market;
--------------------------------------------------------------------------------
/src/model/order.js:
--------------------------------------------------------------------------------
1 |
2 | class Order {
3 |
4 | constructor(exchange) {
5 | this.exchange = exchange;
6 | }
7 |
8 | setOrderType(orderType) {
9 | this.orderType = orderType;
10 | return this;
11 | }
12 |
13 | setQuantity(quantity) {
14 | this.quantity = quantity;
15 | return this;
16 | }
17 |
18 | setPrice(price){
19 | this.price = price;
20 | return this;
21 | }
22 | }
23 |
24 | module.exports = Order;
--------------------------------------------------------------------------------
/src/model/balance.js:
--------------------------------------------------------------------------------
1 | class Balance {
2 |
3 | constructor(currency) {
4 | this.currency = currency;
5 | }
6 |
7 | setBalance(balance) {
8 | this.balance = balance;
9 | return this;
10 | }
11 |
12 | setAvailable(available) {
13 | this.available = available;
14 | return this;
15 | }
16 |
17 | setPending(pending) {
18 | this.pending = pending;
19 | return this;
20 | }
21 | }
22 |
23 | module.exports = Balance;
--------------------------------------------------------------------------------
/src/moonbotLogger.js:
--------------------------------------------------------------------------------
1 | // const logger = require('node-logger').createLogger(); // logs to STDOUT
2 | const properties = require('./moonbotProperties');
3 | const Log = require('log')
4 | , logger = new Log('debug');
5 |
6 | module.exports = {
7 |
8 | info: function info(message) {
9 | logger.info(message);
10 | },
11 |
12 | debug: function debug(message) {
13 | if (properties.get('moonbot.debug')) {
14 | logger.debug(message);
15 | }
16 | },
17 |
18 | warn: function warn(message) {
19 | logger.warning(message);
20 | },
21 | error: function error(message) {
22 | logger.error(message);
23 | }
24 |
25 | };
26 |
27 |
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "moonbot",
3 | "version": "7.0.0",
4 | "description": "moonbot",
5 | "author": "tuli",
6 | "main": "moonbot.js",
7 | "scripts": {
8 | "start": "node moonbot"
9 | },
10 | "dependencies": {
11 | "axios": "^0.17.1",
12 | "binance": "^1.3.3",
13 | "crypto-js": "^3.1.9-1",
14 | "fs-extra": "^5.0.0",
15 | "hjson": "^3.1.1",
16 | "jsonfile": "^4.0.0",
17 | "log": "^1.4.0",
18 | "moment-timezone": "^0.5.14",
19 | "node-bittrex-api": "^0.8.1",
20 | "path": "^0.12.7",
21 | "poloniex-api-node": "^1.8.1",
22 | "properties-parser": "^0.3.1",
23 | "properties-reader": "0.0.16",
24 | "qs": "^6.5.1",
25 | "tail": "^1.2.3",
26 | "telegraf": "^3.17.3"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/filesUtil.js:
--------------------------------------------------------------------------------
1 | const logger = require('./moonbotLogger');
2 | const fs = require('fs-extra');
3 | const path = require('path');
4 |
5 |
6 | class FilesUtil {
7 |
8 | static writeToFile(filePath, dataList) {
9 | let content = '';
10 |
11 | for (let line in dataList) {
12 | content += dataList[line];
13 | content += '\n';
14 | }
15 |
16 | fs.outputFile(path.normalize(filePath), content, err => {
17 | if (err) {
18 | logger.debug('Failed to save file: ' + err)
19 | }
20 | })
21 | }
22 |
23 | static readFile(filePath) {
24 | return fs.readFileSync(path.normalize(filePath)).toString();
25 | }
26 |
27 | }
28 |
29 | module.exports = FilesUtil;
30 |
31 |
--------------------------------------------------------------------------------
/src/handlers/ptShortcutsHandler.js:
--------------------------------------------------------------------------------
1 | const PtSettingsParser = require('../ptSettingsParser');
2 | const properties = require('../moonbotProperties');
3 | const logger = require('../moonbotLogger');
4 | let moonBotMarket = properties.get('moonbot.market');
5 |
6 | // const ptJsonEngine = require('../engines/ptJsonEngine');
7 |
8 | class PtShortcutsHandler {
9 |
10 | constructor() {
11 | this.settingsParser = new PtSettingsParser();
12 | }
13 |
14 |
15 | doubleDown(data, market) {
16 |
17 | let dcaLogData = data.dcaLogData;
18 |
19 | for (let i in dcaLogData) {
20 | if (dcaLogData[i].market === market) {
21 | logger.debug('In progress');
22 | }
23 | }
24 |
25 | // PtSettingsParser.saveSetting(type, setting, value);
26 | }
27 |
28 | getMarketItem(logData, market) {
29 | if (market === moonBotMarket) {
30 | return undefined;
31 | }
32 |
33 | for (let i in logData) {
34 | if (logData[i].market.indexOf(market) >= 0) {
35 | return logData[i];
36 | }
37 | }
38 | }
39 |
40 |
41 | getDcaBuyTimes(data, market) {
42 | return this.getMarketItem(data.dcaLogData, market).boughtTimes;
43 | }
44 | }
45 |
46 | module.exports = PtShortcutsHandler;
--------------------------------------------------------------------------------
/src/keyboards.js:
--------------------------------------------------------------------------------
1 | const Markup = require('telegraf/markup');
2 | const Extra = require('telegraf/extra');
3 |
4 |
5 | let keyboards = {
6 | yesNoKeyboard: Extra.markup(Markup.keyboard([
7 | Markup.callbackButton('Yes'),
8 | Markup.callbackButton('Cancel')
9 | ])),
10 | realYesNoKeyboard: Extra.markup(Markup.keyboard([
11 | Markup.callbackButton('Yes'),
12 | Markup.callbackButton('No')
13 | ])),
14 | ptSettings: Extra.markup(Markup.keyboard([
15 | Markup.callbackButton('Presets'),
16 | Markup.callbackButton('Pairs'),
17 | Markup.callbackButton('DCA'),
18 | Markup.callbackButton('Indicators'),
19 | Markup.callbackButton('⛔️ Toggle SOM ⛔️'),
20 | Markup.callbackButton('Cancel')
21 | ])),
22 | ptPtFSettings: Extra.markup(Markup.keyboard([
23 | Markup.callbackButton('DCA'),
24 | Markup.callbackButton('Indicators'),
25 | Markup.callbackButton('appsettings'),
26 | Markup.callbackButton('hostsettings'),
27 | Markup.callbackButton('⛔️ Toggle SOM ⛔️'),
28 | Markup.callbackButton('Cancel')
29 | ])),
30 | mainKeyboard: Extra.markup(Markup.keyboard([
31 | Markup.callbackButton('Summary'),
32 | ])),
33 | lettersKeyboard: Markup.removeKeyboard().extra()
34 | };
35 |
36 | module.exports = keyboards;
--------------------------------------------------------------------------------
/src/PTProperties.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const PropertiesParser = require('properties-parser');
3 | const properties = require('./moonbotProperties');
4 | const logger = require('./moonbotLogger');
5 |
6 | let ptProperties = {};
7 |
8 | reload();
9 |
10 | function reload() {
11 | let pathPrefix = '';
12 | try {
13 | pathPrefix = properties.get('profit.trailer.directory.path');
14 | ptProperties = PropertiesParser.read(path.normalize(pathPrefix + '/application.properties'));
15 | setTimeout(reload, 1000 * 60);
16 | }
17 | catch (err) {
18 | if (err.code === 'ENOENT') {
19 | logger.debug("profit.trailer.directory.path: " + properties.get('profit.trailer.directory.path'));
20 | logger.error("PT properties could not be found at:" + pathPrefix);
21 | }
22 | logger.debug("reload PT props: " + err);
23 | setTimeout(reload, 1000 * 5);
24 | }
25 | }
26 |
27 | // Public
28 |
29 | module.exports = {
30 |
31 | get: function get(property) {
32 | let value = ptProperties[property];
33 |
34 | if (value === 'true')
35 | return true;
36 | if (value === 'false')
37 | return false;
38 | if (value) {
39 | value = value.replace(/['"]/g, "");
40 | }
41 | return value;
42 | }
43 | };
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/handlers/botSettingsHandler.js:
--------------------------------------------------------------------------------
1 | const Markup = require('telegraf/markup');
2 | const Extra = require('telegraf/extra');
3 | const properties = require('../moonbotProperties');
4 | const isDemo = !!properties.get('bot.demo.mode');
5 |
6 | class BotSettingsHandler {
7 |
8 | saveSetting(key, value) {
9 | properties.setProperty(key, value);
10 | }
11 |
12 | getSetting(key) {
13 | return properties.get(key);
14 | }
15 |
16 | getKeyboardOptions(setting) {
17 |
18 | let keys = [];
19 | switch (setting) {
20 | case "pt.feeder.show.pairs":
21 | case "moonbot.debug":
22 | keys.push('true');
23 | keys.push('false');
24 | break;
25 | }
26 |
27 | if (keys.length === 0) {
28 | return null;
29 | }
30 |
31 | let buttons = [];
32 | for (let i = 0; i < keys.length; i++) {
33 | buttons.push(Markup.callbackButton(keys[i]));
34 |
35 | }
36 | buttons.push(Markup.callbackButton('Cancel'));
37 |
38 | return Extra.markup(Markup.keyboard(buttons));
39 | }
40 |
41 |
42 | getKeyboardSettings(valid = false) {
43 | let buttons = [];
44 | let keys = properties.getAllKeys();
45 | buttons.push(Markup.callbackButton('Help'));
46 |
47 | if (valid && !isDemo) {
48 | buttons.push(Markup.callbackButton('Toggle PT Notifications'));
49 | buttons.push(Markup.callbackButton('Toggle Health Check'));
50 | buttons.push(Markup.callbackButton('Toggle Log Watcher'));
51 | buttons.push(Markup.callbackButton('Reset Log Ignore'));
52 |
53 | for (let i = 0; i < keys.length; i++) {
54 | buttons.push(Markup.callbackButton(keys[i]));
55 | }
56 | }
57 |
58 | buttons.push(Markup.callbackButton('Cancel'));
59 |
60 | return Extra.markup(Markup.keyboard(buttons));
61 | }
62 | }
63 |
64 | module.exports = BotSettingsHandler;
--------------------------------------------------------------------------------
/src/moonbotProperties.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const PropertiesParser = require('properties-parser');
3 |
4 | let properties = {};
5 |
6 | let runningProperties = {};
7 |
8 | reload();
9 |
10 | function reload() {
11 | try {
12 | properties = PropertiesParser.read(path.normalize('./application.properties'));
13 | setTimeout(reload, 1000 * 60);
14 | }
15 | catch (err) {
16 | console.log("moonbot properties loader failed: " + path.normalize('./application.properties') + " -> " + err);
17 | setTimeout(reload, 1000 * 5);
18 | }
19 | }
20 |
21 | // Public
22 |
23 | module.exports = {
24 |
25 | get: function get(property) {
26 | let value = properties[property];
27 |
28 | if (typeof value === 'string' && value.toLowerCase() === 'true')
29 | return true;
30 | if (typeof value === 'string' && value.toLowerCase() === 'false')
31 | return false;
32 | if (value) {
33 | value = value.replace(/['"]/g, "");
34 | }
35 | return value;
36 | },
37 |
38 | getAllKeys: function getAllKeys() {
39 | let keys = [];
40 | if (this.get('bot.demo.mode')) {
41 | return keys;
42 | }
43 |
44 | for (let key in properties) {
45 | keys.push(key);
46 | }
47 | return keys;
48 | },
49 |
50 | setProperty: function setProperty(key, value) {
51 | let settingPath = path.normalize('./application.properties');
52 | let editor = PropertiesParser.createEditor(settingPath);
53 | editor.set(key, value);
54 | editor.save(settingPath);
55 | properties = PropertiesParser.read(settingPath);
56 | },
57 |
58 |
59 | getRunningProperty: function getRunningProperty(property) {
60 | return runningProperties[property];
61 | },
62 |
63 | setRunningProperty: function setRunningProperty(key, value) {
64 | runningProperties[key] = value;
65 | }
66 |
67 |
68 | };
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/handlers/presetsHandler.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const logger = require('../moonbotLogger');
3 | const Markup = require('telegraf/markup');
4 | const Extra = require('telegraf/extra');
5 | const path = require('path');
6 | const ptJsonEngine = require('../engines/ptJsonEngine');
7 |
8 | class PresetsHandler {
9 |
10 | getPreSetsKeyboard() {
11 | let presetsFolders = this.loadPreSets();
12 | if (presetsFolders && presetsFolders.length > 0) {
13 | presetsFolders.push("Cancel");
14 | return Extra.markup(Markup.keyboard(presetsFolders))
15 | }
16 |
17 | return null;
18 | }
19 |
20 | loadPreSets() {
21 | const presetsFolder = './presets';
22 | try {
23 | return fs.readdirSync(path.normalize(presetsFolder));
24 | }
25 | catch (err) {
26 | logger.warn("Could not load presets folders");
27 | logger.debug("loadPreSets " + err);
28 | }
29 | }
30 |
31 | switchToPreset(presetFolder) {
32 | try {
33 | let destTradingDir = path.normalize('cache');
34 | let sourceTradingDir = path.normalize('./presets/' + presetFolder);
35 | try {
36 | fs.copySync(sourceTradingDir + '/PAIRS.properties', destTradingDir + '/PAIRS.properties.new');
37 | ptJsonEngine.saveActiveConf('PAIRS');
38 | fs.copySync(sourceTradingDir + '/INDICATORS.properties', destTradingDir + '/INDICATORS.properties.new');
39 | ptJsonEngine.saveActiveConf('INDICATORS');
40 | fs.copySync(sourceTradingDir + '/DCA.properties', destTradingDir + '/DCA.properties.new');
41 | ptJsonEngine.saveActiveConf('DCA');
42 | logger.debug("switchToPreset: switched to " + presetFolder);
43 | return true;
44 | } catch (err) {
45 | logger.debug("switchToPreset inner" + err);
46 | return false;
47 | }
48 | }
49 | catch (err) {
50 | logger.debug("switchToPreset " + err);
51 | return false;
52 | }
53 | }
54 | }
55 |
56 | module.exports = PresetsHandler;
57 |
58 |
--------------------------------------------------------------------------------
/src/handlers/ptfSettingsHandler.js:
--------------------------------------------------------------------------------
1 | const Markup = require('telegraf/markup');
2 | const Extra = require('telegraf/extra');
3 | const PtfJsonParser = require('../ptfJsonParser');
4 | const properties = require('../moonbotProperties');
5 |
6 | class PtfSettingsHandler {
7 | constructor() {
8 | this.settingsParser = new PtfJsonParser();
9 | }
10 |
11 | saveSetting(type, sub, key, value) {
12 | this.settingsParser.saveSetting(type, sub, key, value);
13 | }
14 |
15 | saveConfigSetting(type, sub, config, key, value) {
16 | this.settingsParser.saveConfigSetting(type, sub, config, key, value);
17 | }
18 |
19 | getSetting(command, prev1, prev2, prev3 = null) {
20 | switch (prev2) {
21 | case "appsettings" :
22 | return this.settingsParser.getAppSettingValue(command, prev1);
23 | case "hostsettings" :
24 | return this.settingsParser.getHostSettingValue(command, prev1);
25 | default:
26 | switch (prev3) {
27 | case "appsettings" :
28 | return this.settingsParser.getAppSettingValue(command, prev1, prev2);
29 | case "hostsettings" :
30 | return this.settingsParser.getHostSettingValue(command, prev1, prev2);
31 | }
32 | }
33 |
34 | return "NA";
35 | }
36 |
37 |
38 | getFeederSettingsKeyboard(command, prev1 = null, prev2 = null) {
39 | let max = properties.get('moonbot.keys.max.length') ? parseFloat(properties.get('moonbot.keys.max.length')) : 300;
40 | let keys = this.settingsParser.getSettingKeys(command, prev1, prev2).slice(0, max);
41 |
42 | let buttons = [];
43 |
44 | for (let i = 0; i < keys.length; i++) {
45 | if (keys[i].indexOf('//') !== 0) {
46 | let label = '';
47 | if (isInt(keys[i])) {
48 | label = 'Group ' + keys[i];
49 | } else {
50 | label = keys[i];
51 | }
52 | buttons.push(Markup.callbackButton(label));
53 | }
54 | }
55 | buttons.push(Markup.callbackButton('Cancel'));
56 |
57 | return Extra.markup(Markup.keyboard(buttons));
58 | }
59 |
60 | }
61 |
62 | function isInt(value) {
63 | return !isNaN(value) && (function (x) {
64 | return (x | 0) === x;
65 | })(parseFloat(value))
66 | }
67 | module.exports = PtfSettingsHandler;
--------------------------------------------------------------------------------
/src/ptSettingsParser.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const PropertiesParser = require('properties-parser');
3 | const logger = require('./moonbotLogger');
4 | const ptJsonEngine = require('./engines/ptJsonEngine');
5 |
6 | class PtSettingsParser {
7 |
8 | constructor() {
9 | }
10 |
11 | reload() {
12 | let pathPrefix = 'cache';
13 | let parsedPaires = [];
14 | let parsedDca = [];
15 | let parsedIndicators = [];
16 |
17 | try {
18 | parsedPaires = PropertiesParser.read(path.normalize(pathPrefix + "/PAIRS.properties"));
19 | parsedDca = PropertiesParser.read(path.normalize(pathPrefix + "/DCA.properties"));
20 | parsedIndicators = PropertiesParser.read(path.normalize(pathPrefix + "/INDICATORS.properties"));
21 | }
22 | catch (err) {
23 | if (err.code === 'ENOENT') {
24 | logger.error("Profit Trailer config files could not be loaded " + err);
25 | }
26 | }
27 |
28 | this.properties = {
29 | PAIRS: parsedPaires,
30 | DCA: parsedDca,
31 | INDICATORS: parsedIndicators
32 | };
33 |
34 | }
35 |
36 | static deleteSetting(type, setting) {
37 | let pathPrefix = 'cache';
38 | try {
39 | let settingPath = path.normalize(pathPrefix + '/' + type.toUpperCase() + ".properties");
40 | let editor = PropertiesParser.createEditor(settingPath);
41 | editor.unset(setting);
42 | editor.save(settingPath + '.new');
43 | editor.save(settingPath);
44 | return ptJsonEngine.saveActiveConf(type.toUpperCase());
45 | }
46 | catch (err) {
47 | if (err.code === 'ENOENT') {
48 | logger.error("Profit Trailer config files could not be loaded " + err);
49 | }
50 | return false;
51 | }
52 | }
53 |
54 |
55 | static saveSetting(type, setting, value) {
56 | let pathPrefix = 'cache';
57 | try {
58 | let settingPath = path.normalize(pathPrefix + '/' + type.toUpperCase() + ".properties");
59 | let editor = PropertiesParser.createEditor(settingPath);
60 | editor.set(setting, value);
61 | editor.save(settingPath + '.new');
62 | editor.save(settingPath);
63 | return ptJsonEngine.saveActiveConf(type.toUpperCase());
64 | }
65 | catch (err) {
66 | if (err.code === 'ENOENT') {
67 | logger.error("Profit Trailer config files could not be loaded " + err);
68 | }
69 | return false;
70 | }
71 | }
72 |
73 | getIndicators() {
74 | return Object.assign({}, this.properties.INDICATORS);
75 | }
76 |
77 | getPairs() {
78 | return Object.assign({}, this.properties.PAIRS);
79 | }
80 |
81 | getDca() {
82 | return Object.assign({}, this.properties.DCA);
83 | }
84 | }
85 |
86 | module.exports = PtSettingsParser;
87 |
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PT-MoonBot 
2 |
3 | Demo bot: [https://t.me/demoMoonBot](https://t.me/demoMoonBot?start=gh)
4 |
5 | The most integrated, feature-enabled tool to quickly update your PT bot, remotely!
6 | MoonBot is the ultimate addon for mobile control Profit Trailer.
7 | Monitor and track your crypto tradings using your private and secured telegram bot.
8 | Moonbot has advanced integration with **Profit Trailer**, you can view (and share) your pbls, sales, pairs and dca status, update your PT settings remotely (**PT Feeder** too), get complete notifications, switch between settings presets and much more!! [Wiki](https://github.com/tulihub/PT-MoonBot/wiki)
9 |
10 | [Get your version now!](https://github.com/tulihub/moonbot/wiki/Quick-Installation)
11 |
12 | 
13 |
14 | [Install your version now](https://github.com/tulihub/moonbot/wiki/Quick-Installation)!
15 |
16 |
17 | * Get instant updated summary of all your crypto exchanges
18 | * View and share your sales, pairs and dca status
19 | * **[PT Presets](https://github.com/tulihub/PT-MoonBot/wiki/Presets)** - switch between predefined sets of PT configs (Pairs,Dca,Indicators) in just few clicks!
20 | * **Pattern Search** - a quick way to search your PARIS and DCA settings for some text.
21 | * Get notifications when your bot buys or sell (dca too!)
22 | * **[PT Settings](https://github.com/tulihub/PT-MoonBot/wiki/PT-Settings)** - Update and control your PT from everywhere!
23 | * **[PTF Settings](https://github.com/tulihub/PT-MoonBot/wiki/PTF-Settings)** - Update PT Feeder settings from everywhere!
24 | * Track your profit in BTC and in USD (coinbase rate)
25 | * Get your portfolio status anytime
26 | * Clear view of your current tradings on each exchange
27 | * **[PT Log Watcher](https://github.com/tulihub/PT-MoonBot/wiki/Log-Watcher)** - Monitor you PT log files for error and sends you alerts!
28 | * Monitor BTC price
29 | * Get notified when your Profit Trailer is down
30 | * **[Groups](https://github.com/tulihub/PT-MoonBot/wiki/Groups)** support!
31 | * More to come!!!
32 |
33 | Check our demo bot:[https://t.me/demoMoonBot](https://t.me/demoMoonBot?start=gh)
34 |
35 |
36 | ### Main Commands:
37 | Summary: View summary of your accounts.
38 | Binance: Show binance status including coin information.
39 | Bittrex: bittrex status including coin information.
40 | PBL 👀 (`/pbl [n]`): Display possible buy list, include triggers values and true trailing.
41 | Pairs 💼 (`/pairs [n]`): Display current n (all) pairs.
42 | DCA 💰 (`/dca [n]`): Display n n (all) DCAs.
43 | Sales 💸 (`/sales [n]`): Display latest n (5) Profit Trailer sells.
44 | PT Settings 🛠: Remote update your Profit Trailer / Feeder settings.
45 | Bot Settings 🤖: Update MoonBot settings & `/help` .
46 | `/togglesom` : Instant toggling of SOM (ALL_sell_only_mode) on/off.
47 | `/restartpt` : execute PT restart command
48 | You can type 'Cancel' (`/cancel`) anytime to go back home.
49 |
50 |
51 | ### Groups:
52 | Invite your bot to groups!
53 | ##### Use the following commands to share Profit Trailer sales, pairs and dca(s) status:
54 | `/sales` [n]: Share latest n (5) sells.
55 | `/pairs` [n]: Share current n (all) pairs.
56 | `/dca` [n]: Share n (all) DCAs.
57 | `/notify`: Send buy\sold notifications to the group
58 | ##### Other available commands in groups:
59 | `/allowgroup` : ⛔️ Allow all group members to control your bot.
60 | `/bangroup` : Ban group members from controlling your bot.
61 |
62 | ** Don't forget to mention your bot name ([@](@))
63 |
#Supports Binance, Bittrex and Poloniex(Alpha) exchanges.
64 |
65 | ## Donate:
66 | BTC 1Er2DrLMk7xEdY19LgSwH1kzsA3iCcBmzA
67 | ETH 0x5ab32694458ac90f1e0c0c817aee8dd9cd1f3d47
68 | LTC LaD8n73u8BKZB43JLCyRp3ju7ST2SBqQRi
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/engines/bittrexEngine.js:
--------------------------------------------------------------------------------
1 | const Balance = require('../model/balance');
2 | const Market = require('../model/market');
3 | const Order = require('../model/order');
4 | const bittrex = require('node-bittrex-api');
5 | const properties = require('../moonbotProperties');
6 | const logger = require('../moonbotLogger');
7 | let moonBotMarket = properties.get('moonbot.market');
8 |
9 | bittrex.options({
10 | 'apikey': properties.get('bittrex.api.key'),
11 | 'apisecret': properties.get('bittrex.api.secret')
12 | });
13 |
14 |
15 | const bittrexData = {};
16 |
17 | class BittrexEngine {
18 |
19 | static getCurrentData() {
20 | return Object.assign({}, bittrexData);
21 | }
22 |
23 | start() {
24 | try {
25 | setInterval(this.callExchanges, 30000);
26 | this.callExchanges();
27 | }
28 | catch (err) {
29 | logger.error("Bittrex engine ERR: " + err);
30 | }
31 | }
32 |
33 | callExchanges() {
34 |
35 | new Promise(function (resolve, reject) {
36 |
37 | bittrex.getbalances(function (data, err) {
38 | if (err) {
39 | logger.debug("bittrex getbalances fault " + err);
40 | reject(err);
41 | }
42 |
43 | let balances = [];
44 | if (data) {
45 | for (let i in data.result) {
46 | let balance = new Balance(data.result[i].Currency.toString());
47 | balance.setBalance(data.result[i].Balance).setAvailable(data.result[i].Available).setPending(data.result[i].Pending);
48 | balances[balance.currency] = balance;
49 | }
50 | }
51 | resolve(balances);
52 | });
53 |
54 | }).then(bittrexBalances => {
55 | bittrexData.balances = bittrexBalances;
56 | }).catch(err => {
57 | logger.debug("bittrex balance fault " + err);
58 | });
59 |
60 |
61 | new Promise(function (resolve, reject) {
62 |
63 | bittrex.getorderhistory(null, function (data, err) {
64 | if (err) {
65 | reject(err);
66 | }
67 |
68 | let orders = [];
69 | if (data) {
70 | for (let i in data.result) {
71 | let currency = data.result[i].Exchange.toString().replace(moonBotMarket, '').replace('-', '');
72 |
73 | if (currency) {
74 | let order = new Order(currency);
75 | order.setOrderType(data.result[i].OrderType).setQuantity(data.result[i].Quantity).setPrice(data.result[i].Price);
76 | orders[order.exchange] = orders[order.exchange] || [];
77 | orders[order.exchange].push(order);
78 | }
79 | }
80 | }
81 | resolve(orders);
82 | });
83 |
84 | }).then(orders => {
85 | bittrexData.orders = orders
86 | }).catch(err => {
87 | logger.debug("bittrex getorderhistory fault " + err);
88 | });
89 |
90 | new Promise(function (resolve, reject) {
91 |
92 | bittrex.getmarketsummaries(function (data, err) {
93 | if (err) {
94 | reject(err);
95 | }
96 |
97 | let markets = [];
98 | if (data) {
99 | for (let i in data.result) {
100 | let marketName = data.result[i].MarketName.toString();
101 | if (marketName.indexOf(moonBotMarket) >= 0) {
102 | let market = new Market(marketName.replace(moonBotMarket, '').replace('-', ''));
103 | market.setBid(data.result[i].Bid).setAsk(data.result[i].Ask).setPervDay(data.result[i].PrevDay);
104 | markets[market.marketName] = market;
105 | }
106 | }
107 | }
108 | resolve(markets);
109 | });
110 |
111 | }).then(marketRes => {
112 | bittrexData.markets = marketRes;
113 | }).catch(err => {
114 | logger.debug("bittrex getmarketsummaries fault " + err);
115 | });
116 | }
117 |
118 | }
119 |
120 | module.exports = BittrexEngine;
--------------------------------------------------------------------------------
/src/engines/poloniexEngine.js:
--------------------------------------------------------------------------------
1 | const Balance = require('../model/balance');
2 | const Market = require('../model/market');
3 | const Order = require('../model/order');
4 | const properties = require('../moonbotProperties');
5 | const logger = require('../moonbotLogger');
6 | const Poloniex = require('poloniex-api-node');
7 |
8 |
9 | const poloniex = new Poloniex(properties.get('poloniex.api.key'), properties.get('poloniex.api.secret'));
10 |
11 |
12 | let poloniexData = {
13 | balances: [],
14 | orders: [],
15 | markets: []
16 | };
17 |
18 | class PoloniexEngine {
19 |
20 | static getCurrentData() {
21 | return Object.assign({}, poloniexData);
22 | }
23 |
24 | start() {
25 | try {
26 | setInterval(this.callBalance, 30000);
27 | setInterval(this.callMarkets, 60000);
28 | this.callBalance();
29 | this.callMarkets();
30 | }
31 | catch (err) {
32 | logger.warn("Poloniex engine fault:" + err);
33 | }
34 | }
35 |
36 |
37 | callMarkets() {
38 |
39 | poloniex.returnTicker((err, ticker) => {
40 | if (err) {
41 | logger.debug("poloniex returnTicker fault " + err);
42 | } else {
43 | try {
44 | for (let currency in ticker) {
45 | let symbol = currency.replace(/BTC_/, "");
46 | if (symbol !== 'BTC') {
47 | let market = new Market(symbol);
48 | let prev = parseFloat(ticker[currency].highestBid) / (1 + parseFloat(ticker[currency].percentChange));
49 | market.setBid(parseFloat(ticker[currency].highestBid)).setAsk(parseFloat(ticker[currency].lowestAsk)).setPervDay(prev);
50 | poloniexData.markets[market.marketName] = market;
51 | }
52 | }
53 | }
54 | catch (err) {
55 | logger.debug("poloniex returnTicker fault " + err);
56 | }
57 | }
58 | });
59 | }
60 |
61 |
62 | callBalance() {
63 | poloniex.returnCompleteBalances(null, (err, data) => {
64 | if (err) {
65 | logger.debug("poloniex returnCompleteBalances fault " + err);
66 | return;
67 | }
68 |
69 | try {
70 | let tmpBalances = [];
71 |
72 | if (data.length > 0) {
73 | for (let currency in data.balances) {
74 | let balance = new Balance(currency);
75 | let a = parseFloat(data[currency].available);
76 | let p = parseFloat(data[currency].onOrders);
77 |
78 | balance.setBalance(a + p).setAvailable(a).setPending(p);
79 |
80 | let crumb = properties.get('crumbs.' + data.balances[currency].asset.toLowerCase());
81 | let crumbDefault = properties.get('crumbs.default') ? properties.get('crumbs.default') : 1;
82 | let minimum = crumb ? crumb : crumbDefault;
83 | if (balance.balance >= minimum) {
84 | tmpBalances[balance.currency] = balance;
85 |
86 | if (balance.currency !== "BTC") {
87 | //BTC_NXT
88 | poloniex.returnTradeHistory(currencyPair, (err, trades) => {
89 | if (err) {
90 | logger.debug("poloniex myTrades err " + err);
91 | } else {
92 | let orders = [];
93 |
94 | for (let trade in trades) {
95 | let order = new Order(balance.currency);
96 | order.setOrderType(trades[trade].type === "buy" ? "LIMIT_BUY" : "LIMIT_SELL").setQuantity(parseFloat(trades[trade].amount)).setPrice(parseFloat(trades[trade].amount) * parseFloat(trades[trade].rate));
97 | orders.push(order);
98 | }
99 | poloniexData.orders[balance.currency] = orders;
100 | }
101 | });
102 | }
103 | }
104 | }
105 |
106 | poloniexData.balances = Object.assign({}, tmpBalances);
107 | }
108 | }
109 | catch (err) {
110 | logger.debug("poloniex callBalance 2 fault " + err);
111 | }
112 | });
113 | }
114 | }
115 |
116 | module.exports = PoloniexEngine;
--------------------------------------------------------------------------------
/src/ptfJsonParser.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const properties = require('./moonbotProperties');
3 | const fs = require('fs');
4 | const logger = require('./moonbotLogger');
5 | const Hjson = require('hjson');
6 |
7 | class PtfJsonParser {
8 |
9 | reload() {
10 | let pathPrefix = '';
11 | try {
12 | pathPrefix = properties.get('pt.feeder.directory.path');
13 | this.appSettings = Hjson.parse(fs.readFileSync(path.normalize(pathPrefix + '/config/appsettings.json')).toString(), {keepWsc: true});
14 | this.hostsSettings = Hjson.parse(fs.readFileSync(path.normalize(pathPrefix + '/config/hostsettings.json')).toString(), {keepWsc: true});
15 | }
16 | catch (err) {
17 | if (err.code === 'ENOENT') {
18 | logger.debug("pt.feeder.directory.path: " + properties.get('pt.feeder.directory.path'));
19 | logger.error("appsettings.json or hostsettings.json could not be found at:" + path.normalize(pathPrefix + '/config'));
20 | }
21 | logger.debug("pt.feeder loader: " + err);
22 | }
23 | }
24 |
25 |
26 | saveSetting(type, sub, key, value) {
27 | switch (type) {
28 | case 'appsettings':
29 | this.appSettings[sub][key] = value;
30 | PtfJsonParser.saveAppSettingsJson(this.appSettings);
31 | break;
32 | case 'hostsettings':
33 | this.hostsSettings[sub][key] = value;
34 | PtfJsonParser.saveHostsJson(this.hostsSettings);
35 | break;
36 | }
37 | }
38 |
39 |
40 | saveConfigSetting(type, sub, configPos, key, value) {
41 | switch (type) {
42 | case 'appsettings':
43 | this.appSettings[sub].Configs[configPos][key] = value;
44 | PtfJsonParser.saveAppSettingsJson(this.appSettings);
45 | break;
46 | case 'hostsettings':
47 | this.hostsSettings[sub].Configs[configPos][key] = value;
48 | PtfJsonParser.saveHostsJson(this.hostsSettings);
49 | break;
50 | }
51 | }
52 |
53 | getSettingKeys(key, prev1 = null, prev2 = null) {
54 | this.reload();
55 | switch (key) {
56 | case 'appsettings':
57 | return Object.keys(this.appSettings);
58 | case 'hostsettings':
59 | return Object.keys(this.hostsSettings);
60 |
61 | default:
62 | switch (prev1) {
63 | case 'appsettings':
64 | if (this.appSettings[key].Configs) {
65 | return Object.keys(this.appSettings[key].Configs).map(String);
66 | }
67 | return Object.keys(this.appSettings[key]);
68 | case 'hostsettings':
69 | return Object.keys(this.hostsSettings[key]);
70 | default:
71 | switch (prev2) {
72 | case 'appsettings':
73 | return Object.keys(this.appSettings[prev1].Configs[key]);
74 | default :
75 | return [];
76 | }
77 | }
78 | }
79 | }
80 |
81 | getAppSettingValue(key, prev1 = null, prev2 = null) {
82 | if (!prev2) {
83 | return this.appSettings[prev1][key];
84 | }
85 | else {
86 | return this.appSettings[prev2].Configs[prev1][key];
87 | }
88 | }
89 |
90 | getHostSettingValue(key, prev1 = null, prev2 = null) {
91 | if (!prev2) {
92 | return this.hostsSettings[prev1][key];
93 | }
94 | else {
95 | return this.hostsSettings[prev2].Configs[prev1][key];
96 | }
97 | }
98 |
99 | static saveHostsJson(hostsSettings) {
100 | let pathPrefix = '';
101 |
102 | try {
103 | pathPrefix = properties.get('pt.feeder.directory.path');
104 | fs.writeFileSync(path.normalize(pathPrefix + '/config/hostsettings.json'), Hjson.stringify(hostsSettings, {
105 | separator: true,
106 | bracesSameLine: true,
107 | keepWsc: true,
108 | quotes: "all"
109 | }));
110 | }
111 | catch (err) {
112 | if (err.code === 'ENOENT') {
113 | logger.error("hostsettings.json could not be found at:" + pathPrefix);
114 | }
115 | logger.debug("pt.feeder saveHostsJson: " + err);
116 | }
117 | }
118 |
119 | static saveAppSettingsJson(appSettings) {
120 | let pathPrefix = '';
121 |
122 | try {
123 | pathPrefix = properties.get('pt.feeder.directory.path');
124 | fs.writeFileSync(path.normalize(pathPrefix + '/config/appsettings.json'), Hjson.stringify(appSettings, {
125 | separator: true,
126 | bracesSameLine: true,
127 | keepWsc: true,
128 | quotes: "all"
129 | }));
130 | }
131 | catch (err) {
132 | if (err.code === 'ENOENT') {
133 | logger.error("hostsettings.json could not be found at:" + pathPrefix);
134 | }
135 | logger.debug("pt.feeder saveAppSettingsJson: " + err);
136 | }
137 | }
138 |
139 | }
140 |
141 | module.exports = PtfJsonParser;
142 |
143 |
--------------------------------------------------------------------------------
/src/handlers/poloniexHandler.js:
--------------------------------------------------------------------------------
1 | const logger = require('../moonbotLogger');
2 | const properties = require('../moonbotProperties');
3 | let moonBotMarket = properties.get('moonbot.market');
4 |
5 | class PoloniexHandler {
6 |
7 | handlePoloniexMessage(data) {
8 | let orders = [];
9 | let balances = [];
10 | let markets = [];
11 |
12 | try {
13 | orders = Object.assign({}, data.poloniex.orders);
14 | balances = Object.assign({}, data.poloniex.balances);
15 | markets = Object.assign({}, data.poloniex.markets);
16 | } catch (err) {
17 | logger.debug("error handling poloniex message: " + err);
18 | }
19 |
20 | let stats = [];
21 | let marketBalance = 0;
22 | let alts = 0;
23 |
24 |
25 | for (let curr in balances) {
26 |
27 | // let currentMarket = markets[curr] ? markets[curr] :
28 | if (curr == moonBotMarket) {
29 | marketBalance += balances[curr].balance;
30 | }
31 | else if (curr == 'USDT') {
32 | marketBalance += balances[curr].balance / data.coinbaseRate;
33 | }
34 | else {
35 | try {
36 | let change24 = ((markets[curr].ask - markets[curr].prevDay) / markets[curr].prevDay) * 100;
37 | let currentBalance = balances[curr].balance * markets[curr].ask;
38 | alts += currentBalance;
39 | if (currentBalance > 0) {
40 | let ordersBalance = this.calcOrdersBalance(orders, curr);
41 | marketBalance += currentBalance;
42 | let lastBuyPrice = this.calcLastBuyPrice(orders[curr]);
43 | let avgBuyPrice = this.calcAvgBuyPrice(orders[curr]);
44 | stats.push({
45 | balance: balances[curr].balance,
46 | currency: curr,
47 | ordersBalance: ordersBalance,
48 | currentBalance: currentBalance,
49 | change24: change24,
50 | lastBuyPrice: lastBuyPrice,
51 | currentPrice: markets[curr].ask,
52 | avgBuyPrice: avgBuyPrice,
53 | });
54 | }
55 | }
56 | catch (err) {
57 | logger.warn("problem with loading poloniex balance of " + curr)
58 | }
59 | }
60 | }
61 |
62 | let altsRate = (alts / marketBalance) * 100;
63 | let b = moonBotMarket === 'USDT' ? 2 : 8;
64 | let response = "POLONIEX\nTotal: " + marketBalance.toFixed(b) + " " + moonBotMarket;
65 | response = response.concat("\n" + alts.toFixed(b) + " in trade, " + altsRate.toFixed(0) + "%\n");
66 |
67 | stats = stats.sort(this.sortOrders);
68 | for (let i = 0; i < stats.length; i++) {
69 | let usdval = stats[i].currentBalance * data.coinbaseRate;
70 | let emoji = stats[i].currentPrice > stats[i].lastBuyPrice ? '💹️' : '🔻';
71 | response = response.concat("\n" + emoji + " " + stats[i].currency);
72 | response = response.concat("\nAmount: " + stats[i].balance.toFixed(2) + " | $" + usdval.toFixed(2));
73 | response = response.concat("\nVal:" + stats[i].currentBalance.toFixed(b) + " | 24h:" + stats[i].change24.toFixed(0) + "%");
74 | let lastBuyPrice = stats[i].lastBuyPrice ? stats[i].lastBuyPrice.toFixed(b) : 'NA';
75 | response = response.concat("\nLast buy at: " + lastBuyPrice);
76 | response = response.concat("\nCurrent: " + stats[i].currentPrice.toFixed(b));
77 | response = response.concat("\n");
78 | }
79 |
80 | return response;
81 | }
82 |
83 | sortOrders(a, b) {
84 | if (a.currentBalance < b.currentBalance)
85 | return 1;
86 | if (a.currentBalance > b.currentBalance)
87 | return -1;
88 | return 0;
89 | }
90 |
91 | calcOrdersBalance(orders, curr) {
92 |
93 | let orderBalance = 0;
94 | if (!orders)
95 | return undefined;
96 |
97 |
98 | if (orders && orders[curr]) {
99 | for (let i in orders[curr]) {
100 | orderBalance = orders[curr][i].orderType === 'LIMIT_SELL' ? orderBalance + orders[curr][i].price : orderBalance - orders[curr][i].price
101 | }
102 | }
103 |
104 | return orderBalance;
105 | }
106 |
107 | calcLastBuyPrice(orders) {
108 | if (!orders)
109 | return undefined;
110 |
111 | for (let i = orders.length - 1; i >= 0; i--) {
112 | if (orders[i].orderType === 'LIMIT_BUY') {
113 | return orders[i].price / orders[i].quantity;
114 | }
115 | }
116 | };
117 |
118 | calcAvgBuyPrice(orders) {
119 | let totalPrice = 0;
120 | let totalQuantity = 0;
121 |
122 | if (!orders)
123 | return undefined;
124 |
125 | for (let i = 0; i < orders.length; i++) {
126 | if (orders[i].orderType === 'LIMIT_BUY') {
127 | totalPrice += orders[i].price;
128 | totalQuantity += orders[i].quantity;
129 | }
130 | }
131 | return totalPrice / totalQuantity;
132 | };
133 | }
134 |
135 | module.exports = PoloniexHandler;
--------------------------------------------------------------------------------
/src/handlers/bittrexHandler.js:
--------------------------------------------------------------------------------
1 | const logger = require('../moonbotLogger');
2 | const properties = require('../moonbotProperties');
3 | let moonBotMarket = properties.get('moonbot.market');
4 |
5 |
6 | class BittrexHandler {
7 |
8 | handleBittrexMessage(data) {
9 |
10 | let orders = [];
11 | let balances = [];
12 | let markets = [];
13 | try {
14 | orders = Object.assign({}, data.bittrex.orders);
15 | balances = Object.assign({}, data.bittrex.balances);
16 | markets = Object.assign({}, data.bittrex.markets);
17 | } catch (err) {
18 | logger.debug("error handling bittrex message: " + err);
19 | }
20 |
21 | let stats = [];
22 | let marketBalance = 0;
23 | let alts = 0;
24 |
25 |
26 | for (let curr in balances) {
27 |
28 | if (curr == moonBotMarket) {
29 | marketBalance += balances[curr].balance;
30 | }
31 | else if (curr == 'USDT') {
32 | marketBalance += balances[curr].balance / data.coinbaseRate;
33 | }
34 | else {
35 | try {
36 | if (markets[curr]) {
37 | let change24 = ((markets[curr].ask - markets[curr].prevDay) / markets[curr].prevDay) * 100;
38 | let currentBalance = balances[curr].balance * markets[curr].ask;
39 | alts += currentBalance;
40 | if (currentBalance > 0) {
41 | let ordersBalance = this.calcOrdersBalance(orders, curr);
42 | marketBalance += currentBalance;
43 | let lastBuyPrice = this.calcLastBuyPrice(orders[curr]);
44 | let avgBuyPrice = this.calcAvgBuyPrice(orders[curr]);
45 | stats.push({
46 | balance: balances[curr].balance,
47 | currency: curr,
48 | ordersBalance: ordersBalance,
49 | currentBalance: currentBalance,
50 | change24: change24,
51 | lastBuyPrice: lastBuyPrice,
52 | currentPrice: markets[curr].ask,
53 | avgBuyPrice: avgBuyPrice,
54 | });
55 | }
56 | }
57 | }
58 | catch (err) {
59 | logger.warn("problem with loading bittrex balance of " + curr)
60 | }
61 |
62 | }
63 | }
64 |
65 | let altsRate = (alts / marketBalance) * 100;
66 | let b = moonBotMarket === 'USDT' ? 2 : 8;
67 | let response = "BITTREX\nTotal: " + marketBalance.toFixed(b) + " " + moonBotMarket;
68 | response = response.concat("\n" + alts.toFixed(b) + " in trade, " + altsRate.toFixed(0) + "%\n");
69 |
70 | stats = stats.sort(this.sortOrders);
71 | for (let i = 0; i < stats.length; i++) {
72 | let usdval = stats[i].currentBalance * data.coinbaseRate;
73 | let emoji = stats[i].currentPrice > stats[i].lastBuyPrice ? '💹️' : '🔻';
74 | response = response.concat("\n" + emoji + " " + stats[i].currency);
75 | response = response.concat("\nAmount: " + stats[i].balance.toFixed(2) + " | $" + usdval.toFixed(2));
76 | response = response.concat("\nVal:" + stats[i].currentBalance.toFixed(b) + " | 24h:" + stats[i].change24.toFixed(0) + "%");
77 | let lastBuyPrice = stats[i].lastBuyPrice ? stats[i].lastBuyPrice.toFixed(b) : 'NA';
78 | response = response.concat("\nLast buy at: " + lastBuyPrice);
79 | response = response.concat("\nCurrent: " + stats[i].currentPrice.toFixed(b));
80 | response = response.concat("\n");
81 | }
82 |
83 | return response;
84 | }
85 |
86 | sortOrders(a, b) {
87 | if (a.currentBalance < b.currentBalance)
88 | return 1;
89 | if (a.currentBalance > b.currentBalance)
90 | return -1;
91 | return 0;
92 | }
93 |
94 | calcOrdersBalance(orders, curr) {
95 |
96 | let orderBalance = 0;
97 |
98 | if (orders && orders[curr]) {
99 | for (let i in orders[curr]) {
100 | orderBalance = orders[curr][i].orderType === 'LIMIT_SELL' ? orderBalance + orders[curr][i].price : orderBalance - orders[curr][i].price
101 | }
102 | }
103 |
104 | return orderBalance;
105 | }
106 |
107 | calcLastBuyPrice(orders) {
108 |
109 | if (!orders)
110 | return undefined;
111 |
112 |
113 | for (let i = 0; i < orders.length; i++) {
114 | if (orders[i].orderType === 'LIMIT_BUY') {
115 | return orders[i].price / orders[i].quantity;
116 | }
117 | }
118 | };
119 |
120 | calcAvgBuyPrice(orders) {
121 | let totalPrice = 0;
122 | let totalQuantity = 0;
123 |
124 | if (!orders)
125 | return undefined;
126 |
127 | for (let i = 0; i < orders.length; i++) {
128 | if (orders[i].orderType === 'LIMIT_BUY') {
129 | totalPrice += orders[i].price;
130 | totalQuantity += orders[i].quantity;
131 | }
132 | }
133 |
134 | return totalPrice / totalQuantity;
135 | };
136 | }
137 |
138 | module.exports = BittrexHandler;
--------------------------------------------------------------------------------
/src/handlers/binanceHandler.js:
--------------------------------------------------------------------------------
1 | const logger = require('../moonbotLogger');
2 | const properties = require('../moonbotProperties');
3 | let moonBotMarket = properties.get('moonbot.market');
4 |
5 | class BinanceHandler {
6 |
7 | handleBinanceMessage(data) {
8 | let orders = [];
9 | let balances = [];
10 | let markets = [];
11 |
12 | try {
13 | orders = Object.assign({}, data.binance.orders);
14 | balances = Object.assign({}, data.binance.balances);
15 | markets = Object.assign({}, data.binance.markets);
16 | } catch (err) {
17 | logger.debug("error handling binance message: " + err);
18 | }
19 |
20 | let stats = [];
21 | let marketBalance = 0;
22 | let alts = 0;
23 |
24 |
25 | for (let curr in balances) {
26 |
27 | // let currentMarket = markets[curr] ? markets[curr] :
28 | if (curr == moonBotMarket) {
29 | marketBalance += balances[curr].balance;
30 | }
31 | else if (curr == 'USDT') {
32 | marketBalance += balances[curr].balance / data.coinbaseRate;
33 | }
34 | else {
35 | try {
36 | if (markets[curr]) {
37 | let change24 = ((markets[curr].ask - markets[curr].prevDay) / markets[curr].prevDay) * 100;
38 | let currentBalance = balances[curr].balance * markets[curr].ask;
39 | alts += currentBalance;
40 | if (currentBalance > 0) {
41 | let ordersBalance = this.calcOrdersBalance(orders, curr);
42 | marketBalance += currentBalance;
43 | let lastBuyPrice = this.calcLastBuyPrice(orders[curr]);
44 | let avgBuyPrice = this.calcAvgBuyPrice(orders[curr]);
45 | stats.push({
46 | balance: balances[curr].balance,
47 | currency: curr,
48 | ordersBalance: ordersBalance,
49 | currentBalance: currentBalance,
50 | change24: change24,
51 | lastBuyPrice: lastBuyPrice,
52 | currentPrice: markets[curr].ask,
53 | avgBuyPrice: avgBuyPrice,
54 | });
55 | }
56 | }
57 | }
58 | catch (err) {
59 | logger.warn("problem with loading binance balance of " + curr)
60 | }
61 | }
62 | }
63 |
64 | let altsRate = (alts / marketBalance) * 100;
65 | let b = moonBotMarket === 'USDT' ? 2 : 8;
66 | let response = "BINANCE\nTotal: " + marketBalance.toFixed(b) + " " + moonBotMarket;
67 | response = response.concat("\n" + alts.toFixed(b) + " in trade, " + altsRate.toFixed(0) + "%\n");
68 |
69 | stats = stats.sort(this.sortOrders);
70 | for (let i = 0; i < stats.length; i++) {
71 | let usdval = stats[i].currentBalance * data.coinbaseRate;
72 | let emoji = stats[i].currentPrice > stats[i].lastBuyPrice ? '💹️' : '🔻';
73 | response = response.concat("\n" + emoji + " " + stats[i].currency);
74 | response = response.concat("\nAmount: " + stats[i].balance.toFixed(2) + " | $" + usdval.toFixed(2));
75 | response = response.concat("\nVal:" + stats[i].currentBalance.toFixed(b) + " | 24h:" + stats[i].change24.toFixed(0) + "%");
76 | let lastBuyPrice = stats[i].lastBuyPrice ? stats[i].lastBuyPrice.toFixed(b) : 'NA';
77 | response = response.concat("\nLast buy at: " + lastBuyPrice);
78 | response = response.concat("\nCurrent: " + stats[i].currentPrice.toFixed(b));
79 | response = response.concat("\n");
80 | }
81 |
82 | return response;
83 | }
84 |
85 | sortOrders(a, b) {
86 | if (a.currentBalance < b.currentBalance)
87 | return 1;
88 | if (a.currentBalance > b.currentBalance)
89 | return -1;
90 | return 0;
91 | }
92 |
93 | calcOrdersBalance(orders, curr) {
94 |
95 | let orderBalance = 0;
96 | if (!orders)
97 | return undefined;
98 |
99 |
100 | if (orders && orders[curr]) {
101 | for (let i in orders[curr]) {
102 | orderBalance = orders[curr][i].orderType === 'LIMIT_SELL' ? orderBalance + orders[curr][i].price : orderBalance - orders[curr][i].price
103 | }
104 | }
105 |
106 | return orderBalance;
107 | }
108 |
109 | calcLastBuyPrice(orders) {
110 | if (!orders)
111 | return undefined;
112 |
113 | for (let i = orders.length - 1; i >= 0; i--) {
114 | if (orders[i].orderType === 'LIMIT_BUY') {
115 | return orders[i].price / orders[i].quantity;
116 | }
117 | }
118 | };
119 |
120 | calcAvgBuyPrice(orders) {
121 | let totalPrice = 0;
122 | let totalQuantity = 0;
123 |
124 | if (!orders)
125 | return undefined;
126 |
127 | for (let i = 0; i < orders.length; i++) {
128 | if (orders[i].orderType === 'LIMIT_BUY') {
129 | totalPrice += orders[i].price;
130 | totalQuantity += orders[i].quantity;
131 | }
132 | }
133 | return totalPrice / totalQuantity;
134 | };
135 | }
136 |
137 | module.exports = BinanceHandler;
--------------------------------------------------------------------------------
/src/engines/binanceEngine.js:
--------------------------------------------------------------------------------
1 | const Balance = require('../model/balance');
2 | const Market = require('../model/market');
3 | const Order = require('../model/order');
4 | const binance = require('binance');
5 | const properties = require('../moonbotProperties');
6 | const logger = require('../moonbotLogger');
7 | let moonBotMarket = properties.get('moonbot.market');
8 |
9 |
10 | const binanceRest = new binance.BinanceRest({
11 | key: properties.get('binance.api.key'), // Get this from your account on binance.com
12 | secret: properties.get('binance.api.secret'), // Same for this
13 | timeout: 15000, // Optional, defaults to 15000, is the request time out in milliseconds
14 | recvWindow: 10000, // Optional, defaults to 5000, increase if you're getting timestamp errors
15 | disableBeautification: false,
16 | /*
17 | * Optional, default is false. Binance's API returns objects with lots of one letter keys. By
18 | * default those keys will be replaced with more descriptive, longer ones.
19 | */
20 | handleDrift: false
21 | /* Optional, default is false. If turned on, the library will attempt to handle any drift of
22 | * your clock on it's own. If a request fails due to drift, it'll attempt a fix by requesting
23 | * binance's server time, calculating the difference with your own clock, and then reattempting
24 | * the request.
25 | */
26 | });
27 |
28 |
29 | let binanceData = {
30 | balances: [],
31 | orders: [],
32 | markets: []
33 | };
34 |
35 | class BinanceEngine {
36 |
37 | static getCurrentData() {
38 | return Object.assign({}, binanceData);
39 | }
40 |
41 | start() {
42 | let binanceThrottle = properties.get('moonbot.binance.throttle.min') ? properties.get('moonbot.binance.throttle.min') : 0.5;
43 | try {
44 | setInterval(this.callBalance, binanceThrottle * 60 * 1000);
45 | setInterval(this.callMarkets, binanceThrottle * 2 * 60 * 1000);
46 | this.callBalance();
47 | this.callMarkets();
48 | }
49 | catch (err) {
50 | logger.warn("Binance engine fault:" + err);
51 | }
52 | }
53 |
54 |
55 | callMarkets() {
56 | binanceRest.ticker24hr({}, (err, data) => {
57 | if (err) {
58 | logger.debug("binance callMarkets fault " + err);
59 | return;
60 | }
61 | try {
62 | for (let currency in data) {
63 | let rawsymbol = data[currency].symbol;
64 | let replace = moonBotMarket + '$';
65 | let re = new RegExp(replace, "g");
66 | let symbol = rawsymbol.replace(re, "");
67 | if (rawsymbol.indexOf(moonBotMarket) > 0 && symbol !== moonBotMarket) {
68 | let market = new Market(symbol);
69 | market.setBid(parseFloat(data[currency].bidPrice)).setAsk(parseFloat(data[currency].askPrice)).setPervDay(parseFloat(data[currency].prevClosePrice));
70 | binanceData.markets[market.marketName] = market;
71 | }
72 | }
73 | }
74 | catch (err) {
75 | logger.debug("binance callMarkets 2 fault " + err);
76 | }
77 | });
78 | }
79 |
80 |
81 | callBalance() {
82 | binanceRest.account((err, data) => {
83 | if (err) {
84 | logger.debug("binance account fault " + err);
85 | return;
86 | }
87 |
88 | try {
89 |
90 | let tmpBalances = [];
91 |
92 | if (data.balances) {
93 | for (let currency in data.balances) {
94 | let balance = new Balance(data.balances[currency].asset);
95 | let a = parseFloat(data.balances[currency].free);
96 | let p = parseFloat(data.balances[currency].locked);
97 |
98 | balance.setBalance(a + p).setAvailable(a).setPending(p);
99 |
100 | let crumb = properties.get('crumbs.' + data.balances[currency].asset.toLowerCase());
101 | let crumbDefault = properties.get('crumbs.default') ? properties.get('crumbs.default') : 1;
102 | let minimum = crumb ? crumb : crumbDefault;
103 | if (balance.balance >= minimum) {
104 | tmpBalances[balance.currency] = balance;
105 |
106 | if (balance.currency !== moonBotMarket) {
107 | binanceRest.myTrades(balance.currency + moonBotMarket, (err, trades) => {
108 | if (err) {
109 | logger.debug("binance myTrades err " + err);
110 | return;
111 | }
112 | let orders = [];
113 |
114 | for (let trade in trades) {
115 | let order = new Order(balance.currency);
116 | order.setOrderType(trades[trade].isBuyer ? "LIMIT_BUY" : "LIMIT_SELL").setQuantity(parseFloat(trades[trade].qty)).setPrice(parseFloat(trades[trade].qty) * parseFloat(trades[trade].price));
117 | orders.push(order);
118 | }
119 | binanceData.orders[balance.currency] = orders;
120 | });
121 | }
122 | }
123 | }
124 |
125 | binanceData.balances = Object.assign({}, tmpBalances);
126 | }
127 | }
128 | catch (err) {
129 | logger.debug("binance callBalance 2 fault " + err);
130 | }
131 | });
132 | }
133 | }
134 |
135 | module.exports = BinanceEngine;
--------------------------------------------------------------------------------
/src/engines/engine.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const BittrexEngine = require('./bittrexEngine');
3 | const BinanceEngine = require('./binanceEngine');
4 | const PoloniexEngine = require('./poloniexEngine');
5 | const PropertiesValidator = require('../propertiesValidator');
6 | const logger = require('../moonbotLogger');
7 | const ptJsonEngine = require('./ptJsonEngine');
8 | const properties = require('../moonbotProperties');
9 |
10 | const commonData = {};
11 |
12 | class Engine {
13 |
14 | constructor() {
15 | this.bittrexEngine = new BittrexEngine();
16 | this.binanceEngine = new BinanceEngine();
17 | this.poloniexEngine = new PoloniexEngine();
18 | }
19 |
20 | static getCurrentBittrexData() {
21 | return Object.assign({bittrex: BittrexEngine.getCurrentData()}, commonData);
22 | }
23 |
24 | static getCurrentBinanceData() {
25 | return Object.assign({binance: BinanceEngine.getCurrentData()}, commonData);
26 | }
27 |
28 | static getCurrentPoloniexData() {
29 | return Object.assign({poloniex: PoloniexEngine.getCurrentData()}, commonData);
30 | }
31 |
32 | static getCurrentSellLogData() {
33 | return Object.assign({sellLogData: ptJsonEngine.sellLogData()}, commonData);
34 | }
35 |
36 | static getPtJsonSummaryData() {
37 | return Object.assign({}, ptJsonEngine.summaryData());
38 | }
39 |
40 | static getCurrentPairsLogData() {
41 | return Object.assign({
42 | gainLogData: ptJsonEngine.gainLogData(),
43 | pendingLogData: ptJsonEngine.pendingLogData(),
44 | watchModeLogData: ptJsonEngine.watchModeLogData()
45 | }, commonData);
46 | }
47 |
48 | static getCurrentDcaLogData() {
49 | return Object.assign({dcaLogData: ptJsonEngine.dcaLogData()}, commonData);
50 | }
51 |
52 | static getCurrentPblLogData() {
53 | return Object.assign({bbBuyLogData: ptJsonEngine.bbBuyLogData()}, commonData);
54 | }
55 |
56 | static getCurrentData() {
57 | let bittrexData = BittrexEngine.getCurrentData();
58 | let binanceData = BinanceEngine.getCurrentData();
59 | let poloniexData = PoloniexEngine.getCurrentData();
60 | let ptData = ptJsonEngine.summaryData();
61 |
62 | return Object.assign({
63 | bittrex: bittrexData,
64 | binance: binanceData,
65 | poloniex: poloniexData,
66 | ptData: ptData
67 | }, commonData)
68 | }
69 |
70 | start() {
71 | try {
72 |
73 | if (PropertiesValidator.checkProp('poloniex.api.key', false, false) && PropertiesValidator.checkProp('poloniex.api.secret', false, false)) {
74 | logger.debug("poloniex is on");
75 | this.poloniexEngine.start();
76 | }
77 |
78 | if (PropertiesValidator.checkProp('bittrex.api.key', false, false) && PropertiesValidator.checkProp('bittrex.api.secret', false, false)) {
79 | logger.debug("bittrex is on");
80 | this.bittrexEngine.start();
81 | }
82 |
83 | if (PropertiesValidator.checkProp('binance.api.key', false, false) && PropertiesValidator.checkProp('binance.api.secret', false, false)) {
84 | logger.debug("binance is on");
85 | this.binanceEngine.start();
86 | }
87 |
88 | if (PropertiesValidator.checkProp('pt.server.api_token', false, true)) {
89 | ptJsonEngine.start();
90 | }
91 |
92 | setInterval(this.runCoinbase, 30000);
93 | this.runCoinbase();
94 | setInterval(this.runCoinmarketcap, 30000);
95 | this.runCoinmarketcap();
96 |
97 | }
98 | catch (err) {
99 | logger.error("Error starting engine: " + err)
100 | }
101 | }
102 |
103 |
104 | runCoinbase() {
105 | let moonBotMarket = properties.get('moonbot.market');
106 | if (moonBotMarket !== 'USDT') {
107 | new Promise(function (resolve, reject) {
108 | axios.get('https://api.coinbase.com/v2/prices/' + moonBotMarket.toLowerCase() + '-usd/spot')
109 | .then(response => {
110 | let rate = response.data.data.amount;
111 | resolve(rate)
112 | })
113 | .catch(error => {
114 | reject(error);
115 | });
116 | }).then(rate => {
117 | commonData.coinbaseRate = rate;
118 | }).catch(err => {
119 | logger.warn("Coinbase api is acting up, retry in few seconds");
120 | });
121 |
122 | } else {
123 | commonData.coinbaseRate = 1;
124 | }
125 | }
126 |
127 | runCoinmarketcap() {
128 | let moonBotMarket = properties.get('moonbot.market');
129 | if (moonBotMarket !== 'USDT') {
130 | new Promise(function (resolve, reject) {
131 | axios.get('https://api.coinmarketcap.com/v1/global/')
132 | .then(response => {
133 | let total_market_cap_usd = response.data.total_market_cap_usd;
134 | let total_24h_volume_usd = response.data.total_24h_volume_usd;
135 | let bitcoin_percentage_of_market_cap = response.data.bitcoin_percentage_of_market_cap;
136 | resolve({
137 | total_market_cap_usd: total_market_cap_usd,
138 | total_24h_volume_usd: total_24h_volume_usd,
139 | bitcoin_percentage_of_market_cap: bitcoin_percentage_of_market_cap
140 | });
141 | })
142 | .catch(error => {
143 | reject(error);
144 | });
145 | }).then(rate => {
146 | commonData.total_market_cap_usd = rate.total_market_cap_usd;
147 | commonData.total_24h_volume_usd = rate.total_24h_volume_usd;
148 | commonData.bitcoin_percentage_of_market_cap = rate.bitcoin_percentage_of_market_cap;
149 | }).catch(err => {
150 | logger.warn("Coinmarketcap api is acting up, retry in few seconds");
151 | });
152 |
153 | } else {
154 | commonData.coinbaseRate = 1;
155 | }
156 | }
157 |
158 |
159 | }
160 |
161 | module.exports = Engine;
--------------------------------------------------------------------------------
/src/handlers/ptSettingsHandler.js:
--------------------------------------------------------------------------------
1 | const Markup = require('telegraf/markup');
2 | const Extra = require('telegraf/extra');
3 | const PtSettingsParser = require('../ptSettingsParser');
4 | const properties = require('../moonbotProperties');
5 | const logger = require('../moonbotLogger');
6 | const {exec} = require('child_process');
7 | const isDemo = !!properties.get('bot.demo.mode');
8 |
9 | class PtSettingsHandler {
10 | constructor() {
11 | this.settingsParser = new PtSettingsParser();
12 | }
13 |
14 | getSearchInSettingsMessage(pattern) {
15 | this.settingsParser.reload();
16 |
17 | let pairs = this.settingsParser.getPairs();
18 | let pairsResult = isDemo;
19 | let pairsConfs = '';
20 | for (let pair in pairs) {
21 | if (pair.indexOf(pattern) >= 0 || pairs[pair].indexOf(pattern) >= 0) {
22 | pairsConfs = pairsConfs.concat('\n' + pair + '=' + pairs[pair]);
23 | pairsResult = true;
24 | }
25 | }
26 |
27 | pairsConfs = isDemo && pairsConfs.length === 0 ? '\n' + pattern + '_trading_enabled=true (demo)' : pairsConfs;
28 |
29 | pairsConfs = pairsResult ? '\nPAIRS:' + pairsConfs : '';
30 |
31 |
32 | let dcas = this.settingsParser.getDca();
33 | let dcasConfs = '';
34 | let dcasResult = false;
35 | for (let dca in dcas) {
36 | if (dca.indexOf(pattern) >= 0 || dcas[dca].indexOf(pattern) >= 0) {
37 | dcasConfs = dcasConfs.concat('\n' + dca + '=' + dcas[dca]);
38 | dcasResult = true;
39 | }
40 | }
41 | dcasConfs = dcasResult ? '\nDCA:' + dcasConfs : '';
42 |
43 | if (pairsResult || dcasResult) {
44 | return '/' + pattern + " pattern found\n" + pairsConfs + '\n' + dcasConfs;
45 | }
46 | else {
47 | return 'No entries were found';
48 | }
49 |
50 | }
51 |
52 | restartCommand(ctx) {
53 | if (properties.get("profit.trailer.restart.command")) {
54 | exec(properties.get("profit.trailer.restart.command"), (err, stdout, stderr) => {
55 | logger.debug(stderr);
56 | logger.debug(stdout);
57 | if (err) {
58 | ctx.telegram.sendMessage(ctx.chat.id, "⛔ Failed to restart PT");
59 | return;
60 | }
61 | logger.info("Profit Trailer restarted");
62 | ctx.telegram.sendMessage(ctx.chat.id, "✅ PT restarted.");
63 | });
64 | }
65 | }
66 |
67 | saveSetting(type, setting, value) {
68 | PtSettingsParser.saveSetting(type, setting, value);
69 | }
70 |
71 | deleteSetting(type, setting) {
72 | PtSettingsParser.deleteSetting(type, setting);
73 | }
74 |
75 | toggleSom() {
76 | let som = this.getSetting('Pairs', 'DEFAULT_sell_only_mode_enabled');
77 | if (som === 'true') {
78 | som = 'false';
79 | }
80 | else {
81 | som = 'true';
82 | }
83 | return {
84 | currentSom: som,
85 | success: PtSettingsParser.saveSetting('Pairs', 'DEFAULT_sell_only_mode_enabled', som)
86 | }
87 | }
88 |
89 | getSetting(type, setting) {
90 | this.settingsParser.reload();
91 |
92 | let tmpSettings = null;
93 |
94 | if (setting === 'Add Property') {
95 | return "Type your property:\ni.e 'DEFAULT_sell_only_mode_enabled=true'\n"
96 | }
97 |
98 | switch (type) {
99 | case "Indicators" :
100 | tmpSettings = this.settingsParser.getIndicators();
101 | break;
102 | case "DCA" :
103 | tmpSettings = this.settingsParser.getDca();
104 | break;
105 | case "Pairs" :
106 | tmpSettings = this.settingsParser.getPairs();
107 | break;
108 | }
109 |
110 | for (let key in tmpSettings) {
111 | if (key === setting) {
112 | return tmpSettings[key];
113 | }
114 | }
115 |
116 | return "NA"
117 | }
118 |
119 | getKeyboardOptions(setting) {
120 |
121 | let keys = [];
122 | switch (setting) {
123 | case "ALL_trading_enabled":
124 | case "ALL_panic_sell_enabled":
125 | case "ALL_sell_only_mode":
126 | case "ALL_DCA_enabled":
127 | case "ignore_sell_only_mode":
128 | case "enabled" :
129 | keys.push('true');
130 | keys.push('false');
131 | break;
132 | case "MARKET":
133 | keys.push("BTC");
134 | keys.push("ETH");
135 | keys.push("USDT");
136 | break;
137 | case "ALL_sell_strategy":
138 | keys.push("GAIN");
139 | keys.push("HIGHBB");
140 | break;
141 | default:
142 | if (setting.endsWith("_enabled")) {
143 | keys.push('true');
144 | keys.push('false');
145 | }
146 | }
147 |
148 | if (keys.length === 0) {
149 | return null;
150 | }
151 |
152 | let buttons = [];
153 | for (let i = 0; i < keys.length; i++) {
154 | buttons.push(Markup.callbackButton(keys[i]));
155 |
156 | }
157 | buttons.push(Markup.callbackButton('Delete'));
158 | buttons.push(Markup.callbackButton('Cancel'));
159 |
160 | return Extra.markup(Markup.keyboard(buttons));
161 | }
162 |
163 |
164 | handleSettings(ctx) {
165 | this.settingsParser.reload();
166 | let max = properties.get('moonbot.keys.max.length') ? parseFloat(properties.get('moonbot.keys.max.length')) : 150;
167 |
168 | let command = ctx.message.text;
169 | let tmpSettings = null;
170 | let keys = [];
171 | keys.push('Add Property');
172 | switch (command) {
173 | case "Indicators" :
174 | tmpSettings = this.settingsParser.getIndicators();
175 | break;
176 | case "DCA" :
177 | tmpSettings = this.settingsParser.getDca();
178 | break;
179 | case "Pairs" :
180 | tmpSettings = this.settingsParser.getPairs();
181 | break;
182 | }
183 |
184 | let buttons = [];
185 |
186 | for (let key in tmpSettings) {
187 | keys.push(key);
188 | }
189 |
190 | keys = keys.slice(0, max);
191 | for (let i = 0; i < keys.length; i++) {
192 | buttons.push(Markup.callbackButton(keys[i]));
193 |
194 | }
195 | buttons.push(Markup.callbackButton('Cancel'));
196 |
197 | return Extra.markup(Markup.keyboard(buttons));
198 | }
199 | }
200 |
201 | module.exports = PtSettingsHandler;
--------------------------------------------------------------------------------
/src/propertiesValidator.js:
--------------------------------------------------------------------------------
1 | const Markup = require('telegraf/markup');
2 | const Extra = require('telegraf/extra');
3 | const keyboards = require('./keyboards');
4 | const properties = require('./moonbotProperties');
5 | const ptProperties = require('./PTProperties');
6 | const logger = require('./moonbotLogger');
7 |
8 | class PropertiesValidator {
9 |
10 | static validate() {
11 | PropertiesValidator.backwardCompatible();
12 |
13 | let result = PropertiesValidator.checkProp('bot.user_id', true);
14 | result = result && PropertiesValidator.checkProp('bot.token', true);
15 |
16 | if (result) {
17 | let isnum = /^\d+$/.test(properties.get('bot.user_id'));
18 | if (!isnum) {
19 | logger.error("Wrong bot.user_id value, should be numbers only, use https://t.me/MyTelegramID_bot")
20 | }
21 | }
22 | PropertiesValidator.buildKeyboards();
23 |
24 | return result;
25 | }
26 |
27 | static buildKeyboards() {
28 | PropertiesValidator.buildMainKeyboard();
29 | }
30 |
31 | static buildMainKeyboard() {
32 | let row1 = [];
33 | let row2 = [];
34 | let row3 = [];
35 | let keys = [row1, row2, row3];
36 |
37 | if (PropertiesValidator.isPoloniexAvailable()) {
38 | row1.push(Markup.callbackButton('Poloniex'));
39 | }
40 |
41 | if (PropertiesValidator.isBittrexAvailable()) {
42 | row1.push(Markup.callbackButton('Bittrex'));
43 | }
44 |
45 | if (PropertiesValidator.isBinanceAvailable()) {
46 | row1.push(Markup.callbackButton('Binance'));
47 | }
48 | row1.push(Markup.callbackButton('Summary'));
49 |
50 | //row2
51 | if (PropertiesValidator.checkProp('pt.server.api_token', false, false)) {
52 | row2.push(Markup.callbackButton('PBL 👀'));
53 | row2.push(Markup.callbackButton('Pairs 💼'));
54 | row2.push(Markup.callbackButton('DCA 💰'));
55 | row2.push(Markup.callbackButton('Sales 💸'));
56 | }
57 |
58 | row3.push(Markup.callbackButton('Bot Settings 🤖'));
59 | if (PropertiesValidator.checkProp('pt.server.api_token', false, false)) {
60 | if (PropertiesValidator.checkProp('pt.feeder.directory.path', false, false)) {
61 | row3.push(Markup.callbackButton('PT/PTF Settings 🛠'));
62 | if (properties.get('pt.feeder.show.pairs')) {
63 | keyboards.ptPtFSettings = Extra.markup(Markup.keyboard([
64 | Markup.callbackButton('Pairs'),
65 | Markup.callbackButton('DCA'),
66 | Markup.callbackButton('Indicators'),
67 | Markup.callbackButton('appsettings'),
68 | Markup.callbackButton('hostsettings'),
69 | Markup.callbackButton('⛔️ Toggle SOM ⛔️'),
70 | Markup.callbackButton('Cancel')
71 | ]));
72 | }
73 | }
74 | else {
75 | row3.push(Markup.callbackButton('PT Settings 🛠'));
76 | }
77 | }
78 |
79 | // let large = ? properties.get('moonbot.large.keyboard') : false;
80 | keyboards.mainKeyboard = Extra.markup(Markup.keyboard(keys).resize(!properties.get('moonbot.large.keyboard')));
81 | }
82 |
83 |
84 | static checkProp(prop, error = false, print = true) {
85 | if (!properties.get(prop)) {
86 | if (print) {
87 | let message = "Missing property " + prop + " in application.properties file";
88 | error ? logger.error(message) : logger.warn(message);
89 | }
90 | return false;
91 | }
92 | return true;
93 | }
94 |
95 | static isBinanceAvailable() {
96 | return PropertiesValidator.checkProp('binance.api.key', false, false) && PropertiesValidator.checkProp('binance.api.secret', false, false);
97 | }
98 |
99 | static isBittrexAvailable() {
100 | return PropertiesValidator.checkProp('bittrex.api.key', false, false) && PropertiesValidator.checkProp('bittrex.api.secret', false, false);
101 | }
102 |
103 | static isPoloniexAvailable() {
104 | return PropertiesValidator.checkProp('poloniex.api.key', false, false) && PropertiesValidator.checkProp('poloniex.api.secret', false, false);
105 | }
106 |
107 | static backwardCompatible() {
108 | if (!PropertiesValidator.checkProp('pt.feeder.directory.path', false, false) && PropertiesValidator.checkProp('pt.feeder.config.path', false, false)) {
109 | properties.setProperty('pt.feeder.directory.path', properties.get('pt.feeder.config.path').replace(/\/config\/*/, ''));
110 | }
111 | logger.debug("pt.feeder.directory.path: " + properties.get('pt.feeder.directory.path'));
112 |
113 | if (!PropertiesValidator.checkProp('profit.trailer.directory.path', false, false)) {
114 | properties.setProperty('profit.trailer.directory.path', '../');
115 | logger.warn("profit.trailer.directory.path is not set, default to '../'");
116 | }
117 | logger.debug("profit.trailer.directory.path: " + properties.get('profit.trailer.directory.path'));
118 |
119 | if (!PropertiesValidator.checkProp('profit.trailer.host', false, false)) {
120 | logger.warn("profit.trailer.host is not set, default to '127.0.0.1'");
121 | properties.setProperty('profit.trailer.host', '127.0.0.1');
122 | }
123 | logger.debug("profit.trailer.host: " + properties.get('profit.trailer.host'));
124 |
125 | if (!PropertiesValidator.checkProp('moonbot.market', false, false)) {
126 | logger.warn("moonbot.market is not set, default to 'BTC'");
127 | properties.setProperty('moonbot.market', 'BTC');
128 | } else {
129 | let market = properties.get('moonbot.market').toUpperCase().trim();
130 | if (market !== 'USDT' && market !== 'ETH' && market !== 'BTC') {
131 | logger.error("moonbot.market Market not supported: " + market);
132 | }
133 | }
134 | properties.setProperty('moonbot.market', properties.get('moonbot.market').toUpperCase());
135 | logger.debug("moonbot.market: " + properties.get('moonbot.market'));
136 |
137 | if (!PropertiesValidator.checkProp('profit.trailer.port', false, false)) {
138 | if (ptProperties.get('server.port')) {
139 | properties.setProperty('profit.trailer.port', ptProperties.get('server.port'));
140 | }
141 | else {
142 | logger.warn("server.port is not set, default to '8081'");
143 | properties.setProperty('profit.trailer.port', '8081');
144 | }
145 | }
146 | logger.debug("profit.trailer.port: " + properties.get('profit.trailer.port'));
147 |
148 | if (!PropertiesValidator.checkProp('pt.server.api_token', false, false)) {
149 | if (ptProperties.get('server.api_token')) {
150 | properties.setProperty('pt.server.api_token', ptProperties.get('server.api_token'));
151 | }
152 | else {
153 | logger.error("server.api_token is not set, please set server.api_token=pt_api_token at Profit Trailer's application.properties");
154 | }
155 | }
156 | }
157 | }
158 |
159 | module.exports = PropertiesValidator;
160 |
161 |
--------------------------------------------------------------------------------
/src/engines/ptJsonEngine.js:
--------------------------------------------------------------------------------
1 | const properties = require('../moonbotProperties');
2 | const ptProperties = require('../PTProperties');
3 | const logger = require('../moonbotLogger');
4 | const jsonfile = require('jsonfile');
5 | const path = require('path');
6 | const request = require("request");
7 | const axios = require('axios');
8 | const filesUtil = require('../filesUtil');
9 | const isDemo = !!properties.get('bot.demo.mode');
10 |
11 |
12 | let data = {};
13 | let on = false;
14 |
15 |
16 | async function runWithJsonFile(init = false) {
17 | let pathPrefix = '';
18 | return new Promise((resolve, reject) => {
19 | try {
20 | pathPrefix = properties.get('profit.trailer.directory.path');
21 | data = jsonfile.readFileSync(path.normalize(pathPrefix + "/ProfitTrailerData.json"));
22 | }
23 | catch (err) {
24 | reject(err);
25 | }
26 | resolve();
27 | }).then(() => {
28 | if (init) {
29 | setTimeout(runWithJsonFile, 1000 * 30, true);
30 | }
31 | else {
32 | setTimeout(runWithAPI, 1000 * 30);
33 | }
34 | }).catch((err) => {
35 | if (err.code === 'ENOENT') {
36 | logger.error("ProfitTrailerData.json could not be found at:" + pathPrefix);
37 | }
38 | logger.debug("runWithJsonFile: " + err);
39 | if (init) {
40 | setTimeout(runWithJsonFile, 1000 * 15, true);
41 | }
42 | else {
43 | setTimeout(runWithAPI, 1000 * 15);
44 | }
45 | });
46 | }
47 |
48 |
49 | let getPtUrl = function () {
50 | let host = properties.get('profit.trailer.host') ? properties.get('profit.trailer.host') : '127.0.0.1';
51 | let port = properties.get('profit.trailer.port') ? properties.get('profit.trailer.port') : ptProperties.get('server.port');
52 | let ssl = properties.get('profit.trailer.use.ssl') ? properties.get('profit.trailer.use.ssl') : false;
53 | let method = ssl ? 'https' : 'http';
54 | return method + '://' + host + ':' + port;
55 | };
56 |
57 |
58 | async function runWithAPI() {
59 | let nextReload = 1000 * 10;
60 | return new Promise((resolve, reject) => {
61 | let url = getPtUrl();
62 | let token = properties.get('pt.server.api_token');
63 |
64 | axios.get(url + '/api/data?token=' + token, {
65 | headers: {
66 | 'cache-control': 'no-cache',
67 | }
68 | }).then(response => {
69 | try {
70 | let jsondata = Object.assign({}, response.data);
71 | let mysummaryData = {
72 | balance: jsondata.realBalance,
73 | totalPairsCurrentValue: jsondata.totalPairsCurrentValue,
74 | totalPairsRealCost: jsondata.totalPairsRealCost,
75 | startBalance: jsondata.startBalance,
76 | totalDCACurrentValue: jsondata.totalDCACurrentValue,
77 | totalDCARealCost: jsondata.totalDCARealCost,
78 | totalPendingCurrentValue: jsondata.totalPendingCurrentValue,
79 | totalPendingTargetPrice: jsondata.totalPendingTargetPrice,
80 | totalProfitYesterday: jsondata.totalProfitYesterday,
81 | totalProfitToday: jsondata.totalProfitToday,
82 | totalProfitWeek: jsondata.totalProfitWeek,
83 | sellOnlyMode: jsondata.settings.sellOnlyMode,
84 | sellOnlyModeOverride: jsondata.settings.sellOnlyModeOverride,
85 | BTCUSDTPercChange: jsondata.BTCUSDTPercChange,
86 | ETHUSDTPercChange: jsondata.ETHUSDTPercChange,
87 | activeConfig: jsondata.settings.activeConfig,
88 | availableConfigs: jsondata.settings.availableConfigs,
89 | summaryString: '' + jsondata.bbBuyLogData.length + '-' + jsondata.gainLogData.length + '-' + jsondata.dcaLogData.length
90 | };
91 |
92 | data.summaryData = Object.assign({}, mysummaryData);
93 | properties.setRunningProperty('active.config', mysummaryData.activeConfig);
94 | data.sellLogData = Object.assign({}, jsondata.sellLogData);
95 | data.gainLogData = Object.assign({}, jsondata.gainLogData);
96 | data.dcaLogData = Object.assign({}, jsondata.dcaLogData);
97 | data.bbBuyLogData = Object.assign({}, jsondata.bbBuyLogData);
98 | data.pendingLogData = Object.assign({}, jsondata.pendingLogData);
99 | data.watchModeLogData = Object.assign({}, jsondata.watchModeLogData);
100 | loadActiveConfig();
101 | resolve(nextReload);
102 | }
103 | catch (error) {
104 | reject(error);
105 | }
106 | }).catch(error => {
107 | reject(error);
108 | });
109 | }).then((nextReload) => {
110 | setTimeout(runWithAPI, nextReload);
111 | }).catch((err) => {
112 | logger.debug("API request failed, fallback to Json Data");
113 | runWithJsonFile();
114 | });
115 | }
116 |
117 |
118 | async function loadActiveConfig() {
119 | if (isDemo) return;
120 | let url = getPtUrl();
121 | let license = ptProperties.get('license');
122 | let activeConfig = properties.getRunningProperty('active.config');
123 | let configPath = 'cache/';
124 |
125 | let options = {
126 | method: 'POST',
127 | url: url + '/settingsapi/settings/load',
128 | timeout: 10000,
129 | headers: {
130 | 'cache-control': 'no-cache',
131 | 'content-type': 'application/x-www-form-urlencoded'
132 | },
133 | form: {
134 | fileName: '',
135 | configName: activeConfig,
136 | license: license
137 | }
138 | };
139 |
140 | let callRequest = function (type) {
141 | options.form.fileName = type;
142 | request(options, function (error, response, body) {
143 | if (error) {
144 | logger.debug("Failed load settings: " + response);
145 | }
146 | try {
147 | filesUtil.writeToFile(configPath + type + '.properties', JSON.parse(body));
148 | }
149 | catch (error) {
150 | logger.debug("callRequest: " + error);
151 | }
152 |
153 | });
154 | };
155 |
156 | try {
157 | callRequest('PAIRS');
158 | callRequest('DCA');
159 | callRequest('INDICATORS');
160 | }
161 | catch (err) {
162 | logger.debug("Failed loadActiveConfig: " + err);
163 | }
164 | }
165 |
166 |
167 | // Public
168 |
169 | module.exports = {
170 |
171 | sellLogData: function sellLogData() {
172 | let result = data && data.sellLogData ? data.sellLogData : {};
173 | return Object.assign({}, result);
174 | },
175 |
176 | gainLogData: function gainLogData() {
177 | let result = data && data.gainLogData ? data.gainLogData : {};
178 | return Object.assign({}, result);
179 | },
180 |
181 | pendingLogData: function pendingLogData() {
182 | let result = data && data.pendingLogData ? data.pendingLogData : {};
183 | return Object.assign({}, result);
184 | },
185 |
186 | watchModeLogData: function watchModeLogData() {
187 | let result = data && data.watchModeLogData ? data.watchModeLogData : {};
188 | return Object.assign({}, result);
189 | },
190 |
191 | dcaLogData: function dcaLogData() {
192 | let result = data && data.dcaLogData ? data.dcaLogData : {};
193 | return Object.assign({}, result);
194 | },
195 |
196 | bbBuyLogData: function bbBuyLogData() {
197 | let result = data && data.bbBuyLogData ? data.bbBuyLogData : {};
198 | return Object.assign({}, result);
199 | },
200 |
201 | summaryData: function summaryData() {
202 | let result = data && data.summaryData ? data.summaryData : {};
203 | return Object.assign({}, result);
204 | },
205 |
206 | start: function start() {
207 | if (!on) {
208 | on = true;
209 | logger.info("Running PT DATA engine");
210 | try {
211 | if (properties.get('pt.server.api_token')) {
212 | logger.debug("Running PT API engine");
213 | runWithAPI();
214 | } else {
215 | logger.debug("Running PT JSON file engine");
216 | runWithJsonFile(true);
217 | }
218 | }
219 | catch (err) {
220 | logger.debug("Fallback to PT JSON file engine");
221 | runWithJsonFile();
222 | }
223 | }
224 |
225 | },
226 |
227 | async saveActiveConf(type) {
228 | if (isDemo) {
229 | return;
230 | }
231 |
232 | try {
233 | let url = getPtUrl();
234 | let pathPrefix = 'cache/';
235 | let settingPath = path.normalize(pathPrefix + '/' + type.toUpperCase() + ".properties.new");
236 | let content = filesUtil.readFile(settingPath);
237 | let license = ptProperties.get('license');
238 |
239 | logger.debug("saving active conf: " + type);
240 | let options = {
241 | method: 'POST',
242 | url: url + '/settingsapi/settings/save',
243 | timeout: 10000,
244 | headers: {
245 | 'cache-control': 'no-cache',
246 | 'content-type': 'application/x-www-form-urlencoded'
247 | },
248 | form: {
249 | fileName: type.toUpperCase(),
250 | configName: properties.getRunningProperty('active.config'),
251 | saveData: content,
252 | license: license
253 | }
254 | };
255 |
256 | try {
257 | request(options, function (error, response, body) {
258 | if (error) {
259 | logger.debug("Failed saving config: " + response);
260 | }
261 | loadActiveConfig();
262 | });
263 | }
264 | catch (err) {
265 | logger.debug("Failed saving config (2): " + response);
266 | }
267 |
268 | } catch (err) {
269 | logger.error("Failure at saveActiveConf: " + err);
270 | }
271 | }
272 |
273 | };
274 |
275 |
276 |
--------------------------------------------------------------------------------
/src/handlers/balanceHandler.js:
--------------------------------------------------------------------------------
1 | const properties = require('../moonbotProperties');
2 | const PropertiesValidator = require('../propertiesValidator');
3 | const logger = require('../moonbotLogger');
4 | let moonBotMarket = properties.get('moonbot.market');
5 |
6 |
7 | class BalanceHandler {
8 |
9 |
10 | handleBalanceMessage(data) {
11 |
12 | let response = '';
13 | try {
14 | if (PropertiesValidator.isBittrexAvailable() || PropertiesValidator.isBinanceAvailable()) {
15 | response = response.concat(this.buildExchSummary(data))
16 | } else {
17 | response = 'Set api keys for extended summary and more data'
18 | }
19 |
20 | if (properties.get('pt.server.api_token')) {
21 | response = response.concat(this.buildPTSummary(data))
22 | }
23 | } catch (err) {
24 | logger.debug("handleBalanceMessage falied: " + err);
25 | throw err;
26 | }
27 |
28 | return response;
29 | }
30 |
31 | buildPTSummary(data) {
32 | if (!data.ptData.balance) {
33 | return '';
34 | }
35 |
36 | let summaryData = {
37 | balance: data.ptData.balance,
38 | totalPairsCurrentValue: data.ptData.totalPairsCurrentValue,
39 | totalPairsRealCost: data.ptData.totalPairsRealCost,
40 | totalDCACurrentValue: data.ptData.totalDCACurrentValue,
41 | totalDCARealCost: data.ptData.totalDCARealCost,
42 | totalPendingCurrentValue: data.ptData.totalPendingCurrentValue,
43 | totalPendingTargetPrice: data.ptData.totalPendingTargetPrice,
44 | totalProfitYesterday: data.ptData.totalProfitYesterday,
45 | totalProfitToday: data.ptData.totalProfitToday,
46 | totalProfitWeek: data.ptData.totalProfitWeek,
47 | sellOnlyMode: data.ptData.sellOnlyMode,
48 | sellOnlyModeOverride: data.ptData.sellOnlyModeOverride,
49 | ETHUSDTPercChange: data.ptData.ETHUSDTPercChange,
50 | BTCUSDTPercChange: data.ptData.BTCUSDTPercChange,
51 | startBalance: data.ptData.startBalance,
52 | summaryString: data.ptData.summaryString
53 | };
54 |
55 | let tcv = summaryData.balance + summaryData.totalDCACurrentValue + summaryData.totalPairsCurrentValue + summaryData.totalPendingCurrentValue;
56 | let startBalance = summaryData.startBalance;
57 |
58 |
59 | let headerMessage = "\n🔸\nProfit Trailer " + summaryData.summaryString;
60 | let balanceMessage = "\nBAL:" + summaryData.balance.toFixed(3);
61 | let values = "\nTCV: " + tcv.toFixed(3) + " | SB: " + startBalance.toFixed(3);
62 | let profits = "\nProfits: TD: " + summaryData.totalProfitToday.toFixed(3) + " | YD: " + summaryData.totalProfitYesterday.toFixed(3);
63 | let somos = "\nSOM: " + summaryData.sellOnlyMode + " | SOMO: " + summaryData.sellOnlyModeOverride;
64 | let trend = '';
65 |
66 | if (moonBotMarket === 'ETH') {
67 | if (summaryData.ETHUSDTPercChange) {
68 | let rate = summaryData.ETHUSDTPercChange * 100;
69 | trend = "\nETH 24h trend: ".concat(rate.toFixed(2) + "%");
70 | }
71 | }
72 | else if (summaryData.BTCUSDTPercChange) {
73 | let rate = summaryData.BTCUSDTPercChange * 100;
74 | trend = "\nBTC 24h trend: ".concat(rate.toFixed(2) + "%");
75 | }
76 |
77 | let response = headerMessage;
78 | response = response.concat(balanceMessage);
79 | response = response.concat(values);
80 | response = response.concat(profits);
81 | response = response.concat(somos);
82 | response = response.concat(trend);
83 |
84 | return response;
85 | }
86 |
87 | getProfitsMessage(marketProfit, dolarProfit) {
88 | let profits = [];
89 | if (moonBotMarket !== 'USDT') {
90 | if (marketProfit) {
91 | profits.push(moonBotMarket + ": " + marketProfit.toFixed(1) + "%");
92 | }
93 | }
94 | if (dolarProfit) {
95 | profits.push("USD: " + dolarProfit.toFixed(1) + "%");
96 | }
97 |
98 |
99 | let profitsMessage = profits.join(" | ");
100 | profitsMessage = profitsMessage ? "\nProfits: " + profitsMessage : "";
101 | return profitsMessage;
102 | }
103 |
104 | calcBtcProfit(btcBalance) {
105 | let market = moonBotMarket ? moonBotMarket.toLowerCase() : 'btc';
106 | let btcInvestment = properties.get(market + '.investment');
107 |
108 | if (btcInvestment) {
109 | return ((btcBalance - btcInvestment) / btcInvestment) * 100;
110 | }
111 |
112 | return null;
113 | }
114 |
115 | calcUsdProfit(usdBalance) {
116 |
117 | let usdInvestment = properties.get('usd.investment');
118 |
119 | if (usdInvestment) {
120 | return ((usdBalance - usdInvestment) / usdInvestment) * 100;
121 | }
122 |
123 | return null;
124 | }
125 |
126 | getBinanceBalance(data) {
127 | if (!PropertiesValidator.isBinanceAvailable()) {
128 | return 0;
129 | }
130 |
131 | let balances = data.binance.balances;
132 | let markets = data.binance.markets;
133 | let marketRate = data.coinbaseRate;
134 | let binanceMarketBalance = 0;
135 |
136 | try {
137 | for (let curr in balances) {
138 | if (curr == moonBotMarket) {
139 | binanceMarketBalance += balances[curr].balance;
140 | }
141 | else if (curr == 'USDT') {
142 | try {
143 | binanceMarketBalance += balances[curr].balance / marketRate;
144 | }
145 | catch (err) {
146 | }
147 | }
148 | else {
149 | if (markets[curr]) {
150 | let b = balances[curr].balance * markets[curr].ask;
151 | binanceMarketBalance += b;
152 | }
153 | }
154 | }
155 | } catch (err) {
156 | logger.warn("getBinanceBalance" + err);
157 | }
158 |
159 | return binanceMarketBalance;
160 |
161 | // return PropertiesValidator.isBinanceAvailable() ? data.binance.binanceBalance : 0;
162 | }
163 |
164 | getBittrexBalance(data) {
165 | if (!PropertiesValidator.isBittrexAvailable()) {
166 | return 0;
167 | }
168 |
169 | let balances = data.bittrex.balances;
170 | let markets = data.bittrex.markets;
171 | let marketRate = data.coinbaseRate;
172 | let bittrexMarketBalance = 0;
173 |
174 | for (let curr in balances) {
175 | if (curr == moonBotMarket) {
176 | bittrexMarketBalance += balances[curr].balance;
177 | }
178 | else if (curr == 'USDT') {
179 | try {
180 | bittrexMarketBalance += balances[curr].balance / marketRate;
181 | }
182 | catch (err) {
183 | }
184 | }
185 | else {
186 | if (markets[curr]) {
187 | let b = balances[curr].balance * markets[curr].ask;
188 | bittrexMarketBalance += b;
189 | }
190 | }
191 | }
192 |
193 | return bittrexMarketBalance;
194 | }
195 |
196 | getBinanceInTrade(data) {
197 | if (!PropertiesValidator.isBinanceAvailable()) {
198 | return 0;
199 | }
200 | let binanceInTrade = 0;
201 |
202 | try {
203 | let balances = data.binance.balances;
204 | let markets = data.binance.markets;
205 |
206 | for (let curr in balances) {
207 | if (curr != moonBotMarket && markets[curr]) {
208 | let b = 0;
209 | if (curr == 'USDT') {
210 | b = balances[curr].balance / markets[curr].ask;
211 | } else {
212 | b = balances[curr].balance * markets[curr].ask;
213 | }
214 | binanceInTrade += b;
215 | }
216 | }
217 |
218 | } catch (err) {
219 | logger.warn("Couldnt get binance trades" + err);
220 | }
221 | return binanceInTrade;
222 | }
223 |
224 | getBittrexInTrade(data) {
225 | if (!PropertiesValidator.isBittrexAvailable()) {
226 | return 0;
227 | }
228 |
229 | let balances = data.bittrex.balances;
230 | let markets = data.bittrex.markets;
231 | let bittrexInTrade = 0;
232 |
233 | for (let curr in balances) {
234 | if (curr != moonBotMarket && markets[curr]) {
235 | let b = 0;
236 | if (curr == 'USDT') {
237 | b = balances[curr].balance / markets[curr].ask;
238 | } else {
239 | b = balances[curr].balance * markets[curr].ask;
240 | }
241 | bittrexInTrade += b;
242 | }
243 | }
244 |
245 | return bittrexInTrade;
246 | }
247 |
248 | buildExchSummary(data) {
249 | let bittrexInTrade = this.getBittrexInTrade(data);
250 | let binanceInTrade = this.getBinanceInTrade(data);
251 |
252 | let marketBalance = this.getBittrexBalance(data) + this.getBinanceBalance(data);
253 | let inTradeBalance = bittrexInTrade + binanceInTrade;
254 |
255 | let usdBalance = marketBalance * data.coinbaseRate;
256 | marketBalance = marketBalance === 0 ? 0.000000000001 : marketBalance;
257 | let inTradeRate = (inTradeBalance.toFixed(8) / marketBalance.toFixed(8)) * 100;
258 |
259 | let btcProfit = 0;
260 | let balanceMessage = '';
261 | let usdbtcRate = '';
262 | let toFixed = 8;
263 |
264 | if (moonBotMarket !== 'USDT') {
265 | btcProfit = this.calcBtcProfit(marketBalance);
266 | balanceMessage = "Current Balance: " + marketBalance.toFixed(3) + " " + moonBotMarket + ", " + usdBalance.toFixed(0) + " $";
267 | usdbtcRate = "\n" + moonBotMarket + "\\USD: " + data.coinbaseRate
268 | }
269 | else {
270 | balanceMessage = "Current Balance: " + usdBalance.toFixed(0) + " $";
271 | toFixed = 2;
272 | }
273 | let dolarProfit = this.calcUsdProfit(usdBalance);
274 | let mcapb = data.total_market_cap_usd / 1000000000;
275 | let profitsMessage = this.getProfitsMessage(btcProfit, dolarProfit);
276 | let inTradeMessage = "\n" + inTradeBalance.toFixed(toFixed) + " in trade, " + inTradeRate.toFixed(0) + "%";
277 | let mcap = "\nMarket Cap: " + mcapb.toFixed(2) + "B";
278 | let binanceTrade = PropertiesValidator.isBinanceAvailable() ? "\nBinance total tradings: " + binanceInTrade.toFixed(toFixed) : "";
279 | let bittrexTrade = PropertiesValidator.isBittrexAvailable() ? "\nBittrex total tradings: " + bittrexInTrade.toFixed(toFixed) : "";
280 | let response = '';
281 |
282 | response = response.concat(balanceMessage);
283 | response = response.concat(profitsMessage);
284 | response = response.concat(inTradeMessage);
285 | response = response.concat(bittrexTrade);
286 | response = response.concat(binanceTrade);
287 | response = response.concat(mcap);
288 | response = response.concat(usdbtcRate);
289 | return response;
290 | }
291 | }
292 |
293 | module.exports = BalanceHandler;
--------------------------------------------------------------------------------
/src/monitor.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 | const keyboards = require('./keyboards');
3 | const properties = require('./moonbotProperties');
4 | const logger = require('./moonbotLogger');
5 | const Tail = require('tail').Tail;
6 | const path = require('path');
7 | const Engine = require('./engines/engine');
8 | const PtJsonDataHandler = require('./handlers/ptJsonDataHandler');
9 | let moonBotMarket = properties.get('moonbot.market');
10 |
11 | let telegram;
12 | let ptDataHandler;
13 | let engine;
14 | let lastPrice = 0;
15 | let snooze = false;
16 | let snoozePercentChange = false;
17 | let snoozebnb = false;
18 | let health_check_count = 3;
19 | let filterList = ["BinanceWebSocketAdapterKline - onWebSocketError: Timeout on Read"];
20 | let lastLogError = '';
21 |
22 | let notificationsContext = {
23 | previousSellsLength: 0,
24 | alreadyNotified: [],
25 | allBuysByDate: []
26 | };
27 |
28 | let prevNotificationsData = {};
29 |
30 | function notifySales(init = false) {
31 | let data = Engine.getCurrentSellLogData();
32 | let sellLogData = data.sellLogData;
33 |
34 | let arr = [];
35 | for (let i in sellLogData) {
36 | arr.push(sellLogData[i]);
37 | }
38 |
39 | if (arr.length > notificationsContext.previousSellsLength && !init) {
40 | let diff = arr.length - notificationsContext.previousSellsLength;
41 | for (let i = 0; i < arr.length && i < diff; i++) {
42 | notificationsContext.allBuysByDate[arr[i].market] = null;
43 | }
44 | notify(ptDataHandler.handleSales(data, diff, true));
45 | }
46 | notificationsContext.previousSellsLength = arr.length;
47 | }
48 |
49 | function notify(message) {
50 | if (!properties.get('profit.trailer.disable.notifications')) {
51 | telegram.sendMessage(properties.get('bot.user_id'), message);
52 | }
53 | let notifyTo = properties.get('moonbot.notify.groups');
54 | try {
55 | if (notifyTo) {
56 | let groupsTo = notifyTo.split(',');
57 | for (let i in groupsTo) {
58 | if (groupsTo[i] !== '') {
59 | telegram.sendMessage(groupsTo[i], message);
60 | }
61 | }
62 | }
63 | }
64 | catch (err) {
65 | logger.error("Could not notify groups: " + notifyTo)
66 | }
67 | }
68 |
69 | function notifyGains(init = false) {
70 | let data = Engine.getCurrentPairsLogData();
71 | let gainLogData = prevNotificationsData.gainLogData;
72 |
73 | let arr = [];
74 | for (let i in gainLogData) {
75 | let item = gainLogData[i];
76 | if (!notificationsContext.allBuysByDate[item.market]) {
77 | notificationsContext.alreadyNotified[item.market] = true;
78 | arr.push(item);
79 | }
80 | }
81 |
82 | if (arr.length > 0 && !init) {
83 | let cutme = {coinbaseRate: data.coinbaseRate, gainLogData: arr, watchModeLogData: [], pendingLogData: []};
84 | notify(ptDataHandler.handlePairs(cutme, cutme.gainLogData.length, true));
85 | }
86 | }
87 |
88 | function notifyDcas(init = false) {
89 | let data = Engine.getCurrentDcaLogData();
90 | let dcaLogData = prevNotificationsData.dcaLogData;
91 |
92 | let arr = [];
93 | for (let i in dcaLogData) {
94 | let item = dcaLogData[i];
95 | if (!notificationsContext.alreadyNotified[item.market] && (!notificationsContext.allBuysByDate[item.market] ||
96 | (notificationsContext.allBuysByDate[item.market] && item.boughtTimes && item.boughtTimes >= 1 && item.boughtTimes > notificationsContext.allBuysByDate[item.market].boughtTimes))) {
97 | arr.push(item);
98 | }
99 | }
100 |
101 | if (arr.length > 0 && !init) {
102 | notify(ptDataHandler.handleDCA({coinbaseRate: data.coinbaseRate, dcaLogData: arr}, arr.length, true));
103 | }
104 | }
105 |
106 |
107 | async function watchForNotifications(init = false) {
108 | notificationsContext.alreadyNotified = [];
109 |
110 | let data = Engine.getCurrentPairsLogData();
111 | prevNotificationsData.dcaLogData = Engine.getCurrentDcaLogData().dcaLogData;
112 | prevNotificationsData.gainLogData = data.gainLogData;
113 | prevNotificationsData.watchModeLogData = data.watchModeLogData;
114 | prevNotificationsData.pendingLogData = data.pendingLogData;
115 |
116 | notifyGains(init);
117 | notifyDcas(init);
118 | notifySales(init);
119 |
120 | let addAllToArray = function (dataArr) {
121 | for (let i in dataArr) {
122 | let item = dataArr[i];
123 | notificationsContext.allBuysByDate[item.market] = item;
124 | }
125 | };
126 |
127 | addAllToArray(prevNotificationsData.gainLogData);
128 | addAllToArray(prevNotificationsData.watchModeLogData);
129 | addAllToArray(prevNotificationsData.pendingLogData);
130 | addAllToArray(prevNotificationsData.dcaLogData);
131 |
132 | }
133 |
134 | let getPtUrl = function () {
135 | let host = properties.get('profit.trailer.host') ? properties.get('profit.trailer.host') : '127.0.0.1';
136 | let port = properties.get('profit.trailer.port') ? properties.get('profit.trailer.port') : ptProperties.get('server.port');
137 | let ssl = properties.get('profit.trailer.use.ssl') ? properties.get('profit.trailer.use.ssl') : false;
138 | let method = ssl ? 'https' : 'http';
139 | return method + '://' + host + ':' + port;
140 | };
141 |
142 |
143 | function ptWatcher() {
144 | let interval = 1000 * 30;
145 | let url = getPtUrl();
146 | axios({
147 | method: 'GET',
148 | url: url,
149 | timeout: 5000
150 | }).then(response => {
151 | health_check_count = 3;
152 | setTimeout(ptWatcher, interval);
153 | }).catch(error => {
154 | logger.debug("ptWatcher: " + error);
155 | health_check_count--;
156 |
157 | if (!snooze) {
158 | if (health_check_count <= 0 && !properties.get('pt.monitor.disabled')) {
159 | telegram.sendMessage(properties.get('bot.user_id'), "MoonBot Monitor:\nProfitTrailer health-check failed! /snooze");
160 | }
161 | }
162 | else {
163 | interval = 1000 * 60 * 30;
164 | health_check_count = 3;
165 | snooze = false;
166 | }
167 | setTimeout(ptWatcher, interval);
168 | });
169 | }
170 |
171 | function btWatcher() {
172 | let interval = properties.get('monitor.btc.interval') ? properties.get('monitor.btc.interval') + 0 : 1;
173 | new Promise(function (resolve, reject) {
174 | axios.get('https://api.coinbase.com/v2/prices/spot?currency=USD')
175 | .then(response => {
176 | let rate = response.data.data.amount;
177 | resolve(rate)
178 | })
179 | .catch(error => {
180 | reject(error);
181 | });
182 | }).then(currentPrice => {
183 | if (lastPrice === 0) {
184 | lastPrice = currentPrice;
185 | }
186 | else {
187 | let rate = currentPrice / properties.get('monitor.btc.watch_rate');
188 | let previousRate = lastPrice / properties.get('monitor.btc.watch_rate');
189 | rate = Math.floor(rate);
190 | previousRate = Math.floor(previousRate);
191 |
192 | if (rate > previousRate) {
193 | telegram.sendMessage(properties.get('bot.user_id'), "MoonBot Monitor:\n✅ BTC is up " + lastPrice + " -> " + currentPrice);
194 | }
195 | else if (rate < previousRate) {
196 | telegram.sendMessage(properties.get('bot.user_id'), "MoonBot Monitor:\n🔻 BTC is down: " + lastPrice + " -> " + currentPrice);
197 | }
198 |
199 | lastPrice = currentPrice;
200 | }
201 | setTimeout(btWatcher, interval * 60 * 1000);
202 | }).catch(error => {
203 | logger.debug("btWatcher: " + error);
204 | setTimeout(btWatcher, 1000);
205 | });
206 | }
207 |
208 | function isErrorToAlert(line) {
209 | let result;
210 |
211 | result = line.indexOf("ERROR") >= 0;
212 | for (let lineFilter in filterList) {
213 | result = result && !(line.indexOf(filterList[lineFilter]) >= 0);
214 | if (!result) {
215 | return result;
216 | }
217 | }
218 |
219 | return result;
220 | }
221 |
222 | function logWatcher() {
223 | let logpath = '';
224 | let tail = undefined;
225 | try {
226 | logpath = path.normalize(properties.get('profit.trailer.directory.path') + '/logs/ProfitTrailer.log');
227 | let options = {fromBeginning: false, useWatchFile: true, follow: true};
228 |
229 | tail = new Tail(logpath, options);
230 |
231 | tail.on("line", function (line) {
232 | if (isErrorToAlert(line) && !properties.get('profit.trailer.disable.logwatcher')) {
233 | telegram.sendMessage(properties.get('bot.user_id'), "MoonBot Log Watcher:\nFound error in ProfitTrailer log!\n" + line + "\n\nAdd to ignore list /ignore\n\nToggle SOM: /togglesom", keyboards.mainKeyboard);
234 | lastLogError = line.substr(line.indexOf('ERROR') + 5, line.length).trim();
235 | }
236 | });
237 | tail.on("error", function (error) {
238 | logger.debug("logWatcher error: " + err);
239 | throw error;
240 | });
241 | } catch (err) {
242 | if (err.code === 'ENOENT') {
243 | logger.error("ProfitTrailer.log could not be found at:" + logpath);
244 | }
245 | logger.debug("logWatcher: " + err);
246 | if (tail) {
247 | tail.unwatch();
248 | }
249 | setTimeout(logWatcher, 1000 * 60 * 10);
250 | }
251 | }
252 |
253 | function marketPercentageMonitor() {
254 | try {
255 | let ptSummaryData = Engine.getPtJsonSummaryData();
256 | let rate = 10000;
257 | let threshold = parseFloat(properties.get('monitor.percentage.change') ? properties.get('monitor.percentage.change') : 101);
258 |
259 | if (moonBotMarket === 'ETH') {
260 | if (ptSummaryData.ETHUSDTPercChange) {
261 | rate = ptSummaryData.ETHUSDTPercChange * 100;
262 | }
263 | }
264 | else if (ptSummaryData.BTCUSDTPercChange) {
265 | rate = ptSummaryData.BTCUSDTPercChange * 100;
266 | }
267 |
268 | if (rate < 10000 && !snoozePercentChange && Math.abs(rate) >= Math.abs(threshold)) {
269 | telegram.sendMessage(properties.get('bot.user_id'), "MoonBot Monitor:\nMarket percent change reached, current change: " + rate.toFixed(2) + "%\n/snoozeme");
270 | setTimeout(marketPercentageMonitor, 1000 * 60 * 60);
271 | }
272 | else if (snoozePercentChange) {
273 | snoozePercentChange = false;
274 | setTimeout(marketPercentageMonitor, 1000 * 60 * 60 * 5);
275 | } else {
276 | // setTimeout(marketPercentageMonitor, 1000 * 60 * 3);
277 | setTimeout(marketPercentageMonitor, 3000);
278 | }
279 | } catch (err) {
280 | logger.debug("marketPercentageMonitor: " + err);
281 | setTimeout(marketPercentageMonitor, 1000 * 60 * 60);
282 | }
283 | }
284 |
285 |
286 | function minimumBnbWatcher() {
287 | try {
288 | let binanceData = Engine.getCurrentBinanceData();
289 | let threshold = parseFloat(properties.get('monitor.bnb.minimum.balance') ? properties.get('monitor.bnb.minimum.balance') : 0.01);
290 |
291 | if (binanceData.binance.balances["BNB"] && binanceData.binance.markets["BNB"]) {
292 | let balance = binanceData.binance.balances["BNB"].balance * binanceData.binance.markets["BNB"].ask;
293 |
294 | if (!snoozebnb && balance <= threshold) {
295 | telegram.sendMessage(properties.get('bot.user_id'), "MoonBot Monitor:\nLow BNB balance " + balance.toFixed(8) + "\n/snoozebnb");
296 | setTimeout(minimumBnbWatcher, 1000 * 60 * 60);
297 | }
298 | else if (snoozebnb) {
299 | snoozebnb = false;
300 | setTimeout(minimumBnbWatcher, 1000 * 60 * 60 * 5);
301 | } else {
302 | setTimeout(minimumBnbWatcher, 1000 * 60 * 3);
303 | }
304 |
305 | } else {
306 | setTimeout(minimumBnbWatcher, 1000 * 60 * 3);
307 | }
308 | } catch (err) {
309 | logger.debug("minimumBnbWatcher: " + err);
310 | setTimeout(minimumBnbWatcher, 1000 * 60 * 60);
311 | }
312 |
313 | }
314 |
315 |
316 | class Monitor {
317 |
318 | constructor(mybot, myengine) {
319 | engine = myengine;
320 | telegram = mybot.telegram;
321 | ptDataHandler = new PtJsonDataHandler();
322 | }
323 |
324 | start() {
325 | try {
326 | if (properties.get('profit.trailer.host') && properties.get('profit.trailer.port')) {
327 | logger.info("Profit trailer watcher started");
328 | ptWatcher();
329 | }
330 | if (properties.get('monitor.btc.watch_rate')) {
331 | logger.info("BTC price watcher started");
332 | btWatcher();
333 | }
334 |
335 | if (properties.get('monitor.percentage.change')) {
336 | logger.info("Market percentage change monitor started");
337 | marketPercentageMonitor();
338 | }
339 |
340 | if (properties.get('monitor.bnb.minimum.balance')) {
341 | logger.info("BNB minimum value monitor started");
342 | minimumBnbWatcher();
343 | }
344 |
345 | if (properties.get('profit.trailer.directory.path')) {
346 | logger.info("PT log file watcher started");
347 | logWatcher();
348 |
349 | logger.info("PT Notification engine started");
350 | watchForNotifications(true);
351 | setInterval(watchForNotifications, 1000 * 15);
352 | }
353 |
354 | }
355 | catch (err) {
356 | logger.error("Monitor error: " + err);
357 | }
358 | }
359 |
360 | snoozeHC() {
361 | snooze = true;
362 | }
363 |
364 | snoozePercentChange() {
365 | snoozePercentChange = true;
366 | }
367 |
368 | snoozeBnb() {
369 | snoozebnb = true;
370 | }
371 |
372 | addToIgnoreList(reset = false) {
373 | if (reset) {
374 | filterList = [];
375 | }
376 | else {
377 | filterList.push(lastLogError);
378 | }
379 | }
380 |
381 |
382 | }
383 |
384 | module.exports = Monitor;
--------------------------------------------------------------------------------
/src/handlers/ptJsonDataHandler.js:
--------------------------------------------------------------------------------
1 | const properties = require('../moonbotProperties');
2 | const logger = require('../moonbotLogger');
3 | const moment = require('moment-timezone');
4 | let moonBotMarket = properties.get('moonbot.market');
5 | let toFixed = moonBotMarket === 'USDT' ? 2 : 8;
6 |
7 | class PtJsonDataHandler {
8 | constructor() {
9 | }
10 |
11 | handleSales(data, lastOf = null, notify = false) {
12 | let lessEmojis = properties.get('moonbot.less.emojis') ? properties.get('moonbot.less.emojis') : false;
13 |
14 | let coinbaseRate = data.coinbaseRate;
15 | let emojies = ['👍', '😁', '😇', '🤑', '💪', '💫', '🚀', '💰', '💵', '💴'];
16 |
17 | let sellLogData = data.sellLogData;
18 | if (lastOf == 0) {
19 | return lessEmojis ? 'NA' : '🤐';
20 | }
21 | if (!lastOf) {
22 | lastOf = properties.get('pt.sale.list.length') && (properties.get('pt.sale.list.length') > 0 && properties.get('pt.sale.list.length') <= 10) ? properties.get('pt.sale.list.length') : 5;
23 | }
24 |
25 |
26 | let arr = [];
27 | for (let i in sellLogData) {
28 | arr.push(sellLogData[i]);
29 | }
30 |
31 | arr = arr.sort(this.sortByTime);
32 |
33 | lastOf = lastOf > 10 ? 10 : lastOf;
34 |
35 | let result = '';
36 | if (notify) {
37 | result = lessEmojis ? 'Sold:\n\n' : '🔔 Sold:\n\n';
38 | }
39 | else {
40 | result = 'Recent ' + lastOf + ':\n\n';
41 | }
42 |
43 | for (let i = 0; i < arr.length && i < lastOf; i++) {
44 | try {
45 | let coin = arr[i].market.replace(moonBotMarket, '').replace('-', '');
46 | let totalBought = arr[i].averageCalculator.totalWeightedPrice * arr[i].soldAmount / arr[i].averageCalculator.totalAmount;
47 | let profitPrecentage = arr[i].profit;
48 | let totalSold = totalBought + (totalBought * profitPrecentage / 100);
49 | let numberOfBuys = arr[i].boughtTimes;
50 | let btcProfit = totalSold - totalBought;
51 | let usdVal = btcProfit * coinbaseRate;
52 | let isPanic = arr[i].sellStrategies[0].name == 'PANICSELL';
53 |
54 | let numberDCAs = numberOfBuys > 0 ? '(' + numberOfBuys + ')' : '';
55 | let emoji = lessEmojis ? '' : emojies[Math.floor(Math.random() * emojies.length)] + ' ';
56 | let soldDate = PtJsonDataHandler.getSoldDate(arr, i);
57 | let fixedProfit = moonBotMarket === 'USDT' ? '' : ' | ' + btcProfit.toFixed(toFixed);
58 | let revert_panicSell = isPanic ? '\n/revert_panicSell_' + coin : '';
59 | result = result.concat(emoji + ' /' + coin + ' | Val: ' + totalSold.toFixed(toFixed) + '' + numberDCAs + '');
60 | result = result.concat('\nProfits: ' + profitPrecentage + '%' + fixedProfit + ' | $' + usdVal.toFixed(2) + '');
61 | result = result.concat('\nRate: ' + arr[i].currentPrice);
62 | result = result.concat(soldDate);
63 | result = result.concat(revert_panicSell);
64 | result = result.concat('\n');
65 | result = result.concat('\n');
66 | }
67 | catch (err) {
68 | logger.debug("ERROR printing sell:" + err + '' + arr[i]);
69 | }
70 | }
71 |
72 | return result;
73 | }
74 |
75 | static getSoldDate(arr, i) {
76 | try {
77 | let ts = this.getTs(arr[i]);
78 | return "\nDate: " + moment.utc(ts).format("DD.MM.YYYY") + " | Time: " + moment.utc(ts).format("HH:mm");
79 | } catch (err) {
80 | logger.debug("getSoldDate: " + err);
81 | return '';
82 | }
83 |
84 | }
85 |
86 |
87 | static getTs(element) {
88 | let ts = new Date(Date.UTC(element.soldDate['date'].year, element.soldDate['date'].month - 1, element.soldDate['date'].day, element.soldDate['time'].hour, element.soldDate['time'].minute));
89 | let timezone = properties.get('moonbot.timezone.offset') ? properties.get('moonbot.timezone.offset') : "0";
90 | ts.setHours(ts.getHours() + parseFloat(timezone));
91 | return ts;
92 | }
93 |
94 | handlePairs(data, lastOf = null, notify = false) {
95 | let gainLogData = data.gainLogData;
96 | let pendingLogData = data.pendingLogData;
97 | let watchModeLogData = data.watchModeLogData;
98 | let lessEmojis = properties.get('moonbot.less.emojis') ? properties.get('moonbot.less.emojis') : false;
99 | let coinbaseRate = data.coinbaseRate;
100 |
101 | if (!gainLogData[0] && !pendingLogData[0] && !watchModeLogData[0]) {
102 | return 'Nothing there for now';
103 | }
104 |
105 | if (lastOf === 0) {
106 | return 'None';
107 | }
108 |
109 | let gainLogDataArr = this.getSorted(gainLogData);
110 | let pendingLogDataArr = this.getSorted(pendingLogData);
111 | let watchModeLogDataArr = this.getSorted(watchModeLogData);
112 |
113 | let allsize = gainLogDataArr.length + pendingLogDataArr.length + watchModeLogDataArr.length;
114 | let length = lastOf && lastOf <= allsize ? lastOf : allsize;
115 |
116 | let result = '';
117 | let pairs = '';
118 | if (notify) {
119 | pairs = lessEmojis ? 'Bought' : '🔔 Bought';
120 | }
121 | else {
122 | pairs = 'Pairs';
123 | }
124 | result = result.concat(this.buildList(pairs, gainLogDataArr, Math.min(gainLogDataArr.length, length), coinbaseRate));
125 | length = length - gainLogDataArr.length;
126 | result = result.concat(this.buildList('Pending', pendingLogDataArr, Math.min(pendingLogDataArr.length, length), coinbaseRate));
127 | length = length - pendingLogDataArr.length;
128 | result = result.concat(this.buildList('WatchMode', watchModeLogDataArr, Math.min(watchModeLogDataArr.length, length), coinbaseRate));
129 |
130 | if (result.length > 4000) {
131 | result = result.substr(0, 4000);
132 | result = result.concat('\n\nmessage is too long, trimming..');
133 | }
134 |
135 | if (!notify) {
136 | result = result.concat('\nTotal ' + gainLogDataArr.length + ' Pairs in trade');
137 | }
138 |
139 | return result;
140 | }
141 |
142 |
143 | handlePbl(data, lastOf = null, notify = false) {
144 | let bbBuyLogData = data.bbBuyLogData;
145 |
146 | lastOf = lastOf ? lastOf : 200;
147 | if (lastOf == 0) {
148 | return 'NA';
149 | }
150 |
151 | let arr = [];
152 | for (let i in bbBuyLogData) {
153 | arr.push(bbBuyLogData[i]);
154 | }
155 |
156 | let sortByTrueTrailing = function (a, b) {
157 | let strategiesLenghta = a.buyStrategies.length;
158 | let trueStrategiesa = 0;
159 | for (let k = 0; k < strategiesLenghta; k++) {
160 | trueStrategiesa += a.buyStrategies[k].positive != 'false' ? 1 : 0;
161 | }
162 |
163 | let strategiesLenghtb = b.buyStrategies.length;
164 | let trueStrategiesb = 0;
165 | for (let k = 0; k < strategiesLenghtb; k++) {
166 | trueStrategiesb += b.buyStrategies[k].positive != 'false' ? 1 : 0;
167 | }
168 |
169 | if (trueStrategiesa < trueStrategiesb)
170 | return 1;
171 | if (trueStrategiesa > trueStrategiesb)
172 | return -1;
173 | return 0;
174 | };
175 |
176 | arr = arr.sort(sortByTrueTrailing);
177 |
178 | lastOf = lastOf > arr.length ? arr.length : lastOf;
179 |
180 | let result = 'PBL: \n\n';
181 |
182 | for (let i = 0; i < arr.length && i < lastOf; i++) {
183 | try {
184 | let coin = arr[i].market.replace(moonBotMarket, '').replace('-', '');
185 | let currentResult = '';
186 |
187 | let currentPrice = arr[i].currentPrice;
188 | let last24hChange = arr[i].percChange * 100;
189 | let volume = arr[i].volume;
190 | let strategies = this.bstbsvgenerator(arr[i]);
191 | currentResult = currentResult.concat('/' + coin + ' | Price: ' + currentPrice.toFixed(toFixed));
192 | currentResult = currentResult.concat('\nVol: ' + volume.toFixed(0) + ' | 24h: ' + last24hChange.toFixed(2) + "% ");
193 | currentResult = currentResult.concat(strategies);
194 | currentResult = currentResult.concat('\n/disableTrading_' + coin);
195 | currentResult = currentResult.concat('\n');
196 | currentResult = currentResult.concat('\n');
197 |
198 | if (result.length + currentResult.length > 4000) {
199 | result = result.concat('message is too long, trimming..');
200 | break;
201 | }
202 | result = result.concat(currentResult);
203 | }
204 | catch (err) {
205 | logger.debug("ERROR printing PBL:" + err + '' + arr[i]);
206 | }
207 | }
208 |
209 | return result;
210 | }
211 |
212 | bstbsvgenerator(item, trailing = true) {
213 | let bsts = [];
214 | let bsvs = [];
215 | let trueStrategies = 0;
216 | let strategiesLenght = item.buyStrategies.length;
217 | for (let k = 0; k < strategiesLenght; k++) {
218 | if (item.buyStrategies[k].currentValue != 0) {
219 | bsts.push(item.buyStrategies[k].entryValue.toFixed(2));
220 | bsvs.push(item.buyStrategies[k].currentValue.toFixed(2));
221 | }
222 | trueStrategies += item.buyStrategies[k].positive != 'false' ? 1 : 0;
223 | }
224 |
225 | let bst = '\nBST: ' + bsts.join(', ');
226 | let bsv = '\nBSV: ' + bsvs.join(', ');
227 |
228 | let strategies = '';
229 | strategies = strategies.concat(trailing ? bst : '');
230 | strategies = strategies.concat(bsv);
231 | strategies = strategies.concat(trailing ? '\n' : ' | ');
232 | strategies = strategies.concat('BS: ' + trueStrategies + '/' + strategiesLenght);
233 | strategies = trailing && trueStrategies === strategiesLenght ? strategies.concat(' true trailing... 💹️') : strategies;
234 |
235 | if (bsts.length === 0 && bsvs.length === 0) {
236 | strategies = '\nStrategy NA'
237 | }
238 |
239 | return strategies;
240 | }
241 |
242 | buildList(head, list, length, coinbaseRate) {
243 | if (list.length > 0 && length > 0) {
244 | let result = head + ':\n\n';
245 |
246 | for (let i = 0; i < length; i++) {
247 | let coin = list[i].market.replace(moonBotMarket, '').replace('-', '');
248 | let totalCost = list[i].averageCalculator.totalWeightedPrice;
249 | let profitPrecentage = list[i].profit;
250 | let last24hChange = list[i].percChange * 100;
251 |
252 | let currentVal = totalCost + (totalCost * profitPrecentage / 100);
253 | if (currentVal === 0 && !list[i].averageCalculator.firstBoughtDate) {
254 | currentVal = list[i].averageCalculator.totalAmount * list[i].currentPrice;
255 | }
256 | let daysSince = this.calcDayDiff(list[i].averageCalculator.firstBoughtDate);
257 | let emoji = profitPrecentage > 0 ? '✅' : '🔻';
258 | let usdval = coinbaseRate * currentVal;
259 |
260 | result = result.concat(emoji + ' /' + coin + " (" + daysSince + 'D)' + ' | Amount: ' + list[i].averageCalculator.totalAmount.toFixed(2));
261 | result = result.concat('\nVal: ' + currentVal.toFixed(toFixed) + ' | $' + usdval.toFixed(2));
262 | result = result.concat('\nProfit: ' + profitPrecentage.toFixed(2) + "% | Cost: " + totalCost.toFixed(toFixed));
263 | result = result.concat('\nBid: ' + list[i].currentPrice.toFixed(toFixed) + ' | Vol: ' + list[i].volume.toFixed(2));
264 | result = result.concat('\n24h: ' + last24hChange.toFixed(1) + '%');
265 | result = result.concat(head == 'Pairs' ? '\n/panicSell_' + coin : '');
266 | result = result.concat('\n');
267 | result = result.concat('\n');
268 | }
269 |
270 | return result;
271 | }
272 | return '';
273 | }
274 |
275 |
276 | getSorted(gainLogData) {
277 | let itemsArr = [];
278 |
279 | for (let i in gainLogData) {
280 | itemsArr.push(gainLogData[i]);
281 | }
282 | itemsArr = itemsArr.sort(this.sortProfit);
283 | return itemsArr;
284 | }
285 |
286 | handleDCA(data, lastOf = null, notify = false) {
287 | let dcaLogData = data.dcaLogData;
288 | let lessEmojis = properties.get('moonbot.less.emojis') ? properties.get('moonbot.less.emojis') : false;
289 | let coinbaseRate = data.coinbaseRate;
290 |
291 | if (!dcaLogData[0]) {
292 | return lessEmojis ? 'No DCAs' : 'No DCAs 👍';
293 | }
294 | if (lastOf == 0) {
295 | return lessEmojis ? 'NA' : 'Really?! 🤔';
296 | }
297 |
298 | let itemsArr = [];
299 |
300 | for (let i in dcaLogData) {
301 | itemsArr.push(dcaLogData[i]);
302 | }
303 | itemsArr = itemsArr.sort(this.sortProfit);
304 | let length = lastOf && lastOf <= itemsArr.length ? lastOf : itemsArr.length;
305 |
306 | let result = '';
307 | if (notify) {
308 | result = lessEmojis ? 'Bought DCA:\n\n' : '🔔 Bought DCA:\n\n';
309 | }
310 | else {
311 | result = 'DCAs:\n\n';
312 | }
313 |
314 | for (let i = 0; i < length; i++) {
315 | let coin = itemsArr[i].market.replace(moonBotMarket, '').replace('-', '');
316 | let totalCost = itemsArr[i].averageCalculator.totalWeightedPrice;
317 | let profitPrecentage = itemsArr[i].profit;
318 | let currentVal = totalCost + (totalCost * profitPrecentage / 100);
319 | let daysSince = this.calcDayDiff(itemsArr[i].averageCalculator.firstBoughtDate);
320 | let numberOfBuys = itemsArr[i].boughtTimes;
321 | let numberDCAs = numberOfBuys > 0 ? '(' + numberOfBuys + ')' : '';
322 | let usdval = coinbaseRate * currentVal;
323 | let last24hChange = itemsArr[i].percChange * 100;
324 |
325 | let emoji = profitPrecentage > 0 ? '✅' : '🔻';
326 | let strategies = this.bstbsvgenerator(itemsArr[i], false);
327 | result = result.concat(emoji + ' /' + coin + " (" + daysSince + 'D)' + " | Amount: " + itemsArr[i].averageCalculator.totalAmount.toFixed(2));
328 | result = result.concat('\nVal: ' + currentVal.toFixed(toFixed) + '' + numberDCAs + ", $" + usdval.toFixed(2));
329 | result = result.concat('\nProfit: ' + profitPrecentage.toFixed(2) + "% | Cost: " + totalCost.toFixed(toFixed));
330 | result = result.concat('\nBid: ' + itemsArr[i].currentPrice.toFixed(8) + ' | Vol: ' + itemsArr[i].volume.toFixed(2));
331 | result = result.concat('\nAvg: ' + itemsArr[i].averageCalculator.avgPrice.toFixed(8) + ' | 24h: ' + last24hChange.toFixed(1) + '%');
332 | result = result.concat(strategies);
333 | // result = result.concat('\n/doubleDown_' + itemsArr[i].market);
334 | result = result.concat('\n/panicSell_' + coin);
335 | result = result.concat('\n/setMaxBuyTimes_' + coin);
336 | result = result.concat('\n');
337 | result = result.concat('\n');
338 | }
339 |
340 | if (result.length > 4000) {
341 | result = result.substr(0, 4000);
342 | result = result.concat('\n\nmessage is too long, trimming..');
343 | }
344 |
345 | if (!notify) {
346 | result = result.concat('\nTotal ' + itemsArr.length + ' DCAs in trade');
347 | }
348 |
349 | return result;
350 | }
351 |
352 | sortProfit(a, b) {
353 | if (a.profit < b.profit)
354 | return 1;
355 | if (a.profit > b.profit)
356 | return -1;
357 | return 0;
358 | }
359 |
360 | sortByTime(a, b) {
361 | let aTs = PtJsonDataHandler.getTs(a);
362 | let bTs = PtJsonDataHandler.getTs(b);
363 | if (aTs < bTs)
364 | return 1;
365 | if (aTs > bTs)
366 | return -1;
367 | return 0;
368 | }
369 |
370 |
371 | calcDayDiff(sinceDay) {
372 | try {
373 | let times = new Date(sinceDay.date.year,
374 | sinceDay.date.month - 1,
375 | sinceDay.date.day,
376 | sinceDay.time.hour,
377 | sinceDay.time.minute,
378 | sinceDay.time.second);
379 |
380 | let oneDay = 24 * 60 * 60 * 1000;
381 | let now = new Date();
382 | return Math.round(Math.abs((times.getTime() - now.getTime()) / (oneDay)));
383 | }
384 | catch (err) {
385 | return 0;
386 | }
387 | }
388 | }
389 |
390 | module.exports = PtJsonDataHandler;
391 |
--------------------------------------------------------------------------------
/moonbot.js:
--------------------------------------------------------------------------------
1 | const Telegraf = require('telegraf');
2 | const constants = require('./src/constants');
3 | const keyboards = require('./src/keyboards');
4 | const Engine = require('./src/engines/engine');
5 | const Monitor = require('./src/monitor');
6 | const BalanceHandler = require('./src/handlers/balanceHandler');
7 | const BittrexHandler = require('./src/handlers/bittrexHandler');
8 | const BinanceHandler = require('./src/handlers/binanceHandler');
9 | const PoloniexHandler = require('./src/handlers/poloniexHandler');
10 | const properties = require('./src/moonbotProperties');
11 | const RegistrationHandler = require('./src/handlers/registrationHandler');
12 | const PtSettingsHandler = require('./src/handlers/ptSettingsHandler');
13 | const PtfSettingsHandler = require('./src/handlers/ptfSettingsHandler');
14 | const BotSettingsHandler = require('./src/handlers/botSettingsHandler');
15 | const PresetsHandler = require('./src/handlers/presetsHandler');
16 | const PropertiesValidator = require('./src/propertiesValidator');
17 | const PtJsonDataHandler = require('./src/handlers/ptJsonDataHandler');
18 | const PtShortcutsHandler = require('./src/handlers/ptShortcutsHandler');
19 | const logger = require('./src/moonbotLogger');
20 | const isDemo = !!properties.get('bot.demo.mode');
21 | let moonBotMarket = properties.get('moonbot.market');
22 |
23 | logger.info("LOADING MOONBOT VERSION " + constants.VERSION);
24 |
25 | let getGroups = function () {
26 | let groups = properties.get('bot.groups.allow.control');
27 |
28 | return groups ? groups : '';
29 | };
30 |
31 | let getNotificationsGroups = function () {
32 | let groups = properties.get('moonbot.notify.groups');
33 |
34 | return groups ? groups : '';
35 | };
36 |
37 | if (PropertiesValidator.validate()) {
38 |
39 | let registerationObj;
40 | const bot = new Telegraf(properties.get('bot.token'));
41 |
42 | const handlers = {
43 | balance: new BalanceHandler(),
44 | bittrexHandler: new BittrexHandler(),
45 | binanceHandler: new BinanceHandler(),
46 | poloniexHandler: new PoloniexHandler(),
47 | registration: new RegistrationHandler(),
48 | ptSettings: new PtSettingsHandler(),
49 | ptfSettings: new PtfSettingsHandler(),
50 | botSettingsHandler: new BotSettingsHandler(),
51 | ptJsonDataHandler: new PtJsonDataHandler(),
52 | presetsHandler: new PresetsHandler(),
53 | ptShortcutsHandler: new PtShortcutsHandler()
54 | };
55 |
56 | let engine = new Engine();
57 | let monitor = new Monitor(bot, engine);
58 |
59 |
60 | let commandQueue = [];
61 |
62 | checkLicence(true);
63 |
64 | function readyForBot() {
65 | bot.start((ctx) => {
66 | if (isDemo) {
67 | ctx.reply("This is a demo version of MoonBot (Bittrex exchange).\nData and numbers might not be aligned with reality. Several features were hidden.\nInstall your private bot to see all available features.", getKeyboard(ctx));
68 | } else {
69 | ctx.reply("Hey there!", getKeyboard(ctx));
70 | }
71 | });
72 |
73 | initBotForGroups();
74 | bot.on('message', handleMessage);
75 |
76 | bot.startPolling();
77 | if (registerationObj.state === 'VALID' || registerationObj.state === 'TRAIL') {
78 | monitor.start();
79 | }
80 | bot.telegram.sendMessage(properties.get('bot.user_id'), "I'm UP!\n/setup", keyboards.mainKeyboard);
81 | logger.info("MOONBOT READY");
82 | };
83 |
84 | function checkLicence(init = false, refresh = false) {
85 | try {
86 | handlers.registration.checkLicence().then(result => {
87 | registerationObj = result;
88 | if (init) {
89 | engine.start();
90 | setTimeout(readyForBot, 1000 * 20);
91 | }
92 | if (!refresh) {
93 | setTimeout(checkLicence, 1000 * 60 * 30);
94 | }
95 | });
96 | } catch (err) {
97 | if (init) {
98 | bot.telegram.sendMessage(properties.get('bot.user_id'), "Can't start bot! Network error");
99 | }
100 | logger.debug("checkLicence err " + err);
101 | if (!refresh) {
102 | setTimeout(checkLicence, 1000 * 30);
103 | }
104 | }
105 | }
106 |
107 |
108 | let lastPrompt = new Date().getTime();
109 |
110 | function checkLegitimate(prompt = true) {
111 | return true;
112 | }
113 |
114 |
115 | function handleMessage(ctx) {
116 | let response = '';
117 | let regex = /(\/sales|\/dca|\/pairs|\/pbl|\/bangroup|\/allowgroup|\/togglesom|\/restartpt|\/snooze) (\d*)/i;
118 | let shortCutsRegex = /(\/doubleDown|\/panicSell|\/revert_panicSell|\/setMaxBuyTimes|\/disableTrading|\/enableTrading)_(\S*)/i;
119 | let command = ctx.message.text + '';
120 | let option = null;
121 |
122 | command = command.replace('@' + bot.options.username, '');
123 |
124 | if (command.match(regex)) {
125 | let match = regex.exec(command);
126 | if (match[0]) {
127 | command = match[1];
128 | option = match[2];
129 | }
130 | }
131 |
132 | if (command.match(shortCutsRegex)) {
133 | let match = shortCutsRegex.exec(command);
134 | if (match[0]) {
135 | command = match[1];
136 | option = match[2];
137 | }
138 | }
139 |
140 |
141 | let ingroup = ctx.from.id != ctx.chat.id && ctx.chat.id < 0;
142 |
143 | let previousCommand = '';
144 | let toggleSom = function () {
145 | if (PropertiesValidator.checkProp('pt.server.api_token', false, true)) {
146 | let toggle = handlers.ptSettings.toggleSom();
147 | if (toggle.success) {
148 | ctx.telegram.sendMessage(ctx.chat.id, "✅ Done!\nDEFAULT_sell_only_mode_enabled=" + toggle.currentSom + "\n/togglesom", getKeyboard(ctx));
149 | } else {
150 | ctx.telegram.sendMessage(ctx.chat.id, "⛔ Failed!\nCheck your logs\n/togglesom", getKeyboard(ctx));
151 | }
152 | }
153 | };
154 |
155 | if (isValidRecipient(ctx)) {
156 | try {
157 | logger.debug("Handling command: " + command);
158 | switch (command) {
159 | case "/setup" :
160 | if (!isDemo) {
161 | ctx.telegram.sendMessage(ctx.chat.id, "Enter your initial " + moonBotMarket + " investment to track your profit percentage, i.e. '1.5'", getKeyboard(ctx, keyboards.lettersKeyboard));
162 | commandQueue.push("btc.investment");
163 | commandQueue.push("SETUP1");
164 | }
165 | break;
166 | case "/version" :
167 | ctx.telegram.sendMessage(ctx.chat.id, constants.VERSION , getKeyboard(ctx));
168 | commandQueue = [];
169 | break;
170 | case "Home" :
171 | ctx.telegram.sendMessage(ctx.chat.id, command, getKeyboard(ctx));
172 | commandQueue = [];
173 | break;
174 | case "/demo" :
175 | ctx.telegram.sendMessage(ctx.chat.id, "Go to demo bot - @demoMoonBot", getKeyboard(ctx));
176 | break;
177 | case "Summary" :
178 | response = handlers.balance.handleBalanceMessage(Engine.getCurrentData());
179 | ctx.telegram.sendMessage(ctx.chat.id, response, getKeyboard(ctx));
180 | break;
181 | case "Bittrex" :
182 | response = handlers.bittrexHandler.handleBittrexMessage(Engine.getCurrentBittrexData());
183 | ctx.telegram.sendMessage(ctx.chat.id, response, getKeyboard(ctx));
184 | break;
185 | case "Binance" :
186 | response = handlers.binanceHandler.handleBinanceMessage(Engine.getCurrentBinanceData());
187 | ctx.telegram.sendMessage(ctx.chat.id, response, getKeyboard(ctx));
188 | break;
189 | case "Poloniex" :
190 | response = handlers.poloniexHandler.handlePoloniexMessage(Engine.getCurrentPoloniexData());
191 | ctx.telegram.sendMessage(ctx.chat.id, response, getKeyboard(ctx));
192 | break;
193 | case "Help" :
194 | case "/help" :
195 | commandQueue = [];
196 | response = getHelpMessage();
197 | ctx.telegram.sendMessage(ctx.chat.id, response, getKeyboard(ctx));
198 | break;
199 | case "Bot Settings 🤖":
200 | let keyboard = handlers.botSettingsHandler.getKeyboardSettings(true);
201 | ctx.telegram.sendMessage(ctx.chat.id, "Choose one:\nOr /cancel", getKeyboard(ctx, keyboard));
202 | commandQueue.push("BTS");
203 | break;
204 | case "Cancel" :
205 | case "cancel" :
206 | case "/cancel" :
207 | ctx.telegram.sendMessage(ctx.chat.id, "Ok, canceled.", getKeyboard(ctx));
208 | commandQueue = [];
209 | break;
210 | default:
211 | let setupSetting = commandQueue[commandQueue.length - 1];
212 | if (setupSetting && setupSetting.indexOf('SETUP') === 0) {
213 | commandQueue.pop();
214 | let key = '';
215 | switch (setupSetting) {
216 | case 'SETUP1':
217 | key = commandQueue.pop();
218 | handlers.botSettingsHandler.saveSetting(key, command);
219 | ctx.telegram.sendMessage(ctx.chat.id, "Enter your initial USD investment to track your profit percentage, i.e. '10000'", getKeyboard(ctx, keyboards.lettersKeyboard));
220 | commandQueue.push("usd.investment");
221 | commandQueue.push("SETUP2");
222 | break;
223 | case 'SETUP2':
224 | key = commandQueue.pop();
225 | handlers.botSettingsHandler.saveSetting(key, command);
226 | ctx.telegram.sendMessage(ctx.chat.id, "Please set your time zone offset, i.e -5 for GMT-5 or 6 for GMT+6", getKeyboard(ctx, keyboards.lettersKeyboard));
227 | commandQueue.push("moonbot.timezone.offset");
228 | commandQueue.push("SETUP3");
229 | break;
230 | case 'SETUP3':
231 | key = commandQueue.pop();
232 | handlers.botSettingsHandler.saveSetting(key, command);
233 | ctx.telegram.sendMessage(ctx.chat.id, "Do you like emojis?", getKeyboard(ctx, keyboards.realYesNoKeyboard));
234 | commandQueue.push("moonbot.less.emojis");
235 | commandQueue.push("SETUP4");
236 | break;
237 | case 'SETUP4':
238 | key = commandQueue.pop();
239 | handlers.botSettingsHandler.saveSetting(key, command === 'Yes' ? 'false' : 'true');
240 | ctx.telegram.sendMessage(ctx.chat.id, "Please set the percent change you would like to be notified on. (i.e 6 for both bellow -6% and above +6%)", getKeyboard(ctx, keyboards.lettersKeyboard));
241 | commandQueue.push("monitor.percentage.change");
242 | commandQueue.push("SETUP5");
243 | break;
244 | case 'SETUP5':
245 | key = commandQueue.pop();
246 | handlers.botSettingsHandler.saveSetting(key, command);
247 | ctx.telegram.sendMessage(ctx.chat.id, "Great! we're ready.", getKeyboard(ctx));
248 | commandQueue = [];
249 | break;
250 | }
251 |
252 | }
253 |
254 | if (checkLegitimate()) {
255 | switch (command) {
256 | case "/delete" :
257 | case "Delete" :
258 | commandQueue.pop();
259 | let setting = commandQueue[commandQueue.length - 1];
260 | commandQueue.pop();
261 | let type = commandQueue[commandQueue.length - 1];
262 | commandQueue.pop();
263 | handlers.ptSettings.deleteSetting(type, setting);
264 | ctx.telegram.sendMessage(ctx.chat.id, "Done!", getKeyboard(ctx));
265 | break;
266 | case "/doubleDown" :
267 | handlers.ptShortcutsHandler.doubleDown(Engine.getCurrentDcaLogData(), option);
268 | ctx.telegram.sendMessage(ctx.chat.id, "WIP", getKeyboard(ctx));
269 | break;
270 | case "/panicSell" :
271 | case "/revert_panicSell" :
272 | previousCommand = option + '_panic_sell_enabled';
273 | let value = command === '/revert_panicSell' ? 'false' : 'true';
274 | ctx.telegram.sendMessage(ctx.chat.id, "Are you sure? " + previousCommand + "=" + value, getKeyboard(ctx, keyboards.yesNoKeyboard));
275 | commandQueue.push('PAIRS');
276 | commandQueue.push(previousCommand);
277 | commandQueue.push(value);
278 | commandQueue.push("PDIACK");
279 | break;
280 | case "/disableTrading" :
281 | case "/enableTrading" :
282 | previousCommand = option + '_trading_enabled';
283 | let enableTrading = command === '/enableTrading' ? 'true' : 'false';
284 | ctx.telegram.sendMessage(ctx.chat.id, "Are you sure? " + previousCommand + "=" + enableTrading, getKeyboard(ctx, keyboards.yesNoKeyboard));
285 | commandQueue.push('PAIRS');
286 | commandQueue.push(previousCommand);
287 | commandQueue.push(enableTrading);
288 | commandQueue.push("PDIACK");
289 | break;
290 | case "/setMaxBuyTimes" :
291 | let curBuyTime = handlers.ptShortcutsHandler.getDcaBuyTimes(Engine.getCurrentDcaLogData(), option);
292 | previousCommand = option + '_DCA_max_buy_times';
293 | ctx.telegram.sendMessage(ctx.chat.id, "Are you sure? " + previousCommand + "=" + curBuyTime, getKeyboard(ctx, keyboards.yesNoKeyboard));
294 | commandQueue.push('DCA');
295 | commandQueue.push(previousCommand);
296 | commandQueue.push(curBuyTime.toString());
297 | commandQueue.push("PDIACK");
298 | break;
299 | case "/ignore" :
300 | monitor.addToIgnoreList();
301 | ctx.telegram.sendMessage(ctx.chat.id, "Ok, will ignore those.\n\nReset ignore list: /resetignore");
302 | break;
303 | case "/resetignore" :
304 | case "Reset Log Ignore" :
305 | monitor.addToIgnoreList(true);
306 | ctx.telegram.sendMessage(ctx.chat.id, "Done!", getKeyboard(ctx));
307 | break;
308 | case "Presets" :
309 | let presets = handlers.presetsHandler.getPreSetsKeyboard();
310 | if (presets) {
311 | ctx.telegram.sendMessage(ctx.chat.id, "Please select one:\nOr /cancel", getKeyboard(ctx, presets));
312 | commandQueue.push("PSC");
313 | }
314 | else {
315 | ctx.telegram.sendMessage(ctx.chat.id, "No presets folders, please add your pre-defined presets", getKeyboard(ctx));
316 | }
317 | break;
318 | case "/notify":
319 | case "Toggle PT Notifications":
320 | if (ctx.chat.id == properties.get('bot.user_id')) {
321 | if (properties.get("profit.trailer.disable.notifications")) {
322 | properties.setProperty('profit.trailer.disable.notifications', 'false');
323 | ctx.telegram.sendMessage(ctx.chat.id, "PT Notifications: ON", getKeyboard(ctx));
324 | }
325 | else {
326 | properties.setProperty('profit.trailer.disable.notifications', 'true');
327 | ctx.telegram.sendMessage(ctx.chat.id, "PT Notifications: OFF", getKeyboard(ctx));
328 | }
329 | } else {
330 | let owner = ctx.from.id == properties.get('bot.user_id');
331 | let groups = getGroups().split(',');
332 | let index = groups.indexOf(ctx.chat.id.toString());
333 |
334 | if (owner || index >= 0) {
335 | let groupsIn = getNotificationsGroups().split(',');
336 | let index = groupsIn.indexOf(ctx.chat.id.toString()); // <-- Not supported in 12) {
716 | commandQueue = [];
717 | }
718 | }
719 |
720 | function initBotForGroups() {
721 | bot.telegram.getMe().then((botInfo) => {
722 | bot.options.username = botInfo.username
723 | });
724 | const regex = new RegExp(/\/(sales|dca|pairs|allowgroup|bangroup|togglesom|restartpt|snooze) (\d*)/i);
725 | bot.hears(regex, (ctx) => handleMessage(ctx));
726 | }
727 |
728 |
729 | function getHelpMessage() {
730 | return "MoonBot is a private telegram bot that helps you monitor and track your crypto tradings.\n" +
731 | "Moonbot has advanced integration with Profit Trailer, you can view (and share) your sales, pairs and dca status, update your PT settings remotely (PT Feeder too), get notification on sells, buys (dca too!) and much more!!\n" +
732 | "\n\n" +
733 | "Freemium and Premium membership available.\n" +
734 | "Install your version and get 48h trial of Premium features!\n" +
735 | "\nMain Commands:\n" +
736 | "Summary: View summary of your accounts.\n" +
737 | "Binance: Show binance status including coin information.\n" +
738 | "Bittrex: bittrex status including coin information.\n" +
739 | "Sales 💸 (/sales [n]): Display latest n (5) Profit Trailer sells.\n" +
740 | "Pairs 💼 (/pairs [n]): Display current n (all) pairs.\n" +
741 | "DCA 💰 (/dca [n]): Display n n (all) DCAs.\n" +
742 | "PT Settings 🛠: Remote update your Profit Trailer / Feeder settings.\n" +
743 | "Bot Settings 🤖: Update MoonBot settings & /help .\n" +
744 | "/togglesom : Instant toggling of SOM (DEFAULT_sell_only_mode_enabled) on/off.\n" +
745 | "/restartpt : execute PT restart command.\n" +
746 | "\n" +
747 | "You can type 'Cancel' (/cancel) anytime to go back home.\n" +
748 | "\n" +
749 | "Groups:\n" +
750 | "Invite your bot to groups!\n" +
751 | "Use the following commands in groups to share Profit Trailer sales, pairs and dca(s) status:\n" +
752 | "/sales [n]: Share latest n (5) sells.\n" +
753 | "/pairs [n]: Share current n (all) pairs.\n" +
754 | "/dca [n]: Share n (all) DCAs.\n" +
755 | "/allowgroup : ⛔️ Allow all group members to control your bot.\n" +
756 | "/bangroup : Ban group members from controlling your bot.\n" +
757 | "/notify : Send buys/sell notification to group.\n" +
758 | "Don't forget to mention your bot name (@your_bot)\n" +
759 | "\n\n" +
760 | "Terminology:\n" +
761 | "Bal: Life time balance (all_sells - all_buys + current_balance).\n" +
762 | "Val: Current value of your holdings.\n" +
763 | "\n\n" +
764 | "\nMore info:\n" +
765 | "GitHub: https://github.com/tulihub/PT-MoonBot\n" +
766 | "Discord: https://discord.gg/aJ3Ryu7\n" +
767 | "Assistant: https://t.me/AssistantMoonBot?start=bot\n" +
768 | "";
769 | }
770 |
771 |
772 | function getKeyboard(ctx, defaultKeyboard = keyboards.mainKeyboard) {
773 | let ingroup = ctx.from.id != ctx.chat.id && ctx.chat.id < 0;
774 | let groupId = ctx.chat.id;
775 |
776 | if (ingroup) {
777 | return isValidGroupId(groupId) ? defaultKeyboard : keyboards.lettersKeyboard;
778 | } else {
779 | return defaultKeyboard;
780 | }
781 | }
782 |
783 |
784 | function isValidRecipient(ctx) {
785 | let ingroup = ctx.from.id != ctx.chat.id && ctx.chat.id < 0;
786 | let groupId = ctx.chat.id;
787 | if (ingroup && isValidGroupId(groupId)) {
788 | return true;
789 | }
790 | return isDemo || ctx.from.id == properties.get('bot.user_id');
791 | }
792 |
793 | function isValidGroupId(groupId) {
794 | let groups = getGroups().split(',');
795 |
796 | if (groups.indexOf(groupId.toString()) >= 0) {
797 | return true;
798 | }
799 |
800 | return false;
801 | }
802 |
803 | function isInt(value) {
804 | return !isNaN(value) && (function (x) {
805 | return (x | 0) === x;
806 | })(parseFloat(value))
807 | }
808 |
809 | }
810 |
--------------------------------------------------------------------------------