├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── requirements.txt ├── LICENSE ├── CONTRIBUTING.md ├── .gitignore ├── README.md ├── webhook.py ├── helpers ├── logging.py ├── badcoins.py ├── misc.py └── threecommas.py ├── Smart_Trade_with_RR.py └── Smart_Trade_fixed_TPs.py /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.4 2 | APScheduler==3.9.1 3 | black==22.8.0 4 | cachetools==5.2.0 5 | certifi==2023.07.22 6 | chardet==5.0.0 7 | click==8.1.3 8 | flake8==5.0.4 9 | Flask==2.3.2 10 | idna==2.10 11 | isort==5.10.1 12 | itsdangerous==2.1.2 13 | Jinja2==3.1.4 14 | MarkupSafe==2.1.1 15 | mccabe==0.7.0 16 | mypy-extensions==0.4.3 17 | oauthlib==3.2.2 18 | pathspec==0.10.1 19 | pycodestyle==2.9.1 20 | pyflakes==2.5.0 21 | PySocks==1.7.1 22 | python-telegram-bot==13.6 23 | pytz==2022.2.1 24 | regex==2022.9.13 25 | requests==2.31.0 26 | requests-oauthlib==1.3.1 27 | six==1.16.0 28 | slack-webhook==1.0.7 29 | toml==0.10.2 30 | tornado==6.4.1 31 | tzlocal==4.2 32 | waitress==2.1.2 33 | Werkzeug==2.3.8 34 | binance==0.3 35 | colorama~=0.4.5 36 | telethon~=1.24.0 37 | python-binance==1.0.16 38 | py3cw==0.0.39 39 | apprise~=0.9.9 40 | aiohttp 41 | schedule 42 | bs4~=0.0.1 43 | telegram~=0.0.1 44 | numpy==1.23.1 45 | lunarcrush~=1.0.1 46 | pathlib~=1.0.1 47 | plotly~=5.9.0 48 | tronpy~=0.2.6 49 | python-dateutil~=2.8.2 50 | Werkzeug>=2.2.3 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 T Zacks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | **Contributing Guidelines** 2 | 3 | Thank you for your interest in contributing to our project! We welcome contributions of all types, including code, documentation, bug reports, and feature requests. 4 | 5 | **How to Contribute** 6 | 7 | Here are a few ways you can get involved: 8 | 9 | **Report bugs and suggest features**: If you find a bug or have an idea for a new feature, please open an issue on our GitHub repository. 10 | 11 | **Contribute code**: If you would like to contribute code changes, please fork the repository and submit a pull request. 12 | 13 | **Improve documentation**: We always appreciate improvements to our documentation, including tutorials and guides. 14 | 15 | **Guidelines for Code Contributions 16 | 17 | **Code style:** Please adhere to the existing code style and conventions used in the project. 18 | **Testing**: All new code should include appropriate tests. 19 | **Documentation**: Please update any relevant documentation, including the README.md and inline code comments, when making code changes. 20 | 21 | **Submitting a Pull Request** 22 | 23 | **Fork the repository:** Before making any changes, please fork the repository to your own GitHub account. 24 | **Create a new branch:** Create a new branch for your changes and give it a descriptive name. 25 | **Commit your changes:** Make sure to include a clear and detailed commit message describing the changes you have made. 26 | **Submit the pull request:** Once you have pushed your changes to your fork, submit a pull request to the main repository. 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 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 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3commas-Smart-Trades-helpers 2 | Scripts for using 3commas Smart Trade with Risk Management 3 | 4 | Most of us fail in Crypto Day Trading Due to Bad risk management. 5 | therefore, these Scripts were created 6 | ****************** 7 | # How it works? 8 | I have designed these scripts to risk a certain amount per trade with self adjusting position Size/value with 9 | Risk-Reward Ratio or Multi Take profit levels system , **controlled via Telegram** 10 | 11 | 12 | Works for spot and futures **USDT Market ONLY** ( LONG and SHORT ) with Break-even Feature. 13 | 14 | 15 | # Explanation of The Money management Strategy 16 | 17 | Assuming your **capital** is $20K 18 | 19 | if you enter a BTC long position with **5% stoploss** without any risk management you lose $1,000 . too big ?! if yes then here comes the Risk management part 20 | 21 | Your **risk per trade** = 1% of capital = $200 22 | 23 | **Your Position value** will be calculated and adjusted to match your stoploss USD value so your Position value will be = **$4,000** 24 | 25 | BTC would need to drop 5% in price value in order to lose the **Risk Per trade value** which is in our case is **1% of the capital = $200** 26 | 27 | **so your risk USD value per trade stays the same , no matter how small/big your stoploss is** 28 | 29 | and SO ON . 30 | 31 | ***************************** 32 | 33 | # Take profit 34 | 35 | **We have 2 Scripts with different Take profit strategies** 36 | * **Fixed Tps** which is [Smart_Trade_fixed_TPs.py](https://github.com/TZacksEG/3commas-Smart-Trades-helpers/wiki/Smart-Trade-with-Fixed-TP's-level) Fixed % of Tps (adjustable) . 37 | 38 | you can set whatever % of tps you want: 2% and 5% and 10% Up to 5 TP's levels. 39 | 40 | 41 | * **Risk-Reward Ratio** which is [Smart_Trade_with_RR.py](https://github.com/TZacksEG/3commas-Smart-Trades-helpers/blob/main/Smart_Trade_with_RR.py) RR Strategy. 42 | 43 | it calculates your profit automatically using the value of the stoploss used in the trade. 44 | 45 | **Default RR is 1 to 2.4 RR, so you lose 1 dollar to earn $2.4** 46 | 47 | **BOTH OF the scripts have a Break-even feature which will automatically MOVE your Stoploss level to entry when first TP is Reached** 48 | 49 | **Trailing TP has been added to the last TP value in Fixed TP's Scripts** 50 | 51 | 52 | ********************** 53 | 54 | # How to use? 55 | * if you want to run the whole operation automatically 56 | - BY automatically Open trades Via sending alerts from Tradingview 57 | to your Telegram Channel and then Telegram to **3commas** 58 | - then you need to have your own webhook server to manage the whole automation 59 | - [Install Webhook Server](https://github.com/TZacksEG/3commas-Smart-Trades-helpers/wiki/Personal-Webhook-Setup) 60 | 61 | 62 | * if you want to run the smart Trades manually by sending yourself the Trades to Telegram 63 | - Just Send Trades manually to the Telegram Channel , where it will send your Trades to 3commas Automaticlly 64 | 65 | 66 | * Scripts 67 | - [Smart_Trade_fixed_TPs](https://github.com/TZacksEG/3commas-Smart-Trades-helpers/wiki/Smart-Trade-with-Fixed-TP's-level) 68 | - [Smart_Trade_with_RR ( RISK and REWARD )](https://github.com/TZacksEG/3commas-Smart-Trades-helpers/wiki/Smart-Trades-with-RR-(-RISK-and-REWARD-)) 69 | 70 | 71 | # 72 | 73 | ********************** 74 | # 75 | 76 | # Installation 77 | 78 | * Get a VPS i recommend [Contabo.com](https://contabo.com/en/) 79 | * need python3.8 or higher 80 | * git clone https://github.com/TZacksEG/3commas-Smart-Trades-helpers.git 81 | * pip install -r requirements.txt 82 | * **python3 Smart_Trade_fixed_TPs.py** 83 | * OR 84 | * **python3 Smart_Trade_with_RR.py** 85 | * **if Webhook is Needed then python3 webhook.py** 86 | 87 | Need Help ?! 88 | 89 | join discord : https://discord.com/invite/as3yHDugHE 90 | -------------------------------------------------------------------------------- /webhook.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import configparser 3 | import os 4 | from pathlib import Path 5 | import json 6 | from telegram import Bot 7 | import time 8 | import sys 9 | from flask import Flask, request 10 | 11 | 12 | def get_timestamp(): 13 | timestamp = time.strftime("%Y-%m-%d %X") 14 | return timestamp 15 | 16 | 17 | def load_config(): 18 | """Create default or load existing config file.""" 19 | 20 | cfg = configparser.ConfigParser() 21 | if cfg.read(f"{datadir}/{program}.ini"): 22 | return cfg 23 | 24 | cfg["settings"] = { 25 | "timezone": "Africa/Cairo", 26 | "debug": False, 27 | "logrotate": 7, 28 | "secret-key": 877876, 29 | "LONG-bot": "Telegram BOT Token for LONG alerts", 30 | "LONG-channel": "Telegram Channel ID for LONG alerts ", 31 | "SHORT-bot": "Telegram BOT Token for SHORT alerts", 32 | "SHORT-Channel": "Telegram Channel ID for SHORT alerts ", 33 | } 34 | with open(f"{datadir}/{program}.ini", "w") as cfgfile: 35 | cfg.write(cfgfile) 36 | 37 | return None 38 | 39 | 40 | def send_alert(data): 41 | msg = data["msg"].encode("latin-1", "backslashreplace").decode("unicode_escape") 42 | if "SHORT" in msg: 43 | tg_bot = Bot(token=str(config.get("settings", "SHORT-bot"))) 44 | msg = msg.replace("--", "\n") 45 | try: 46 | tg_bot.sendMessage( 47 | data["telegram"], 48 | msg, 49 | # parse_mode="MARKDOWN", 50 | ) 51 | print(get_timestamp(), f"Alert Received & Sent!") 52 | except KeyError: 53 | tg_bot.sendMessage( 54 | int(config.get("settings", "SHORT-Channel")), 55 | msg, 56 | # parse_mode="MARKDOWN", 57 | ) 58 | print("[X]", get_timestamp(), "Alert Received & Refused! (Wrong Key)") 59 | elif "LONG" in msg: 60 | tg_bot = Bot(token=str(config.get("settings", "LONG-bot"))) 61 | msg = msg.replace("--", "\n") 62 | try: 63 | tg_bot.sendMessage( 64 | data["telegram"], 65 | msg, 66 | # parse_mode="MARKDOWN", 67 | ) 68 | print(get_timestamp(), f"Alert Received & Sent!") 69 | except KeyError: 70 | tg_bot.sendMessage( 71 | int(config.get("settings", "LONG-Channel")), 72 | msg, 73 | # parse_mode="MARKDOWN", 74 | ) 75 | print("[X]", get_timestamp(), "Alert Received & Refused! (Wrong Key)") 76 | except Exception as e: 77 | print("[X] Telegram Error:\n>", e) 78 | 79 | else: 80 | return 81 | 82 | 83 | # Start application 84 | core = Flask(__name__) 85 | program = Path(__file__).stem 86 | 87 | # Parse and interpret options. 88 | parser = argparse.ArgumentParser(description="ZCrypto helpers.") 89 | parser.add_argument( 90 | "-d", "--datadir", help="directory to use for config and logs files", type=str 91 | ) 92 | parser.add_argument( 93 | "-s", "--sharedir", help="directory to use for shared files", type=str 94 | ) 95 | parser.add_argument( 96 | "-b", "--blacklist", help="local blacklist to use instead of 3Commas's", type=str 97 | ) 98 | 99 | args = parser.parse_args() 100 | if args.datadir: 101 | datadir = args.datadir 102 | else: 103 | datadir = os.getcwd() 104 | 105 | if args.sharedir: 106 | sharedir = args.sharedir 107 | else: 108 | sharedir = None 109 | if args.blacklist: 110 | blacklistfile = f"{datadir}/{args.blacklist}" 111 | else: 112 | blacklistfile = None 113 | 114 | # Create or load configuration file 115 | config = load_config() 116 | if not config: 117 | # Initialise temp logging 118 | print( 119 | f"Created example config file '{datadir}/{program}.ini', edit it and restart the program" 120 | ) 121 | sys.exit(0) 122 | else: 123 | # Handle timezone 124 | if hasattr(time, "tzset"): 125 | os.environ["TZ"] = config.get( 126 | "settings", "timezone", fallback="Africa/Cairo" 127 | ) 128 | time.tzset() 129 | 130 | 131 | @core.route("/webhook", methods=["POST"]) 132 | def webhook(): 133 | try: 134 | if request.method == "POST": 135 | data = request.get_json() 136 | key = data["key"] 137 | if key == str(config.get("settings", "secret-key")): 138 | send_alert(data) 139 | return "Sent alert", 200 140 | else: 141 | 142 | return "Refused alert", 400 143 | 144 | except Exception as e: 145 | print(get_timestamp(), "Error:\n>", e) 146 | return "Error", 400 147 | 148 | 149 | while True: 150 | config = load_config() 151 | print(f"Reloaded configuration from '{datadir}/{program}.ini'") 152 | if __name__ == "__main__": 153 | from waitress import serve 154 | 155 | print("Webhook Service Has been activated") 156 | serve(core, host="0.0.0.0", port=80) 157 | 158 | # TO DO 159 | -------------------------------------------------------------------------------- /helpers/logging.py: -------------------------------------------------------------------------------- 1 | """ ZCrypto bot helpers.""" 2 | import json 3 | import logging 4 | import os 5 | import queue 6 | import threading 7 | import time 8 | from logging.handlers import TimedRotatingFileHandler as _TimedRotatingFileHandler 9 | from colorama import Fore, init 10 | import sys 11 | 12 | init(autoreset=True) 13 | import apprise 14 | 15 | 16 | class NotificationHandler: 17 | """Notification class.""" 18 | 19 | def __init__(self, program, enabled=False, notify_urls=None): 20 | self.program = program 21 | self.message = "" 22 | 23 | if enabled and notify_urls: 24 | self.apobj = apprise.Apprise() 25 | urls = json.loads(notify_urls) 26 | for url in urls: 27 | self.apobj.add(url) 28 | self.queue = queue.Queue() 29 | self.start_worker() 30 | self.enabled = True 31 | else: 32 | self.enabled = False 33 | 34 | def start_worker(self): 35 | """Start notification worker.""" 36 | threading.Thread(target=self.process_queue, daemon=True).start() 37 | 38 | def process_queue(self): 39 | """Process the queue.""" 40 | while True: 41 | message, attachments = self.queue.get() 42 | if attachments: 43 | self.apobj.notify(body=message, attach=attachments) 44 | else: 45 | self.apobj.notify(body=message) 46 | self.queue.task_done() 47 | 48 | def queue_notification(self, message): 49 | """Queue notification messages.""" 50 | if self.enabled: 51 | message.encode(encoding='UTF-8', errors='strict') 52 | self.message += f"{message}\n\n" 53 | 54 | def send_notification(self): 55 | """Send the notification messages if there are any.""" 56 | if self.enabled and self.message: 57 | msg = f"-|- ZCrypto {self.program} -|- \n\n" + self.message 58 | self.queue.put((msg, [])) 59 | self.message = "" 60 | 61 | 62 | class TimedRotatingFileHandler(_TimedRotatingFileHandler): 63 | """Override original code to fix bug with not deleting old logfiles.""" 64 | 65 | def __init__(self, filename="", when="midnight", interval=1, backupCount=7, encoding='utf-8'): 66 | super().__init__( 67 | filename=filename, 68 | when=when, 69 | interval=int(interval), 70 | backupCount=int(backupCount), 71 | encoding=encoding 72 | ) 73 | 74 | def getFilesToDelete(self): 75 | """Find all logfiles present.""" 76 | dirname, basename = os.path.split(self.baseFilename) 77 | filenames = os.listdir(dirname) 78 | result = [] 79 | prefix = basename + "." 80 | plen = len(prefix) 81 | for filename in filenames: 82 | if filename[:plen] == prefix: 83 | suffix = filename[plen:] 84 | if self.extMatch.match(suffix): 85 | result.append(os.path.join(dirname, filename)) 86 | result.sort() 87 | if len(result)0: 106 | for oldlog in self.getFilesToDelete(): 107 | os.remove(oldlog) 108 | 109 | self.stream = open(self.baseFilename, "w") 110 | 111 | currenttime = int(time.time()) 112 | newrolloverat = self.computeRollover(currenttime) 113 | while newrolloverat<=currenttime: 114 | newrolloverat = newrolloverat + self.interval 115 | 116 | self.rolloverAt = newrolloverat 117 | 118 | 119 | class Logger: 120 | """Logger class.""" 121 | 122 | my_logger = None 123 | 124 | def __init__( 125 | self, 126 | datadir, 127 | program, 128 | notificationhandler, 129 | logstokeep, 130 | debug_enabled, 131 | notify_enabled, 132 | ): 133 | """Logger init.""" 134 | self.my_logger = logging.getLogger() 135 | self.datadir = datadir 136 | self.program = program 137 | self.notify_enabled = notify_enabled 138 | self.notificationhandler = notificationhandler 139 | 140 | if debug_enabled: 141 | self.my_logger.setLevel(logging.DEBUG) 142 | self.my_logger.propagate = False 143 | else: 144 | self.my_logger.setLevel(logging.INFO) 145 | self.my_logger.propagate = False 146 | 147 | date_fmt = "%Y-%m-%d %H:%M:%S" 148 | 149 | formatter = logging.Formatter( 150 | f"%(asctime)s - {program} - %(levelname)s - %(message)s", date_fmt 151 | ) 152 | console_formatter = logging.Formatter( 153 | f"%(asctime)s - {program} - %(levelname)s - %(message)s", date_fmt 154 | ) 155 | # Create directory if not exists 156 | if not os.path.exists(f"{self.datadir}/logs"): 157 | os.makedirs(f"{self.datadir}/logs") 158 | 159 | # Log to file and rotate if needed 160 | file_handle = TimedRotatingFileHandler( 161 | filename=f"{self.datadir}/logs/{self.program}.log", backupCount=logstokeep, encoding='utf-8' 162 | ) 163 | file_handle.setFormatter(formatter) 164 | self.my_logger.addHandler(file_handle) 165 | 166 | # Log to console 167 | console_handle = logging.StreamHandler() 168 | console_handle.setLevel(logging.INFO) 169 | console_handle.setFormatter(console_formatter) 170 | self.my_logger.addHandler(console_handle) 171 | 172 | self.info(f"ZCrypto {program}") 173 | self.info("Started on %s" % time.strftime("%A %H:%M:%S %Y-%m-%d")) 174 | 175 | if self.notify_enabled: 176 | self.info("Notifications are enabled") 177 | else: 178 | self.info("Notifications are disabled") 179 | 180 | def log(self, message, level="info"): 181 | """Call the log levels.""" 182 | if level == "info": 183 | self.my_logger.info(message) 184 | elif level == "warning": 185 | self.my_logger.warning(message) 186 | elif level == "error": 187 | self.my_logger.error(message) 188 | elif level == "debug": 189 | self.my_logger.debug(message) 190 | 191 | def info(self, message, notify=False): 192 | """Info level.""" 193 | self.log(Fore.LIGHTWHITE_EX + message, "info") 194 | if self.notify_enabled and notify: 195 | self.notificationhandler.queue_notification(message) 196 | 197 | def warning(self, message, notify=True): 198 | """Warning level.""" 199 | self.log(Fore.LIGHTYELLOW_EX + message, "warning") 200 | if self.notify_enabled and notify: 201 | self.notificationhandler.queue_notification(message) 202 | 203 | def error(self, message, notify=True): 204 | """Error level.""" 205 | self.log(Fore.LIGHTRED_EX + message, "error") 206 | if self.notify_enabled and notify: 207 | self.notificationhandler.queue_notification(message) 208 | 209 | def debug(self, message, notify=False): 210 | """Debug level.""" 211 | self.log(message, "debug") 212 | if self.notify_enabled and notify: 213 | self.notificationhandler.queue_notification(message) 214 | -------------------------------------------------------------------------------- /helpers/badcoins.py: -------------------------------------------------------------------------------- 1 | # manipulated_coins = ["YFIUSDT", 2 | # "UNIUSDT", 3 | # "AAVEUSDT", 4 | # "C98USDT", 5 | # "USTCUSDT", 6 | # "BNBUSDT", 7 | # "TRUUSDT", 8 | # "CAKEUSDT", 9 | # "FISUSDT", 10 | # "RUNEUSDT", 11 | # "SHIBUSDT", 12 | # "BZRXUSDT", 13 | # "DEXEUSDT", 14 | # "MFTUSDT", 15 | # "WBTCUSDT", 16 | # "WINUSDT", 17 | # "HEGICUSDT", 18 | # "XVSUSDT", 19 | # "AKROUSDT", 20 | # "COMPUSDT", 21 | # "PERPUSDT", 22 | # "RAMPUSDT", 23 | # "SRMUSDT", 24 | # "RAYUSDT", 25 | # "MIRUSDT", 26 | # "UMAUSDT", 27 | # "DFUSDT", 28 | # "GVTUSDT", 29 | # "FARMUSDT", 30 | # "MLNUSDT", 31 | # "RENUSDT", 32 | # "TROYUSDT", 33 | # "BETAUSDT", 34 | # "KAVAUSDT", 35 | # "REEFUSDT", 36 | # "BONDUSDT", 37 | # "OMUSDT", 38 | # "ALPHAUSDT", 39 | # "SUNUSDT", 40 | # "BADGERUSDT", 41 | # "ALPACAUSDT", 42 | # "DYDXUSDT", 43 | # "SAFEMOONUSDT", 44 | # "BELUSDT", 45 | # "IDEXUSDT", 46 | # "KCSUSDT", 47 | # "USTUSDT", 48 | # "BUSDUSDT", 49 | # "USDCUSDT", 50 | # "TUSDUSDT", 51 | # "USDPUSDT", 52 | # "EURUSDT", 53 | # "AUDUSDT", 54 | # "SHIBUSDT", 55 | # "GBPUSDT", 56 | # "SUSDUSDT", 57 | # "EURTUSDT", 58 | # "DIAUSDT", 59 | # "MIMUSDT", 60 | # "FRAXUSDT", 61 | # "SUSHIUSDT", 62 | # "LUSDUSDT", 63 | # "NUSDUSDT", 64 | # "XUATUSDT", 65 | # "HUSDUSDT", 66 | # "OUSDUSDT", 67 | # "ALUSDUSDT", 68 | # "GUSDUSDT", 69 | # "CUSDUSDT", 70 | # "MUSDUSDT", 71 | # "CEURUSDT", 72 | # "USDKUSDT", 73 | # "RSVUSDT", 74 | # "EOSDTUSDT", 75 | # "SAUDUSDT", 76 | # "BITCNYUSDT", 77 | # "YFIIUSDT", 78 | # "ESDUSDT", 79 | # "PARUSDT", 80 | # "DUSDUSDT", 81 | # "DSDUSDT", 82 | # "USDAPUSDT", 83 | # "ARTHUSDT", 84 | # "ZUSDUSDT", 85 | # "WUSDUSDT", 86 | # "ONCUSDT", 87 | # "DEBASEUSDT", 88 | # "BSDUSDT", 89 | # "SACUSDT", 90 | # "COUSDUSDT", 91 | # "NZDSUSDT", 92 | # "USDUSDT", 93 | # "XDAIUSDT", 94 | # "FLEXUSDUSDT", 95 | # "BDOUSDT", 96 | # "THKDUSDT", 97 | # "USNBTUSDT", 98 | # "IRONUSDT", 99 | # "WUSDUSDT", 100 | # "USDOUSDT", 101 | # "QCUSDT", 102 | # "JEURUSDT", 103 | # "JGBPUSDT", 104 | # "JCHFUSDT", 105 | # "MALTUSDT", 106 | # "XUSDUSDT", 107 | # "IRONUSDT", 108 | # "USDSUSDT", 109 | # "OUSDUSDT", 110 | # "JPYCUSDT", 111 | # "HBTCUSDT", 112 | # "WBTCUSDT", 113 | # "STETHUSDT", 114 | # "BNBUSDT", 115 | # "MKRUSDT", 116 | # "CETHUSDT", 117 | # "CREAMUSDT", 118 | # "CDAIUSDT", 119 | # "TUSDT", 120 | # "BCCUSDT", 121 | # "BTTCUSDT", 122 | # "YFIBUSD", 123 | # "UNIBUSD", 124 | # "AAVEBUSD", 125 | # "C98BUSD", 126 | # "BNBBUSD", 127 | # "TRUBUSD", 128 | # "CAKEBUSD", 129 | # "FISBUSD", 130 | # "RUNEBUSD", 131 | # "SHIBBUSD", 132 | # "BZRXBUSD", 133 | # "DEXEBUSD", 134 | # "MFTBUSD", 135 | # "WBTCBUSD", 136 | # "WINBUSD", 137 | # "HEGICBUSD", 138 | # "XVSBUSD", 139 | # "AKROBUSD", 140 | # "COMPBUSD", 141 | # "PERPBUSD", 142 | # "RAMPBUSD", 143 | # "SRMBUSD", 144 | # "USTCBUSD", 145 | # "RAYBUSD", 146 | # "MIRBUSD", 147 | # "UMABUSD", 148 | # "DFBUSD", 149 | # "GVTBUSD", 150 | # "FARMBUSD", 151 | # "MLNBUSD", 152 | # "RENBUSD", 153 | # "TROYBUSD", 154 | # "BETABUSD", 155 | # "KAVABUSD", 156 | # "REEFBUSD", 157 | # "BONDBUSD", 158 | # "OMBUSD", 159 | # "ALPHABUSD", 160 | # "SUNBUSD", 161 | # "BADGERBUSD", 162 | # "ALPACABUSD", 163 | # "DYDXBUSD", 164 | # "SAFEMOONBUSD", 165 | # "BELBUSD", 166 | # "IDEXBUSD", 167 | # "KCSBUSD", 168 | # "USTBUSD", 169 | # "BUSDBUSD", 170 | # "USDCBUSD", 171 | # "TUSDBUSD", 172 | # "USDPBUSD", 173 | # "EURBUSD", 174 | # "AUDBUSD", 175 | # "SHIBBUSD", 176 | # "GBPBUSD", 177 | # "SUSDBUSD", 178 | # "EURTBUSD", 179 | # "DIABUSD", 180 | # "MIMBUSD", 181 | # "FRAXBUSD", 182 | # "SUSHIBUSD", 183 | # "LUSDBUSD", 184 | # "NUSDBUSD", 185 | # "XUATBUSD", 186 | # "HUSDBUSD", 187 | # "OUSDBUSD", 188 | # "ALUSDBUSD", 189 | # "GUSDBUSD", 190 | # "CUSDBUSD", 191 | # "MUSDBUSD", 192 | # "CEURBUSD", 193 | # "USDKBUSD", 194 | # "RSVBUSD", 195 | # "EOSDTBUSD", 196 | # "SAUDBUSD", 197 | # "BITCNYBUSD", 198 | # "YFIIBUSD", 199 | # "ESDBUSD", 200 | # "PARBUSD", 201 | # "DUSDBUSD", 202 | # "DSDBUSD", 203 | # "USDAPBUSD", 204 | # "ARTHBUSD", 205 | # "ZUSDBUSD", 206 | # "WUSDBUSD", 207 | # "ONCBUSD", 208 | # "DEBASEBUSD", 209 | # "BSDBUSD", 210 | # "SACBUSD", 211 | # "COUSDBUSD", 212 | # "NZDSBUSD", 213 | # "USDBUSD", 214 | # "XDAIBUSD", 215 | # "FLEXUSDBUSD", 216 | # "BDOBUSD", 217 | # "THKDBUSD", 218 | # "USNBTBUSD", 219 | # "IRONBUSD", 220 | # "WUSDBUSD", 221 | # "USDOBUSD", 222 | # "QCBUSD", 223 | # "JEURBUSD", 224 | # "JGBPBUSD", 225 | # "JCHFBUSD", 226 | # "MALTBUSD", 227 | # "XUSDBUSD", 228 | # "IRONBUSD", 229 | # "USDSBUSD", 230 | # "OUSDBUSD", 231 | # "JPYCBUSD", 232 | # "HBTCBUSD", 233 | # "WBTCBUSD", 234 | # "STETHBUSD", 235 | # "BNBBUSD", 236 | # "MKRBUSD", 237 | # "CETHBUSD", 238 | # "CREAMBUSD", 239 | # "CDAIBUSD", 240 | # "TBUSD", 241 | # "BCCBUSD", 242 | # "BTTCUSDT"] 243 | 244 | manipulated_coins = ["ICXUSDT", "LDOUSDT", "EPSUSDT", "BCHSVUSDT", "USDPUSDT", "GALAXUSDT", "OLEUSDT", "FORTUSDT", 245 | "CSPRUSDT", "REV3LUSDT", "CULTUSDT", "WEMIXUSDT", "WELLUSDT"] 246 | -------------------------------------------------------------------------------- /Smart_Trade_with_RR.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ZCrypto 3commas helpers.""" 3 | import argparse 4 | import configparser 5 | import os 6 | import sys 7 | import time 8 | from pathlib import Path 9 | from telethon import TelegramClient, events 10 | from binance.client import Client, BinanceAPIException 11 | from helpers.misc import binance_pair, futures_pair, spot_pair 12 | from helpers.logging import Logger, NotificationHandler 13 | from helpers.threecommas import ( 14 | get_threecommas_account_balance, 15 | get_threecommas_account_marketcode, 16 | init_threecommas_api, 17 | smart_lev_trades_risk_reward, 18 | smart_spot_trades_risk_reward, 19 | panic_close_smart, 20 | get_active_smart_trades, 21 | ) 22 | 23 | 24 | def load_config(): 25 | """Create default or load existing config file.""" 26 | 27 | cfg = configparser.ConfigParser() 28 | if cfg.read(f"{datadir}/{program}.ini"): 29 | return cfg 30 | 31 | cfg["settings"] = { 32 | "timezone": "Africa/Cairo", 33 | "debug": False, 34 | "logrotate": 7, 35 | "3c-apikey": "Your 3Commas API Key", 36 | "3c-apisecret": "Your 3Commas API Secret", 37 | "accounts": 0000000, 38 | "risk-per-trade": 50, 39 | "risk_reward-1": 1.0, 40 | "risk_reward-2": 2.4, 41 | "vol1": 2.0, 42 | "vol2": 98, 43 | "break_even": True, 44 | "leverage": 1, 45 | } 46 | cfg["telegram"] = { 47 | "tgram-phone-number": "Your Telegram Phone number", 48 | "tgram-channel": "Telegram Channel to watch", 49 | "tgram-api-id": "Your Telegram API ID", 50 | "tgram-api-hash": "Your Telegram API Hash", 51 | "notifications": False, 52 | "notify-urls": ["notify-url1"], 53 | } 54 | 55 | with open(f"{datadir}/{program}.ini", "w") as cfgfile: 56 | cfg.write(cfgfile) 57 | 58 | return None 59 | 60 | 61 | def smart_trade(cfg, side, pair, price, inputstoploss): 62 | accounts = (cfg.get("settings", "accounts")).split(",") 63 | risk = float(cfg.get("settings", "risk-per-trade")) 64 | lev = int(cfg.get("settings", "leverage")) 65 | rr1 = float(cfg.get("settings", "risk_reward-1")) 66 | rr2 = float(cfg.get("settings", "risk_reward-2")) 67 | side = side 68 | inputstoploss = inputstoploss 69 | coin = pair 70 | try: 71 | check_price = clients.get_symbol_ticker(symbol=binance_pair(pair))["price"] 72 | pair_price = price 73 | print(pair_price) 74 | except BinanceAPIException as e: 75 | logger.info(f"{e.message} is Invalid symbol {e.status_code}", True) 76 | return 77 | 78 | # Do we want Stop loss ?! 79 | if inputstoploss == 0: 80 | stop = True 81 | else: 82 | stop = False 83 | if side == "CLOSE": 84 | panic_close_smart(config, logger, api, get_active_smart_trades(config, logger, api)) 85 | else: 86 | if side == "SHORT": 87 | types = "sell" 88 | if stop: 89 | stoploss = 0 90 | else: 91 | stoploss = float(pair_price) + (float(pair_price) * (inputstoploss / 100)) 92 | tp1 = float(pair_price) - ((float(stoploss) - float(pair_price)) * rr1) 93 | tp2 = float(pair_price) - ((float(stoploss) - float(pair_price)) * rr2) 94 | 95 | else: 96 | types = "buy" 97 | if stop: 98 | stoploss = 0 99 | else: 100 | stoploss = float(pair_price) - (float(pair_price) * (inputstoploss / 100)) 101 | tp1 = float(pair_price) - ((float(stoploss) - float(pair_price)) * rr1) 102 | tp2 = float(pair_price) - ((float(stoploss) - float(pair_price)) * rr2) 103 | 104 | for account in accounts: 105 | market_code = get_threecommas_account_marketcode(logger, api, account) 106 | capital = float(get_threecommas_account_balance(logger, api, account)["usd_amount"]) 107 | if stop: 108 | capital_per_trade = risk * 10 109 | else: 110 | capital_per_trade = risk / inputstoploss * 100 111 | 112 | if market_code == "binance_futures": 113 | posvalue = capital_per_trade / float(pair_price) / lev 114 | pair = futures_pair(coin) 115 | smart_lev_trades_risk_reward(config, logger, api, pair, posvalue, lev, account, types, stoploss, tp1, 116 | tp2, pair_price) 117 | elif market_code == "binance_futures_coin": 118 | continue 119 | else: 120 | # no SHORT for SPOT 121 | if types == "sell": 122 | logger.error("Selling SPOT is not available ") 123 | continue 124 | 125 | else: 126 | posvalue = capital_per_trade / float(pair_price) 127 | pair = spot_pair(coin) 128 | smart_spot_trades_risk_reward(config, logger, api, pair, posvalue, account, stoploss, 129 | tp1, tp2, pair_price) 130 | 131 | 132 | # Start application 133 | program = Path(__file__).stem 134 | 135 | # Parse and interpret options. 136 | parser = argparse.ArgumentParser(description="Zcrypto Smart-Trade telegram.") 137 | parser.add_argument("-d", "--datadir", help="data directory to use", type=str) 138 | parser.add_argument("-b", "--blacklist", help="blacklist to use", type=str) 139 | 140 | args = parser.parse_args() 141 | if args.datadir: 142 | datadir = args.datadir 143 | else: 144 | datadir = os.getcwd() 145 | 146 | # pylint: disable-msg=C0103 147 | if args.blacklist: 148 | blacklistfile = f"{datadir}/{args.blacklist}" 149 | else: 150 | blacklistfile = "" 151 | 152 | # Create or load configuration file 153 | config = load_config() 154 | if not config: 155 | logger = Logger(datadir, program, None, 7, False, False) 156 | logger.info( 157 | f"Created example config file '{program}.ini', edit it and restart the program" 158 | ) 159 | sys.exit(0) 160 | else: 161 | # Handle timezone 162 | if hasattr(time, "tzset"): 163 | os.environ["TZ"] = config.get( 164 | "settings", "timezone", fallback="Africa/Cairo" 165 | ) 166 | time.tzset() 167 | 168 | # Init notification handler 169 | notification = NotificationHandler( 170 | program, 171 | config.getboolean("telegram", "notifications"), 172 | config.get("telegram", "notify-urls"), 173 | ) 174 | 175 | # Initialise logging 176 | logger = Logger( 177 | datadir, 178 | program, 179 | notification, 180 | int(config.get("settings", "logrotate", fallback=7)), 181 | config.getboolean("settings", "debug"), 182 | config.getboolean("telegram", "notifications"), 183 | ) 184 | 185 | # Initialize 3Commas API 186 | api = init_threecommas_api(config) 187 | 188 | # Watchlist telegram trigger 189 | # noinspection PyTypeChecker 190 | client = TelegramClient( 191 | f"{datadir}/{program}", 192 | config.get("telegram", "tgram-api-id"), 193 | config.get("telegram", "tgram-api-hash"), 194 | ).start(config.get("telegram", "tgram-phone-number")) 195 | 196 | 197 | @client.on(events.NewMessage(chats=config.get("telegram", "tgram-channel"))) 198 | async def callback(event): 199 | """Parse Telegram message.""" 200 | logger.info( 201 | "Received telegram message: '%s'" % event.message.text.replace("\n", " - "), 202 | True, 203 | ) 204 | 205 | trigger = event.raw_text.splitlines() 206 | 207 | if trigger[0] == "LONG" or trigger[0] == "SHORT" or trigger[0] == "CLOSE": 208 | try: 209 | if trigger[0] == "CLOSE": 210 | coin = "BNB" 211 | price = 0 212 | stop = 0 213 | else: 214 | 215 | coin = trigger[1] 216 | price = float(trigger[2]) 217 | stop = float(trigger[3]) 218 | smart_trade(config, trigger[0], coin, price, stop) 219 | 220 | except IndexError: 221 | logger.info("--- Invalid trigger message format! ---\n\nMessage format " 222 | "LONG/SHORT\n" 223 | "COIN\n" 224 | "PRICE\n" 225 | "STOPLOSS\n" 226 | "\n------------\n" 227 | "Example\n" 228 | "------------\n" 229 | "LONG\n" 230 | "BNB\n" 231 | "250\n" 232 | "2", 233 | True, 234 | ) 235 | return 236 | except ValueError: 237 | logger.info("--- Invalid trigger message format! ---\n\nMessage format " 238 | "LONG/SHORT\n" 239 | "COIN\n" 240 | "PRICE\n" 241 | "STOPLOSS\n" 242 | "\n------------\n" 243 | "Example\n" 244 | "------------\n" 245 | "LONG\n" 246 | "BNB\n" 247 | "250\n" 248 | "2", 249 | True, 250 | ) 251 | return 252 | 253 | if trigger[0] not in ('LONG', 'SHORT', 'CLOSE'): 254 | logger.debug(f"Trade type '{trigger[0]}' is not supported yet!") 255 | return 256 | else: 257 | logger.info("--- Not a crypto trigger message ---\n\nMessage format " 258 | "LONG/SHORT\n" 259 | "COIN\n" 260 | "PRICE\n" 261 | "STOPLOSS " 262 | "\n------------\n" 263 | "Example\n" 264 | "------------\n" 265 | "LONG\n" 266 | "BNB\n" 267 | "250\n" 268 | "2", 269 | True, 270 | ) 271 | 272 | notification.send_notification() 273 | 274 | 275 | clients = Client() 276 | # Start telegram client 277 | client.start() 278 | logger.info( 279 | "Listening to telegram chat '%s' for triggers" 280 | % config.get("telegram", "tgram-channel"), 281 | True, 282 | ) 283 | 284 | client.run_until_disconnected() 285 | -------------------------------------------------------------------------------- /Smart_Trade_fixed_TPs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ZCrypto 3commas helpers.""" 3 | import argparse 4 | import configparser 5 | import os 6 | import sys 7 | import time 8 | from pathlib import Path 9 | from telethon import TelegramClient, events 10 | from binance.client import Client, BinanceAPIException 11 | from helpers.misc import binance_pair, futures_pair, spot_pair 12 | from helpers.logging import Logger, NotificationHandler 13 | from helpers.threecommas import ( 14 | get_threecommas_account_balance, 15 | get_threecommas_account_marketcode, 16 | init_threecommas_api, 17 | smart_lev_trades, 18 | smart_spot_trades, 19 | panic_close_smart, 20 | get_active_smart_trades, 21 | ) 22 | 23 | 24 | def load_config(): 25 | """Create default or load existing config file.""" 26 | 27 | cfg = configparser.ConfigParser() 28 | if cfg.read(f"{datadir}/{program}.ini"): 29 | return cfg 30 | 31 | cfg["settings"] = { 32 | "timezone": "Africa/Cairo", 33 | "debug": False, 34 | "logrotate": 7, 35 | "3c-apikey": "Your 3Commas API Key", 36 | "3c-apisecret": "Your 3Commas API Secret", 37 | "accounts": 31905461, 38 | "risk-of-trade": 1.0, 39 | "tp1": 2.5, 40 | "tp2": 5.0, 41 | "tp3": 7.5, 42 | "tp4": 10.0, 43 | "tp5": 12.0, 44 | "trailing-percent": 10.5, 45 | "leverage": 1, 46 | } 47 | cfg["telegram"] = { 48 | "tgram-phone-number": "Your Telegram Phone number", 49 | "tgram-channel": "Telegram Channel to watch", 50 | "tgram-api-id": "Your Telegram API ID", 51 | "tgram-api-hash": "Your Telegram API Hash", 52 | "notifications": False, 53 | "notify-urls": ["notify-url1"], 54 | } 55 | 56 | with open(f"{datadir}/{program}.ini", "w") as cfgfile: 57 | cfg.write(cfgfile) 58 | 59 | return None 60 | 61 | 62 | def smart_trade(cfg, side, inputstoploss, pair): 63 | accounts = (cfg.get("settings", "accounts")).split(",") 64 | risk = float(cfg.get("settings", "risk-of-trade")) 65 | lev = int(cfg.get("settings", "leverage")) 66 | take_profit1 = float(cfg.get("settings", "tp1")) 67 | take_profit2 = float(cfg.get("settings", "tp2")) 68 | take_profit3 = float(cfg.get("settings", "tp3")) 69 | take_profit4 = float(cfg.get("settings", "tp4")) 70 | take_profit5 = float(cfg.get("settings", "tp5")) 71 | side = side 72 | inputstoploss = inputstoploss 73 | coin = pair 74 | try: 75 | pair_price = clients.get_symbol_ticker(symbol=binance_pair(pair))["price"] 76 | except BinanceAPIException as e: 77 | logger.info(f"{e.message} is Invalid symbol {e.status_code}", True) 78 | return 79 | 80 | # Do we want Stop loss ?! 81 | if inputstoploss == 0: 82 | stop = True 83 | else: 84 | stop = False 85 | if side == "CLOSE": 86 | panic_close_smart(config, logger, api, get_active_smart_trades(config, logger, api)) 87 | else: 88 | if side == "SHORT": 89 | types = "sell" 90 | if stop: 91 | stoploss = 0 92 | else: 93 | stoploss = float(pair_price) + (float(pair_price) * (inputstoploss / 100)) 94 | tp1 = float(pair_price) - (float(pair_price) * (take_profit1 / 100)) 95 | tp2 = float(pair_price) - (float(pair_price) * (take_profit2 / 100)) 96 | tp3 = float(pair_price) - (float(pair_price) * (take_profit3 / 100)) 97 | tp4 = float(pair_price) - (float(pair_price) * (take_profit4 / 100)) 98 | tp5 = float(pair_price) - (float(pair_price) * (take_profit5 / 100)) 99 | 100 | else: 101 | types = "buy" 102 | if stop: 103 | stoploss = 0 104 | else: 105 | stoploss = float(pair_price) - (float(pair_price) * (inputstoploss / 100)) 106 | tp1 = float(pair_price) + (float(pair_price) * (take_profit1 / 100)) 107 | tp2 = float(pair_price) + (float(pair_price) * (take_profit2 / 100)) 108 | tp3 = float(pair_price) + (float(pair_price) * (take_profit3 / 100)) 109 | tp4 = float(pair_price) + (float(pair_price) * (take_profit4 / 100)) 110 | tp5 = float(pair_price) + (float(pair_price) * (take_profit5 / 100)) 111 | for account in accounts: 112 | market_code = get_threecommas_account_marketcode(logger, api, account) 113 | capital = float(get_threecommas_account_balance(logger, api, account)["usd_amount"]) 114 | risk_percentage = capital * (risk / 100) 115 | if stop: 116 | capital_per_trade = risk_percentage * 10 117 | else: 118 | capital_per_trade = risk_percentage / inputstoploss * 100 119 | 120 | if market_code == "binance_futures": 121 | capital = capital * (risk / 100) / lev 122 | posvalue = capital_per_trade / float(pair_price) 123 | pair = futures_pair(coin) 124 | smart_lev_trades(config, logger, api, pair, posvalue, lev, account, types, stoploss, tp1, tp2, tp3, tp4, 125 | tp5) 126 | 127 | elif market_code == "binance_futures_coin": 128 | continue 129 | else: 130 | # no SHORT for SPOT 131 | if types == "sell": 132 | logger.error("Selling SPOT is not available ") 133 | continue 134 | 135 | else: 136 | capital = capital * (risk / 100) 137 | posvalue = capital_per_trade / float(pair_price) 138 | pair = spot_pair(coin) 139 | smart_spot_trades(config, logger, api, pair, posvalue, account, stoploss, tp1, tp2, tp3, tp4, tp5) 140 | 141 | 142 | # Start application 143 | program = Path(__file__).stem 144 | 145 | # Parse and interpret options. 146 | parser = argparse.ArgumentParser(description="Zcrypto Smart-Trade telegram.") 147 | parser.add_argument("-d", "--datadir", help="data directory to use", type=str) 148 | parser.add_argument("-b", "--blacklist", help="blacklist to use", type=str) 149 | 150 | args = parser.parse_args() 151 | if args.datadir: 152 | datadir = args.datadir 153 | else: 154 | datadir = os.getcwd() 155 | 156 | # pylint: disable-msg=C0103 157 | if args.blacklist: 158 | blacklistfile = f"{datadir}/{args.blacklist}" 159 | else: 160 | blacklistfile = "" 161 | 162 | # Create or load configuration file 163 | config = load_config() 164 | if not config: 165 | logger = Logger(datadir, program, None, 7, False, False) 166 | logger.info( 167 | f"Created example config file '{program}.ini', edit it and restart the program" 168 | ) 169 | sys.exit(0) 170 | else: 171 | # Handle timezone 172 | if hasattr(time, "tzset"): 173 | os.environ["TZ"] = config.get( 174 | "settings", "timezone", fallback="Africa/Cairo" 175 | ) 176 | time.tzset() 177 | 178 | # Init notification handler 179 | notification = NotificationHandler( 180 | program, 181 | config.getboolean("telegram", "notifications"), 182 | config.get("telegram", "notify-urls"), 183 | ) 184 | 185 | # Initialise logging 186 | logger = Logger( 187 | datadir, 188 | program, 189 | notification, 190 | int(config.get("settings", "logrotate", fallback=7)), 191 | config.getboolean("settings", "debug"), 192 | config.getboolean("telegram", "notifications"), 193 | ) 194 | 195 | # Initialize 3Commas API 196 | api = init_threecommas_api(config) 197 | 198 | # Watchlist telegram trigger 199 | # noinspection PyTypeChecker 200 | client = TelegramClient( 201 | f"{datadir}/{program}", 202 | config.get("telegram", "tgram-api-id"), 203 | config.get("telegram", "tgram-api-hash"), 204 | ).start(config.get("telegram", "tgram-phone-number")) 205 | 206 | 207 | @client.on(events.NewMessage(chats=config.get("telegram", "tgram-channel"))) 208 | async def callback(event): 209 | """Parse Telegram message.""" 210 | logger.info( 211 | "Received telegram message: '%s'" % event.message.text.replace("\n", " - "), 212 | True, 213 | ) 214 | 215 | trigger = event.raw_text.splitlines() 216 | 217 | if trigger[0] == "LONG" or trigger[0] == "SHORT" or trigger[0] == "CLOSE": 218 | try: 219 | if trigger[0] == "CLOSE": 220 | coin = "BNB" 221 | stop = 0 222 | else: 223 | 224 | coin = trigger[1] 225 | stop = float(trigger[2]) 226 | smart_trade(config, trigger[0], stop, coin) 227 | 228 | except IndexError: 229 | logger.info("--- Invalid trigger message format! ---\n\nMessage format " 230 | "LONG/SHORT " 231 | "COIN " 232 | "STOPLOSS " 233 | "\n------------\n" 234 | "Example\n" 235 | "------------\n" 236 | "LONG\n" 237 | "BNB\n" 238 | "2", 239 | True, 240 | ) 241 | return 242 | except ValueError: 243 | logger.info("--- Invalid trigger message format! ---\n\nMessage format " 244 | "LONG/SHORT " 245 | "COIN " 246 | "STOPLOSS " 247 | "\n------------\n" 248 | "Example\n" 249 | "------------\n" 250 | "LONG\n" 251 | "BNB\n" 252 | "2", 253 | True, 254 | ) 255 | return 256 | 257 | if trigger[0] not in ('LONG', 'SHORT', 'CLOSE'): 258 | logger.debug(f"Trade type '{trigger[0]}' is not supported yet!") 259 | return 260 | else: 261 | logger.info("--- Not a crypto trigger message ---\n\nMessage format " 262 | "LONG/SHORT " 263 | "COIN " 264 | "STOPLOSS " 265 | "\n------------\n" 266 | "Example\n" 267 | "------------\n" 268 | "LONG\n" 269 | "BNB\n" 270 | "2", 271 | True, 272 | ) 273 | 274 | notification.send_notification() 275 | 276 | 277 | clients = Client() 278 | # Start telegram client 279 | client.start() 280 | logger.info( 281 | "Listening to telegram chat '%s' for triggers" 282 | % config.get("telegram", "tgram-channel"), 283 | True, 284 | ) 285 | 286 | client.run_until_disconnected() 287 | -------------------------------------------------------------------------------- /helpers/misc.py: -------------------------------------------------------------------------------- 1 | """ZCrypto bot helpers.""" 2 | import datetime 3 | import time 4 | import re 5 | 6 | import requests 7 | from bs4 import BeautifulSoup 8 | 9 | PAIREXCLUDE_EXT = "pairexclude" 10 | 11 | 12 | def wait_time_interval(logger, notification, time_interval, notify=True): 13 | """Wait for time interval.""" 14 | 15 | if time_interval>0: 16 | localtime = time.time() 17 | nexttime = localtime + int(time_interval) 18 | timeresult = time.strftime("%H:%M:%S", time.localtime(nexttime)) 19 | logger.info( 20 | "Next update in %s at %s" % (str(datetime.timedelta(seconds=time_interval)), timeresult), notify 21 | ) 22 | notification.send_notification() 23 | time.sleep(time_interval) 24 | return True 25 | 26 | notification.send_notification() 27 | time.sleep(2) 28 | 29 | return False 30 | 31 | 32 | def futures_pair(pair): 33 | pair = "USDT_" + pair.upper() + "USDT" 34 | return pair 35 | 36 | 37 | def spot_pair(pair): 38 | pair = "USDT_" + pair.upper() 39 | return pair 40 | 41 | 42 | def back_to_binance(pair): 43 | """Convert a binance/tradingview formatted pair to threecommas format.""" 44 | a = pair.split("_") 45 | pair = a[1] + a[0] 46 | return pair 47 | 48 | 49 | def binance_pair(pair): 50 | pair = pair.upper() + "USDT" 51 | return pair 52 | 53 | 54 | def populate_pair_lists(pair, blacklist, blackpairs, badpairs, newpairs, tickerlist): 55 | """Create pair lists.""" 56 | 57 | # Check if pair is in tickerlist and on 3Commas blacklist 58 | if pair in tickerlist: 59 | if pair in blacklist: 60 | blackpairs.append(pair) 61 | else: 62 | newpairs.append(pair) 63 | else: 64 | badpairs.append(pair) 65 | 66 | 67 | def get_lunarcrush_data(logger, program, config, usdtbtcprice): 68 | """Get the top x GalaxyScore, AltRank or Volatile coins from LunarCrush.""" 69 | 70 | lccoins = {} 71 | lcapikey = config.get("settings", "lc-apikey") 72 | lcfetchlimit = config.get("settings", "lc-fetchlimit") 73 | 74 | # Construct query for LunarCrush data 75 | if "altrank" in program: 76 | parms = { 77 | "data": "market", 78 | "type": "fast", 79 | "sort": "acr", 80 | "limit": lcfetchlimit, 81 | "key": lcapikey, 82 | } 83 | elif "galaxyscore" in program: 84 | parms = { 85 | "data": "market", 86 | "type": "fast", 87 | "sort": "gs", 88 | "limit": lcfetchlimit, 89 | "key": lcapikey, 90 | "desc": True, 91 | } 92 | elif "volatility" in program: 93 | parms = { 94 | "data": "market", 95 | "type": "fast", 96 | "sort": "vt", 97 | "limit": lcfetchlimit, 98 | "key": lcapikey, 99 | "desc": True, 100 | } 101 | 102 | try: 103 | result = requests.get("https://api.lunarcrush.com/v2", params=parms) 104 | result.raise_for_status() 105 | data = result.json() 106 | 107 | if "data" in data.keys(): 108 | for i, crush in enumerate(data["data"], start=1): 109 | crush["categories"] = ( 110 | list(crush["categories"].split(",")) if crush["categories"] else [] 111 | ) 112 | crush["rank"] = i 113 | crush["volbtc"] = crush["v"] / float(usdtbtcprice) 114 | logger.debug( 115 | f"rank:{crush['rank']:3d} acr:{crush['acr']:4d} gs:{crush['gs']:3.1f} " 116 | f"s:{crush['s']:8s} '{crush['n']:25}' volume in btc:{crush['volbtc']:12.2f}" 117 | f" categories:{crush['categories']}" 118 | ) 119 | lccoins = data["data"] 120 | 121 | except requests.exceptions.HTTPError as err: 122 | logger.error("Fetching LunarCrush data failed with error: %s" % err) 123 | return {} 124 | 125 | logger.info("Fetched LunarCrush ranking OK (%s coins)" % (len(lccoins))) 126 | 127 | return lccoins 128 | 129 | 130 | def get_coinmarketcap_data(logger, cmc_apikey, start_number, limit): 131 | """Get the data from CoinMarketCap.""" 132 | 133 | cmcdict = {} 134 | 135 | # Construct query for CoinMarketCap data 136 | parms = { 137 | "start": start_number, 138 | "limit": limit, 139 | "convert": "BTC", 140 | "aux": "cmc_rank", 141 | } 142 | 143 | headrs = { 144 | "X-CMC_PRO_API_KEY": cmc_apikey, 145 | } 146 | 147 | try: 148 | result = requests.get( 149 | "https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest", 150 | params=parms, 151 | headers=headrs, 152 | ) 153 | result.raise_for_status() 154 | data = result.json() 155 | 156 | if "data" in data.keys(): 157 | for i, cmc in enumerate(data["data"], start=1): 158 | cmc["rank"] = i 159 | logger.debug( 160 | f"rank:{cmc['rank']:3d} cmc_rank:{cmc['cmc_rank']:3d} s:{cmc['symbol']:8} " 161 | f"'{cmc['name']:25}' volume_24h:{cmc['quote']['BTC']['volume_24h']:12.2f} " 162 | f"volume_change_24h:{cmc['quote']['BTC']['volume_change_24h']:5.2f} " 163 | f"market_cap:{cmc['quote']['BTC']['market_cap']:12.2f}" 164 | ) 165 | cmcdict = data["data"] 166 | 167 | except requests.exceptions.HTTPError as err: 168 | logger.error("Fetching CoinMarketCap data failed with error: %s" % err) 169 | return {} 170 | 171 | logger.info("Fetched CoinMarketCap data OK (%s coins)" % (len(cmcdict))) 172 | 173 | return cmcdict 174 | 175 | 176 | def check_deal(cursor, dealid): 177 | """Check if deal was already logged.""" 178 | 179 | return cursor.execute(f"SELECT * FROM deals WHERE dealid = {dealid}").fetchone() 180 | 181 | 182 | def format_pair(logger, marketcode, base, coin): 183 | """Format pair depending on exchange.""" 184 | 185 | # Construct pair based on bot settings (BTC stays BTC, but USDT can become BUSD) 186 | if marketcode == "binance_futures": 187 | pair = f"{base}_{coin}{base}" 188 | elif marketcode == "ftx_futures": 189 | pair = f"{base}_{coin}-PERP" 190 | else: 191 | pair = f"{base}_{coin}" 192 | 193 | logger.debug("New pair constructed: %s" % pair) 194 | 195 | return pair 196 | 197 | 198 | def get_round_digits(pair): 199 | """Get the number of digits for the round function.""" 200 | 201 | numberofdigits = 4 202 | 203 | if pair: 204 | base = pair.split("_")[0] 205 | 206 | if base in ("BTC", "BNB", "ETH"): 207 | numberofdigits = 8 208 | 209 | return numberofdigits 210 | 211 | 212 | def remove_prefix(text, prefix): 213 | """Get the string without prefix, required for Python < 3.9.""" 214 | 215 | if text.startswith(prefix): 216 | return text[len(prefix):] 217 | return text 218 | 219 | 220 | def get_botassist_data(logger, botassistlist, start_number, limit): 221 | """Get the top pairs from 3c-tools bot-assist explorer.""" 222 | 223 | url = "https://www.3c-tools.com/markets/bot-assist-explorer" 224 | parms = {"list": botassistlist} 225 | 226 | pairs = list() 227 | try: 228 | result = requests.get(url, params=parms) 229 | result.raise_for_status() 230 | soup = BeautifulSoup(result.text, features="html.parser") 231 | data = soup.find("table", class_="table table-striped table-sm") 232 | 233 | columncount = 0 234 | columndict = {} 235 | 236 | # Build list of columns we are interested in 237 | tablecolumns = data.find_all("th") 238 | for column in tablecolumns: 239 | if column.text not in ("#", "symbol"): 240 | columndict[columncount] = column.text 241 | 242 | columncount += 1 243 | 244 | tablerows = data.find_all("tr") 245 | for row in tablerows: 246 | rowcolums = row.find_all("td") 247 | if len(rowcolums)>0: 248 | rank = int(rowcolums[0].text) 249 | if rank0: 295 | newpairs.remove(pair) 296 | 297 | 298 | def load_bot_excluded_coins(logger, share_dir, bot_id, extension): 299 | """Load excluded coins from file, for the specified bot""" 300 | 301 | excludedlist = [] 302 | excludefilename = f"{share_dir}/{bot_id}.{extension}" 303 | 304 | try: 305 | with open(excludefilename, "r") as file: 306 | excludedlist = file.read().splitlines() 307 | if excludedlist: 308 | logger.info( 309 | "Reading exclude file '%s' OK (%s coins)" 310 | % (excludefilename, len(excludedlist)) 311 | ) 312 | except FileNotFoundError: 313 | logger.info( 314 | "Exclude file (%s) not found for bot '%s'; no coins to exclude." 315 | % (excludefilename, bot_id) 316 | ) 317 | 318 | return excludedlist 319 | 320 | 321 | def unix_timestamp_to_string(timestamp, date_time_format): 322 | """Convert the given timestamp to a readable date/time in the specified format""" 323 | 324 | return datetime.datetime.fromtimestamp(timestamp).strftime(date_time_format) 325 | 326 | 327 | def format_pair_three(pair): 328 | """Convert a binance/tradingview formatted pair to threecommas format.""" 329 | 330 | for coin in ["BTC", "USDT", "BUSD"]: 331 | x = re.search(f"{coin}$", pair) 332 | if x: 333 | y = re.split(f"{coin}$", pair) 334 | pair = f"{coin}_{y[0]}" 335 | 336 | return pair 337 | 338 | return None -------------------------------------------------------------------------------- /helpers/threecommas.py: -------------------------------------------------------------------------------- 1 | """ZCrypto bot helpers.""" 2 | from py3cw.request import Py3CW 3 | 4 | 5 | def load_blacklist(logger, api, blacklistfile): 6 | """Return blacklist data to be used.""" 7 | 8 | # Return file based blacklist 9 | if blacklistfile: 10 | newblacklist = [] 11 | try: 12 | with open(blacklistfile, "r") as file: 13 | newblacklist = file.read().splitlines() 14 | if newblacklist: 15 | logger.info( 16 | "Reading local blacklist file '%s' OK (%s pairs)" 17 | % (blacklistfile, len(newblacklist)) 18 | ) 19 | except FileNotFoundError: 20 | logger.error( 21 | "Reading local blacklist file '%s' failed with error: File not found" 22 | % blacklistfile 23 | ) 24 | 25 | return newblacklist 26 | 27 | # Return defined blacklist from 3Commaas 28 | return get_threecommas_blacklist(logger, api) 29 | 30 | 31 | def init_threecommas_api(cfg): 32 | """Init the 3commas API.""" 33 | 34 | return Py3CW( 35 | key=cfg.get("settings", "3c-apikey"), 36 | secret=cfg.get("settings", "3c-apisecret"), 37 | request_options={ 38 | "request_timeout": 10, 39 | "nr_of_retries": 3, 40 | "retry_status_codes": [502, 429], 41 | "retry_backoff_factor": 1, 42 | }, 43 | ) 44 | 45 | 46 | def get_threecommas_blacklist(logger, api): 47 | """Get the pair blacklist from 3Commas.""" 48 | 49 | newblacklist = list() 50 | error, data = api.request( 51 | entity="bots", 52 | action="pairs_black_list", 53 | ) 54 | if data: 55 | logger.info( 56 | "Fetched 3Commas pairs blacklist OK (%s pairs)" % len(data["pairs"]) 57 | ) 58 | newblacklist = data["pairs"] 59 | else: 60 | if "msg" in error: 61 | logger.error( 62 | "Fetching 3Commas pairs blacklist failed with error: %s" % error["msg"] 63 | ) 64 | else: 65 | logger.error("Fetching 3Commas pairs blacklist failed") 66 | 67 | return newblacklist 68 | 69 | 70 | def get_threecommas_btcusd(logger, api): 71 | """Get current USDT_BTC value to calculate BTC volume24h in USDT.""" 72 | 73 | price = 60000 74 | error, data = api.request( 75 | entity="accounts", 76 | action="currency_rates", 77 | payload={"market_code": "binance", "pair": "USDT_BTC"}, 78 | ) 79 | if data: 80 | logger.info("Fetched 3Commas BTC price OK (%s USDT)" % data["last"]) 81 | price = data["last"] 82 | else: 83 | if error and "msg" in error: 84 | logger.error( 85 | "Fetching 3Commas BTC price in USDT failed with error: %s" 86 | % error["msg"] 87 | ) 88 | else: 89 | logger.error("Fetching 3Commas BTC price in USDT failed") 90 | 91 | logger.debug("Current price of BTC is %s USDT" % price) 92 | return price 93 | 94 | 95 | def get_threecommas_accounts_paper(logger, api): 96 | """Get all data for an account.""" 97 | 98 | # Fetch all account data, in real mode 99 | error, data = api.request( 100 | entity="accounts", 101 | action="", 102 | additional_headers={"Forced-Mode": "paper"}, 103 | ) 104 | if data: 105 | return data 106 | 107 | if error and "msg" in error: 108 | logger.error("Fetching 3Commas accounts data failed error: %s" % error["msg"]) 109 | else: 110 | logger.error("Fetching 3Commas accounts data failed") 111 | 112 | return None 113 | 114 | 115 | def get_threecommas_accounts(logger, api): 116 | """Get all data for an account.""" 117 | 118 | # Fetch all account data, in real mode 119 | error, data = api.request( 120 | entity="accounts", 121 | action="", 122 | additional_headers={"Forced-Mode": "real"}, 123 | ) 124 | if data: 125 | return data 126 | 127 | if error and "msg" in error: 128 | logger.error("Fetching 3Commas accounts data failed error: %s" % error["msg"]) 129 | else: 130 | logger.error("Fetching 3Commas accounts data failed") 131 | 132 | return None 133 | 134 | 135 | def get_threecommas_account(logger, api, accountid): 136 | """Get account details.""" 137 | 138 | # Find account data for accountid, in real mode 139 | error, data = api.request( 140 | entity="accounts", 141 | action="account_info", 142 | action_id=str(accountid), 143 | additional_headers={"Forced-Mode": "real"}, 144 | ) 145 | if data: 146 | return data 147 | 148 | if error and "msg" in error: 149 | logger.error( 150 | "Fetching 3Commas account data failed for id %s error: %s" 151 | % (accountid, error["msg"]) 152 | ) 153 | else: 154 | logger.error("Fetching 3Commas account data failed for id %s", accountid) 155 | 156 | return None 157 | 158 | 159 | def get_threecommas_account_marketcode(logger, api, accountid): 160 | """Get market_code for account.""" 161 | 162 | # get account data for accountid, in real mode 163 | error, data = api.request( 164 | entity="accounts", 165 | action="account_info", 166 | action_id=str(accountid), 167 | additional_headers={"Forced-Mode": "real"}, 168 | ) 169 | if data: 170 | marketcode = data["market_code"] 171 | logger.info( 172 | "Fetched 3Commas account market code in real mode OK (%s)" % marketcode 173 | ) 174 | return marketcode 175 | 176 | if error and "msg" in error: 177 | logger.error( 178 | "Fetching 3Commas account market code failed for id %s error: %s" 179 | % (accountid, error["msg"]) 180 | ) 181 | else: 182 | logger.error("Fetching 3Commas account market code failed for id %s", accountid) 183 | 184 | return None 185 | 186 | 187 | def get_threecommas_account_balance(logger, api, accountid): 188 | """Get account balances.""" 189 | 190 | # Fetch account balance data for accountid, in real mode 191 | error, data = api.request( 192 | entity="accounts", 193 | action="load_balances", 194 | action_id=str(accountid), 195 | additional_headers={"Forced-Mode": "real"}, 196 | ) 197 | if data: 198 | return data 199 | 200 | if error and "msg" in error: 201 | logger.error( 202 | "Fetching 3Commas account balances data failed for id %s error: %s" 203 | % (accountid, error["msg"]) 204 | ) 205 | else: 206 | logger.error( 207 | "Fetching 3Commas account balances data failed for id %s", accountid 208 | ) 209 | 210 | return None 211 | 212 | 213 | def get_threecommas_account_balance_chart_data( 214 | logger, api, accountid, begindate, enddate 215 | ): 216 | """Get account balance chart data.""" 217 | 218 | # Fetch account balance chart data for accountid, in real mode 219 | error, data = api.request( 220 | entity="accounts", 221 | action="balance_chart_data", 222 | action_id=str(accountid), 223 | additional_headers={"Forced-Mode": "real"}, 224 | payload={"date_from": begindate, "date_to": enddate}, 225 | ) 226 | if data: 227 | return data 228 | 229 | if error and "msg" in error: 230 | logger.error( 231 | "Fetching 3Commas account balance chart data failed for id %s error: %s" 232 | % (accountid, error["msg"]) 233 | ) 234 | else: 235 | logger.error("Fetching 3Commas account balance chart data for id %s", accountid) 236 | 237 | return None 238 | 239 | 240 | def get_threecommas_market(logger, api, market_code): 241 | """Get all the valid pairs for market_code from 3Commas account.""" 242 | 243 | tickerlist = [] 244 | error, data = api.request( 245 | entity="accounts", 246 | action="market_pairs", 247 | payload={"market_code": market_code}, 248 | ) 249 | if data: 250 | tickerlist = data 251 | logger.info( 252 | "Fetched 3Commas market data for '%s' OK (%s pairs)" 253 | % (market_code, len(tickerlist)) 254 | ) 255 | else: 256 | if error and "msg" in error: 257 | logger.error( 258 | "Fetching 3Commas market data failed for market code %s with error: %s" 259 | % (market_code, error["msg"]) 260 | ) 261 | else: 262 | logger.error( 263 | "Fetching 3Commas market data failed for market code %s", market_code 264 | ) 265 | 266 | return tickerlist 267 | 268 | 269 | def set_threecommas_ss(logger, api, thebot, raw_pairs, ss, notify=True, notify_uptodate=True): 270 | """Update bot with ss.""" 271 | 272 | error, data = api.request( 273 | entity="bots", 274 | action="update", 275 | action_id=str(thebot["id"]), 276 | payload={ 277 | "name": str(thebot["name"]), 278 | "pairs": str(thebot["pairs"]), 279 | "base_order_volume": float(thebot["base_order_volume"]), 280 | "take_profit": float(thebot["take_profit"]), 281 | "safety_order_volume": float(thebot["safety_order_volume"]), 282 | "martingale_volume_coefficient": float( 283 | thebot["martingale_volume_coefficient"] 284 | ), 285 | "martingale_step_coefficient": ss, 286 | "max_safety_orders": int(thebot["max_safety_orders"]), 287 | "max_active_deals": int(thebot["max_active_deals"]), 288 | "active_safety_orders_count": int(thebot["active_safety_orders_count"]), 289 | "safety_order_step_percentage": float( 290 | thebot["safety_order_step_percentage"] 291 | ), 292 | "take_profit_type": thebot["take_profit_type"], 293 | "strategy_list": thebot["strategy_list"], 294 | "leverage_type": thebot["leverage_type"], 295 | "leverage_custom_value": thebot["leverage_custom_value"], 296 | "bot_id": int(thebot["id"]), 297 | }, 298 | ) 299 | if data: 300 | logger.debug("Bot pair(s) updated: %s" % data) 301 | if ss: 302 | newss = round(ss, 2) 303 | logger.info( 304 | "Bot '%s' with id '%s' updated with ss '%s'" 305 | % (thebot["name"], thebot["id"], newss), 306 | notify, 307 | ) 308 | # else: 309 | # logger.info( 310 | # "Bot '%s' with id '%s' updated with %d SS" 311 | # % ( 312 | # thebot["name"], 313 | # thebot["id"], 314 | # ss 315 | # ), 316 | # notify, 317 | # ) 318 | 319 | else: 320 | if error and "msg" in error: 321 | logger.error( 322 | "Error occurred while updating bot '%s' error: %s" 323 | % (thebot["name"], error["msg"]), 324 | True, 325 | ) 326 | else: 327 | logger.error( 328 | "Error occurred while updating bot '%s'" % thebot["name"], 329 | True, 330 | ) 331 | 332 | 333 | def set_threecommas_bot_pairs(logger, api, thebot, newpairs, newmaxdeals, notify=True, notify_uptodate=True): 334 | """Update bot with new pairs.""" 335 | 336 | # Do we already use these pairs? 337 | if newpairs == thebot["pairs"]: 338 | logger.info( 339 | "Bot '%s' with id '%s' is already using the new pair(s)" 340 | % (thebot["name"], thebot["id"]), 341 | notify_uptodate, 342 | ) 343 | return 344 | 345 | if not newmaxdeals: 346 | maxactivedeals = thebot["max_active_deals"] 347 | else: 348 | maxactivedeals = newmaxdeals 349 | 350 | logger.debug("Current pair(s): %s\nNew pair(s): %s" % (thebot["pairs"], newpairs)) 351 | 352 | error, data = api.request( 353 | entity="bots", 354 | action="update", 355 | action_id=str(thebot["id"]), 356 | payload={ 357 | "name": str(thebot["name"]), 358 | "pairs": newpairs, 359 | "base_order_volume": float(thebot["base_order_volume"]), 360 | "take_profit": float(thebot["take_profit"]), 361 | "safety_order_volume": float(thebot["safety_order_volume"]), 362 | "martingale_volume_coefficient": float( 363 | thebot["martingale_volume_coefficient"] 364 | ), 365 | "martingale_step_coefficient": float(thebot["martingale_step_coefficient"]), 366 | "max_safety_orders": int(thebot["max_safety_orders"]), 367 | "max_active_deals": int(maxactivedeals), 368 | "active_safety_orders_count": int(thebot["active_safety_orders_count"]), 369 | "safety_order_step_percentage": float( 370 | thebot["safety_order_step_percentage"] 371 | ), 372 | "take_profit_type": thebot["take_profit_type"], 373 | "strategy_list": thebot["strategy_list"], 374 | "leverage_type": thebot["leverage_type"], 375 | "leverage_custom_value": thebot["leverage_custom_value"], 376 | "bot_id": int(thebot["id"]), 377 | }, 378 | ) 379 | if data: 380 | logger.debug("Bot pair(s) updated: %s" % data) 381 | if len(newpairs) == 1: 382 | logger.info( 383 | "Bot '%s' with id '%s' updated with pair '%s'" 384 | % (thebot["name"], thebot["id"], newpairs[0]), 385 | notify, 386 | ) 387 | else: 388 | logger.info( 389 | "Bot '%s' with id '%s' updated with %d pairs %s" 390 | % ( 391 | thebot["name"], 392 | thebot["id"], 393 | len(newpairs), 394 | newpairs 395 | ), 396 | notify, 397 | ) 398 | if newmaxdeals: 399 | logger.info( 400 | "Max active deals changed to %s" % newmaxdeals, 401 | notify, 402 | ) 403 | else: 404 | if error and "msg" in error: 405 | logger.error( 406 | "Error occurred while updating bot '%s' error: %s" 407 | % (thebot["name"], error["msg"]), 408 | True, 409 | ) 410 | else: 411 | logger.error( 412 | "Error occurred while updating bot '%s'" % thebot["name"], 413 | True, 414 | ) 415 | 416 | 417 | def set_threecommas_bot_hmss(logger, api, thebot, newpairs, newmaxdeals, pdi, notify=True, notify_uptodate=True): 418 | """Update bot with new pairs.""" 419 | 420 | # Do we already use these pairs? 421 | if newpairs == thebot["pairs"]: 422 | if pdi: 423 | pdi_new = round(pdi, 2) 424 | logger.info( 425 | "Bot '%s' with id '%s' is already using the new pair(s) and SS of '%s" 426 | % (thebot["name"], thebot["id"], pdi_new), 427 | notify_uptodate, 428 | ) 429 | return 430 | 431 | if not newmaxdeals: 432 | maxactivedeals = thebot["max_active_deals"] 433 | else: 434 | maxactivedeals = newmaxdeals 435 | 436 | logger.debug("Current pair(s): %s\nNew pair(s): %s" % (thebot["pairs"], newpairs)) 437 | 438 | error, data = api.request( 439 | entity="bots", 440 | action="update", 441 | action_id=str(thebot["id"]), 442 | payload={ 443 | "name": str(thebot["name"]), 444 | "pairs": newpairs, 445 | "base_order_volume": float(thebot["base_order_volume"]), 446 | "take_profit": float(thebot["take_profit"]), 447 | "safety_order_volume": float(thebot["safety_order_volume"]), 448 | "martingale_volume_coefficient": float( 449 | thebot["martingale_volume_coefficient"] 450 | ), 451 | "martingale_step_coefficient": pdi, 452 | "max_safety_orders": int(thebot["max_safety_orders"]), 453 | "max_active_deals": int(maxactivedeals), 454 | "active_safety_orders_count": int(thebot["active_safety_orders_count"]), 455 | "safety_order_step_percentage": float( 456 | thebot["safety_order_step_percentage"] 457 | ), 458 | "take_profit_type": thebot["take_profit_type"], 459 | "strategy_list": thebot["strategy_list"], 460 | "leverage_type": thebot["leverage_type"], 461 | "leverage_custom_value": thebot["leverage_custom_value"], 462 | "bot_id": int(thebot["id"]), 463 | }, 464 | ) 465 | if data: 466 | logger.debug("Bot pair(s) updated: %s" % data) 467 | if len(newpairs) == 1: 468 | if pdi: 469 | pdi_new = round(pdi, 2) 470 | logger.info( 471 | "Bot '%s' with id '%s' updated with pair '%s' and SS of '%s" 472 | % (thebot["name"], thebot["id"], newpairs[0], pdi_new), 473 | notify, 474 | ) 475 | else: 476 | if pdi: 477 | pdi_new = round(pdi, 2) 478 | logger.info( 479 | "Bot '%s' with id '%s' updated with %d pairs %s and SS of '%s" 480 | % ( 481 | thebot["name"], 482 | thebot["id"], 483 | len(newpairs), 484 | newpairs, 485 | pdi_new 486 | ), 487 | notify, 488 | ) 489 | if newmaxdeals: 490 | logger.info( 491 | "Max active deals changed to %s" % newmaxdeals, 492 | notify, 493 | ) 494 | if pdi: 495 | newss = round(pdi, 2) 496 | logger.info( 497 | "Bot '%s' with id '%s' updated with ss '%s'" 498 | % (thebot["name"], thebot["id"], newss), 499 | notify, 500 | ) 501 | else: 502 | if error and "msg" in error: 503 | logger.error( 504 | "Error occurred while updating bot '%s' error: %s" 505 | % (thebot["name"], error["msg"]), 506 | True, 507 | ) 508 | else: 509 | logger.error( 510 | "Error occurred while updating bot '%s'" % thebot["name"], 511 | True, 512 | ) 513 | 514 | 515 | def trigger_threecommas_bot_deal(logger, api, thebot, pair, skip_checks=False): 516 | """Trigger bot to start deal asap.""" 517 | 518 | error, data = api.request( 519 | entity="bots", 520 | action="start_new_deal", 521 | action_id=str(thebot["id"]), 522 | payload={"pair": pair, "skip_signal_checks": skip_checks, "bot_id": thebot["id"]}, 523 | ) 524 | if data: 525 | logger.debug("Bot deal triggered: %s" % data) 526 | logger.info( 527 | "Bot '%s' with id '%s' triggered start_new_deal for: %s" 528 | % (thebot["name"], thebot["id"], pair), 529 | True, 530 | ) 531 | else: 532 | if error and "msg" in error: 533 | logger.error( 534 | "Error occurred while triggering start_new_deal bot '%s' error: %s" 535 | % (thebot["name"], error["msg"]), 536 | ) 537 | else: 538 | logger.error( 539 | "Error occurred while triggering start_new_deal bot '%s'" 540 | % thebot["name"], 541 | ) 542 | 543 | 544 | def control_threecommas_bots(logger, api, thebot, cmd): 545 | """Enable or disable a bot.""" 546 | 547 | error, data = api.request( 548 | entity="bots", 549 | action=cmd, 550 | action_id=str(thebot["id"]), 551 | ) 552 | if data: 553 | logger.debug(f"Bot {cmd}: {data}") 554 | logger.info( 555 | "Bot '%s' is %sd" % (thebot["name"], cmd), 556 | True, 557 | ) 558 | else: 559 | if error and "msg" in error: 560 | logger.error( 561 | "Error occurred while '%s' bot was %sd error: %s" 562 | % (thebot["name"], cmd, error["msg"]), 563 | ) 564 | else: 565 | logger.error( 566 | "Error occurred while '%s' bot was %sd" % (thebot["name"], cmd), 567 | ) 568 | 569 | 570 | def get_threecommas_deals(logger, api, botid, actiontype="finished"): 571 | """Get all deals from 3Commas linked to a bot.""" 572 | 573 | data = None 574 | if actiontype == "finished": 575 | payload = { 576 | "scope": "finished", 577 | "bot_id": str(botid), 578 | "limit": 100, 579 | "order": "closed_at", 580 | } 581 | else: 582 | payload = { 583 | "scope": "active", 584 | "bot_id": str(botid), 585 | "limit": 100, 586 | } 587 | 588 | error, data = api.request( 589 | entity="deals", 590 | action="", 591 | payload=payload, 592 | ) 593 | if error: 594 | if "msg" in error: 595 | logger.error( 596 | "Error occurred while fetching deals error: %s" % error["msg"], 597 | ) 598 | else: 599 | logger.error("Error occurred while fetching deals") 600 | else: 601 | logger.info("Fetched the deals for bot OK (%s deals)" % len(data)) 602 | 603 | return data 604 | 605 | 606 | def close_threecommas_deal(logger, api, dealid, pair): 607 | """Close deal with certain id.""" 608 | 609 | data = None 610 | error, data = api.request( 611 | entity="deals", 612 | action="panic_sell", 613 | action_id=str(dealid), 614 | ) 615 | if error: 616 | if "msg" in error: 617 | logger.error( 618 | "Error occurred while closing deal error: %s" % error["msg"], 619 | ) 620 | else: 621 | logger.error("Error occurred while closing deal") 622 | else: 623 | logger.info( 624 | "Closed deal (panic_sell) for deal with id '%s' and pair '%s'" 625 | % (dealid, pair), 626 | True, 627 | ) 628 | 629 | return data 630 | 631 | 632 | def smart_trades(cfg, logger, api, pair, posvalue, stoploss, accountid, notify=True, notify_uptodate=True): 633 | """Get all data for an account.""" 634 | # Fetch all account data, in real mode 635 | error, data = api.request( 636 | entity="smart_trades_v2", 637 | action="new", 638 | payload={ 639 | "account_id": accountid, 640 | "pair": pair, 641 | "position": { 642 | "type": "buy", 643 | "units": { 644 | "value": posvalue 645 | }, 646 | "order_type": "market" 647 | }, 648 | "take_profit": { 649 | "enabled": "false" 650 | }, 651 | "stop_loss": { 652 | "enabled": "true", 653 | "order_type": "market", 654 | "conditional": { 655 | "price": { 656 | "value": stoploss, 657 | "type": "bid" 658 | } 659 | } 660 | } 661 | } 662 | ) 663 | if data: 664 | logger.info( 665 | "Opening a deal for '%s' successes " 666 | % pair, 667 | notify 668 | ) 669 | 670 | elif error and "msg" in error: 671 | logger.error(f"Opening a deal for : failed error: %s" % error["msg"], notify) 672 | 673 | return None 674 | 675 | 676 | def update_smart_trades(cfg, logger, api, accountid, notify=True, notify_uptodate=True): 677 | """Get all data for an account.""" 678 | # Fetch all account data, in real mode 679 | error, data = api.request( 680 | entity="smart_trades_v2", 681 | action="", 682 | payload={ 683 | "account_id": accountid}) 684 | if data: 685 | print(data) 686 | 687 | elif error and "msg" in error: 688 | logger.error(f"Opening a deal for : failed error: %s" % error["msg"], notify) 689 | 690 | return None 691 | 692 | 693 | def smart_spot_trades(cfg, logger, api, pair, posvalue, accountid, stoploss, tp1, tp2, tp3, tp4, tp5, true=True, notify=True, 694 | notify_uptodate=True): 695 | """Get all data for an account.""" 696 | # Fetch all account data, in real mode 697 | error, data = api.request( 698 | entity="smart_trades_v2", 699 | action="new", 700 | payload={ 701 | "account_id": accountid, 702 | "pair": pair, 703 | "position": { 704 | "type": "buy", 705 | "units": { 706 | "value": posvalue 707 | }, 708 | "order_type": "market" 709 | }, 710 | "take_profit": { 711 | "enabled": "true", 712 | "steps": [ 713 | { 714 | "order_type": "limit", 715 | "price": { 716 | "value": tp1, 717 | "type": "bid" 718 | }, 719 | "volume": 20 720 | }, 721 | { 722 | "order_type": "limit", 723 | "price": { 724 | "value": tp2, 725 | "type": "bid" 726 | }, 727 | "volume": 20 728 | }, 729 | { 730 | "order_type": "limit", 731 | "price": { 732 | "value": tp3, 733 | "type": "bid" 734 | }, 735 | "volume": 20 736 | }, 737 | { 738 | "order_type": "limit", 739 | "price": { 740 | "value": tp4, 741 | "type": "bid" 742 | }, 743 | "volume": 20 744 | }, 745 | { 746 | "order_type": "limit", 747 | "price": { 748 | "value": tp5, 749 | "type": "bid" 750 | }, 751 | "volume": 20, 752 | "trailing": { 753 | "enabled": true, 754 | "percent": float(cfg.get("settings", "trailing-percent")) 755 | } 756 | } 757 | ] 758 | }, 759 | "stop_loss": { 760 | "enabled": "true", 761 | "breakeven": "true", 762 | "order_type": "market", 763 | "conditional": { 764 | "price": { 765 | "value": stoploss, 766 | "type": "bid" 767 | } 768 | } 769 | } 770 | } 771 | ) 772 | if data: 773 | logger.info( 774 | f"\ndeal for pair {pair}\nposition size: {round(posvalue, 2)}\n " 775 | f"successfully opened from Smart Trade account : {accountid}", 776 | True 777 | ) 778 | 779 | elif error: 780 | if "msg" in error: 781 | logger.error(f"Order has Invalid parameters\n{error['msg']}", 782 | True 783 | ) 784 | else: 785 | logger.error("UNKNOWN ERROR") 786 | return None 787 | 788 | 789 | def smart_lev_trades(cfg, logger, api, pair, posvalue, lev, accountid, side, stoploss, tp1, tp2, tp3, tp4, tp5, 790 | true=True, 791 | notify=True, 792 | notify_uptodate=True): 793 | """Smart Trade with lev.""" 794 | # Fetch all account data, in real mode 795 | error, data = api.request( 796 | entity="smart_trades_v2", 797 | action="new", 798 | payload={ 799 | "account_id": accountid, 800 | "pair": pair, 801 | "position": { 802 | "type": side, 803 | "units": { 804 | "value": posvalue 805 | }, 806 | "order_type": "market" 807 | }, 808 | "take_profit": { 809 | "enabled": "true", 810 | "steps": [ 811 | { 812 | "order_type": "limit", 813 | "price": { 814 | "value": tp1, 815 | "type": "bid" 816 | }, 817 | "volume": 20 818 | }, 819 | { 820 | "order_type": "limit", 821 | "price": { 822 | "value": tp2, 823 | "type": "bid" 824 | }, 825 | "volume": 20 826 | }, 827 | { 828 | "order_type": "limit", 829 | "price": { 830 | "value": tp3, 831 | "type": "bid" 832 | }, 833 | "volume": 20 834 | }, 835 | { 836 | "order_type": "limit", 837 | "price": { 838 | "value": tp4, 839 | "type": "bid" 840 | }, 841 | "volume": 20 842 | }, 843 | { 844 | "order_type": "limit", 845 | "price": { 846 | "value": tp5, 847 | "type": "bid" 848 | }, 849 | "volume": 20, 850 | "trailing": { 851 | "enabled": true, 852 | "percent": float(cfg.get("settings", "trailing-percent")) 853 | } 854 | } 855 | ] 856 | }, 857 | "leverage": { 858 | "enabled": true, 859 | "type": "cross", 860 | "value": lev, 861 | }, 862 | "stop_loss": { 863 | "enabled": "true", 864 | "breakeven": "true", 865 | "order_type": "market", 866 | "conditional": { 867 | "price": { 868 | "value": stoploss, 869 | "type": "bid" 870 | } 871 | } 872 | } 873 | } 874 | ) 875 | if data: 876 | logger.info( 877 | f"\ndeal for pair {pair} \n, position size: {round(posvalue, 2)} \n " 878 | f"successfully opened from Smart Trade account : {accountid}", 879 | True 880 | ) 881 | elif error: 882 | if "msg" in error: 883 | logger.error(f"Order has Invalid parameters\n{error['msg']}", 884 | True 885 | ) 886 | else: 887 | logger.error("UNKNOWN ERROR") 888 | return None 889 | 890 | 891 | # 892 | def get_active_smart_trades(cfg, logger, api): 893 | dealsid = [] 894 | """Get all data for an account.""" 895 | # Fetch all account data, in real mode 896 | error, data = api.request( 897 | entity="smart_trades_v2", 898 | action="", 899 | payload={ 900 | 901 | "status": "active" 902 | } 903 | ) 904 | if data: 905 | for deal in data: 906 | dealsid.append(deal["id"]) 907 | elif error: 908 | logger.error("NO Active Deals", 909 | True, 910 | ) 911 | else: 912 | logger.error("NO Active Deals", 913 | True, 914 | ) 915 | return dealsid 916 | 917 | 918 | def panic_close_smart(cfg, logger, api, deals): 919 | for deal in deals: 920 | error, data = api.request( 921 | entity="smart_trades_v2", 922 | action="close_by_market", 923 | action_id=str(deal), 924 | ) 925 | if data: 926 | logger.info(f"Deal : {deal} has been closed", 927 | True, 928 | ) 929 | elif error: 930 | logger.error(f"NO Active Deals or any other errors f{error}", 931 | True, 932 | ) 933 | else: 934 | logger.error(f"NO Active Deals or any other errors f{error}", 935 | True, 936 | ) 937 | 938 | 939 | # lev Smart Trades with RR strategy 940 | 941 | 942 | def smart_lev_trades_risk_reward(cfg, logger, api, pair, posvalue, lev, accountid, side, stoploss, tp1, tp2, price, 943 | true=True, 944 | notify=True, 945 | notify_uptodate=True): 946 | """Smart Trade with lev.""" 947 | # Fetch all account data, in real mode 948 | error, data = api.request( 949 | entity="smart_trades_v2", 950 | action="new", 951 | payload={ 952 | "account_id": accountid, 953 | "pair": pair, 954 | "position": { 955 | "type": side, 956 | "units": { 957 | "value": posvalue 958 | }, 959 | "price": { 960 | "value": price 961 | }, 962 | "order_type": "limit" 963 | }, 964 | "take_profit": { 965 | "enabled": "true", 966 | "steps": [ 967 | { 968 | "order_type": "limit", 969 | "price": { 970 | "value": tp1, 971 | "type": "bid" 972 | }, 973 | "volume": float(cfg.get("settings", "vol1")) 974 | }, 975 | { 976 | "order_type": "limit", 977 | "price": { 978 | "value": tp2, 979 | "type": "bid" 980 | }, 981 | "volume": float(cfg.get("settings", "vol2")) 982 | }, 983 | ] 984 | }, 985 | "leverage": { 986 | "enabled": true, 987 | "type": "cross", 988 | "value": lev, 989 | }, 990 | "stop_loss": { 991 | "enabled": "true", 992 | "breakeven": "true", 993 | "order_type": "market", 994 | "conditional": { 995 | "price": { 996 | "value": stoploss, 997 | "type": "bid" 998 | } 999 | } 1000 | } 1001 | } 1002 | ) 1003 | if data: 1004 | logger.info( 1005 | f"\ndeal for pair {pair} \n, position size: {round(posvalue, 2)} \n " 1006 | f"successfully opened from Smart Trade account : {accountid}", 1007 | True 1008 | ) 1009 | elif error: 1010 | if "msg" in error: 1011 | logger.error(f"Order has Invalid parameters\n{error['msg']}", 1012 | True 1013 | ) 1014 | else: 1015 | logger.error("UNKNOWN ERROR") 1016 | return None 1017 | 1018 | 1019 | # SPOT Smart trade with RR strategy 1020 | 1021 | 1022 | def smart_spot_trades_risk_reward(cfg, logger, api, pair, posvalue, accountid, stoploss, tp1, tp2, price, 1023 | notify=True, 1024 | notify_uptodate=True): 1025 | """Get all data for an account.""" 1026 | # Fetch all account data, in real mode 1027 | print(f"price from {price}") 1028 | error, data = api.request( 1029 | entity="smart_trades_v2", 1030 | action="new", 1031 | payload={ 1032 | "account_id": accountid, 1033 | "pair": pair, 1034 | "position": { 1035 | "type": "buy", 1036 | "units": { 1037 | "value": posvalue 1038 | }, 1039 | "price": { 1040 | "value": price 1041 | }, 1042 | "order_type": "limit" 1043 | }, 1044 | "take_profit": { 1045 | "enabled": "true", 1046 | "steps": [ 1047 | { 1048 | "order_type": "limit", 1049 | "price": { 1050 | "value": tp1, 1051 | "type": "bid" 1052 | }, 1053 | "volume": float(cfg.get("settings", "vol1")) 1054 | }, 1055 | { 1056 | "order_type": "limit", 1057 | "price": { 1058 | "value": tp2, 1059 | "type": "bid" 1060 | }, 1061 | "volume": float(cfg.get("settings", "vol2")) 1062 | }, 1063 | ] 1064 | }, 1065 | "stop_loss": { 1066 | "enabled": "true", 1067 | "breakeven": cfg.get("settings", "break_even"), 1068 | "order_type": "market", 1069 | "conditional": { 1070 | "price": { 1071 | "value": stoploss, 1072 | "type": "bid" 1073 | } 1074 | } 1075 | } 1076 | } 1077 | ) 1078 | if data: 1079 | logger.info( 1080 | f"\ndeal for pair {pair}\nposition size: {round(posvalue, 2)}\n " 1081 | f"successfully opened from Smart Trade account : {accountid}", 1082 | True 1083 | ) 1084 | 1085 | elif error: 1086 | if "msg" in error: 1087 | logger.error(f"Order has Invalid parameters\n{error['msg']}", 1088 | True 1089 | ) 1090 | else: 1091 | logger.error("UNKNOWN ERROR") 1092 | return None 1093 | --------------------------------------------------------------------------------