├── _config.yml ├── kraken.key ├── Procfile ├── demo.gif ├── requirements.txt ├── config.json ├── .gitignore ├── README.md └── telegram_kraken_bot.py /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /kraken.key: -------------------------------------------------------------------------------- 1 | some_api_key 2 | some_private_key -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python telegram_kraken_bot.py 2 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Endogen/Telegram-Kraken-Bot/HEAD/demo.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | krakenex==2.0.0 2 | requests==2.18.4 3 | beautifulsoup4==4.6.0 4 | python-telegram-bot==9.0.0 -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "user_id": "some_user", 3 | "bot_token": "some_bot_token", 4 | "base_currency": "EUR", 5 | "check_trade": 30, 6 | "history_items": 3, 7 | "update_url": "https://raw.githubusercontent.com/endogen/Telegram-Kraken-Bot/master/telegram_kraken_bot.py", 8 | "update_hash": "some_hash", 9 | "update_check": 86400, 10 | "send_error": false, 11 | "show_access_denied": true, 12 | "used_pairs": { 13 | "XBT": "EUR", 14 | "BCH": "EUR", 15 | "ETH": "EUR", 16 | "XMR": "EUR", 17 | "XRP": "EUR", 18 | "XLM": "XBT", 19 | "GNO": "ETH", 20 | "ICN": "XBT", 21 | "EOS": "EUR" 22 | }, 23 | "coin_charts": { 24 | "XBT": "https://tinyurl.com/y9p6g5a8", 25 | "BCH": "https://tinyurl.com/yas7972g", 26 | "ETH": "https://tinyurl.com/ya3fkha4", 27 | "LTC": "https://tinyurl.com/y8n7ohfh", 28 | "XMR": "https://tinyurl.com/y98ygfuw", 29 | "XRP": "https://tinyurl.com/ya4wcy3h", 30 | "XLM": "https://tinyurl.com/y8aulzp7" 31 | }, 32 | "log_level": 0, 33 | "log_to_file": false, 34 | "retries": 2, 35 | "single_price": true, 36 | "single_chart": true, 37 | "single_order": true, 38 | "decimals": 6, 39 | "webhook_enabled": false, 40 | "webhook_listen": "0.0.0.0", 41 | "webhook_port": 8443, 42 | "webhook_key": "path_to_privkey.pem", 43 | "webhook_cert": "path_to_cert.pem", 44 | "webhook_url": "HTTPS_URL:PORT/TOKEN" 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | .idea/ 99 | .DS_Store 100 | 101 | # Telegarm-Kraken-Bot related 102 | config.json 103 | kraken.key 104 | LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Kraken Bot 2 | Python 3 bot to trade on [Kraken](https://www.kraken.com) via [Telegram messenger](https://telegram.org) 3 | 4 |

5 | Demo GIF of bot 6 |

