├── _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 |
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 |
--------------------------------------------------------------------------------