├── .editorconfig ├── .eslintignore ├── .gitattributes ├── .gitignore ├── .travis.yml ├── HELP-commands-for-gmon.txt ├── LICENSE ├── README.md ├── RELEASE-message-for-gmon.txt ├── RELEASE-message-telegram-for-gmon.txt ├── __tests__ ├── osData.test.js └── tradePairs.test.js ├── gmon-screenshot.png ├── package-lock.json ├── package.json └── src ├── index.js └── modules ├── formatter.js ├── netData.js ├── osData.js ├── outputter.js ├── pm2Data.js ├── settings.js ├── tabelData.js ├── tradePairParser.js └── tradePairs.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | coverage/ 3 | dev-data/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | 4 | # Log files 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Editor directories and files 10 | .idea 11 | *.iml 12 | .yo-rc.json 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | 19 | # Testing 20 | dev-data 21 | coverage 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | 5 | after_script: 'cat ./coverage/lcov.info | coveralls' 6 | -------------------------------------------------------------------------------- /HELP-commands-for-gmon.txt: -------------------------------------------------------------------------------- 1 | $ lsb_release -a 2 | No LSB modules are available. 3 | Distributor ID: Ubuntu 4 | Description: Ubuntu 17.04 5 | Release: 17.04 6 | Codename: zesty 7 | 8 | $ node -v 9 | v7.10.0 10 | 11 | 12 | useradd -s /bin/bash -m -d /home/gb -c "gb" gb 13 | usermod -aG sudo gb 14 | echo gbpass | passwd gb --stdin 15 | 16 | 17 | 18 | curl -sL https://raw.githubusercontent.com/BeerK0in/generator-gunbot/master/install-loud.sh | bash -- && exec bash 19 | 20 | 21 | #!/bin/bash 22 | 23 | # Set variables 24 | # ----------------------------------- 25 | GUNBOT_GITHUB_FOLDER_NAME="Gunbot3.3.2" 26 | GUNBOT_GITHUB_FILE_NAME="GUNBOT_v3.3.2_Poloniex_Bittrex_Patch" 27 | 28 | https://github.com/GuntharDeNiro/BTCT/releases/tag/Gunbot3.3SMART 29 | https://github.com/GuntharDeNiro/BTCT/releases/tag/Gunbot3.3.2 30 | 31 | 32 | 33 | 34 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 35 | apt-get update 36 | 37 | 38 | logMessage "(2/6) Install nodejs 8.x" 39 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 40 | curl -sL https://deb.nodesource.com/setup_7.x | bash - 41 | apt-get -y install nodejs chpasswd curl wget 42 | 43 | 44 | # Create gbuser 45 | useradd -s /bin/bash -m -d /home/gbuser -c "gbuser" gbuser 46 | usermod -aG sudo gbuser 47 | echo gbpass | passwd gbuser --stdin 48 | 49 | echo "" >> /etc/ssh/sshd_config 50 | echo "DenyUsers gbuser" >> /etc/ssh/sshd_config 51 | 52 | 53 | logMessage "Automatically login root as gbuser user" 54 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 55 | echo "" >> ~/.bashrc 56 | echo "# automatically login as gbuser" >> ~/.bashrc 57 | echo "if [[ -n $SSH_CONNECTION ]] ; then" >> ~/.bashrc 58 | echo "su gbuser" >> ~/.bashrc 59 | echo "fi" >> ~/.bashrc 60 | 61 | # Create swap 62 | fallocate -l 6G /gunbotswap2 63 | chmod 600 /gunbotswap2 64 | mkswap /gunbotswap2 65 | swapon /gunbotswap2 66 | cp /etc/fstab /etc/fstab.bak2 67 | echo '/gunbotswap2 none swap sw 0 0' | sudo tee -a /etc/fstab 68 | 69 | 70 | logMessage "(3/6) Install tools" 71 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 72 | apt-get -y install unzip 73 | 74 | npm install -g pm2 yo@1.8.5 generator-gunbot gunbot-monitor 75 | 76 | chown -R gbuser:gbuser /usr/lib/node_modules 77 | chown -R gbuser:gbuser /opt 78 | chmod -R g+wx /opt 79 | 80 | su gbuser 81 | cd ~ 82 | echo "" >> ~/.bashrc 83 | echo "cd ~" >> ~/.bashrc 84 | 85 | 86 | logMessage "(4/6) Install GUNBOT" 87 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 88 | wget https://github.com/GuntharDeNiro/BTCT/releases/download/Gunbot3.3SMART/GUNBOT_V3.3_ALL_OS.zip -P /opt/ 89 | unzip -o /opt/GUNBOT_V3.3_ALL_OS.zip -d /opt/unzip-tmp 90 | 91 | # create folder for the current version. 92 | mkdir /opt/_gunbot3.3 -p 93 | 94 | # Copy only the executables. 95 | cp /opt/unzip-tmp/gunthy-* /opt/_gunbot3.3 96 | 97 | # Set rights 98 | chmod +x /opt/_gunbot3.3/gunthy-* 99 | 100 | # Cleanup 101 | rm /opt/GUNBOT_V3.3_ALL_OS.zip 102 | rm -R /opt/unzip-tmp 103 | 104 | 105 | wget https://github.com/GuntharDeNiro/BTCT/releases/download/Gunbot3.3.2/GUNBOT_v3.3.2_Poloniex_Bittrex_Patch.zip -P /opt/ 106 | unzip -o /opt/GUNBOT_v3.3.2_Poloniex_Bittrex_Patch.zip -d /opt/unzip-tmp 107 | 108 | # create folder for the current version. 109 | mkdir /opt/_gunbot3.3.2 -p 110 | 111 | # Copy only the executables. 112 | cp /opt/unzip-tmp/gunthy-* /opt/_gunbot3.3.2 113 | 114 | # Set rights 115 | chmod +x /opt/_gunbot3.3.2/gunthy-* 116 | 117 | # Cleanup 118 | rm /opt/GUNBOT_v3.3.2_Poloniex_Bittrex_Patch.zip 119 | rm -R /opt/unzip-tmp 120 | 121 | mkdir /opt/p1 -p 122 | mkdir /opt/p2 -p 123 | mkdir /opt/b1 -p 124 | mkdir /opt/b2 -p 125 | mkdir /opt/k1 -p 126 | mkdir /opt/k2 -p 127 | 128 | cp /opt/z_gunbot3.3/gunthy-linuxx64 /opt/k1/gunthy-linuxx64 129 | cp /opt/z_gunbot3.3/gunthy-linuxx64 /opt/k2/gunthy-linuxx64 130 | cp /opt/z_gunbot3.3.2/gunthy-linuxx64 /opt/p1/gunthy-linuxx64 131 | cp /opt/z_gunbot3.3.2/gunthy-linuxx64 /opt/p2/gunthy-linuxx64 132 | cp /opt/z_gunbot3.3.2/gunthy-linuxx64 /opt/b1/gunthy-linuxx64 133 | cp /opt/z_gunbot3.3.2/gunthy-linuxx64 /opt/b2/gunthy-linuxx64 134 | 135 | 136 | 137 | 138 | 139 | echo "" >> ~/.bashrc 140 | echo "# GUNBOT ALIASES" >> ~/.bashrc 141 | echo "alias ginit='yo gunbot init'" >> ~/.bashrc 142 | echo "alias gadd='yo gunbot add'" >> ~/.bashrc 143 | echo "alias gl='pm2 l'" >> ~/.bashrc 144 | echo "alias glog='pm2 logs'" >> ~/.bashrc 145 | echo "alias gstart='pm2 start'" >> ~/.bashrc 146 | echo "alias gstop='pm2 stop'" >> ~/.bashrc 147 | 148 | 149 | 150 | logMessage "(6/6) Init generator" 151 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 152 | # Create folder for yeoman. 153 | chmod g+rwx ~ 154 | chmod g+rwx /opt/gunbot 155 | 156 | # Yeoman write rights. 157 | mkdir ~/.config/configstore -p 158 | cat > /root/.config/configstore/insight-yo.json << EOM 159 | { 160 | "clientId": 1337, 161 | "optOut": true 162 | } 163 | EOM 164 | chmod g+rwx /root/.config 165 | chmod g+rwx /root/.config/configstore 166 | chmod g+rw /root/.config/configstore/* 167 | 168 | # pm2 write rights. 169 | mkdir ~/.pm2 -p 170 | echo "1337" > ~/.pm2/touch 171 | chmod g+rwx ~/.pm2 172 | chmod g+rw ~/.pm2/* 173 | 174 | 175 | echo "" 176 | echo " ============================================================" 177 | echo " GUNBOT SETUP complete!" 178 | echo "" 179 | echo " Please run this command to init the GUNBOT:" 180 | echo " gcd" 181 | echo " ginit" 182 | echo "" 183 | echo " ============================================================" 184 | echo "" 185 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 BeerK0in 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gunbot-monitor [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Coverage percentage][coveralls-image]][coveralls-url] 2 | > gmon - A command line application to monitor your Gunbot 3 | 4 | ![gmon screenshot][gmon-screenshot-image] 5 | 6 | 7 | ## 👍 Basics 8 | 9 | 🚨 **gmon 1.x will only work with Gunbot v9!** 🚨 10 | 11 | - gmon works best on Unix based systems (Linux and OSX) 12 | - gmon requires Gunbot v9, Node.js 6 or higher and npm 13 | - gmon uses up to 100MB memory 14 | - you should not use gmon if your server has less than 230MB available free memory 15 | - you need a wide window / screen 16 | - gmon and it's creator are not responsible/liable for wrong numbers or wrong calculations 17 | - use at own risk 18 | 19 | 20 | ## 💻 Installation 21 | 22 | You need to install Gunbot-Monitor on the same machine as Gunbot. 23 | 24 | This is a [Node.js](https://nodejs.org/en/) command line app available through the 25 | [npm registry](https://www.npmjs.com/). 26 | 27 | Before installing, [download and install Node.js](https://nodejs.org/en/download/). 28 | Node.js 6.11.2 or higher is required. 29 | 30 | Installation is done using the 31 | [`npm install -g` command](https://docs.npmjs.com/getting-started/installing-npm-packages-globally): 32 | 33 | ```sh 34 | $ npm install -g gunbot-monitor 35 | ``` 36 | 37 | 38 | ## 💎 Update 39 | 40 | Use npm to update: 41 | 42 | ```sh 43 | $ npm uninstall -g gunbot-monitor && npm install -g gunbot-monitor 44 | ``` 45 | 46 | 47 | ## 📋 Usage 48 | 49 | To start gmon, go to the folder your Gunbot is installed (`cd /path/to/gunbot`) and run this command: 50 | 51 | ```sh 52 | $ gmon [options] 53 | ``` 54 | 55 | **Options** 56 | 57 | ```sh 58 | -h, --help Output usage information 59 | -v, --version Output the version number 60 | -p, --path Path to the GUNBOT folder. Separate multiple paths with ":" (like: -p /path1:/path2). [Default: current folder] 61 | -N, --path-name Optional name for each path to the GUNBOT folder(s). Separate multiple path names with ":" (like: -N Kraken_Bot:Proxy_Mega_Bot). [Default: No path name] 62 | -c, --compact [groupSize] Do not draw row lines. Optional set the number of rows after which a line is drawn. [Default: 0] 63 | -s, --small Reduce columns for small screens 64 | -d, --digits Amount of digits for all numbers. Min = 0, max = 10. [Default: 4] 65 | -r, --refresh Seconds between table refresh. Min = 1, max = 600. [Default: 60] 66 | -m, --markets Filter of markets to show. Separate multiple markets with ":" (like: -m poloniex:kraken) [Default: all] 67 | -P, --profit Use to activate the parsing of the profit. NOT WORKING CORRECTLY! 68 | -H, --hide-inactive Hides trading pairs which last log entry is older than given hours. Min = 1, max = 854400. [Default: 720] 69 | -C, --connections-check-delay Seconds between netstats checks. Higher numbers result in more inaccurate statistics but reduce cpu usage. Min = 1, max = 600. [Default: 1] 70 | ``` 71 | 72 | 73 | ### Path option `-p` 74 | 75 | To run gmon outside of your Gunbot folder, use the `-p` option to specify the path where Gunbot is installed: 76 | 77 | ```sh 78 | $ gmon -p /path/to/gunbot 79 | ``` 80 | 81 | If you have multiple Gunbots running, you can use the `-p` option to specify all paths to your Gunbots: 82 | 83 | ```sh 84 | $ gmon -p /path/to/AAA_bot:/path/to/BBB_bot 85 | ``` 86 | 87 | ### Path name option `-N` 88 | 89 | When using `-p` option you can set names for your different Gunbots: 90 | 91 | ```sh 92 | $ gmon -N Kraken-Bot-tssl:Bittrex-emo 93 | ``` 94 | 95 | ### Compact mode `-c` 96 | 97 | With this option, gmon will not draw row lines to separate the rows: 98 | 99 | ```sh 100 | $ gmon -c 101 | ``` 102 | 103 | Optional set the number of rows after which a line should be drawn to have a little visual guide: 104 | 105 | ```sh 106 | $ gmon -c 4 107 | ``` 108 | 109 | ### Small mode `-s` 110 | 111 | With this option, gmon will not draw the columns *OO?*, *# Coins*, *1 6 h d +* to support smaller screens: 112 | 113 | ```sh 114 | $ gmon -s 115 | ``` 116 | 117 | ### Digits option `-d` 118 | 119 | When using `-d` option you can set the number of displayed digits. Set to a lower number on small screens: 120 | 121 | ```sh 122 | $ gmon -d 3 123 | ``` 124 | 125 | ### Refresh option `-r` 126 | 127 | This option allows you to set the time in seconds how long gmon waits until it checks all values again. 128 | 129 | ⚡️ Please check your server when setting `-r` to low numbers! ⚡️ Faster updates mean more work for your server. 130 | 131 | ```sh 132 | $ gmon -r 90 133 | ``` 134 | 135 | ### Market filter `-m` 136 | 137 | With option `-m` you are able to define specific markets you want to see in the output: 138 | 139 | ```sh 140 | $ gmon -m kraken:bitfinex # This will only show pairs on those exchanges 141 | ``` 142 | 143 | ### Profit option `-P` 144 | 145 | Now working at the moment. 146 | 147 | ### Hide inactive pairs `-H` 148 | 149 | gmon will show all trading pairs inside a folder, as long as there is a `state.json` file. If you disable a pair you can set this option `-H` to hide inactive trading pairs when there last update is older than the set number. 150 | 151 | ```sh 152 | $ gmon -H 2 153 | ``` 154 | 155 | ### Connections check delay `-C` 156 | 157 | Set the time interval in seconds how often the number of open connections to the exchanges should be checked. Higher numbers result in more inaccurate statistics but reduce cpu usage. 158 | 159 | ```sh 160 | $ gmon -C 10 161 | ``` 162 | 163 | ## 🤔 How to read gmon's output 164 | 165 | | Column | Description | 166 | | --- | --- | 167 | | **Name** | Market name and trading pair name | 168 | | **Str** | Buy and sell strategy | 169 | | **LL** | Last Log - seconds since the last log update of this trading pair | 170 | | **OO?** | Open Order? - says "yes" if there is an open order on the market | 171 | | | 172 | | **# Coins** | Amount of coins of the quote currency | 173 | | **in BTC** | Value of the quote currency in BTC (or other base currency) | 174 | | **Diff since buy** | Indicator how much the value of the holding quote currency has changed in BTC (or other base currency) | 175 | | | 176 | | **Buy/Bought** | If numbers are white: Price which needs to be reached till the bot will buy quote currency _(if **# Coins** == 0)_
If numbers are yellow: Price the bot paid to buy the quote currency" | 177 | | **Sell** | Price which needs to be reached till the bot will sell the holding quote currency _(if **# Coins** > 0)_ | 178 | | **Last Price** | Current market price for the quote currency | 179 | | **Price diff** | Difference between Last Price and the Buy/Bought if waiting to buy
or difference between Last Price and the Sell if waiting to sell.
_If something is not correct (the log does not contain all needed prices) it shows an error hint_ | 180 | | | 181 | | **# Buys** | Number of total buys - How often did the bot buy this quote currency and time since the last buy | 182 | | **1 6 h d +** | Number of buys in the last 1 hour / 6 hours / 12 hours / 24 hours / more than 24 hours. | 183 | | **# Sells** | Number of total sells - How often did the bot sell this quote currency and time since the last sell | 184 | | **1 6 h d +** | Number of sells in the last 1 hour / 6 hours / 12 hours / 24 hours / more than 24 hours | 185 | 186 | 187 | ## 😎 Windows issues 188 | 189 | Do yourself a favor and use a console emulator like [cmder](https://github.com/cmderdev/cmder). 190 | 191 | 192 | ## ✨ Wishlist 193 | 194 | https://github.com/BeerK0in/gunbot-monitor/issues/15 195 | 196 | 197 | ## 🍺 Support & Tips 198 | 199 | You like gmon and it helps you earning money? 200 | 201 | - Report bugs in [this forum thread](https://gunthy.org/forum/index.php?topic=319.0) or via the Telegram group [t.me/beercrypto](https://t.me/beercrypto). 202 | - Support gmon and send a tip to BTC wallet: 1GJCGZPn6okFefrRjPPWU73XgMrctSW1jT 203 | 204 | 205 | ## License 206 | 207 | MIT © BeerK0in - [www.beer-crypto.com](https://www.beer-crypto.com) 208 | 209 | 210 | [npm-image]: https://badge.fury.io/js/gunbot-monitor.svg 211 | [npm-url]: https://npmjs.org/package/gunbot-monitor 212 | [travis-image]: https://travis-ci.org/BeerK0in/gunbot-monitor.svg?branch=master 213 | [travis-url]: https://travis-ci.org/BeerK0in/gunbot-monitor 214 | [daviddm-image]: https://david-dm.org/BeerK0in/gunbot-monitor.svg?theme=shields.io 215 | [daviddm-url]: https://david-dm.org/BeerK0in/gunbot-monitor 216 | [coveralls-image]: https://coveralls.io/repos/github/BeerK0in/gunbot-monitor/badge.svg?branch=master 217 | [coveralls-url]: https://coveralls.io/github/BeerK0in/gunbot-monitor?branch=master 218 | [gmon-screenshot-image]: https://raw.githubusercontent.com/BeerK0in/gunbot-monitor/master/gmon-screenshot.png "Screenshot of gmon" 219 | -------------------------------------------------------------------------------- /RELEASE-message-for-gmon.txt: -------------------------------------------------------------------------------- 1 | [size=24pt][b]gmon update[/b][/size] 2 | [size=10pt][size=14pt][b]Version 0.0.50[/b][/size][/size] 3 | 4 | [size=14pt][b]Changelog[/b][/size] 5 | 6 | - Bugfix: Show pm2 ids for process names like `AAA_XXX`, `AAA_XXX_M` and `AAA_XXX_m` 7 | - Added total number of monitored pairs per folder. 8 | - Added total BTC value of available BTC and current ALT value. 9 | - Added option to set names for different paths ( `-N kraken:polo` ). 10 | - Added option to set netstat connection check delay time ( `-C 5` ). 11 | 12 | [hr] 13 | 14 | [size=14pt][b]Update[/b][/size] 15 | 16 | Run [code]npm uninstall -g gunbot-monitor && npm install -g gunbot-monitor/code] to update it on your server. 17 | 18 | Run [code]npm install -g gunbot-monitor[/code] to install it on your server. 19 | 20 | [hr] 21 | 22 | [size=14pt][b]Total number of monitored pairs per folder[/b][/size] 23 | 24 | Shows the total number of pairs in the last row: [tt]= TOTAL (123) =[/tt] 25 | 26 | [hr] 27 | 28 | [size=14pt][b]Total BTC value of available BTC and current ALT value[/b][/size] 29 | 30 | Shows the total BTC value of the available BTC + the monitored ALTs: 31 | [tt]Available BitCoins: poloniex 0.41640129 bittrex 0.467 | Total BTC value: 3.1025 (in BTC: 0.8834, in ALTs: 2.2191)[/tt] 32 | 33 | It does not connect to the market to collect all you balances. The value next to ALTs is the value of the monitored currencies. 34 | 35 | [hr] 36 | 37 | [size=14pt][b]Set names for different paths[/b][/size] 38 | 39 | For every path to the GUNBOT folder(s) you can set a name ([tt]-N[/tt]) which will be displayed as headline. Separate multiple path names with ":". 40 | 41 | If you want no name for some folder, just dont type anything. 42 | 43 | Example: 44 | [code] 45 | gmon -p /opt/polobot/:/opt/v3.1/kraken/ -N "Polo with Proxy":Kraken 46 | [/code] 47 | [code] 48 | gmon -p /opt/polobot/:~/bot-with-no-name:/opt/v3.1/kraken/:/home/bk/awesome_gun -N "Polo with Proxy"::Kraken:"BK is cool" 49 | [/code] 50 | 51 | [hr] 52 | 53 | [size=14pt][b]netstat connection check delay time[/b][/size] 54 | 55 | With the option [tt]-C[/tt] it is possible to set the delay between each netstats check in seconds. This is for low end systems to reduce the CPU load by increasing the delay time. 56 | 57 | [hr] 58 | 59 | [size=14pt][b]Usage[/b][/size] 60 | 61 | All available options can be displayed with 62 | [code] 63 | gmon -h 64 | [/code] 65 | 66 | [code] 67 | Usage: gmon [options] 68 | 69 | Options: 70 | 71 | -h, --help Output usage information 72 | -v, --version Output the version number 73 | -p, --path Path to the GUNBOT folder. Separate multiple paths with ":" (like: -p /path1:/path2). [Default: current folder] 74 | -N, --path-name Optional name for each path to the GUNBOT folder(s). Separate multiple path names with ":" (like: -N Kraken_Bot:Proxy_Mega_Bot). [Default: No path name] 75 | -c, --compact [groupSize] Do not draw row lines. Optional set the number of rows after which a line is drawn. [Default: 0] 76 | -s, --small Reduce columns for small screens 77 | -d, --digits Amount of digits for all numbers. Min = 0, max = 10. [Default: 4] 78 | -r, --refresh Seconds between table refresh. Min = 1, max = 600. [Default: 60] 79 | -m, --markets List of markets to show. Separate multiple markets with ":" (like: -m poloniex:kraken) [Default: poloniex:kraken:bittrex] 80 | -P, --profit Use to activate the parsing of the profit. THIS WILL SLOW DOWN YOUR SYSTEM! 81 | -H, --hide-inactive Hides trading pairs which last log entry is older than given hours. Min = 1, max = 854400. [Default: 720] 82 | -E, --show-all-errors Use to list 422 errors in the last column. 83 | -C, --connections-check-delay Seconds between netstats checks. Higher numbers result in more inaccurate statistics but reduce cpu usage. Min = 1, max = 600. [Default: 1] 84 | [/code] 85 | 86 | You can also combine the options like: 87 | 88 | [code]gmon -sPEr 120 -p /path/to/GB/:another/path -H 1 -d 3 -c 4 -m bittrex -C 2 -N "GB one":"Bot 2"[/code] 89 | 90 | [hr] 91 | 92 | [size=14pt][b]Support & Tips[/b][/size] 93 | 94 | You like gmon and it helps you earning money? 95 | 96 | - Report bugs in this forum thread or via pm in Telegram at @BeerK0in 97 | - Support gmon and send a tip to BTC wallet: 1GJCGZPn6okFefrRjPPWU73XgMrctSW1jT 98 | 99 | 100 | BK 101 | -------------------------------------------------------------------------------- /RELEASE-message-telegram-for-gmon.txt: -------------------------------------------------------------------------------- 1 | gmon update v0.0.51 is available now. 2 | 3 | Changelog 4 | 5 | - Bugfix: Show pm2 ids for process names like `AAA_XXX`, `AAA_XXX_M` and `AAA_XXX_m` 6 | - Added total number of monitored pairs per folder. 7 | - Added total BTC value of available BTC and current ALT value. 8 | - Added option to set names for different paths ( `-N kraken:polo` ). 9 | - Added option to set netstat connection check delay time ( `-C 5` ). 10 | 11 | 12 | Details at https://gunthy.org/index.php?topic=319.msg3441#msg3441 13 | 14 | To update to v0.0.51 (it removes gmon first): 15 | `npm uninstall -g gunbot-monitor && npm install -g gunbot-monitor` 16 | 17 | 18 | `npm uninstall -g gunbot-monitor` 19 | `rm /usr/bin/gmon` 20 | `rm -r /usr/lib/node_modules/gunbot-monitor` 21 | `rm -r /usr/local/lib/node_modules/gunbot-monitor` 22 | `npm install -g gunbot-monitor` 23 | -------------------------------------------------------------------------------- /__tests__/osData.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const osData = require('../src/modules/osData'); 3 | 4 | describe('OsData', function () { 5 | it('defines a megabyte', function () { 6 | assert.equal(osData.inMegabyte(30 * 1024 * 1024), 30); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /__tests__/tradePairs.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const tradePairs = require('../src/modules/tradePairs'); 3 | 4 | describe('TradePairs', function () { 5 | it('defines the RegExp for a state file.', function () { 6 | const marketPrefixs = [ 7 | 'binance', 8 | 'bittrex', 9 | 'bitfinex', 10 | 'cex', 11 | 'cryptopia', 12 | 'gdax', 13 | 'kraken', 14 | 'poloniex' 15 | ]; 16 | let regExStr = '('; 17 | for (let marketPrefix of marketPrefixs) { 18 | regExStr += marketPrefix + '|'; 19 | } 20 | 21 | regExStr = regExStr.slice(0, -1); 22 | regExStr += ')-(([A-Z0-9]{3,4})-[A-Z0-9]{2,16})-state.json'; 23 | const regExp = new RegExp(regExStr); 24 | assert.equal(tradePairs.fileRegExp.source, regExp.source); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /gmon-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BeerK0in/gunbot-monitor/169b9ea57abfa986801d41e3e0739eaae1e22271/gmon-screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gunbot-monitor", 3 | "version": "1.0.9", 4 | "description": "Monitors your GUNBOTs", 5 | "homepage": "https://github.com/BeerK0in/gunbot-monitor", 6 | "author": { 7 | "name": "BeerK0in", 8 | "email": "bk@beer-crypto.com", 9 | "url": "https://github.com/BeerK0in" 10 | }, 11 | "files": [ 12 | "src" 13 | ], 14 | "main": "src/index.js", 15 | "bin": { 16 | "gmon": "src/index.js" 17 | }, 18 | "keywords": [ 19 | "Gunbot", 20 | "monitor", 21 | "cli", 22 | "bitcoin", 23 | "gunthy", 24 | "traiding", 25 | "crypto" 26 | ], 27 | "scripts": { 28 | "lint": "eslint . --fix", 29 | "pretest": "npm run lint", 30 | "test": "jest", 31 | "release": "np" 32 | }, 33 | "devDependencies": { 34 | "coveralls": "3.0.4", 35 | "eslint": "5.16.0", 36 | "eslint-config-xo-space": "0.21.0", 37 | "jest": "24.8.0", 38 | "jest-cli": "24.8.0", 39 | "np": "5.0.3" 40 | }, 41 | "dependencies": { 42 | "chalk": "2.4.2", 43 | "cli-table": "0.3.1", 44 | "clui": "0.3.6", 45 | "commander": "2.20.0", 46 | "figlet": "1.2.3", 47 | "graceful-fs": "4.1.15", 48 | "log-update": "3.2.0", 49 | "node-netstat": "1.6.1" 50 | }, 51 | "jest": { 52 | "testEnvironment": "node", 53 | "collectCoverage": true, 54 | "coverageDirectory": "coverage" 55 | }, 56 | "eslintConfig": { 57 | "extends": "xo-space", 58 | "env": { 59 | "jest": true, 60 | "node": true 61 | } 62 | }, 63 | "repository": { 64 | "type": "git", 65 | "url": "git@github.com:BeerK0in/gunbot-monitor.git" 66 | }, 67 | "bugs": { 68 | "url": "https://github.com/BeerK0in/gunbot-monitor/issues" 69 | }, 70 | "license": "MIT" 71 | } 72 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const program = require('commander'); 6 | const path = require('path'); 7 | const chalk = require('chalk'); 8 | const settings = require('./modules/settings'); 9 | const outputter = require('./modules/outputter'); 10 | const pj = require('../package.json'); 11 | 12 | program 13 | .version(pj.version, '-v, --version') 14 | .option('-p, --path ', 'Path to the GUNBOT folder. Separate multiple paths with ":" (like: -p /path1:/path2). [Default: current folder]') 15 | .option('-N, --path-name ', 'Optional name for each path to the GUNBOT folder(s). Separate multiple path names with ":" (like: -N Kraken_Bot:Proxy_Mega_Bot). [Default: No path name]') 16 | .option('-c, --compact [groupSize]', 'Do not draw row lines. Optional set the number of rows after which a line is drawn. [Default: 0]') 17 | .option('-s, --small', 'Reduce columns for small screens') 18 | .option('-d, --digits ', 'Amount of digits for all numbers. Min = 0, max = 10. [Default: 4]') 19 | .option('-r, --refresh ', 'Seconds between table refresh. Min = 1, max = 600. [Default: 60]') 20 | .option('-m, --markets ', 'Filter of markets to show. Separate multiple markets with ":" (like: -m poloniex:kraken) [Default: all]') 21 | .option('-P, --profit', 'Use to activate the parsing of the profit. NOT WORKING CORRECTLY!') 22 | .option('-H, --hide-inactive ', 'Hides trading pairs which last log entry is older than given hours. Min = 1, max = 854400. [Default: 720]') 23 | // .option('-E, --show-all-errors', 'Use to list 422 errors in the last column.') 24 | .option('-C, --connections-check-delay ', 'Seconds between netstats checks. Higher numbers result in more inaccurate statistics but reduce cpu usage. Min = 1, max = 600. [Default: 1]') 25 | .option('-T, --i-have-sent-a-tip', 'Use this if you have sent a tip to BTC wallet: 1GJCGZPn6okFefrRjPPWU73XgMrctSW1jT') 26 | .parse(process.argv); 27 | 28 | // Set all paths to gunbot setup sub folders. 29 | if (program.path && program.path.length > 0) { 30 | let pathsToGunbot = program.path.split(':'); 31 | let pathNames = []; 32 | 33 | if (program.pathName && program.pathName.length > 0) { 34 | pathNames = program.pathName.split(':'); 35 | } 36 | 37 | settings.pathsToGunbot = []; 38 | 39 | for (const [index, pathToGunbot] of pathsToGunbot.entries()) { 40 | let pathName = null; 41 | 42 | if (pathNames[index] && pathNames[index].length > 0) { 43 | pathName = pathNames[index]; 44 | } 45 | 46 | if (pathToGunbot[0] === path.sep) { 47 | settings.pathsToGunbot.push({ 48 | path: path.normalize(pathToGunbot + path.sep), 49 | name: pathName 50 | }); 51 | } else { 52 | settings.pathsToGunbot.push({ 53 | path: path.normalize(process.cwd() + path.sep + pathToGunbot + path.sep), 54 | name: pathName 55 | }); 56 | } 57 | } 58 | } else { 59 | settings.pathsToGunbot = [{ 60 | path: path.normalize(process.cwd() + path.sep), 61 | name: null 62 | }]; 63 | } 64 | 65 | // Set all setups folder contents. 66 | try { 67 | settings.prepareSetupsFolders(); 68 | } catch (error) { 69 | console.error(chalk.red('')); 70 | console.error(chalk.red(`Error: ${error.message}`)); 71 | process.exit(); 72 | } 73 | 74 | // Enable compact mode. 75 | if (program.compact) { 76 | settings.compact = true; 77 | 78 | let groupSize = parseInt(program.compact, 10); 79 | if (groupSize > 0 && groupSize < 1000) { 80 | settings.compactGroupSize = groupSize; 81 | } 82 | } 83 | 84 | // Enable small mode. 85 | if (program.small) { 86 | settings.small = true; 87 | } 88 | 89 | // Set number of displayed digits. 90 | if (program.digits) { 91 | let numberOfDigits = parseInt(program.digits, 10); 92 | 93 | if (isNaN(numberOfDigits)) { 94 | numberOfDigits = 4; 95 | } 96 | 97 | if (numberOfDigits < 0) { 98 | numberOfDigits = 0; 99 | } 100 | 101 | if (numberOfDigits > 10) { 102 | numberOfDigits = 10; 103 | } 104 | 105 | settings.numberOfDigits = numberOfDigits; 106 | } 107 | 108 | // Set refresh rate. 109 | if (program.refresh) { 110 | let refreshRate = parseInt(program.refresh, 10); 111 | 112 | if (isNaN(refreshRate)) { 113 | refreshRate = 60; 114 | } 115 | 116 | if (refreshRate < 1) { 117 | refreshRate = 1; 118 | } 119 | 120 | if (refreshRate > 600) { 121 | refreshRate = 600; 122 | } 123 | 124 | settings.outputIntervalDelaySeconds = refreshRate; 125 | } 126 | 127 | // Set the markets to display. 128 | if (program.markets && program.markets.length > 0) { 129 | let markets = program.markets.split(':'); 130 | const allowedMarkets = [ 131 | 'binance', 132 | 'bittrex', 133 | 'bitfinex', 134 | 'cex', 135 | 'cryptopia', 136 | 'gdax', 137 | 'kraken', 138 | 'poloniex' 139 | ]; 140 | settings.marketPrefixs = []; 141 | for (let market of markets) { 142 | if (allowedMarkets.includes(market)) { 143 | settings.marketPrefixs.push(market); 144 | } 145 | } 146 | } 147 | 148 | // Set profit mode. 149 | if (program.profit) { 150 | settings.parseProfit = true; 151 | } 152 | 153 | // Set hide inactive mode. 154 | if (program.hideInactive) { 155 | let hideInactiveAfterHours = parseInt(program.hideInactive, 10); 156 | 157 | if (isNaN(hideInactiveAfterHours)) { 158 | hideInactiveAfterHours = 720; 159 | } 160 | 161 | if (hideInactiveAfterHours < 1) { 162 | hideInactiveAfterHours = 1; 163 | } 164 | 165 | if (hideInactiveAfterHours > 854400) { 166 | hideInactiveAfterHours = 854400; 167 | } 168 | 169 | settings.hideInactiveAfterHours = hideInactiveAfterHours; 170 | } 171 | 172 | // Set delay of checks of open connections. 173 | if (program.connectionsCheckDelay) { 174 | let connectionsCheckDelay = parseInt(program.connectionsCheckDelay, 10); 175 | 176 | if (isNaN(connectionsCheckDelay)) { 177 | connectionsCheckDelay = 1; 178 | } 179 | 180 | if (connectionsCheckDelay < 1) { 181 | connectionsCheckDelay = 1; 182 | } 183 | 184 | if (connectionsCheckDelay > 600) { 185 | connectionsCheckDelay = 600; 186 | } 187 | 188 | settings.connectionsCheckDelay = connectionsCheckDelay; 189 | } 190 | 191 | // If (program.showAllErrors) { 192 | // settings.showAllErrors = true; 193 | // } 194 | 195 | // Set whether there is a thank you message. 196 | if (program.iHaveSentATip) { 197 | settings.iHaveSentATip = true; 198 | } 199 | 200 | // And the magic begins. 201 | outputter.start(); 202 | -------------------------------------------------------------------------------- /src/modules/formatter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | const settings = require('./settings'); 5 | 6 | const TOO_LOW_TO_SELL = 1000; 7 | const TOO_HIGH_TO_BUY = 1100; 8 | const PRICE_IS_SWEET = 1200; 9 | 10 | class Formatter { 11 | tradePair(tradePair, marketName) { 12 | if (tradePair === undefined) { 13 | return chalk.gray('-'); 14 | } 15 | 16 | return `${this.market(marketName)}${chalk.green.bold(tradePair)}`; 17 | } 18 | 19 | coins(coins) { 20 | if (coins === undefined || isNaN(parseFloat(coins)) || parseFloat(coins) === 0) { 21 | return chalk.gray('-'); 22 | } 23 | 24 | return parseFloat(coins).toFixed(settings.numberOfDigits); 25 | } 26 | 27 | currentProfit(numberOfCoins, boughtPrice, lastPrice) { 28 | if (numberOfCoins === undefined || boughtPrice === undefined || lastPrice === undefined) { 29 | return chalk.gray('-'); 30 | } 31 | 32 | if (parseFloat(lastPrice) === 0 || parseFloat(boughtPrice) === 0) { 33 | return chalk.gray('-'); 34 | } 35 | 36 | if (isNaN(parseFloat(lastPrice)) || isNaN(parseFloat(boughtPrice))) { 37 | return chalk.gray('-'); 38 | } 39 | 40 | if (parseFloat(numberOfCoins) === 0) { 41 | return chalk.gray('-'); 42 | } 43 | 44 | let diff = parseFloat(lastPrice) - parseFloat(boughtPrice); 45 | let profit = this.price(parseFloat(numberOfCoins) * diff); 46 | 47 | return this.price(profit); 48 | } 49 | 50 | currentProfitWithPercent(numberOfCoins, boughtPrice, lastPrice) { 51 | let currentProfit = this.currentProfit(numberOfCoins, boughtPrice, lastPrice); 52 | 53 | if (currentProfit === undefined || currentProfit === chalk.gray('-') || currentProfit === '-') { 54 | return chalk.gray('-'); 55 | } 56 | 57 | let diff = parseFloat(lastPrice) - parseFloat(boughtPrice); 58 | let profitPercent = diff * 100 / parseFloat(boughtPrice); 59 | let negativePadding = (profitPercent < 0) ? '' : ' '; 60 | let percentPadding = (profitPercent >= 10 || profitPercent <= -10) ? '' : ' '; 61 | 62 | return `${this.profit(currentProfit)} ${negativePadding}${percentPadding}${chalk.gray(`${profitPercent.toFixed(1)}%`)}`; 63 | } 64 | 65 | profitPercent(boughtPrice, diffSinceBuy) { 66 | if (boughtPrice === undefined || diffSinceBuy === undefined) { 67 | return chalk.gray('-'); 68 | } 69 | 70 | if (isNaN(parseFloat(boughtPrice)) || isNaN(parseFloat(diffSinceBuy))) { 71 | return chalk.gray('-'); 72 | } 73 | 74 | if (parseFloat(boughtPrice) === 0 || parseFloat(diffSinceBuy) === 0) { 75 | return chalk.gray('-'); 76 | } 77 | 78 | let profitPercent = parseFloat(diffSinceBuy) * 100 / parseFloat(boughtPrice); 79 | let negativePadding = (profitPercent < 0) ? '' : ' '; 80 | let percentPadding = (profitPercent >= 10 || profitPercent <= -10) ? '' : ' '; 81 | 82 | return `${this.profit(diffSinceBuy)} ${negativePadding}${percentPadding}${chalk.gray(`${profitPercent.toFixed(1)}%`)}`; 83 | } 84 | 85 | price(price) { 86 | if (price === null) { 87 | return chalk.gray('-'); 88 | } 89 | 90 | if (price === undefined) { 91 | return chalk.gray('-'); 92 | } 93 | 94 | let priceOut = parseFloat(price).toFixed(settings.numberOfDigits); 95 | return priceOut; 96 | } 97 | 98 | priceFormatSmallNumbers(price) { 99 | if (price === 0 || price === null) { 100 | return this.price(null); 101 | } 102 | 103 | if (price < 0.01) { 104 | return this.price(price * 10000); 105 | } 106 | 107 | if (price < 0.001) { 108 | return this.price(price * 100000); 109 | } 110 | 111 | if (price < 0.0001) { 112 | return this.price(price * 1000000); 113 | } 114 | 115 | if (price < 0.00001) { 116 | return this.price(price * 10000000); 117 | } 118 | 119 | return this.price(price); 120 | } 121 | 122 | profit(price) { 123 | price = this.price(price); 124 | let priceAsFloat = parseFloat(price); 125 | 126 | if (isNaN(priceAsFloat)) { 127 | return chalk.white('0'); 128 | } 129 | 130 | if (priceAsFloat < 0) { 131 | return chalk.red(price); 132 | } 133 | 134 | if (priceAsFloat > 0) { 135 | return chalk.green(price); 136 | } 137 | 138 | return chalk.white(price); 139 | } 140 | 141 | getLatestBuySellSweetMessage(buyMessageDate, sellMessageDate, sweetMessageDate) { 142 | if (!(buyMessageDate instanceof Date)) { 143 | buyMessageDate = new Date(buyMessageDate || 0); 144 | } 145 | 146 | if (!(sellMessageDate instanceof Date)) { 147 | sellMessageDate = new Date(sellMessageDate || 0); 148 | } 149 | 150 | if (!(sweetMessageDate instanceof Date)) { 151 | sweetMessageDate = new Date(sweetMessageDate || 0); 152 | } 153 | 154 | if (buyMessageDate > sellMessageDate && buyMessageDate > sweetMessageDate) { 155 | return TOO_HIGH_TO_BUY; 156 | } 157 | 158 | if (sellMessageDate > buyMessageDate && sellMessageDate > sweetMessageDate) { 159 | return TOO_LOW_TO_SELL; 160 | } 161 | 162 | if (sweetMessageDate > buyMessageDate && sweetMessageDate > sellMessageDate) { 163 | return PRICE_IS_SWEET; 164 | } 165 | 166 | return false; 167 | } 168 | 169 | openOrders(count) { 170 | if (count > 0) { 171 | return chalk.red('yes'); 172 | } 173 | 174 | return chalk.gray('-'); 175 | } 176 | 177 | buySellMessage(buyMessageDate, sellMessageDate, sweetMessageDate) { 178 | let bss = this.getLatestBuySellSweetMessage(buyMessageDate, sellMessageDate, sweetMessageDate); 179 | 180 | if (bss === TOO_HIGH_TO_BUY) { 181 | return chalk.blue('too high'); 182 | } 183 | 184 | if (bss === TOO_LOW_TO_SELL) { 185 | return chalk.magenta('too low'); 186 | } 187 | 188 | if (bss === PRICE_IS_SWEET) { 189 | return chalk.green('sweet'); 190 | } 191 | 192 | return chalk.gray('-'); 193 | } 194 | 195 | buyPrice(numberOfCoins, boughtPrice, buyPrice) { 196 | if (numberOfCoins === undefined || 197 | boughtPrice === undefined || 198 | parseFloat(boughtPrice) === 0 || 199 | isNaN(parseFloat(boughtPrice)) || 200 | parseFloat(numberOfCoins) === 0) { 201 | return this.priceFormatSmallNumbers(buyPrice); 202 | } 203 | 204 | return chalk.yellow(this.priceFormatSmallNumbers(boughtPrice)); 205 | } 206 | 207 | priceDiff(buyPrice, sellPrice, lastPrice, coins) { 208 | const bp = parseFloat(buyPrice) || 0; 209 | const sp = parseFloat(sellPrice) || 0; 210 | const lp = parseFloat(lastPrice) || 0; 211 | const c = parseFloat(coins) || 0; 212 | 213 | if (lp === 0) { 214 | return chalk.red('No last price found'); 215 | } 216 | 217 | if (c > 0 && sp === 0) { 218 | return chalk.red('No sell price found'); 219 | } 220 | 221 | if (c === 0 && bp === 0) { 222 | return chalk.red('No buy price found'); 223 | } 224 | 225 | return this.calculatePriceDiff(c, sp, bp, lp); 226 | } 227 | 228 | calculatePriceDiff(c, sp, bp, lp) { 229 | if (c > 0 && sp > lp) { 230 | let diff = sp - lp; 231 | let percent = (diff / sp * 100).toFixed(2); 232 | let percentPadding = (percent >= 10 || percent <= -10) ? '' : ' '; 233 | 234 | return `${chalk.magenta(this.priceFormatSmallNumbers(diff))} ${percentPadding}${chalk.gray(`${percent}%`)}`; 235 | } 236 | 237 | if (c === 0 && lp > bp) { 238 | let diff = lp - bp; 239 | let percent = (diff / lp * 100).toFixed(2); 240 | let percentPadding = (percent >= 10 || percent <= -10) ? '' : ' '; 241 | 242 | return `${chalk.blue(this.priceFormatSmallNumbers(diff))} ${percentPadding}${chalk.gray(`${percent}%`)}`; 243 | } 244 | 245 | // Sweet to sell 246 | if (c > 0 && lp > sp) { 247 | let diff = lp - sp; 248 | let percent = (diff / lp * 100).toFixed(2); 249 | let percentPadding = (percent >= 10 || percent <= -10) ? '' : ' '; 250 | 251 | return `${chalk.green(this.priceFormatSmallNumbers(diff))} ${percentPadding}${chalk.gray(`${percent}%`)}`; 252 | } 253 | 254 | // Sweet to buy 255 | if (c === 0 && bp > lp) { 256 | let diff = bp - lp; 257 | let percent = (diff / bp * 100).toFixed(2); 258 | let percentPadding = (percent >= 10 || percent <= -10) ? '' : ' '; 259 | 260 | return `${chalk.green(this.priceFormatSmallNumbers(diff))} ${percentPadding}${chalk.gray(`${percent}%`)}`; 261 | } 262 | 263 | return chalk.gray('-'); 264 | } 265 | 266 | btcValue(numberOfCoins, lastPrice) { 267 | if (numberOfCoins === undefined || lastPrice === undefined || isNaN(parseFloat(numberOfCoins)) || parseFloat(numberOfCoins) === 0) { 268 | return chalk.gray('-'); 269 | } 270 | 271 | return this.price(parseFloat(numberOfCoins) * parseFloat(lastPrice)); 272 | } 273 | 274 | trades(numberOfTrades, lastTradeDate) { 275 | if (numberOfTrades === undefined || lastTradeDate === undefined) { 276 | return chalk.gray('-'); 277 | } 278 | 279 | if (numberOfTrades <= 0) { 280 | return chalk.gray('-'); 281 | } 282 | 283 | let padding = ' '; 284 | if (numberOfTrades > 9) { 285 | padding = ' '; 286 | } 287 | 288 | if (numberOfTrades > 99) { 289 | padding = ''; 290 | } 291 | 292 | return `${padding}${chalk.bold(numberOfTrades)} ${this.timeSince(lastTradeDate, 'trades')}`; 293 | } 294 | 295 | errorCode(errors, lastTimeStamp) { 296 | if (errors === undefined || errors.length === 0 || lastTimeStamp === undefined) { 297 | return chalk.gray('-'); 298 | } 299 | 300 | if (!(lastTimeStamp instanceof Date)) { 301 | lastTimeStamp = new Date(lastTimeStamp); 302 | } 303 | 304 | let output = []; 305 | let oldestErrorDate = new Date(); 306 | 307 | for (let code of Object.keys(errors)) { 308 | if (code === '422' && !settings.showAllErrors) { 309 | continue; 310 | } 311 | 312 | for (let date of errors[code].dates) { 313 | if (!(date instanceof Date)) { 314 | date = new Date(date); 315 | } 316 | 317 | if (date < oldestErrorDate) { 318 | oldestErrorDate = date; 319 | } 320 | } 321 | 322 | let seconds = Math.floor((lastTimeStamp - oldestErrorDate) / 1000); 323 | 324 | let codeOutput = ''; 325 | if (code === '429') { 326 | codeOutput = chalk.bold.bgRed(code); 327 | } else { 328 | codeOutput = chalk.red(code); 329 | } 330 | 331 | output.push(`${errors[code].counter} x ${codeOutput} in ${this.formatSeconds(seconds, 'errors')}`); 332 | } 333 | 334 | if (output.length === 0) { 335 | return chalk.gray('-'); 336 | } 337 | 338 | return output.join('\n'); 339 | } 340 | 341 | lastPrice(lastPrice, tendency) { 342 | let output = this.price(lastPrice); 343 | let tendencyOutput = ''; 344 | 345 | tendency = parseInt(tendency, 10); 346 | 347 | if (tendency <= -10) { 348 | tendencyOutput = chalk.red.bold('\u2193\u2193'); 349 | } 350 | 351 | if (tendency > -10 && tendency <= -2) { 352 | tendencyOutput = chalk.magenta.bold(' \u2193'); 353 | } 354 | 355 | if (tendency > -2 && tendency <= 1) { 356 | tendencyOutput = chalk.yellow.bold(' \u2192'); 357 | } 358 | 359 | if (tendency > 1 && tendency <= 9) { 360 | tendencyOutput = chalk.cyan.bold(' \u2191'); 361 | } 362 | 363 | if (tendency > 9) { 364 | tendencyOutput = chalk.green.bold('\u2191\u2191'); 365 | } 366 | 367 | return `${output} ${tendencyOutput}`; 368 | } 369 | 370 | tradesInTimeSlots(slots) { 371 | if (slots === undefined) { 372 | return chalk.gray('-'); 373 | } 374 | 375 | return `${this.colorizeTradesInTimeSlots(slots['1hr'])} ${this.colorizeTradesInTimeSlots(slots['6hr'])} ${this.colorizeTradesInTimeSlots(slots['12hr'])} ${this.colorizeTradesInTimeSlots(slots['24hr'])} ${this.colorizeTradesInTimeSlots(slots.older)}`; 376 | } 377 | 378 | colorizeTradesInTimeSlots(number) { 379 | if (number === undefined || number === 0) { 380 | return chalk.gray('-'); 381 | } 382 | 383 | return chalk.blue(number); 384 | } 385 | 386 | pm2Id(pairName, pm2Data) { 387 | if (pairName === undefined || pm2Data === undefined) { 388 | return chalk.gray('-'); 389 | } 390 | 391 | if (pm2Data[pairName] === undefined || pm2Data[pairName].id === undefined) { 392 | return chalk.gray('-'); 393 | } 394 | 395 | return pm2Data[pairName].id; 396 | } 397 | 398 | market(marketName) { 399 | if (marketName === undefined || marketName.length === 0) { 400 | return ''; 401 | } 402 | 403 | switch (marketName) { 404 | case 'binance': 405 | return `${chalk.yellow('Bi')} `; 406 | case 'bittrex': 407 | return `${chalk.blue('Bt')} `; 408 | case 'bitfinex': 409 | return `${chalk.green('Bf')} `; 410 | case 'cex': 411 | return `${chalk.magenta('CX')} `; 412 | case 'cryptopia': 413 | return `${chalk.gray('Cr')} `; 414 | case 'gdax': 415 | return `${chalk.cyan('GD')} `; 416 | case 'kraken': 417 | return `${chalk.yellow('Kr')} `; 418 | case 'poloniex': 419 | return `${chalk.cyan('Po')} `; 420 | default: 421 | return `${chalk.red(marketName)} `; 422 | } 423 | } 424 | 425 | strategies(strategy) { 426 | if (strategy === undefined || strategy.length === 0) { 427 | return chalk.gray('-'); 428 | } 429 | 430 | return strategy; 431 | } 432 | 433 | /** 434 | * @param pairName 435 | * @param pm2Data 436 | * @param market 437 | * @returns {*} 438 | */ 439 | pm2Status(pairName, pm2Data, market) { 440 | if (pairName === undefined || pm2Data === undefined) { 441 | return chalk.gray('-'); 442 | } 443 | 444 | // 1. Test for no suffix. 445 | if (pm2Data[pairName] === undefined || pm2Data[pairName].status === undefined) { 446 | if (market === undefined || market.length === 0) { 447 | return chalk.gray('-'); 448 | } 449 | 450 | // 2. Test for upper case suffix. 451 | pairName = `${pairName}_${market[0].toUpperCase()}`; 452 | if (pm2Data[pairName] === undefined || pm2Data[pairName].status === undefined) { 453 | // 3. Test for lower case suffix. 454 | pairName = `${pairName.slice(0, -2)}_${market[0].toLowerCase()}`; 455 | if (pm2Data[pairName] === undefined || pm2Data[pairName].status === undefined) { 456 | return chalk.gray('-'); 457 | } 458 | } 459 | } 460 | 461 | return this.colorStatus(pm2Data[pairName].id, pm2Data[pairName].status); 462 | } 463 | 464 | /** 465 | * Description 466 | * @method colorStatus 467 | * @param {} status 468 | * @return 469 | */ 470 | colorStatus(id, status) { 471 | switch (status) { 472 | case 'online': 473 | return chalk.green.bold(id); 474 | case 'offline': 475 | case 'stopped': 476 | return chalk.red.bold(id + ' off'); 477 | case 'launching': 478 | return chalk.blue.bold(id + ' launching'); 479 | default: 480 | return chalk.red.bold(id + ' ' + status); 481 | } 482 | } 483 | 484 | /** 485 | * Convert date object to a string containing time since 486 | * 487 | * @method timeSince 488 | * @return String 489 | * @param date 490 | * @param timeColorSchemeName 491 | */ 492 | timeSince(date, timeColorSchemeName = 'll') { 493 | if (date === undefined) { 494 | return chalk.gray(''); 495 | } 496 | 497 | if (!(date instanceof Date)) { 498 | date = new Date(date); 499 | } 500 | 501 | let seconds = Math.floor((new Date() - date) / 1000); 502 | return this.formatSeconds(seconds, timeColorSchemeName); 503 | } 504 | 505 | formatSeconds(seconds, timeColorSchemeName = 'll') { 506 | if (settings.timeColorScheme[timeColorSchemeName] === undefined) { 507 | timeColorSchemeName = 'll'; 508 | } 509 | 510 | let interval = Math.floor(seconds / 31536000); 511 | 512 | if (interval > 1) { 513 | return chalk[settings.timeColorScheme[timeColorSchemeName].yearsColor](interval + 'Y'); 514 | } 515 | 516 | interval = Math.floor(seconds / 2592000); 517 | if (interval > 1) { 518 | return chalk[settings.timeColorScheme[timeColorSchemeName].monthColor](interval + 'M'); 519 | } 520 | 521 | interval = Math.floor(seconds / 86400); 522 | if (interval > 1) { 523 | return chalk[settings.timeColorScheme[timeColorSchemeName].daysColor](interval + 'D'); 524 | } 525 | 526 | interval = Math.floor(seconds / 3600); 527 | if (interval > 1 && interval < 6) { 528 | return chalk[settings.timeColorScheme[timeColorSchemeName].hours1pColor](interval + 'h'); 529 | } 530 | 531 | if (interval > 1 && interval < 24) { 532 | return chalk[settings.timeColorScheme[timeColorSchemeName].hours6pColor](interval + 'h'); 533 | } 534 | 535 | if (interval > 1 && interval < 49) { 536 | return chalk[settings.timeColorScheme[timeColorSchemeName].hours24pColor](interval + 'h'); 537 | } 538 | 539 | if (interval > 1) { 540 | return chalk[settings.timeColorScheme[timeColorSchemeName].hours48pColor](interval + 'h'); 541 | } 542 | 543 | interval = Math.floor(seconds / 60); 544 | if (interval > 1) { 545 | return chalk[settings.timeColorScheme[timeColorSchemeName].minutesColor](interval + 'm'); 546 | } 547 | 548 | return chalk[settings.timeColorScheme[timeColorSchemeName].secondsColor](Math.floor(seconds) + 's'); 549 | } 550 | } 551 | 552 | module.exports = new Formatter(); 553 | 554 | -------------------------------------------------------------------------------- /src/modules/netData.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const netstat = require('node-netstat'); 4 | const settings = require('./settings'); 5 | 6 | class NetData { 7 | constructor() { 8 | this.interval = null; 9 | this.connectionsHistory = { 10 | binance: new Array(80).fill(0), 11 | bittrex: new Array(80).fill(0), 12 | bitfinex: new Array(80).fill(0), 13 | cex: new Array(80).fill(0), 14 | cryptopia: new Array(80).fill(0), 15 | gdax: new Array(80).fill(0), 16 | kraken: new Array(80).fill(0), 17 | poloniex: new Array(80).fill(0) 18 | }; 19 | } 20 | 21 | addToConnectionsHistory(market, value) { 22 | if (!this.connectionsHistory[market] || this.connectionsHistory[market] === undefined) { 23 | return false; 24 | } 25 | 26 | this.connectionsHistory[market].shift(); 27 | this.connectionsHistory[market].push(value); 28 | } 29 | 30 | getConnections() { 31 | if (this.interval === null) { 32 | this.start(); 33 | } 34 | 35 | return new Promise(resolve => { 36 | let sparkLine = require('clui').Sparkline; 37 | let result = { 38 | binance: `Connections: ${sparkLine(this.connectionsHistory.binance, ' cons/sec')} - Connections to Binance per sec.`, 39 | bittrex: `Connections: ${sparkLine(this.connectionsHistory.bittrex, ' cons/sec')} - Connections to Bittrex per sec.`, 40 | bitfinex: `Connections: ${sparkLine(this.connectionsHistory.bitfinex, ' cons/sec')} - Connections to Bitfinex per sec.`, 41 | cex: `Connections: ${sparkLine(this.connectionsHistory.cex, ' cons/sec')} - Connections to CEX per sec.`, 42 | cryptopia: `Connections: ${sparkLine(this.connectionsHistory.cryptopia, ' cons/sec')} - Connections to Cryptopia per sec.`, 43 | gdax: `Connections: ${sparkLine(this.connectionsHistory.gdax, ' cons/sec')} - Connections to GDAX per sec.`, 44 | kraken: `Connections: ${sparkLine(this.connectionsHistory.kraken, ' cons/sec')} - Connections to Kraken per sec.`, 45 | poloniex: `Connections: ${sparkLine(this.connectionsHistory.poloniex, ' cons/sec')} - Connections to Poloniex per sec.` 46 | }; 47 | 48 | resolve(result); 49 | }); 50 | } 51 | 52 | start() { 53 | this.calculateConnections(); 54 | 55 | this.interval = setInterval(() => this.calculateConnections(), settings.connectionsCheckDelay * 1000); 56 | } 57 | 58 | calculateConnections() { 59 | for (let market of settings.marketPrefixs) { 60 | let counter = 0; 61 | try { 62 | netstat({ 63 | filter: { 64 | state: 'ESTABLISHED' 65 | }, 66 | limit: 100, 67 | done: () => this.addToConnectionsHistory(market, counter) 68 | }, data => { 69 | if (!data || !data.remote || !data.remote.address) { 70 | return; 71 | } 72 | 73 | for (let ip of settings.marketApiIps[market]) { 74 | if (data.remote.address === ip) { 75 | counter++; 76 | } 77 | } 78 | }); 79 | } catch (error) { 80 | // Just go on with 0 81 | this.addToConnectionsHistory(0); 82 | } 83 | } 84 | } 85 | } 86 | 87 | module.exports = new NetData(); 88 | -------------------------------------------------------------------------------- /src/modules/osData.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const os = require('os'); 4 | const exec = require('child_process').exec; 5 | const clui = require('clui'); 6 | const chalk = require('chalk'); 7 | const settings = require('./settings'); 8 | 9 | class OsData { 10 | constructor() { 11 | this.megaByte = 1 / (Math.pow(1024, 2)); 12 | this.loadHistory = Array(80).fill(0); 13 | this.cpuTimingsTicks = 0; 14 | this.cpuTimingsLoad = 0; 15 | } 16 | 17 | inMegabyte(number) { 18 | return Math.floor(number * this.megaByte) || 1; 19 | } 20 | 21 | addToLoadHistory(value) { 22 | this.loadHistory.shift(); 23 | this.loadHistory.push(value); 24 | } 25 | 26 | getMemoryGauge() { 27 | return new Promise((resolve, reject) => { 28 | let gauge = clui.Gauge; 29 | 30 | this.calculateMemory() 31 | .then(stats => { 32 | let memoryTotal = this.inMegabyte(stats.total); 33 | let memoryAvailable = this.inMegabyte(stats.available); 34 | let memoryUsed = memoryTotal - memoryAvailable; 35 | let memoryUsedPercent = Math.floor(memoryUsed / memoryTotal * 100); 36 | 37 | let output = `Memory: ${gauge(memoryUsed, memoryTotal, 79, memoryTotal * 0.8, `${memoryUsedPercent}% used`)} - available: ${chalk.bold(memoryAvailable)} MB of ${chalk.bold(memoryTotal)} MB`; 38 | 39 | if (!stats.swaptotal || stats.swaptotal === 0) { 40 | resolve(output); 41 | return; 42 | } 43 | 44 | let swapTotal = this.inMegabyte(stats.swaptotal); 45 | let swapAvailable = this.inMegabyte(stats.swapfree); 46 | let swapUsed = this.inMegabyte(stats.swapused); 47 | let swapUsedPercent = Math.floor(swapUsed / swapTotal * 100); 48 | 49 | output += settings.newLine; 50 | output += `Swap: ${gauge(swapUsed, swapTotal, 79, swapTotal * 0.8, `${swapUsedPercent}% used`)} - available: ${chalk.bold(swapAvailable)} MB of ${chalk.bold(swapTotal)} MB`; 51 | resolve(output); 52 | }) 53 | .catch(error => reject(error)); 54 | }); 55 | } 56 | 57 | getLoad() { 58 | return new Promise((resolve, reject) => { 59 | let sparkline = require('clui').Sparkline; 60 | 61 | this.calculateLoad() 62 | .then(currentLoad => { 63 | let load = Math.floor(currentLoad); 64 | this.addToLoadHistory(load); 65 | resolve(`Load: ${sparkline(this.loadHistory, '%')} - current CPU load: ${chalk.bold(load)}%`); 66 | }) 67 | .catch(error => reject(error)); 68 | }); 69 | } 70 | 71 | calculateLoad() { 72 | let cpus = os.cpus(); 73 | let numberOfCPUs = cpus.length; 74 | 75 | return new Promise(resolve => { 76 | let totalUser = 0; 77 | let totalSystem = 0; 78 | let totalNice = 0; 79 | let totalIrq = 0; 80 | let totalIdle = 0; 81 | 82 | for (let i = 0; i < numberOfCPUs; i++) { 83 | let cpuTimings = cpus[i].times; 84 | 85 | totalUser += cpuTimings.user; 86 | totalSystem += cpuTimings.sys; 87 | totalNice += cpuTimings.nice; 88 | totalIrq += cpuTimings.irq; 89 | totalIdle += cpuTimings.idle; 90 | } 91 | 92 | let totalTicks = totalUser + totalSystem + totalNice + totalIrq + totalIdle; 93 | let totalLoad = totalUser + totalSystem + totalNice + totalIrq; 94 | 95 | let currentLoad = (totalLoad - this.cpuTimingsLoad) / (totalTicks - this.cpuTimingsTicks) * 100; 96 | 97 | this.cpuTimingsTicks = totalTicks; 98 | this.cpuTimingsLoad = totalLoad; 99 | 100 | resolve(currentLoad); 101 | }); 102 | } 103 | 104 | calculateMemory() { 105 | return new Promise(resolve => { 106 | let result = { 107 | total: os.totalmem(), 108 | free: os.freemem(), 109 | used: os.totalmem() - os.freemem(), 110 | 111 | active: os.totalmem() - os.freemem(), // Temporarily (fallback) 112 | available: os.freemem(), // Temporarily (fallback) 113 | buffcache: 0, 114 | 115 | swaptotal: 0, 116 | swapused: 0, 117 | swapfree: 0 118 | }; 119 | 120 | if (os.type() === 'Linux') { 121 | // LINUX 122 | // ------------------------------------------------------------------ 123 | exec('free -b', {maxBuffer: 1024 * 2000}, (error, stdout) => { 124 | if (!error) { 125 | let lines = stdout.toString().split('\n'); 126 | 127 | let mem = lines[1].replace(/ +/g, ' ').split(' '); 128 | result.total = parseInt(mem[1], 10); 129 | result.free = parseInt(mem[3], 10); 130 | 131 | if (lines.length === 4) { // Free (since free von procps-ng 3.3.10) 132 | result.buffcache = parseInt(mem[5], 10); 133 | result.available = parseInt(mem[6], 10); 134 | } else { // Free (older versions) 135 | result.buffcache = parseInt(mem[5], 10) + parseInt(mem[6], 10); 136 | result.available = result.free + result.buffcache; 137 | } 138 | 139 | result.active = result.total - result.free - result.buffcache; 140 | 141 | let swap = lines[2].replace(/ +/g, ' ').split(' '); 142 | result.swaptotal = parseInt(swap[1], 10); 143 | result.swapfree = parseInt(swap[3], 10); 144 | result.swapused = parseInt(swap[2], 10); 145 | } 146 | 147 | resolve(result); 148 | }); 149 | } else if (os.type() === 'Darwin') { 150 | // OSX 151 | // ------------------------------------------------------------------ 152 | exec('vm_stat | grep "Pages active"', {maxBuffer: 1024 * 2000}, (error, stdout) => { 153 | if (!error) { 154 | let lines = stdout.toString().split('\n'); 155 | 156 | result.active = parseInt(lines[0].split(':')[1], 10) * 4096; 157 | result.buffcache = result.used - result.active; 158 | result.available = result.free + result.buffcache; 159 | } 160 | 161 | exec('sysctl -n vm.swapusage', {maxBuffer: 1024 * 2000}, (error, stdout) => { 162 | if (!error) { 163 | let lines = stdout.toString().split('\n'); 164 | if (lines.length > 0) { 165 | let line = lines[0].replace(/,/g, '.').replace(/M/g, ''); 166 | line = line.trim().split(' '); 167 | for (let i = 0; i < line.length; i++) { 168 | if (line[i].toLowerCase().indexOf('total') !== -1) { 169 | result.swaptotal = parseFloat(line[i].split('=')[1].trim()) * 1024 * 1024; 170 | } 171 | 172 | if (line[i].toLowerCase().indexOf('used') !== -1) { 173 | result.swapused = parseFloat(line[i].split('=')[1].trim()) * 1024 * 1024; 174 | } 175 | 176 | if (line[i].toLowerCase().indexOf('free') !== -1) { 177 | result.swapfree = parseFloat(line[i].split('=')[1].trim()) * 1024 * 1024; 178 | } 179 | } 180 | } 181 | } 182 | 183 | resolve(result); 184 | }); 185 | }); 186 | } else { 187 | // All other OS. 188 | // ------------------------------------------------------------------ 189 | resolve(result); 190 | } 191 | }); 192 | } 193 | } 194 | 195 | module.exports = new OsData(); 196 | -------------------------------------------------------------------------------- /src/modules/outputter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const netData = require('./netData'); 4 | const osData = require('./osData'); 5 | const tableData = require('./tabelData'); 6 | const settings = require('./settings'); 7 | const logUpdate = require('log-update'); 8 | const figLet = require('figlet'); 9 | const chalk = require('chalk'); 10 | const pj = require('../../package.json'); 11 | 12 | class Outputter { 13 | constructor() { 14 | this.interval = null; 15 | this.headline = ' >>> GUNBOT - MONITOR <<<'; 16 | this.version = pj.version; 17 | } 18 | 19 | print() { 20 | this.collectOutputAndUpdateConsoleLog(); 21 | } 22 | 23 | collectOutputAndUpdateConsoleLog() { 24 | Promise.all([osData.getMemoryGauge(), osData.getLoad(), netData.getConnections(), tableData.getTables()]) 25 | .then(values => logUpdate(this.buildOutput(values))) 26 | .catch(error => { 27 | console.error(settings.newLine + chalk.red.bold(error) + settings.newLine); 28 | this.stop(); 29 | return false; 30 | }); 31 | } 32 | 33 | buildOutput(values) { 34 | const [memory, load, connections, tableData, error] = values; 35 | if (error !== undefined) { 36 | return settings.newLine + chalk.red.bold(error); 37 | } 38 | 39 | let output = settings.newLine; 40 | output += this.getServerTime(); 41 | output += settings.newLine; 42 | output += memory; 43 | output += settings.newLine; 44 | output += load; 45 | 46 | for (let market of settings.marketPrefixs) { 47 | if (tableData.foundMarkets.includes(market)) { 48 | output += settings.newLine; 49 | output += connections[market]; 50 | } 51 | } 52 | 53 | output += settings.newLine; 54 | output += settings.newLine; 55 | output += tableData.tables; 56 | output += settings.newLine; 57 | output += chalk.italic(`Use ${chalk.bold('CTRL+C')} to exit.`); 58 | output += ' | '; 59 | output += chalk.italic(`Type ${chalk.bold('gmon -h')} to see all options.`); 60 | output += ' | '; 61 | if (settings.iHaveSentATip) { 62 | output += chalk.bold.green('You are awesome! Thank you :)'); 63 | } else { 64 | output += chalk.bold.yellow('Support gmon and send a tip to BTC wallet: 1GJCGZPn6okFefrRjPPWU73XgMrctSW1jT'); 65 | } 66 | 67 | output += settings.newLine; 68 | 69 | return output; 70 | } 71 | 72 | start() { 73 | this.printHeadline() 74 | .then(() => { 75 | this.print(); 76 | 77 | this.interval = setInterval(() => { 78 | this.print(); 79 | }, settings.outputIntervalDelaySeconds * 1000); 80 | }); 81 | } 82 | 83 | stop() { 84 | clearInterval(this.interval); 85 | } 86 | 87 | getServerTime() { 88 | let date = new Date(); 89 | return `Server time: ${chalk.bold(date)}`; 90 | } 91 | 92 | getHeadlineText() { 93 | return this.headline; 94 | } 95 | 96 | getSubHeadlineText() { 97 | let output = `Version ${chalk.bold(this.version)}`; 98 | output += ' | '; 99 | output += `Refresh interval ${chalk.bold(settings.outputIntervalDelaySeconds)}s`; 100 | 101 | return output; 102 | } 103 | 104 | printHeadline() { 105 | return new Promise(resolve => { 106 | let newLine = settings.newLine; 107 | let headline = this.getHeadlineText(); 108 | let subHeadline = this.getSubHeadlineText(); 109 | 110 | figLet.text(headline, { 111 | font: 'Small' 112 | }, (err, data) => { 113 | if (err) { 114 | console.log(newLine + chalk.bold.yellow(headline)); 115 | console.log(newLine + chalk.white(subHeadline)); 116 | resolve(true); 117 | return; 118 | } 119 | 120 | console.log(newLine + chalk.bold.yellow(data)); 121 | console.log((subHeadline)); 122 | resolve(true); 123 | }); 124 | }); 125 | } 126 | } 127 | 128 | module.exports = new Outputter(); 129 | -------------------------------------------------------------------------------- /src/modules/pm2Data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const exec = require('child_process').exec; 4 | 5 | class Pm2Data { 6 | getProcesses() { 7 | return new Promise(resolve => { 8 | let result = {}; 9 | let isJson = this.isJson; 10 | 11 | try { 12 | exec('pm2 jlist', {maxBuffer: 1024 * 2000}, (error, stdout) => { 13 | if (error) { 14 | resolve(result); 15 | return; 16 | } 17 | 18 | if (!stdout || !isJson(stdout)) { 19 | resolve(result); 20 | return; 21 | } 22 | 23 | let processes = JSON.parse(stdout); 24 | 25 | for (let process of processes) { 26 | result[process.name] = { 27 | name: process.name, 28 | id: process.pm2_env.pm_id, 29 | status: process.pm2_env.status 30 | }; 31 | } 32 | 33 | resolve(result); 34 | }); 35 | } catch (e) { 36 | resolve(result); 37 | } 38 | }); 39 | } 40 | 41 | isJson(item) { 42 | if (typeof item !== 'string') { 43 | item = JSON.stringify(item); 44 | } 45 | 46 | try { 47 | item = JSON.parse(item); 48 | } catch (e) { 49 | return false; 50 | } 51 | 52 | if (typeof item === 'object' && item !== null) { 53 | return true; 54 | } 55 | 56 | return false; 57 | } 58 | } 59 | 60 | module.exports = new Pm2Data(); 61 | -------------------------------------------------------------------------------- /src/modules/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | const path = require('path'); 5 | 6 | class Settings { 7 | constructor() { 8 | this._pathsToGunbot = [{path: './', name: null}]; 9 | this._compact = false; 10 | this._compactGroupSize = 0; 11 | this._small = false; 12 | this._parseProfit = false; 13 | this._outputIntervalDelaySeconds = 60; 14 | this._showAllErrors = false; 15 | this._numberOfDigits = 4; 16 | this._hideInactiveAfterHours = 720; 17 | this._connectionsCheckDelay = 1; 18 | this._iHaveSentATip = false; 19 | this._marketPrefixs = [ 20 | 'binance', 21 | 'bittrex', 22 | 'bitfinex', 23 | 'cex', 24 | 'cryptopia', 25 | 'gdax', 26 | 'kraken', 27 | 'poloniex' 28 | ]; 29 | this.newLine = '\n'; 30 | this.marketApiIps = { 31 | binance: ['13.114.29.1', '54.65.237.133', '54.230.79.162', '54.192.122.25', '54.192.122.83', '54.192.122.102', '54.192.122.106', '54.192.122.121', '54.192.122.126', '54.192.122.135', '54.192.122.179'], 32 | bittrex: ['104.17.152.108', '104.17.153.108', '104.17.154.108', '104.17.155.108', '104.17.156.108'], 33 | bitfinex: ['104.16.171.181', '104.16.172.181', '104.16.173.181', '104.16.174.181', '104.16.175.181'], 34 | cex: ['104.20.33.190', '104.20.34.190'], 35 | cryptopia: ['45.60.11.241', '45.60.13.241'], 36 | gdax: ['104.16.107.31', '104.16.108.31'], 37 | kraken: ['104.16.211.191', '104.16.212.191', '104.16.213.191', '104.16.214.191', '104.16.215.191'], 38 | poloniex: ['104.20.12.48', '104.20.13.48'] 39 | }; 40 | 41 | this.timeColorScheme = { 42 | ll: { 43 | yearsColor: 'white', 44 | monthColor: 'white', 45 | daysColor: 'white', 46 | hours48pColor: 'white', 47 | hours24pColor: 'white', 48 | hours6pColor: 'white', 49 | hours1pColor: 'white', 50 | minutesColor: 'white', 51 | secondsColor: 'green' 52 | }, 53 | trades: { 54 | yearsColor: 'red', 55 | monthColor: 'red', 56 | daysColor: 'red', 57 | hours48pColor: 'red', 58 | hours24pColor: 'magenta', 59 | hours6pColor: 'yellow', 60 | hours1pColor: 'cyan', 61 | minutesColor: 'green', 62 | secondsColor: 'green' 63 | }, 64 | errors: { 65 | yearsColor: 'red', 66 | monthColor: 'red', 67 | daysColor: 'red', 68 | hours48pColor: 'red', 69 | hours24pColor: 'red', 70 | hours6pColor: 'red', 71 | hours1pColor: 'red', 72 | minutesColor: 'red', 73 | secondsColor: 'red' 74 | } 75 | }; 76 | } 77 | 78 | set pathsToGunbot(paths) { 79 | this._pathsToGunbot = paths; 80 | } 81 | 82 | get pathsToGunbot() { 83 | return this._pathsToGunbot; 84 | } 85 | 86 | set compact(value) { 87 | this._compact = value; 88 | } 89 | 90 | get compact() { 91 | return this._compact; 92 | } 93 | 94 | set compactGroupSize(value) { 95 | this._compactGroupSize = value; 96 | } 97 | 98 | get compactGroupSize() { 99 | return this._compactGroupSize; 100 | } 101 | 102 | set small(value) { 103 | this._small = value; 104 | } 105 | 106 | get small() { 107 | return this._small; 108 | } 109 | 110 | set marketPrefixs(value) { 111 | this._marketPrefixs = value; 112 | } 113 | 114 | get marketPrefixs() { 115 | return this._marketPrefixs; 116 | } 117 | 118 | set parseProfit(value) { 119 | this._parseProfit = value; 120 | } 121 | 122 | get parseProfit() { 123 | return this._parseProfit; 124 | } 125 | 126 | set outputIntervalDelaySeconds(value) { 127 | this._outputIntervalDelaySeconds = value; 128 | } 129 | 130 | get outputIntervalDelaySeconds() { 131 | return this._outputIntervalDelaySeconds; 132 | } 133 | 134 | set showAllErrors(value) { 135 | this._showAllErrors = value; 136 | } 137 | 138 | get showAllErrors() { 139 | return this._showAllErrors; 140 | } 141 | 142 | set numberOfDigits(value) { 143 | this._numberOfDigits = value; 144 | } 145 | 146 | get numberOfDigits() { 147 | return this._numberOfDigits; 148 | } 149 | 150 | set hideInactiveAfterHours(value) { 151 | this._hideInactiveAfterHours = value; 152 | } 153 | 154 | get hideInactiveAfterHours() { 155 | return this._hideInactiveAfterHours; 156 | } 157 | 158 | set connectionsCheckDelay(value) { 159 | this._connectionsCheckDelay = value; 160 | } 161 | 162 | get connectionsCheckDelay() { 163 | return this._connectionsCheckDelay; 164 | } 165 | 166 | set iHaveSentATip(value) { 167 | this._iHaveSentATip = value; 168 | } 169 | 170 | get iHaveSentATip() { 171 | return this._iHaveSentATip; 172 | } 173 | 174 | prepareSetupsFolders() { 175 | let pathsToSetups = []; 176 | for (let pathToGunbot of this._pathsToGunbot) { 177 | // Check if this path contains state.json files. 178 | let files; 179 | try { 180 | files = fs.readdirSync(`${pathToGunbot.path}`); 181 | } catch (e) { 182 | throw new Error(`Can not find directory ${pathToGunbot.path}. Please run gmon in the root Gunbot directory.`); 183 | } 184 | 185 | const stateFileRegExp = new RegExp('.*-[A-Z0-9]{3,4}-[A-Z0-9]{2,16}-state.json'); 186 | for (let file of files) { 187 | let matches = stateFileRegExp.exec(file); 188 | if (matches && matches.length > 0) { 189 | // Set Gunbot root path as one source of state files. 190 | pathsToSetups.push({ 191 | path: `${pathToGunbot.path}`, 192 | name: pathToGunbot.name || 'Root dir' 193 | }); 194 | 195 | break; 196 | } 197 | } 198 | 199 | // Checks if the gui version is running 200 | let setups; 201 | try { 202 | setups = fs.readdirSync(`${pathToGunbot.path}setups${path.sep}`); 203 | } catch (e) { 204 | setups = []; 205 | } 206 | 207 | let index = 1; 208 | for (let setup of setups) { 209 | try { 210 | fs.statSync(`${pathToGunbot.path}setups${path.sep}${setup}${path.sep}`).isDirectory(); 211 | } catch (e) { 212 | continue; 213 | } 214 | 215 | pathsToSetups.push({ 216 | path: `${pathToGunbot.path}setups${path.sep}${setup}${path.sep}`, 217 | name: pathToGunbot.name || `Setup ${index++}` 218 | }); 219 | } 220 | } 221 | 222 | this._pathsToGunbot = pathsToSetups; 223 | } 224 | } 225 | 226 | module.exports = new Settings(); 227 | -------------------------------------------------------------------------------- /src/modules/tabelData.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const CliTable = require('cli-table'); 3 | const chalk = require('chalk'); 4 | const tradePairs = require('./tradePairs'); 5 | const tradePairParser = require('./tradePairParser'); 6 | const formatter = require('./formatter'); 7 | const settings = require('./settings'); 8 | 9 | class TableData { 10 | parseAvailableTradePairsNames(pathToGunbot) { 11 | return new Promise(resolve => { 12 | tradePairs.getTradePairs(pathToGunbot) 13 | .then(pairs => resolve(pairs)) 14 | .catch(error => console.error(error)); 15 | }); 16 | } 17 | 18 | getTables() { 19 | return new Promise((resolve, reject) => { 20 | let allPromises = []; 21 | 22 | for (let pathToGunbot of settings.pathsToGunbot) { 23 | allPromises.push(this.getTable(pathToGunbot)); 24 | } 25 | 26 | Promise.all(allPromises) 27 | .then(tables => { 28 | let tableData = { 29 | tables: '', 30 | availableBitCoins: '', 31 | totalAvailableBitCoins: 0, 32 | availableBitCoinsPerMarket: {}, 33 | foundMarkets: [] 34 | }; 35 | 36 | for (let table of tables) { 37 | tableData.availableBitCoins = ''; 38 | tableData.totalAvailableBitCoins = 0; 39 | tableData.availableBitCoinsPerMarket = {}; 40 | 41 | for (let market of settings.marketPrefixs) { 42 | if (table.availableBitCoins[market] && table.availableBitCoins[market] > 0) { 43 | tableData.availableBitCoinsPerMarket[market] = table.availableBitCoins[market]; 44 | tableData.foundMarkets.push(market); 45 | } 46 | } 47 | 48 | for (let market of Object.keys(tableData.availableBitCoinsPerMarket)) { 49 | if (tableData.availableBitCoinsPerMarket[market] && tableData.availableBitCoinsPerMarket[market] > 0) { 50 | tableData.availableBitCoins += ` ${market} ${parseFloat(tableData.availableBitCoinsPerMarket[market])} `; 51 | tableData.totalAvailableBitCoins += parseFloat(tableData.availableBitCoinsPerMarket[market]); 52 | } 53 | } 54 | 55 | if (table.name && table.name.length > 0) { 56 | tableData.tables += `${chalk.bold.blue(table.name)} `; 57 | } 58 | 59 | const totalBtcValue = (parseFloat(tableData.totalAvailableBitCoins) + parseFloat(table.totalBtcInAltCoins)).toFixed(settings.numberOfDigits); 60 | const totalAvailableBtc = parseFloat(tableData.totalAvailableBitCoins).toFixed(settings.numberOfDigits); 61 | const totalBtcInAlts = parseFloat(table.totalBtcInAltCoins).toFixed(settings.numberOfDigits); 62 | tableData.tables += ` Available BitCoins: ${tableData.availableBitCoins} | Total BTC value: ${chalk.bold.green(totalBtcValue)} (as BTC: ${totalAvailableBtc}, as ALTs: ${totalBtcInAlts})`; 63 | tableData.tables += settings.newLine; 64 | tableData.tables += table.table; 65 | tableData.tables += settings.newLine; 66 | } 67 | 68 | resolve(tableData); 69 | }) 70 | .catch(error => reject(error)); 71 | }); 72 | } 73 | 74 | getTable(pathToGunbot) { 75 | return new Promise((resolve, reject) => { 76 | let table = new CliTable(this.getHead()); 77 | 78 | this.parseAvailableTradePairsNames(pathToGunbot) 79 | .then(pairs => { 80 | this.fillContent(table, pairs, pathToGunbot) 81 | .then(table => resolve(table)) 82 | .catch(error => reject(error)); 83 | }); 84 | }); 85 | } 86 | 87 | getHead() { 88 | let header = { 89 | head: [ 90 | chalk.cyan.bold('Name'), 91 | chalk.cyan.bold('Str'), 92 | chalk.cyan.bold('LL'), 93 | chalk.cyan.bold('OO?'), 94 | chalk.cyan.bold('# Coins'), 95 | chalk.cyan.bold('in BTC'), 96 | chalk.cyan.bold('Diff since buy'), 97 | chalk.cyan.bold('Buy/Bought'), 98 | chalk.cyan.bold('Sell'), 99 | chalk.cyan.bold('Last price'), 100 | chalk.cyan.bold('Price diff'), 101 | chalk.cyan.bold('# Buys'), 102 | chalk.cyan.bold('1 6 h d +'), 103 | chalk.cyan.bold('# Sells'), 104 | chalk.cyan.bold('1 6 h d +'), 105 | chalk.cyan.bold('Profit') 106 | ], 107 | colAligns: [ 108 | 'left', // Name 109 | 'left', // Strategies 110 | 'right', // Last log time 111 | 'left', // Oo? 112 | 'right', // Coins 113 | 'right', // In BTC 114 | 'right', // Diff 115 | 'right', // Buy / Bought price 116 | 'right', // Price to sell 117 | 'right', // Last price 118 | 'right', // Price diff 119 | 'left', // Buys 120 | 'left', // 1 6 h d + 121 | 'left', // Sells 122 | 'left', // 1 6 h d + 123 | 'right' // Profit 124 | ], 125 | style: {compact: settings.compact} 126 | }; 127 | 128 | return this.formatTableHeader(header); 129 | } 130 | 131 | fillContent(table, pairs, pathToGunbot) { 132 | return new Promise((resolve, reject) => { 133 | let result = {}; 134 | let availableBitCoins = {}; 135 | let latestAvailableBitCoinsDate = {}; 136 | 137 | let allPromises = []; 138 | 139 | for (let market of Object.keys(pairs)) { 140 | availableBitCoins[market] = 0; 141 | latestAvailableBitCoinsDate[market] = new Date(0); 142 | 143 | if (pairs[market].length === 0) { 144 | continue; 145 | } 146 | 147 | for (let tradePair of pairs[market]) { 148 | allPromises.push(tradePairParser.getData(tradePair, market, pathToGunbot)); 149 | } 150 | } 151 | 152 | Promise.all(allPromises) 153 | .then(values => { 154 | let totalBTCValue = 0.0; 155 | let totalDiffSinceBuy = 0.0; 156 | let totalBoughtPrice = 0.0; 157 | let totalProfit = 0.0; 158 | let counter = 0; 159 | 160 | // Values.shift(); 161 | 162 | for (let data of values) { 163 | if (data === undefined || data.lastTimeStamp === undefined) { 164 | continue; 165 | } 166 | 167 | // Hides inactive pairs. 168 | let inactiveFilterTimestamp = Math.round(new Date().getTime() / 1000) - (settings.hideInactiveAfterHours * 60 * 60); 169 | let lastLogTimestamp = Math.round(new Date(data.lastTimeStamp).getTime() / 1000); 170 | 171 | if (inactiveFilterTimestamp > lastLogTimestamp) { 172 | continue; 173 | } 174 | 175 | // Get amount of available bitcoins 176 | if (data.availableBitCoins !== undefined) { 177 | if (!(data.availableBitCoinsTimeStamp instanceof Date)) { 178 | data.availableBitCoinsTimeStamp = new Date(data.availableBitCoinsTimeStamp || 0); 179 | } 180 | 181 | if (data.availableBitCoinsTimeStamp > latestAvailableBitCoinsDate[data.market]) { 182 | availableBitCoins[data.market] = data.availableBitCoins; 183 | latestAvailableBitCoinsDate[data.market] = data.availableBitCoinsTimeStamp; 184 | } 185 | } 186 | 187 | if (!isNaN(parseFloat(formatter.btcValue(data.coins, data.lastPrice)))) { 188 | totalBTCValue += parseFloat(formatter.btcValue(data.coins, data.lastPrice)); 189 | } 190 | 191 | if (!isNaN(parseFloat(formatter.currentProfit(data.coins, data.boughtPrice, data.lastPrice)))) { 192 | totalDiffSinceBuy += parseFloat(formatter.currentProfit(data.coins, data.boughtPrice, data.lastPrice)); 193 | } 194 | 195 | if (!isNaN(parseFloat(data.boughtPrice)) && !isNaN(parseFloat(data.coins))) { 196 | totalBoughtPrice += parseFloat(data.boughtPrice) * parseFloat(data.coins); 197 | } 198 | 199 | if (!isNaN(parseFloat(data.profit))) { 200 | totalProfit += parseFloat(data.profit); 201 | } 202 | 203 | if (settings.compact && counter % settings.compactGroupSize === 0) { 204 | table.push([]); 205 | } 206 | 207 | counter++; 208 | 209 | table.push([ 210 | formatter.tradePair(data.tradePair, data.market), 211 | formatter.strategies(data.strategy), 212 | formatter.timeSince(data.lastTimeStamp), 213 | formatter.openOrders(data.openOrders), 214 | formatter.coins(data.coins), 215 | formatter.btcValue(data.coins, data.lastPrice), 216 | formatter.currentProfitWithPercent(data.coins, data.boughtPrice, data.lastPrice), 217 | formatter.buyPrice(data.coins, data.boughtPrice, data.buyPrice), 218 | formatter.priceFormatSmallNumbers(data.sellPrice), 219 | formatter.priceFormatSmallNumbers(data.lastPrice), 220 | formatter.priceDiff(data.buyPrice, data.sellPrice, data.lastPrice, data.coins), 221 | formatter.trades(data.buyCounter, data.lastTimeStampBuy), 222 | formatter.tradesInTimeSlots(data.buys), 223 | formatter.trades(data.sellCounter, data.lastTimeStampSell), 224 | formatter.tradesInTimeSlots(data.sells), 225 | formatter.profit(data.profit) 226 | ]); 227 | } 228 | 229 | const numberOfRows = table.length; 230 | if (settings.compact) { 231 | table.push([]); 232 | } 233 | 234 | table.push([ 235 | chalk.bold(formatter.tradePair(`= TOTAL (${numberOfRows}) =`)), 236 | '', 237 | '', 238 | '', 239 | '', 240 | chalk.bold(formatter.price(totalBTCValue)), 241 | chalk.bold(formatter.profitPercent(totalBoughtPrice, totalDiffSinceBuy)), 242 | '', 243 | '', 244 | '', 245 | '', 246 | '', 247 | '', 248 | '', 249 | '', 250 | chalk.bold(formatter.profit(totalProfit)) 251 | ]); 252 | 253 | result.table = this.formatTableContent(table); 254 | result.availableBitCoins = availableBitCoins; 255 | result.totalBtcInAltCoins = totalBTCValue; 256 | result.name = pathToGunbot.name; 257 | resolve(result); 258 | }) 259 | .catch(error => reject(error)); 260 | }); 261 | } 262 | 263 | formatTableHeader(header) { 264 | if (!settings.parseProfit) { 265 | // Profit 266 | header.head.splice(-1, 1); 267 | header.colAligns.splice(-1, 1); 268 | } 269 | 270 | if (settings.small) { 271 | // Open Order 272 | header.head.splice(3, 1); 273 | header.colAligns.splice(3, 1); 274 | 275 | // Number of Coins 276 | header.head.splice(3, 1); 277 | header.colAligns.splice(3, 1); 278 | 279 | // Buys per time 280 | header.head.splice(10, 1); 281 | header.colAligns.splice(10, 1); 282 | 283 | // Sells per time 284 | header.head.splice(11, 1); 285 | header.colAligns.splice(11, 1); 286 | } 287 | 288 | return header; 289 | } 290 | 291 | formatTableContent(table) { 292 | if (!settings.parseProfit) { 293 | for (let content of table) { 294 | // Profit 295 | content.splice(-1, 1); 296 | } 297 | } 298 | 299 | if (settings.small) { 300 | for (let content of table) { 301 | // Open Order 302 | content.splice(3, 1); 303 | 304 | // Number of Coins 305 | content.splice(3, 1); 306 | 307 | // Buys per time 308 | content.splice(10, 1); 309 | 310 | // Sells per time 311 | content.splice(11, 1); 312 | } 313 | } 314 | 315 | return table; 316 | } 317 | } 318 | 319 | module.exports = new TableData(); 320 | -------------------------------------------------------------------------------- /src/modules/tradePairParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | 5 | class TradePairParser { 6 | getData(tradePair, market, pathToGunbot) { 7 | return new Promise((resolve, reject) => { 8 | Promise.all([ 9 | this.readStateFile(tradePair, market, pathToGunbot.path), 10 | this.readConfigFile(tradePair, market, pathToGunbot.path) 11 | ]) 12 | .then(values => { 13 | resolve(Object.assign({}, ...values)); 14 | }) 15 | .catch(error => reject(error)); 16 | }); 17 | } 18 | 19 | readStateFile(tradePair, market, pathToGunbot) { 20 | return new Promise(resolve => { 21 | let filePath = `${pathToGunbot}${market}-${tradePair}-state.json`; 22 | 23 | fs.stat(filePath, (error, stats) => { 24 | if (error) { 25 | resolve([]); 26 | return; 27 | } 28 | 29 | const contents = fs.readFileSync(filePath); 30 | let state; 31 | try { 32 | state = JSON.parse(contents); 33 | } catch (e) { 34 | resolve([]); 35 | return; 36 | } 37 | 38 | // 1. Step: Iterate through all orders and get the latest buys and sells. 39 | let collectedData = this.getOrderData(state); 40 | 41 | // 2. Step: Set all other needed data. 42 | const openOrders = state.openOrders || []; 43 | collectedData.tradePair = tradePair; 44 | collectedData.market = market; 45 | collectedData.lastTimeStamp = new Date(stats.mtime).getTime() || 0; 46 | collectedData.coins = state.quoteBalance || null; 47 | collectedData.boughtPrice = state.newABP || state.ABP || 0; 48 | collectedData.buyPrice = state.priceToBuy || null; 49 | collectedData.sellPrice = state.priceToSell || null; 50 | collectedData.lastPrice = collectedData.coins === null ? state.Ask || 0 : state.Bid || 0; 51 | collectedData.lastErrorCode = ''; 52 | collectedData.lastErrorTimeStamp = ''; 53 | collectedData.openOrders = openOrders.length || 0; 54 | collectedData.availableBitCoins = state.baseBalance || 0; 55 | collectedData.availableBitCoinsTimeStamp = new Date(stats.mtime).getTime() || 0; 56 | collectedData.profit = state.profitbtc || 0; 57 | 58 | resolve(collectedData); 59 | }); 60 | }); 61 | } 62 | 63 | getOrderData(state) { 64 | let collectedData = []; 65 | collectedData.buyCounter = 0; 66 | collectedData.sellCounter = 0; 67 | collectedData.lastTimeStampBuy = 0; 68 | collectedData.lastTimeStampSell = 0; 69 | collectedData.buys = { 70 | '1hr': 0, 71 | '6hr': 0, 72 | '12hr': 0, 73 | '24hr': 0, 74 | older: 0 75 | }; 76 | collectedData.sells = { 77 | '1hr': 0, 78 | '6hr': 0, 79 | '12hr': 0, 80 | '24hr': 0, 81 | older: 0 82 | }; 83 | const orders = state.orders || {}; 84 | 85 | let orderIds = Object.keys(orders); 86 | for (let orderId of orderIds) { 87 | const order = state.orders[orderId]; 88 | 89 | if (order.type === 'sell') { 90 | collectedData.sellCounter++; 91 | 92 | if (order.time > collectedData.lastTimeStampSell) { 93 | collectedData.lastTimeStampSell = order.time; 94 | } 95 | 96 | this.sortTradesInTimeSlots(collectedData.sells, order.time); 97 | } 98 | 99 | if (order.type === 'buy') { 100 | collectedData.buyCounter++; 101 | 102 | if (order.time > collectedData.lastTimeStampBuy) { 103 | collectedData.lastTimeStampBuy = order.time; 104 | } 105 | 106 | this.sortTradesInTimeSlots(collectedData.buys, order.time); 107 | } 108 | } 109 | 110 | return collectedData; 111 | } 112 | 113 | readConfigFile(tradePair, market, pathToGunbot) { 114 | return new Promise(resolve => { 115 | let filePath = `${pathToGunbot}config.js`; 116 | 117 | fs.stat(filePath, error => { 118 | if (error) { 119 | resolve([]); 120 | return; 121 | } 122 | 123 | let collectedData = []; 124 | 125 | const contents = fs.readFileSync(filePath); 126 | let config; 127 | try { 128 | config = JSON.parse(contents); 129 | collectedData.strategy = config.pairs[market][tradePair].strategy || ''; 130 | } catch (e) { 131 | resolve(collectedData); 132 | return; 133 | } 134 | 135 | resolve(collectedData); 136 | }); 137 | }); 138 | } 139 | 140 | sortTradesInTimeSlots(container, date) { 141 | if (date === undefined) { 142 | return container; 143 | } 144 | 145 | if (!(date instanceof Date)) { 146 | date = new Date(date); 147 | } 148 | 149 | let hours = Math.floor((new Date() - date) / 1000 / 60 / 60); 150 | 151 | if (hours <= 1 && hours >= 0) { 152 | container['1hr']++; 153 | } 154 | 155 | if (hours <= 6 && hours > 1) { 156 | container['6hr']++; 157 | } 158 | 159 | if (hours <= 12 && hours > 6) { 160 | container['12hr']++; 161 | } 162 | 163 | if (hours <= 24 && hours > 12) { 164 | container['24hr']++; 165 | } 166 | 167 | if (hours > 24) { 168 | container.older++; 169 | } 170 | 171 | return container; 172 | } 173 | } 174 | 175 | module.exports = new TradePairParser(); 176 | -------------------------------------------------------------------------------- /src/modules/tradePairs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('graceful-fs'); 4 | const settings = require('./settings'); 5 | 6 | class TradePairs { 7 | constructor() { 8 | this.fileRegExp = TradePairs.buildFileRegExp(); 9 | } 10 | 11 | getTradePairs(pathToGunbot) { 12 | let pairs = TradePairs.initTradePairs(); 13 | return new Promise((resolve, reject) => { 14 | try { 15 | fs.readdir(pathToGunbot.path, (error, files) => { 16 | // If there is an error ... 17 | if (error) { 18 | // ... and the pairs have never been set, reject. 19 | if (pairs.length === 0) { 20 | reject(error); 21 | return; 22 | } 23 | 24 | // ... but the pairs are already set, just return the last pairs. 25 | resolve(pairs); 26 | return; 27 | } 28 | 29 | for (let file of files) { 30 | let matches = this.fileRegExp.exec(file); 31 | if (!matches || matches.length < 2) { 32 | continue; 33 | } 34 | 35 | if (pairs[matches[1]] === undefined) { 36 | continue; 37 | } 38 | 39 | pairs[matches[1]].push(matches[2]); 40 | } 41 | 42 | resolve(pairs); 43 | }); 44 | } catch (error) { 45 | // If there is an error ... 46 | 47 | // ... and the pairs have never been set, reject. 48 | if (pairs.length === 0) { 49 | reject(error); 50 | return; 51 | } 52 | 53 | // ... but the pairs are already set, just return the last pairs. 54 | resolve(pairs); 55 | } 56 | }); 57 | } 58 | 59 | static initTradePairs() { 60 | let pairs = []; 61 | for (let marketPrefix of settings.marketPrefixs) { 62 | pairs[marketPrefix] = []; 63 | } 64 | 65 | return pairs; 66 | } 67 | 68 | static buildFileRegExp() { 69 | let regExStr = '('; 70 | for (let marketPrefix of settings.marketPrefixs) { 71 | regExStr += marketPrefix + '|'; 72 | } 73 | 74 | regExStr = regExStr.slice(0, -1); 75 | regExStr += ')-(([A-Z0-9]{3,4})-[A-Z0-9]{2,16})-state.json'; 76 | return new RegExp(regExStr); 77 | } 78 | } 79 | 80 | module.exports = new TradePairs(); 81 | --------------------------------------------------------------------------------