7 | 8 | ## Overview 9 | This Python script is a polling (not [webhook](https://github.com/python-telegram-bot/python-telegram-bot/wiki/Webhooks)) based Telegram bot. It can trade crypto-currencies on the [Kraken](http://kraken.com) marketplace and has a user friendly interface (custom keyboards with buttons). 10 | 11 | ### Features 12 | - Bound to a specific Telegram user - only that user can use the bot 13 | - No need to login to Kraken - start trading immediately, always 14 | - Integrated update mechanism - get latest version from GitHub 15 | - Notifies you once order is closed and trade successfully executed 16 | - Fully usable with buttons - no need to enter commands manually 17 | - Supports all currencies available on Kraken (configurable) 18 | - Change bot settings via bot 19 | - Following Kraken functionality is implemented 20 | - Create a buy / sell order (type _limit_ or _market_) 21 | - Lookup last trade price for currencies 22 | - Show all your assets 23 | - Current market value of assets (one or all) 24 | - Show / close open orders 25 | - Sell all assets for current market price 26 | - Deposit & withdraw 27 | - Show real-time charts 28 | - List history of closed orders 29 | - Check state of Kraken API 30 | 31 | ## Files 32 | In the following list you will find detailed information about all the files that the project consists of - and if they are necessary to run the bot or not. 33 | 34 | - __.gitignore__: Only relevant if you use [git](https://git-scm.com) as your Source Code Management. If you put a filename in that file, then that file will not be commited to the repository. If you don't intend to code yourself, the file is _not needed_. 35 | - __\_config.yml__: Automatically genereated file from GitHub that holds the theme-name for the [project page](https://endogen.github.io/Telegram-Kraken-Bot). This file is _not needed_. 36 | - __config.json__: The configuration file for this bot. This file is _needed_. 37 | - __demo.gif__: Animated image for GitHub `README.md` to demonstrate how the bot looks and behaves. This file is _not needed_. 38 | - __kraken.key__: The content of this file has to remain secret! _Do not tell anybody anything about the content_. The file consists of two lines. First line: API key. Second line: API secret (you get both from Kraken). This file is _needed_. 39 | - __Procfile__: This file is only necessary if you want to host the bot on [Heroku](https://www.heroku.com). Otherwise, this file is _not needed_. 40 | - __README.md__: The readme file you are reading right now. Includes instructions on how to run and use the bot. The file is _not needed_. 41 | - __requirements.txt__: This file holds all dependencies (Python modules) that are required to run the bot. Once all dependencies are installed, the file is _not needed_ anymore. If you need to know how to install the dependencies from this file, take a look at the [dependencies](#dependencies) section. 42 | - __telegram\_python\_bot.py__: The bot itself. This file has to be executed with Python to run. For more details, see the [installation](#installation) section. This file is _needed_. 43 | 44 | #### Summary 45 | These are the files that are important to run the bot: 46 | 47 | - `kraken.key` (API Secret) 48 | - `config.json` (Configuration) 49 | - `telegram_kraken_bot.py` (Bot itself) 50 | 51 | ## Configuration 52 | Before starting up the bot you have to take care of some settings. You need to edit two files: 53 | 54 | ### config.json 55 | This file holds the configuration for your bot. You have to at least edit the values for __user_id__ and __bot_token__. After a value has been changed you have to restart the bot for the applied changes to take effect. 56 | 57 | - __user_id__: Your Telegram user ID. The bot will only reply to messages from this user. If you don't know your user ID, send a message to Telegram bot `userinfobot` and he will reply your ID (use the ID, not the username) 58 | - __bot_token__: The token that identifies your bot. You will get this from Telegram bot `BotFather` when you create your bot. If you don't know how to register your bot, follow these [instructions](https://core.telegram.org/bots#3-how-do-i-create-a-bot) 59 | - __base_currency__: Command `/value` will use the base currency and show you the current value in this currency. If you want to get the value of all your assets, this only works if all your assets can be traded to this currency. You can enter here any asset: `EUR`, `USD`, `XBT`, `ETH`, ... 60 | - __check_trade__: Time in seconds to check for order status changes. Value `0` disables the check. Every order (already existing or newly created - not only by this bot) will be monitored by a background job and if the status changes to `closed` (which means that a trade was successfully executed) you will be notified by a message. 61 | - __update_url__: URL to the latest GitHub version of the script. This is needed for the update functionality. Per default this points to my repository and if you don't have your own repo with some changes then you should use the default value 62 | - __update_hash__: Hash of the latest version of the script. __Please don't change this__. Will be set automatically after updating. There is not need to play around with this 63 | - __update_check__: Time in seconds to check for bot-updates. Value `0` disables the check. If there is a bot-update available you will be notified by a message 64 | - __send_error__: If `true`, then all errors that happen will trigger a message to the user. If `false`, only the important errors will be send and timeout errors of background jobs will not be send 65 | - __show\_access\_denied__: If `true`, the owner of the bot and any other user who tries to access the bot will both be notified. If `false`, no one will be notified. Set to `false` if you get spammed with `Access denied` messages from people that try to use your bot 66 | - __used_pairs__: List of pairs to use with the bot. You can choose from all available pairs at Kraken: `"XBT": "EUR"`, `"ETH": "EUR"`, `"XLM": "XBT"`, ... 67 | - __coin_charts__: Dictionary of all available currencies with their corresponding chart URLs. Feel free to add new ones or change the ones that are pre-configured if you like to use other charts 68 | - __log_level__: Value has to be an __integer__. Choose the log-level depending on this: `0` = Disabled, `10` = DEBUG, `20` = INFO, `30` = WARNING, `40` = ERROR, `50` = CRITICAL 69 | - __log\_to\_file__: Debug-output that usually goes to the console will be saved in folder `log` in a log-file. Only enable this if you're searching for a bug because the logfiles can get pretty big. 70 | - __history_items__: Number of executed trades to display simultaneously 71 | - __retries__: If bigger then `0`, then Kraken API calls will be retried the specified number of times if they return any kind of error. In most cases this is very helpful since at the second or third time the request will most likely make it through 72 | - __single_price__: If `true`, no need to choose a coin in `/price` command. Only one message will be send with current prices for all coins that are configured in setting `used_pairs` 73 | - __single_chart__: If `true`, no need to choose a coin in `/chart` command. Only one message will be send with links to all coins that are configured in setting `used_pairs` 74 | - __decimals__: Number of decimal places that will be displayed. If you don't want to see small amounts in `/balance`, set this to `6` or smaller. If you use `8`, which is the maximum value and the one that Kraken uses internally, and you experience errors (while buying with volume `ALL` you could get an `Insufficient funds` error) set it to `7` or smaller 75 | - __webhook_enabled__: _Not used yet_ 76 | - __webhook_listen__: _Not used yet_ 77 | - __webhook_port__: _Not used yet_ 78 | - __webhook_key__: _Not used yet_ 79 | - __webhook_cert__: _Not used yet_ 80 | - __webhook_url__: _Not used yet_ 81 | 82 | ### kraken.key 83 | This file holds two keys that are necessary in order to communicate with Kraken. Both keys have to be considered __secret__ and you should be the only one that knows them. 84 | 85 | 86 | If you don't know where to get or how to generate the keys: 87 | 88 | 1. Login to [Kraken](https://www.kraken.com) 89 | 2. Click on `Settings` 90 | 3. Click on `API` 91 | 4. Click on `Generate New Key` 92 | 5. Enter `Telegram-Kraken-Bot` in `Key Description` 93 | 6. Enter `4` in `Nonce Window` (or just use the default value) 94 | 7. Select all available permissions at `Key Permissions` 95 | 8. Click on `Generate Key` 96 | 97 | When you have your Kraken API keys, open the file `kraken.key` and replace `some_api_key` (first line) with the value of `API Key` and `some_private_key` (second line) with the value of `Private Key`. 98 | 99 | 100 | ## Installation 101 | In order to run the bot you need to execute the script `telegram_kraken_bot.py`. If you don't have any idea where to host it, take a look at [Where to host Telegram Bots](https://github.com/python-telegram-bot/python-telegram-bot/wiki/Where-to-host-Telegram-Bots). __Since you have to provide sensitive data (Kraken API keys) to use the bot, i would only host this script on a server that you own__. But services like [Heroku](https://www.heroku.com) should be save too. You can also run the script locally on your computer for testing purposes. 102 | 103 | ### Prerequisites 104 | ##### Python version 105 | You have to use __Python 3.6__ to execute the script (because of enum method `auto()`). If you would like to use Python 3.4 or 3.5, you have to remove `auto` from imports and set the values in `WorkflowEnum` and `KeyboardEnum` yourself. Python 2.x is __not__ supported. 106 | 107 | 108 | ##### Installing needed modules from `requirements.txt` 109 | Install a set of module-versions that is known to work together for sure (__highly recommended__): 110 | ```shell 111 | pip3.6 install -r requirements.txt 112 | ``` 113 | 114 | ##### Install newest versions of needed modules 115 | If you want to install the newest versions of the needed modules, execute the following: 116 | ```shell 117 | pip3.6 install python-telegram-bot -U 118 | pip3.6 install beautifulsoup4 -U 119 | pip3.6 install krakenex -U 120 | ``` 121 | 122 | ### Starting 123 | To start the script, execute 124 | ```shell 125 | python3.6 telegram_kraken_bot.py & 126 | ``` 127 | 128 | ### Stopping 129 | To stop the script, execute 130 | ```shell 131 | pkill python 132 | ``` 133 | 134 | which will kill __every__ Python process that is currently running. Or shut the bot down with the `/shutdown` command (__recommended__). 135 | 136 | ## Usage 137 | If you configured the bot correctly and execute the script, you should see some checks that the bot performs. After that a welcome message will be shown along with the information if you are using the latest version. There should also be a custom keyboard that shows you all the available commands. Click on a button to execute the command or type the command in manually (not case sensitive). 138 | 139 | :warning: In general, while entering the volume, make sure that you don't use smaller values then Kraken supports. Take a look at the [order limits for various coins](https://support.kraken.com/hc/en-us/articles/205893708-What-is-the-minimum-order-size-). If you do use smaller values, the bot will tell you it's not possible to use that value and will let you enter the volume again. 140 | 141 | ### Available commands 142 | ##### Related to Kraken 143 | - `/trade`: Create a new buy or sell order of type `limit` or `market` 144 | - `/orders`: Show all open orders (buy and sell) and close a specific one or all 145 | - `/balance`: Show all assets with the available volume (if open orders exist) 146 | - `/price`: Return last trade price for the selected crypto-currency 147 | - `/value`: Show current market value of chosen currency or all your assets 148 | - `/chart`: Show a trading chart for the chosen currency 149 | - `/history`: Show history of closed (executed) trades 150 | - `/funding`: Deposit or withdraw (only to wallet, not SEPA) funds 151 | - `/state`: Show performance state of Kraken API 152 | 153 | ##### Related to bot 154 | - `/update`: Update the bot to the latest version on GitHub 155 | - `/restart`: Restart the bot 156 | - `/shutdown`: Shutdown the bot 157 | - `/settings`: Show and change bot settings 158 | - `/reload`: Reload custom command keyboard 159 | - `/initialize`: Perform initialization (precondition for start) 160 | 161 | If you want to show a list of available commands as you type, open a chat with Telegram user `BotFather` and send the command `/setcommands`. Then choose the bot you want to activate the list for and after that send the list of commands with description. Something like this: 162 | ``` 163 | trade - buy or sell assets 164 | orders - show or close orders 165 | balance - show all your assets 166 | price - show current price for asset 167 | value - calculate value for assets 168 | chart - display trading charts 169 | history - show completed trades 170 | funding - deposit or withdraw currencies 171 | bot - update, restart or shutdown 172 | ``` 173 | 174 | ## Development 175 | I know that it is unusual to have the whole source code in just one file. At some point i should have been switching to object orientation and multiple files but i kind of like the idea to have it all in just one file and object orientation would only blow up the code. This also makes the `/update` command much simpler :) 176 | 177 | ### Todo 178 | ##### Priority 1 179 | - [x] Add command `/history` that shows executed trades 180 | - [x] Add command `/chart` to show TradingView Chart Widget website 181 | - [x] Add command `/funding` to deposit / withdraw funds 182 | - [ ] Add command `/alert` to be notified once a specified price is reached 183 | - [x] Enable to trade every currency that Kraken supports 184 | - [x] Add possibility to change settings via bot 185 | - [x] Sanity check on start for configuration file 186 | - [x] Add possibility to sell __all__ assets immediately to current market value 187 | - [x] Per asset: Sell to current market price 188 | 189 | ##### Priority 2 190 | - [x] Optimize code to call Kraken API less often 191 | - [x] Automatically check for updates (with configurable timespan) 192 | - [ ] Create webhook-version of this bot 193 | - [x] Log to file (every day a new logfile) 194 | - [ ] Option: Only one open buy or sell order per asset 195 | - [ ] Periodically send current market price of a coin 196 | - [ ] Backup (settings & bot) on update 197 | - [ ] Show trends per asset in `/price` command 198 | - [ ] Point updates to GitHub releases with change logs 199 | - [ ] Option: Backup on update 200 | 201 | ##### Priority 3 202 | - [ ] Internationalisation 203 | - [ ] Add command `/stats` that shows statistics 204 | - [ ] Closed order notifications: Show gain / loss if association between trades possible 205 | 206 | ## Troubleshooting 207 | In case you experience any issues, please take a look at this section to check if it is described here. If not, create an [issue on GitHub](https://github.com/Endogen/Telegram-Kraken-Bot/issues/new). 208 | 209 | :warning: It depends on the error but it is possible that a request to Kraken will return with an error and still be executed correctly. 210 | 211 | :warning: If it happens that a specific command doesn't trigger any action (no response from the bot on button click), try to reload the keyboard with `/reload` or if that doesn't help, restart the bot with `/restart`. 212 | 213 | - __Error `Invalid nonce`__: It might happen that Kraken replies with this error. If you want to understand what a nonce is, read the [Wikipedia article](https://en.wikipedia.org/wiki/Cryptographic_nonce). This error happens mostly if you use different Telegram clients. Maybe you issued some commands on your laptop and then switched to your smartphone? That would be a typical scenario where this might happen. Or you didn't use the bot for a long time. To resolve it, just execute the command again. It should work the second time. Unfortunately there is not much i can do. The correct behavior would be to have one Kraken API key-pair for one device (one for your smartphone and one for your laptop). Unfortunately there is no way to identify the client. You can play around with the nonce value in your Kraken account (take a look at the [settings for the generated key-pair](#api-keys)). If you are really annoyed by this then here is what you could try: Create some key-pairs (5 might do it) and then, before you call the Kraken API, randomly choose one of the keys and use it till the next Kraken API call is made. 214 | - __Error `Service unavailable`__: If you get this error then because Kraken fucked up again. That happens regularly. It means that their API servers are not available or the performance is degraded because the load on the servers is too high. Nothing you can do here - try again later. If you want to have details on the API server performance, go to [Kraken Status](https://status.kraken.com) or execute the `/state` command. 215 | 216 | ## Disclaimer 217 | I use this bot personally to trade on Kraken so i guess it's kind of stable but __if you use it, then you are doing this on your own responsibility__ !!! I can not be made responsible for lost coins or other stuff that might happen due to some fuckup within the code. Use at your own risk! 218 | 219 | ## Donating 220 | If you find __Telegram-Kraken-Bot__ suitable for your needs or maybe even made some money because of it, please consider donating whatever amount you like to: 221 | 222 | #### Monero (XMR) 223 | ``` 224 | 46tUdg4LnqSKroZBR1hnQ2K6NnmPyrYjC8UBLhHiKYufCipQUaACfxcBeQUmYGFvqCdU3ghCpYq2o5Aqyj1nH6mfLVNka26 225 | ``` 226 | 227 | #### Ethereum (ETH) 228 | ``` 229 | 0xccb2fa97f47f0d58558d878f359013fef4097937 230 | ``` 231 | 232 | #### How else can you support me? 233 | If you can't or don't want to donate, please consider signing up on listed exchanges below. They are really good and by using these links to register an account i get a share of the trading-fee that you pay to the exchange if you execute a trade. 234 | 235 | - [Binance](https://www.binance.com/?ref=16770868) 236 | - [KuCoin](https://www.kucoin.com/#/?r=H3QdJJ) 237 | - [Qryptos](https://accounts.qryptos.com/sign-up?affiliate=wVZoZ4uG269520) 238 | -------------------------------------------------------------------------------- /telegram_kraken_bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # coding: utf-8 3 | 4 | import re 5 | import os 6 | import sys 7 | import json 8 | import time 9 | import inspect 10 | import logging 11 | import datetime 12 | import threading 13 | from enum import Enum, auto 14 | 15 | import requests 16 | import krakenex 17 | from bs4 import BeautifulSoup 18 | from telegram import KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, ParseMode 19 | from telegram.ext import Updater, CommandHandler, ConversationHandler, RegexHandler, MessageHandler 20 | from telegram.ext.filters import Filters 21 | 22 | 23 | # Emojis for messages 24 | e_err = "‼ " # Error 25 | e_wit = "⏳ " # Wait 26 | e_fns = "🏁 " # Finished 27 | e_ntf = "🔔 " # Notify 28 | e_bgn = "✨ " # Beginning 29 | e_cnc = "❌ " # Cancel 30 | e_top = "👍 " # Top 31 | e_dne = "✔ " # Done 32 | e_fld = "✖ " # Failed 33 | e_gby = "👋 " # Goodbye 34 | e_qst = "❓ " # Question 35 | 36 | # Check if file 'config.json' exists. Exit if not. 37 | if os.path.isfile("config.json"): 38 | # Read configuration 39 | with open("config.json") as config_file: 40 | config = json.load(config_file) 41 | else: 42 | exit("No configuration file 'config.json' found") 43 | 44 | # Set up logging 45 | 46 | # Formatter string for logging 47 | formatter_str = "%(asctime)s - %(levelname)s - %(name)s - %(message)s" 48 | date_format = "%y%m%d" 49 | 50 | # Folder name for logfiles 51 | log_dir = "log" 52 | 53 | # Do not use the logger directly. Use function 'log(msg, severity)' 54 | logging.basicConfig(level=config["log_level"], format=formatter_str) 55 | logger = logging.getLogger() 56 | 57 | # Current date for logging 58 | date = datetime.datetime.now().strftime(date_format) 59 | 60 | # Add a file handler to the logger if enabled 61 | if config["log_to_file"]: 62 | # If log directory doesn't exist, create it 63 | if not os.path.exists(log_dir): 64 | os.makedirs(log_dir) 65 | 66 | # Create a file handler for logging 67 | logfile_path = os.path.join(log_dir, date + ".log") 68 | handler = logging.FileHandler(logfile_path, encoding="utf-8") 69 | handler.setLevel(config["log_level"]) 70 | 71 | # Format file handler 72 | formatter = logging.Formatter(formatter_str) 73 | handler.setFormatter(formatter) 74 | 75 | # Add file handler to logger 76 | logger.addHandler(handler) 77 | 78 | # Redirect all uncaught exceptions to logfile 79 | sys.stderr = open(logfile_path, "w") 80 | 81 | # Set bot token, get dispatcher and job queue 82 | updater = Updater(token=config["bot_token"]) 83 | dispatcher = updater.dispatcher 84 | job_queue = updater.job_queue 85 | 86 | # Connect to Kraken 87 | kraken = krakenex.API() 88 | kraken.load_key("kraken.key") 89 | 90 | # Cached objects 91 | # All successfully executed trades 92 | trades = list() 93 | # All open orders 94 | orders = list() 95 | # All assets with internal long name & external short name 96 | assets = dict() 97 | # All assets from config with their trading pair 98 | pairs = dict() 99 | # Minimum order limits for assets 100 | limits = dict() 101 | 102 | 103 | # Enum for workflow handler 104 | class WorkflowEnum(Enum): 105 | TRADE_BUY_SELL = auto() 106 | TRADE_CURRENCY = auto() 107 | TRADE_SELL_ALL_CONFIRM = auto() 108 | TRADE_PRICE = auto() 109 | TRADE_VOL_TYPE = auto() 110 | TRADE_VOLUME = auto() 111 | TRADE_VOLUME_ASSET = auto() 112 | TRADE_CONFIRM = auto() 113 | ORDERS_CLOSE = auto() 114 | ORDERS_CLOSE_ORDER = auto() 115 | PRICE_CURRENCY = auto() 116 | VALUE_CURRENCY = auto() 117 | BOT_SUB_CMD = auto() 118 | CHART_CURRENCY = auto() 119 | TRADES_NEXT = auto() 120 | FUNDING_CURRENCY = auto() 121 | FUNDING_CHOOSE = auto() 122 | WITHDRAW_WALLET = auto() 123 | WITHDRAW_VOLUME = auto() 124 | WITHDRAW_CONFIRM = auto() 125 | SETTINGS_CHANGE = auto() 126 | SETTINGS_SAVE = auto() 127 | SETTINGS_CONFIRM = auto() 128 | 129 | 130 | # Enum for keyboard buttons 131 | class KeyboardEnum(Enum): 132 | BUY = auto() 133 | SELL = auto() 134 | VOLUME = auto() 135 | ALL = auto() 136 | YES = auto() 137 | NO = auto() 138 | CANCEL = auto() 139 | CLOSE_ORDER = auto() 140 | CLOSE_ALL = auto() 141 | UPDATE_CHECK = auto() 142 | UPDATE = auto() 143 | RESTART = auto() 144 | SHUTDOWN = auto() 145 | NEXT = auto() 146 | DEPOSIT = auto() 147 | WITHDRAW = auto() 148 | SETTINGS = auto() 149 | API_STATE = auto() 150 | MARKET_PRICE = auto() 151 | 152 | def clean(self): 153 | return self.name.replace("_", " ") 154 | 155 | 156 | # Log an event and save it in a file with current date as name if enabled 157 | def log(severity, msg): 158 | # Check if logging is enabled 159 | if config["log_level"] is 0: 160 | return 161 | 162 | # Add file handler to logger if enabled 163 | if config["log_to_file"]: 164 | now = datetime.datetime.now().strftime(date_format) 165 | 166 | # If current date not the same as initial one, create new FileHandler 167 | if str(now) != str(date): 168 | # Remove old handlers 169 | for hdlr in logger.handlers[:]: 170 | logger.removeHandler(hdlr) 171 | 172 | new_hdlr = logging.FileHandler(logfile_path, encoding="utf-8") 173 | new_hdlr.setLevel(config["log_level"]) 174 | 175 | # Format file handler 176 | new_hdlr.setFormatter(formatter) 177 | 178 | # Add file handler to logger 179 | logger.addHandler(new_hdlr) 180 | 181 | # The actual logging 182 | logger.log(severity, msg) 183 | 184 | 185 | # Issue Kraken API requests 186 | def kraken_api(method, data=None, private=False, retries=None): 187 | # Get arguments of this function 188 | frame = inspect.currentframe() 189 | args, _, _, values = inspect.getargvalues(frame) 190 | 191 | # Get name of caller function 192 | caller = inspect.currentframe().f_back.f_code.co_name 193 | 194 | # Log caller of this function and all arguments 195 | log(logging.DEBUG, caller + " - args: " + str([(i, values[i]) for i in args])) 196 | 197 | try: 198 | if private: 199 | return kraken.query_private(method, data) 200 | else: 201 | return kraken.query_public(method, data) 202 | 203 | except Exception as ex: 204 | log(logging.ERROR, str(ex)) 205 | 206 | ex_name = type(ex).__name__ 207 | 208 | # Handle the following exceptions immediately without retrying 209 | 210 | # Mostly this means that the API keys are not correct 211 | if "Incorrect padding" in str(ex): 212 | msg = "Incorrect padding: please verify that your Kraken API keys are valid" 213 | return {"error": [msg]} 214 | # No need to retry if the API service is not available right now 215 | elif "Service:Unavailable" in str(ex): 216 | msg = "Service: Unavailable" 217 | return {"error": [msg]} 218 | 219 | # Is retrying on error enabled? 220 | if config["retries"] > 0: 221 | # It's the first call, start retrying 222 | if retries is None: 223 | retries = config["retries"] 224 | return kraken_api(method, data, private, retries) 225 | # If 'retries' is bigger then 0, decrement it and retry again 226 | elif retries > 0: 227 | retries -= 1 228 | return kraken_api(method, data, private, retries) 229 | # Return error from last Kraken request 230 | else: 231 | return {"error": [ex_name + ":" + str(ex)]} 232 | # Retrying on error not enabled, return error from last Kraken request 233 | else: 234 | return {"error": [ex_name + ":" + str(ex)]} 235 | 236 | 237 | # Decorator to restrict access if user is not the same as in config 238 | def restrict_access(func): 239 | def _restrict_access(bot, update): 240 | chat_id = get_chat_id(update) 241 | if str(chat_id) != config["user_id"]: 242 | if config["show_access_denied"]: 243 | # Inform user who tried to access 244 | bot.send_message(chat_id, text="Access denied") 245 | 246 | # Inform owner of bot 247 | msg = "Access denied for user %s" % chat_id 248 | bot.send_message(config["user_id"], text=msg) 249 | 250 | log(logging.WARNING, msg) 251 | return 252 | else: 253 | return func(bot, update) 254 | return _restrict_access 255 | 256 | 257 | # Get balance of all currencies 258 | @restrict_access 259 | def balance_cmd(bot, update): 260 | update.message.reply_text(e_wit + "Retrieving balance...") 261 | 262 | # Send request to Kraken to get current balance of all currencies 263 | res_balance = kraken_api("Balance", private=True) 264 | 265 | # If Kraken replied with an error, show it 266 | if handle_api_error(res_balance, update): 267 | return 268 | 269 | # Send request to Kraken to get open orders 270 | res_orders = kraken_api("OpenOrders", private=True) 271 | 272 | # If Kraken replied with an error, show it 273 | if handle_api_error(res_orders, update): 274 | return 275 | 276 | msg = str() 277 | 278 | # Go over all currencies in your balance 279 | for currency_key, currency_value in res_balance["result"].items(): 280 | available_value = currency_value 281 | 282 | # Go through all open orders and check if an order exists for the currency 283 | if res_orders["result"]["open"]: 284 | for order in res_orders["result"]["open"]: 285 | order_desc = res_orders["result"]["open"][order]["descr"]["order"] 286 | order_desc_list = order_desc.split(" ") 287 | 288 | order_type = order_desc_list[0] 289 | order_volume = order_desc_list[1] 290 | price_per_coin = order_desc_list[5] 291 | 292 | # Check if asset is fiat-currency (EUR, USD, ...) and BUY order 293 | if currency_key.startswith("Z") and order_type == "buy": 294 | available_value = float(available_value) - (float(order_volume) * float(price_per_coin)) 295 | 296 | # Current asset is a coin and not a fiat currency 297 | else: 298 | for asset, data in assets.items(): 299 | if order_desc_list[2].endswith(data["altname"]): 300 | order_currency = order_desc_list[2][:-len(data["altname"])] 301 | break 302 | 303 | # Reduce current volume for coin if open sell-order exists 304 | if assets[currency_key]["altname"] == order_currency and order_type == "sell": 305 | available_value = float(available_value) - float(order_volume) 306 | 307 | # Only show assets with volume > 0 308 | if trim_zeros(currency_value) is not "0": 309 | msg += bold(assets[currency_key]["altname"] + ": " + trim_zeros(currency_value) + "\n") 310 | 311 | available_value = trim_zeros(float(available_value)) 312 | currency_value = trim_zeros(float(currency_value)) 313 | 314 | # If orders exist for this asset, show available volume too 315 | if currency_value == available_value: 316 | msg += "(Available: all)\n" 317 | else: 318 | msg += "(Available: " + available_value + ")\n" 319 | 320 | update.message.reply_text(msg, parse_mode=ParseMode.MARKDOWN) 321 | 322 | 323 | # Create orders to buy or sell currencies with price limit - choose 'buy' or 'sell' 324 | @restrict_access 325 | def trade_cmd(bot, update): 326 | reply_msg = "Buy or sell?" 327 | 328 | buttons = [ 329 | KeyboardButton(KeyboardEnum.BUY.clean()), 330 | KeyboardButton(KeyboardEnum.SELL.clean()) 331 | ] 332 | 333 | cancel_btn = [KeyboardButton(KeyboardEnum.CANCEL.clean())] 334 | 335 | menu = build_menu(buttons, n_cols=2, footer_buttons=cancel_btn) 336 | reply_mrk = ReplyKeyboardMarkup(menu, resize_keyboard=True) 337 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 338 | 339 | return WorkflowEnum.TRADE_BUY_SELL 340 | 341 | 342 | # Save if BUY or SELL order and choose the currency to trade 343 | def trade_buy_sell(bot, update, chat_data): 344 | # Clear data in case command is executed again without properly exiting first 345 | clear_chat_data(chat_data) 346 | 347 | chat_data["buysell"] = update.message.text.lower() 348 | 349 | reply_msg = "Choose currency" 350 | 351 | cancel_btn = [KeyboardButton(KeyboardEnum.CANCEL.clean())] 352 | 353 | # If SELL chosen, then include button 'ALL' to sell everything 354 | if chat_data["buysell"].upper() == KeyboardEnum.SELL.clean(): 355 | cancel_btn.insert(0, KeyboardButton(KeyboardEnum.ALL.clean())) 356 | 357 | menu = build_menu(coin_buttons(), n_cols=3, footer_buttons=cancel_btn) 358 | reply_mrk = ReplyKeyboardMarkup(menu, resize_keyboard=True) 359 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 360 | 361 | return WorkflowEnum.TRADE_CURRENCY 362 | 363 | 364 | # Show confirmation to sell all assets 365 | def trade_sell_all(bot, update): 366 | msg = e_qst + "Sell " + bold("all") + " assets to current market price? All open orders will be closed!" 367 | update.message.reply_text(msg, reply_markup=keyboard_confirm(), parse_mode=ParseMode.MARKDOWN) 368 | 369 | return WorkflowEnum.TRADE_SELL_ALL_CONFIRM 370 | 371 | 372 | # Sells all assets for there respective current market value 373 | def trade_sell_all_confirm(bot, update): 374 | if update.message.text.upper() == KeyboardEnum.NO.clean(): 375 | return cancel(bot, update) 376 | 377 | update.message.reply_text(e_wit + "Preparing to sell everything...") 378 | 379 | # Send request for open orders to Kraken 380 | res_open_orders = kraken_api("OpenOrders", private=True) 381 | 382 | # If Kraken replied with an error, show it 383 | if handle_api_error(res_open_orders, update): 384 | return 385 | 386 | # Close all currently open orders 387 | if res_open_orders["result"]["open"]: 388 | for order in res_open_orders["result"]["open"]: 389 | req_data = dict() 390 | req_data["txid"] = order 391 | 392 | # Send request to Kraken to cancel orders 393 | res_open_orders = kraken_api("CancelOrder", data=req_data, private=True) 394 | 395 | # If Kraken replied with an error, show it 396 | if handle_api_error(res_open_orders, update, "Not possible to close order\n" + order + "\n"): 397 | return 398 | 399 | # Send request to Kraken to get current balance of all assets 400 | res_balance = kraken_api("Balance", private=True) 401 | 402 | # If Kraken replied with an error, show it 403 | if handle_api_error(res_balance, update): 404 | return 405 | 406 | # Go over all assets and sell them 407 | for balance_asset, amount in res_balance["result"].items(): 408 | # Asset is fiat-currency and not crypto-currency - skip it 409 | if balance_asset.startswith("Z"): 410 | continue 411 | 412 | # Filter out 0 volume currencies 413 | if amount == "0.0000000000": 414 | continue 415 | 416 | # Get clean asset name 417 | balance_asset = assets[balance_asset]["altname"] 418 | 419 | # Make sure that the order size is at least the minimum order limit 420 | if balance_asset in limits: 421 | if float(amount) < float(limits[balance_asset]): 422 | msg_error = e_err + "Volume to low. Must be > " + limits[balance_asset] 423 | msg_next = "Selling next asset..." 424 | 425 | update.message.reply_text(msg_error + "\n" + msg_next) 426 | log(logging.WARNING, msg_error) 427 | continue 428 | else: 429 | log(logging.WARNING, "No minimum order limit in config for coin " + balance_asset) 430 | continue 431 | 432 | req_data = dict() 433 | req_data["type"] = "sell" 434 | req_data["trading_agreement"] = "agree" 435 | req_data["pair"] = pairs[balance_asset] 436 | req_data["ordertype"] = "market" 437 | req_data["volume"] = amount 438 | 439 | # Send request to create order to Kraken 440 | res_add_order = kraken_api("AddOrder", data=req_data, private=True) 441 | 442 | # If Kraken replied with an error, show it 443 | if handle_api_error(res_add_order, update): 444 | continue 445 | 446 | msg = e_fns + "Created orders to sell all assets" 447 | update.message.reply_text(bold(msg), reply_markup=keyboard_cmds(), parse_mode=ParseMode.MARKDOWN) 448 | 449 | return ConversationHandler.END 450 | 451 | 452 | # Save currency to trade and enter price per unit to trade 453 | def trade_currency(bot, update, chat_data): 454 | chat_data["currency"] = update.message.text.upper() 455 | 456 | asset_one, asset_two = assets_in_pair(pairs[chat_data["currency"]]) 457 | chat_data["one"] = asset_one 458 | chat_data["two"] = asset_two 459 | 460 | button = [KeyboardButton(KeyboardEnum.MARKET_PRICE.clean())] 461 | cancel_btn = [KeyboardButton(KeyboardEnum.CANCEL.clean())] 462 | reply_mrk = ReplyKeyboardMarkup(build_menu(button, footer_buttons=cancel_btn), resize_keyboard=True) 463 | 464 | reply_msg = "Enter price per coin in " + bold(assets[chat_data["two"]]["altname"]) 465 | update.message.reply_text(reply_msg, reply_markup=reply_mrk, parse_mode=ParseMode.MARKDOWN) 466 | return WorkflowEnum.TRADE_PRICE 467 | 468 | 469 | # Save price per unit and choose how to enter the 470 | # trade volume (fiat currency, volume or all available funds) 471 | def trade_price(bot, update, chat_data): 472 | # Check if key 'market_price' already exists. Yes means that we 473 | # already saved the values and we only need to enter the volume again 474 | if "market_price" not in chat_data: 475 | if update.message.text.upper() == KeyboardEnum.MARKET_PRICE.clean(): 476 | chat_data["market_price"] = True 477 | else: 478 | chat_data["market_price"] = False 479 | chat_data["price"] = update.message.text.upper().replace(",", ".") 480 | 481 | reply_msg = "How to enter the volume?" 482 | 483 | # If price is 'MARKET PRICE' and it's a buy-order, don't show options 484 | # how to enter volume since there is only one way to do it 485 | if chat_data["market_price"] and chat_data["buysell"] == "buy": 486 | cancel_btn = build_menu([KeyboardButton(KeyboardEnum.CANCEL.clean())]) 487 | reply_mrk = ReplyKeyboardMarkup(cancel_btn, resize_keyboard=True) 488 | update.message.reply_text("Enter volume", reply_markup=reply_mrk) 489 | chat_data["vol_type"] = KeyboardEnum.VOLUME.clean() 490 | return WorkflowEnum.TRADE_VOLUME 491 | 492 | elif chat_data["market_price"] and chat_data["buysell"] == "sell": 493 | buttons = [ 494 | KeyboardButton(KeyboardEnum.ALL.clean()), 495 | KeyboardButton(KeyboardEnum.VOLUME.clean()) 496 | ] 497 | cancel_btn = [KeyboardButton(KeyboardEnum.CANCEL.clean())] 498 | cancel_btn = build_menu(buttons, n_cols=2, footer_buttons=cancel_btn) 499 | reply_mrk = ReplyKeyboardMarkup(cancel_btn, resize_keyboard=True) 500 | 501 | else: 502 | buttons = [ 503 | KeyboardButton(assets[chat_data["two"]]["altname"]), 504 | KeyboardButton(KeyboardEnum.VOLUME.clean()), 505 | KeyboardButton(KeyboardEnum.ALL.clean()) 506 | ] 507 | cancel_btn = [KeyboardButton(KeyboardEnum.CANCEL.clean())] 508 | cancel_btn = build_menu(buttons, n_cols=3, footer_buttons=cancel_btn) 509 | reply_mrk = ReplyKeyboardMarkup(cancel_btn, resize_keyboard=True) 510 | 511 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 512 | return WorkflowEnum.TRADE_VOL_TYPE 513 | 514 | 515 | # Save volume type decision and enter volume 516 | def trade_vol_asset(bot, update, chat_data): 517 | # Check if correct currency entered 518 | if chat_data["two"].endswith(update.message.text.upper()): 519 | chat_data["vol_type"] = update.message.text.upper() 520 | else: 521 | update.message.reply_text(e_err + "Entered volume type not valid") 522 | return WorkflowEnum.TRADE_VOL_TYPE 523 | 524 | reply_msg = "Enter volume in " + bold(chat_data["vol_type"]) 525 | 526 | cancel_btn = build_menu([KeyboardButton(KeyboardEnum.CANCEL.clean())]) 527 | reply_mrk = ReplyKeyboardMarkup(cancel_btn, resize_keyboard=True) 528 | update.message.reply_text(reply_msg, reply_markup=reply_mrk, parse_mode=ParseMode.MARKDOWN) 529 | 530 | return WorkflowEnum.TRADE_VOLUME_ASSET 531 | 532 | 533 | # Volume type 'VOLUME' chosen - meaning that 534 | # you can enter the volume directly 535 | def trade_vol_volume(bot, update, chat_data): 536 | chat_data["vol_type"] = update.message.text.upper() 537 | 538 | reply_msg = "Enter volume" 539 | 540 | cancel_btn = build_menu([KeyboardButton(KeyboardEnum.CANCEL.clean())]) 541 | reply_mrk = ReplyKeyboardMarkup(cancel_btn, resize_keyboard=True) 542 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 543 | 544 | return WorkflowEnum.TRADE_VOLUME 545 | 546 | 547 | # Volume type 'ALL' chosen - meaning that 548 | # all available funds will be used 549 | def trade_vol_all(bot, update, chat_data): 550 | update.message.reply_text(e_wit + "Calculating volume...") 551 | 552 | # Send request to Kraken to get current balance of all currencies 553 | res_balance = kraken_api("Balance", private=True) 554 | 555 | # If Kraken replied with an error, show it 556 | if handle_api_error(res_balance, update): 557 | return 558 | 559 | # Send request to Kraken to get open orders 560 | res_orders = kraken_api("OpenOrders", private=True) 561 | 562 | # If Kraken replied with an error, show it 563 | if handle_api_error(res_orders, update): 564 | return 565 | 566 | # BUY ----------------- 567 | if chat_data["buysell"].upper() == KeyboardEnum.BUY.clean(): 568 | # Get amount of available currency to buy from 569 | avail_buy_from_cur = float(res_balance["result"][chat_data["two"]]) 570 | 571 | # Go through all open orders and check if buy-orders exist 572 | # If yes, subtract their value from the total of currency to buy from 573 | if res_orders["result"]["open"]: 574 | for order in res_orders["result"]["open"]: 575 | order_desc = res_orders["result"]["open"][order]["descr"]["order"] 576 | order_desc_list = order_desc.split(" ") 577 | coin_price = trim_zeros(order_desc_list[5]) 578 | order_volume = order_desc_list[1] 579 | order_type = order_desc_list[0] 580 | 581 | if order_type == "buy": 582 | avail_buy_from_cur = float(avail_buy_from_cur) - (float(order_volume) * float(coin_price)) 583 | 584 | # Calculate volume depending on available trade-to balance and round it to 8 digits 585 | chat_data["volume"] = trim_zeros(avail_buy_from_cur / float(chat_data["price"])) 586 | 587 | # If available volume is 0, return without creating an order 588 | if chat_data["volume"] == "0.00000000": 589 | msg = e_err + "Available " + assets[chat_data["two"]]["altname"] + " volume is 0" 590 | update.message.reply_text(msg, reply_markup=keyboard_cmds()) 591 | return ConversationHandler.END 592 | else: 593 | trade_show_conf(update, chat_data) 594 | 595 | # SELL ----------------- 596 | if chat_data["buysell"].upper() == KeyboardEnum.SELL.clean(): 597 | available_volume = res_balance["result"][chat_data["one"]] 598 | 599 | # Go through all open orders and check if sell-orders exists for the currency 600 | # If yes, subtract their volume from the available volume 601 | if res_orders["result"]["open"]: 602 | for order in res_orders["result"]["open"]: 603 | order_desc = res_orders["result"]["open"][order]["descr"]["order"] 604 | order_desc_list = order_desc.split(" ") 605 | 606 | # Get the currency of the order 607 | for asset, data in assets.items(): 608 | if order_desc_list[2].endswith(data["altname"]): 609 | order_currency = order_desc_list[2][:-len(data["altname"])] 610 | break 611 | 612 | order_volume = order_desc_list[1] 613 | order_type = order_desc_list[0] 614 | 615 | # Check if currency from oder is the same as currency to sell 616 | if chat_data["currency"] in order_currency: 617 | if order_type == "sell": 618 | available_volume = str(float(available_volume) - float(order_volume)) 619 | 620 | # Get volume from balance and round it to 8 digits 621 | chat_data["volume"] = trim_zeros(float(available_volume)) 622 | 623 | # If available volume is 0, return without creating an order 624 | if chat_data["volume"] == "0.00000000": 625 | msg = e_err + "Available " + chat_data["currency"] + " volume is 0" 626 | update.message.reply_text(msg, reply_markup=keyboard_cmds()) 627 | return ConversationHandler.END 628 | else: 629 | trade_show_conf(update, chat_data) 630 | 631 | return WorkflowEnum.TRADE_CONFIRM 632 | 633 | 634 | # Calculate the volume depending on entered volume type currency 635 | def trade_volume_asset(bot, update, chat_data): 636 | amount = float(update.message.text.replace(",", ".")) 637 | price_per_unit = float(chat_data["price"]) 638 | chat_data["volume"] = trim_zeros(amount / price_per_unit) 639 | 640 | # Make sure that the order size is at least the minimum order limit 641 | if chat_data["currency"] in limits: 642 | if float(chat_data["volume"]) < float(limits[chat_data["currency"]]): 643 | msg_error = e_err + "Volume to low. Must be > " + limits[chat_data["currency"]] 644 | update.message.reply_text(msg_error) 645 | log(logging.WARNING, msg_error) 646 | 647 | reply_msg = "Enter new volume" 648 | cancel_btn = build_menu([KeyboardButton(KeyboardEnum.CANCEL.clean())]) 649 | reply_mrk = ReplyKeyboardMarkup(cancel_btn, resize_keyboard=True) 650 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 651 | 652 | return WorkflowEnum.TRADE_VOLUME 653 | else: 654 | log(logging.WARNING, "No minimum order limit in config for coin " + chat_data["currency"]) 655 | 656 | trade_show_conf(update, chat_data) 657 | 658 | return WorkflowEnum.TRADE_CONFIRM 659 | 660 | 661 | # Calculate the volume depending on entered volume type 'VOLUME' 662 | def trade_volume(bot, update, chat_data): 663 | chat_data["volume"] = trim_zeros(float(update.message.text.replace(",", "."))) 664 | 665 | # Make sure that the order size is at least the minimum order limit 666 | if chat_data["currency"] in limits: 667 | if float(chat_data["volume"]) < float(limits[chat_data["currency"]]): 668 | msg_error = e_err + "Volume to low. Must be > " + limits[chat_data["currency"]] 669 | update.message.reply_text(msg_error) 670 | log(logging.WARNING, msg_error) 671 | 672 | reply_msg = "Enter new volume" 673 | cancel_btn = build_menu([KeyboardButton(KeyboardEnum.CANCEL.clean())]) 674 | reply_mrk = ReplyKeyboardMarkup(cancel_btn, resize_keyboard=True) 675 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 676 | 677 | return WorkflowEnum.TRADE_VOLUME 678 | else: 679 | log(logging.WARNING, "No minimum order limit in config for coin " + chat_data["currency"]) 680 | 681 | trade_show_conf(update, chat_data) 682 | 683 | return WorkflowEnum.TRADE_CONFIRM 684 | 685 | 686 | # Calculate total value and show order description and confirmation for order creation 687 | # This method is used in 'trade_volume' and in 'trade_vol_type_all' 688 | def trade_show_conf(update, chat_data): 689 | asset_two = assets[chat_data["two"]]["altname"] 690 | 691 | # Generate trade string to show at confirmation 692 | if chat_data["market_price"]: 693 | update.message.reply_text(e_wit + "Retrieving estimated price...") 694 | 695 | # Send request to Kraken to get current trading price for pair 696 | res_data = kraken_api("Ticker", data={"pair": pairs[chat_data["currency"]]}, private=False) 697 | 698 | # If Kraken replied with an error, show it 699 | if handle_api_error(res_data, update): 700 | return 701 | 702 | chat_data["price"] = res_data["result"][pairs[chat_data["currency"]]]["c"][0] 703 | 704 | chat_data["trade_str"] = (chat_data["buysell"].lower() + " " + 705 | trim_zeros(chat_data["volume"]) + " " + 706 | chat_data["currency"] + " @ market price ≈" + 707 | trim_zeros(chat_data["price"]) + " " + 708 | asset_two) 709 | 710 | else: 711 | chat_data["trade_str"] = (chat_data["buysell"].lower() + " " + 712 | trim_zeros(chat_data["volume"]) + " " + 713 | chat_data["currency"] + " @ limit " + 714 | trim_zeros(chat_data["price"]) + " " + 715 | asset_two) 716 | 717 | # If fiat currency, then show 2 digits after decimal place 718 | if chat_data["two"].startswith("Z"): 719 | # Calculate total value of order 720 | total_value = trim_zeros(float(chat_data["volume"]) * float(chat_data["price"]), 2) 721 | # Else, show 8 digits after decimal place 722 | else: 723 | # Calculate total value of order 724 | total_value = trim_zeros(float(chat_data["volume"]) * float(chat_data["price"])) 725 | 726 | if chat_data["market_price"]: 727 | total_value_str = "(Value: ≈" + str(trim_zeros(total_value)) + " " + asset_two + ")" 728 | else: 729 | total_value_str = "(Value: " + str(trim_zeros(total_value)) + " " + asset_two + ")" 730 | 731 | msg = e_qst + "Place this order?\n" + chat_data["trade_str"] + "\n" + total_value_str 732 | update.message.reply_text(msg, reply_markup=keyboard_confirm()) 733 | 734 | 735 | # The user has to confirm placing the order 736 | def trade_confirm(bot, update, chat_data): 737 | if update.message.text.upper() == KeyboardEnum.NO.clean(): 738 | return cancel(bot, update, chat_data=chat_data) 739 | 740 | update.message.reply_text(e_wit + "Placing order...") 741 | 742 | req_data = dict() 743 | req_data["type"] = chat_data["buysell"].lower() 744 | req_data["volume"] = chat_data["volume"] 745 | req_data["pair"] = pairs[chat_data["currency"]] 746 | 747 | # Order type MARKET 748 | if chat_data["market_price"]: 749 | req_data["ordertype"] = "market" 750 | req_data["trading_agreement"] = "agree" 751 | 752 | # Order type LIMIT 753 | else: 754 | req_data["ordertype"] = "limit" 755 | req_data["price"] = chat_data["price"] 756 | 757 | # Send request to create order to Kraken 758 | res_add_order = kraken_api("AddOrder", req_data, private=True) 759 | 760 | # If Kraken replied with an error, show it 761 | if handle_api_error(res_add_order, update): 762 | return 763 | 764 | # If there is a transaction ID then the order was placed successfully 765 | if res_add_order["result"]["txid"]: 766 | msg = e_fns + "Order placed:\n" + res_add_order["result"]["txid"][0] + "\n" + chat_data["trade_str"] 767 | update.message.reply_text(bold(msg), reply_markup=keyboard_cmds(), parse_mode=ParseMode.MARKDOWN) 768 | else: 769 | update.message.reply_text("Undefined state: no error and no TXID") 770 | 771 | clear_chat_data(chat_data) 772 | return ConversationHandler.END 773 | 774 | 775 | # Show and manage orders 776 | @restrict_access 777 | def orders_cmd(bot, update): 778 | update.message.reply_text(e_wit + "Retrieving orders...") 779 | 780 | # Send request to Kraken to get open orders 781 | res_data = kraken_api("OpenOrders", private=True) 782 | 783 | # If Kraken replied with an error, show it 784 | if handle_api_error(res_data, update): 785 | return 786 | 787 | # Reset global orders list 788 | global orders 789 | orders = list() 790 | 791 | # Go through all open orders and show them to the user 792 | if res_data["result"]["open"]: 793 | for order_id, order_details in res_data["result"]["open"].items(): 794 | # Add order to global order list so that it can be used later 795 | # without requesting data from Kraken again 796 | orders.append({order_id: order_details}) 797 | 798 | order = "Order: " + order_id 799 | order_desc = trim_zeros(order_details["descr"]["order"]) 800 | update.message.reply_text(bold(order + "\n" + order_desc), parse_mode=ParseMode.MARKDOWN) 801 | else: 802 | update.message.reply_text(e_fns + bold("No open orders"), parse_mode=ParseMode.MARKDOWN) 803 | return ConversationHandler.END 804 | 805 | reply_msg = "What do you want to do?" 806 | 807 | buttons = [ 808 | KeyboardButton(KeyboardEnum.CLOSE_ORDER.clean()), 809 | KeyboardButton(KeyboardEnum.CLOSE_ALL.clean()) 810 | ] 811 | 812 | close_btn = [ 813 | KeyboardButton(KeyboardEnum.CANCEL.clean()) 814 | ] 815 | 816 | menu = build_menu(buttons, n_cols=2, footer_buttons=close_btn) 817 | reply_mrk = ReplyKeyboardMarkup(menu, resize_keyboard=True) 818 | 819 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 820 | return WorkflowEnum.ORDERS_CLOSE 821 | 822 | 823 | # Choose what to do with the open orders 824 | def orders_choose_order(bot, update): 825 | buttons = list() 826 | 827 | # Go through all open orders and create a button 828 | if orders: 829 | for order in orders: 830 | order_id = next(iter(order), None) 831 | buttons.append(KeyboardButton(order_id)) 832 | else: 833 | update.message.reply_text("No open orders") 834 | return ConversationHandler.END 835 | 836 | msg = "Which order to close?" 837 | 838 | close_btn = [ 839 | KeyboardButton(KeyboardEnum.CANCEL.clean()) 840 | ] 841 | 842 | menu = build_menu(buttons, n_cols=1, footer_buttons=close_btn) 843 | reply_mrk = ReplyKeyboardMarkup(menu, resize_keyboard=True) 844 | 845 | update.message.reply_text(msg, reply_markup=reply_mrk) 846 | return WorkflowEnum.ORDERS_CLOSE_ORDER 847 | 848 | 849 | # Close all open orders 850 | def orders_close_all(bot, update): 851 | update.message.reply_text(e_wit + "Closing orders...") 852 | 853 | closed_orders = list() 854 | 855 | if orders: 856 | for x in range(0, len(orders)): 857 | order_id = next(iter(orders[x]), None) 858 | 859 | # Send request to Kraken to cancel orders 860 | res_data = kraken_api("CancelOrder", data={"txid": order_id}, private=True) 861 | 862 | # If Kraken replied with an error, show it 863 | if handle_api_error(res_data, update, "Order not closed:\n" + order_id + "\n"): 864 | # If we are currently not closing the last order, 865 | # show message that we a continuing with the next one 866 | if x+1 != len(orders): 867 | update.message.reply_text(e_wit + "Closing next order...") 868 | else: 869 | closed_orders.append(order_id) 870 | 871 | if closed_orders: 872 | msg = e_fns + bold("Orders closed:\n" + "\n".join(closed_orders)) 873 | update.message.reply_text(msg, reply_markup=keyboard_cmds(), parse_mode=ParseMode.MARKDOWN) 874 | else: 875 | msg = e_fns + bold("No orders closed") 876 | update.message.reply_text(msg, parse_mode=ParseMode.MARKDOWN) 877 | return 878 | else: 879 | msg = e_fns + bold("No open orders") 880 | update.message.reply_text(msg, reply_markup=keyboard_cmds(), parse_mode=ParseMode.MARKDOWN) 881 | 882 | return ConversationHandler.END 883 | 884 | 885 | # Close the specified order 886 | def orders_close_order(bot, update): 887 | update.message.reply_text(e_wit + "Closing order...") 888 | 889 | req_data = dict() 890 | req_data["txid"] = update.message.text 891 | 892 | # Send request to Kraken to cancel order 893 | res_data = kraken_api("CancelOrder", data=req_data, private=True) 894 | 895 | # If Kraken replied with an error, show it 896 | if handle_api_error(res_data, update): 897 | return 898 | 899 | msg = e_fns + bold("Order closed:\n" + req_data["txid"]) 900 | update.message.reply_text(msg, reply_markup=keyboard_cmds(), parse_mode=ParseMode.MARKDOWN) 901 | return ConversationHandler.END 902 | 903 | 904 | # Show the last trade price for a currency 905 | @restrict_access 906 | def price_cmd(bot, update): 907 | # If single-price option is active, get prices for all coins 908 | if config["single_price"]: 909 | update.message.reply_text(e_wit + "Retrieving prices...") 910 | 911 | req_data = dict() 912 | req_data["pair"] = str() 913 | 914 | # Add all configured asset pairs to the request 915 | for asset, trade_pair in pairs.items(): 916 | req_data["pair"] += trade_pair + "," 917 | 918 | # Get rid of last comma 919 | req_data["pair"] = req_data["pair"][:-1] 920 | 921 | # Send request to Kraken to get current trading price for currency-pair 922 | res_data = kraken_api("Ticker", data=req_data, private=False) 923 | 924 | # If Kraken replied with an error, show it 925 | if handle_api_error(res_data, update): 926 | return 927 | 928 | msg = str() 929 | 930 | for pair, data in res_data["result"].items(): 931 | last_trade_price = trim_zeros(data["c"][0]) 932 | coin = list(pairs.keys())[list(pairs.values()).index(pair)] 933 | msg += coin + ": " + last_trade_price + " " + config["used_pairs"][coin] + "\n" 934 | 935 | update.message.reply_text(bold(msg), parse_mode=ParseMode.MARKDOWN) 936 | 937 | return ConversationHandler.END 938 | 939 | # Let user choose for which coin to get the price 940 | else: 941 | reply_msg = "Choose currency" 942 | 943 | cancel_btn = [ 944 | KeyboardButton(KeyboardEnum.CANCEL.clean()) 945 | ] 946 | 947 | menu = build_menu(coin_buttons(), n_cols=3, footer_buttons=cancel_btn) 948 | reply_mrk = ReplyKeyboardMarkup(menu, resize_keyboard=True) 949 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 950 | 951 | return WorkflowEnum.PRICE_CURRENCY 952 | 953 | 954 | # Choose for which currency to show the last trade price 955 | def price_currency(bot, update): 956 | update.message.reply_text(e_wit + "Retrieving price...") 957 | 958 | currency = update.message.text.upper() 959 | req_data = {"pair": pairs[currency]} 960 | 961 | # Send request to Kraken to get current trading price for currency-pair 962 | res_data = kraken_api("Ticker", data=req_data, private=False) 963 | 964 | # If Kraken replied with an error, show it 965 | if handle_api_error(res_data, update): 966 | return 967 | 968 | last_trade_price = trim_zeros(res_data["result"][req_data["pair"]]["c"][0]) 969 | 970 | msg = bold(currency + ": " + last_trade_price + " " + config["used_pairs"][currency]) 971 | update.message.reply_text(msg, reply_markup=keyboard_cmds(), parse_mode=ParseMode.MARKDOWN) 972 | 973 | return ConversationHandler.END 974 | 975 | 976 | # Show the current real money value for a certain asset or for all assets combined 977 | @restrict_access 978 | def value_cmd(bot, update): 979 | reply_msg = "Choose currency" 980 | 981 | footer_btns = [ 982 | KeyboardButton(KeyboardEnum.ALL.clean()), 983 | KeyboardButton(KeyboardEnum.CANCEL.clean()) 984 | ] 985 | 986 | menu = build_menu(coin_buttons(), n_cols=3, footer_buttons=footer_btns) 987 | reply_mrk = ReplyKeyboardMarkup(menu, resize_keyboard=True) 988 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 989 | 990 | return WorkflowEnum.VALUE_CURRENCY 991 | 992 | 993 | # Choose for which currency you want to know the current value 994 | def value_currency(bot, update): 995 | update.message.reply_text(e_wit + "Retrieving current value...") 996 | 997 | # ALL COINS (balance of all coins) 998 | if update.message.text.upper() == KeyboardEnum.ALL.clean(): 999 | req_asset = dict() 1000 | req_asset["asset"] = config["base_currency"] 1001 | 1002 | # Send request to Kraken tp obtain the combined balance of all currencies 1003 | res_trade_balance = kraken_api("TradeBalance", data=req_asset, private=True) 1004 | 1005 | # If Kraken replied with an error, show it 1006 | if handle_api_error(res_trade_balance, update): 1007 | return 1008 | 1009 | for asset, data in assets.items(): 1010 | if data["altname"] == config["base_currency"]: 1011 | if asset.startswith("Z"): 1012 | # It's a fiat currency, show only 2 digits after decimal place 1013 | total_fiat_value = trim_zeros(float(res_trade_balance["result"]["eb"]), 2) 1014 | else: 1015 | # It's not a fiat currency, show 8 digits after decimal place 1016 | total_fiat_value = trim_zeros(float(res_trade_balance["result"]["eb"])) 1017 | 1018 | # Generate message to user 1019 | msg = e_fns + bold("Overall: " + total_fiat_value + " " + config["base_currency"]) 1020 | update.message.reply_text(msg, reply_markup=keyboard_cmds(), parse_mode=ParseMode.MARKDOWN) 1021 | 1022 | # ONE COINS (balance of specific coin) 1023 | else: 1024 | # Send request to Kraken to get balance of all currencies 1025 | res_balance = kraken_api("Balance", private=True) 1026 | 1027 | # If Kraken replied with an error, show it 1028 | if handle_api_error(res_balance, update): 1029 | return 1030 | 1031 | req_price = dict() 1032 | # Get pair string for chosen currency 1033 | req_price["pair"] = pairs[update.message.text.upper()] 1034 | 1035 | # Send request to Kraken to get current trading price for currency-pair 1036 | res_price = kraken_api("Ticker", data=req_price, private=False) 1037 | 1038 | # If Kraken replied with an error, show it 1039 | if handle_api_error(res_price, update): 1040 | return 1041 | 1042 | # Get last trade price 1043 | pair = list(res_price["result"].keys())[0] 1044 | last_price = res_price["result"][pair]["c"][0] 1045 | 1046 | value = float(0) 1047 | 1048 | for asset, data in assets.items(): 1049 | if data["altname"] == update.message.text.upper(): 1050 | buy_from_cur_long = pair.replace(asset, "") 1051 | buy_from_cur = assets[buy_from_cur_long]["altname"] 1052 | # Calculate value by multiplying balance with last trade price 1053 | value = float(res_balance["result"][asset]) * float(last_price) 1054 | break 1055 | 1056 | # If fiat currency, show 2 digits after decimal place 1057 | if buy_from_cur_long.startswith("Z"): 1058 | value = trim_zeros(value, 2) 1059 | last_trade_price = trim_zeros(float(last_price), 2) 1060 | # ... else show 8 digits after decimal place 1061 | else: 1062 | value = trim_zeros(value) 1063 | last_trade_price = trim_zeros(float(last_price)) 1064 | 1065 | msg = update.message.text.upper() + ": " + value + " " + buy_from_cur 1066 | 1067 | # Add last trade price to msg 1068 | msg += "\n(Ticker: " + last_trade_price + " " + buy_from_cur + ")" 1069 | update.message.reply_text(bold(msg), reply_markup=keyboard_cmds(), parse_mode=ParseMode.MARKDOWN) 1070 | 1071 | return ConversationHandler.END 1072 | 1073 | 1074 | # Reloads keyboard with available commands 1075 | @restrict_access 1076 | def reload_cmd(bot, update): 1077 | msg = e_wit + "Reloading keyboard..." 1078 | update.message.reply_text(msg, reply_markup=keyboard_cmds()) 1079 | return ConversationHandler.END 1080 | 1081 | 1082 | # Get current state of Kraken API 1083 | # Is it under maintenance or functional? 1084 | @restrict_access 1085 | def state_cmd(bot, update): 1086 | update.message.reply_text(e_wit + "Retrieving API state...") 1087 | 1088 | msg = "Kraken API Status: " + bold(api_state()) + "\nhttps://status.kraken.com" 1089 | updater.bot.send_message(config["user_id"], 1090 | msg, 1091 | reply_markup=keyboard_cmds(), 1092 | disable_web_page_preview=True, 1093 | parse_mode=ParseMode.MARKDOWN) 1094 | 1095 | return ConversationHandler.END 1096 | 1097 | 1098 | def start_cmd(bot, update): 1099 | msg = e_bgn + "Welcome to Kraken-Telegram-Bot!" 1100 | update.message.reply_text(msg, reply_markup=keyboard_cmds()) 1101 | 1102 | 1103 | # Returns a string representation of a trade. Looks like this: 1104 | # sell 0.03752345 ETH-EUR @ limit 267.5 on 2017-08-22 22:18:22 1105 | def get_trade_str(trade): 1106 | from_asset, to_asset = assets_in_pair(trade["pair"]) 1107 | 1108 | if from_asset and to_asset: 1109 | # Build string representation of trade with asset names 1110 | trade_str = (trade["type"] + " " + 1111 | trim_zeros(trade["vol"]) + " " + 1112 | assets[from_asset]["altname"] + " @ " + 1113 | trim_zeros(trade["price"]) + " " + 1114 | assets[to_asset]["altname"] + "\n" + 1115 | datetime_from_timestamp(trade["time"])) 1116 | else: 1117 | # Build string representation of trade with pair string 1118 | # We need this because who knows if the pair still exists 1119 | trade_str = (trade["type"] + " " + 1120 | trim_zeros(trade["vol"]) + " " + 1121 | trade["pair"] + " @ " + 1122 | trim_zeros(trade["price"]) + "\n" + 1123 | datetime_from_timestamp(trade["time"])) 1124 | 1125 | return trade_str 1126 | 1127 | 1128 | # Shows executed trades with volume and price 1129 | @restrict_access 1130 | def trades_cmd(bot, update): 1131 | update.message.reply_text(e_wit + "Retrieving executed trades...") 1132 | 1133 | # Send request to Kraken to get trades history 1134 | res_trades = kraken_api("TradesHistory", private=True) 1135 | 1136 | # If Kraken replied with an error, show it 1137 | if handle_api_error(res_trades, update): 1138 | return 1139 | 1140 | # Reset global trades list 1141 | global trades 1142 | trades = list() 1143 | 1144 | # Add all trades to global list 1145 | for trade_id, trade_details in res_trades["result"]["trades"].items(): 1146 | trades.append(trade_details) 1147 | 1148 | if trades: 1149 | # Sort global list with trades - on executed time 1150 | trades = sorted(trades, key=lambda k: k['time'], reverse=True) 1151 | 1152 | buttons = [ 1153 | KeyboardButton(KeyboardEnum.NEXT.clean()), 1154 | KeyboardButton(KeyboardEnum.CANCEL.clean()) 1155 | ] 1156 | 1157 | # Get number of first items in list (latest trades) 1158 | for items in range(config["history_items"]): 1159 | newest_trade = next(iter(trades), None) 1160 | 1161 | _, two = assets_in_pair(newest_trade["pair"]) 1162 | 1163 | # It's a fiat currency 1164 | if two.startswith("Z"): 1165 | total_value = trim_zeros(float(newest_trade["cost"]), 2) 1166 | # It's a digital currency 1167 | else: 1168 | total_value = trim_zeros(float(newest_trade["cost"])) 1169 | 1170 | reply_mrk = ReplyKeyboardMarkup(build_menu(buttons, n_cols=2), resize_keyboard=True) 1171 | msg = get_trade_str(newest_trade) + " (Value: " + total_value + " " + assets[two]["altname"] + ")" 1172 | update.message.reply_text(bold(msg), reply_markup=reply_mrk, parse_mode=ParseMode.MARKDOWN) 1173 | 1174 | # Remove the first item in the trades list 1175 | trades.remove(newest_trade) 1176 | 1177 | return WorkflowEnum.TRADES_NEXT 1178 | else: 1179 | update.message.reply_text("No item in trade history", reply_markup=keyboard_cmds()) 1180 | 1181 | return ConversationHandler.END 1182 | 1183 | 1184 | # TODO: Show fee 1185 | # Save if BUY, SELL or ALL trade history and choose how many entries to list 1186 | def trades_next(bot, update): 1187 | if trades: 1188 | # Get number of first items in list (latest trades) 1189 | for items in range(config["history_items"]): 1190 | newest_trade = next(iter(trades), None) 1191 | 1192 | one, two = assets_in_pair(newest_trade["pair"]) 1193 | 1194 | # It's a fiat currency 1195 | if two.startswith("Z"): 1196 | total_value = trim_zeros(float(newest_trade["cost"]), 2) 1197 | # It's a digital currency 1198 | else: 1199 | total_value = trim_zeros(float(newest_trade["cost"])) 1200 | 1201 | msg = get_trade_str(newest_trade) + " (Value: " + total_value + " " + assets[two]["altname"] + ")" 1202 | update.message.reply_text(bold(msg), parse_mode=ParseMode.MARKDOWN) 1203 | 1204 | # Remove the first item in the trades list 1205 | trades.remove(newest_trade) 1206 | 1207 | return WorkflowEnum.TRADES_NEXT 1208 | else: 1209 | msg = e_fns + bold("Trade history is empty") 1210 | update.message.reply_text(msg, reply_markup=keyboard_cmds(), parse_mode=ParseMode.MARKDOWN) 1211 | 1212 | return ConversationHandler.END 1213 | 1214 | 1215 | # Shows sub-commands to control the bot 1216 | @restrict_access 1217 | def bot_cmd(bot, update): 1218 | reply_msg = "What do you want to do?" 1219 | 1220 | buttons = [ 1221 | KeyboardButton(KeyboardEnum.UPDATE_CHECK.clean()), 1222 | KeyboardButton(KeyboardEnum.UPDATE.clean()), 1223 | KeyboardButton(KeyboardEnum.RESTART.clean()), 1224 | KeyboardButton(KeyboardEnum.SHUTDOWN.clean()), 1225 | KeyboardButton(KeyboardEnum.SETTINGS.clean()), 1226 | KeyboardButton(KeyboardEnum.API_STATE.clean()), 1227 | KeyboardButton(KeyboardEnum.CANCEL.clean()) 1228 | ] 1229 | 1230 | reply_mrk = ReplyKeyboardMarkup(build_menu(buttons, n_cols=2), resize_keyboard=True) 1231 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 1232 | 1233 | return WorkflowEnum.BOT_SUB_CMD 1234 | 1235 | 1236 | # Execute chosen sub-cmd of 'bot' cmd 1237 | def bot_sub_cmd(bot, update): 1238 | # Update check 1239 | if update.message.text.upper() == KeyboardEnum.UPDATE_CHECK.clean(): 1240 | status_code, msg = get_update_state() 1241 | update.message.reply_text(msg) 1242 | return 1243 | 1244 | # Update 1245 | elif update.message.text.upper() == KeyboardEnum.UPDATE.clean(): 1246 | return update_cmd(bot, update) 1247 | 1248 | # Restart 1249 | elif update.message.text.upper() == KeyboardEnum.RESTART.clean(): 1250 | restart_cmd(bot, update) 1251 | 1252 | # Shutdown 1253 | elif update.message.text.upper() == KeyboardEnum.SHUTDOWN.clean(): 1254 | shutdown_cmd(bot, update) 1255 | 1256 | # API State 1257 | elif update.message.text.upper() == KeyboardEnum.API_STATE.clean(): 1258 | state_cmd(bot, update) 1259 | 1260 | # Cancel 1261 | elif update.message.text.upper() == KeyboardEnum.CANCEL.clean(): 1262 | return cancel(bot, update) 1263 | 1264 | 1265 | # Show links to Kraken currency charts 1266 | @restrict_access 1267 | def chart_cmd(bot, update): 1268 | # Send only one message with all configured charts 1269 | if config["single_chart"]: 1270 | msg = str() 1271 | 1272 | for coin, url in config["coin_charts"].items(): 1273 | msg += coin + ": " + url + "\n" 1274 | 1275 | update.message.reply_text(msg, parse_mode=ParseMode.MARKDOWN, reply_markup=keyboard_cmds()) 1276 | 1277 | return ConversationHandler.END 1278 | 1279 | # Choose currency and display chart for it 1280 | else: 1281 | reply_msg = "Choose currency" 1282 | 1283 | buttons = list() 1284 | for coin, url in config["coin_charts"].items(): 1285 | buttons.append(KeyboardButton(coin)) 1286 | 1287 | cancel_btn = [ 1288 | KeyboardButton(KeyboardEnum.CANCEL.clean()) 1289 | ] 1290 | 1291 | menu = build_menu(buttons, n_cols=3, footer_buttons=cancel_btn) 1292 | reply_mrk = ReplyKeyboardMarkup(menu, resize_keyboard=True) 1293 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 1294 | 1295 | return WorkflowEnum.CHART_CURRENCY 1296 | 1297 | 1298 | # Get chart URL for every coin in config 1299 | def chart_currency(bot, update): 1300 | currency = update.message.text 1301 | 1302 | for coin, url in config["coin_charts"].items(): 1303 | if currency.upper() == coin.upper(): 1304 | update.message.reply_text(url, reply_markup=keyboard_cmds()) 1305 | break 1306 | 1307 | return ConversationHandler.END 1308 | 1309 | 1310 | # Choose currency to deposit or withdraw funds to / from 1311 | @restrict_access 1312 | def funding_cmd(bot, update): 1313 | reply_msg = "Choose currency" 1314 | 1315 | cancel_btn = [ 1316 | KeyboardButton(KeyboardEnum.CANCEL.clean()) 1317 | ] 1318 | 1319 | menu = build_menu(coin_buttons(), n_cols=3, footer_buttons=cancel_btn) 1320 | reply_mrk = ReplyKeyboardMarkup(menu, resize_keyboard=True) 1321 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 1322 | 1323 | return WorkflowEnum.FUNDING_CURRENCY 1324 | 1325 | 1326 | # Choose withdraw or deposit 1327 | def funding_currency(bot, update, chat_data): 1328 | # Clear data in case command is executed again without properly exiting first 1329 | clear_chat_data(chat_data) 1330 | 1331 | chat_data["currency"] = update.message.text.upper() 1332 | 1333 | reply_msg = "What do you want to do?" 1334 | 1335 | buttons = [ 1336 | KeyboardButton(KeyboardEnum.DEPOSIT.clean()), 1337 | KeyboardButton(KeyboardEnum.WITHDRAW.clean()) 1338 | ] 1339 | 1340 | cancel_btn = [ 1341 | KeyboardButton(KeyboardEnum.CANCEL.clean()) 1342 | ] 1343 | 1344 | menu = build_menu(buttons, n_cols=2, footer_buttons=cancel_btn) 1345 | reply_mrk = ReplyKeyboardMarkup(menu, resize_keyboard=True) 1346 | update.message.reply_text(reply_msg, reply_markup=reply_mrk) 1347 | 1348 | return WorkflowEnum.FUNDING_CHOOSE 1349 | 1350 | 1351 | # Get wallet addresses to deposit to 1352 | def funding_deposit(bot, update, chat_data): 1353 | update.message.reply_text(e_wit + "Retrieving wallets to deposit...") 1354 | 1355 | req_data = dict() 1356 | req_data["asset"] = chat_data["currency"] 1357 | 1358 | # Send request to Kraken to get trades history 1359 | res_dep_meth = kraken_api("DepositMethods", data=req_data, private=True) 1360 | 1361 | # If Kraken replied with an error, show it 1362 | if handle_api_error(res_dep_meth, update): 1363 | return 1364 | 1365 | req_data["method"] = res_dep_meth["result"][0]["method"] 1366 | 1367 | # Send request to Kraken to get trades history 1368 | res_dep_addr = kraken_api("DepositAddresses", data=req_data, private=True) 1369 | 1370 | # If Kraken replied with an error, show it 1371 | if handle_api_error(res_dep_addr, update): 1372 | return 1373 | 1374 | # Wallet found 1375 | if res_dep_addr["result"]: 1376 | for wallet in res_dep_addr["result"]: 1377 | expire_info = datetime_from_timestamp(wallet["expiretm"]) if wallet["expiretm"] != "0" else "No" 1378 | msg = wallet["address"] + "\nExpire: " + expire_info 1379 | update.message.reply_text(bold(msg), parse_mode=ParseMode.MARKDOWN, reply_markup=keyboard_cmds()) 1380 | # No wallet found 1381 | else: 1382 | update.message.reply_text("No wallet found", reply_markup=keyboard_cmds()) 1383 | 1384 | return ConversationHandler.END 1385 | 1386 | 1387 | def funding_withdraw(bot, update): 1388 | update.message.reply_text("Enter target wallet name", reply_markup=ReplyKeyboardRemove()) 1389 | 1390 | return WorkflowEnum.WITHDRAW_WALLET 1391 | 1392 | 1393 | def funding_withdraw_wallet(bot, update, chat_data): 1394 | chat_data["wallet"] = update.message.text 1395 | 1396 | update.message.reply_text("Enter " + chat_data["currency"] + " volume to withdraw") 1397 | 1398 | return WorkflowEnum.WITHDRAW_VOLUME 1399 | 1400 | 1401 | def funding_withdraw_volume(bot, update, chat_data): 1402 | chat_data["volume"] = update.message.text.replace(",", ".") 1403 | 1404 | volume = chat_data["volume"] 1405 | currency = chat_data["currency"] 1406 | wallet = chat_data["wallet"] 1407 | msg = e_qst + "Withdraw " + volume + " " + currency + " to wallet " + wallet + "?" 1408 | 1409 | update.message.reply_text(msg, reply_markup=keyboard_confirm()) 1410 | 1411 | return WorkflowEnum.WITHDRAW_CONFIRM 1412 | 1413 | 1414 | # Withdraw funds from wallet 1415 | def funding_withdraw_confirm(bot, update, chat_data): 1416 | if update.message.text.upper() == KeyboardEnum.NO.clean(): 1417 | return cancel(bot, update, chat_data=chat_data) 1418 | 1419 | update.message.reply_text(e_wit + "Withdrawal initiated...") 1420 | 1421 | req_data = dict() 1422 | req_data["asset"] = chat_data["currency"] 1423 | req_data["key"] = chat_data["wallet"] 1424 | req_data["amount"] = chat_data["volume"] 1425 | 1426 | # Send request to Kraken to get withdrawal info to lookup fee 1427 | res_data = kraken_api("WithdrawInfo", data=req_data, private=True) 1428 | 1429 | # If Kraken replied with an error, show it 1430 | if handle_api_error(res_data, update): 1431 | return 1432 | 1433 | # Add up volume and fee and set the new value as 'amount' 1434 | volume_and_fee = float(req_data["amount"]) + float(res_data["result"]["fee"]) 1435 | req_data["amount"] = str(volume_and_fee) 1436 | 1437 | # Send request to Kraken to withdraw digital currency 1438 | res_data = kraken_api("Withdraw", data=req_data, private=True) 1439 | 1440 | # If Kraken replied with an error, show it 1441 | if handle_api_error(res_data, update): 1442 | return 1443 | 1444 | # If a REFID exists, the withdrawal was initiated 1445 | if res_data["result"]["refid"]: 1446 | msg = e_fns + "Withdrawal executed\nREFID: " + res_data["result"]["refid"] 1447 | update.message.reply_text(msg) 1448 | else: 1449 | msg = e_err + "Undefined state: no error and no REFID" 1450 | update.message.reply_text(msg) 1451 | 1452 | clear_chat_data(chat_data) 1453 | return ConversationHandler.END 1454 | 1455 | 1456 | # Download newest script, update the currently running one and restart. 1457 | # If 'config.json' changed, update it also 1458 | @restrict_access 1459 | def update_cmd(bot, update): 1460 | # Get newest version of this script from GitHub 1461 | headers = {"If-None-Match": config["update_hash"]} 1462 | github_script = requests.get(config["update_url"], headers=headers) 1463 | 1464 | # Status code 304 = Not Modified 1465 | if github_script.status_code == 304: 1466 | msg = "You are running the latest version" 1467 | update.message.reply_text(msg, reply_markup=keyboard_cmds()) 1468 | # Status code 200 = OK 1469 | elif github_script.status_code == 200: 1470 | # Get github 'config.json' file 1471 | last_slash_index = config["update_url"].rfind("/") 1472 | github_config_path = config["update_url"][:last_slash_index + 1] + "config.json" 1473 | github_config_file = requests.get(github_config_path) 1474 | github_config = json.loads(github_config_file.text) 1475 | 1476 | # Compare current config keys with 1477 | # config keys from github-config 1478 | if set(config) != set(github_config): 1479 | # Go through all keys in github-config and 1480 | # if they are not present in current config, add them 1481 | for key, value in github_config.items(): 1482 | if key not in config: 1483 | config[key] = value 1484 | 1485 | # Save current ETag (hash) of bot script in github-config 1486 | e_tag = github_script.headers.get("ETag") 1487 | config["update_hash"] = e_tag 1488 | 1489 | # Save changed github-config as new config 1490 | with open("config.json", "w") as cfg: 1491 | json.dump(config, cfg, indent=4) 1492 | 1493 | # Get the name of the currently running script 1494 | path_split = os.path.split(str(sys.argv[0])) 1495 | filename = path_split[len(path_split)-1] 1496 | 1497 | # Save the content of the remote file 1498 | with open(filename, "w") as file: 1499 | file.write(github_script.text) 1500 | 1501 | # Restart the bot 1502 | restart_cmd(bot, update) 1503 | 1504 | # Every other status code 1505 | else: 1506 | msg = e_err + "Update not executed. Unexpected status code: " + github_script.status_code 1507 | update.message.reply_text(msg, reply_markup=keyboard_cmds()) 1508 | 1509 | return ConversationHandler.END 1510 | 1511 | 1512 | # This needs to be run on a new thread because calling 'updater.stop()' inside a 1513 | # handler (shutdown_cmd) causes a deadlock because it waits for itself to finish 1514 | def shutdown(): 1515 | updater.stop() 1516 | updater.is_idle = False 1517 | 1518 | 1519 | # Terminate this script 1520 | @restrict_access 1521 | def shutdown_cmd(bot, update): 1522 | update.message.reply_text(e_gby + "Shutting down...", reply_markup=ReplyKeyboardRemove()) 1523 | 1524 | # See comments on the 'shutdown' function 1525 | threading.Thread(target=shutdown).start() 1526 | 1527 | 1528 | # Restart this python script 1529 | @restrict_access 1530 | def restart_cmd(bot, update): 1531 | msg = e_wit + "Bot is restarting..." 1532 | update.message.reply_text(msg, reply_markup=ReplyKeyboardRemove()) 1533 | 1534 | time.sleep(0.2) 1535 | os.execl(sys.executable, sys.executable, *sys.argv) 1536 | 1537 | 1538 | # Get current settings 1539 | @restrict_access 1540 | def settings_cmd(bot, update): 1541 | settings = str() 1542 | buttons = list() 1543 | 1544 | # Go through all settings in config file 1545 | for key, value in config.items(): 1546 | settings += key + " = " + str(value) + "\n\n" 1547 | buttons.append(KeyboardButton(key.upper())) 1548 | 1549 | # Send message with all current settings (key & value) 1550 | update.message.reply_text(settings) 1551 | 1552 | cancel_btn = [ 1553 | KeyboardButton(KeyboardEnum.CANCEL.clean()) 1554 | ] 1555 | 1556 | msg = "Choose key to change value" 1557 | 1558 | menu = build_menu(buttons, n_cols=2, footer_buttons=cancel_btn) 1559 | reply_mrk = ReplyKeyboardMarkup(menu, resize_keyboard=True) 1560 | update.message.reply_text(msg, reply_markup=reply_mrk) 1561 | 1562 | return WorkflowEnum.SETTINGS_CHANGE 1563 | 1564 | 1565 | # Change setting 1566 | def settings_change(bot, update, chat_data): 1567 | # Clear data in case command is executed again without properly exiting first 1568 | clear_chat_data(chat_data) 1569 | 1570 | chat_data["setting"] = update.message.text.lower() 1571 | 1572 | # Don't allow to change setting 'user_id' 1573 | if update.message.text.upper() == "USER_ID": 1574 | update.message.reply_text("It's not possible to change USER_ID value") 1575 | return 1576 | 1577 | msg = "Enter new value" 1578 | 1579 | update.message.reply_text(msg, reply_markup=ReplyKeyboardRemove()) 1580 | 1581 | return WorkflowEnum.SETTINGS_SAVE 1582 | 1583 | 1584 | # Save new value for chosen setting 1585 | def settings_save(bot, update, chat_data): 1586 | new_value = update.message.text 1587 | 1588 | # Check if new value is a boolean 1589 | if new_value.lower() == "true": 1590 | chat_data["value"] = True 1591 | elif new_value.lower() == "false": 1592 | chat_data["value"] = False 1593 | else: 1594 | # Check if new value is an integer ... 1595 | try: 1596 | chat_data["value"] = int(new_value) 1597 | # ... if not, save as string 1598 | except ValueError: 1599 | chat_data["value"] = new_value 1600 | 1601 | msg = e_qst + "Save new value and restart bot?" 1602 | update.message.reply_text(msg, reply_markup=keyboard_confirm()) 1603 | 1604 | return WorkflowEnum.SETTINGS_CONFIRM 1605 | 1606 | 1607 | # Confirm saving new setting and restart bot 1608 | def settings_confirm(bot, update, chat_data): 1609 | if update.message.text.upper() == KeyboardEnum.NO.clean(): 1610 | return cancel(bot, update, chat_data=chat_data) 1611 | 1612 | # Set new value in config dictionary 1613 | config[chat_data["setting"]] = chat_data["value"] 1614 | 1615 | # Save changed config as new one 1616 | with open("config.json", "w") as cfg: 1617 | json.dump(config, cfg, indent=4) 1618 | 1619 | update.message.reply_text(e_fns + "New value saved") 1620 | 1621 | # Restart bot to activate new setting 1622 | restart_cmd(bot, update) 1623 | 1624 | 1625 | # Remove all data from 'chat_data' since we are canceling / ending 1626 | # the conversation. If this is not done, next conversation will 1627 | # have all the old values 1628 | def clear_chat_data(chat_data): 1629 | if chat_data: 1630 | for key in list(chat_data.keys()): 1631 | del chat_data[key] 1632 | 1633 | 1634 | # Will show a cancel message, end the conversation and show the default keyboard 1635 | def cancel(bot, update, chat_data=None): 1636 | # Clear 'chat_data' for next conversation 1637 | clear_chat_data(chat_data) 1638 | 1639 | # Show the commands keyboard and end the current conversation 1640 | update.message.reply_text(e_cnc + "Canceled...", reply_markup=keyboard_cmds()) 1641 | return ConversationHandler.END 1642 | 1643 | 1644 | # Check if GitHub hosts a different script then the currently running one 1645 | def get_update_state(): 1646 | # Get newest version of this script from GitHub 1647 | headers = {"If-None-Match": config["update_hash"]} 1648 | github_file = requests.get(config["update_url"], headers=headers) 1649 | 1650 | # Status code 304 = Not Modified (remote file has same hash, is the same version) 1651 | if github_file.status_code == 304: 1652 | msg = e_top + "Bot is up to date" 1653 | # Status code 200 = OK (remote file has different hash, is not the same version) 1654 | elif github_file.status_code == 200: 1655 | msg = e_ntf + "New version available. Get it with /update" 1656 | # Every other status code 1657 | else: 1658 | msg = e_err + "Update check not possible. Unexpected status code: " + github_file.status_code 1659 | 1660 | return github_file.status_code, msg 1661 | 1662 | 1663 | # Return chat ID for an update object 1664 | def get_chat_id(update=None): 1665 | if update: 1666 | if update.message: 1667 | return update.message.chat_id 1668 | elif update.callback_query: 1669 | return update.callback_query.from_user["id"] 1670 | else: 1671 | return config["user_id"] 1672 | 1673 | 1674 | # Create a button menu to show in Telegram messages 1675 | def build_menu(buttons, n_cols=1, header_buttons=None, footer_buttons=None): 1676 | menu = [buttons[i:i + n_cols] for i in range(0, len(buttons), n_cols)] 1677 | 1678 | if header_buttons: 1679 | menu.insert(0, header_buttons) 1680 | if footer_buttons: 1681 | menu.append(footer_buttons) 1682 | 1683 | return menu 1684 | 1685 | 1686 | # Custom keyboard that shows all available commands 1687 | def keyboard_cmds(): 1688 | command_buttons = [ 1689 | KeyboardButton("/trade"), 1690 | KeyboardButton("/orders"), 1691 | KeyboardButton("/balance"), 1692 | KeyboardButton("/price"), 1693 | KeyboardButton("/value"), 1694 | KeyboardButton("/chart"), 1695 | KeyboardButton("/trades"), 1696 | KeyboardButton("/funding"), 1697 | KeyboardButton("/bot") 1698 | ] 1699 | 1700 | return ReplyKeyboardMarkup(build_menu(command_buttons, n_cols=3), resize_keyboard=True) 1701 | 1702 | 1703 | # Generic custom keyboard that shows YES and NO 1704 | def keyboard_confirm(): 1705 | buttons = [ 1706 | KeyboardButton(KeyboardEnum.YES.clean()), 1707 | KeyboardButton(KeyboardEnum.NO.clean()) 1708 | ] 1709 | 1710 | return ReplyKeyboardMarkup(build_menu(buttons, n_cols=2), resize_keyboard=True) 1711 | 1712 | 1713 | # Create a list with a button for every coin in config 1714 | def coin_buttons(): 1715 | buttons = list() 1716 | 1717 | for coin in config["used_pairs"]: 1718 | buttons.append(KeyboardButton(coin)) 1719 | 1720 | return buttons 1721 | 1722 | 1723 | # Monitor closed orders 1724 | def check_order_exec(bot, job): 1725 | # Current datetime 1726 | datetime_now = datetime.datetime.now(datetime.timezone.utc) 1727 | # Datetime minus seconds since last check 1728 | datetime_last_check = datetime_now - datetime.timedelta(seconds=config["check_trade"]) 1729 | 1730 | # Send request for closed orders to Kraken 1731 | orders_req = {"start": datetime_last_check.timestamp(), "trades": True} 1732 | res_data = kraken_api("ClosedOrders", orders_req, private=True) 1733 | 1734 | error_prefix = "Check order execution:\n" 1735 | if handle_api_error(res_data, None, error_prefix, config["send_error"]): 1736 | return 1737 | 1738 | # Check if there are closed orders 1739 | if res_data["result"]["closed"]: 1740 | # Go through closed orders 1741 | for order_id, details in res_data["result"]["closed"].items(): 1742 | if trim_zeros(details["vol_exec"]) is not "0": 1743 | # Create trade string 1744 | trade_str = details["descr"]["type"] + " " + \ 1745 | details["vol_exec"] + " " + \ 1746 | details["descr"]["pair"] + " @ " + \ 1747 | details["descr"]["ordertype"] + " " + \ 1748 | details["price"] 1749 | 1750 | usr = config["user_id"] 1751 | msg = e_ntf + "Trade executed: " + details["misc"] + "\n" + trim_zeros(trade_str) 1752 | updater.bot.send_message(chat_id=usr, text=bold(msg), parse_mode=ParseMode.MARKDOWN) 1753 | 1754 | 1755 | # Start periodical job to check if new bot version is available 1756 | def monitor_updates(): 1757 | if config["update_check"] > 0: 1758 | # Check if current bot version is the latest 1759 | def version_check(bot, job): 1760 | status_code, msg = get_update_state() 1761 | 1762 | # Status code 200 means that the remote file is not the same 1763 | if status_code == 200: 1764 | msg = e_ntf + "New version available. Get it with /update" 1765 | bot.send_message(chat_id=config["user_id"], text=msg) 1766 | 1767 | # Add Job to JobQueue to run periodically 1768 | job_queue.run_repeating(version_check, config["update_check"], first=0) 1769 | 1770 | 1771 | # TODO: Complete sanity check 1772 | # Check sanity of settings in config file 1773 | def is_conf_sane(trade_pairs): 1774 | for setting, value in config.items(): 1775 | # Check if user ID is a digit 1776 | if "USER_ID" == setting.upper(): 1777 | if not value.isdigit(): 1778 | return False, setting.upper() 1779 | # Check if trade pairs are correctly configured, 1780 | # and save pairs in global variable 1781 | elif "USED_PAIRS" == setting.upper(): 1782 | global pairs 1783 | for coin, to_cur in value.items(): 1784 | found = False 1785 | for pair, data in trade_pairs.items(): 1786 | if coin in pair and to_cur in pair: 1787 | if not pair.endswith(".d"): 1788 | pairs[coin] = pair 1789 | found = True 1790 | if not found: 1791 | return False, setting.upper() + " - " + coin 1792 | 1793 | return True, None 1794 | 1795 | 1796 | # Make sure preconditions are met and show welcome screen 1797 | def init_cmd(bot, update): 1798 | uid = config["user_id"] 1799 | cmds = "/initialize - retry again\n/shutdown - shut down the bot" 1800 | 1801 | # Show start up message 1802 | msg = e_bgn + "Preparing Kraken-Bot" 1803 | updater.bot.send_message(uid, msg, disable_notification=True, reply_markup=ReplyKeyboardRemove()) 1804 | 1805 | # Assets ----------------- 1806 | 1807 | msg = e_wit + "Reading assets..." 1808 | m = updater.bot.send_message(uid, msg, disable_notification=True) 1809 | 1810 | res_assets = kraken_api("Assets") 1811 | 1812 | # If Kraken replied with an error, show it 1813 | if res_assets["error"]: 1814 | msg = e_fld + "Reading assets... FAILED\n" + cmds 1815 | updater.bot.edit_message_text(msg, chat_id=uid, message_id=m.message_id) 1816 | 1817 | error = btfy(res_assets["error"][0]) 1818 | updater.bot.send_message(uid, error) 1819 | log(logging.ERROR, error) 1820 | return 1821 | 1822 | # Save assets in global variable 1823 | global assets 1824 | assets = res_assets["result"] 1825 | 1826 | msg = e_dne + "Reading assets... DONE" 1827 | updater.bot.edit_message_text(msg, chat_id=uid, message_id=m.message_id) 1828 | 1829 | # Asset pairs ----------------- 1830 | 1831 | msg = e_wit + "Reading asset pairs..." 1832 | m = updater.bot.send_message(uid, msg, disable_notification=True) 1833 | 1834 | res_pairs = kraken_api("AssetPairs") 1835 | 1836 | # If Kraken replied with an error, show it 1837 | if res_pairs["error"]: 1838 | msg = e_fld + "Reading asset pairs... FAILED\n" + cmds 1839 | updater.bot.edit_message_text(msg, chat_id=uid, message_id=m.message_id) 1840 | 1841 | error = btfy(res_pairs["error"][0]) 1842 | updater.bot.send_message(uid, error) 1843 | log(logging.ERROR, error) 1844 | return 1845 | 1846 | msg = e_dne + "Reading asset pairs... DONE" 1847 | updater.bot.edit_message_text(msg, chat_id=uid, message_id=m.message_id) 1848 | 1849 | # Order limits ----------------- 1850 | 1851 | msg = e_wit + "Reading order limits..." 1852 | m = updater.bot.send_message(uid, msg, disable_notification=True) 1853 | 1854 | # Save order limits in global variable 1855 | global limits 1856 | limits = min_order_size() 1857 | 1858 | msg = e_dne + "Reading order limits... DONE" 1859 | updater.bot.edit_message_text(msg, chat_id=uid, message_id=m.message_id) 1860 | 1861 | # Sanity check ----------------- 1862 | 1863 | msg = e_wit + "Checking sanity..." 1864 | m = updater.bot.send_message(uid, msg, disable_notification=True) 1865 | 1866 | # Check sanity of configuration file 1867 | # Sanity check not finished successfully 1868 | sane, parameter = is_conf_sane(res_pairs["result"]) 1869 | if not sane: 1870 | msg = e_fld + "Checking sanity... FAILED\n/shutdown - shut down the bot" 1871 | updater.bot.edit_message_text(msg, chat_id=uid, message_id=m.message_id) 1872 | 1873 | msg = e_err + "Wrong configuration: " + parameter 1874 | updater.bot.send_message(uid, msg) 1875 | return 1876 | 1877 | msg = e_dne + "Checking sanity... DONE" 1878 | updater.bot.edit_message_text(msg, chat_id=uid, message_id=m.message_id) 1879 | 1880 | # Bot is ready ----------------- 1881 | 1882 | msg = e_bgn + "Kraken-Bot is ready!" 1883 | updater.bot.send_message(uid, msg, reply_markup=keyboard_cmds()) 1884 | 1885 | 1886 | # Converts a Unix timestamp to a data-time object with format 'Y-m-d H:M:S' 1887 | def datetime_from_timestamp(unix_timestamp): 1888 | return datetime.datetime.fromtimestamp(int(unix_timestamp)).strftime('%Y-%m-%d %H:%M:%S') 1889 | 1890 | 1891 | # From pair string (XBTEUR) get from-asset (XBT) and to-asset (ZEUR) 1892 | def assets_in_pair(pair): 1893 | for asset, _ in assets.items(): 1894 | altname = _.get("altname") 1895 | # If TRUE, we know that 'to_asset' exists in assets 1896 | if pair.endswith(altname): 1897 | from_asset = pair[:len(altname)] 1898 | to_asset = pair[len(pair)-len(altname):] 1899 | 1900 | # If TRUE, we assume its a fiat currency and adding Z to it. 1901 | if to_asset not in assets: 1902 | to_asset = ("Z" + to_asset) 1903 | 1904 | # If TRUE, we know that 'from_asset' exists in assets 1905 | if from_asset in assets: 1906 | return from_asset, to_asset 1907 | else: 1908 | return None, to_asset 1909 | 1910 | return None, None 1911 | 1912 | 1913 | # Remove trailing zeros and cut decimal places to get clean values 1914 | def trim_zeros(value_to_trim, decimals=config["decimals"]): 1915 | if isinstance(value_to_trim, float): 1916 | return (("%." + str(decimals) + "f") % value_to_trim).rstrip("0").rstrip(".") 1917 | elif isinstance(value_to_trim, str): 1918 | str_list = value_to_trim.split(" ") 1919 | for i in range(len(str_list)): 1920 | old_str = str_list[i] 1921 | if old_str.replace(".", "").replace(",", "").isdigit(): 1922 | new_str = str((("%." + str(decimals) + "f") % float(old_str)).rstrip("0").rstrip(".")) 1923 | str_list[i] = new_str 1924 | return " ".join(str_list) 1925 | else: 1926 | return value_to_trim 1927 | 1928 | 1929 | # Add asterisk as prefix and suffix for a string 1930 | # Will make the text bold if used with Markdown 1931 | def bold(text): 1932 | return "*" + text + "*" 1933 | 1934 | 1935 | # Beautifies Kraken error messages 1936 | def btfy(text): 1937 | # Remove whitespaces 1938 | text = text.strip() 1939 | 1940 | new_text = str() 1941 | 1942 | for x in range(0, len(list(text))): 1943 | new_text += list(text)[x] 1944 | 1945 | if list(text)[x] == ":": 1946 | new_text += " " 1947 | 1948 | return e_err + new_text 1949 | 1950 | 1951 | # Return state of Kraken API 1952 | # State will be extracted from Kraken Status website 1953 | def api_state(): 1954 | url = "https://status.kraken.com" 1955 | response = requests.get(url) 1956 | 1957 | # If response code is not 200, return state 'UNKNOWN' 1958 | if response.status_code != 200: 1959 | return "UNKNOWN" 1960 | 1961 | soup = BeautifulSoup(response.content, "html.parser") 1962 | 1963 | for comp_inner_cont in soup.find_all(class_="component-inner-container"): 1964 | for name in comp_inner_cont.find_all(class_="name"): 1965 | if "API" in name.get_text(): 1966 | return comp_inner_cont.find(class_="component-status").get_text().strip() 1967 | 1968 | 1969 | # Return dictionary with asset name as key and order limit as value 1970 | def min_order_size(): 1971 | url = "https://support.kraken.com/hc/en-us/articles/205893708-What-is-the-minimum-order-size-" 1972 | response = requests.get(url) 1973 | 1974 | # If response code is not 200, return empty dictionary 1975 | if response.status_code != 200: 1976 | return {} 1977 | 1978 | min_order_size = dict() 1979 | 1980 | soup = BeautifulSoup(response.content, "html.parser") 1981 | 1982 | for article_body in soup.find_all(class_="article-body"): 1983 | for ul in article_body.find_all("ul"): 1984 | for li in ul.find_all("li"): 1985 | text = li.get_text().strip() 1986 | limit = text[text.find(":") + 1:].strip() 1987 | match = re.search('\((.+?)\)', text) 1988 | 1989 | if match: 1990 | min_order_size[match.group(1)] = limit 1991 | 1992 | return min_order_size 1993 | 1994 | 1995 | # Returns a pre compiled Regex pattern to ignore case 1996 | def comp(pattern): 1997 | return re.compile(pattern, re.IGNORECASE) 1998 | 1999 | 2000 | # Returns regex representation of OR for all coins in config 'used_pairs' 2001 | def regex_coin_or(): 2002 | coins_regex_or = str() 2003 | 2004 | for coin in config["used_pairs"]: 2005 | coins_regex_or += coin + "|" 2006 | 2007 | return coins_regex_or[:-1] 2008 | 2009 | 2010 | # Returns regex representation of OR for all fiat currencies in config 'used_pairs' 2011 | def regex_asset_or(): 2012 | fiat_regex_or = str() 2013 | 2014 | for asset, data in assets.items(): 2015 | fiat_regex_or += data["altname"] + "|" 2016 | 2017 | return fiat_regex_or[:-1] 2018 | 2019 | 2020 | # Return regex representation of OR for all settings in config 2021 | def regex_settings_or(): 2022 | settings_regex_or = str() 2023 | 2024 | for key, value in config.items(): 2025 | settings_regex_or += key.upper() + "|" 2026 | 2027 | return settings_regex_or[:-1] 2028 | 2029 | 2030 | def handle_api_error(response, update, msg_prefix="", send_msg=True): 2031 | if response["error"]: 2032 | error = btfy(msg_prefix + response["error"][0]) 2033 | log(logging.ERROR, error) 2034 | 2035 | if send_msg: 2036 | if update: 2037 | update.message.reply_text(error) 2038 | else: 2039 | updater.bot.send_message(chat_id=config["user_id"], text=error) 2040 | 2041 | return True 2042 | 2043 | return False 2044 | 2045 | 2046 | # Handle all telegram and telegram.ext related errors 2047 | def handle_telegram_error(bot, update, error): 2048 | error_str = "Update '%s' caused error '%s'" % (update, error) 2049 | log(logging.ERROR, error_str) 2050 | 2051 | if config["send_error"]: 2052 | updater.bot.send_message(chat_id=config["user_id"], text=error_str) 2053 | 2054 | 2055 | # Make sure preconditions are met and show welcome screen 2056 | init_cmd(None, None) 2057 | 2058 | 2059 | # Log all errors 2060 | dispatcher.add_error_handler(handle_telegram_error) 2061 | 2062 | # Add command handlers to dispatcher 2063 | dispatcher.add_handler(CommandHandler("update", update_cmd)) 2064 | dispatcher.add_handler(CommandHandler("restart", restart_cmd)) 2065 | dispatcher.add_handler(CommandHandler("shutdown", shutdown_cmd)) 2066 | dispatcher.add_handler(CommandHandler("initialize", init_cmd)) 2067 | dispatcher.add_handler(CommandHandler("balance", balance_cmd)) 2068 | dispatcher.add_handler(CommandHandler("reload", reload_cmd)) 2069 | dispatcher.add_handler(CommandHandler("state", state_cmd)) 2070 | dispatcher.add_handler(CommandHandler("start", start_cmd)) 2071 | 2072 | 2073 | # TODO: Use enums inside RegexHandlers 2074 | # FUNDING conversation handler 2075 | funding_handler = ConversationHandler( 2076 | entry_points=[CommandHandler('funding', funding_cmd)], 2077 | states={ 2078 | WorkflowEnum.FUNDING_CURRENCY: 2079 | [RegexHandler(comp("^(" + regex_coin_or() + ")$"), funding_currency, pass_chat_data=True), 2080 | RegexHandler(comp("^(CANCEL)$"), cancel, pass_chat_data=True)], 2081 | WorkflowEnum.FUNDING_CHOOSE: 2082 | [RegexHandler(comp("^(DEPOSIT)$"), funding_deposit, pass_chat_data=True), 2083 | RegexHandler(comp("^(WITHDRAW)$"), funding_withdraw), 2084 | RegexHandler(comp("^(CANCEL)$"), cancel, pass_chat_data=True)], 2085 | WorkflowEnum.WITHDRAW_WALLET: 2086 | [MessageHandler(Filters.text, funding_withdraw_wallet, pass_chat_data=True)], 2087 | WorkflowEnum.WITHDRAW_VOLUME: 2088 | [MessageHandler(Filters.text, funding_withdraw_volume, pass_chat_data=True)], 2089 | WorkflowEnum.WITHDRAW_CONFIRM: 2090 | [RegexHandler(comp("^(YES|NO)$"), funding_withdraw_confirm, pass_chat_data=True)] 2091 | }, 2092 | fallbacks=[CommandHandler('cancel', cancel, pass_chat_data=True)], 2093 | allow_reentry=True) 2094 | dispatcher.add_handler(funding_handler) 2095 | 2096 | 2097 | # TRADES conversation handler 2098 | trades_handler = ConversationHandler( 2099 | entry_points=[CommandHandler('trades', trades_cmd)], 2100 | states={ 2101 | WorkflowEnum.TRADES_NEXT: 2102 | [RegexHandler(comp("^(NEXT)$"), trades_next), 2103 | RegexHandler(comp("^(CANCEL)$"), cancel)] 2104 | }, 2105 | fallbacks=[CommandHandler('cancel', cancel)], 2106 | allow_reentry=True) 2107 | dispatcher.add_handler(trades_handler) 2108 | 2109 | 2110 | # CHART conversation handler 2111 | chart_handler = ConversationHandler( 2112 | entry_points=[CommandHandler('chart', chart_cmd)], 2113 | states={ 2114 | WorkflowEnum.CHART_CURRENCY: 2115 | [RegexHandler(comp("^(" + regex_coin_or() + ")$"), chart_currency), 2116 | RegexHandler(comp("^(CANCEL)$"), cancel)] 2117 | }, 2118 | fallbacks=[CommandHandler('cancel', cancel)], 2119 | allow_reentry=True) 2120 | dispatcher.add_handler(chart_handler) 2121 | 2122 | 2123 | # ORDERS conversation handler 2124 | orders_handler = ConversationHandler( 2125 | entry_points=[CommandHandler('orders', orders_cmd)], 2126 | states={ 2127 | WorkflowEnum.ORDERS_CLOSE: 2128 | [RegexHandler(comp("^(CLOSE ORDER)$"), orders_choose_order), 2129 | RegexHandler(comp("^(CLOSE ALL)$"), orders_close_all), 2130 | RegexHandler(comp("^(CANCEL)$"), cancel)], 2131 | WorkflowEnum.ORDERS_CLOSE_ORDER: 2132 | [RegexHandler(comp("^(CANCEL)$"), cancel), 2133 | RegexHandler(comp("^[A-Z0-9]{6}-[A-Z0-9]{5}-[A-Z0-9]{6}$"), orders_close_order)] 2134 | }, 2135 | fallbacks=[CommandHandler('cancel', cancel)], 2136 | allow_reentry=True) 2137 | dispatcher.add_handler(orders_handler) 2138 | 2139 | 2140 | # TRADE conversation handler 2141 | trade_handler = ConversationHandler( 2142 | entry_points=[CommandHandler('trade', trade_cmd)], 2143 | states={ 2144 | WorkflowEnum.TRADE_BUY_SELL: 2145 | [RegexHandler(comp("^(BUY|SELL)$"), trade_buy_sell, pass_chat_data=True), 2146 | RegexHandler(comp("^(CANCEL)$"), cancel, pass_chat_data=True)], 2147 | WorkflowEnum.TRADE_CURRENCY: 2148 | [RegexHandler(comp("^(" + regex_coin_or() + ")$"), trade_currency, pass_chat_data=True), 2149 | RegexHandler(comp("^(CANCEL)$"), cancel, pass_chat_data=True), 2150 | RegexHandler(comp("^(ALL)$"), trade_sell_all)], 2151 | WorkflowEnum.TRADE_SELL_ALL_CONFIRM: 2152 | [RegexHandler(comp("^(YES|NO)$"), trade_sell_all_confirm)], 2153 | WorkflowEnum.TRADE_PRICE: 2154 | [RegexHandler(comp("^((?=.*?\d)\d*[.,]?\d*|MARKET PRICE)$"), trade_price, pass_chat_data=True), 2155 | RegexHandler(comp("^(CANCEL)$"), cancel, pass_chat_data=True)], 2156 | WorkflowEnum.TRADE_VOL_TYPE: 2157 | [RegexHandler(comp("^(" + regex_asset_or() + ")$"), trade_vol_asset, pass_chat_data=True), 2158 | RegexHandler(comp("^(VOLUME)$"), trade_vol_volume, pass_chat_data=True), 2159 | RegexHandler(comp("^(ALL)$"), trade_vol_all, pass_chat_data=True), 2160 | RegexHandler(comp("^(CANCEL)$"), cancel, pass_chat_data=True)], 2161 | WorkflowEnum.TRADE_VOLUME: 2162 | [RegexHandler(comp("^^(?=.*?\d)\d*[.,]?\d*$"), trade_volume, pass_chat_data=True), 2163 | RegexHandler(comp("^(CANCEL)$"), cancel, pass_chat_data=True)], 2164 | WorkflowEnum.TRADE_VOLUME_ASSET: 2165 | [RegexHandler(comp("^^(?=.*?\d)\d*[.,]?\d*$"), trade_volume_asset, pass_chat_data=True), 2166 | RegexHandler(comp("^(CANCEL)$"), cancel, pass_chat_data=True)], 2167 | WorkflowEnum.TRADE_CONFIRM: 2168 | [RegexHandler(comp("^(YES|NO)$"), trade_confirm, pass_chat_data=True)] 2169 | }, 2170 | fallbacks=[CommandHandler('cancel', cancel, pass_chat_data=True)], 2171 | allow_reentry=True) 2172 | dispatcher.add_handler(trade_handler) 2173 | 2174 | 2175 | # PRICE conversation handler 2176 | price_handler = ConversationHandler( 2177 | entry_points=[CommandHandler('price', price_cmd)], 2178 | states={ 2179 | WorkflowEnum.PRICE_CURRENCY: 2180 | [RegexHandler(comp("^(" + regex_coin_or() + ")$"), price_currency), 2181 | RegexHandler(comp("^(CANCEL)$"), cancel)] 2182 | }, 2183 | fallbacks=[CommandHandler('cancel', cancel)], 2184 | allow_reentry=True) 2185 | dispatcher.add_handler(price_handler) 2186 | 2187 | 2188 | # VALUE conversation handler 2189 | value_handler = ConversationHandler( 2190 | entry_points=[CommandHandler('value', value_cmd)], 2191 | states={ 2192 | WorkflowEnum.VALUE_CURRENCY: 2193 | [RegexHandler(comp("^(" + regex_coin_or() + "|ALL)$"), value_currency), 2194 | RegexHandler(comp("^(CANCEL)$"), cancel)] 2195 | }, 2196 | fallbacks=[CommandHandler('cancel', cancel)], 2197 | allow_reentry=True) 2198 | dispatcher.add_handler(value_handler) 2199 | 2200 | 2201 | # Will return the SETTINGS_CHANGE state for a conversation handler 2202 | # This way the state is reusable 2203 | def settings_change_state(): 2204 | return [WorkflowEnum.SETTINGS_CHANGE, 2205 | [RegexHandler(comp("^(" + regex_settings_or() + ")$"), settings_change, pass_chat_data=True), 2206 | RegexHandler(comp("^(CANCEL)$"), cancel, pass_chat_data=True)]] 2207 | 2208 | 2209 | # Will return the SETTINGS_SAVE state for a conversation handler 2210 | # This way the state is reusable 2211 | def settings_save_state(): 2212 | return [WorkflowEnum.SETTINGS_SAVE, 2213 | [MessageHandler(Filters.text, settings_save, pass_chat_data=True)]] 2214 | 2215 | 2216 | # Will return the SETTINGS_CONFIRM state for a conversation handler 2217 | # This way the state is reusable 2218 | def settings_confirm_state(): 2219 | return [WorkflowEnum.SETTINGS_CONFIRM, 2220 | [RegexHandler(comp("^(YES|NO)$"), settings_confirm, pass_chat_data=True)]] 2221 | 2222 | 2223 | # BOT conversation handler 2224 | bot_handler = ConversationHandler( 2225 | entry_points=[CommandHandler('bot', bot_cmd)], 2226 | states={ 2227 | WorkflowEnum.BOT_SUB_CMD: 2228 | [RegexHandler(comp("^(UPDATE CHECK|UPDATE|RESTART|SHUTDOWN)$"), bot_sub_cmd), 2229 | RegexHandler(comp("^(API STATE)$"), state_cmd), 2230 | RegexHandler(comp("^(SETTINGS)$"), settings_cmd), 2231 | RegexHandler(comp("^(CANCEL)$"), cancel)], 2232 | settings_change_state()[0]: settings_change_state()[1], 2233 | settings_save_state()[0]: settings_save_state()[1], 2234 | settings_confirm_state()[0]: settings_confirm_state()[1] 2235 | }, 2236 | fallbacks=[CommandHandler('cancel', cancel)], 2237 | allow_reentry=True) 2238 | dispatcher.add_handler(bot_handler) 2239 | 2240 | 2241 | # SETTINGS conversation handler 2242 | settings_handler = ConversationHandler( 2243 | entry_points=[CommandHandler('settings', settings_cmd)], 2244 | states={ 2245 | settings_change_state()[0]: settings_change_state()[1], 2246 | settings_save_state()[0]: settings_save_state()[1], 2247 | settings_confirm_state()[0]: settings_confirm_state()[1] 2248 | }, 2249 | fallbacks=[CommandHandler('cancel', cancel)], 2250 | allow_reentry=True) 2251 | dispatcher.add_handler(settings_handler) 2252 | 2253 | 2254 | # Write content of configuration file to log 2255 | log(logging.DEBUG, "Configuration: " + str(config)) 2256 | 2257 | # If webhook is enabled, don't use polling 2258 | # https://github.com/python-telegram-bot/python-telegram-bot/wiki/Webhooks 2259 | if config["webhook_enabled"]: 2260 | updater.start_webhook(listen=config["webhook_listen"], 2261 | port=config["webhook_port"], 2262 | url_path=config["bot_token"], 2263 | key=config["webhook_key"], 2264 | cert=config["webhook_cert"], 2265 | webhook_url=config["webhook_url"]) 2266 | else: 2267 | # Start polling to handle all user input 2268 | # Dismiss all in the meantime send commands 2269 | updater.start_polling(clean=True) 2270 | 2271 | # Check for new bot version periodically 2272 | monitor_updates() 2273 | 2274 | # Periodically monitor status changes of open orders 2275 | if config["check_trade"] > 0: 2276 | job_queue.run_repeating(check_order_exec, config["check_trade"], first=0) 2277 | 2278 | # Run the bot until you press Ctrl-C or the process receives SIGINT, 2279 | # SIGTERM or SIGABRT. This should be used most of the time, since 2280 | # start_polling() is non-blocking and will stop the bot gracefully. 2281 | updater.idle() 2282 | --------------------------------------------------------------------------------