├── .dockerignore ├── .gitignore ├── requirements.txt ├── Dockerfile ├── examples ├── 3cqsbot.service.example ├── config.ini.single-minimal.example ├── config.ini.multi-minimal.example ├── .env.single-minimal.example ├── .env.multi-minimal.example ├── .env.full.example └── config.ini.full.example ├── config.py ├── start.sh ├── signals.py ├── singlebot.py ├── multibot.py ├── 3cqsbot.py └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.pyc 3 | *.session 4 | *.session-journal 5 | config.ini 6 | config.ini.old 7 | launch.json -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | telethon 2 | py3cw 3 | pycoingecko 4 | yfinance 5 | numpy 6 | tenacity 7 | portalocker 8 | Babel -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.10-slim 2 | 3 | COPY . /App/ 4 | RUN pip install -r /App/requirements.txt 5 | 6 | WORKDIR /App 7 | 8 | ENTRYPOINT ["./start.sh", ""] 9 | -------------------------------------------------------------------------------- /examples/3cqsbot.service.example: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=3CQSBot Daemon 3 | After=multi-user.target 4 | 5 | [Service] 6 | # Set WorkingDirectory and ExecStart to your file paths accordingly 7 | WorkingDirectory=/home/ubuntu/3cqsbot/ 8 | ExecStart=/usr/bin/python3 /home/ubuntu/3cqsbot/3cqsbot.py 9 | User=ubuntu 10 | Restart=on-failure 11 | 12 | [Install] 13 | WantedBy=default.target -------------------------------------------------------------------------------- /examples/config.ini.single-minimal.example: -------------------------------------------------------------------------------- 1 | [general] 2 | debug = false 3 | 4 | [telegram] 5 | api_id = "Your api id from Telegram here - without Quotes" 6 | api_hash = "Your api hash from Telegram here - without Quotes" 7 | 8 | [commas] 9 | key = "Your api key from 3Commas here - without Quotes" 10 | secret = "Your secret from 3Commas here - without Quotes" 11 | 12 | [dcabot] 13 | prefix = 3CQSBOT 14 | subprefix = SINGLE 15 | suffix = TA_SAFE 16 | tp = 1.5 17 | bo = 11 18 | so = 11 19 | os = 1.05 20 | ss = 1 21 | sos = 2.4 22 | mad = 1 23 | max = 1 24 | mstc = 25 25 | sdsp = 1 26 | single = true 27 | single_count = 1 28 | 29 | [trading] 30 | market = USDT 31 | trade_mode = paper 32 | account_name = "Your paper account here - without quotes" 33 | 34 | [filter] 35 | symrank_signal = triple100 36 | token_denylist = [USDT_BTC, USDT_ETH, USDT_BUSD, USDT_USDC] 37 | -------------------------------------------------------------------------------- /examples/config.ini.multi-minimal.example: -------------------------------------------------------------------------------- 1 | [general] 2 | debug = false 3 | 4 | [telegram] 5 | api_id = "Your api id from Telegram here - without Quotes" 6 | api_hash = "Your api hash from Telegram here - without Quotes" 7 | 8 | [commas] 9 | key = "Your api key from 3Commas here - without Quotes" 10 | secret = "Your secret from 3Commas here - without Quotes" 11 | 12 | [dcabot] 13 | prefix = 3CQSBOT 14 | subprefix = MULTI 15 | suffix = TA_SAFE 16 | tp = 1.5 17 | bo = 11 18 | so = 11 19 | os = 1.05 20 | ss = 1 21 | sos = 2.4 22 | mad = 2 23 | max = 1 24 | mstc = 25 25 | sdsp = 1 26 | single = false 27 | 28 | [trading] 29 | market = USDT 30 | trade_mode = paper 31 | account_name = "Your paper account here - without quotes" 32 | 33 | [filter] 34 | symrank_signal = triple100 35 | deal_mode = [{"options": {"time": "3m", "points": "100", "time_period": "7", "trigger_condition": "less"}, "strategy": "rsi"}] 36 | token_denylist = [USDT_BTC, USDT_ETH, USDT_BUSD, USDT_USDC] 37 | -------------------------------------------------------------------------------- /examples/.env.single-minimal.example: -------------------------------------------------------------------------------- 1 | # General 2 | ######################################## 3 | DEBUG=false 4 | 5 | # Telegram 6 | ######################################## 7 | TG_ID="Your api id from Telegram here - without Quotes" 8 | TG_HASH="Your api hash from Telegram here - without Quotes" 9 | 10 | # 3Commas 11 | ######################################## 12 | API_KEY="Your api key from 3Commas here - without Quotes" 13 | API_SECRET="Your secret from 3Commas here - without Quotes" 14 | 15 | # DCABOT 16 | ######################################## 17 | DCABOT_PREFIX=3CQSBOT 18 | DCABOT_SUBPREFIX=SINGLE 19 | DCABOT_SUFFIX=TA_Lite_v2.0 20 | DCABOT_TP=2.0 21 | DCABOT_BO=50 22 | DCABOT_SO=10 23 | DCABOT_OS=1.4 24 | DCABOT_SS=1.22 25 | DCABOT_SOS=3 26 | DCABOT_MAD=1 27 | DCABOT_MAX=1 28 | DCABOT_MSTC=7 29 | DCABOT_SDSP=1 30 | DCABOT_SINGLE=true 31 | DCABOT_SINGLE_COUNT=1 32 | 33 | # TRADING 34 | ######################################## 35 | MARKET=USDT 36 | TRADE_MODE=paper 37 | ACCOUNT="Add your papertrading account here" 38 | 39 | # FILTER 40 | ######################################### 41 | SYMRANK_SIGNAL=triple100 42 | DENYLIST='[USDT_BUSD, USDT_USDC, USDT_TUSD, USDT_UST, USDT_SUSD, USDT_USDP]' -------------------------------------------------------------------------------- /examples/.env.multi-minimal.example: -------------------------------------------------------------------------------- 1 | # General 2 | ######################################## 3 | DEBUG=false 4 | 5 | # Telegram 6 | ######################################## 7 | TG_ID="Your api id from Telegram here - without Quotes" 8 | TG_HASH="Your api hash from Telegram here - without Quotes" 9 | 10 | # 3Commas 11 | ######################################## 12 | API_KEY="Your api key from 3Commas here - without Quotes" 13 | API_SECRET="Your secret from 3Commas here - without Quotes" 14 | 15 | # DCABOT 16 | ######################################## 17 | DCABOT_PREFIX=3CQSBOT 18 | DCABOT_SUBPREFIX=MULTI 19 | DCABOT_SUFFIX=TA_Lite_v2.0 20 | DCABOT_TP=2.0 21 | DCABOT_BO=50 22 | DCABOT_SO=10 23 | DCABOT_OS=1.4 24 | DCABOT_SS=1.22 25 | DCABOT_SOS=3 26 | DCABOT_MAD=2 27 | DCABOT_MAX=1 28 | DCABOT_MSTC=7 29 | DCABOT_SDSP=1 30 | DCABOT_SINGLE=false 31 | 32 | # TRADING 33 | ######################################## 34 | MARKET=USDT 35 | TRADE_MODE=paper 36 | ACCOUNT="Add your papertrading account here" 37 | 38 | # FILTER 39 | ######################################### 40 | SYMRANK_SIGNAL=triple100 41 | DEAL_MODE=[{"options": {"time": "3m", "points": "100", "time_period": "7", "trigger_condition": "less"}, "strategy": "rsi"}] 42 | DENYLIST='[USDT_BUSD, USDT_USDC, USDT_TUSD, USDT_UST, USDT_SUSD, USDT_USDP]' -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import sys 3 | 4 | 5 | class Config: 6 | def __init__(self): 7 | self.config = configparser.ConfigParser() 8 | self.dataset = self.config.read("config.ini") 9 | self.fixstrings = ["account_name", "prefix", "subprefix", "suffix"] 10 | 11 | def get(self, attribute, defaultvalue=""): 12 | data = "" 13 | 14 | if len(self.dataset) != 1: 15 | sys.tracebacklimit = 0 16 | sys.exit( 17 | "Cannot read config.ini! - Please make sure it exists in the folder where 3cqsbot.py is executed." 18 | ) 19 | 20 | sections = self.config.sections() 21 | 22 | for section in sections: 23 | if self.config.has_option(section, attribute): 24 | raw_value = self.config[section].get(attribute) 25 | 26 | if raw_value: 27 | if attribute in self.fixstrings: 28 | data = raw_value 29 | else: 30 | data = self.check_type(raw_value) 31 | break 32 | 33 | if data == "" and str(defaultvalue): 34 | data = defaultvalue 35 | elif data == "" and defaultvalue == "": 36 | sys.tracebacklimit = 0 37 | sys.exit( 38 | "Attribute " 39 | + attribute 40 | + " is not set, but mandatory! Please check the readme for configuration." 41 | ) 42 | 43 | return data 44 | 45 | def isfloat(self, element): 46 | try: 47 | float(element) 48 | return True 49 | except ValueError: 50 | return False 51 | 52 | def check_type(self, raw_value): 53 | data = "" 54 | 55 | if raw_value.isdigit(): 56 | data = int(raw_value) 57 | elif raw_value.lower() == "true": 58 | data = True 59 | elif raw_value.lower() == "false": 60 | data = False 61 | elif self.isfloat(raw_value): 62 | data = float(raw_value) 63 | else: 64 | data = str(raw_value) 65 | 66 | return data 67 | -------------------------------------------------------------------------------- /examples/.env.full.example: -------------------------------------------------------------------------------- 1 | # General 2 | ######################################## 3 | DEBUG=false 4 | #LOGFILE=false 5 | #LOGFILEPATH=3cqbsbot.log 6 | #LOGFILESIZE=200000 7 | #LOGFILECOUNT=5 8 | 9 | # Telegram 10 | ######################################## 11 | TG_ID="Your api id from Telegram here - without Quotes" 12 | TG_HASH="Your api hash from Telegram here - without Quotes" 13 | #TG_SESSIONFILE=session/tgsession 14 | #CHATROOM=3C Quick Stats 15 | 16 | # 3Commas 17 | ######################################## 18 | API_KEY="Your api key from 3Commas here - without Quotes" 19 | API_SECRET="Your secret from 3Commas here - without Quotes" 20 | #API_TIMEOUT=3 21 | #API_RETRIES=5 22 | #API_RETRY_DELAY=2.0 23 | #SYS_BOT_VALUE=300 24 | 25 | # DCABOT 26 | ######################################## 27 | DCABOT_PREFIX=3CQSBOT 28 | DCABOT_SUBPREFIX=MULTI 29 | DCABOT_SUFFIX=TA_Lite_v2.0 30 | DCABOT_TP=2.0 31 | DCABOT_BO=50 32 | DCABOT_SO=10 33 | DCABOT_OS=1.4 34 | DCABOT_SS=1.22 35 | DCABOT_SOS=3 36 | DCABOT_MAD=2 37 | DCABOT_MAX=1 38 | DCABOT_MSTC=7 39 | DCABOT_SDSP=1 40 | DCABOT_SINGLE=true 41 | DCABOT_SINGLE_COUNT=1 42 | #DCABOT_BTC_MIN_VOL=300 43 | #DCABOT_COOLDOWN=30 44 | #DCABOT_DEALS_COUNT=1 45 | 46 | # TRADING 47 | ######################################## 48 | MARKET=USDT 49 | TRADE_MODE=paper 50 | ACCOUNT=Paper trading 123456 51 | #DELETE_SINGLE=false 52 | #UPDATE_SINGLE=true 53 | # Binance only 54 | #TRAILING=false 55 | #TRAILING_DEVIATION=0.2 56 | # Futures trading only 57 | TRADE_FUTURE=false 58 | LEVERAGE_TYPE=cross 59 | LEVERAGE_VALUE=2 60 | STOP_LOSS_PERCENT=1 61 | STOP_LOSS_TYPE=stop_loss_and_disable_bot 62 | STOP_LOSS_TIMEOUT_ENABLED=false 63 | STOP_LOSS_TIMEOUT_SECONDS=5 64 | 65 | 66 | # FILTER 67 | ######################################### 68 | SYMRANK_SIGNAL=triple100 69 | #SYMRANK_LIMIT_MIN=1 70 | #SYMRANK_LIMIT_MAX=10000 71 | #VOLATILITY_LIMIT_MIN=1 72 | #VOLATILITY_LIMIT_MAX=10000 73 | #PRICE_ACTION_LIMIT_MIN=1 74 | #PRICE_ACTION_LIMIT_MAX=10000 75 | #TOPCOIN_FILTER: false 76 | #TOPCOIN_LIMIT=100 77 | #TOPCOIN_VOLUME=300 78 | #TOPCOIN_EXCHANGE=binance 79 | DEAL_MODE='[{"options": {"time": "3m", "points": "100", "time_period": "7", "trigger_condition": "less"}, "strategy": "rsi"}]' 80 | #LIMIT_INIT_PAIRS=false 81 | #RANDOM_PAIR=false 82 | #BTC_PULSE=false 83 | #EXT_BOTSWITCH=false 84 | DENYLIST='[USDT_BUSD, USDT_USDC, USDT_TUSD, USDT_UST, USDT_SUSD, USDT_USDP]' -------------------------------------------------------------------------------- /examples/config.ini.full.example: -------------------------------------------------------------------------------- 1 | [general] 2 | debug = false 3 | #log_to_file = false 4 | #log_file_path = 3cqsbot.log 5 | #log_file_size = 200000 6 | #log_file_count = 5 7 | 8 | [telegram] 9 | api_id = "Your api id from Telegram here - without Quotes" 10 | api_hash = "Your api hash from Telegram here - without Quotes" 11 | #sessionfile = tgsession 12 | #chatroom = 3C Quick Stats 13 | 14 | [commas] 15 | key = "Your api key from 3Commas here - without Quotes" 16 | secret = "Your secret from 3Commas here - without Quotes" 17 | #timeout = 3 18 | #retries = 5 19 | #delay_between_retries = 2.0 20 | #system_bot_value = 300 21 | 22 | [dcabot] 23 | prefix = 3CQSBOT 24 | subprefix = MULTI 25 | suffix = TA_SAFE 26 | tp = 1.5 27 | bo = 11 28 | so = 11 29 | os = 1.05 30 | ss = 1 31 | sos = 2.4 32 | mad = 1 33 | max = 1 34 | mstc = 25 35 | sdsp = 1 36 | single = false 37 | single_count = 1 38 | #btc_min_vol = 100 39 | #cooldown = 30 40 | #deals_count = 1 41 | 42 | [trading] 43 | market = USDT 44 | trade_mode = paper 45 | account_name = "Your paper account here - without quotes" 46 | #delete_single_bots = false 47 | #singlebot_update = true 48 | # Binance only 49 | #trailing = false 50 | #trailing_deviation = 0.2 51 | # Futures trading only 52 | trade_future = false 53 | leverage_type = cross 54 | leverage_value = 2 55 | stop_loss_percent = 1 56 | stop_loss_type = stop_loss_and_disable_bot 57 | stop_loss_timeout_enabled = false 58 | stop_loss_timeout_seconds = 5 59 | 60 | [filter] 61 | symrank_signal = triple100 62 | #symrank_limit_min = 1 63 | #symrank_limit_max = 100 64 | #volatility_limit_min = 0.1 65 | #volatility_limit_max = 100 66 | #price_action_limit_min = 0.1 67 | #price_action_limit_max = 100 68 | #topcoin_filter = False 69 | # warning to not use this attribute for large bots! 70 | # set it to 0 if you want to disable it 71 | # topcoin_volume = 300 72 | # not more then 3500 73 | #topcoin_limit = 3500 74 | #topcoin_exchange = binance 75 | #limit_initial_pairs = false 76 | #random_pair = true 77 | # RSI-7 < 100 (means asap) 78 | # single strategy 79 | deal_mode = [{"options": {"time": "3m", "points": "100", "time_period": "7", "trigger_condition": "less"}, "strategy": "rsi"}] 80 | # multiple strategies 81 | #deal_mode = [{"options": {"time": "5m", "type": "buy_or_strong_buy"}, "strategy": "trading_view"},{"options": {"time": "15m", "points": "70", "time_period": "7", "trigger_condition": "less"}, "strategy": "rsi"},{"options": {"time": "1h", "points": "70", "time_period": "7", "trigger_condition": "less"},{"options": {"time": "4h", "points": "70", "time_period": "7", "trigger_condition": "less"}] 82 | #btc_pulse = false 83 | # ATTENTION: if ext_botswitch set to true, btc_pulse will be ignored 84 | #ext_botswitch = false 85 | token_denylist = [USDT_BTC, USDT_ETH, USDT_BUSD, USDT_USDC] 86 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | # General 5 | echo "[general]" > config.ini 6 | [ $DEBUG ] && echo "debug = $DEBUG" >> config.ini 7 | [ $LOGFILE ] && echo "log_to_file = $LOGFILE" >> config.ini 8 | [ $LOGFILEPATH ] && echo "log_file_path = $LOGFILEPATH" >> config.ini 9 | [ $LOGFILESIZE ] && echo "log_file_size = $LOGFILESIZE" >> config.ini 10 | [ $LOGFILECOUNT ] && echo "log_file_count = $LOGFILECOUNT" >> config.ini 11 | 12 | # Telegram settings 13 | echo "[telegram]" >> config.ini 14 | [ $TG_ID ] && echo "api_id = $TG_ID" >> config.ini 15 | [ $TG_HASH ] && echo "api_hash = $TG_HASH" >> config.ini 16 | [ $TG_SESSIONFILE ] && echo "sessionfile = $TG_SESSIONFILE" >> config.ini 17 | [ $CHATROOM ] && echo "chatroom = $CHATROOM" >> config.ini 18 | 19 | # 3Commas settings 20 | echo "[commas]" >> config.ini 21 | [ $API_KEY ] && echo "key = $API_KEY" >> config.ini 22 | [ $API_SECRET ] && echo "secret = $API_SECRET" >> config.ini 23 | [ $API_TIMEOUT ] && echo "timeout = $API_TIMEOUT" >> config.ini 24 | [ $API_RETRIES ] && echo "retries = $API_RETRIES" >> config.ini 25 | [ $API_RETRY_DELAY ] && echo "delay_between_retries = $API_RETRY_DELAY" >> config.ini 26 | [ $SYS_BOT_VALUE ] && echo "system_bot_value = $SYS_BOT_VALUE" >> config.ini 27 | 28 | # DCABOT settings 29 | echo "[dcabot]" >> config.ini 30 | [ $DCABOT_PREFIX ] && echo "prefix = $DCABOT_PREFIX" >> config.ini 31 | [ $DCABOT_SUBPREFIX ] && echo "subprefix = $DCABOT_SUBPREFIX" >> config.ini 32 | [ $DCABOT_SUFFIX ] && echo "suffix = $DCABOT_SUFFIX" >> config.ini 33 | [ $DCABOT_TP ] && echo "tp = $DCABOT_TP" >> config.ini 34 | [ $DCABOT_BO ] && echo "bo = $DCABOT_BO" >> config.ini 35 | [ $DCABOT_SO ] && echo "so = $DCABOT_SO" >> config.ini 36 | [ $DCABOT_OS ] && echo "os = $DCABOT_OS" >> config.ini 37 | [ $DCABOT_SS ] && echo "ss = $DCABOT_SS" >> config.ini 38 | [ $DCABOT_SOS ] && echo "sos = $DCABOT_SOS" >> config.ini 39 | [ $DCABOT_MAD ] && echo "mad = $DCABOT_MAD" >> config.ini 40 | [ $DCABOT_MAX ] && echo "max = $DCABOT_MAX" >> config.ini 41 | [ $DCABOT_MSTC ] && echo "mstc = $DCABOT_MSTC" >> config.ini 42 | [ $DCABOT_SDSP ] && echo "sdsp = $DCABOT_SDSP" >> config.ini 43 | [ $DCABOT_SINGLE ] && echo "single = $DCABOT_SINGLE" >> config.ini 44 | [ $DCABOT_SINGLE_COUNT ] && echo "single_count = $DCABOT_SINGLE_COUNT" >> config.ini 45 | [ $DCABOT_BTC_MIN_VOL ] && echo "btc_min_vol = $DCABOT_BTC_MIN_VOL" >> config.ini 46 | [ $DCABOT_COOLDOWN ] && echo "cooldown = $DCABOT_COOLDOWN" >> config.ini 47 | [ $DCABOT_DEALS_COUNT ] && echo "deals_count = $DCABOT_DEALS_COUNT" >> config.ini 48 | 49 | # Trading settings 50 | echo "[trading]" >> config.ini 51 | [ $MARKET ] && echo "market = $MARKET" >> config.ini 52 | [ $TRADE_MODE ] && echo "trade_mode = $TRADE_MODE" >> config.ini 53 | [ "$ACCOUNT" ] && echo "account_name = $ACCOUNT" >> config.ini 54 | [ $DELETE_SINGLE ] && echo "delete_single_bots = $DELETE_SINGLE" >> config.ini 55 | [ $UPDATE_SINGLE ] && echo "singlebot_update = $UPDATE_SINGLE" >> config.ini 56 | [ $TRAILING ] && echo "trailing = $TRAILING" >> config.ini 57 | [ $TRAILING_DEVIATION ] && echo "trailing_deviation = $TRAILING_DEVIATION" >> config.ini 58 | 59 | # Filter settings 60 | echo "[filter]" >> config.ini 61 | [ $SYMRANK_SIGNAL ] && echo "symrank_signal = $SYMRANK_SIGNAL" >> config.ini 62 | [ $SYMRANK_LIMIT_MIN ] && echo "symrank_limit_min = $SYMRANK_LIMIT_MIN" >> config.ini 63 | [ $SYMRANK_LIMIT_MAX ] && echo "symrank_limit_max = $SYMRANK_LIMIT_MAX" >> config.ini 64 | [ $VOLATILITY_LIMIT_MIN ] && echo "volatility_limit_min = $VOLATILITY_LIMIT_MIN" >> config.ini 65 | [ $VOLATILITY_LIMIT_MAX ] && echo "volatility_limit_max = $VOLATILITY_LIMIT_MAX" >> config.ini 66 | [ $PRICE_ACTION_LIMIT_MIN ] && echo "price_action_limit_min = $PRICE_ACTION_LIMIT_MIN" >> config.ini 67 | [ $PRICE_ACTION_LIMIT_MAX ] && echo "price_action_limit_max =$PRICE_ACTION_LIMIT_MAX" >> config.ini 68 | [ $TOPCOIN_LIMIT ] && echo "topcoin_limit = $TOPCOIN_LIMIT" >> config.ini 69 | [ $TOPCOIN_VOLUME ] && echo "topcoin_volume = $TOPCOIN_VOLUME" >> config.ini 70 | [ $TOPCOIN_EXCHANGE ] && echo "topcoin_exchange = $TOPCOIN_EXCHANGE" >> config.ini 71 | [ "$DEAL_MODE" ] && echo "deal_mode = ${DEAL_MODE}" >> config.ini 72 | [ $LIMIT_INIT_PAIRS ] && echo "limit_init_pairs = $LIMIT_INIT_PAIRS" >> config.ini 73 | [ $RANDOM_PAIR ] && echo "random_pair = $RANDOM_PAIR" >> config.ini 74 | [ $BTC_PULSE ] && echo "btc_pulse = $BTC_PULSE" >> config.ini 75 | [ $EXT_BOTSWITCH ] && echo "ext_botswitch = $EXT_BOTSWITCH" >> config.ini 76 | [ "$DENYLIST" ] && echo "token_denylist = $DENYLIST" >> config.ini 77 | 78 | python3 -u 3cqsbot.py 79 | -------------------------------------------------------------------------------- /signals.py: -------------------------------------------------------------------------------- 1 | import yfinance as yf 2 | import numpy as np 3 | import asyncio 4 | import math 5 | import re 6 | import babel.numbers 7 | 8 | from pycoingecko import CoinGeckoAPI 9 | from tenacity import retry, wait_fixed 10 | from functools import lru_cache, wraps 11 | from time import monotonic_ns 12 | 13 | 14 | class Signals: 15 | def __init__(self, logging): 16 | self.logging = logging 17 | 18 | # Credits goes to https://gist.github.com/Morreski/c1d08a3afa4040815eafd3891e16b945 19 | def timed_lru_cache( 20 | _func=None, *, seconds: int = 600, maxsize: int = 128, typed: bool = False 21 | ): 22 | """Extension of functools lru_cache with a timeout 23 | 24 | Parameters: 25 | seconds (int): Timeout in seconds to clear the WHOLE cache, default = 10 minutes 26 | maxsize (int): Maximum Size of the Cache 27 | typed (bool): Same value of different type will be a different entry 28 | 29 | """ 30 | 31 | def wrapper_cache(f): 32 | f = lru_cache(maxsize=maxsize, typed=typed)(f) 33 | f.delta = seconds * 10**9 34 | f.expiration = monotonic_ns() + f.delta 35 | 36 | @wraps(f) 37 | def wrapped_f(*args, **kwargs): 38 | if monotonic_ns() >= f.expiration: 39 | f.cache_clear() 40 | f.expiration = monotonic_ns() + f.delta 41 | return f(*args, **kwargs) 42 | 43 | wrapped_f.cache_info = f.cache_info 44 | wrapped_f.cache_clear = f.cache_clear 45 | return wrapped_f 46 | 47 | # To allow decorator to be used without arguments 48 | if _func is None: 49 | return wrapper_cache 50 | else: 51 | return wrapper_cache(_func) 52 | 53 | @staticmethod 54 | @timed_lru_cache(seconds=10800) 55 | def cgexchanges(exchange, id): 56 | cg = CoinGeckoAPI() 57 | exchange = cg.get_exchanges_tickers_by_id(id=exchange, coin_ids=id) 58 | 59 | return exchange 60 | 61 | @staticmethod 62 | @timed_lru_cache(seconds=10800) 63 | def cgvalues(rank): 64 | cg = CoinGeckoAPI() 65 | market = [] 66 | 67 | if rank <= 250: 68 | pages = 1 69 | else: 70 | pages = math.ceil(rank / 250) 71 | 72 | for page in range(1, pages + 1): 73 | page = cg.get_coins_markets(vs_currency="usd", page=page, per_page=250) 74 | for entry in page: 75 | market.append(entry) 76 | 77 | return market 78 | 79 | def topvolume(self, id, volume, exchange, market): 80 | # Check if topcoin has enough volume 81 | volume_target = True 82 | 83 | if volume > 0: 84 | 85 | exchange = self.cgexchanges(exchange, id) 86 | 87 | self.logging.debug(self.cgvalues.cache_info()) 88 | 89 | for target in exchange["tickers"]: 90 | converted_btc = babel.numbers.format_currency( 91 | target["converted_volume"]["btc"], "", locale="en_US" 92 | ) 93 | converted_usd = babel.numbers.format_currency( 94 | target["converted_volume"]["usd"], "USD", locale="en_US" 95 | ) 96 | btc_price = ( 97 | target["converted_volume"]["usd"] 98 | / target["converted_volume"]["btc"] 99 | ) 100 | configured_usd = babel.numbers.format_currency( 101 | (volume * btc_price), 102 | "USD", 103 | locale="en_US", 104 | ) 105 | if ( 106 | target["target"] == market 107 | and target["converted_volume"]["btc"] >= volume 108 | ): 109 | volume_target = True 110 | self.logging.info( 111 | str(target["base"]) 112 | + " daily volume is " 113 | + converted_btc 114 | + " BTC (" 115 | + converted_usd 116 | + ") and over the configured value of " 117 | + str(volume) 118 | + " BTC (" 119 | + configured_usd 120 | + ")" 121 | ) 122 | break 123 | elif ( 124 | target["target"] == market 125 | and target["converted_volume"]["btc"] < volume 126 | ): 127 | volume_target = False 128 | self.logging.info( 129 | str(target["base"]) 130 | + " daily volume is " 131 | + converted_btc 132 | + " BTC (" 133 | + converted_usd 134 | + ") NOT passing the minimum daily BTC volume of " 135 | + str(volume) 136 | + " BTC (" 137 | + configured_usd 138 | + ")" 139 | ) 140 | break 141 | else: 142 | volume_target = False 143 | else: 144 | volume_target = True 145 | 146 | return volume_target 147 | 148 | def topcoin(self, pairs, rank, volume, exchange, trademarket): 149 | 150 | market = self.cgvalues(rank) 151 | 152 | self.logging.debug(self.cgvalues.cache_info()) 153 | self.logging.info( 154 | "Applying CG's top coin filter settings: marketcap <= " 155 | + str(rank) 156 | + " with daily BTC volume >= " 157 | + str(volume) 158 | + " on " 159 | + str(exchange) 160 | ) 161 | 162 | if isinstance(pairs, list): 163 | self.logging.info( 164 | str(len(pairs)) 165 | + " symrank pair(s) BEFORE top coin filter: " 166 | + str(pairs) 167 | ) 168 | pairlist = [] 169 | for pair in pairs: 170 | for symbol in market: 171 | coin = pair 172 | if ( 173 | coin.lower() == symbol["symbol"] 174 | and int(symbol["market_cap_rank"]) <= rank 175 | ): 176 | self.logging.info( 177 | str(pair) 178 | + " is ranked #" 179 | + str(symbol["market_cap_rank"]) 180 | + " and has passed marketcap filter limit of #" 181 | + str(rank) 182 | ) 183 | # Check if topcoin has enough volume 184 | if self.topvolume(symbol["id"], volume, exchange, trademarket): 185 | pairlist.append(pair) 186 | break 187 | else: 188 | pairlist = "" 189 | coin = re.search("(\w+)_(\w+)", pairs).group(2) 190 | 191 | for symbol in market: 192 | if ( 193 | coin.lower() == symbol["symbol"] 194 | and int(symbol["market_cap_rank"]) <= rank 195 | ): 196 | self.logging.info( 197 | str(pairs) 198 | + " is ranked #" 199 | + str(symbol["market_cap_rank"]) 200 | + " and has passed marketcap filter limit of #" 201 | + str(rank) 202 | ) 203 | # Check if topcoin has enough volume 204 | if self.topvolume(symbol["id"], volume, exchange, trademarket): 205 | pairlist = pairs 206 | break 207 | 208 | if not pairlist: 209 | self.logging.info(str(pairs) + " did not match the topcoin filter criteria") 210 | else: 211 | if isinstance(pairlist, str): 212 | self.logging.info(str(pairlist) + " matching top coin filter criteria") 213 | else: 214 | self.logging.info( 215 | str(len(pairlist)) 216 | + " symrank pair(s) AFTER top coin filter: " 217 | + str(pairlist) 218 | ) 219 | 220 | return pairlist 221 | 222 | # Credits goes to @IamtheOnewhoKnocks from 223 | # https://discord.gg/tradealts 224 | def ema(self, data, period, smoothing=2): 225 | # Calculate EMA without dependency for TA-Lib 226 | ema = [sum(data[:period]) / period] 227 | 228 | for price in data[period:]: 229 | ema.append( 230 | (price * (smoothing / (1 + period))) 231 | + ema[-1] * (1 - (smoothing / (1 + period))) 232 | ) 233 | 234 | for i in range(period - 1): 235 | ema.insert(0, np.nan) 236 | 237 | return ema 238 | 239 | # Credits goes to @IamtheOnewhoKnocks from 240 | # https://discord.gg/tradealts 241 | @retry(wait=wait_fixed(2)) 242 | def btctechnical(self, symbol): 243 | btcusdt = yf.download( 244 | tickers=symbol, period="6h", interval="5m", progress=False 245 | ) 246 | if len(btcusdt) > 0: 247 | btcusdt = btcusdt.iloc[:, :5] 248 | btcusdt.columns = ["Time", "Open", "High", "Low", "Close"] 249 | btcusdt = btcusdt.astype(float) 250 | btcusdt["EMA9"] = self.ema(btcusdt["Close"], 9) 251 | btcusdt["EMA50"] = self.ema(btcusdt["Close"], 50) 252 | btcusdt["per_5mins"] = (np.log(btcusdt["Close"].pct_change() + 1)) * 100 253 | btcusdt["percentchange_15mins"] = ( 254 | np.log(btcusdt["Close"].pct_change(3) + 1) 255 | ) * 100 256 | else: 257 | raise IOError("Downloading YFinance chart broken, retry....") 258 | 259 | return btcusdt 260 | 261 | # Credits goes to @IamtheOnewhoKnocks from 262 | # https://discord.gg/tradealts 263 | async def getbtcbool(self, asyncState): 264 | 265 | self.logging.info("Starting btc-pulse") 266 | 267 | while True: 268 | btcusdt = self.btctechnical("BTC-USD") 269 | # if EMA 50 > EMA9 or <-1% drop then the sleep mode is activated 270 | # else bool is false and while loop is broken 271 | if ( 272 | btcusdt.percentchange_15mins[-1] < -1 273 | or btcusdt.EMA50[-1] > btcusdt.EMA9[-1] 274 | ): 275 | self.logging.info("btc-pulse signaling downtrend") 276 | 277 | # after 5mins getting the latest BTC data to see if it has had a sharp rise in previous 5 mins 278 | await asyncio.sleep(300) 279 | btcusdt = self.btctechnical("BTC-USD") 280 | 281 | # this is the golden cross check fast moving EMA 282 | # cuts slow moving EMA from bottom, if that is true then bool=false and break while loop 283 | if ( 284 | btcusdt.EMA9[-1] > btcusdt.EMA50[-1] 285 | and btcusdt.EMA50[-2] > btcusdt.EMA9[-2] 286 | ): 287 | self.logging.info("btc-pulse signaling uptrend") 288 | asyncState.btcbool = False 289 | else: 290 | self.logging.info("btc-pulse signaling downtrend") 291 | asyncState.btcbool = True 292 | 293 | else: 294 | self.logging.info("btc-pulse signaling uptrend") 295 | asyncState.btcbool = False 296 | 297 | self.logging.info("Next btc-pulse check in 5m") 298 | await asyncio.sleep(300) 299 | -------------------------------------------------------------------------------- /singlebot.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import time 4 | 5 | from signals import Signals 6 | 7 | deal_lock = False 8 | 9 | 10 | class SingleBot: 11 | def __init__(self, tg_data, bot_data, account_data, attributes, p3cw, logging): 12 | self.tg_data = tg_data 13 | self.bot_data = bot_data 14 | self.account_data = account_data 15 | self.attributes = attributes 16 | self.p3cw = p3cw 17 | self.logging = logging 18 | self.signal = Signals(logging) 19 | self.prefix = self.attributes.get("prefix") 20 | self.subprefix = self.attributes.get("subprefix") 21 | self.suffix = self.attributes.get("suffix") 22 | self.bot_name = ( 23 | self.prefix 24 | + "_" 25 | + self.subprefix 26 | + "_" 27 | + self.attributes.get("market") 28 | + "(.*)" 29 | + "_" 30 | + self.suffix 31 | ) 32 | 33 | def strategy(self): 34 | if self.attributes.get("deal_mode", "signal") == "signal": 35 | strategy = [{"strategy": "nonstop"}] 36 | else: 37 | try: 38 | strategy = json.loads(self.attributes.get("deal_mode")) 39 | except ValueError: 40 | self.logging.error( 41 | "Decoding JSON string of deal_mode failed. Please check https://jsonformatter.curiousconcept.com/ for correct format" 42 | ) 43 | 44 | return strategy 45 | 46 | def deal_count(self): 47 | account = self.account_data 48 | deals = [] 49 | 50 | error, data = self.p3cw.request( 51 | entity="deals", 52 | action="", 53 | action_id=account["id"], 54 | additional_headers={"Forced-Mode": self.attributes.get("trade_mode")}, 55 | payload={"limit": 1000, "scope": "active", "account_id": account["id"]}, 56 | ) 57 | 58 | if error: 59 | self.logging.error( 60 | "Setting deal count temporary to maximum - because of API errors!" 61 | ) 62 | self.logging.error(error["msg"]) 63 | return self.attributes.get("single_count") 64 | else: 65 | for deal in data: 66 | if re.search(self.bot_name, deal["bot_name"]): 67 | deals.append(deal["bot_name"]) 68 | 69 | self.logging.debug(str(deals)) 70 | self.logging.info("Deal count: " + str(len(deals))) 71 | 72 | return len(deals) 73 | 74 | def bot_count(self): 75 | 76 | bots = [] 77 | 78 | for bot in self.bot_data: 79 | if re.search(self.bot_name, bot["name"]) and bot["is_enabled"]: 80 | bots.append(bot["name"]) 81 | 82 | self.logging.info("Enabled single bot count: " + str(len(bots))) 83 | 84 | return len(bots) 85 | 86 | def payload(self, pair, new_bot): 87 | payload = { 88 | "name": self.prefix + "_" + self.subprefix + "_" + pair + "_" + self.suffix, 89 | "account_id": self.account_data["id"], 90 | "pairs": self.tg_data["pair"], 91 | "max_active_deals": self.attributes.get("mad"), 92 | "base_order_volume": self.attributes.get("bo"), 93 | "take_profit": self.attributes.get("tp"), 94 | "safety_order_volume": self.attributes.get("so"), 95 | "martingale_volume_coefficient": self.attributes.get("os"), 96 | "martingale_step_coefficient": self.attributes.get("ss"), 97 | "max_safety_orders": self.attributes.get("mstc"), 98 | "safety_order_step_percentage": self.attributes.get("sos"), 99 | "take_profit_type": "total", 100 | "active_safety_orders_count": self.attributes.get("max"), 101 | "cooldown": self.attributes.get("cooldown", 0), 102 | "strategy_list": self.strategy(), 103 | "trailing_enabled": self.attributes.get("trailing", False), 104 | "trailing_deviation": self.attributes.get("trailing_deviation", 0.2), 105 | "min_volume_btc_24h": self.attributes.get("btc_min_vol"), 106 | "disable_after_deals_count": self.attributes.get("deals_count", 0), 107 | } 108 | 109 | if new_bot: 110 | if payload["disable_after_deals_count"] == 0: 111 | self.logging.info("This is a new bot and deal_count set to 0, removing from payload") 112 | payload.pop("disable_after_deals_count") 113 | 114 | if self.attributes.get("trade_future", False): 115 | payload.update( 116 | { 117 | "leverage_type": self.attributes.get("leverage_type"), 118 | "leverage_custom_value": self.attributes.get("leverage_value"), 119 | "stop_loss_percentage": self.attributes.get("stop_loss_percent"), 120 | "stop_loss_type": self.attributes.get("stop_loss_type"), 121 | "stop_loss_timeout_enabled": self.attributes.get( 122 | "stop_loss_timeout_enabled" 123 | ), 124 | "stop_loss_timeout_in_seconds": self.attributes.get( 125 | "stop_loss_timeout_seconds" 126 | ), 127 | } 128 | ) 129 | 130 | return payload 131 | 132 | def update(self, bot): 133 | # Update settings on an existing bot 134 | self.logging.info("Updating bot settings on " + bot["name"]) 135 | 136 | error, data = self.p3cw.request( 137 | entity="bots", 138 | action="update", 139 | action_id=str(bot["id"]), 140 | additional_headers={"Forced-Mode": self.attributes.get("trade_mode")}, 141 | payload=self.payload(bot["pairs"][0], new_bot=False), 142 | ) 143 | 144 | if error: 145 | self.logging.error(error["msg"]) 146 | 147 | def enable(self, bot): 148 | 149 | self.logging.info( 150 | "Enabling single bot " + bot["name"] + " because of a START signal" 151 | ) 152 | 153 | if self.attributes.get("singlebot_update", "true"): 154 | self.update(bot) 155 | 156 | # Enables an existing bot 157 | error, data = self.p3cw.request( 158 | entity="bots", 159 | action="enable", 160 | action_id=str(bot["id"]), 161 | additional_headers={"Forced-Mode": self.attributes.get("trade_mode")}, 162 | ) 163 | 164 | if error: 165 | self.logging.error(error["msg"]) 166 | 167 | def disable(self, bot, allbots=False): 168 | # Disable all bots 169 | error = {} 170 | 171 | if allbots: 172 | 173 | self.logging.info("Disabling all single bots, because of BTC Pulse") 174 | 175 | for bots in bot: 176 | if ( 177 | self.prefix 178 | + "_" 179 | + self.subprefix 180 | + "_" 181 | + self.attributes.get("market") 182 | ) in bots["name"]: 183 | 184 | self.logging.info( 185 | "Disabling single bot " 186 | + bots["name"] 187 | + " because of a STOP signal" 188 | ) 189 | 190 | error, data = self.p3cw.request( 191 | entity="bots", 192 | action="disable", 193 | action_id=str(bots["id"]), 194 | additional_headers={ 195 | "Forced-Mode": self.attributes.get("trade_mode") 196 | }, 197 | ) 198 | 199 | if error: 200 | self.logging.error(error["msg"]) 201 | else: 202 | # Disables an existing bot 203 | self.logging.info( 204 | "Disabling single bot " + bot["name"] + " because of a STOP signal" 205 | ) 206 | 207 | error, data = self.p3cw.request( 208 | entity="bots", 209 | action="disable", 210 | action_id=str(bot["id"]), 211 | additional_headers={"Forced-Mode": self.attributes.get("trade_mode")}, 212 | ) 213 | 214 | if error: 215 | self.logging.error(error["msg"]) 216 | 217 | def create(self): 218 | # Creates a single bot with start signal 219 | self.logging.info("Create single bot with pair " + self.tg_data["pair"]) 220 | 221 | error, data = self.p3cw.request( 222 | entity="bots", 223 | action="create_bot", 224 | additional_headers={"Forced-Mode": self.attributes.get("trade_mode")}, 225 | payload=self.payload(self.tg_data["pair"], new_bot=True), 226 | ) 227 | 228 | if error: 229 | self.logging.error(error["msg"]) 230 | else: 231 | # Fix - 3commas needs some time for bot creation 232 | time.sleep(2) 233 | self.enable(data) 234 | 235 | def delete(self, bot): 236 | if bot["active_deals_count"] == 0 and self.attributes.get( 237 | "delete_single_bots", False 238 | ): 239 | # Deletes a single bot with stop signal 240 | self.logging.info("Delete single bot with pair " + self.tg_data["pair"]) 241 | error, data = self.p3cw.request( 242 | entity="bots", 243 | action="delete", 244 | action_id=str(bot["id"]), 245 | additional_headers={"Forced-Mode": self.attributes.get("trade_mode")}, 246 | ) 247 | 248 | if error: 249 | self.logging.error(error["msg"]) 250 | else: 251 | self.logging.info( 252 | "Cannot delete single bot, because of active deals or configuration. Disabling it!" 253 | ) 254 | self.disable(bot, False) 255 | 256 | def trigger(self): 257 | # Triggers a single bot deal 258 | 259 | self.logging.info("Got new 3cqs signal") 260 | 261 | global deal_lock 262 | new_bot = True 263 | pair = self.tg_data["pair"] 264 | running_deals = self.deal_count() 265 | 266 | if self.bot_data: 267 | for bot in self.bot_data: 268 | if ( 269 | self.prefix + "_" + self.subprefix + "_" + pair + "_" + self.suffix 270 | ) == bot["name"]: 271 | new_bot = False 272 | break 273 | 274 | if new_bot: 275 | if self.tg_data["action"] == "START": 276 | if self.bot_count() < self.attributes.get("single_count"): 277 | 278 | if self.attributes.get("topcoin_filter", False): 279 | pair = self.signal.topcoin( 280 | pair, 281 | self.attributes.get("topcoin_limit", 0), 282 | self.attributes.get("topcoin_volume", 0), 283 | self.attributes.get("topcoin_exchange", "binance"), 284 | self.attributes.get("market"), 285 | ) 286 | else: 287 | self.logging.info( 288 | "Topcoin filter disabled, not filtering pairs!" 289 | ) 290 | 291 | if pair: 292 | self.logging.info( 293 | "No single bot for " + pair + " found - creating one" 294 | ) 295 | # avoid deals over limit 296 | if running_deals < self.attributes.get("single_count") - 1: 297 | self.create() 298 | deal_lock = False 299 | elif ( 300 | running_deals == self.attributes.get("single_count") - 1 301 | ) and not deal_lock: 302 | self.create() 303 | deal_lock = True 304 | else: 305 | self.logging.info( 306 | "Blocking new deals, because last enabled bot can potentially reach max deals!" 307 | ) 308 | 309 | else: 310 | self.logging.info( 311 | "Pair " 312 | + str(self.tg_data["pair"]) 313 | + " is not in the top coin list - not added!" 314 | ) 315 | else: 316 | self.logging.info( 317 | "Maximum bots/deals reached. Bot with pair: " 318 | + pair 319 | + " not added." 320 | ) 321 | 322 | elif self.tg_data["action"] == "STOP": 323 | self.logging.info( 324 | "Stop command on a non-existing single bot with pair: " + pair 325 | ) 326 | else: 327 | self.logging.debug("Pair: " + pair) 328 | self.logging.debug("Bot-Name: " + bot["name"]) 329 | 330 | if self.tg_data["action"] == "START": 331 | if self.bot_count() < self.attributes.get("single_count"): 332 | # avoid deals over limit 333 | if self.deal_count() < self.attributes.get("single_count") - 1: 334 | self.enable(bot) 335 | deal_lock = False 336 | elif ( 337 | self.deal_count() == self.attributes.get("single_count") - 1 338 | ) and not deal_lock: 339 | self.enable(bot) 340 | deal_lock = True 341 | else: 342 | self.logging.info( 343 | "Blocking new deals, because last enabled bot can potentially reach max deals!" 344 | ) 345 | 346 | else: 347 | self.logging.info( 348 | "Maximum enabled bots/deals reached. Single bot with pair: " 349 | + pair 350 | + " not enabled." 351 | ) 352 | else: 353 | self.delete(bot) 354 | 355 | else: 356 | self.logging.info("No single bots found") 357 | self.create() 358 | -------------------------------------------------------------------------------- /multibot.py: -------------------------------------------------------------------------------- 1 | import random 2 | import json 3 | from sys import prefix 4 | 5 | from signals import Signals 6 | 7 | 8 | class MultiBot: 9 | def __init__( 10 | self, tg_data, bot_data, account_data, pair_data, attributes, p3cw, logging 11 | ): 12 | self.tg_data = tg_data 13 | self.bot_data = bot_data 14 | self.account_data = account_data 15 | self.pair_data = pair_data 16 | self.attributes = attributes 17 | self.p3cw = p3cw 18 | self.logging = logging 19 | self.signal = Signals(logging) 20 | self.prefix = self.attributes.get("prefix") 21 | self.subprefix = self.attributes.get("subprefix") 22 | self.suffix = self.attributes.get("suffix") 23 | 24 | def strategy(self): 25 | if self.attributes.get("deal_mode", "signal") == "signal": 26 | strategy = [{"strategy": "manual"}] 27 | else: 28 | try: 29 | strategy = json.loads(self.attributes.get("deal_mode")) 30 | except ValueError: 31 | self.logging.error( 32 | "Decoding JSON string of deal_mode failed. Please check https://jsonformatter.curiousconcept.com/ for correct format" 33 | ) 34 | 35 | return strategy 36 | 37 | def adjustmad(self, pairs, mad): 38 | # Lower max active deals, when pairs are under mad 39 | if len(pairs) * self.attributes.get("sdsp") < mad: 40 | self.logging.debug( 41 | "Pairs are under 'mad' - Lower max active deals to actual pairs" 42 | ) 43 | mad = len(pairs) 44 | # Raise max active deals to minimum pairs or mad if possible 45 | elif len(pairs) * self.attributes.get("sdsp") >= mad: 46 | self.logging.debug("Pairs are over 'mad' - nothing to do") 47 | mad = self.attributes.get("mad") 48 | 49 | return mad 50 | 51 | def payload(self, pairs, mad, new_bot): 52 | 53 | payload = { 54 | "name": self.prefix + "_" + self.subprefix + "_" + self.suffix, 55 | "account_id": self.account_data["id"], 56 | "pairs": pairs, 57 | "max_active_deals": mad, 58 | "base_order_volume": self.attributes.get("bo"), 59 | "take_profit": self.attributes.get("tp"), 60 | "safety_order_volume": self.attributes.get("so"), 61 | "martingale_volume_coefficient": self.attributes.get("os"), 62 | "martingale_step_coefficient": self.attributes.get("ss"), 63 | "max_safety_orders": self.attributes.get("mstc"), 64 | "safety_order_step_percentage": self.attributes.get("sos"), 65 | "take_profit_type": "total", 66 | "active_safety_orders_count": self.attributes.get("max"), 67 | "cooldown": self.attributes.get("cooldown", 0), 68 | "strategy_list": self.strategy(), 69 | "trailing_enabled": self.attributes.get("trailing", False), 70 | "trailing_deviation": self.attributes.get("trailing_deviation", 0.2), 71 | "allowed_deals_on_same_pair": self.attributes.get("sdsp"), 72 | "min_volume_btc_24h": self.attributes.get("btc_min_vol", 0), 73 | "disable_after_deals_count": self.attributes.get("deals_count", 0), 74 | } 75 | 76 | if new_bot: 77 | if payload["disable_after_deals_count"] == 0: 78 | self.logging.info("This is a new bot and deal_count set to 0, removing from payload") 79 | payload.pop("disable_after_deals_count") 80 | 81 | if self.attributes.get("trade_future", False): 82 | payload.update( 83 | { 84 | "leverage_type": self.attributes.get("leverage_type"), 85 | "leverage_custom_value": self.attributes.get("leverage_value"), 86 | "stop_loss_percentage": self.attributes.get("stop_loss_percent"), 87 | "stop_loss_type": self.attributes.get("stop_loss_type"), 88 | "stop_loss_timeout_enabled": self.attributes.get( 89 | "stop_loss_timeout_enabled" 90 | ), 91 | "stop_loss_timeout_in_seconds": self.attributes.get( 92 | "stop_loss_timeout_seconds" 93 | ), 94 | } 95 | ) 96 | 97 | return payload 98 | 99 | def enable(self, bot): 100 | # Enables an existing bot 101 | if not bot["is_enabled"]: 102 | self.logging.info("Enabling bot: " + bot["name"]) 103 | 104 | error, data = self.p3cw.request( 105 | entity="bots", 106 | action="enable", 107 | action_id=str(bot["id"]), 108 | additional_headers={"Forced-Mode": self.attributes.get("trade_mode")}, 109 | ) 110 | 111 | if error: 112 | self.logging.error(error["msg"]) 113 | 114 | else: 115 | self.logging.info(bot["name"] + " enabled") 116 | 117 | def disable(self): 118 | # Disables an existing bot 119 | for bot in self.bot_data: 120 | if (self.prefix + "_" + self.subprefix + "_" + self.suffix) == bot["name"]: 121 | 122 | # Disables an existing bot 123 | self.logging.info("Disabling bot: " + bot["name"]) 124 | 125 | error, data = self.p3cw.request( 126 | entity="bots", 127 | action="disable", 128 | action_id=str(bot["id"]), 129 | additional_headers={ 130 | "Forced-Mode": self.attributes.get("trade_mode") 131 | }, 132 | ) 133 | 134 | if error: 135 | self.logging.error(error["msg"]) 136 | 137 | def new_deal(self, bot, triggerpair): 138 | # Triggers a new deal 139 | if triggerpair: 140 | pair = triggerpair 141 | else: 142 | if self.attributes.get("random_pair", "true"): 143 | pair = random.choice(bot["pairs"]) 144 | else: 145 | pair = "" 146 | 147 | if pair: 148 | self.logging.info("Trigger new deal with pair " + pair) 149 | error, data = self.p3cw.request( 150 | entity="bots", 151 | action="start_new_deal", 152 | action_id=str(bot["id"]), 153 | additional_headers={"Forced-Mode": self.attributes.get("trade_mode")}, 154 | payload={"pair": pair}, 155 | ) 156 | 157 | if error: 158 | if bot["active_deals_count"] == bot["max_active_deals"]: 159 | self.logging.info( 160 | "Max active deals of " 161 | + str(bot["max_active_deals"]) 162 | + " reached, not adding a new one." 163 | ) 164 | else: 165 | self.logging.error(error["msg"]) 166 | 167 | def create(self): 168 | # Creates a multi bot with start signal 169 | new_bot = True 170 | pairs = [] 171 | botnames = [] 172 | mad = self.attributes.get("mad") 173 | 174 | # Check for existing or new bot 175 | for bot in self.bot_data: 176 | 177 | botnames.append(bot["name"]) 178 | 179 | if (self.prefix + "_" + self.subprefix + "_" + self.suffix) == bot["name"]: 180 | botid = str(bot["id"]) 181 | new_bot = False 182 | break 183 | 184 | self.logging.debug("Existing bot names: " + str(botnames)) 185 | 186 | # Initial pairlist 187 | pairlist = self.tg_data 188 | 189 | # Filter topcoins (if set) 190 | if self.attributes.get("topcoin_filter", False): 191 | pairlist = self.signal.topcoin( 192 | self.tg_data, 193 | self.attributes.get("topcoin_limit", 3500), 194 | self.attributes.get("topcoin_volume", 0), 195 | self.attributes.get("topcoin_exchange", "binance"), 196 | self.attributes.get("market"), 197 | ) 198 | else: 199 | self.logging.info("Topcoin filter disabled, not filtering pairs!") 200 | 201 | for pair in pairlist: 202 | pair = self.attributes.get("market") + "_" + pair 203 | # Traded on our exchange? 204 | if pair in self.pair_data: 205 | self.logging.debug(pair + " added to the list") 206 | pairs.append(pair) 207 | else: 208 | self.logging.info( 209 | pair 210 | + " removed because pair is blacklisted on 3commas or in config.ini or not tradable on '" 211 | + self.attributes.get("account_name") 212 | + "'" 213 | ) 214 | 215 | self.logging.debug("Pairs after topcoin filter " + str(pairs)) 216 | 217 | # Run filters to adapt pair list 218 | if self.attributes.get("limit_initial_pairs", False): 219 | # Limit pairs to the maximal deals (mad) 220 | if self.attributes.get("mad") == 1: 221 | maxpairs = 2 222 | elif self.attributes.get("mad") <= len(pairs): 223 | maxpairs = self.attributes.get("mad") 224 | else: 225 | maxpairs = len(pairs) 226 | pairs = pairs[0:maxpairs] 227 | 228 | self.logging.debug("Pairs after limit initial pairs filter " + str(pairs)) 229 | 230 | # Adapt mad if pairs are under value 231 | mad = self.adjustmad(pairs, mad) 232 | 233 | if new_bot: 234 | 235 | self.logging.info( 236 | "Creating multi bot " 237 | + self.prefix 238 | + "_" 239 | + self.subprefix 240 | + "_" 241 | + self.suffix 242 | + " with filtered symrank pairs" 243 | ) 244 | error, data = self.p3cw.request( 245 | entity="bots", 246 | action="create_bot", 247 | additional_headers={"Forced-Mode": self.attributes.get("trade_mode")}, 248 | payload=self.payload(pairs, mad, new_bot), 249 | ) 250 | 251 | if error: 252 | self.logging.error(error["msg"]) 253 | else: 254 | if not self.attributes.get("ext_botswitch", False): 255 | self.enable(data) 256 | else: 257 | self.logging.info( 258 | "ext_botswitch set to true, bot has to be enabled by external TV signal" 259 | ) 260 | self.new_deal(data, triggerpair="") 261 | else: 262 | self.logging.info( 263 | "Updating multi bot " + bot["name"] + " with filtered symrank pairs" 264 | ) 265 | error, data = self.p3cw.request( 266 | entity="bots", 267 | action="update", 268 | action_id=botid, 269 | additional_headers={"Forced-Mode": self.attributes.get("trade_mode")}, 270 | payload=self.payload(pairs, mad, new_bot), 271 | ) 272 | 273 | if error: 274 | self.logging.error(error["msg"]) 275 | else: 276 | self.logging.debug("Pairs: " + str(pairs)) 277 | if not self.attributes.get("ext_botswitch", False): 278 | self.enable(data) 279 | else: 280 | self.logging.info( 281 | "ext_botswitch set to true, bot enabling/disabling has to be managed by external TV signal" 282 | ) 283 | 284 | def trigger(self, triggeronly=False): 285 | # Updates multi bot with new pairs 286 | triggerpair = "" 287 | mad = self.attributes.get("mad") 288 | 289 | for bot in self.bot_data: 290 | if (self.prefix + "_" + self.subprefix + "_" + self.suffix) == bot["name"]: 291 | 292 | if not triggeronly: 293 | pair = self.tg_data["pair"] 294 | 295 | self.logging.info( 296 | "Got new 3cqs " + self.tg_data["action"] + " signal for " + pair 297 | ) 298 | 299 | if self.tg_data["action"] == "START": 300 | triggerpair = pair 301 | 302 | if pair in bot["pairs"]: 303 | self.logging.info( 304 | pair + " is already included in the pair list" 305 | ) 306 | else: 307 | # Filter topcoins (if set) 308 | if self.attributes.get("topcoin_filter", False): 309 | pair = self.signal.topcoin( 310 | pair, 311 | self.attributes.get("topcoin_limit", 3500), 312 | self.attributes.get("topcoin_volume", 0), 313 | self.attributes.get("topcoin_exchange", "binance"), 314 | self.attributes.get("market"), 315 | ) 316 | else: 317 | self.logging.info( 318 | "Topcoin filter disabled, not filtering pairs!" 319 | ) 320 | 321 | if pair: 322 | self.logging.info("Adding pair " + pair) 323 | bot["pairs"].append(pair) 324 | else: 325 | if pair in bot["pairs"]: 326 | self.logging.info("Remove pair " + pair) 327 | bot["pairs"].remove(pair) 328 | else: 329 | self.logging.info( 330 | pair + " was not included in the pair list, not removed" 331 | ) 332 | 333 | # Adapt mad if pairs are under value 334 | mad = self.adjustmad(bot["pairs"], mad) 335 | self.logging.info( 336 | "Adjusting mad to amount of included symrank pairs: " + str(mad) 337 | ) 338 | 339 | error, data = self.p3cw.request( 340 | entity="bots", 341 | action="update", 342 | action_id=str(bot["id"]), 343 | additional_headers={ 344 | "Forced-Mode": self.attributes.get("trade_mode") 345 | }, 346 | payload=self.payload(bot["pairs"], mad, new_bot=False), 347 | ) 348 | 349 | if error: 350 | self.logging.error(error["msg"]) 351 | else: 352 | data = bot 353 | 354 | if self.attributes.get("deal_mode") == "signal" and data: 355 | self.new_deal(data, triggerpair) 356 | -------------------------------------------------------------------------------- /3cqsbot.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import re 3 | import logging 4 | import asyncio 5 | import sys 6 | import os 7 | import portalocker 8 | import math 9 | 10 | from telethon import TelegramClient, events 11 | from py3cw.request import Py3CW 12 | from singlebot import SingleBot 13 | from multibot import MultiBot 14 | from signals import Signals 15 | from logging.handlers import RotatingFileHandler 16 | from config import Config 17 | 18 | 19 | ###################################################### 20 | # Config # 21 | ###################################################### 22 | attributes = Config() 23 | 24 | 25 | parser = argparse.ArgumentParser( 26 | description="3CQSBot bringing 3CQS signals to 3Commas." 27 | ) 28 | parser.add_argument( 29 | "-l", 30 | "--loglevel", 31 | metavar="loglevel", 32 | type=str, 33 | nargs="?", 34 | default="info", 35 | help="loglevel during runtime - use info, debug, warning, ...", 36 | ) 37 | 38 | args = parser.parse_args() 39 | 40 | ###################################################### 41 | # Init # 42 | ###################################################### 43 | 44 | # Initialize 3Commas API client 45 | p3cw = Py3CW( 46 | key=attributes.get("key"), 47 | secret=attributes.get("secret"), 48 | request_options={ 49 | "request_timeout": attributes.get("timeout", 3), 50 | "nr_of_retries": attributes.get("retries", 5), 51 | "retry_backoff_factor": attributes.get("delay_between_retries", 2.0), 52 | }, 53 | ) 54 | 55 | # Initialize Telegram API client 56 | client = TelegramClient( 57 | attributes.get("sessionfile", "tgsesssion"), 58 | attributes.get("api_id"), 59 | attributes.get("api_hash"), 60 | ) 61 | 62 | # Set logging facility 63 | if attributes.get("debug", False): 64 | loglevel = "DEBUG" 65 | else: 66 | loglevel = getattr(logging, args.loglevel.upper(), None) 67 | 68 | # Set logging output 69 | # Thanks to @M1cha3l for improving logging output 70 | handler = logging.StreamHandler() 71 | 72 | if attributes.get("log_to_file", False): 73 | handler = logging.handlers.RotatingFileHandler( 74 | attributes.get("log_file_path", "3cqsbot.log"), 75 | maxBytes=attributes.get("log_file_size", 200000), 76 | backupCount=attributes.get("log_file_count", 5), 77 | ) 78 | 79 | logging.basicConfig( 80 | format="%(asctime)s %(levelname)-8s %(message)s", 81 | level=loglevel, 82 | datefmt="%Y-%m-%d %H:%M:%S", 83 | handlers=[handler], 84 | ) 85 | 86 | # Initialize global variables 87 | asyncState = type("", (), {})() 88 | asyncState.btcbool = True 89 | asyncState.botswitch = True 90 | asyncState.chatid = "" 91 | asyncState.fh = 0 92 | asyncState.accountData = {} 93 | asyncState.pairData = [] 94 | 95 | ###################################################### 96 | # Methods # 97 | ###################################################### 98 | def run_once(): 99 | asyncState.fh = open(os.path.realpath(__file__), "r") 100 | try: 101 | portalocker.lock(asyncState.fh, portalocker.LOCK_EX | portalocker.LOCK_NB) 102 | except: 103 | sys.exit( 104 | "Another 3CQSBot is already running in this directory - please use another one!" 105 | ) 106 | 107 | 108 | # Check for single instance run 109 | run_once() 110 | 111 | 112 | def parse_tg(raw_text): 113 | return raw_text.split("\n") 114 | 115 | 116 | def tg_data(text_lines): 117 | # Make sure the message is a signal 118 | if len(text_lines) == 7: 119 | data = {} 120 | signal = text_lines[1] 121 | token = text_lines[2].replace("#", "") 122 | action = text_lines[3].replace("BOT_", "") 123 | volatility_score = text_lines[4].replace("Volatility Score ", "") 124 | 125 | if volatility_score == "N/A": 126 | volatility_score = 9999999 127 | 128 | priceaction_score = text_lines[5].replace("Price Action Score ", "") 129 | 130 | if priceaction_score == "N/A": 131 | priceaction_score = 9999999 132 | 133 | symrank = text_lines[6].replace("SymRank #", "") 134 | 135 | if symrank == "N/A": 136 | symrank = 9999999 137 | 138 | if signal == "SymRank Top 30": 139 | signal = "top30" 140 | elif signal == "SymRank Top 100 Triple Tracker": 141 | signal = "triple100" 142 | elif signal == "SymRank Top 100 Quadruple Tracker": 143 | signal = "quad100" 144 | elif signal == "SymRank Top 250 Quadruple Tracker": 145 | signal = "quad250" 146 | elif signal == "Super Volatility": 147 | signal = "svol" 148 | elif signal == "Super Volatility Double Tracker": 149 | signal = "svoldouble" 150 | elif signal == "Hyper Volatility": 151 | signal = "hvol" 152 | elif signal == "Hyper Volatility Double Tracker": 153 | signal = "hvoldouble" 154 | elif signal == "Ultra Volatility": 155 | signal = "uvol" 156 | else: 157 | signal = "xvol" 158 | 159 | data = { 160 | "signal": signal, 161 | "pair": attributes.get("market") + "_" + token, 162 | "action": action, 163 | "volatility": float(volatility_score), 164 | "price_action": float(priceaction_score), 165 | "symrank": int(symrank), 166 | } 167 | # Symrank list 168 | elif len(text_lines) == 17: 169 | pairs = {} 170 | data = [] 171 | 172 | if "Volatile" not in text_lines[0]: 173 | for row in text_lines: 174 | if ". " in row: 175 | # Sort the pair list from Telegram 176 | line = re.split(" +", row) 177 | pairs.update( 178 | {int(line[0][:-1]): line[1], int(line[2][:-1]): line[3]} 179 | ) 180 | 181 | allpairs = dict(sorted(pairs.items())) 182 | data = list(allpairs.values()) 183 | 184 | else: 185 | data = False 186 | 187 | return data 188 | 189 | 190 | def bot_data(): 191 | # Gets information about existing bot in 3Commas 192 | botlimit = attributes.get("system_bot_value", 300) 193 | pages = math.ceil(botlimit / 100) 194 | bots = [] 195 | 196 | for page in range(1, pages + 1): 197 | if page == 1: 198 | offset = 0 199 | else: 200 | offset = (page - 1) * 100 201 | 202 | error, data = p3cw.request( 203 | entity="bots", 204 | action="", 205 | additional_headers={"Forced-Mode": attributes.get("trade_mode")}, 206 | payload={"limit": 100, "offset": offset}, 207 | ) 208 | 209 | if error: 210 | sys.exit(error["msg"]) 211 | else: 212 | if data: 213 | bots += data 214 | else: 215 | break 216 | 217 | return bots 218 | 219 | 220 | def account_data(): 221 | # Gets information about the used 3commas account (paper or real) 222 | account = {} 223 | 224 | error, data = p3cw.request( 225 | entity="accounts", 226 | action="", 227 | additional_headers={"Forced-Mode": attributes.get("trade_mode")}, 228 | ) 229 | 230 | if error: 231 | logging.debug(error["msg"]) 232 | sys.tracebacklimit = 0 233 | sys.exit("Problem fetching account data from 3commas api - stopping!") 234 | else: 235 | for accounts in data: 236 | if accounts["name"] == attributes.get("account_name"): 237 | account.update({"id": str(accounts["id"])}) 238 | account.update({"market_code": str(accounts["market_code"])}) 239 | 240 | if "id" not in account: 241 | sys.tracebacklimit = 0 242 | sys.exit( 243 | "Account with name '" + attributes.get("account_name") + "' not found" 244 | ) 245 | 246 | return account 247 | 248 | 249 | def pair_data(account): 250 | pairs = [] 251 | 252 | error, data = p3cw.request( 253 | entity="accounts", 254 | action="market_pairs", 255 | additional_headers={"Forced-Mode": attributes.get("trade_mode")}, 256 | payload={"market_code": account["market_code"]}, 257 | ) 258 | 259 | if error: 260 | logging.debug(error["msg"]) 261 | sys.tracebacklimit = 0 262 | sys.exit("Problem fetching pair data from 3commas api - stopping!") 263 | 264 | error, blacklist_data = p3cw.request(entity="bots", action="pairs_black_list") 265 | 266 | if error: 267 | logging.debug(error["msg"]) 268 | sys.tracebacklimit = 0 269 | sys.exit("Problem fetching pairs blacklist data from 3commas api - stopping!") 270 | 271 | for pair in data: 272 | if attributes.get("market") in pair: 273 | if ( 274 | pair not in attributes.get("token_denylist") 275 | and pair not in blacklist_data["pairs"] 276 | ): 277 | pairs.append(pair) 278 | 279 | return pairs 280 | 281 | 282 | async def symrank(): 283 | logging.info( 284 | "Sending /symrank command to 3C Quick Stats on Telegram to get new pairs" 285 | ) 286 | await client.send_message(asyncState.chatid, "/symrank") 287 | 288 | 289 | async def botswitch(): 290 | while True: 291 | if not asyncState.btcbool and not asyncState.botswitch: 292 | asyncState.botswitch = True 293 | logging.debug("Botswitch: " + str(asyncState.botswitch)) 294 | if attributes.get("single"): 295 | logging.info("Not activating old single bots (waiting for new signals)") 296 | else: 297 | # Send new top 30 for activating the multibot 298 | await symrank() 299 | 300 | elif asyncState.btcbool and asyncState.botswitch: 301 | asyncState.botswitch = False 302 | logging.debug("Botswitch: " + str(asyncState.botswitch)) 303 | if attributes.get("single"): 304 | bot = SingleBot([], bot_data(), {}, attributes, p3cw, logging) 305 | bot.disable(bot_data(), True) 306 | else: 307 | bot = MultiBot([], bot_data(), {}, 0, attributes, p3cw, logging) 308 | bot.disable() 309 | 310 | else: 311 | logging.debug("Nothing do to") 312 | logging.debug("Botswitch: " + str(asyncState.botswitch)) 313 | 314 | await asyncio.sleep(60) 315 | 316 | 317 | def _handle_task_result(task: asyncio.Task) -> None: 318 | try: 319 | task.result() 320 | except asyncio.CancelledError: 321 | pass # Task cancellation should not be logged as an error. 322 | except Exception: # pylint: disable=broad-except 323 | logging.exception( 324 | "Exception raised by task = %r", 325 | task, 326 | ) 327 | 328 | 329 | @client.on(events.NewMessage(chats=attributes.get("chatroom", "3C Quick Stats"))) 330 | async def my_event_handler(event): 331 | 332 | if ( 333 | asyncState.btcbool 334 | and attributes.get("btc_pulse", False) 335 | and not attributes.get("ext_botswitch", False) 336 | ): 337 | logging.info( 338 | "New 3CQS signal not processed - 3cqsbot stopped because of BTC downtrend" 339 | ) 340 | else: 341 | 342 | tg_output = tg_data(parse_tg(event.raw_text)) 343 | logging.debug("TG msg: " + str(tg_output)) 344 | bot_output = bot_data() 345 | account_output = asyncState.accountData 346 | pair_output = asyncState.pairData 347 | 348 | if tg_output and not isinstance(tg_output, list): 349 | 350 | logging.info( 351 | "New 3CQS signal '" + str(tg_output["signal"]) + "' incoming..." 352 | ) 353 | 354 | # Check if it is the right signal 355 | if ( 356 | tg_output["signal"] == attributes.get("symrank_signal") 357 | or attributes.get("symrank_signal") == "all" 358 | ): 359 | 360 | # Choose multibot or singlebot 361 | if attributes.get("single"): 362 | bot = SingleBot( 363 | tg_output, bot_output, account_output, attributes, p3cw, logging 364 | ) 365 | else: 366 | bot = MultiBot( 367 | tg_output, 368 | bot_output, 369 | account_output, 370 | pair_output, 371 | attributes, 372 | p3cw, 373 | logging, 374 | ) 375 | # Every signal triggers a new multibot deal 376 | bot.trigger(triggeronly=True) 377 | 378 | # Trigger bot if limits passed 379 | if tg_output["volatility"] != 0 and tg_output["pair"] in pair_output: 380 | if ( 381 | tg_output["volatility"] 382 | >= attributes.get("volatility_limit_min", 0.1) 383 | and tg_output["volatility"] 384 | <= attributes.get("volatility_limit_max", 100) 385 | and tg_output["price_action"] 386 | >= attributes.get("price_action_limit_min", 0.1) 387 | and tg_output["price_action"] 388 | <= attributes.get("price_action_limit_max", 100) 389 | and tg_output["symrank"] 390 | >= attributes.get("symrank_limit_min", 1) 391 | and tg_output["symrank"] 392 | <= attributes.get("symrank_limit_max", 100) 393 | ) or tg_output["action"] == "STOP": 394 | 395 | bot.trigger() 396 | 397 | else: 398 | logging.info( 399 | "Start signal for " 400 | + str(tg_output["pair"]) 401 | + " with symrank: " 402 | + str(tg_output["symrank"]) 403 | + ", volatility: " 404 | + str(tg_output["volatility"]) 405 | + " and price action: " 406 | + str(tg_output["price_action"]) 407 | + " not meeting config filter limits - signal ignored" 408 | ) 409 | else: 410 | logging.info( 411 | str(tg_output["pair"]) 412 | + " is not traded on '" 413 | + attributes.get("account_name") 414 | + "'" 415 | ) 416 | else: 417 | logging.info( 418 | "Signal ignored because '" 419 | + attributes.get("symrank_signal") 420 | + "' is configured" 421 | ) 422 | 423 | elif tg_output and isinstance(tg_output, list): 424 | if not attributes.get("single"): 425 | # Create or update multibot with pairs from "/symrank" 426 | bot = MultiBot( 427 | tg_output, 428 | bot_output, 429 | account_output, 430 | pair_output, 431 | attributes, 432 | p3cw, 433 | logging, 434 | ) 435 | bot.create() 436 | else: 437 | logging.debug( 438 | "Ignoring /symrank call, because we're running in single mode!" 439 | ) 440 | 441 | 442 | async def main(): 443 | signals = Signals(logging) 444 | asyncState.accountData = account_data() 445 | asyncState.pairData = pair_data(asyncState.accountData) 446 | 447 | logging.debug("Refreshing cache...") 448 | 449 | user = await client.get_participants("The3CQSBot") 450 | asyncState.chatid = user[0].id 451 | 452 | logging.info("*** 3CQS Bot started ***") 453 | 454 | if not attributes.get("single"): 455 | await symrank() 456 | 457 | if attributes.get("btc_pulse", False) and not attributes.get( 458 | "ext_botswitch", False 459 | ): 460 | btcbooltask = client.loop.create_task(signals.getbtcbool(asyncState)) 461 | btcbooltask.add_done_callback(_handle_task_result) 462 | switchtask = client.loop.create_task(botswitch()) 463 | switchtask.add_done_callback(_handle_task_result) 464 | 465 | while True: 466 | await btcbooltask 467 | await switchtask 468 | elif attributes.get("btc_pulse", False) and attributes.get("ext_botswitch", False): 469 | sys.tracebacklimit = 0 470 | sys.exit( 471 | "Check config.ini, btc_pulse and ext_botswitch both set to true - not allowed" 472 | ) 473 | 474 | 475 | with client: 476 | client.loop.run_until_complete(main()) 477 | 478 | client.start() 479 | 480 | if not attributes.get("btc_pulse", False): 481 | client.run_until_disconnected() 482 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | The 3cqsbot can be used to start and stop [3commas](https://3commas.io) dca bots with the help of the 3cqs signals. You can subscribe to the [telegram channel](https://t.me/The3CQSBot) to receive these signals. If you have any questions regarding the signals, please contact the developer [directly](https://www.3cqs.com/contact/). 3 | 4 | # Disclaimer 5 | **The 3cqsbot is meant to be used for educational purposes only. Use with real funds at your own risk** 6 | 7 | # Prerequisites/Installation 8 | 9 | ## 3CQS Signals Bot 10 | 11 | Join the telegram channel [telegram channel](https://t.me/The3CQSBot) according to the official Telegram [documentation](https://core.telegram.org/api/obtaining_api_id) 12 | 13 | Wait for the signals. Actually the signals are in a beta phase and you have to be chosen to get them. Be patient if they not arrive after joining 14 | 15 | ## Telegram API 16 | In the meantime create your [telegram api account](https://my.telegram.org/apps) and insert them into `api_id` and `api_hash` fields in the *'telegram'* section of the `config.ini` 17 | 18 | ## 3Commas API 19 | Create a [3commas api account](https://3commas.io/api_access_tokens) too and insert the values in the `key` and `secret` fields in the *'commas'* section of the `config.ini` 20 | 21 | **Permissions needed:** BotsRead, BotsWrite, AccountsRead 22 | 23 | ## Operating Systems 24 | - MacOS 25 | - Linux 26 | - Ubuntu 27 | - Windows 28 | - untested (please let me know if it works) 29 | - Docker 30 | 31 | ## Installation 32 | ### Python 33 | Please install at least version 3.7 on your system 34 | 35 | ### Python modules 36 | ``` 37 | pip3 install -r requirements.txt 38 | ``` 39 | 40 | # Configuration (config.ini) 41 | Copy the `*.example*` from the examples directory to `config.ini` in the root folder and change your settings regarding the available settings below. The value type doesn't matter, because Pythons configparser is taking care of the types. So you don't need '' or "" around the values. 42 | 43 | ## General 44 | Name | Type | Mandatory | Values(default) | Description 45 | ------------ | ------------ | ------------ | ------------ | ------------ 46 | debug | boolean | NO | (false) true | Set logging to debug 47 | log_to_file | boolean | NO | (false) true | Log to file instead of console 48 | log_file_path | string | NO | (3cqsbot.log) | Location of the log file 49 | log_file_size | integer | NO | (200000) | Log file size 50 | log_file_count | integer | NO | (5) | How many logfiles will be archived, before deleted 51 | 52 | ## Telegram 53 | Name | Type | Mandatory | Values(default) | Description 54 | ------------ | ------------ | ------------ | ------------ | ------------ 55 | api_id | string | YES | | Telegram API ID 56 | api_hash | string | YES | | Telegram API Hash 57 | sessionfile | string | NO | (tgsession) | Telegram sessionfile location 58 | 59 | **!!! ATTENTION - Do not share your sessionfile with other 3cqsbot instances - this will lead to problems and misfunctional bots. For each instance you have to create a new sessionfile !!!** 60 | 61 | ## 3Commas 62 | Name | Type | Mandatory | Values(default) | Description 63 | ------------ | ------------ | ------------ | ------------ | ------------ 64 | chatroom | string |NO | (3C Quick Stats) | Name of the chatroom - on Windows please use the ID 5011413076 65 | key | string | YES | | 3Commas API Key 66 | secret | string | YES | | 3Commas API Secret 67 | timeout | integer | NO | (3) | Timeout waiting for a 3Commas api response 68 | retries | integer | NO | (5) | Number of retries after a 3Commas api call was not successful 69 | delay_between_retries | number | NO | (2.0) | Waiting time factor between unsuccessful retries 70 | system_bot_value | integer | NO | (300) | Number of actual bots running on your account. This is important, so that the script can see all running bots and does not start duplicates! 71 | 72 | ## DCABot configuration 73 | 74 | Name | Type | Mandatory | Values(default) | Description 75 | ------------ | ------------ | ------------ | ------------ | ------------ 76 | prefix | string | YES | (3CQSBOT) | The name prefix of the created bot 77 | subprefix | string | YES | (MULTI) | Subprefix of the bot (Best would be SINGLE or MULTI) 78 | suffix | string | YES | (TA_SAFE) | Suffix in the bot name - could be the used bot configuration 79 | tp | number | YES | (1.5) | Take profit in percent 80 | bo | number | YES | (11) | Base order volume 81 | so | number | YES | (11) | Safety order volume 82 | os | number | YES | (1.05) | Safety order volume scale 83 | ss | number | YES | (1) | Safety order step scale 84 | sos | number | YES | (2.4) | Price deviation to open safety orders 85 | mad | integer | YES | (3) | Max active deals for a bot 86 | max | integer | YES | (1) | Max active safety trades count 87 | mstc | integer | YES | (25) | Max safety trades count 88 | sdsp | integer | NO | (1) | Simultaneous deals per same pair (only Multibot) 89 | single | boolean | YES | (false) true | Type of Bot creation (True for Single DCA Bots) 90 | single_count | integer | YES | (3) | Maximum single bots - only have to configured for singlebots 91 | btc_min_vol | number | NO | (100) | Minimum 24h volume trading calculated in BTC 92 | cooldown | number | NO | (30) | Number of seconds to wait until starting another deal 93 | deals_count | integer | NO | (0) | Bot will be disabled after completing this number of deals. If 0 bot will not be disabled (default) 94 | 95 | Configure the 'dcabot' section in the `config.ini` according to your favourite bot configuration. 96 | 97 | If you don't have any, please take a look at [this site](https://www.buymeacoffee.com/Ribsy/posts) for published settings. 98 | 99 | Default configuration is based on Trade Alts Safer settings: https://discord.gg/tradealts 100 | 101 | ### Note about single/multibot deal/bots settings 102 | #### Singlebot configuration 103 | **single_count** = how many singlebots can run overall 104 | 105 | **mad** = how many deals can run on a singlebot pair 106 | 107 | **Examples:** 108 | 109 | `single_count=1`, `mad=1` - Only one singlebot is started, and only one deal is started 110 | 111 | `single_count=3`, `mad=1` - Three singlebots are started, and only one deal per singlebot is started 112 | 113 | `single_count=3`, `mad=2` - Three singlebots are started, and two deals are started per singlebot 114 | 115 | #### Multibot configuration 116 | **mad** = how many deals per composite bot can run 117 | 118 | **Example:** 119 | 120 | `mad=20` - 20 deals with different pairs can run at the same time 121 | 122 | 123 | ## Trading mode 124 | 125 | Name | Type | Mandatory | Values(default) | Description 126 | ------------ | ------------ | ------------ | ------------ | ------------ 127 | market | string | YES | (USDT) | Trading market (Example: BUSD, USDT, USDC) 128 | trade_mode | string | YES | (paper) real | Real or Paper trading mode 129 | account_name | string | YES | (Paper trading 123456) | Account name for trading. Can be found unter "My Exchanges". 130 | deal_mode | string | NO | ([{"options": {"time": "3m", "points": "100", "time_period": "7", "trigger_condition": "less"}, "strategy": "rsi"}]) signal | Method how the script is creating new deals in multipair bot 131 | limit_initial_pairs | boolean |NO | (false) | Limit initial pairs to the max number of deals (MAD) - bot chooses the top pairs 132 | btc_pulse | boolean | NO | (false), true | Activates or deactivates the bots according to Bitcoins behaviour. If Bitcoin is going down, the bot will be disabled 133 | delete_single_bots | boolean | NO | (false), true | If set to true, bots without an active deal will be deleted in single bot configuration 134 | singlebot_update | boolean | NO | (true), false | If set to true, singlebots settings will be updated when enabled again (new settings only work after restart of the script) 135 | trailing | boolean | NO | (false), true | Trailing profit enabled 136 | trailing_deviation | number | NO | (0.2) | Deviation of trailing profit 137 | trade_future | boolean | NO | (false), true | Enable futures trading 138 | leverage_type | string | NO | (cross), custom, not_specified, isolated | Different leverage types for futures trading from 3commas 139 | leverage_value | integer | NO | (2) | Leverage value for futures trading 140 | stop_loss_percent | integer | NO | (1) | Stop loss value in percent for futures trading 141 | stop_loss_type | string | NO | (stop_loss_and_disable_bot), stop_loss | Stop Loss type for futures trading 142 | stop_loss_timeout_enabled | boolean | NO | (false), true | Enable stop loss timeout for futures trading 143 | stop_loss_timeout_seconds | integer | NO | (5) | Time interval for stop loss in seconds for futures trading 144 | 145 | ### Deal Mode explanation 146 | 147 | **single=true deal_mode=signal** 148 | 149 | A single bot for a specific pair (the signal) will be created when the signal fits your configured filters and when the signal is a "START" signal. The deal will be start immediately. The bot will be disabled on a stop signal for this specific pair. If `delete_single_bots`is set to true, the script tries do delete the bot. This only works, when no deal is running. 150 | 151 | **single=true deal_mode="self asigned strategy"** 152 | 153 | Everything is the same as with the other single mode, but the deals are started dependent on your configured `deal_mode` strategy. 154 | 155 | **single=false deal_mode=signal** 156 | 157 | A multi bot will be created with the top 30 Symrank list (initical call to /symrank). A new deal will be started when a new signal is coming in. 158 | 159 | If it is a STOP/START signal which is not from an existing pair a random pair from the initial top 30 Symrank list is used for a new deal. 160 | 161 | If it is a START signal from an existing pair or a freshly added pair, exactly that pair is used for a new deal. 162 | 163 | Pairs will be deleted from the list during a STOP signal and added with a START signal, if it fits the filters. 164 | 165 | **single=false deal_mode="self asigned strategy"** 166 | 167 | Everything is the same as with the other multi mode, but the deals are started dependent on your configured `deal_mode` strategy. 168 | 169 | ## Filter 170 | 171 | Name | Type | Mandatory | Values(default) | Description 172 | ------------ | ------------ | ------------ | ------------ | ------------ 173 | symrank_signal | string | YES | (triple100), top30, quad100, quad250, svol, svoldouble, xvol, hvol, hvoldouble, uvol, all | Decide which signal the bot should parse. 174 | symrank_limit_min | integer | NO | (1) | Bots will be created when the symrank value is over this limit 175 | symrank_limit_max | integer | NO | (100) | Bots will be created when the symrank value is under this limit 176 | volatility_limit_min | number | NO | (0.1) | Bots will be created when the volatility value is over this limit 177 | volatility_limit_max | number | NO | (100) | Bots will be created when the volatility value is under this limit 178 | price_action_limit_min | number | NO | (0.1) | Bots will be created when the price_action value is over this limit 179 | price_action_limit_max | number | NO | (100) | Bots will be created when the price_action value is under this limit 180 | topcoin_filter | boolean | NO | (false), true | Disables the topcoin filter (default) 181 | topcoin_limit | integer | NO | (3500) | Token pair has to be in the configured topcoin limit to be traded by the bot 182 | topcoin_volume | integer | NO | (0) | Volume check against Coingecko (btc_min_vol means volume check directly in 3commas - not before like this setting). Only pairs with the given volume are traded. Default is 0 and means volume check is disabled 183 | topcoin_exchange | string | NO | (binance), gdax | Name of the exchange to check the volume. Because every exchange has another id, please contact me for your exchange and I will update this list here for configuration 184 | deal_mode | string | NO | ([{"options": {"time": "3m", "points": "100"}, "strategy": "rsi"}]) signal | Deal strategy how the script is creating new deals in multipair bot - for more see the "Deal Modes" section 185 | limit_initial_pairs | boolean |NO | (false), true | Limit initial pairs to the max number of deals (MAD) - bot chooses the top pairs 186 | random_pair | boolean | NO | (false), true | If true then random pairs from the symrank list will be used for new deals in multibot 187 | btc_pulse | boolean | NO | (false), true | Activates or deactivates the bots according to Bitcoins behaviour. If Bitcoin is going down, the bot will be disabled 188 | ext_botswitch | boolean | NO | (false), true | If enabled the automatic multibot enablement will be disabled and only triggered by external events - you must disable BTC Pulse if you enable this switch !!! 189 | token_denylist | array |YES | ([BUSD_USDT, USDC_USDT, USDT_USDT, USDT_USDP]) | Denylist of pairs which not be used by the bot for new deals 190 | 191 | ### Signals 192 | The new version of 3cqs signals is now separated into three main versions. To decide which version fit your needs, please take a look at the indicators beneath. The description can be found on Discord too: https://discord.com/channels/720875074806349874/835100061583015947/958724423513419876 193 | 194 | #### triple100 195 | SIGNAL NAME: SymRank Top 100 Triple Tracker 196 | BOT_START: SymRank <= 100 197 | Volatility Score >= 3, 198 | Price Action Score >= 2 199 | 200 | #### top30 201 | SIGNAL NAME: SymRank Top 30 202 | BOT_START: SymRank <= 30 203 | 204 | #### hvol 205 | SIGNAL NAME: Hyper Volatility 206 | BOT_START: Volatility Score >= 6 207 | 208 | #### uvol 209 | SIGNAL NAME: Hyper Volatility 210 | BOT_START: Volatility Score >= 8 211 | 212 | #### xvol 213 | SIGNAL NAME: X-treme Volatility 214 | BOT_START: Volatility Score >= 10 215 | 216 | #### all 217 | Pass through all signals 218 | 219 | ### BTC Pulse 220 | BTCPulse is a simple strategy which monitors BTC Price Action to start new deals or just put the bot to sleep ( no new deals but active deals keep running) based on:- 221 | If BTC is in upswing new deals are started 222 | If BTC is dumping no new deals are started 223 | 224 | BTCPulse hence is determined using the 2 factors :- 225 | % price change of BTC in the last 15 minutes or 226 | Fast and Slow moving EMAs crossses 227 | 228 | Please test this strategy on paper before putting real money on it. 229 | TBMoonWalker or IamtheOnewhoKnocks take no responsibility for losses occurred due to the script/strategy 230 | 231 | **Again, please use 3cqsbot only on paper trading. Usage with real funds is at your own risk** 232 | 233 | 234 | ### Deal Modes 235 | This section is all about the deal start signals. Tested are the following modes: 236 | 237 | - `signal` --> starting the bot after a 3CQS signal 238 | - `[{"options": {"time": "3m", "points": "100"}, "strategy": "rsi"}]` --> start the bot when the RSI-7 value is under 100 in the 3 minute view 239 | 240 | More modes are possible, but not tested. You can minimize the value of the RSI-7 entry point for example. A whole list of deal signals can be found with the api call `GET /ver1/bots/strategy_list`. Details can be found under: https://github.com/3commas-io/3commas-official-api-docs/blob/master/bots_api.md 241 | 242 | # Run 243 | If you get signals, you can run the script with the command: 244 | 245 | ``` 246 | python3 3cqsbot.py 247 | ``` 248 | When running for the first time, you will be asked for your Telegram phonenumber and you will get a code you have to insert! 249 | 250 | ## You don't get the code 251 | Some users had to put spaces in the phone number. It seems the number has to be the same format as in Telegram. For example type in your telephone number as `+XXX XXX XXX XXX` to receive the code. 252 | 253 | # Docker 254 | ## Create the docker image 255 | ``` 256 | docker build -t "your repo name"/3cqsbot:"version number" 257 | ``` 258 | ## Create a persistent volume for your Telegram session file 259 | ``` 260 | docker volume create session 261 | ``` 262 | 263 | ## Create your .env file 264 | Copy one of the `.env.*.example` files from the example directory to `.env` in the root directory and change the settings. The same settings as in the config.ini can be used 265 | 266 | ## Run your container 267 | ``` 268 | docker run --name 3cqsbot --volume session:/App/session --env-file .env -d "your repo name"/3cqsbot:"version number" 269 | ``` 270 | 271 | ## Logfile monitoring 272 | ``` 273 | docker logs --follow 3cqsbot 274 | ``` 275 | # PythonAnywhere 276 | If you want to run 3cqsbot 24h/7d without running your home computer all the time and you do not have a Rasperry Pi, 277 | then PythonAnywhere might be a cheap option for you to run the script. 278 | 279 | ## Create account 280 | If you live in the EU go to https://eu.pythonanywhere.com otherwise https://www.pythonanywhere.com. 281 | The 'Hacker account' plan for 5€/5$ is sufficient enough to run 3cqsbot 282 | 283 | ## Preparing PythonAnywhere to run 3CQSBot 284 | Click on `Dashboard`. Under menue "New console" Click on `$ Bash` to open a Bash console in your home directory. 285 | Clone the actual version of 3cqsbot from Github and install the requirements for the bot by following commands 286 | ``` 287 | git clone https://github.com/TBMoonwalker/3cqsbot.git 3cqsbot 288 | cd 3cqsbot 289 | pip3 install -r requirements.txt 290 | cp config.ini.example config.ini 291 | ``` 292 | When you want to use multiple 3cqsbots simultanously you have to clone 3cqsbot to different directories 293 | ``` 294 | git clone https://github.com/TBMoonwalker/3cqsbot.git 3cqsbot_TAsafe 295 | git clone https://github.com/TBMoonwalker/3cqsbot.git 3cqsbot_Urmav6 296 | git clone https://github.com/TBMoonwalker/3cqsbot.git 3cqsbot_Mars 297 | ``` 298 | ## Edit config.ini settings 299 | Edit the config.ini with the integrated editor of PythonAnywhere in the `Files` menue. Paste the necessary keys of 3commas and Telegram and 300 | configure your DCA settings. Once done you can copy your config.ini to other directories of 3cqsbot and adapt the DCA settings. 301 | 302 | ## Scheduled tasks 303 | Because the consoles of PythonAnywhere are frequently restarted due to maintenance you have to use scheduled task to ensure continuous work of your 3cqsbot. 304 | Before running 3cqsbot as scheduled task, make sure that you run the script once on the console to establish the Telegram security session. 305 | Enter your phone number (international format, e.g. +49xxx or 0049xxx) of your Telegram account and enter the code you receive from Telegram. 306 | Check if 3cqsbot is running without any problems under the console. 307 | Do not copy the tgsession file to other directories of 3cqsbots, because it is an individual security file and you will invalidate the established Telegram session 308 | when used with another version of 3cqsbot. 309 | 310 | Click on `Tasks` menue. If you have only one python script running you can use the Always-on task (only one Always-on task allowed on the "Hacker account" plan). 311 | In case of using more scripts, e.g. for testing DCA settings, you have to use scheduled tasks on an hourly basis. Select "Hourly" and paste 312 | ``` 313 | cd ~/3cqsbot && python 3cqsbot.py 314 | ``` 315 | If you encounter problems with this command then try this where YOUR_USERNAME has to be replaced by your chosen username on PythonAnywhere. 316 | ``` 317 | cd /home/YOUR_USERNAME/3cqsbot && python 3cqsbot.py 318 | ``` 319 | When using multiple 3cqsbots with different settings you have to add each 3cqsbot directory as seperate hourly task. 320 | Rename 3cqsbot.py according to your DCA setting configuration in the `Files` menue to identify the task running in the process list. 321 | 322 | 1. Hourly Task: 10min ```cd ~/3cqsbot_TAsafe && python 3cqsbot_TAsafe.py``` 323 | 2. Hourly Task: 11min ```cd ~/3cqsbot_Urmav6 && python 3cqsbot_Urmav6.py``` 324 | 3. Hourly Task: 12min ```cd ~/3cqsbot_Urmav6 && python 3cqsbot_Mars.py``` 325 | 326 | In case you have to kill a process you can know easily identify your task to kill after fetching the process list under `Running tasks` 327 | Check the log files in the scheduled task under `Actions` for errors. 328 | 329 | ## Updating 3cqsbot 330 | If you want to update 3cqsbot to the newest version open the Bash console. Change to your desired 3cqsbot directory with following commands 331 | ``` 332 | cd 3cqsbot 333 | git pull 334 | pip3 install -r requirements.txt 335 | ``` 336 | Check the config.ini.example for new config options. Make sure to update your existent config.ini for the new options with the integrated 337 | PythonAnywhere editor (Files menue). 338 | 339 | # Debugging 340 | The script can be started with 341 | 342 | ``` 343 | python3 3cqsbot.py -l debug 344 | ``` 345 | 346 | do show debug logging 347 | 348 | # Bug reports 349 | Please submit bugs or problems through the Github [issues page](https://github.com/TBMoonwalker/3cqsbot/issues). 350 | 351 | # Donation 352 | If you like to support this project, you can donate to the following wallet: 353 | 354 | - USDT or BUSD (BEP20 - Binance Smart Chain): 0xB3C6DD82a203E3b6f399187DB265AdC664E2beF9 355 | --------------------------------------------------------------------------------