├── debug ├── test.jpg └── test2.jpg ├── exchange ├── __init__.py ├── mt5_api.py └── mt5_oms.py ├── requirements.txt ├── configs ├── exchange_config.json ├── ma_cross_config.json ├── ma_hekin_ashi_config.json ├── price_action_config.json ├── break_strategy_config.json ├── trend_following_config.json ├── rsi_hidden_divergence_config.json ├── rsi_regular_divergence_config.json ├── macd_trading_config.json ├── strategies_def.py └── symbols_trading_config.json ├── indicators ├── __init__.py ├── heikin_ashi.py └── zigzag.py ├── strategies ├── __init__.py ├── ma_cross_strategy.py ├── ma_heikin_ashi.py ├── base_strategy.py ├── price_action.py ├── rsi_regular_divergence.py ├── rsi_hidden_divergence.py └── break_strategy.py ├── strategy_utils.py ├── tuning_configs ├── heikin_ashi_tuning_config.json ├── break_strategy_tuning_config.json ├── rsi_hidden_divergence_tuning_config.json ├── rsi_regular_divergence_tuning_config.json └── macd_divergence_tuning_config.json ├── logging_config.ini ├── trade.py ├── exchange_loader.py ├── main.py ├── .gitignore ├── README.md ├── utils.py ├── MT5.ipynb ├── StatisticTuning.ipynb ├── backtest.py ├── trader.py ├── trade_engine.py ├── tuning.py └── order.py /debug/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zsunflower/Monn/HEAD/debug/test.jpg -------------------------------------------------------------------------------- /debug/test2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zsunflower/Monn/HEAD/debug/test2.jpg -------------------------------------------------------------------------------- /exchange/__init__.py: -------------------------------------------------------------------------------- 1 | from .mt5_api import MT5API 2 | from .mt5_oms import MT5OMS -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zsunflower/Monn/HEAD/requirements.txt -------------------------------------------------------------------------------- /configs/exchange_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mt5": { 3 | "account": 122334, 4 | "server": "Exness-MT5Trial7" 5 | } 6 | } -------------------------------------------------------------------------------- /indicators/__init__.py: -------------------------------------------------------------------------------- 1 | from .heikin_ashi import heikin_ashi, heikin_ashi_stream 2 | from .zigzag import zigzag, zigzag_stream, zigzag_conv, zigzag_conv_stream, POINT_TYPE -------------------------------------------------------------------------------- /strategies/__init__.py: -------------------------------------------------------------------------------- 1 | from .ma_cross_strategy import MACross 2 | from .ma_heikin_ashi import MAHeikinAshi 3 | from .rsi_hidden_divergence import RSIDivergence 4 | from .macd_divergence import MACDDivergence 5 | from .rsi_regular_divergence import RSIRegularDivergence 6 | from .break_strategy import BreakStrategy 7 | from .price_action import PriceAction 8 | from .trend_following import TrendFollowing 9 | -------------------------------------------------------------------------------- /configs/ma_cross_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbol": "XAUUSDm", 4 | "strategies": [ 5 | { 6 | "name": "ma_cross", 7 | "params": { 8 | "fast_ma": 10, 9 | "slow_ma": 20, 10 | "type": "SMA", 11 | "sl_fix_mode": "ADJ_SL" 12 | }, 13 | "tfs": { 14 | "tf": "15m" 15 | }, 16 | "max_sl_pct": 0.25, 17 | "volume": 0.01 18 | } 19 | ], 20 | "months": [ 21 | 1 22 | ], 23 | "year": 2023 24 | } 25 | ] -------------------------------------------------------------------------------- /strategy_utils.py: -------------------------------------------------------------------------------- 1 | from strategies import * 2 | 3 | strategies = { 4 | "ma_cross": "MACross", 5 | "ma_heikin_ashi": "MAHeikinAshi", 6 | "rsi_hidden_divergence": "RSIDivergence", 7 | "macd_divergence": "MACDDivergence", 8 | "rsi_regular_divergence": "RSIRegularDivergence", 9 | "break_strategy": "BreakStrategy", 10 | "price_action": "PriceAction", 11 | "trend_following": "TrendFollowing", 12 | "zigzag_follower": "ZigzagFollower", 13 | } 14 | 15 | 16 | def load_strategy(strategy_def): 17 | strategy_name = strategy_def["name"] 18 | return globals()[strategies[strategy_name]](strategy_name, strategy_def["params"], strategy_def["tfs"]) 19 | -------------------------------------------------------------------------------- /tuning_configs/heikin_ashi_tuning_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbols": ["BTCUSDT"], 4 | "name": "ma_heikin_ashi", 5 | "params": { 6 | "ha_smooth": [5, 7, 9], 7 | "fast_ma": [10, 20, 30], 8 | "slow_ma": [50, 70, 90], 9 | "type": ["SMA", "EMA"], 10 | "n_kline_trend": [20, 30, 40], 11 | "sl_fix_mode": ["IGNORE", "ADJ_SL"] 12 | }, 13 | "tfs": { 14 | "tf": "15m" 15 | }, 16 | "usdt": 200, 17 | "leverage": 20, 18 | "max_sl_pct": 2, 19 | "months": [ 20 | 1 21 | ], 22 | "year": 2023 23 | } 24 | ] -------------------------------------------------------------------------------- /logging_config.ini: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root, bot_logger 3 | 4 | [handlers] 5 | keys=console_handle, file_handle 6 | 7 | [formatters] 8 | keys=formatter 9 | 10 | [logger_root] 11 | level=WARNING 12 | class=StreamHandler 13 | handlers=console_handle 14 | 15 | [logger_bot_logger] 16 | level=DEBUG 17 | handlers=console_handle, file_handle 18 | qualname=bot_logger 19 | propagate=0 20 | 21 | [handler_console_handle] 22 | class=StreamHandler 23 | level=DEBUG 24 | formatter=formatter 25 | args=(sys.stdout,) 26 | 27 | [handler_file_handle] 28 | class=FileHandler 29 | level=DEBUG 30 | formatter=formatter 31 | args=("%(logfilename)s", "w", "utf-8") 32 | 33 | [formatter_formatter] 34 | format=%(asctime)s %(module)-20s:%(lineno)4d %(name)-12s %(message)s -------------------------------------------------------------------------------- /tuning_configs/break_strategy_tuning_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbols": ["BTCUSDT"], 4 | "name": "break_strategy", 5 | "params": { 6 | "up_trend_pct": [2.5, 3], 7 | "down_trend_pct": [1, 1.5], 8 | "down_pct_min": [1.5], 9 | "ma_vol": [25], 10 | "vol_ratio_ma": [2.5, 3, 3.5], 11 | "kline_body_ratio": [3, 4], 12 | "n_kline_trend": [70, 100, 120], 13 | "sl_pad_pct": [0.5], 14 | "sl_fix_mode": ["ADJ_SL"] 15 | }, 16 | "tfs": { 17 | "tf": "15m" 18 | }, 19 | "usdt": 200, 20 | "leverage": 20, 21 | "max_sl_pct": 1.5, 22 | "months": [ 23 | 1, 2, 3 24 | ], 25 | "year": 2023 26 | } 27 | ] -------------------------------------------------------------------------------- /configs/ma_hekin_ashi_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbol": "BTCUSDT", 4 | "strategies": [ 5 | { 6 | "name": "ma_heikin_ashi", 7 | "params": { 8 | "ha_smooth": 7, 9 | "fast_ma": 25, 10 | "slow_ma": 70, 11 | "type": "SMA", 12 | "n_kline_trend": 100, 13 | "sl_fix_mode": "ADJ_SL" 14 | }, 15 | "tfs": { 16 | "tf": "15m" 17 | }, 18 | "max_sl_pct": 1, 19 | "volume": 0.01 20 | } 21 | ], 22 | "months": [ 23 | 1, 24 | 2, 25 | 3 26 | ], 27 | "year": 2023 28 | } 29 | ] -------------------------------------------------------------------------------- /configs/price_action_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbol": "GBPUSDm", 4 | "strategies": [ 5 | { 6 | "name": "price_action", 7 | "params": { 8 | "min_num_cuml": 10, 9 | "min_zz_pct": 0.2, 10 | "zz_dev": 2, 11 | "ma_vol": 25, 12 | "vol_ratio_ma": 1.8, 13 | "kline_body_ratio": 2, 14 | "sl_fix_mode": "ADJ_SL" 15 | }, 16 | "tfs": { 17 | "tf": "15m" 18 | }, 19 | "max_sl_pct": 0.7, 20 | "volume": 0.01 21 | } 22 | ], 23 | "months": [ 24 | 1 25 | ], 26 | "year": 2023 27 | } 28 | ] -------------------------------------------------------------------------------- /configs/break_strategy_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbol": "XAUUSDm", 4 | "strategies": [ 5 | { 6 | "name": "break_strategy", 7 | "params": { 8 | "min_num_cuml": 10, 9 | "min_zz_pct": 0.5, 10 | "zz_dev": 2, 11 | "ma_vol": 25, 12 | "vol_ratio_ma": 1.8, 13 | "kline_body_ratio": 2.5, 14 | "sl_fix_mode": "ADJ_SL" 15 | }, 16 | "tfs": { 17 | "tf": "15m" 18 | }, 19 | "max_sl_pct": 0.75, 20 | "volume": 0.01 21 | } 22 | ], 23 | "months": [ 24 | 1, 25 | 2, 26 | 3, 27 | 4, 28 | 5 29 | ], 30 | "year": 2023 31 | } 32 | ] -------------------------------------------------------------------------------- /configs/trend_following_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbol": "XAUUSDm", 4 | "strategies": [ 5 | { 6 | "name": "trend_following", 7 | "params": { 8 | "min_zz_pct": 0.5, 9 | "min_order_zz_pct": 0.075, 10 | "max_last_trend_line": 3, 11 | "max_last_zigzag": 5, 12 | "delta_zz": 3, 13 | "delta_order": 1, 14 | "sl_fix_mode": "ADJ_ENTRY" 15 | }, 16 | "tfs": { 17 | "tf": "15m" 18 | }, 19 | "max_sl_pct": 0.3, 20 | "volume": 0.01 21 | } 22 | ], 23 | "months": [ 24 | 1, 25 | 2, 26 | 3, 27 | 4, 28 | 5 29 | ], 30 | "year": 2023 31 | } 32 | ] -------------------------------------------------------------------------------- /trade.py: -------------------------------------------------------------------------------- 1 | from order import Order 2 | 3 | 4 | class Trade: 5 | __trade_id__ = 10000 6 | 7 | def __init__(self, order: Order, volume): 8 | self.trade_id = Trade.__trade_id__ 9 | Trade.__trade_id__ += 1 10 | self.order = order 11 | self.volume = volume 12 | self.main_order_params = None 13 | self.close_order_params = None 14 | self.main_order = None 15 | self.close_order = None 16 | self.trace = [] 17 | 18 | def __to_dict__(self): 19 | trade_dict = self.order.__to_dict__() 20 | trade_dict["volume"] = self.volume 21 | trade_dict["main_order_params"] = self.main_order_params 22 | trade_dict["close_order_params"] = self.close_order_params 23 | trade_dict["main_order"] = self.main_order 24 | trade_dict["close_order"] = self.close_order 25 | trade_dict["trace"] = self.trace 26 | return trade_dict 27 | -------------------------------------------------------------------------------- /tuning_configs/rsi_hidden_divergence_tuning_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbols": ["BTCUSDT"], 4 | "name": "rsi_hidden_divergence", 5 | "params": { 6 | "rsi_len": [14], 7 | "delta_rsi": [3], 8 | "delta_price_pct": [0.2], 9 | "n_last_point": [5], 10 | "min_rr": [2], 11 | "min_rw_pct": [1.5], 12 | "min_zz_pct": [1.5], 13 | "min_trend_pct": [6], 14 | "min_updown_ratio": [0.65], 15 | "zz_type": "ZZ_CONV", 16 | "zz_conv_size": [4], 17 | "sl_fix_mode": ["IGNORE", "ADJ_SL", "ADJ_ENTRY"], 18 | "n_trend_point": [30] 19 | }, 20 | "tfs": { 21 | "tf": "15m" 22 | }, 23 | "usdt": 200, 24 | "leverage": 20, 25 | "max_sl_pct": 2, 26 | "months": [ 27 | 2, 3, 4 28 | ], 29 | "year": 2022 30 | } 31 | ] -------------------------------------------------------------------------------- /tuning_configs/rsi_regular_divergence_tuning_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbols": ["BTCUSDT"], 4 | "name": "rsi_regular_divergence", 5 | "params": { 6 | "rsi_len": [14], 7 | "delta_rsi": [3], 8 | "delta_price_pct": [0.2, 0.3, 0.4], 9 | "n_last_point": [7], 10 | "min_rr": [3], 11 | "min_rw_pct": [1.5], 12 | "min_zz_pct": [2], 13 | "min_trend_pct": [4], 14 | "min_updown_ratio": [0.65], 15 | "ob_rsi": [70, 75], 16 | "os_rsi": [25, 30], 17 | "fib_retr_lv": [0.5, 0.618, 0.786, 1], 18 | "zz_type": ["ZZ_CONV"], 19 | "zz_conv_size": [3], 20 | "sl_fix_mode": ["ADJ_ENTRY"], 21 | "n_trend_point": [24, 30] 22 | }, 23 | "tfs": { 24 | "tf": "15m" 25 | }, 26 | "usdt": 200, 27 | "leverage": 20, 28 | "max_sl_pct": 2, 29 | "months": [ 30 | 2, 3, 4 31 | ], 32 | "year": 2022 33 | } 34 | ] -------------------------------------------------------------------------------- /configs/rsi_hidden_divergence_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbol": "XAUUSDm", 4 | "strategies": [ 5 | { 6 | "name": "rsi_hidden_divergence", 7 | "params": { 8 | "delta_price_pct": 0.1, 9 | "delta_rsi": 2, 10 | "min_rr": 2, 11 | "min_rw_pct": 0.5, 12 | "min_trend_pct": 1, 13 | "min_updown_ratio": 0.65, 14 | "min_zz_pct": 0.5, 15 | "n_last_point": 5, 16 | "n_trend_point": 14, 17 | "rsi_len": 14, 18 | "sl_fix_mode": "ADJ_SL", 19 | "zz_conv_size": 3, 20 | "zz_type": "ZZ_DRC" 21 | }, 22 | "tfs": { 23 | "tf": "15m" 24 | }, 25 | "volume": 0.01, 26 | "max_sl_pct": 1 27 | } 28 | ], 29 | "months": [ 30 | 1 31 | ], 32 | "year": 2023 33 | } 34 | ] -------------------------------------------------------------------------------- /configs/rsi_regular_divergence_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbol": "XAUUSDm", 4 | "strategies": [ 5 | { 6 | "name": "rsi_regular_divergence", 7 | "params": { 8 | "rsi_len": 14, 9 | "delta_rsi": 2, 10 | "delta_price_pct": 0.1, 11 | "n_last_point": 5, 12 | "min_rr": 2, 13 | "min_rw_pct": 0.75, 14 | "min_zz_pct": 0.5, 15 | "ob_rsi": 70, 16 | "os_rsi": 25, 17 | "fib_retr_lv": 1, 18 | "zz_type": "ZZ_DRC", 19 | "zz_conv_size": 3, 20 | "sl_fix_mode": "ADJ_ENTRY", 21 | "n_trend_point": 40 22 | }, 23 | "tfs": { 24 | "tf": "15m" 25 | }, 26 | "volume": 0.01, 27 | "max_sl_pct": 0.25 28 | } 29 | ], 30 | "months": [ 31 | 1 32 | ], 33 | "year": 2023 34 | } 35 | ] -------------------------------------------------------------------------------- /tuning_configs/macd_divergence_tuning_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbols": ["BTCUSDT"], 4 | "name": "macd_divergence", 5 | "params": { 6 | "macd_inputs": [{ 7 | "fast_len": 8, 8 | "slow_len": 17, 9 | "signal": 5 10 | }], 11 | "macd_type": ["MACD_SIGNAL"], 12 | "delta_macd": [10, 20], 13 | "delta_price_pct": [0.2, 0.4], 14 | "n_last_point": [5], 15 | "min_rr": [2], 16 | "min_rw_pct": [1.5, 2], 17 | "type": ["BOTH"], 18 | "min_zz_pct": [1.5, 2], 19 | "min_trend_pct": [4], 20 | "min_updown_ratio": [0.5], 21 | "zz_type": ["ZZ_CONV"], 22 | "zz_conv_size": [3, 4], 23 | "sl_fix_mode": ["ADJ_ENTRY"], 24 | "n_trend_point": [20] 25 | }, 26 | "tfs": { 27 | "tf": "15m" 28 | }, 29 | "usdt": 200, 30 | "leverage": 20, 31 | "max_sl_pct": 2, 32 | "months": [ 33 | 2, 3, 4 34 | ], 35 | "year": 2022 36 | } 37 | ] -------------------------------------------------------------------------------- /configs/macd_trading_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbol": "XAUUSDm", 4 | "strategies": [ 5 | { 6 | "name": "macd_divergence", 7 | "params": { 8 | "delta_macd": 2, 9 | "delta_price_pct": 0.1, 10 | "macd_inputs": { 11 | "fast_len": 12, 12 | "signal": 9, 13 | "slow_len": 26 14 | }, 15 | "macd_type": "MACD", 16 | "min_rr": 2, 17 | "min_rw_pct": 1, 18 | "min_trend_pct": 0.5, 19 | "min_updown_ratio": 0.5, 20 | "min_zz_pct": 0.5, 21 | "n_last_point": 5, 22 | "n_trend_point": 14, 23 | "sl_fix_mode": "ADJ_SL", 24 | "zz_conv_size": 3, 25 | "zz_type": "ZZ_DRC" 26 | }, 27 | "tfs": { 28 | "tf": "15m" 29 | }, 30 | "max_sl_pct": 0.75, 31 | "volume": 0.01 32 | } 33 | ], 34 | "months": [ 35 | 1, 2, 3, 4, 5 36 | ], 37 | "year": 2023 38 | } 39 | ] -------------------------------------------------------------------------------- /exchange_loader.py: -------------------------------------------------------------------------------- 1 | import json 2 | from exchange import * 3 | 4 | 5 | class ExchangeLoader: 6 | __exchanges__ = {} 7 | __exchanges_map__ = { 8 | "mt5": "MT5API", 9 | } 10 | 11 | def __init__(self, exc_cfg_file): 12 | with open(exc_cfg_file) as f: 13 | self.exchange_configs = json.load(f) 14 | 15 | def get_exchange(self, exchange_name): 16 | if exchange_name not in ExchangeLoader.__exchanges__: 17 | ExchangeLoader.__exchanges__[exchange_name] = self.__load_exchange__(exchange_name) 18 | return ExchangeLoader.__exchanges__[exchange_name] 19 | 20 | def __load_exchange__(self, exchange_name): 21 | exchange_name_config = self.exchange_configs[exchange_name] 22 | return globals()[ExchangeLoader.__exchanges_map__[exchange_name]](exchange_name_config) 23 | 24 | 25 | class OMSLoader: 26 | __oms__ = {} 27 | __oms_map__ = { 28 | "mt5": "MT5OMS", 29 | } 30 | 31 | def get_oms(self, exchange_name, exchange): 32 | if exchange_name not in OMSLoader.__oms__: 33 | OMSLoader.__oms__[exchange_name] = self.__load_oms__(exchange_name, exchange) 34 | return OMSLoader.__oms__[exchange_name] 35 | 36 | def __load_oms__(self, exchange_name, exchange): 37 | return globals()[OMSLoader.__oms_map__[exchange_name]](exchange) 38 | -------------------------------------------------------------------------------- /configs/strategies_def.py: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "ma_heikin_ashi", 4 | "params": { 5 | "ha_smooth": int, 6 | "fast_ma": int, 7 | "slow_ma": int, 8 | "type": ["EMA", "SMA"], 9 | "n_kline_trend": int 10 | } 11 | }, 12 | { 13 | "name": "rsi_hidden_divergence", 14 | "params": { 15 | "rsi_len": int, # RSI length, Ex 14 16 | "delta_rsi": int, # delta for compare RSI, rsi_1 < rsi_2 <-> rsi_1 + delta_rsi < rsi_2 , Ex 3 17 | "delta_price_pct": float, # price percent for compare price, price_1 < price_2 <-> price_2 > (1 + delta_price_pct / 100) * price_!, Ex 0.2 18 | "n_last_point": int, # number of zz_points lookback to search for hidden divergence 19 | "min_rr": int, 20 | "min_rw_pct": float, 21 | "type": ["DIVER", "HIDDEN_DIVER", "BOTH"], 22 | "min_zz_pct": float, # minimum of zigzag strength 23 | "min_trend_pct": float, # minimum price percent to determine a trend, Ex 6% 24 | "min_updown_ratio": float, # maximum close price follow up/down trend lines [0 - 1], Ex 0.5 25 | "zz_type": ["ZZ_CONV", "ZZ_DRC"], 26 | "zz_conv_size": int, 27 | "sl_fix_mode": ["IGNORE, ADJ_SL, ADJ_ENTRY"], 28 | "n_trend_point": int, # number of zz_points to determine up/down trend 29 | } 30 | }, 31 | { 32 | "name": "macd_divergence", 33 | "params": { 34 | "macd_inputs": {"fast_len": int, "slow_len": int, "signal": int}, 35 | "macd_type": ["MACD", "MACD_SIGNAL", "MACD_HIST"], 36 | "delta_macd": int, 37 | "delta_price_pct": float, 38 | "n_last_point": int, 39 | "min_rr": float, 40 | "min_rw_pct": float, 41 | "type": ["DIVER", "HIDDEN_DIVER", "BOTH"], 42 | "min_zz_pct": float, 43 | "min_trend_pct": float, 44 | "min_updown_ratio": float, 45 | "zz_type": ["ZZ_CONV", "ZZ_DRC"], 46 | "zz_conv_size": int, 47 | "sl_fix_mode": ["IGNORE, ADJ_SL, ADJ_ENTRY"], 48 | "n_trend_point": int, 49 | } 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /exchange/mt5_api.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | from datetime import datetime, timedelta 4 | import pandas as pd 5 | import MetaTrader5 as mt5 6 | 7 | bot_logger = logging.getLogger("bot_logger") 8 | 9 | 10 | class MT5API: 11 | def __init__(self, config): 12 | self.config = config 13 | 14 | def initialize(self): 15 | # connect to MetaTrader 5 16 | if not mt5.initialize(): 17 | mt5.shutdown() 18 | return False 19 | return True 20 | 21 | def login(self): 22 | # connect to the trade account specifying a server 23 | authorized = mt5.login(self.config["account"], server=self.config["server"]) 24 | if authorized: 25 | bot_logger.info("[+] Login success, account info: ") 26 | account_info = mt5.account_info()._asdict() 27 | bot_logger.info(account_info) 28 | return True 29 | else: 30 | bot_logger.info("[-] Login failed, check account infor") 31 | return False 32 | 33 | def round_price(self, symbol, price): 34 | symbol_info = mt5.symbol_info(symbol) 35 | return round(price, symbol_info.digits) 36 | 37 | def get_assets_balance(self, assets=["USD"]): 38 | assets = {} 39 | return assets 40 | 41 | def tick_ask_price(self, symbol): 42 | return mt5.symbol_info_tick(symbol).ask 43 | 44 | def tick_bid_price(self, symbol): 45 | return mt5.symbol_info_tick(symbol).bid 46 | 47 | def klines(self, symbol: str, interval: str, **kwargs): 48 | symbol_rates = mt5.copy_rates_from_pos( 49 | symbol, getattr(mt5, "TIMEFRAME_" + (interval[-1:] + interval[:-1]).upper()), 0, kwargs["limit"] 50 | ) 51 | df = pd.DataFrame(symbol_rates) 52 | df["time"] += -time.timezone 53 | df["time"] = pd.to_datetime(df["time"], unit="s") 54 | df.columns = ["Open time", "Open", "High", "Low", "Close", "Volume", "Spread", "Real_Volume"] 55 | df = df[["Open time", "Open", "High", "Low", "Close", "Volume"]] 56 | return df 57 | 58 | def place_order(self, params): 59 | return mt5.order_send(params) 60 | 61 | def history_deals_get(self, position_id): 62 | return mt5.history_deals_get(position=position_id) 63 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from datetime import datetime 4 | import argparse 5 | import logging 6 | import logging.config 7 | from trade_engine import TradeEngine 8 | from backtest import BackTest 9 | from utils import datetime_to_filename 10 | 11 | 12 | def config_logging(exchange): 13 | if not os.path.isdir(os.path.join(os.environ["LOG_DIR"], exchange)): 14 | os.makedirs(os.path.join(os.environ["LOG_DIR"], exchange), exist_ok=True) 15 | curr_time = datetime.now() 16 | logging.config.fileConfig( 17 | "logging_config.ini", 18 | defaults={"logfilename": "logs/{}/bot_{}.log".format(exchange, datetime_to_filename(curr_time))}, 19 | ) 20 | logging.getLogger().setLevel(logging.WARNING) 21 | 22 | 23 | if __name__ == "__main__": 24 | parser = argparse.ArgumentParser(description="Monn auto trading bot") 25 | parser.add_argument("--mode", required=True, type=str, choices=["live", "test"]) 26 | parser.add_argument("--exch", required=True, type=str) 27 | parser.add_argument("--exch_cfg_file", required=True, type=str) 28 | parser.add_argument("--sym_cfg_file", required=True, type=str) 29 | parser.add_argument("--data_dir", required=False, type=str) 30 | args = parser.parse_args() 31 | 32 | os.environ["DEBUG_DIR"] = "debug" 33 | os.environ["LOG_DIR"] = "logs" 34 | config_logging(args.exch) 35 | 36 | if not os.path.isdir(os.environ["DEBUG_DIR"]): 37 | os.mkdir(os.environ["DEBUG_DIR"]) 38 | 39 | if args.mode == "live": 40 | trade_engine = TradeEngine(args.exch, args.exch_cfg_file, args.sym_cfg_file) 41 | if trade_engine.init(): 42 | trade_engine.start() 43 | try: 44 | while True: 45 | time.sleep(1) 46 | except (KeyboardInterrupt, SystemExit): 47 | trade_engine.stop() 48 | trade_engine.summary_trade_result() 49 | trade_engine.log_all_trades() 50 | time.sleep(3) # Wait for exchange return income 51 | trade_engine.log_income_history() 52 | elif args.mode == "test": 53 | start_time = time.time() 54 | backtest_engine = BackTest(args.exch, args.sym_cfg_file, args.data_dir) 55 | backtest_engine.start() 56 | backtest_engine.summary_trade_result() 57 | backtest_engine.stop() 58 | end_time = time.time() 59 | print("|--------------------------------") 60 | print(" Backtest finished, time: {:.4f}".format(end_time - start_time)) 61 | print("|--------------------------------") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | launch.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | Draft.ipynb 132 | *.csv 133 | test_symbol_trading_config.json 134 | ReportTuning.ipynb -------------------------------------------------------------------------------- /indicators/heikin_ashi.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | 4 | def heikin_ashi(df_src, smooth=1): 5 | # calculate HeikinAshi candelsticks 6 | # return DataFrame("Open", "High", "Low", "Close") 7 | """ 8 | - Calculate HeikinAshi(HA) candelstick 9 | - Formula of HA 10 | 1. The Heikin-Ashi Close is simply an average of the open, 11 | high, low and close for the current period. 12 | 13 | HA-Close = (Open(0) + High(0) + Low(0) + Close(0)) / 4 14 | 15 | 2. The Heikin-Ashi Open is the average of the prior Heikin-Ashi 16 | candlestick open plus the close of the prior Heikin-Ashi candlestick. 17 | 18 | HA-Open = (HA-Open(-1) + HA-Close(-1)) / 2 19 | 20 | 3. The Heikin-Ashi High is the maximum of three data points: 21 | the current period's high, the current Heikin-Ashi 22 | candlestick open or the current Heikin-Ashi candlestick close. 23 | 24 | HA-High = Maximum of the High(0), HA-Open(0) or HA-Close(0) 25 | 26 | 4. The Heikin-Ashi low is the minimum of three data points: 27 | the current period's low, the current Heikin-Ashi 28 | candlestick open or the current Heikin-Ashi candlestick close. 29 | 30 | HA-Low = Minimum of the Low(0), HA-Open(0) or HA-Close(0) 31 | - Ref: https://school.stockcharts.com/doku.php?id=chart_analysis:heikin_ashi 32 | - Custom smoothed version: 33 | HA-Open = Average of smooth HA-Open previous candelsticks(origin smooth=1) 34 | """ 35 | ha = [] 36 | for i, row in df_src.iterrows(): 37 | if i < smooth: 38 | ha.append( 39 | ( 40 | (row["Open"] + row["Close"]) / 2, # HA open 41 | row["High"], # HA high 42 | row["Low"], # HA low 43 | (row["Open"] + row["High"] + row["Low"] + row["Close"]) / 4, # HA close 44 | ) 45 | ) 46 | else: 47 | prev_ha = ha[i - smooth :] 48 | ha_close = (row["Open"] + row["High"] + row["Low"] + row["Close"]) / 4 49 | ha_open = sum([(ph[0] + ph[3]) / 2 for ph in prev_ha]) / len(prev_ha) 50 | ha_high = max(row["High"], ha_open, ha_close) 51 | ha_low = min(row["Low"], ha_open, ha_close) 52 | ha.append((ha_open, ha_high, ha_low, ha_close)) 53 | return pd.DataFrame(ha, columns=["Open", "High", "Low", "Close"]) 54 | 55 | def heikin_ashi_stream(ha_df, last_kline, smooth=1): 56 | # return HA of last_kline 57 | prev_ha = ha_df[-smooth :] 58 | ha_close = (last_kline["Open"] + last_kline["High"] + last_kline["Low"] + last_kline["Close"]) / 4 59 | ha_open = sum([(ph["Open"] + ph["Close"]) / 2 for _, ph in prev_ha.iterrows()]) / len(prev_ha) 60 | ha_high = max(last_kline["High"], ha_open, ha_close) 61 | ha_low = min(last_kline["Low"], ha_open, ha_close) 62 | return pd.DataFrame([(ha_open, ha_high, ha_low, ha_close)], columns=["Open", "High", "Low", "Close"]) -------------------------------------------------------------------------------- /configs/symbols_trading_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "symbol": "XAUUSDm", 4 | "strategies": [ 5 | { 6 | "name": "break_strategy", 7 | "params": { 8 | "min_num_cuml": 10, 9 | "min_zz_pct": 0.5, 10 | "zz_dev": 2.5, 11 | "ma_vol": 25, 12 | "vol_ratio_ma": 1.8, 13 | "kline_body_ratio": 2.5, 14 | "sl_fix_mode": "ADJ_SL" 15 | }, 16 | "tfs": { 17 | "tf": "15m" 18 | }, 19 | "max_sl_pct": 0.75, 20 | "volume": 0.01 21 | }, 22 | { 23 | "name": "macd_divergence", 24 | "params": { 25 | "delta_macd": 2, 26 | "delta_price_pct": 0.1, 27 | "macd_inputs": { 28 | "fast_len": 12, 29 | "signal": 9, 30 | "slow_len": 26 31 | }, 32 | "macd_type": "MACD", 33 | "min_rr": 2, 34 | "min_rw_pct": 1, 35 | "min_trend_pct": 0.5, 36 | "min_updown_ratio": 0.5, 37 | "min_zz_pct": 0.5, 38 | "n_last_point": 5, 39 | "n_trend_point": 14, 40 | "sl_fix_mode": "ADJ_SL", 41 | "zz_conv_size": 3, 42 | "zz_type": "ZZ_DRC" 43 | }, 44 | "tfs": { 45 | "tf": "15m" 46 | }, 47 | "max_sl_pct": 0.75, 48 | "volume": 0.01 49 | }, 50 | { 51 | "name": "rsi_hidden_divergence", 52 | "params": { 53 | "delta_price_pct": 0.1, 54 | "delta_rsi": 2, 55 | "min_rr": 2, 56 | "min_rw_pct": 0.5, 57 | "min_trend_pct": 1, 58 | "min_updown_ratio": 0.65, 59 | "min_zz_pct": 0.5, 60 | "n_last_point": 5, 61 | "n_trend_point": 14, 62 | "rsi_len": 14, 63 | "sl_fix_mode": "ADJ_SL", 64 | "zz_conv_size": 3, 65 | "zz_type": "ZZ_DRC" 66 | }, 67 | "tfs": { 68 | "tf": "15m" 69 | }, 70 | "volume": 0.01, 71 | "max_sl_pct": 1 72 | }, 73 | { 74 | "name": "rsi_regular_divergence", 75 | "params": { 76 | "rsi_len": 14, 77 | "delta_rsi": 2, 78 | "delta_price_pct": 0.1, 79 | "n_last_point": 5, 80 | "min_rr": 2, 81 | "min_rw_pct": 0.75, 82 | "min_zz_pct": 0.5, 83 | "ob_rsi": 70, 84 | "os_rsi": 25, 85 | "fib_retr_lv": 1, 86 | "zz_type": "ZZ_DRC", 87 | "zz_conv_size": 3, 88 | "sl_fix_mode": "ADJ_ENTRY", 89 | "n_trend_point": 40 90 | }, 91 | "tfs": { 92 | "tf": "15m" 93 | }, 94 | "volume": 0.01, 95 | "max_sl_pct": 0.25 96 | } 97 | ], 98 | "months": [ 99 | 1, 100 | 2, 101 | 3, 102 | 4, 103 | 5 104 | ], 105 | "year": 2023 106 | } 107 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trading Bot using MetaTrader5 API 2 | 3 | This repository contains a trading bot that utilizes the MetaTrader5 API for automated trading in the financial markets. The bot is designed to execute trades based on pre-defined strategies and market conditions. 4 | 5 | ## Table of Contents 6 | - [Features](features) 7 | - [Prerequisites](prerequisites) 8 | - [Installation](#installation) 9 | - [Configuration](#configuration) 10 | - [Usage](#usage) 11 | - [Disclaimer](#disclaimer) 12 | 13 | ## Features 14 | 1. Multiple Strategies: You can configure and run multiple strategies simultaneously. Each strategy will be analyzed independently, and the bot will make trading decisions accordingly. 15 | 2. Multiple Timeframes: The bot can analyze multiple timeframes simultaneously. By specifying different timeframes in the config file, the bot will gather data from those timeframes to make more informed trading decisions. 16 | 3. Backtesting: You can backtest a strategy before running it live. The bot provides a backtesting module that allows you to test your strategies against historical data and evaluate their performance(using MT5 notebook to download historical data). 17 | 4. Parameter Tuning: The bot provides flexibility in tuning strategy parameters. You can tuning the parameters to optimize your strategies based on market conditions and historical performance. 18 | 5. Custom Strategy: The bot is designed to make it easy to add custom strategies. You can create your own strategy and adding it to the config file. 19 | 20 | ## Prerequisites 21 | Before using the trading bot, make sure you have the following: 22 | - MetaTrader 5 platform installed on your computer 23 | - Active trading account with a broker that supports MetaTrader 5 24 | - Basic knowledge of trading concepts and strategies 25 | 26 | ## Installation 27 | 1. Clone the repository to your local machine: 28 | ```bash 29 | git clone https://github.com/Zsunflower/MetaTrader5-auto-trading-bot 30 | 2. Install the required dependencies: 31 | ```bash 32 | pip install -r requirements.txt 33 | 3. Install the TA-Lib library by following the installation guide provided on the [TA-Lib GitHub repository](https://github.com/TA-Lib/ta-lib-python). 34 | ## Configuration 35 | 1. Log in to your MetaTrader 5 account using the MetaTrader 5 platform. 36 | 2. Edit the 'configs/exchange_config.json' file and update the following: 37 | - account: Your MetaTrader 5 account number. 38 | - server: The server name for your MetaTrader 5 account. 39 | 40 | ## Usage 41 | 1. Edit all strategies's parameters in 'configs/symbols_trading_config.json' file. 42 | 2. Start the trading bot in live mode by running the following command: 43 | ```bash 44 | python main.py --mode live --exch mt5 --exch_cfg_file configs/exchange_config.json --sym_cfg_file configs/symbols_trading_config.json 45 | 46 | 3. For backtesting a strategy, use the following command: 47 | ```bash 48 | python main.py --mode test --exch mt5 --exch_cfg_file configs/exchange_config.json --sym_cfg_file --data_dir 49 | 50 | Backtest result of each strategy will be output like this: 51 | ![Screenshot 1](debug/test.jpg) 52 | ![Screenshot 1](debug/test2.jpg) 53 | 54 | The bot will generate an HTML file that visualizes the generated orders in 'debug' folder. [View example](https://1drv.ms/f/s!AtOy_2VZv2ojo3CdJpdBvbBtZBdP?e=7eyLd4) 55 | 56 | 4. For tuning a strategy's parameters, use the following command: 57 | ```bash 58 | python tuning.py --sym_cfg_file --data_dir 59 | 60 | ## Disclaimer 61 | Trading in financial markets involves risks, and the trading bot provided in this project is for educational and informational purposes only. The use of this bot is at your own risk, and the developers cannot be held responsible for any financial losses incurred. 62 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from tabulate import tabulate 3 | import numpy as np 4 | from scipy.optimize import linprog 5 | 6 | 7 | tf_cron = { 8 | "4h": {"minute": [0], "hour": [3, 7, 11, 15, 19, 23]}, 9 | "2h": {"minute": [0], "hour": [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23]}, 10 | "1h": {"minute": [0]}, 11 | "30m": {"minute": [0, 30]}, 12 | "15m": {"minute": [0, 15, 30, 45]}, 13 | "5m": {"minute": [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]}, 14 | "3m": { 15 | "minute": [ 16 | 0, 17 | 3, 18 | 6, 19 | 9, 20 | 12, 21 | 15, 22 | 18, 23 | 21, 24 | 24, 25 | 27, 26 | 30, 27 | 33, 28 | 36, 29 | 39, 30 | 42, 31 | 45, 32 | 48, 33 | 51, 34 | 54, 35 | 57, 36 | ] 37 | }, 38 | "1m": {}, 39 | } 40 | 41 | NUM_KLINE_INIT = 300 42 | CANDLE_COLUMNS = [ 43 | "Open time", 44 | "Open", 45 | "High", 46 | "Low", 47 | "Close", 48 | "Volume", 49 | "Close time", 50 | "Quote asset volume", 51 | "Number of trades", 52 | "Taker buy base asset volume", 53 | "Taker buy quote asset volume", 54 | "Ignore", 55 | ] 56 | 57 | 58 | def datetime_to_filename(dt): 59 | return str(dt).replace(" ", "-").replace(":", "-") 60 | 61 | 62 | def timestamp_to_datetime(df, columns): 63 | for column in columns: 64 | df[column] = df[column].apply(lambda timestamp: datetime.fromtimestamp(timestamp / 1000.0)) 65 | 66 | 67 | def get_pretty_table(df, title_name, transpose=False, tran_col=None): 68 | if transpose: 69 | df_T = df.drop([tran_col], axis=1).T 70 | df_T.columns = df[tran_col] 71 | df = df_T 72 | table_stats = tabulate(df, headers="keys", tablefmt="fancy_grid") 73 | title = "╒" + " {} ".format(title_name).center(len(table_stats.splitlines()[0]) - 2, "═") + "╕" 74 | table_stats_split = table_stats.splitlines() 75 | table_stats_split[0] = title 76 | return "\n" + "\n".join(table_stats_split) 77 | 78 | 79 | def get_line_coffs(a, c): 80 | # calculate the slope and y-intercept of the line passing through a and c 81 | slope = (c[1] - a[1]) / (c[0] - a[0]) 82 | intercept = a[1] - slope * a[0] 83 | return slope, intercept 84 | 85 | 86 | # ----------------------------------------------------------------------------------------# 87 | def parse_line_coffs(points): 88 | x0 = points[0][0] 89 | xn = points[-1][0] 90 | X = [ 91 | [ 92 | (point[0] - x0) / (xn - x0), 93 | (xn - point[0]) / (xn - x0), 94 | ] 95 | for point in points 96 | ] 97 | Y = [point[1] for point in points] 98 | return np.array(X), np.array(Y) 99 | 100 | 101 | def get_y_on_line(line, xi): 102 | # line (x0, y0), (xn, yn) 103 | # x0/xi/xn: datetime 104 | (x0, y0), (xn, yn) = line 105 | yi = ((xi - x0) / (xn - x0)) * yn + ((xn - xi) / (xn - x0)) * y0 106 | return yi 107 | 108 | 109 | def find_uptrend_line(poke_points): 110 | # poke_points: list (xi, yi) 111 | # return: ((x0d, y0d), (xnd, ynd)) 112 | 113 | X, Y = parse_line_coffs(poke_points) 114 | obj = [-X[:, 0].sum(), -X[:, 1].sum()] 115 | lhs_ineq = X 116 | rhs_ineq = Y 117 | opt = linprog(c=obj, A_ub=lhs_ineq, b_ub=rhs_ineq, method="highs") 118 | yn, y0 = opt.x 119 | return ((poke_points[0][0], y0), (poke_points[-1][0], yn)) 120 | 121 | 122 | def is_cross_line(line, kline): 123 | # line: ((x0d, y0d), (xnd, ynd)) 124 | ykline = get_y_on_line(line, kline["Open time"]) 125 | return (line[0][1] - ykline) * (line[1][1] - ykline) < 0 126 | 127 | 128 | def find_downtrend_line(peak_points): 129 | # peak_points: list (xi, yi) 130 | # return: ((x0u, y0u), (xnu, ynu)) 131 | 132 | X, Y = parse_line_coffs(peak_points) 133 | obj = [X[:, 0].sum(), X[:, 1].sum()] 134 | lhs_ineq = -X 135 | rhs_ineq = -Y 136 | opt = linprog(c=obj, A_ub=lhs_ineq, b_ub=rhs_ineq, method="highs") 137 | yn, y0 = opt.x 138 | return ((peak_points[0][0], y0), (peak_points[-1][0], yn)) 139 | 140 | 141 | # ----------------------------------------------------------------------------------------# 142 | -------------------------------------------------------------------------------- /MT5.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from datetime import datetime, timezone\n", 10 | "import MetaTrader5 as mt5\n" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "import os\n", 20 | "import time\n", 21 | "import pandas as pd\n", 22 | "import pprint\n", 23 | "from plotly.subplots import make_subplots\n", 24 | "import plotly.graph_objects as go\n" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "# connect to MetaTrader 5\n", 34 | "if not mt5.initialize():\n", 35 | " print(\"initialize() failed\")\n", 36 | " mt5.shutdown()\n" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "# request connection status and parameters\n", 46 | "pprint.pprint(mt5.terminal_info()._asdict())\n", 47 | "# get data on MetaTrader 5 version\n", 48 | "print(mt5.version())\n" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "# connect to the trade account specifying a server\n", 58 | "authorized = mt5.login(122334, server=\"Exness-MT5Trial7\")\n", 59 | "if authorized:\n", 60 | " account_info = mt5.account_info()\n", 61 | " if account_info != None:\n", 62 | " account_info_dict = account_info._asdict()\n", 63 | " # convert the dictionary into DataFrame and print\n", 64 | " df = pd.DataFrame(list(account_info_dict.items()), columns=[\"property\", \"value\"])\n", 65 | " print(df)\n", 66 | "else:\n", 67 | " print(\"failed to connect to trade account, error code =\", mt5.last_error())\n" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "# get bars\n", 77 | "import calendar\n", 78 | "\n", 79 | "symbols = [\"XAUUSDm\", \"GBPUSDm\", \"EURUSDm\", \"EURCADm\"]\n", 80 | "interval = \"15m\"\n", 81 | "tf = mt5.TIMEFRAME_M15\n", 82 | "months = [1, 2, 3, 4, 5]\n", 83 | "year = 2023\n", 84 | "\n", 85 | "for symbol in symbols:\n", 86 | " for month in months:\n", 87 | " from_date = datetime(year, month, 1).replace(tzinfo=timezone.utc).timestamp()\n", 88 | " to_date = datetime(year, month, calendar.monthrange(year, month)[1]).replace(tzinfo=timezone.utc).timestamp()\n", 89 | "\n", 90 | " df = mt5.copy_rates_range(symbol, tf, from_date, to_date)\n", 91 | " df = pd.DataFrame(df)\n", 92 | " df[\"time\"] += -time.timezone\n", 93 | " df[\"time\"] = pd.to_datetime(df[\"time\"], unit=\"s\")\n", 94 | " print(\"{}, from: {} to: {}\".format(symbol, df.iloc[0][\"time\"], df.iloc[-1][\"time\"]))\n", 95 | "\n", 96 | " df.columns = [\"Open time\", \"Open\", \"High\", \"Low\", \"Close\", \"Volume\", \"Spread\", \"Real_Volume\"]\n", 97 | " df = df[[\"Open time\", \"Open\", \"High\", \"Low\", \"Close\", \"Volume\"]]\n", 98 | " filename = \"{}-{}-{}-{:02d}\".format(symbol, interval, year, month)\n", 99 | " df.to_csv(r\"G:\\AI\\MT5\\{}.csv\".format(filename), index=0)" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [] 108 | } 109 | ], 110 | "metadata": { 111 | "kernelspec": { 112 | "display_name": "mon_bot", 113 | "language": "python", 114 | "name": "python3" 115 | }, 116 | "language_info": { 117 | "codemirror_mode": { 118 | "name": "ipython", 119 | "version": 3 120 | }, 121 | "file_extension": ".py", 122 | "mimetype": "text/x-python", 123 | "name": "python", 124 | "nbconvert_exporter": "python", 125 | "pygments_lexer": "ipython3", 126 | "version": "3.10.11" 127 | }, 128 | "orig_nbformat": 4 129 | }, 130 | "nbformat": 4, 131 | "nbformat_minor": 2 132 | } 133 | -------------------------------------------------------------------------------- /StatisticTuning.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "### Statistic Tuning" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "metadata": {}, 15 | "outputs": [], 16 | "source": [ 17 | "import os\n", 18 | "import pandas as pd" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "tuning_result_file = r\"G:\\AI\\MonPot\\debug\\break_strategy_tuning_config.csv\"" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "df = pd.read_csv(tuning_result_file, index_col=0)\n", 37 | "df.head()" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "nlargest = df.nlargest(5, \"TOTAL_PnL(%)\")\n", 47 | "nlargest" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "for i, row in nlargest.iterrows():\n", 57 | " print(row[\"params\"].replace(\"'\", '\"'), row[\"TOTAL_PnL(%)\"])" 58 | ] 59 | }, 60 | { 61 | "attachments": {}, 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "### PnL Cumulative Visualization" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "import os\n", 75 | "import pandas as pd\n", 76 | "from pandas import Timestamp\n", 77 | "from plotly.subplots import make_subplots\n", 78 | "import plotly.graph_objects as go" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "def visualize_PnL(order_paths, names, colors):\n", 88 | " fig = make_subplots()\n", 89 | " fig.update_layout(\n", 90 | " plot_bgcolor=\"#2E4053\",\n", 91 | " paper_bgcolor=\"#797D7F\",\n", 92 | " font=dict(color=\"#F7F9F9\"),\n", 93 | " xaxis=dict(showgrid=False),\n", 94 | " yaxis=dict(showgrid=False),\n", 95 | " )\n", 96 | " for order_path, name, color in zip(order_paths, names, colors):\n", 97 | " df_od = pd.read_csv(order_path)\n", 98 | " cum_PnL = []\n", 99 | " PnL = 0\n", 100 | " for i, row in df_od.iterrows():\n", 101 | " if row[\"status\"] == \"HIT_SL\":\n", 102 | " pnl = round(abs(row[\"sl\"] - row[\"entry\"]) / row[\"entry\"], 4)\n", 103 | " if (row[\"side\"] == \"SELL\" and row[\"sl\"] <= row[\"entry\"]) or (\n", 104 | " row[\"side\"] == \"BUY\" and row[\"sl\"] >= row[\"entry\"]\n", 105 | " ):\n", 106 | " PnL += pnl * 100\n", 107 | " else:\n", 108 | " PnL -= pnl * 100\n", 109 | " elif row[\"status\"] == \"HIT_TP\":\n", 110 | " PnL += row[\"reward_ratio\"] * 100\n", 111 | " elif row[\"status\"] == \"STOPPED\":\n", 112 | " PnL += eval(row[\"attrs\"])[\"PnL\"] * 100\n", 113 | " cum_PnL.append(PnL)\n", 114 | "\n", 115 | " fig.add_trace(\n", 116 | " go.Scatter(x=list(range(len(cum_PnL))), y=cum_PnL, mode=\"lines\", line=dict(color=color, width=2), name=name)\n", 117 | " )\n", 118 | " return fig\n" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "visualize_PnL(\n", 128 | " [r\"debug\\XAUUSDm\\XAUUSDm_orders.csv\"],\n", 129 | " [\"XAUUSDm\"],\n", 130 | " [\"green\"],\n", 131 | ")\n" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [] 140 | } 141 | ], 142 | "metadata": { 143 | "kernelspec": { 144 | "display_name": "mon_bot", 145 | "language": "python", 146 | "name": "python3" 147 | }, 148 | "language_info": { 149 | "codemirror_mode": { 150 | "name": "ipython", 151 | "version": 3 152 | }, 153 | "file_extension": ".py", 154 | "mimetype": "text/x-python", 155 | "name": "python", 156 | "nbconvert_exporter": "python", 157 | "pygments_lexer": "ipython3", 158 | "version": "3.10.10" 159 | }, 160 | "orig_nbformat": 4 161 | }, 162 | "nbformat": 4, 163 | "nbformat_minor": 2 164 | } 165 | -------------------------------------------------------------------------------- /backtest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime, timedelta 3 | import logging 4 | import json 5 | import shutil 6 | from typing import List 7 | import pandas as pd 8 | from trader import Trader 9 | from utils import tf_cron, NUM_KLINE_INIT, CANDLE_COLUMNS 10 | from utils import get_pretty_table 11 | 12 | 13 | bot_logger = logging.getLogger("bot_logger") 14 | 15 | 16 | class BackTest: 17 | def __init__(self, exch, symbols_trading_cfg_file, data_dir): 18 | self.exch = exch 19 | self.data_dir = data_dir 20 | self.bot_traders: List[Trader] = [] 21 | self.symbols_trading_cfg_file = symbols_trading_cfg_file 22 | self.debug_dir = os.environ["DEBUG_DIR"] 23 | 24 | def load_mt5_klines_monthly_data(self, symbol, interval, month, year): 25 | csv_data_path = os.path.join(self.data_dir, "{}-{}-{}-{:02d}.csv".format(symbol, interval, year, month)) 26 | df = pd.read_csv(csv_data_path) 27 | df["Open time"] = pd.to_datetime(df["Open time"]) 28 | return df 29 | 30 | def load_klines_monthly_data(self, symbol, interval, month, year): 31 | return self.load_mt5_klines_monthly_data(symbol, interval, month, year) 32 | 33 | def backtest_bot_trader(self, symbol_cfg): 34 | bot_trader = Trader(symbol_cfg) 35 | bot_trader.init_strategies() 36 | tfs_chart = {} 37 | # Load kline data for all required timeframes 38 | for tf in bot_trader.get_required_tfs(): 39 | chart_df = pd.concat( 40 | [ 41 | self.load_klines_monthly_data(symbol_cfg["symbol"], tf, month, symbol_cfg["year"]) 42 | for month in sorted(symbol_cfg["months"]) 43 | ], 44 | ignore_index=True, 45 | ) 46 | tfs_chart[tf] = chart_df 47 | max_time = max([tf_chart.iloc[NUM_KLINE_INIT - 1]["Open time"] for tf_chart in tfs_chart.values()]) 48 | end_time = max([tf_chart.iloc[-1]["Open time"] for tf_chart in tfs_chart.values()]) 49 | tfs_chart_init = {} 50 | for tf, tf_chart in tfs_chart.items(): 51 | tfs_chart_init[tf] = tf_chart[tf_chart["Open time"] <= max_time][-NUM_KLINE_INIT:] 52 | tfs_chart[tf] = tf_chart[tf_chart["Open time"] > max_time] 53 | start_index = tfs_chart_init[tf].index[0] 54 | tfs_chart_init[tf].index -= start_index 55 | tfs_chart[tf].index -= start_index 56 | bot_trader.init_chart(tfs_chart_init) 57 | bot_trader.attach_oms(None) # for backtesting don't need oms 58 | 59 | timer = max_time 60 | end_time = end_time 61 | bot_logger.info(" [+] Start timer from: {} to {}".format(timer, end_time)) 62 | required_tfs = [tf for tf in tf_cron.keys() if tf in bot_trader.get_required_tfs()] 63 | # c = 0 64 | while timer <= end_time: 65 | timer += timedelta(seconds=60) 66 | hour, minute = timer.hour, timer.minute 67 | for tf in required_tfs: 68 | cron_time = tf_cron[tf] 69 | if ("hour" not in cron_time or hour in cron_time["hour"]) and ( 70 | "minute" not in cron_time or minute in cron_time["minute"] 71 | ): 72 | last_kline = tfs_chart[tf][:1] 73 | tfs_chart[tf] = tfs_chart[tf][1:] 74 | bot_trader.on_kline(tf, last_kline) 75 | # c += 1 76 | # if c > 1000: 77 | # break 78 | return bot_trader 79 | 80 | def start(self): 81 | with open(self.symbols_trading_cfg_file) as f: 82 | symbols_config = json.load(f) 83 | c = 0 84 | bot_logger.info("[*] Start backtesting ...") 85 | for symbol_cfg in symbols_config: 86 | bot_logger.info("[+] Backtest bot for symbol: {}".format(symbol_cfg["symbol"])) 87 | bot_trader = self.backtest_bot_trader(symbol_cfg) 88 | self.bot_traders.append(bot_trader) 89 | c += 1 90 | if c >= 2: 91 | break 92 | bot_logger.info("[*] Backtesting finished") 93 | 94 | def summary_trade_result(self): 95 | final_backtest_stats = [] 96 | for bot_trader in self.bot_traders: 97 | bot_trader.close_opening_orders() 98 | backtest_stats = bot_trader.statistic_trade() 99 | bot_logger.info(get_pretty_table(backtest_stats, bot_trader.get_symbol_name(), transpose=True, tran_col="NAME")) 100 | backtest_stats.loc[len(backtest_stats) - 1, "NAME"] = bot_trader.get_symbol_name() 101 | final_backtest_stats.append(backtest_stats.loc[len(backtest_stats) - 1 :]) 102 | 103 | bot_trader.log_orders() 104 | bot_trader.plot_strategy_orders() 105 | 106 | table_stats = pd.concat(final_backtest_stats, axis=0, ignore_index=True) 107 | s = table_stats.sum(axis=0) 108 | table_stats.loc[len(table_stats)] = s 109 | table_stats.loc[len(table_stats) - 1, "NAME"] = "TOTAL" 110 | bot_logger.info(get_pretty_table(table_stats, "SUMMARY", transpose=True, tran_col="NAME")) 111 | return table_stats 112 | 113 | def stop(self): 114 | pass -------------------------------------------------------------------------------- /trader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import pandas as pd 4 | from strategy_utils import load_strategy 5 | from order import Order, OrderSide, OrderStatus, OrderType 6 | 7 | bot_logger = logging.getLogger("bot_logger") 8 | 9 | 10 | class Trader: 11 | # Trading bot for single symbol 12 | ### Example ### 13 | # { 14 | # "symbol": "XAUUSDm", 15 | # "strategies": [ 16 | # { 17 | # "name": "break_strategy", 18 | # "params": { 19 | # "min_num_cuml": 10, 20 | # "min_zz_pct": 0.5, 21 | # "zz_dev": 2, 22 | # "ma_vol": 25, 23 | # "vol_ratio_ma": 1.8, 24 | # "kline_body_ratio": 2, 25 | # "sl_fix_mode": "ADJ_SL" 26 | # }, 27 | # "tfs": { 28 | # "tf": "15m" 29 | # }, 30 | # "max_sl_pct": 1, # max sl percent(example: sl will not smaller than (1 - max_Sl_pct / 100) * entry in buy order), 0 for infinity sl 31 | # "volume": 0.01 32 | # } 33 | # ], 34 | # "months": [ 35 | # 1 36 | # ], 37 | # "year": 2023 38 | # } 39 | 40 | def __init__(self, json_cfg): 41 | self.json_cfg = json_cfg 42 | self.symbol_name = self.json_cfg["symbol"] 43 | self.required_tfs = {} 44 | self.strategies = [] 45 | self.log_dir = os.path.join(os.environ["DEBUG_DIR"], self.symbol_name) 46 | if not os.path.isdir(self.log_dir): 47 | os.mkdir(self.log_dir) 48 | 49 | def init_chart(self, tfs_chart): 50 | # tfs_chart: {"1h": chart_1h, "15m": chart_15m} 51 | self.tfs_chart = tfs_chart 52 | for strategy in self.strategies: 53 | strategy.attach(self.tfs_chart) 54 | strategy.attach_trader(self) 55 | 56 | def init_strategies(self): 57 | for strategy_def in self.json_cfg["strategies"]: 58 | strategy = load_strategy(strategy_def) 59 | if strategy.is_params_valid(): 60 | bot_logger.info( 61 | " [+] Load strategy: {}, params {} success".format(strategy_def["name"], strategy_def["params"]) 62 | ) 63 | strategy.set_volume(strategy_def["volume"]) 64 | strategy.set_max_sl_pct(strategy_def.get("max_sl_pct")) 65 | self.strategies.append(strategy) 66 | for tf in strategy_def["tfs"].values(): 67 | if tf in self.required_tfs: 68 | self.required_tfs[tf].append(strategy) 69 | else: 70 | self.required_tfs[tf] = [strategy] 71 | else: 72 | bot_logger.error( 73 | " [-] Load strategy: {}, params {} failed, invalid params".format( 74 | strategy_def["name"], strategy_def["params"] 75 | ) 76 | ) 77 | 78 | def attach_oms(self, oms): 79 | self.oms = oms 80 | 81 | def create_trade(self, order: Order, volume): 82 | if self.oms: 83 | bot_logger.info( 84 | " [+] Create new order, symbol: {}, strategy: {}".format(self.symbol_name, order["description"]) 85 | ) 86 | bot_logger.info(" - {}".format(order)) 87 | order["symbol"] = self.symbol_name 88 | order["trade_id"] = self.oms.create_trade(order, volume) 89 | 90 | def close_trade(self, order: Order): 91 | if self.oms: 92 | if "trade_id" in order: 93 | self.oms.close_trade(order["trade_id"]) 94 | 95 | def adjust_sl(self, order: Order, sl): 96 | if self.oms: 97 | if "trade_id" in order: 98 | self.oms.adjust_sl(order["trade_id"], sl) 99 | 100 | def get_required_tfs(self): 101 | return list(self.required_tfs.keys()) 102 | 103 | def get_symbol_name(self): 104 | return self.symbol_name 105 | 106 | def statistic_trade(self): 107 | # statistic closed trades for each strategy 108 | stats = [] 109 | for strategy in self.strategies: 110 | stats.append(strategy.summary_PnL()) 111 | df_stats = pd.DataFrame(stats) 112 | s = df_stats.sum(axis=0) 113 | s["AVG(%)"] = s["TOTAL_PnL(%)"] / s["TOTAL"] if s["TOTAL"] > 0 else 0 114 | df_stats.loc[len(df_stats)] = s 115 | df_stats.loc[len(df_stats) - 1, "NAME"] = "TOTAL" 116 | # bot_logger.info("\n" + 30 * "-" + " {} ".format(self.symbol_name) + 30 * "-" + "\n" + df_stats.to_string() + "\n" + 70 * "-") 117 | return df_stats 118 | 119 | def close_opening_orders(self): 120 | for strategy in self.strategies: 121 | strategy.close_opening_orders() 122 | 123 | def get_strategy_params(self): 124 | return [strategy.params for strategy in self.strategies] 125 | 126 | def fix_order(self, order: Order, sl_fix_mode, max_sl_pct): 127 | # set tp/sl follow config(max sl, min tp, min rr, sl_fix_mode) 128 | if max_sl_pct: 129 | # max_sl_pct was set 130 | max_sl_pct = max_sl_pct / 100 131 | max_sl = (1 - max_sl_pct) * order.entry if order.side == OrderSide.BUY else (1 + max_sl_pct) * order.entry 132 | if not order.has_sl(): 133 | order.adjust_sl(max_sl) 134 | return order 135 | if (order.side == OrderSide.BUY and order.sl >= max_sl) or ( 136 | order.side == OrderSide.SELL and order.sl <= max_sl 137 | ): 138 | return order 139 | if sl_fix_mode == "ADJ_SL": 140 | order.adjust_sl(max_sl) 141 | return order 142 | elif sl_fix_mode == "IGNORE": 143 | bot_logger.info(" [-] IGNORE order: {}".format(order)) 144 | return None 145 | elif sl_fix_mode == "ADJ_ENTRY": 146 | adjusted_entry = ( 147 | order.sl / (1 - max_sl_pct) if order.side == OrderSide.BUY else order.sl / (1 + max_sl_pct) 148 | ) 149 | order.adjust_entry(adjusted_entry) 150 | order.status = OrderStatus.PENDING 151 | order.type = OrderType.LIMIT 152 | return order 153 | return order 154 | 155 | def log_orders(self): 156 | df_orders = [] 157 | for strategy in self.strategies: 158 | df = pd.DataFrame([order.__to_dict__() for order in strategy.orders_closed]) 159 | df_orders.append(df) 160 | df_orders = pd.concat(df_orders) 161 | df_orders.to_csv(os.path.join(self.log_dir, "{}_orders.csv".format(self.symbol_name))) 162 | 163 | def plot_strategy_orders(self): 164 | for strategy in self.strategies: 165 | fig = strategy.plot_orders() 166 | fig.write_html( 167 | os.path.join(self.log_dir, "{}.html".format(strategy.get_name())), 168 | include_plotlyjs="https://cdn.plot.ly/plotly-latest.min.js", 169 | ) 170 | 171 | def on_kline(self, tf, kline): 172 | # update strategies 173 | self.tfs_chart[tf] = pd.concat([self.tfs_chart[tf], kline], ignore_index=True) 174 | for strategy in self.required_tfs[tf]: 175 | strategy.update(tf) 176 | -------------------------------------------------------------------------------- /trade_engine.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from datetime import datetime 4 | import logging 5 | import json 6 | from typing import List 7 | import pandas as pd 8 | import tzlocal 9 | from apscheduler.schedulers.background import BackgroundScheduler 10 | from trader import Trader 11 | from exchange_loader import ExchangeLoader, OMSLoader 12 | from utils import tf_cron, NUM_KLINE_INIT 13 | from utils import get_pretty_table 14 | 15 | 16 | bot_logger = logging.getLogger("bot_logger") 17 | 18 | 19 | class TradeEngine: 20 | def __init__(self, exchange_name, exc_cfg_file, symbols_trading_cfg_file): 21 | self.exchange_name = exchange_name 22 | self.exc_cfg_file = exc_cfg_file 23 | self.symbols_trading_cfg_file = symbols_trading_cfg_file 24 | self.sched = BackgroundScheduler(timezone=str(tzlocal.get_localzone())) 25 | self.required_tfs = [] 26 | self.last_updated_tfs = {} 27 | self.bot_traders: List[Trader] = [] 28 | 29 | def init(self): 30 | self.mt5_api = ExchangeLoader(self.exc_cfg_file).get_exchange(exchange_name=self.exchange_name) 31 | if self.mt5_api.initialize() and self.mt5_api.login(): 32 | bot_logger.info("[+] Init MT5 API success") 33 | else: 34 | bot_logger.info("[-] Init MT5 API failed, stop") 35 | return False 36 | self.oms = OMSLoader().get_oms(self.exchange_name, self.mt5_api) 37 | self.init_bot_traders(self.symbols_trading_cfg_file) 38 | return True 39 | 40 | def init_bot_traders(self, symbols_trading_cfg_file): 41 | with open(symbols_trading_cfg_file) as f: 42 | symbols_config = json.load(f) 43 | for symbol_cfg in symbols_config: 44 | bot_logger.info("[+] Create trading bot for symbol: {}".format(symbol_cfg["symbol"])) 45 | bot_trader = Trader(symbol_cfg) 46 | bot_trader.init_strategies() 47 | tfs_chart = {} 48 | for tf in bot_trader.get_required_tfs(): 49 | chart_df = self.mt5_api.klines(symbol_cfg["symbol"], tf, limit=NUM_KLINE_INIT + 1) 50 | chart_df = chart_df[:-1] 51 | tfs_chart[tf] = chart_df 52 | bot_logger.info( 53 | "[+] Init bot symbol: {}, tf: {}, from: {} to: {}".format( 54 | symbol_cfg["symbol"], tf, chart_df.iloc[0]["Open time"], chart_df.iloc[-1]["Open time"] 55 | ) 56 | ) 57 | self.last_updated_tfs[tf] = chart_df.iloc[-1]["Open time"] 58 | bot_trader.init_chart(tfs_chart) 59 | bot_trader.attach_oms(self.oms) 60 | self.required_tfs.extend(bot_trader.get_required_tfs()) 61 | self.bot_traders.append(bot_trader) 62 | self.required_tfs = set(self.required_tfs) 63 | # Sort timeframes from large to small 64 | self.required_tfs = [tf for tf in tf_cron.keys() if tf in self.required_tfs] 65 | bot_logger.info("[+] Required timeframes: {}".format(self.required_tfs)) 66 | bot_logger.info("[+] Last updated timeframes: {}".format(self.last_updated_tfs)) 67 | 68 | def __update_next_kline__(self): 69 | curr_time = datetime.now() 70 | hour, minute = curr_time.hour, curr_time.minute 71 | time_retry = 0 72 | for tf in self.required_tfs: 73 | cron_time = tf_cron[tf] 74 | if ("hour" not in cron_time or hour in cron_time["hour"]) and ( 75 | "minute" not in cron_time or minute in cron_time["minute"] 76 | ): 77 | bot_logger.info(" [+] Update tf: {}, time: {}".format(tf, curr_time)) 78 | last_updated_tf = self.last_updated_tfs[tf] 79 | for bot_trader in self.bot_traders: 80 | if tf in bot_trader.get_required_tfs(): 81 | while True: 82 | chart_df = self.mt5_api.klines(bot_trader.get_symbol_name(), tf, limit=2) 83 | last_kline = chart_df[:1] 84 | if last_kline.iloc[0]["Open time"] > self.last_updated_tfs[tf]: 85 | break 86 | time_retry += 0.1 87 | time.sleep(0.1) 88 | if time_retry > 10: 89 | bot_logger.info(" [+] Update next kline timeout: {}".format(curr_time)) 90 | break 91 | if last_kline.iloc[0]["Open time"] > self.last_updated_tfs[tf]: 92 | bot_logger.info( 93 | " [+] {}, kline: {}".format( 94 | bot_trader.get_symbol_name(), last_kline.iloc[0]["Open time"] 95 | ) 96 | ) 97 | bot_trader.on_kline(tf, last_kline) 98 | last_updated_tf = last_kline.iloc[0]["Open time"] 99 | self.last_updated_tfs[tf] = last_updated_tf 100 | 101 | def __oms_loop__(self): 102 | self.oms.monitor_trades() 103 | 104 | def start(self): 105 | bot_logger.info("[*] Start trading bot, time: {}".format(datetime.now())) 106 | self.sched.add_job( 107 | self.__update_next_kline__, 108 | "cron", 109 | args=[], 110 | max_instances=1, 111 | replace_existing=False, 112 | minute="0-59", 113 | second=1, 114 | ) 115 | self.sched.add_job(self.__oms_loop__, "interval", seconds=15) 116 | self.sched.start() 117 | 118 | def stop(self): 119 | bot_logger.info("[*] Stop trading bot, time: {}".format(datetime.now())) 120 | self.oms.close_all_trade() 121 | self.sched.shutdown(wait=True) 122 | 123 | def summary_trade_result(self): 124 | final_backtest_stats = [] 125 | for bot_trader in self.bot_traders: 126 | bot_trader.close_opening_orders() 127 | backtest_stats = bot_trader.statistic_trade() 128 | bot_logger.info(get_pretty_table(backtest_stats, bot_trader.get_symbol_name(), transpose=True, tran_col="NAME")) 129 | backtest_stats.loc[len(backtest_stats) - 1, "NAME"] = bot_trader.get_symbol_name() 130 | summary = backtest_stats.loc[len(backtest_stats) - 1 :] 131 | final_backtest_stats.append(summary) 132 | 133 | bot_trader.log_orders() 134 | bot_trader.plot_strategy_orders() 135 | 136 | table_stats = pd.concat(final_backtest_stats, axis=0, ignore_index=True) 137 | s = table_stats.sum(axis=0) 138 | table_stats.loc[len(table_stats)] = s 139 | table_stats.loc[len(table_stats) - 1, "NAME"] = "TOTAL" 140 | bot_logger.info(get_pretty_table(table_stats, "SUMMARY", transpose=True, tran_col="NAME")) 141 | return table_stats 142 | 143 | def log_all_trades(self): 144 | closed_trades, active_trades = self.oms.get_trades() 145 | df_closed = pd.DataFrame([trade.__to_dict__() for trade in closed_trades.values()]) 146 | df_closed.to_csv(os.path.join(os.environ["DEBUG_DIR"], "closed_trades.csv")) 147 | df_active = pd.DataFrame([trade.__to_dict__() for trade in active_trades.values()]) 148 | df_active.to_csv(os.path.join(os.environ["DEBUG_DIR"], "active_trades.csv")) 149 | 150 | def log_income_history(self): 151 | df_ic = self.oms.get_income_history() 152 | if len(df_ic) > 0: 153 | bot_logger.info(get_pretty_table(df_ic, "INCOME SUMMARY")) 154 | df_ic.to_csv(os.path.join(os.environ["DEBUG_DIR"], "income_summary.csv")) 155 | -------------------------------------------------------------------------------- /tuning.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from datetime import datetime, timedelta 4 | import logging 5 | import logging.config 6 | import json 7 | import itertools 8 | import tempfile 9 | import zipfile 10 | import shutil 11 | from multiprocessing import cpu_count 12 | from multiprocessing.pool import ThreadPool as Pool 13 | from typing import List 14 | import pandas as pd 15 | from trader import Trader 16 | from utils import tf_cron, NUM_KLINE_INIT, CANDLE_COLUMNS 17 | from utils import get_pretty_table, datetime_to_filename 18 | 19 | 20 | def get_combination(params): 21 | keys = [] 22 | values = [] 23 | for k, v in params.items(): 24 | keys.append(k) 25 | values.append(v) 26 | combinations = list(itertools.product(*values)) 27 | return keys, combinations 28 | 29 | bot_logger = logging.getLogger("bot_logger") 30 | 31 | 32 | class Tuning: 33 | def __init__(self, symbols_trading_cfg_file, data_dir): 34 | self.data_dir = data_dir 35 | self.bot_traders: List[Trader] = [] 36 | self.symbols_trading_cfg_file = symbols_trading_cfg_file 37 | self.debug_dir = os.environ["DEBUG_DIR"] 38 | self.temp_dir = tempfile.mkdtemp() 39 | 40 | def load_mt5_klines_monthly_data(self, symbol, interval, month, year): 41 | csv_data_path = os.path.join(self.data_dir, "{}-{}-{}-{:02d}.csv".format(symbol, interval, year, month)) 42 | df = pd.read_csv(csv_data_path) 43 | df["Open time"] = pd.to_datetime(df["Open time"]) 44 | return df 45 | 46 | def load_klines_monthly_data(self, symbol, interval, month, year): 47 | return self.load_mt5_klines_monthly_data(symbol, interval, month, year) 48 | 49 | def backtest_bot_trader(self, symbol_cfg): 50 | bot_trader = Trader(symbol_cfg) 51 | bot_trader.init_strategies() 52 | tfs_chart = {} 53 | # Load kline data for all required timeframes 54 | for tf in bot_trader.get_required_tfs(): 55 | chart_df = pd.concat( 56 | [ 57 | self.load_klines_monthly_data(symbol_cfg["symbol"], tf, month, symbol_cfg["year"]) 58 | for month in sorted(symbol_cfg["months"]) 59 | ], 60 | ignore_index=True, 61 | ) 62 | tfs_chart[tf] = chart_df 63 | max_time = max([tf_chart.iloc[NUM_KLINE_INIT - 1]["Open time"] for tf_chart in tfs_chart.values()]) 64 | end_time = max([tf_chart.iloc[-1]["Open time"] for tf_chart in tfs_chart.values()]) 65 | tfs_chart_init = {} 66 | for tf, tf_chart in tfs_chart.items(): 67 | tfs_chart_init[tf] = tf_chart[tf_chart["Open time"] <= max_time][-NUM_KLINE_INIT:] 68 | tfs_chart[tf] = tf_chart[tf_chart["Open time"] > max_time] 69 | bot_trader.init_chart(tfs_chart_init) 70 | bot_trader.attach_oms(None) # for backtesting don't need oms 71 | 72 | timer = max_time 73 | end_time = end_time 74 | bot_logger.info("Start timer from: {} to {}".format(timer, end_time)) 75 | required_tfs = [tf for tf in tf_cron.keys() if tf in bot_trader.get_required_tfs()] 76 | 77 | while timer <= end_time: 78 | timer += timedelta(seconds=60) 79 | hour, minute = timer.hour, timer.minute 80 | for tf in required_tfs: 81 | cron_time = tf_cron[tf] 82 | if ("hour" not in cron_time or hour in cron_time["hour"]) and ( 83 | "minute" not in cron_time or minute in cron_time["minute"] 84 | ): 85 | last_kline = tfs_chart[tf][:1] 86 | tfs_chart[tf] = tfs_chart[tf][1:] 87 | bot_trader.on_kline(tf, last_kline) 88 | return bot_trader 89 | 90 | def start(self): 91 | with open(self.symbols_trading_cfg_file) as f: 92 | symbols_config = json.load(f) 93 | 94 | def split_list(lst, n): 95 | """Split a list into n equal segments""" 96 | k, m = divmod(len(lst), n) 97 | return [lst[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n)] 98 | bot_logger.info(" [+] Start tuning ...") 99 | args = [] 100 | for symbol_cfg in symbols_config: 101 | symbols = symbol_cfg["symbols"] 102 | params_cb = get_combination(symbol_cfg["params"]) 103 | bot_logger.info(" [+] Tuning symbols: {}, total: {} combinations".format(symbols, len(params_cb[1]))) 104 | params_cb = [dict(zip(params_cb[0], params)) for params in params_cb[1]] 105 | for symbol in symbols: 106 | sb_cfg = {"symbol": symbol} 107 | for k, v in symbol_cfg.items(): 108 | if k not in ["tfs", "params", "symbols", "name"]: 109 | sb_cfg[k] = v 110 | strategies = [{"name": symbol_cfg["name"], "params": param, "tfs": symbol_cfg["tfs"], 111 | "max_sl_pct": symbol_cfg["max_sl_pct"], 112 | "volume": symbol_cfg["volume"]} for param in params_cb] 113 | # Split list of strategies into list of 10 elements sublist 114 | sublist_strategies = split_list(strategies, len(strategies) // 10 + 1) 115 | for sublist_strategy in sublist_strategies: 116 | sb_cfg_tpl = {k: v for k, v in sb_cfg.items()} 117 | sb_cfg_tpl["strategies"] = sublist_strategy 118 | args.append(sb_cfg_tpl) 119 | bot_logger.info(" [+] Run total {} trials".format(len(args))) 120 | # preload data 121 | for symbol_cfg in symbols_config: 122 | symbols = symbol_cfg["symbols"] 123 | for symbol in symbols: 124 | for tf in symbol_cfg["tfs"].values(): 125 | for mnt in symbol_cfg["months"]: 126 | self.load_klines_monthly_data(symbol, tf, mnt, symbol_cfg["year"]) 127 | 128 | with Pool(cpu_count()) as pool: 129 | self.bot_traders.extend(pool.map(self.backtest_bot_trader, args)) 130 | bot_logger.info(" [*] Tuning finished") 131 | 132 | def summary_trade_result(self): 133 | final_backtest_stats = [] 134 | for bot_trader in self.bot_traders: 135 | bot_trader.close_opening_orders() 136 | backtest_stats = bot_trader.statistic_trade() 137 | backtest_stats.insert(loc=0, column="SYMBOL", value=bot_trader.get_symbol_name()) 138 | backtest_stats = backtest_stats.iloc[:-1] 139 | backtest_stats.insert(loc=2, column="params", value=bot_trader.get_strategy_params()) 140 | final_backtest_stats.append(backtest_stats) 141 | table_stats = pd.concat(final_backtest_stats, axis=0, ignore_index=True) 142 | return table_stats 143 | 144 | def stop(self): 145 | shutil.rmtree(self.temp_dir) 146 | 147 | 148 | def config_logging(exchange): 149 | curr_time = datetime.now() 150 | logging.config.fileConfig( 151 | "logging_config.ini", 152 | defaults={"logfilename": "logs/{}/bot_tuning_{}.log".format(exchange, datetime_to_filename(curr_time))}, 153 | ) 154 | logging.getLogger().setLevel(logging.WARNING) 155 | 156 | 157 | if __name__ == "__main__": 158 | parser = argparse.ArgumentParser(description="Monn auto trading bot") 159 | parser.add_argument("--sym_cfg_file", required=True, type=str) 160 | parser.add_argument("--data_dir", required=False, type=str) 161 | args = parser.parse_args() 162 | 163 | config_logging("binance") 164 | os.environ["DEBUG_DIR"] = "debug" 165 | 166 | 167 | tun_engine = Tuning(args.sym_cfg_file, args.data_dir) 168 | tun_engine.start() 169 | table_stats = tun_engine.summary_trade_result() 170 | table_stats.to_csv(os.path.splitext(args.sym_cfg_file)[0] + ".csv") 171 | tun_engine.stop() -------------------------------------------------------------------------------- /strategies/ma_cross_strategy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pandas as pd 3 | from datetime import datetime 4 | import talib as ta 5 | import plotly.graph_objects as go 6 | from plotly.subplots import make_subplots 7 | from .base_strategy import BaseStrategy 8 | import indicators as mta 9 | from order import Order, OrderType, OrderSide, OrderStatus 10 | 11 | bot_logger = logging.getLogger("bot_logger") 12 | IN_BUYING = 1 13 | IN_SELLING = 2 14 | 15 | 16 | class MACross(BaseStrategy): 17 | """ 18 | Test broker API strategy 19 | random place order and close 20 | use only for testing API 21 | """ 22 | 23 | def __init__(self, name, params, tfs): 24 | super().__init__(name, params, tfs) 25 | self.tf = self.tfs["tf"] 26 | self.state = None 27 | self.trend = None 28 | self.ma_func = ta.SMA if self.params["type"] == "SMA" else ta.EMA 29 | self.ma_stream_func = ta.stream.SMA if self.params["type"] == "SMA" else ta.stream.EMA 30 | 31 | def attach(self, tfs_chart): 32 | self.tfs_chart = tfs_chart 33 | self.init_indicators() 34 | 35 | def init_indicators(self): 36 | # calculate HA candelstick 37 | chart = self.tfs_chart[self.tf] 38 | self.fast_ma = self.ma_func(self.tfs_chart[self.tf]["Close"], self.params["fast_ma"]) 39 | self.slow_ma = self.ma_func(self.tfs_chart[self.tf]["Close"], self.params["slow_ma"]) 40 | self.start_trading_time = chart.iloc[-1]["Open time"] 41 | 42 | def update_indicators(self, tf): 43 | if tf != self.tf: 44 | return 45 | last_kline = self.tfs_chart[self.tf].iloc[-1] # last kline 46 | self.fast_ma.loc[len(self.fast_ma)] = self.ma_stream_func( 47 | self.tfs_chart[self.tf]["Close"], self.params["fast_ma"] 48 | ) 49 | self.slow_ma.loc[len(self.slow_ma)] = self.ma_stream_func( 50 | self.tfs_chart[self.tf]["Close"], self.params["slow_ma"] 51 | ) 52 | 53 | def check_required_params(self): 54 | return all([key in self.params.keys() for key in ["fast_ma", "slow_ma", "type"]]) 55 | 56 | def is_params_valid(self): 57 | if not self.check_required_params(): 58 | bot_logger.info(" [-] Missing required params") 59 | return False 60 | return self.params["fast_ma"] < self.params["slow_ma"] and self.params["type"] in ["SMA", "EMA"] 61 | 62 | def update(self, tf): 63 | # update when new kline arrive 64 | super().update(tf) 65 | # determind trend 66 | if len(self.orders_opening) == 0: 67 | self.state = None 68 | self.check_signal() 69 | 70 | def close_opening_orders(self): 71 | super().close_opening_orders(self.tfs_chart[self.tf].iloc[-1]) 72 | 73 | def check_signal(self): 74 | chart = self.tfs_chart[self.tf] 75 | last_kline = chart.iloc[-1] 76 | if self.state is None: 77 | if ( 78 | self.fast_ma.iloc[-1] > self.slow_ma.iloc[-1] 79 | and self.slow_ma.iloc[-1] > ta.stream.SMA(chart["Close"], 100) 80 | and last_kline["Close"] >= ta.stream.SMA(chart["Close"], 100) 81 | and last_kline["Close"] > last_kline["Open"] 82 | ): 83 | # new buy order 84 | order = Order(OrderType.MARKET, OrderSide.BUY, last_kline["Close"], status=OrderStatus.FILLED) 85 | order = self.trader.fix_order(order, self.params["sl_fix_mode"], self.max_sl_pct) 86 | if order: 87 | if order.type == OrderType.MARKET: 88 | order["FILL_TIME"] = last_kline["Open time"] 89 | order["strategy"] = self.name 90 | order["description"] = self.description 91 | order["desc"] = {} 92 | self.trader.create_trade(order, self.volume) 93 | self.orders_opening.append(order) 94 | self.state = IN_BUYING 95 | elif ( 96 | self.fast_ma.iloc[-1] < self.slow_ma.iloc[-1] 97 | and self.slow_ma.iloc[-1] < ta.stream.SMA(chart["Close"], 100) 98 | and last_kline["Close"] <= ta.stream.SMA(chart["Close"], 100) 99 | and last_kline["Close"] < last_kline["Open"] 100 | ): 101 | # new sell order 102 | order = Order(OrderType.MARKET, OrderSide.SELL, last_kline["Close"], status=OrderStatus.FILLED) 103 | order = self.trader.fix_order(order, self.params["sl_fix_mode"], self.max_sl_pct) 104 | if order: 105 | if order.type == OrderType.MARKET: 106 | order["FILL_TIME"] = last_kline["Open time"] 107 | order["strategy"] = self.name 108 | order["description"] = self.description 109 | order["desc"] = {} 110 | self.trader.create_trade(order, self.volume) 111 | self.orders_opening.append(order) 112 | self.state = IN_SELLING 113 | elif self.state == IN_BUYING and last_kline["Close"] < last_kline["Open"]: 114 | if self.fast_ma.iloc[-1] < self.slow_ma.iloc[-1]: 115 | # close buy order 116 | self.close_order_by_side(last_kline, OrderSide.BUY) 117 | self.state = None 118 | elif self.state == IN_SELLING and last_kline["Close"] > last_kline["Open"]: 119 | if self.fast_ma.iloc[-1] > self.slow_ma.iloc[-1]: 120 | # close sell order 121 | self.close_order_by_side(last_kline, OrderSide.SELL) 122 | self.state = None 123 | 124 | def plot_orders(self): 125 | fig = make_subplots(2, 1, vertical_spacing=0.02, shared_xaxes=True, row_heights=[0.8, 0.2]) 126 | fig.update_layout( 127 | xaxis_rangeslider_visible=False, 128 | xaxis2_rangeslider_visible=False, 129 | yaxis=dict(showgrid=False), 130 | xaxis=dict(showgrid=False), 131 | yaxis2=dict(showgrid=False), 132 | plot_bgcolor="#2E4053", 133 | paper_bgcolor="#797D7F", 134 | font=dict(color="#F7F9F9"), 135 | ) 136 | df = self.tfs_chart[self.tf] 137 | dt2idx = dict(zip(df["Open time"], list(range(len(df))))) 138 | tmp_ot = df["Open time"] 139 | df["Open time"] = list(range(len(df))) 140 | super().plot_orders(fig, self.tf, 1, 1, dt2idx=dt2idx) 141 | df = self.tfs_chart[self.tf] 142 | fig.add_trace( 143 | go.Scatter( 144 | x=df["Open time"], 145 | y=self.fast_ma, 146 | mode="lines", 147 | line=dict(color="blue"), 148 | name="FastMA_{}".format(self.params["fast_ma"]), 149 | ), 150 | row=1, 151 | col=1, 152 | ) 153 | fig.add_trace( 154 | go.Scatter( 155 | x=df["Open time"], 156 | y=self.slow_ma, 157 | mode="lines", 158 | line=dict(color="red"), 159 | name="SlowMA_{}".format(self.params["slow_ma"]), 160 | ), 161 | row=1, 162 | col=1, 163 | ) 164 | fig.add_trace( 165 | go.Scatter( 166 | x=df["Open time"], y=ta.SMA(df["Close"], 100), mode="lines", line=dict(color="orange"), name="MA_100" 167 | ), 168 | row=1, 169 | col=1, 170 | ) 171 | 172 | colors = ["red" if kline["Open"] > kline["Close"] else "green" for i, kline in df.iterrows()] 173 | fig.add_trace(go.Bar(x=df["Open time"], y=df["Volume"], marker_color=colors), row=2, col=1) 174 | 175 | fig.update_xaxes(showspikes=True, spikesnap="data") 176 | fig.update_yaxes(showspikes=True, spikesnap="data") 177 | fig.update_layout(hovermode="x", spikedistance=-1) 178 | fig.update_layout(hoverlabel=dict(bgcolor="white", font_size=16)) 179 | fig.update_layout( 180 | title={"text": "MA Cross Strategy (tf {})".format(self.tf), "x": 0.5, "xanchor": "center"} 181 | ) 182 | df["Open time"] = tmp_ot 183 | return fig 184 | -------------------------------------------------------------------------------- /exchange/mt5_oms.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from datetime import datetime 3 | import logging 4 | from trade import Trade 5 | from order import Order, OrderType, OrderSide, OrderType 6 | import MetaTrader5 as mt5 7 | 8 | bot_logger = logging.getLogger("bot_logger") 9 | 10 | 11 | class MT5OrderTemplate: 12 | def __init__(self, symbol, volume, entry, tp, sl, order_side: OrderSide, order_type: OrderType): 13 | self.symbol = symbol 14 | self.volume = volume 15 | self.price = entry 16 | self.tp = tp 17 | self.sl = sl 18 | self.order_type = order_type 19 | self.order_side = order_side 20 | 21 | def get_main_order(self): 22 | params = { 23 | "symbol": self.symbol, 24 | "volume": self.volume, 25 | "type_filling": mt5.ORDER_FILLING_IOC, 26 | "type_time": mt5.ORDER_TIME_GTC, 27 | } 28 | if self.tp: 29 | params["tp"] = self.tp 30 | if self.sl: 31 | params["sl"] = self.sl 32 | if self.order_type == OrderType.LIMIT: 33 | params["action"] = mt5.TRADE_ACTION_PENDING 34 | params["type"] = mt5.ORDER_TYPE_BUY_LIMIT if self.order_side == OrderSide.BUY else mt5.ORDER_TYPE_SELL_LIMIT 35 | params["price"] = self.price 36 | else: 37 | params["action"] = mt5.TRADE_ACTION_DEAL 38 | params["type"] = mt5.ORDER_TYPE_BUY if self.order_side == OrderSide.BUY else mt5.ORDER_TYPE_SELL 39 | return params 40 | 41 | def get_close_order(self): 42 | params = { 43 | "action": mt5.TRADE_ACTION_DEAL, 44 | "symbol": self.symbol, 45 | "volume": self.volume, 46 | "position": None, 47 | "price": None, 48 | "type": mt5.ORDER_TYPE_BUY if self.order_side == OrderSide.SELL else mt5.ORDER_TYPE_SELL, 49 | "type_time": mt5.ORDER_TIME_GTC, 50 | "type_filling": mt5.ORDER_FILLING_IOC, 51 | } 52 | return params 53 | 54 | 55 | class MT5OMS: 56 | def __init__(self, exchange): 57 | self.mt5_api = exchange 58 | self.active_trades = {} 59 | self.closed_trades = {} 60 | self.start_time = datetime.now() 61 | 62 | def create_trade(self, order: Order, volume): 63 | # round tp/sl price 64 | if order.has_sl(): 65 | order.sl = self.mt5_api.round_price(order["symbol"], order.sl) 66 | if order.has_tp(): 67 | order.tp = self.mt5_api.round_price(order["symbol"], order.tp) 68 | order.entry = self.mt5_api.round_price(order["symbol"], order.entry) 69 | 70 | trade = Trade(order, volume) 71 | bot_logger.info(" [*] create trade, trade_id: {}".format(trade.trade_id)) 72 | order_tpl = MT5OrderTemplate(order["symbol"], volume, order.entry, order.tp, order.sl, order.side, order.type) 73 | trade.main_order_params = order_tpl.get_main_order() 74 | trade.close_order_params = order_tpl.get_close_order() 75 | # update ask/bid price for market order 76 | if order.type == OrderType.MARKET: 77 | if order.side == OrderSide.BUY: 78 | trade.main_order_params["price"] = self.mt5_api.tick_ask_price(order["symbol"]) 79 | else: 80 | trade.main_order_params["price"] = self.mt5_api.tick_bid_price(order["symbol"]) 81 | trade.main_order_params["comment"] = order["description"] 82 | result = self.mt5_api.place_order(trade.main_order_params) 83 | result_dict = result._asdict() 84 | result_dict["request"] = result_dict["request"]._asdict() 85 | trade.main_order = result_dict 86 | trade.close_order_params["position"] = result_dict["order"] 87 | if result.retcode == mt5.TRADE_RETCODE_DONE: 88 | bot_logger.info(" [+] create main order success") 89 | self.active_trades[trade.trade_id] = trade 90 | return trade.trade_id 91 | else: 92 | bot_logger.info(" [+] create main order failed: {}".format(result_dict)) 93 | self.closed_trades[trade.trade_id] = trade 94 | return None 95 | 96 | def get_trade(self, trade_id): 97 | return self.active_trades.get(trade_id) 98 | 99 | def close_trade(self, trade_id): 100 | bot_logger.debug(" [*] close trade: {}".format(trade_id)) 101 | trade = self.get_trade(trade_id) 102 | if trade is None: 103 | bot_logger.debug(" [*] trade: {} not exist or already closed".format(trade_id)) 104 | return 105 | # check if order is type limit and pending 106 | result = mt5.orders_get(ticket=trade.main_order["order"]) 107 | if len(result) > 0: 108 | pending_order = result[0] 109 | if pending_order.state == mt5.ORDER_STATE_PLACED: 110 | request = { 111 | "action": mt5.TRADE_ACTION_REMOVE, 112 | "symbol": trade.order["symbol"], 113 | "order": trade.main_order["order"], 114 | } 115 | result = self.mt5_api.place_order(request) 116 | if result.retcode == mt5.TRADE_RETCODE_DONE: 117 | bot_logger.info(" [+] close limit trade success") 118 | else: 119 | bot_logger.info(" [+] close limit trade failed: {}".format(result._asdict())) 120 | else: 121 | # update ask/bid price for market order 122 | if trade.order.side == OrderSide.BUY: 123 | trade.close_order_params["price"] = self.mt5_api.tick_bid_price(trade.order["symbol"]) 124 | else: 125 | trade.close_order_params["price"] = self.mt5_api.tick_ask_price(trade.order["symbol"]) 126 | result = self.mt5_api.place_order(trade.close_order_params) 127 | result_dict = result._asdict() 128 | result_dict["request"] = result_dict["request"]._asdict() 129 | trade.close_order = result_dict 130 | if result.retcode == mt5.TRADE_RETCODE_DONE: 131 | bot_logger.info(" [+] close trade success") 132 | else: 133 | bot_logger.info(" [+] close trade failed: {}".format(result_dict)) 134 | self.closed_trades[trade_id] = self.active_trades[trade_id] 135 | del self.active_trades[trade_id] 136 | 137 | def adjust_sl(self, trade_id, sl): 138 | bot_logger.debug(" [+] adjust sl, trade: {}, sl: {}".format(trade_id, sl)) 139 | trade = self.get_trade(trade_id) 140 | if trade is None: 141 | bot_logger.debug(" [*] trade: {} not exist or already closed".format(trade_id)) 142 | return 143 | sl = self.mt5_api.round_price(trade.order["symbol"], sl) 144 | params = { 145 | "action": mt5.TRADE_ACTION_SLTP, 146 | "symbol": trade.order["symbol"], 147 | "position": trade.main_order["order"], 148 | "sl": sl, 149 | } 150 | if trade.order.tp: 151 | params["tp"] = trade.order.tp 152 | 153 | result = self.mt5_api.place_order(params) 154 | if result.retcode == mt5.TRADE_RETCODE_DONE: 155 | bot_logger.info(" [+] adjust sl success") 156 | trade.order.sl = sl 157 | else: 158 | bot_logger.info(" [+] adjust sl failed") 159 | 160 | def adjust_tp(self, trade_id, tp): 161 | bot_logger.debug(" [+] adjust tp, trade: {}, tp: {}".format(trade_id, tp)) 162 | trade = self.get_trade(trade_id) 163 | if trade is None: 164 | bot_logger.debug(" [*] trade: {} not exist or already closed".format(trade_id)) 165 | return 166 | tp = self.mt5_api.round_price(trade.order["symbol"], tp) 167 | params = { 168 | "action": mt5.TRADE_ACTION_SLTP, 169 | "symbol": trade.order["symbol"], 170 | "position": trade.main_order["order"], 171 | "tp": tp, 172 | } 173 | if trade.order.sl: 174 | params["sl"] = trade.order.sl 175 | 176 | result = self.mt5_api.place_order(params) 177 | if result.retcode == mt5.TRADE_RETCODE_DONE: 178 | bot_logger.info(" [+] adjust tp success") 179 | trade.order.tp = tp 180 | else: 181 | bot_logger.info(" [+] adjust tp failed") 182 | 183 | def monitor_trades(self): 184 | if len(self.active_trades) == 0: 185 | return 186 | 187 | def close_all_trade(self): 188 | bot_logger.debug(" [*] close all trade") 189 | for trade_id in list(self.active_trades.keys()): 190 | self.close_trade(trade_id) 191 | 192 | def get_income_history(self): 193 | total_deals = [] 194 | for _, trade in self.closed_trades.items(): 195 | result = self.mt5_api.history_deals_get(trade.main_order["order"]) 196 | if result: 197 | total_deals.extend([res._asdict() for res in result]) 198 | return pd.DataFrame(total_deals) 199 | 200 | def get_trades(self): 201 | return self.closed_trades, self.active_trades 202 | -------------------------------------------------------------------------------- /order.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class OrderType(Enum): 5 | LIMIT = "LIMIT" 6 | MARKET = "MARKET" 7 | 8 | 9 | class OrderSide(Enum): 10 | BUY = "BUY" 11 | SELL = "SELL" 12 | 13 | 14 | class PositionSide(Enum): 15 | LONG = "LONG" 16 | SHORT = "SHORT" 17 | 18 | 19 | class OrderStatus(Enum): 20 | PENDING = "PENDING" 21 | FILLED = "FILLED" 22 | HIT_SL = "HIT_SL" 23 | HIT_TP = "HIT_TP" 24 | STOPPED = "STOPPED" 25 | 26 | 27 | class OrderTemplate: 28 | def __init__(self, symbol, quantity, entry, order_side: OrderSide, order_type: OrderType): 29 | self.symbol = symbol 30 | self.quantity = quantity 31 | self.entry = entry 32 | self.order_type = order_type 33 | self.order_side = order_side 34 | if self.order_side == OrderSide.BUY: 35 | self.position_side = PositionSide.LONG 36 | else: 37 | self.position_side = PositionSide.SHORT 38 | 39 | def get_order_params(self, **kwargs): 40 | params = { 41 | "symbol": self.symbol, 42 | "type": self.order_type.value, 43 | "side": self.order_side.value, 44 | "positionSide": self.position_side.value, 45 | "quantity": self.quantity, 46 | } 47 | params.update(kwargs) 48 | return params 49 | 50 | def get_main_order(self): 51 | return ( 52 | self.get_order_params() 53 | if self.order_type == OrderType.MARKET 54 | else self.get_order_params(timeInForce="GTC", price=self.entry) 55 | ) 56 | 57 | def get_close_order(self): 58 | return self.get_order_params( 59 | type=OrderType.MARKET.value, 60 | side=OrderSide.BUY.value if self.order_side == OrderSide.SELL else OrderSide.SELL.value, 61 | ) 62 | 63 | def get_tp_order(self, tp): 64 | return self.get_order_params( 65 | type="TAKE_PROFIT_MARKET", 66 | side=OrderSide.BUY.value if self.order_side == OrderSide.SELL else OrderSide.SELL.value, 67 | stopPrice=tp, 68 | workingType="MARK_PRICE", 69 | ) 70 | 71 | def get_sl_order(self, sl): 72 | return self.get_order_params( 73 | type="STOP_MARKET", 74 | side=OrderSide.BUY.value if self.order_side == OrderSide.SELL else OrderSide.SELL.value, 75 | stopPrice=sl, 76 | workingType="MARK_PRICE", 77 | ) 78 | 79 | 80 | class Order: 81 | # 82 | # |-------------| TP 83 | # | | 84 | # | | 85 | # | BUY | 86 | # | | 87 | # |-------------| ENTRY 88 | # | | 89 | # |-------------| SL 90 | # =================================# 91 | # |-------------| SL 92 | # | | 93 | # |-------------| ENTRY 94 | # | | 95 | # | | 96 | # | SELL | 97 | # | | 98 | # |-------------| TP 99 | # 100 | __order_id__ = 1 101 | 102 | def __init__( 103 | self, 104 | order_type: OrderType, 105 | order_side: OrderSide, 106 | entry, 107 | tp=None, 108 | sl=None, 109 | status=OrderStatus.PENDING, 110 | ): 111 | # type="LIMIT"/"MARKET", 112 | # side="BUY"/"SELL", 113 | self.order_id = Order.__order_id__ 114 | Order.__order_id__ += 1 115 | self.type = order_type 116 | self.side = order_side 117 | self.entry = entry 118 | self.tp = tp 119 | self.sl = sl 120 | self.status = status 121 | self.rr = 0 122 | self.__attrs__ = {} 123 | self.calc_stats() 124 | 125 | def calc_stats(self): 126 | self.reward_ratio = None 127 | self.risk_ratio = None 128 | if self.tp: 129 | self.reward_ratio = round(abs(self.tp - self.entry) / self.entry, 4) 130 | if self.sl: 131 | self.risk_ratio = round(abs(self.sl - self.entry) / self.entry, 4) 132 | if self.reward_ratio and self.risk_ratio: 133 | self.rr = round(self.reward_ratio / self.risk_ratio, 4) 134 | 135 | def is_valid(self): 136 | if self.side == OrderSide.BUY: 137 | if self.has_tp() and self.tp < self.entry: 138 | return False 139 | if self.has_sl() and self.sl > self.entry: 140 | return False 141 | return True 142 | if self.has_tp() and self.tp > self.entry: 143 | return False 144 | if self.has_sl() and self.sl < self.entry: 145 | return False 146 | return True 147 | 148 | def has_sl(self): 149 | return self.sl is not None 150 | 151 | def has_tp(self): 152 | return self.tp is not None 153 | 154 | def adjust_tp(self, tp): 155 | self.tp = tp 156 | self.calc_stats() 157 | 158 | def adjust_sl(self, sl): 159 | self.sl = sl 160 | self.calc_stats() 161 | if (self.side == OrderSide.SELL and self.sl <= self.entry) or ( 162 | self.side == OrderSide.BUY and self.sl >= self.entry 163 | ): 164 | self.risk_ratio = 0 165 | 166 | def adjust_entry(self, entry): 167 | self.entry = entry 168 | self.calc_stats() 169 | 170 | def update_status(self, kline): 171 | open_time = kline["Open time"] 172 | ohlc = kline[["Open", "High", "Low", "Close"]] 173 | if self.status == OrderStatus.PENDING: 174 | # limit order 175 | if (ohlc["High"] - self.entry) * (ohlc["Low"] - self.entry) <= 0: 176 | self.status = OrderStatus.FILLED 177 | self.__attrs__["FILL_TIME"] = open_time 178 | if ( 179 | self.status == OrderStatus.FILLED 180 | and self.has_sl() 181 | and (ohlc["High"] - self.sl) * (ohlc["Low"] - self.sl) <= 0 182 | ): 183 | # order hit sl 184 | self.status = OrderStatus.HIT_SL 185 | self.__attrs__["STOP_TIME"] = open_time 186 | if ( 187 | self.status == OrderStatus.FILLED 188 | and self.has_tp() 189 | and (ohlc["High"] - self.tp) * (ohlc["Low"] - self.tp) <= 0 190 | ): 191 | # order hit tp 192 | self.status = OrderStatus.HIT_TP 193 | self.__attrs__["STOP_TIME"] = open_time 194 | elif self.status == OrderStatus.FILLED: 195 | if ( 196 | self.has_sl() 197 | and (ohlc["High"] - self.sl) * (ohlc["Low"] - self.sl) <= 0 198 | or (self.side == OrderSide.SELL and ohlc["High"] >= self.sl) 199 | or (self.side == OrderSide.BUY and ohlc["Low"] <= self.sl) 200 | ): 201 | # order hit sl 202 | self.status = OrderStatus.HIT_SL 203 | self.__attrs__["STOP_TIME"] = open_time 204 | elif self.has_tp() and (ohlc["High"] - self.tp) * (ohlc["Low"] - self.tp) <= 0: 205 | # order hit tp 206 | self.status = OrderStatus.HIT_TP 207 | self.__attrs__["STOP_TIME"] = open_time 208 | 209 | def close(self, kline): 210 | if self.status == OrderStatus.FILLED: 211 | self.status = OrderStatus.STOPPED 212 | self.__attrs__["STOP_TIME"] = kline["Open time"] 213 | self.__attrs__["STOP_PRICE"] = kline["Close"] 214 | change_pc = round((kline["Close"] - self.entry) / self.entry, 4) 215 | self.__attrs__["PnL"] = change_pc if self.side == OrderSide.BUY else -change_pc 216 | 217 | def is_closed(self): 218 | return self.status in [OrderStatus.HIT_SL, OrderStatus.HIT_TP, OrderStatus.STOPPED] 219 | 220 | def get_PnL(self): 221 | if self.status == OrderStatus.HIT_SL: 222 | pnl = round(abs(self.sl - self.entry) / self.entry, 4) 223 | if (self.side == OrderSide.SELL and self.sl <= self.entry) or ( 224 | self.side == OrderSide.BUY and self.sl >= self.entry 225 | ): 226 | return pnl 227 | return -pnl 228 | if self.status == OrderStatus.HIT_TP: 229 | return self.reward_ratio 230 | if self.status == OrderStatus.STOPPED: 231 | return self.__attrs__["PnL"] 232 | return 0 233 | 234 | def __getitem__(self, __name: str): 235 | return self.__attrs__[__name] 236 | 237 | def __setitem__(self, __name, __value): 238 | self.__attrs__[__name] = __value 239 | 240 | def __contains__(self, key): 241 | return key in self.__attrs__ 242 | 243 | def __to_dict__(self): 244 | return { 245 | "order_id": self.order_id, 246 | "type": self.type.value, 247 | "side": self.side.value, 248 | "entry": self.entry, 249 | "tp": self.tp, 250 | "sl": self.sl, 251 | "status": self.status.value, 252 | "reward_ratio": self.reward_ratio, 253 | "risk_ratio": self.risk_ratio, 254 | "rr": self.rr, 255 | "attrs": self.__attrs__, 256 | } 257 | 258 | def __str__(self) -> str: 259 | return self.__to_dict__().__str__() 260 | -------------------------------------------------------------------------------- /strategies/ma_heikin_ashi.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pandas as pd 3 | from datetime import datetime 4 | import talib as ta 5 | import plotly.graph_objects as go 6 | from plotly.subplots import make_subplots 7 | from .base_strategy import BaseStrategy 8 | import indicators as mta 9 | from order import Order, OrderType, OrderSide, OrderStatus 10 | from utils import get_line_coffs, find_uptrend_line, find_downtrend_line, get_y_on_line 11 | 12 | 13 | bot_logger = logging.getLogger("bot_logger") 14 | UP_TREND = 0 15 | DOWN_TREND = 1 16 | IN_BUYING = 0 17 | IN_SELLING = 1 18 | 19 | 20 | class MAHeikinAshi(BaseStrategy): 21 | """ 22 | - EMA HeikinAshi strategy 23 | """ 24 | 25 | def __init__(self, name, params, tfs): 26 | super().__init__(name, params, tfs) 27 | self.tf = self.tfs["tf"] 28 | self.state = None 29 | self.trend = None 30 | self.ma_func = ta.SMA if self.params["type"] == "SMA" else ta.EMA 31 | self.ma_stream_func = ta.stream.SMA if self.params["type"] == "SMA" else ta.stream.EMA 32 | 33 | def attach(self, tfs_chart): 34 | self.tfs_chart = tfs_chart 35 | self.init_indicators() 36 | 37 | def init_indicators(self): 38 | # calculate HA candelstick 39 | chart = self.tfs_chart[self.tf] 40 | self.ha = mta.heikin_ashi(chart, self.params["ha_smooth"]) 41 | self.fast_ma = self.ma_func(self.tfs_chart[self.tf]["Close"], self.params["fast_ma"]) 42 | self.slow_ma = self.ma_func(self.tfs_chart[self.tf]["Close"], self.params["slow_ma"]) 43 | self.start_trading_time = chart.iloc[-1]["Open time"] 44 | 45 | def update_indicators(self, tf): 46 | if tf != self.tf: 47 | return 48 | last_kline = self.tfs_chart[self.tf].iloc[-1] # last kline 49 | self.ha = pd.concat([self.ha, mta.heikin_ashi_stream(self.ha, last_kline, self.params["ha_smooth"])]) 50 | self.fast_ma.loc[len(self.fast_ma)] = self.ma_stream_func( 51 | self.tfs_chart[self.tf]["Close"], self.params["fast_ma"] 52 | ) 53 | self.slow_ma.loc[len(self.slow_ma)] = self.ma_stream_func( 54 | self.tfs_chart[self.tf]["Close"], self.params["slow_ma"] 55 | ) 56 | 57 | def check_required_params(self): 58 | return all([key in self.params.keys() for key in ["ha_smooth", "fast_ma", "slow_ma", "type", "n_kline_trend"]]) 59 | 60 | def is_params_valid(self): 61 | if not self.check_required_params(): 62 | bot_logger.info(" [-] Missing required params") 63 | return False 64 | return self.params["fast_ma"] < self.params["slow_ma"] and self.params["type"] in ["SMA", "EMA"] 65 | 66 | def update(self, tf): 67 | # update when new kline arrive 68 | super().update(tf) 69 | # determind trend 70 | chart = self.tfs_chart[self.tf] 71 | n_df = chart[-self.params["n_kline_trend"] :].reset_index() 72 | n_last_poke_points = [] 73 | n_last_peak_points = [] 74 | for i, kline in n_df.iterrows(): 75 | n_last_poke_points.append((len(chart) - self.params["n_kline_trend"] + i, kline["Low"])) 76 | n_last_peak_points.append((len(chart) - self.params["n_kline_trend"] + i, kline["High"])) 77 | self.up_trend_line = find_uptrend_line(n_last_poke_points) 78 | self.down_trend_line = find_downtrend_line(n_last_peak_points) 79 | self.up_pct = (self.up_trend_line[1][1] - self.up_trend_line[0][1]) / self.up_trend_line[0][1] 80 | self.down_pct = (self.down_trend_line[1][1] - self.down_trend_line[0][1]) / self.down_trend_line[0][1] 81 | self.check_signal(self.tfs_chart[self.tf].iloc[-1]) 82 | 83 | def close_opening_orders(self): 84 | super().close_opening_orders(self.tfs_chart[self.tf].iloc[-1]) 85 | 86 | def check_signal(self, last_kline): 87 | last_ha = self.ha.iloc[-1] 88 | if self.state is None: 89 | if self.up_pct > 0.03 and self.down_pct > 0 and self.fast_ma.iloc[-1] > self.slow_ma.iloc[-1]: 90 | if last_ha["Open"] < last_ha["Close"] and last_ha["Open"] == last_ha["Low"]: # HA open < HA close 91 | # new buy order 92 | order = Order(OrderType.MARKET, OrderSide.BUY, last_kline["Close"], status=OrderStatus.FILLED) 93 | order = self.trader.fix_order(order, self.params["sl_fix_mode"], self.max_sl_pct) 94 | if order: 95 | if order.type == OrderType.MARKET: 96 | order["FILL_TIME"] = last_kline["Open time"] 97 | order["strategy"] = self.name 98 | order["description"] = self.description 99 | order["desc"] = {"up_trend_line": self.up_trend_line, "down_trend_line": self.down_trend_line} 100 | self.trader.create_trade(order, self.volume) 101 | self.orders_opening.append(order) 102 | self.state = IN_BUYING 103 | elif self.down_pct < -0.03 and self.up_pct < 0 and self.fast_ma.iloc[-1] < self.slow_ma.iloc[-1]: 104 | if last_ha["Open"] > last_ha["Close"] and last_ha["Open"] == last_ha["High"]: # HA open > HA close 105 | # new sell order 106 | order = Order(OrderType.MARKET, OrderSide.SELL, last_kline["Close"], status=OrderStatus.FILLED) 107 | order = self.trader.fix_order(order, self.params["sl_fix_mode"], self.max_sl_pct) 108 | if order: 109 | if order.type == OrderType.MARKET: 110 | order["FILL_TIME"] = last_kline["Open time"] 111 | order["strategy"] = self.name 112 | order["description"] = self.description 113 | order["desc"] = {"up_trend_line": self.up_trend_line, "down_trend_line": self.down_trend_line} 114 | self.trader.create_trade(order, self.volume) 115 | self.orders_opening.append(order) 116 | self.state = IN_SELLING 117 | elif self.state == IN_BUYING: 118 | if self.fast_ma.iloc[-1] < self.slow_ma.iloc[-1] and self.down_pct < 0: 119 | # close buy order 120 | self.close_order_by_side(last_kline, OrderSide.BUY) 121 | self.state = None 122 | elif self.state == IN_SELLING: 123 | if self.fast_ma.iloc[-1] > self.slow_ma.iloc[-1] and self.up_pct > 0: 124 | # close sell order 125 | self.close_order_by_side(last_kline, OrderSide.SELL) 126 | self.state = None 127 | 128 | def plot_orders(self): 129 | fig = make_subplots(3, 1, vertical_spacing=0.02, shared_xaxes=True, row_heights=[0.4, 0.4, 0.2]) 130 | fig.update_layout( 131 | xaxis_rangeslider_visible=False, 132 | xaxis2_rangeslider_visible=False, 133 | xaxis3_rangeslider_visible=False, 134 | yaxis=dict(showgrid=False), 135 | yaxis2=dict(showgrid=False), 136 | yaxis3=dict(showgrid=False), 137 | plot_bgcolor="#2E4053", 138 | paper_bgcolor="#797D7F", 139 | font=dict(color="#F7F9F9"), 140 | ) 141 | df = self.tfs_chart[self.tf] 142 | dt2idx = dict(zip(df["Open time"], list(range(len(df))))) 143 | tmp_ot = df["Open time"] 144 | df["Open time"] = list(range(len(df))) 145 | super().plot_orders(fig, self.tf, 1, 1) 146 | 147 | for order in self.orders_closed: 148 | desc = order["desc"] 149 | up_trend = desc["up_trend_line"] 150 | down_trend = desc["down_trend_line"] 151 | fig.add_shape( 152 | type="line", 153 | x0=df.iloc[up_trend[0][0]]["Open time"], 154 | y0=up_trend[0][1], 155 | x1=df.iloc[up_trend[1][0]]["Open time"], 156 | y1=up_trend[1][1], 157 | line=dict(color="green"), 158 | row=1, 159 | col=1, 160 | ) 161 | fig.add_shape( 162 | type="line", 163 | x0=df.iloc[down_trend[0][0]]["Open time"], 164 | y0=down_trend[0][1], 165 | x1=df.iloc[down_trend[1][0]]["Open time"], 166 | y1=down_trend[1][1], 167 | line=dict(color="red"), 168 | row=1, 169 | col=1, 170 | ) 171 | 172 | fig.add_trace( 173 | go.Scatter( 174 | x=df["Open time"], 175 | y=self.fast_ma, 176 | mode="lines", 177 | line=dict(color="blue"), 178 | name="FastMA_{}".format(self.params["fast_ma"]), 179 | ), 180 | row=1, 181 | col=1, 182 | ) 183 | fig.add_trace( 184 | go.Scatter( 185 | x=df["Open time"], 186 | y=self.slow_ma, 187 | mode="lines", 188 | line=dict(color="red"), 189 | name="SlowMA_{}".format(self.params["slow_ma"]), 190 | ), 191 | row=1, 192 | col=1, 193 | ) 194 | fig.add_trace( 195 | go.Candlestick( 196 | x=df["Open time"], 197 | open=self.ha["Open"], 198 | high=self.ha["High"], 199 | low=self.ha["Low"], 200 | close=self.ha["Close"], 201 | ), 202 | row=2, 203 | col=1, 204 | ) 205 | colors = ["red" if kline["Open"] > kline["Close"] else "green" for i, kline in df.iterrows()] 206 | fig.add_trace(go.Bar(x=df["Open time"], y=df["Volume"], marker_color=colors), row=3, col=1) 207 | 208 | fig.update_layout(title={"text": "Heikin AShi Strategy (tf {})".format(self.tf), "x": 0.5, "xanchor": "center"}) 209 | df["Open time"] = tmp_ot 210 | return fig 211 | -------------------------------------------------------------------------------- /strategies/base_strategy.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List 3 | import plotly.graph_objects as go 4 | from plotly.subplots import make_subplots 5 | from order import Order, OrderSide, OrderStatus 6 | 7 | 8 | class BaseStrategy: 9 | def __init__(self, name, params, tfs): 10 | self.name = name 11 | self.params = params 12 | self.tfs = tfs 13 | # orders in activate created by the strategy 14 | self.orders_opening: List[Order] = [] 15 | # orders closed created by the strategy 16 | self.orders_closed: List[Order] = [] 17 | self.start_trading_time = None 18 | self.set_description() 19 | 20 | def attach(self, tfs_chart): 21 | self.tfs_chart = tfs_chart 22 | self.init_indicators() 23 | 24 | def set_description(self): 25 | self.description = self.name 26 | for tfn, tf in self.tfs.items(): 27 | self.description += "_{}_{}".format(tfn, tf) 28 | 29 | def set_volume(self, volume): 30 | self.volume = volume 31 | 32 | def set_max_sl_pct(self, max_sl_pct): 33 | self.max_sl_pct = max_sl_pct 34 | 35 | def get_name(self): 36 | return self.name 37 | 38 | def attach_trader(self, trader): 39 | self.trader = trader 40 | 41 | def init_indicators(self): 42 | pass 43 | 44 | def update_indicators(self, tf): 45 | pass 46 | 47 | def check_required_params(self): 48 | return False 49 | 50 | def is_params_valid(self): 51 | return False 52 | 53 | def update_orders_status(self, last_kline): 54 | for i in range(len(self.orders_opening) - 1, -1, -1): 55 | order = self.orders_opening[i] 56 | order.update_status(last_kline) 57 | if order.is_closed(): 58 | self.orders_closed.append(order) 59 | del self.orders_opening[i] 60 | 61 | def close_order_by_side(self, last_kline, order_side: OrderSide): 62 | for i in range(len(self.orders_opening) - 1, -1, -1): 63 | order = self.orders_opening[i] 64 | if order.side == order_side: 65 | order.close(last_kline) 66 | self.trader.close_trade(order) 67 | if order.is_closed(): 68 | self.orders_closed.append(order) 69 | del self.orders_opening[i] 70 | 71 | def close_opening_orders(self, last_kline): 72 | for i in range(len(self.orders_opening) - 1, -1, -1): 73 | order = self.orders_opening[i] 74 | order.close(last_kline) 75 | self.trader.close_trade(order) 76 | if order.is_closed(): 77 | self.orders_closed.append(order) 78 | del self.orders_opening[i] 79 | 80 | def summary_PnL(self): 81 | num_long_order = 0 82 | num_short_order = 0 83 | num_long_profit = 0 84 | num_short_profit = 0 85 | long_PnL = 0 86 | long_tp_profit = 0 87 | long_sl_loss = 0 88 | short_PnL = 0 89 | short_tp_profit = 0 90 | short_sl_loss = 0 91 | 92 | def get_percent(x, y): 93 | return x / y * 100 if y > 0 else 0 94 | 95 | for order in self.orders_closed: 96 | pnl = order.get_PnL() 97 | if order.side == OrderSide.BUY: 98 | num_long_order += 1 99 | long_PnL += pnl 100 | if pnl > 0: 101 | num_long_profit += 1 102 | long_tp_profit += pnl 103 | elif pnl < 0: 104 | long_sl_loss += pnl 105 | else: 106 | num_short_order += 1 107 | short_PnL += pnl 108 | if pnl > 0: 109 | num_short_profit += 1 110 | short_tp_profit += pnl 111 | elif pnl < 0: 112 | short_sl_loss += pnl 113 | return { 114 | "NAME": self.get_name(), 115 | "LONG": num_long_order, 116 | "LONG_TP_PnL(%)": 100 * long_tp_profit, 117 | "LONG_SL_PnL(%)": 100 * long_sl_loss, 118 | "LONG_PnL(%)": 100 * long_PnL, 119 | "NUM_LONG_TP": num_long_profit, 120 | "LONG_TP_PCT": get_percent(num_long_profit, num_long_order), 121 | "SHORT": num_short_order, 122 | "SHORT_TP_PnL(%)": 100 * short_tp_profit, 123 | "SHORT_SL_PnL(%)": 100 * short_sl_loss, 124 | "SHORT_PnL(%)": 100 * short_PnL, 125 | "NUM_SHORT_TP": num_short_profit, 126 | "SHORT_TP_PCT": get_percent(num_short_profit, num_short_order), 127 | "TOTAL": num_long_order + num_short_order, 128 | "TOTAL_TP_PCT": get_percent(num_long_profit + num_short_profit, num_long_order + num_short_order), 129 | "TOTAL_PnL(%)": 100 * (long_PnL + short_PnL), 130 | "AVG(%)": get_percent(long_PnL + short_PnL, num_long_order + num_short_order), 131 | } 132 | 133 | def update(self, tf): 134 | self.update_indicators(tf) 135 | self.update_orders_status(self.tfs_chart[tf].iloc[-1]) 136 | 137 | def plot_orders(self, fig, tf, row, col, dt2idx=None): 138 | df = self.tfs_chart[tf] 139 | fig.add_trace( 140 | go.Candlestick( 141 | x=df["Open time"], 142 | open=df["Open"], 143 | high=df["High"], 144 | low=df["Low"], 145 | close=df["Close"], 146 | text=list(dt2idx.keys()) if dt2idx else None, 147 | increasing={"fillcolor": "rgb(8, 153, 129)"}, 148 | decreasing={"fillcolor": "rgb(242, 54, 69)"}, 149 | ), 150 | row=row, 151 | col=col, 152 | ) 153 | if self.start_trading_time: 154 | fig.add_shape( 155 | type="line", 156 | x0=dt2idx[self.start_trading_time] if dt2idx else self.start_trading_time, 157 | y0=0, 158 | x1=dt2idx[self.start_trading_time] if dt2idx else self.start_trading_time, 159 | y1=1, 160 | line=dict(width=1, color="orange"), 161 | yref="y domain", 162 | ) 163 | for order in self.orders_closed: 164 | if order.get_PnL() > 0: 165 | line_color = "rgba(0, 255, 0, 1.0)" 166 | else: 167 | line_color = "rgba(255, 0, 0, 1.0)" 168 | x0 = order.__attrs__["FILL_TIME"] if dt2idx is None else dt2idx[order.__attrs__["FILL_TIME"]] 169 | x1 = order.__attrs__["STOP_TIME"] if dt2idx is None else dt2idx[order.__attrs__["STOP_TIME"]] 170 | text = "risk: {}
reward: {}
rr: {}
PnL: {}
id: {}".format( 171 | order.risk_ratio, order.reward_ratio, order.rr, order.get_PnL(), order.order_id 172 | ) 173 | y = order.entry 174 | if order.has_sl(): 175 | if order.risk_ratio > 0: 176 | fig.add_trace( 177 | go.Scatter( 178 | x=[x0, x1, x1, x0, x0], 179 | y=[order.entry, order.entry, order.sl, order.sl, order.entry], 180 | mode="lines", 181 | line=dict(width=1, color=line_color), 182 | fill="toself", 183 | fillcolor="rgba(242, 54, 69, 0.2)", 184 | hoverinfo="skip", 185 | showlegend=False, 186 | legendgroup="order_{}".format(order.order_id), 187 | ), 188 | row=row, 189 | col=col, 190 | ) 191 | elif order.status == OrderStatus.HIT_SL: 192 | fig.add_trace( 193 | go.Scatter( 194 | x=[x0, x1, x1, x0, x0], 195 | y=[order.entry, order.entry, order.sl, order.sl, order.entry], 196 | mode="lines", 197 | line=dict(width=1, color=line_color), 198 | fill="toself", 199 | fillcolor="rgba(0, 0, 200, 0.2)", 200 | hoverinfo="skip", 201 | showlegend=False, 202 | legendgroup="order_{}".format(order.order_id), 203 | ), 204 | row=row, 205 | col=col, 206 | ) 207 | y = max(y, order.sl) 208 | if order.has_tp(): 209 | fig.add_trace( 210 | go.Scatter( 211 | x=[x0, x1, x1, x0, x0], 212 | y=[order.entry, order.entry, order.tp, order.tp, order.entry], 213 | mode="lines", 214 | line=dict(width=1, color=line_color), 215 | fill="toself", 216 | fillcolor="rgba(8, 153, 129, 0.2)", 217 | hoverinfo="skip", 218 | showlegend=False, 219 | legendgroup="order_{}".format(order.order_id), 220 | ), 221 | row=row, 222 | col=col, 223 | ) 224 | y = max(y, order.tp) 225 | if order.status == OrderStatus.STOPPED: 226 | fig.add_trace( 227 | go.Scatter( 228 | x=[x0, x1, x1, x0, x0], 229 | y=[ 230 | order.entry, 231 | order.entry, 232 | order.__attrs__["STOP_PRICE"], 233 | order.__attrs__["STOP_PRICE"], 234 | order.entry, 235 | ], 236 | mode="lines", 237 | line=dict(width=1, color=line_color), 238 | fill="toself", 239 | fillcolor="rgba(0, 0, 200, 0.2)", 240 | hoverinfo="skip", 241 | showlegend=False, 242 | legendgroup="order_{}".format(order.order_id), 243 | ), 244 | row=row, 245 | col=col, 246 | ) 247 | y = max(y, order.__attrs__["STOP_PRICE"]) 248 | fig.add_trace( 249 | go.Scatter( 250 | x=[x0, x1], 251 | y=[order.entry, order.entry], 252 | mode="lines", 253 | line=dict(width=2, color="rgba(255, 255, 255, 1.0)"), 254 | hoverinfo="skip", 255 | showlegend=True, 256 | name="order_{}".format(order.order_id), 257 | legendgroup="order_{}".format(order.order_id), 258 | ), 259 | row=row, 260 | col=col, 261 | ) 262 | if "LIMIT_TIME" in order.__attrs__: 263 | fig.add_trace( 264 | go.Scatter( 265 | x=[ 266 | x0, 267 | order.__attrs__["LIMIT_TIME"] if dt2idx is None else dt2idx[order.__attrs__["LIMIT_TIME"]], 268 | ], 269 | y=[order.entry, order.entry], 270 | mode="lines", 271 | line=dict(width=1, dash="dash", color="rgba(255, 255, 255, 0.7)"), 272 | hoverinfo="skip", 273 | showlegend=False, 274 | legendgroup="order_{}".format(order.order_id), 275 | ), 276 | row=row, 277 | col=col, 278 | ) 279 | fig.add_trace( 280 | go.Scatter( 281 | x=[x0], 282 | y=[y], 283 | mode="text", 284 | text=[text], 285 | textposition="top right", 286 | textfont={"color": "rgba(255, 255, 255, 1.0)"}, 287 | hoverinfo="skip", 288 | showlegend=True, 289 | name="order_{}".format(order.order_id), 290 | legendgroup="order_text", 291 | ), 292 | row=row, 293 | col=col, 294 | ) 295 | -------------------------------------------------------------------------------- /indicators/zigzag.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List 3 | import numpy as np 4 | 5 | 6 | class POINT_TYPE(Enum): 7 | PEAK_POINT = "PEAK_POINT" 8 | POKE_POINT = "POKE_POINT" 9 | 10 | 11 | class TREND_TYPE(Enum): 12 | UP_TREND = "UP_TREND" 13 | DOWN_TREND = "DOWN_TREND" 14 | UNK_TREND = "UNK_TREND" 15 | 16 | 17 | class SRLine(object): 18 | # Support/Resistance line 19 | def __init__(self, low, high): 20 | self.low = low 21 | self.high = high 22 | 23 | def __to_dict__(self): 24 | return {"low": self.low, "high": self.high} 25 | 26 | def __repr__(self): 27 | return str(self.__to_dict__()) 28 | 29 | 30 | class ZZPoint(object): 31 | def __init__(self, pidx, ptype: POINT_TYPE, pline: SRLine): 32 | self.pidx = pidx 33 | self.ptype = ptype 34 | self.pline = pline 35 | 36 | def __to_dict__(self): 37 | return {"pidx": self.pidx, "ptype": self.ptype.value, "pline": self.pline} 38 | 39 | def __repr__(self): 40 | return str(self.__to_dict__()) 41 | 42 | 43 | def merge_break_points(zz_points, min_div, from_idx=0): 44 | # merge zz_points to wave >= min_div 45 | i = from_idx 46 | while i < len(zz_points) - 2: 47 | ftp = zz_points[i] 48 | scp = zz_points[i + 1] 49 | if ftp.ptype == POINT_TYPE.POKE_POINT: 50 | l = abs(scp.pline.high - ftp.pline.low) / ftp.pline.low 51 | else: 52 | l = abs(scp.pline.low - ftp.pline.high) / ftp.pline.high 53 | if l < min_div: 54 | # delete 2 zz_points have wave len < min_div 55 | del zz_points[i + 1] 56 | del zz_points[i] 57 | if ftp.ptype == POINT_TYPE.POKE_POINT: 58 | # merge old zz_point with new zz_point 59 | if ftp.pline.high < zz_points[i].pline.high: 60 | ftp.pline.low = min(ftp.pline.low, zz_points[i].pline.low) 61 | zz_points[i] = ftp 62 | if i + 1 < len(zz_points): 63 | if scp.pline.low > zz_points[i + 1].pline.low: 64 | scp.pline.high = max(scp.pline.high, zz_points[i + 1].pline.high) 65 | zz_points[i + 1] = scp 66 | else: 67 | zz_points[i + 1].pline.high = max(scp.pline.high, zz_points[i + 1].pline.high) 68 | else: 69 | zz_points[i].pline.low = min(ftp.pline.low, zz_points[i].pline.low) 70 | else: 71 | if ftp.pline.low > zz_points[i].pline.low: 72 | ftp.pline.high = max(ftp.pline.high, zz_points[i].pline.high) 73 | zz_points[i] = ftp 74 | if i + 1 < len(zz_points): 75 | if scp.pline.high < zz_points[i + 1].pline.high: 76 | scp.pline.low = min(scp.pline.low, zz_points[i + 1].pline.low) 77 | zz_points[i + 1] = scp 78 | else: 79 | zz_points[i + 1].pline.low = min(scp.pline.low, zz_points[i + 1].pline.low) 80 | else: 81 | zz_points[i].pline.high = max(ftp.pline.high, zz_points[i].pline.high) 82 | i -= 1 83 | i += 1 84 | return zz_points 85 | 86 | 87 | class ZigZag(object): 88 | def __init__(self, df, kernel_size=3): 89 | self.df = df 90 | self.kernel_size = kernel_size 91 | self.zz_points: List[ZZPoint] = [] 92 | 93 | self.conv_col = self.df["Close"] #self.df[["Open", "Close"]].max(axis=1) 94 | self.find_break_points() 95 | self.fix_break_points() 96 | 97 | def find_break_points(self): 98 | # |-------------------------------| 99 | # |------------------------| 100 | kernel = [-1] * self.kernel_size + [0] + self.kernel_size * [1] 101 | conv_out = np.convolve(self.conv_col, kernel, "valid") 102 | conv_zeros_idx = [] 103 | for idx, (conv_i, conv_j) in enumerate(zip(conv_out[:-1], conv_out[1:])): 104 | if conv_i * conv_j <= 0: 105 | if abs(conv_i) < abs(conv_j): 106 | conv_zeros_idx.append(idx) 107 | else: 108 | conv_zeros_idx.append(idx + 1) 109 | conv_zeros_idx = list(set(conv_zeros_idx)) 110 | conv_zeros_idx.sort() 111 | zeros_idx = [idx + self.kernel_size for idx in conv_zeros_idx] 112 | self.correct_break_points(zeros_idx) 113 | 114 | def correct_break_points(self, zeros_idx): 115 | # classify peak_points into PEAK_POINT/POKE_POINT 116 | break_points_padded = [0] + zeros_idx + [len(self.df) - 1] 117 | start_idx = 0 118 | for i, (fp, sp, tp) in enumerate( 119 | zip(break_points_padded[:-2], break_points_padded[1:-1], break_points_padded[2:]) 120 | ): 121 | fp = max(fp, start_idx) 122 | if self.conv_col.iloc[fp] > self.conv_col.iloc[sp] and self.conv_col.iloc[tp] > self.conv_col.iloc[sp]: 123 | idx_min = self.df[fp + 1 : tp][["Close", "Low"]].idxmin() 124 | start_idx = idx_min[0] 125 | self.add_poke_point(idx_min) 126 | elif self.conv_col.iloc[fp] < self.conv_col.iloc[sp] and self.conv_col.iloc[tp] < self.conv_col.iloc[sp]: 127 | idx_max = self.df[fp + 1 : tp][["Close", "High"]].idxmax() 128 | start_idx = idx_max[0] 129 | self.add_peak_point(idx_max) 130 | 131 | def fix_break_points(self): 132 | temp_zz_points = ( 133 | [ZZPoint(0, POINT_TYPE.POKE_POINT, None)] 134 | + self.zz_points 135 | + [ZZPoint(len(self.df) - 1, POINT_TYPE.PEAK_POINT, None)] 136 | ) 137 | for i, (fp, sp, tp) in enumerate(zip(temp_zz_points[:-2], temp_zz_points[1:-1], temp_zz_points[2:])): 138 | if sp.ptype == POINT_TYPE.PEAK_POINT: 139 | fixed_point = self.df[fp.pidx + 1 : tp.pidx][["Close", "High"]].idxmax() 140 | sp.pidx = fixed_point[0] 141 | sp.pline = SRLine(self.df.iloc[fixed_point[0]]["Close"], self.df.iloc[fixed_point[1]]["High"]) 142 | else: 143 | fixed_point = self.df[fp.pidx + 1 : tp.pidx][["Close", "Low"]].idxmin() 144 | sp.pidx = fixed_point[0] 145 | sp.pline = SRLine(self.df.iloc[fixed_point[1]]["Low"], self.df.iloc[fixed_point[0]]["Close"]) 146 | 147 | def add_poke_point(self, idx): 148 | self.zz_points.append( 149 | ZZPoint(idx[0], POINT_TYPE.POKE_POINT, SRLine(self.df.iloc[idx[1]]["Low"], self.df.iloc[idx[0]]["Close"])) 150 | ) 151 | 152 | def add_peak_point(self, idx): 153 | self.zz_points.append( 154 | ZZPoint(idx[0], POINT_TYPE.PEAK_POINT, SRLine(self.df.iloc[idx[0]]["Close"], self.df.iloc[idx[1]]["High"])) 155 | ) 156 | 157 | def merge_break_points(self, min_div): 158 | merge_break_points(self.zz_points, min_div) 159 | 160 | 161 | def zigzag_conv(df, kernel_size, min_div): 162 | zz = ZigZag(df, kernel_size=kernel_size) 163 | zz.merge_break_points(min_div) 164 | return zz.zz_points 165 | 166 | 167 | def join_zz_points(ftp, scp): 168 | # join 2 zz_points ftp, scp same type 169 | # return a joined zz_point 170 | if ftp.ptype == POINT_TYPE.PEAK_POINT: 171 | return ZZPoint( 172 | ftp.pidx if ftp.pline.low >= scp.pline.low else scp.pidx, 173 | ftp.ptype, 174 | SRLine(max(ftp.pline.low, scp.pline.low), max(ftp.pline.high, scp.pline.high)), 175 | ) 176 | else: 177 | return ZZPoint( 178 | ftp.pidx if ftp.pline.high <= scp.pline.high else scp.pidx, 179 | ftp.ptype, 180 | SRLine(min(ftp.pline.low, scp.pline.low), min(ftp.pline.high, scp.pline.high)), 181 | ) 182 | 183 | 184 | def zigzag_conv_stream(df, kernel_size, min_div, zz_points): 185 | if len(zz_points) < 3: 186 | zz_points.clear() 187 | zz_points.extend(zigzag_conv(df, kernel_size, min_div)) 188 | return 189 | idx = max(zz_points[-3].pidx - 5, 0) 190 | zzps = zigzag_conv(df[idx:].reset_index(), kernel_size, min_div) 191 | for zzp in zzps: 192 | zzp.pidx += idx 193 | zzps = [zp for zp in zzps if zp.pidx > zz_points[-1].pidx] 194 | if len(zzps) > 0: 195 | if zzps[0].ptype == zz_points[-1].ptype: 196 | ftp = zz_points.pop() 197 | scp = zzps[0] 198 | joined_zz_point = join_zz_points(ftp, scp) 199 | last_idx = len(zz_points) - 2 200 | zz_points.append(joined_zz_point) 201 | zz_points.extend(zzps[1:]) 202 | merge_break_points(zz_points, min_div, from_idx=last_idx) 203 | else: 204 | last_idx = len(zz_points) - 2 205 | zz_points.extend(zzps) 206 | merge_break_points(zz_points, min_div, from_idx=last_idx) 207 | 208 | 209 | def zigzag(df, sigma: float): 210 | up_zig = True # Last extreme is a bottom. Next is a top. 211 | tmp_max = df.iloc[0]["High"] 212 | tmp_min = df.iloc[0]["Low"] 213 | tmp_max_i = 0 214 | tmp_min_i = 0 215 | close_max_i = 0 216 | close_min_i = 0 217 | zz_points = [] 218 | for i in range(len(df)): 219 | if up_zig: # Last extreme is a bottom 220 | if df.iloc[i]["High"] > tmp_max: 221 | # New high, update 222 | tmp_max = df.iloc[i]["High"] 223 | tmp_max_i = i 224 | elif ( 225 | df.iloc[i]["Close"] < tmp_max - tmp_max * sigma 226 | and df.iloc[i]["Close"] < df.iloc[tmp_max_i]["Close"] 227 | and df.iloc[i]["Low"] < df.iloc[tmp_max_i]["Low"] 228 | ): 229 | # Price retraced by sigma %. Top confirmed, record it 230 | # zz_points[0] = type 231 | # zz_points[1] = index 232 | # zz_points[2] = price 233 | zz_points.append( 234 | ZZPoint(close_max_i, POINT_TYPE.PEAK_POINT, SRLine(df.iloc[close_max_i]["Close"], tmp_max)) 235 | ) 236 | 237 | # Setup for next bottom 238 | up_zig = False 239 | tmp_min = df.iloc[i]["Low"] 240 | tmp_min_i = i 241 | close_min_i = i 242 | if df.iloc[i]["Close"] > df.iloc[close_max_i]["Close"]: 243 | close_max_i = i 244 | else: # Last extreme is a top 245 | if df.iloc[i]["Low"] < tmp_min: 246 | # New low, update 247 | tmp_min = df.iloc[i]["Low"] 248 | tmp_min_i = i 249 | elif ( 250 | df.iloc[i]["Close"] > tmp_min + tmp_min * sigma 251 | and df.iloc[i]["Close"] > df.iloc[tmp_min_i]["Close"] 252 | and df.iloc[i]["High"] > df.iloc[tmp_min_i]["High"] 253 | ): 254 | # Price retraced by sigma %. Bottom confirmed, record it 255 | # zz_points[0] = type 256 | # zz_points[1] = index 257 | # zz_points[2] = price 258 | zz_points.append( 259 | ZZPoint(close_min_i, POINT_TYPE.POKE_POINT, SRLine(tmp_min, df.iloc[close_min_i]["Close"])) 260 | ) 261 | 262 | # Setup for next top 263 | up_zig = True 264 | tmp_max = df.iloc[i]["High"] 265 | tmp_max_i = i 266 | close_max_i = i 267 | if df.iloc[i]["Close"] < df.iloc[close_min_i]["Close"]: 268 | close_min_i = i 269 | if len(zz_points) == 0: 270 | tmp_max_i = df["High"].idxmax() 271 | tmp_min_i = df["Low"].idxmin() 272 | if tmp_max_i < tmp_min_i: 273 | close_max_i = df["Close"].idxmax() 274 | zz_points.append(ZZPoint(close_max_i, POINT_TYPE.PEAK_POINT, SRLine(df.iloc[close_max_i]["Close"], df.iloc[tmp_max_i]["High"]))) 275 | else: 276 | close_min_i = df["Close"].idxmin() 277 | zz_points.append(ZZPoint(close_min_i, POINT_TYPE.POKE_POINT, SRLine(df.iloc[tmp_min_i]["Low"], df.iloc[close_min_i]["Close"]))) 278 | return zz_points 279 | 280 | 281 | def zigzag_stream(df, sigma: float, zz_points): 282 | last_zz_points = zz_points[-1] 283 | i = len(df) - 1 284 | 285 | if last_zz_points.ptype == POINT_TYPE.POKE_POINT: # Last extreme is a bottom 286 | tmp_max_i = df["High"].iloc[last_zz_points.pidx :].idxmax() 287 | close_max_i = df["Close"].iloc[last_zz_points.pidx :].idxmax() 288 | tmp_max = df["High"].iloc[tmp_max_i] 289 | if ( 290 | df.iloc[i]["Close"] < tmp_max - tmp_max * sigma 291 | and df.iloc[i]["Close"] < df.iloc[tmp_max_i]["Close"] 292 | and df.iloc[i]["Low"] < df.iloc[tmp_max_i]["Low"] 293 | ): 294 | zz_points.append( 295 | ZZPoint(close_max_i, POINT_TYPE.PEAK_POINT, SRLine(df["Close"].iloc[close_max_i], tmp_max)) 296 | ) 297 | else: # Last extreme is a top 298 | tmp_min_i = df["Low"].iloc[last_zz_points.pidx :].idxmin() 299 | close_min_i = df["Close"].iloc[last_zz_points.pidx :].idxmin() 300 | tmp_min = df["Low"].iloc[tmp_min_i] 301 | if ( 302 | df.iloc[i]["Close"] > tmp_min + tmp_min * sigma 303 | and df.iloc[i]["Close"] > df.iloc[tmp_min_i]["Close"] 304 | and df.iloc[i]["High"] > df.iloc[tmp_min_i]["High"] 305 | ): 306 | zz_points.append( 307 | ZZPoint(close_min_i, POINT_TYPE.POKE_POINT, SRLine(tmp_min, df["Close"].iloc[close_min_i].min())) 308 | ) 309 | -------------------------------------------------------------------------------- /strategies/price_action.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pandas as pd 3 | from datetime import datetime 4 | import talib as ta 5 | import plotly.graph_objects as go 6 | from plotly.subplots import make_subplots 7 | from .base_strategy import BaseStrategy 8 | import indicators as mta 9 | from order import Order, OrderType, OrderSide, OrderStatus 10 | from utils import get_line_coffs, find_uptrend_line, find_downtrend_line, get_y_on_line 11 | 12 | bot_logger = logging.getLogger("bot_logger") 13 | 14 | 15 | class PriceAction(BaseStrategy): 16 | """ 17 | - PriceAction strategy 18 | """ 19 | 20 | def __init__(self, name, params, tfs): 21 | super().__init__(name, params, tfs) 22 | self.tf = self.tfs["tf"] 23 | self.state = None 24 | self.trend = None 25 | self.min_zz_ratio = 0.01 * self.params["min_zz_pct"] 26 | self.temp = [] 27 | self.checked_pidx = [] 28 | 29 | def attach(self, tfs_chart): 30 | self.tfs_chart = tfs_chart 31 | self.init_indicators() 32 | 33 | def init_indicators(self): 34 | # calculate HA candelstick 35 | chart = self.tfs_chart[self.tf] 36 | self.ma_vol = ta.SMA(chart["Volume"], self.params["ma_vol"]) 37 | self.zz_points = mta.zigzag(chart, self.min_zz_ratio) 38 | self.init_main_zigzag() 39 | self.start_trading_time = chart.iloc[-1]["Open time"] 40 | 41 | def init_main_zigzag(self): 42 | self.main_zz_idx = [] 43 | if len(self.zz_points) > 0: 44 | self.main_zz_idx.append(0) 45 | else: 46 | return 47 | last_main_zz_idx = 0 48 | while last_main_zz_idx + 3 < len(self.zz_points): 49 | last_main_zz_type = self.zz_points[last_main_zz_idx].ptype 50 | if last_main_zz_type == mta.POINT_TYPE.PEAK_POINT: 51 | if ( 52 | self.zz_points[last_main_zz_idx + 2].pline.high < self.zz_points[last_main_zz_idx].pline.high 53 | and self.zz_points[last_main_zz_idx + 3].pline.low < self.zz_points[last_main_zz_idx + 1].pline.low 54 | ): 55 | last_main_zz_idx += 2 56 | else: 57 | last_main_zz_idx += 1 58 | self.main_zz_idx.append(last_main_zz_idx) 59 | else: 60 | if ( 61 | self.zz_points[last_main_zz_idx + 2].pline.low > self.zz_points[last_main_zz_idx].pline.low 62 | and self.zz_points[last_main_zz_idx + 3].pline.high > self.zz_points[last_main_zz_idx + 1].pline.low 63 | ): 64 | last_main_zz_idx += 2 65 | else: 66 | last_main_zz_idx += 1 67 | self.main_zz_idx.append(last_main_zz_idx) 68 | 69 | def update_main_zigzag(self): 70 | last_main_zz_idx = self.main_zz_idx[-1] 71 | while last_main_zz_idx + 3 < len(self.zz_points): 72 | last_main_zz_type = self.zz_points[last_main_zz_idx].ptype 73 | if last_main_zz_type == mta.POINT_TYPE.PEAK_POINT: 74 | if ( 75 | self.zz_points[last_main_zz_idx + 2].pline.high < self.zz_points[last_main_zz_idx].pline.high 76 | and self.zz_points[last_main_zz_idx + 3].pline.low < self.zz_points[last_main_zz_idx + 1].pline.low 77 | ): 78 | last_main_zz_idx += 2 79 | else: 80 | last_main_zz_idx += 1 81 | self.main_zz_idx.append(last_main_zz_idx) 82 | else: 83 | if ( 84 | self.zz_points[last_main_zz_idx + 2].pline.low > self.zz_points[last_main_zz_idx].pline.low 85 | and self.zz_points[last_main_zz_idx + 3].pline.high 86 | > self.zz_points[last_main_zz_idx + 1].pline.high 87 | ): 88 | last_main_zz_idx += 2 89 | else: 90 | last_main_zz_idx += 1 91 | self.main_zz_idx.append(last_main_zz_idx) 92 | 93 | last_main_zz_type = self.zz_points[last_main_zz_idx].ptype 94 | if last_main_zz_idx + 2 < len(self.zz_points): 95 | if last_main_zz_type == mta.POINT_TYPE.POKE_POINT: 96 | if self.zz_points[last_main_zz_idx + 2].pline.low < self.zz_points[last_main_zz_idx].pline.low: 97 | last_main_zz_idx += 1 98 | self.main_zz_idx.append(last_main_zz_idx) 99 | else: 100 | if self.zz_points[last_main_zz_idx + 2].pline.high > self.zz_points[last_main_zz_idx].pline.high: 101 | last_main_zz_idx += 1 102 | self.main_zz_idx.append(last_main_zz_idx) 103 | 104 | last_main_zz_type = self.zz_points[last_main_zz_idx].ptype 105 | if last_main_zz_idx + 1 < len(self.zz_points): 106 | last_kline = self.tfs_chart[self.tf].iloc[-1] 107 | if last_main_zz_type == mta.POINT_TYPE.POKE_POINT: 108 | if last_kline["Low"] < self.zz_points[last_main_zz_idx].pline.low: 109 | last_main_zz_idx += 1 110 | self.main_zz_idx.append(last_main_zz_idx) 111 | else: 112 | if last_kline["High"] > self.zz_points[last_main_zz_idx].pline.high: 113 | last_main_zz_idx += 1 114 | self.main_zz_idx.append(last_main_zz_idx) 115 | 116 | def update_indicators(self, tf): 117 | if tf != self.tf: 118 | return 119 | chart = self.tfs_chart[self.tf] 120 | self.ma_vol.loc[len(self.ma_vol)] = ta.stream.SMA(chart["Volume"], self.params["ma_vol"]) 121 | last_Zz_pidx = self.zz_points[-1].pidx 122 | mta.zigzag_stream(chart, self.min_zz_ratio, self.zz_points) 123 | last_main_idx = self.main_zz_idx[-1] 124 | self.update_main_zigzag() 125 | if last_main_idx != self.main_zz_idx[-1]: 126 | self.temp.append((self.zz_points[self.main_zz_idx[-1]].pidx, len(chart) - 1)) 127 | 128 | def check_required_params(self): 129 | return all( 130 | [ 131 | key in self.params.keys() 132 | for key in [ 133 | "ma_vol", 134 | "vol_ratio_ma", 135 | "kline_body_ratio", 136 | "sl_fix_mode", 137 | ] 138 | ] 139 | ) 140 | 141 | def is_params_valid(self): 142 | if not self.check_required_params(): 143 | bot_logger.info(" [-] Missing required params") 144 | return False 145 | return True 146 | 147 | def update(self, tf): 148 | # update when new kline arrive 149 | super().update(tf) 150 | # check order signal 151 | self.check_close_signal() 152 | self.check_signal() 153 | 154 | def close_opening_orders(self): 155 | super().close_opening_orders(self.tfs_chart[self.tf].iloc[-1]) 156 | 157 | def check_signal(self): 158 | if len(self.main_zz_idx) < 5: 159 | return 160 | chart = self.tfs_chart[self.tf] 161 | last_kline = chart.iloc[-1] 162 | p_1 = self.zz_points[self.main_zz_idx[-1]] 163 | p_2 = self.zz_points[self.main_zz_idx[-2]] 164 | p_3 = self.zz_points[self.main_zz_idx[-3]] 165 | p_4 = self.zz_points[self.main_zz_idx[-4]] 166 | p_5 = self.zz_points[self.main_zz_idx[-5]] 167 | if p_1.pidx in self.checked_pidx: 168 | return 169 | if p_1.ptype == mta.POINT_TYPE.POKE_POINT: 170 | # /\ 171 | # /\ / \/ 172 | # / \/ 2 1 173 | # / 4 3 174 | # 5 175 | if p_5.pline.low < p_3.pline.low <= p_1.pline.low and p_1.pline.low <= p_4.pline.high: 176 | if p_3.pline.low <= p_4.pline.high - 0.4 * ( 177 | p_4.pline.high - p_5.pline.low 178 | ) and p_3.pline.low >= p_4.pline.high - 0.62 * (p_4.pline.high - p_5.pline.low): 179 | if 1.002 * p_4.pline.high < p_2.pline.high: 180 | sl = p_1.pline.low 181 | tp = None 182 | order = Order( 183 | OrderType.MARKET, 184 | OrderSide.BUY, 185 | last_kline["Close"], 186 | tp=tp, 187 | sl=sl, 188 | status=OrderStatus.FILLED, 189 | ) 190 | order["FILL_TIME"] = last_kline["Open time"] 191 | order["strategy"] = self.name 192 | order["description"] = self.description 193 | order["desc"] = {} 194 | order = self.trader.fix_order(order, self.params["sl_fix_mode"], self.max_sl_pct) 195 | if order: 196 | self.trader.create_trade(order, self.volume) 197 | self.orders_opening.append(order) 198 | self.checked_pidx.append(p_1.pidx) 199 | self.close_order_by_side(last_kline, OrderSide.SELL) 200 | else: 201 | if p_5.pline.high > p_3.pline.high >= p_1.pline.high and p_1.pline.high >= p_4.pline.low: 202 | if p_4.pline.low > p_2.pline.low * 1.002: 203 | sl = p_1.pline.high 204 | tp = None 205 | order = Order( 206 | OrderType.MARKET, 207 | OrderSide.SELL, 208 | last_kline["Close"], 209 | tp=tp, 210 | sl=sl, 211 | status=OrderStatus.FILLED, 212 | ) 213 | order["FILL_TIME"] = last_kline["Open time"] 214 | order["strategy"] = self.name 215 | order["description"] = self.description 216 | order["desc"] = {} 217 | order = self.trader.fix_order(order, self.params["sl_fix_mode"], self.max_sl_pct) 218 | if order: 219 | self.trader.create_trade(order, self.volume) 220 | self.orders_opening.append(order) 221 | self.checked_pidx.append(p_1.pidx) 222 | self.close_order_by_side(last_kline, OrderSide.BUY) 223 | 224 | def check_close_signal(self): 225 | chart = self.tfs_chart[self.tf] 226 | last_kline = chart.iloc[-1] 227 | if last_kline["Volume"] < self.params["vol_ratio_ma"] * self.ma_vol.iloc[-1]: 228 | return 229 | if last_kline["Close"] < last_kline["Open"]: 230 | # red kline, check close buy orders 231 | if (last_kline["High"] - last_kline["Close"]) < 0.75 * (last_kline["High"] - last_kline["Low"]): 232 | return 233 | else: 234 | # green kline, check close sell orders 235 | if (last_kline["Close"] - last_kline["Low"]) < 0.75 * (last_kline["High"] - last_kline["Low"]): 236 | return 237 | 238 | def plot_orders(self): 239 | fig = make_subplots(2, 1, vertical_spacing=0.02, shared_xaxes=True, row_heights=[0.8, 0.2]) 240 | fig.update_layout( 241 | xaxis_rangeslider_visible=False, 242 | xaxis2_rangeslider_visible=False, 243 | yaxis=dict(showgrid=False), 244 | yaxis2=dict(showgrid=False), 245 | xaxis=dict(showgrid=False), 246 | xaxis2=dict(showgrid=False), 247 | plot_bgcolor="rgb(19, 23, 34)", 248 | paper_bgcolor="rgb(121,125,127)", 249 | font=dict(color="rgb(247,249,249)"), 250 | ) 251 | df = self.tfs_chart[self.tf] 252 | dt2idx = dict(zip(df["Open time"], list(range(len(df))))) 253 | tmp_ot = df["Open time"] 254 | df["Open time"] = list(range(len(df))) 255 | super().plot_orders(fig, self.tf, 1, 1, dt2idx=dt2idx) 256 | 257 | fig.add_trace( 258 | go.Scatter( 259 | x=[df.iloc[ad.pidx]["Open time"] for ad in self.zz_points], 260 | y=[ad.pline.low if ad.ptype == mta.POINT_TYPE.POKE_POINT else ad.pline.high for ad in self.zz_points], 261 | mode="lines", 262 | line=dict(dash="dash"), 263 | marker_color=[ 264 | "rgba(255, 255, 0, 1)" if ad.ptype == mta.POINT_TYPE.POKE_POINT else "rgba(0, 255, 0, 1)" 265 | for ad in self.zz_points 266 | ], 267 | name="ZigZag Point", 268 | hoverinfo="skip", 269 | ), 270 | row=1, 271 | col=1, 272 | ) 273 | 274 | fig.add_trace( 275 | go.Scatter( 276 | x=df["Open time"], 277 | y=ta.SMA(df["Close"], 200), 278 | mode="lines", 279 | line=dict(color="orange"), 280 | name="SMA_200", 281 | hoverinfo="skip", 282 | ), 283 | row=1, 284 | col=1, 285 | ) 286 | 287 | main_zz_points = [self.zz_points[i] for i in self.main_zz_idx] 288 | fig.add_trace( 289 | go.Scatter( 290 | x=[df.iloc[ad.pidx]["Open time"] for ad in main_zz_points], 291 | y=[ad.pline.low if ad.ptype == mta.POINT_TYPE.POKE_POINT else ad.pline.high for ad in main_zz_points], 292 | mode="lines", 293 | name="Main ZigZag Point", 294 | hoverinfo="skip", 295 | ), 296 | row=1, 297 | col=1, 298 | ) 299 | for x, y in self.temp: 300 | fig.add_shape( 301 | type="line", 302 | x0=df.iloc[x]["Open time"], 303 | y0=df.iloc[x]["High"], 304 | x1=df.iloc[y]["Open time"], 305 | y1=df.iloc[x]["High"], 306 | line=dict(color="red"), 307 | row=1, 308 | col=1, 309 | ) 310 | 311 | for order in self.orders_closed: 312 | desc = order["desc"] 313 | 314 | colors = [ 315 | "rgb(242, 54, 69)" if kline["Open"] > kline["Close"] else "rgb(8, 153, 129)" for i, kline in df.iterrows() 316 | ] 317 | fig.add_trace( 318 | go.Bar(x=list(range(len(df))), y=df["Volume"], marker_color=colors, marker_line_width=0, name="Volume"), 319 | row=2, 320 | col=1, 321 | ) 322 | fig.add_trace( 323 | go.Scatter( 324 | x=df["Open time"], 325 | y=self.ma_vol, 326 | mode="lines", 327 | line=dict(color="rgba(0, 255, 0, 1)"), 328 | name="MA_VOL_{}".format(self.params["ma_vol"]), 329 | ), 330 | row=2, 331 | col=1, 332 | ) 333 | fig.update_xaxes(showspikes=True, spikesnap="data") 334 | fig.update_yaxes(showspikes=True, spikesnap="data") 335 | fig.update_layout(hovermode="x", spikedistance=-1) 336 | fig.update_layout(hoverlabel=dict(bgcolor="white", font_size=16)) 337 | fig.update_layout( 338 | title={ 339 | "text": "Breakout Strategy({}) (tf {})".format(self.trader.symbol_name, self.tf), 340 | "x": 0.5, 341 | "xanchor": "center", 342 | } 343 | ) 344 | df["Open time"] = tmp_ot 345 | return fig 346 | -------------------------------------------------------------------------------- /strategies/rsi_regular_divergence.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pandas as pd 3 | from datetime import datetime 4 | import talib as ta 5 | import plotly.graph_objects as go 6 | from plotly.subplots import make_subplots 7 | from .base_strategy import BaseStrategy 8 | import indicators as mta 9 | from order import Order, OrderType, OrderSide, OrderStatus 10 | from utils import get_line_coffs, find_uptrend_line, find_downtrend_line, get_y_on_line 11 | 12 | bot_logger = logging.getLogger("bot_logger") 13 | 14 | 15 | class RSIRegularDivergence(BaseStrategy): 16 | """ 17 | - RSI Divergence/Hidden Divergence strategy 18 | - Divergence: price move in opposite direction of technical indicator 19 | - Regular divergence 20 | - price creates higher highs but indicator creates lower highs 21 | - price creates lower low but indicator creates higher low 22 | -> trend change reversal signal 23 | - HIdden divergence 24 | - price creates higher low but indicator creates lower low 25 | - price creates lower high but dincator creates higer high 26 | -> trend continuation signals 27 | 28 | """ 29 | 30 | def __init__(self, name, params, tfs): 31 | super().__init__(name, params, tfs) 32 | self.tf = self.tfs["tf"] 33 | self.checked_pidx = [] 34 | self.checked_seg = [] 35 | self.divergenced_segs = [] 36 | 37 | def attach(self, tfs_chart): 38 | self.tfs_chart = tfs_chart 39 | self.init_indicators() 40 | 41 | def init_indicators(self): 42 | # calculate ZigZag indicator 43 | chart = self.tfs_chart[self.tf] 44 | self.rsi = ta.RSI(chart["Close"], self.params["rsi_len"]) 45 | self.delta_rsi = self.params["delta_rsi"] 46 | self.delta_price_ratio = 0.01 * self.params["delta_price_pct"] 47 | self.min_reward_ratio = 0.01 * self.params["min_rw_pct"] 48 | self.min_zz_ratio = 0.01 * self.params["min_zz_pct"] 49 | if self.params["zz_type"] == "ZZ_CONV": 50 | self.zz_points = mta.zigzag_conv(chart, self.params["zz_conv_size"], self.min_zz_ratio) 51 | else: 52 | self.zz_points = mta.zigzag(chart, self.min_zz_ratio) 53 | self.start_trading_time = chart.iloc[-1]["Open time"] 54 | 55 | def update_indicators(self, tf): 56 | if tf != self.tf: 57 | return 58 | chart = self.tfs_chart[self.tf] 59 | self.rsi.loc[len(self.rsi)] = ta.stream.RSI(chart["Close"], self.params["rsi_len"]) 60 | if self.params["zz_type"] == "ZZ_CONV": 61 | mta.zigzag_conv_stream(chart, self.params["zz_conv_size"], self.min_zz_ratio, self.zz_points) 62 | else: 63 | mta.zigzag_stream(chart, self.min_zz_ratio, self.zz_points) 64 | 65 | def check_required_params(self): 66 | return all( 67 | [ 68 | key in self.params.keys() 69 | for key in [ 70 | "rsi_len", 71 | "delta_rsi", 72 | "delta_price_pct", 73 | "n_last_point", 74 | "min_rr", 75 | "min_rw_pct", 76 | "min_zz_pct", 77 | "zz_type", 78 | "zz_conv_size", 79 | "sl_fix_mode", 80 | "n_trend_point", 81 | ] 82 | ] 83 | ) 84 | 85 | def is_params_valid(self): 86 | if not self.check_required_params(): 87 | bot_logger.info(" [-] Missing required params") 88 | return False 89 | return True 90 | 91 | def update(self, tf): 92 | # update when new kline arrive 93 | super().update(tf) 94 | # determind trend 95 | self.check_close_signal() 96 | self.check_signal() 97 | 98 | def close_opening_orders(self): 99 | super().close_opening_orders(self.tfs_chart[self.tf].iloc[-1]) 100 | 101 | def filter_poke_points(self, poke_points, last_point): 102 | valid_points = [] 103 | for i, poke_point in enumerate(poke_points): 104 | slope, intercept = get_line_coffs( 105 | (poke_point.pidx, poke_point.pline.low), (last_point.pidx, last_point.pline.low) 106 | ) 107 | b_1 = self.tfs_chart[self.tf].iloc[poke_point.pidx + 1 : last_point.pidx]["Low"] # try replace with Low 108 | b_0 = self.tfs_chart[self.tf].iloc[poke_point.pidx + 1 : last_point.pidx].index.to_series() 109 | if ((slope * b_0 + intercept) * (1 - self.delta_price_ratio) <= b_1).all(): 110 | valid_points.append(i) 111 | return valid_points 112 | 113 | def filter_peak_points(self, peak_points, last_point): 114 | valid_points = [] 115 | for i, peak_point in enumerate(peak_points): 116 | slope, intercept = get_line_coffs( 117 | (peak_point.pidx, peak_point.pline.high), (last_point.pidx, last_point.pline.high) 118 | ) 119 | b_1 = self.tfs_chart[self.tf].iloc[peak_point.pidx + 1 : last_point.pidx]["High"] # try replace with Low 120 | b_0 = self.tfs_chart[self.tf].iloc[peak_point.pidx + 1 : last_point.pidx].index.to_series() 121 | if ((slope * b_0 + intercept) * (1 + self.delta_price_ratio) >= b_1).all(): 122 | valid_points.append(i) 123 | return valid_points 124 | 125 | def check_rsi(self, rsi, above=True): 126 | idxs = rsi.index.to_series() 127 | slope, intercept = get_line_coffs((idxs.iloc[0], rsi.iloc[0]), (idxs.iloc[-1], rsi.iloc[-1])) 128 | b_1 = rsi.iloc[1:-1] 129 | b_0 = idxs.iloc[1:-1] 130 | if above: 131 | return (slope * b_0 + intercept - self.delta_rsi <= b_1).all() 132 | return (slope * b_0 + intercept + self.delta_rsi >= b_1).all() 133 | 134 | def find_divergenced_seg(self): 135 | last_zz_point = self.zz_points[-1] 136 | # Specify up/down trend lines using n_trend_point ZZPoint 137 | n_last_poke_points = [] 138 | n_last_peak_points = [] 139 | for zz_point in self.zz_points[-self.params["n_trend_point"] :]: 140 | if zz_point.ptype == mta.POINT_TYPE.POKE_POINT: 141 | n_last_poke_points.append((zz_point.pidx, zz_point.pline.low)) 142 | else: 143 | n_last_peak_points.append((zz_point.pidx, zz_point.pline.high)) 144 | if len(n_last_peak_points) < 2 or len(n_last_poke_points) < 2: 145 | return 146 | if last_zz_point.ptype == mta.POINT_TYPE.PEAK_POINT: 147 | # price higher highs, indicator lower highs. 148 | # get n_last_point peak points lower high 149 | n_zz_points = [] 150 | i = 1 151 | while 2 * i + 1 <= len(self.zz_points): 152 | current_zz_point = self.zz_points[-(2 * i + 1)] 153 | if current_zz_point.pline.high < last_zz_point.pline.high: 154 | n_zz_points.append(current_zz_point) 155 | if len(n_zz_points) >= self.params["n_last_point"]: 156 | break 157 | i += 1 158 | valid_points_idx = self.filter_peak_points(n_zz_points, last_zz_point) 159 | for valid_point_idx in valid_points_idx: 160 | zz_point = n_zz_points[valid_point_idx] 161 | low_2_idx = self.rsi.iloc[last_zz_point.pidx :].idxmax() 162 | if self.rsi[zz_point.pidx] > self.rsi[low_2_idx] + self.delta_rsi: 163 | if (last_zz_point.pline.high - zz_point.pline.high) < self.delta_price_ratio * zz_point.pline.high: 164 | continue 165 | if not self.check_rsi(self.rsi.iloc[zz_point.pidx : low_2_idx + 1], above=False): 166 | continue 167 | self.divergenced_segs.append( 168 | ((zz_point, last_zz_point), (self.rsi[zz_point.pidx], self.rsi[low_2_idx])) 169 | ) 170 | break 171 | 172 | if last_zz_point.ptype == mta.POINT_TYPE.POKE_POINT: 173 | # price lower low, indicator higher low. 174 | # get n_last_point peak points lower high 175 | n_zz_points = [] 176 | i = 1 177 | while 2 * i + 1 <= len(self.zz_points): 178 | current_zz_point = self.zz_points[-(2 * i + 1)] 179 | if current_zz_point.pline.low > last_zz_point.pline.low: 180 | n_zz_points.append(current_zz_point) 181 | if len(n_zz_points) >= self.params["n_last_point"]: 182 | break 183 | i += 1 184 | valid_points_idx = self.filter_poke_points(n_zz_points, last_zz_point) 185 | for valid_point_idx in valid_points_idx: 186 | zz_point = n_zz_points[valid_point_idx] 187 | low_2_idx = self.rsi.iloc[last_zz_point.pidx :].idxmin() 188 | if self.rsi[zz_point.pidx] < self.rsi[low_2_idx] - self.delta_rsi: 189 | if (zz_point.pline.low - last_zz_point.pline.low) < self.delta_price_ratio * zz_point.pline.low: 190 | continue 191 | if not self.check_rsi(self.rsi.iloc[zz_point.pidx : low_2_idx + 1], above=True): 192 | continue 193 | self.divergenced_segs.append( 194 | ((zz_point, last_zz_point), (self.rsi[zz_point.pidx], self.rsi[low_2_idx])) 195 | ) 196 | break 197 | 198 | def check_signal(self): 199 | last_zz_point = self.zz_points[-1] 200 | if last_zz_point.pidx in self.checked_pidx: 201 | return 202 | self.checked_pidx.append(last_zz_point.pidx) 203 | self.find_divergenced_seg() 204 | if len(self.divergenced_segs) == 0: 205 | return 206 | chart = self.tfs_chart[self.tf] 207 | last_kline = chart.iloc[-1] 208 | last_diverg_seg, last_rsi_diverg = self.divergenced_segs[-1] 209 | if last_diverg_seg[1].pidx in self.checked_seg: 210 | return 211 | # Specify up/down trend lines using n_trend_point ZZPoint 212 | n_last_poke_points = [] 213 | n_last_peak_points = [] 214 | for zz_point in self.zz_points[-self.params["n_trend_point"] :]: 215 | if zz_point.ptype == mta.POINT_TYPE.POKE_POINT: 216 | n_last_poke_points.append((zz_point.pidx, zz_point.pline.low)) 217 | else: 218 | n_last_peak_points.append((zz_point.pidx, zz_point.pline.high)) 219 | if len(n_last_peak_points) < 2 or len(n_last_poke_points) < 2: 220 | return 221 | max_high = max([zp[1] for zp in n_last_peak_points]) 222 | min_low = min([zp[1] for zp in n_last_poke_points]) 223 | if last_zz_point.ptype == mta.POINT_TYPE.PEAK_POINT and last_diverg_seg[0].ptype == mta.POINT_TYPE.PEAK_POINT: 224 | if last_zz_point.pline.high > self.zz_points[-3].pline.high: 225 | return 226 | # price higher highs, indicator lower highs. 227 | sl = last_zz_point.pline.high 228 | entry = last_kline["Close"] 229 | tp = max_high - self.params["fib_retr_lv"] * (max_high - min_low) 230 | order = Order( 231 | OrderType.MARKET, 232 | OrderSide.SELL, 233 | entry, 234 | tp=tp, 235 | sl=sl, 236 | status=OrderStatus.FILLED, 237 | ) 238 | if order.rr >= self.params["min_rr"] and order.reward_ratio > self.min_reward_ratio and order.is_valid(): 239 | if order.type == OrderType.MARKET: 240 | order["FILL_TIME"] = last_kline["Open time"] 241 | order["strategy"] = self.name 242 | order["description"] = self.description 243 | self.trader.create_trade(order, self.volume) 244 | order["desc"] = { 245 | "type": "higher_high_lower_high", 246 | "zz_point_1": last_diverg_seg[0], 247 | "zz_point_2": last_diverg_seg[1], 248 | "rsi_1": last_rsi_diverg[0], 249 | "rsi_2": last_rsi_diverg[1], 250 | } 251 | self.orders_opening.append(order) 252 | self.checked_seg.append(last_diverg_seg[1].pidx) 253 | 254 | elif last_zz_point.ptype == mta.POINT_TYPE.POKE_POINT and last_diverg_seg[0].ptype == mta.POINT_TYPE.POKE_POINT: 255 | if last_zz_point.pline.low < self.zz_points[-3].pline.low: 256 | return 257 | # price lower low, indicator higher low. 258 | sl = last_zz_point.pline.low 259 | entry = last_kline["Close"] 260 | tp = min_low + self.params["fib_retr_lv"] * (max_high - min_low) 261 | order = Order( 262 | OrderType.MARKET, 263 | OrderSide.BUY, 264 | entry, 265 | tp=tp, 266 | sl=sl, 267 | status=OrderStatus.FILLED, 268 | ) 269 | if order.rr >= self.params["min_rr"] and order.reward_ratio > self.min_reward_ratio and order.is_valid(): 270 | if order.type == OrderType.MARKET: 271 | order["FILL_TIME"] = last_kline["Open time"] 272 | order["strategy"] = self.name 273 | order["description"] = self.description 274 | self.trader.create_trade(order, self.volume) 275 | order["desc"] = { 276 | "type": "lower_low_higher_low", 277 | "zz_point_1": last_diverg_seg[0], 278 | "zz_point_2": last_diverg_seg[1], 279 | "rsi_1": last_rsi_diverg[0], 280 | "rsi_2": last_rsi_diverg[1], 281 | } 282 | self.orders_opening.append(order) 283 | self.checked_seg.append(last_diverg_seg[1].pidx) 284 | 285 | def check_close_signal(self): 286 | if len(self.zz_points) < 4: 287 | return 288 | chart = self.tfs_chart[self.tf] 289 | last_kline = chart.iloc[-1] 290 | zz_point_1 = self.zz_points[-1] 291 | if self.zz_points[-1].ptype == mta.POINT_TYPE.PEAK_POINT: 292 | if self.zz_points[-1].pline.high < self.zz_points[-3].pline.high: 293 | if self.zz_points[-2].pline.low < self.zz_points[-4].pline.low: 294 | self.close_order_by_side(last_kline, OrderSide.BUY) 295 | else: 296 | if self.zz_points[-2].pline.low > self.zz_points[-4].pline.low: 297 | self.close_order_by_side(last_kline, OrderSide.SELL) 298 | else: 299 | if self.zz_points[-1].pline.low > self.zz_points[-3].pline.low: 300 | if self.zz_points[-2].pline.high > self.zz_points[-4].pline.high: 301 | self.close_order_by_side(last_kline, OrderSide.SELL) 302 | else: 303 | if self.zz_points[-2].pline.high < self.zz_points[-3].pline.high: 304 | self.close_order_by_side(last_kline, OrderSide.BUY) 305 | 306 | def plot_orders(self): 307 | fig = make_subplots(3, 1, vertical_spacing=0.02, shared_xaxes=True, row_heights=[0.6, 0.2, 0.2]) 308 | fig.update_layout( 309 | xaxis_rangeslider_visible=False, 310 | xaxis2_rangeslider_visible=False, 311 | xaxis=dict(showgrid=False), 312 | xaxis2=dict(showgrid=False), 313 | yaxis=dict(showgrid=False), 314 | yaxis2=dict(showgrid=False), 315 | plot_bgcolor="#2E4053", 316 | paper_bgcolor="#797D7F", 317 | font=dict(color="#F7F9F9"), 318 | ) 319 | df = self.tfs_chart[self.tf] 320 | dt2idx = dict(zip(df["Open time"], list(range(len(df))))) 321 | tmp_ot = df["Open time"] 322 | df["Open time"] = list(range(len(df))) 323 | super().plot_orders(fig, self.tf, 1, 1, dt2idx=dt2idx) 324 | df = self.tfs_chart[self.tf] 325 | 326 | fig.add_trace( 327 | go.Scatter( 328 | x=[df.iloc[ad.pidx]["Open time"] for ad in self.zz_points], 329 | y=[ad.pline.low if ad.ptype == mta.POINT_TYPE.POKE_POINT else ad.pline.high for ad in self.zz_points], 330 | mode="lines", 331 | line=dict(color="orange", dash="dash"), 332 | hoverinfo="skip", 333 | name="ZigZag Point", 334 | ), 335 | row=1, 336 | col=1, 337 | ) 338 | fig.add_trace( 339 | go.Scatter( 340 | x=df["Open time"], 341 | y=self.rsi, 342 | mode="lines", 343 | line=dict(color="orange"), 344 | name="RSI_{}".format(self.params["rsi_len"]), 345 | ), 346 | row=2, 347 | col=1, 348 | ) 349 | fig.add_shape( 350 | type="line", 351 | x0=0, 352 | y0=30, 353 | x1=1, 354 | y1=30, 355 | xref="x domain", 356 | line=dict(color="magenta", width=1, dash="dash"), 357 | row=2, 358 | col=1, 359 | ) 360 | fig.add_shape( 361 | type="line", 362 | x0=0, 363 | y0=70, 364 | x1=1, 365 | y1=70, 366 | xref="x domain", 367 | line=dict(color="magenta", width=1, dash="dash"), 368 | row=2, 369 | col=1, 370 | ) 371 | for order in self.orders_closed: 372 | desc = order["desc"] 373 | zz_point_1 = desc["zz_point_1"] 374 | zz_point_2 = desc["zz_point_2"] 375 | fig.add_trace( 376 | go.Scatter( 377 | x=[df.iloc[zz_point_1.pidx]["Open time"], df.iloc[zz_point_2.pidx]["Open time"]], 378 | y=[desc["rsi_1"], desc["rsi_2"]], 379 | mode="markers+lines", 380 | line=dict(color="orange", width=1), 381 | hoverinfo="skip", 382 | showlegend=False, 383 | ), 384 | row=2, 385 | col=1, 386 | ) 387 | fig.add_trace( 388 | go.Scatter( 389 | x=[df.iloc[zz_point_1.pidx]["Open time"], df.iloc[zz_point_2.pidx]["Open time"]], 390 | y=[zz_point_1.pline.low, zz_point_2.pline.low] 391 | if zz_point_1.ptype == mta.POINT_TYPE.POKE_POINT 392 | else [zz_point_1.pline.high, zz_point_2.pline.high], 393 | mode="markers+lines", 394 | line=dict(color="orange", width=2), 395 | hoverinfo="skip", 396 | showlegend=False, 397 | ), 398 | row=1, 399 | col=1, 400 | ) 401 | 402 | colors = ["red" if kline["Open"] > kline["Close"] else "green" for i, kline in df.iterrows()] 403 | fig.add_trace(go.Bar(x=df["Open time"], y=df["Volume"], marker_color=colors), row=3, col=1) 404 | 405 | fig.update_xaxes(showspikes=True, spikesnap="data") 406 | fig.update_yaxes(showspikes=True, spikesnap="data") 407 | fig.update_layout(hovermode="x", spikedistance=-1) 408 | fig.update_layout(hoverlabel=dict(bgcolor="white", font_size=16)) 409 | 410 | fig.update_layout( 411 | title={ 412 | "text": "RSI Regular Divergence Strategy (symbol: {}, tf: {})".format(self.trader.symbol_name, self.tf), 413 | "x": 0.5, 414 | "xanchor": "center", 415 | } 416 | ) 417 | df["Open time"] = tmp_ot 418 | return fig 419 | -------------------------------------------------------------------------------- /strategies/rsi_hidden_divergence.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pandas as pd 3 | from datetime import datetime 4 | import talib as ta 5 | import plotly.graph_objects as go 6 | from plotly.subplots import make_subplots 7 | from .base_strategy import BaseStrategy 8 | import indicators as mta 9 | from order import Order, OrderType, OrderSide, OrderStatus 10 | from utils import get_line_coffs, find_uptrend_line, find_downtrend_line, get_y_on_line 11 | 12 | bot_logger = logging.getLogger("bot_logger") 13 | 14 | 15 | class RSIDivergence(BaseStrategy): 16 | """ 17 | - RSI Divergence/Hidden Divergence strategy 18 | - Divergence: price move in opposite direction of technical indicator 19 | - Regular divergence 20 | - price creates higher highs but indicator creates lower highs 21 | - price creates lower low but indicator creates higher low 22 | -> trend change reversal signal 23 | - HIdden divergence 24 | - price creates higher low but indicator creates lower low 25 | - price creates lower high but dincator creates higer high 26 | -> trend continuation signals 27 | 28 | """ 29 | 30 | def __init__(self, name, params, tfs): 31 | super().__init__(name, params, tfs) 32 | self.tf = self.tfs["tf"] 33 | self.checked_pidx = [] 34 | 35 | def attach(self, tfs_chart): 36 | self.tfs_chart = tfs_chart 37 | self.init_indicators() 38 | 39 | def init_indicators(self): 40 | # calculate ZigZag indicator 41 | chart = self.tfs_chart[self.tf] 42 | self.rsi = ta.RSI(chart["Close"], self.params["rsi_len"]) 43 | self.delta_rsi = self.params["delta_rsi"] 44 | self.delta_price_ratio = 0.01 * self.params["delta_price_pct"] 45 | self.min_reward_ratio = 0.01 * self.params["min_rw_pct"] 46 | self.min_zz_ratio = 0.01 * self.params["min_zz_pct"] 47 | self.min_trend_ratio = 0.01 * self.params["min_trend_pct"] 48 | if self.params["zz_type"] == "ZZ_CONV": 49 | self.zz_points = mta.zigzag_conv(chart, self.params["zz_conv_size"], self.min_zz_ratio) 50 | else: 51 | self.zz_points = mta.zigzag(chart, self.min_zz_ratio) 52 | self.last_check_zz_point = self.zz_points[0] 53 | self.start_trading_time = chart.iloc[-1]["Open time"] 54 | 55 | def update_indicators(self, tf): 56 | if tf != self.tf: 57 | return 58 | chart = self.tfs_chart[self.tf] 59 | self.rsi.loc[len(self.rsi)] = ta.stream.RSI(chart["Close"], self.params["rsi_len"]) 60 | if self.params["zz_type"] == "ZZ_CONV": 61 | mta.zigzag_conv_stream(chart, self.params["zz_conv_size"], self.min_zz_ratio, self.zz_points) 62 | else: 63 | mta.zigzag_stream(chart, self.min_zz_ratio, self.zz_points) 64 | 65 | def check_required_params(self): 66 | return all( 67 | [ 68 | key in self.params.keys() 69 | for key in [ 70 | "rsi_len", 71 | "delta_rsi", 72 | "delta_price_pct", 73 | "n_last_point", 74 | "min_rr", 75 | "min_rw_pct", 76 | "min_zz_pct", 77 | "min_trend_pct", 78 | "min_updown_ratio", 79 | "zz_type", 80 | "zz_conv_size", 81 | "sl_fix_mode", 82 | "n_trend_point", 83 | ] 84 | ] 85 | ) 86 | 87 | def is_params_valid(self): 88 | if not self.check_required_params(): 89 | bot_logger.info(" [-] Missing required params") 90 | return False 91 | return True 92 | 93 | def update(self, tf): 94 | # update when new kline arrive 95 | super().update(tf) 96 | # determind trend 97 | self.check_signal() 98 | 99 | def close_opening_orders(self): 100 | super().close_opening_orders(self.tfs_chart[self.tf].iloc[-1]) 101 | 102 | def filter_poke_points(self, poke_points, last_point): 103 | valid_points = [] 104 | for i, poke_point in enumerate(poke_points): 105 | slope, intercept = get_line_coffs( 106 | (poke_point.pidx, poke_point.pline.low), (last_point.pidx, last_point.pline.low) 107 | ) 108 | b_1 = self.tfs_chart[self.tf].iloc[poke_point.pidx + 1 : last_point.pidx]["Low"] # try replace with Low 109 | b_0 = self.tfs_chart[self.tf].iloc[poke_point.pidx + 1 : last_point.pidx].index.to_series() 110 | if ((slope * b_0 + intercept) * (1 - self.delta_price_ratio) <= b_1).all(): 111 | valid_points.append(i) 112 | return valid_points 113 | 114 | def filter_peak_points(self, peak_points, last_point): 115 | valid_points = [] 116 | for i, peak_point in enumerate(peak_points): 117 | slope, intercept = get_line_coffs( 118 | (peak_point.pidx, peak_point.pline.high), (last_point.pidx, last_point.pline.high) 119 | ) 120 | b_1 = self.tfs_chart[self.tf].iloc[peak_point.pidx + 1 : last_point.pidx]["High"] # try replace with Low 121 | b_0 = self.tfs_chart[self.tf].iloc[peak_point.pidx + 1 : last_point.pidx].index.to_series() 122 | if ((slope * b_0 + intercept) * (1 + self.delta_price_ratio) >= b_1).all(): 123 | valid_points.append(i) 124 | return valid_points 125 | 126 | def check_rsi(self, rsi, above=True): 127 | idxs = rsi.index.to_series() 128 | slope, intercept = get_line_coffs((idxs.iloc[0], rsi.iloc[0]), (idxs.iloc[-1], rsi.iloc[-1])) 129 | b_1 = rsi.iloc[1:-1] 130 | b_0 = idxs.iloc[1:-1] 131 | if above: 132 | return (slope * b_0 + intercept - self.delta_rsi <= b_1).all() 133 | return (slope * b_0 + intercept + self.delta_rsi >= b_1).all() 134 | 135 | def check_signal(self): 136 | chart = self.tfs_chart[self.tf] 137 | last_kline = chart.iloc[-1] 138 | last_zz_point = self.zz_points[-1] 139 | if last_zz_point.pidx in self.checked_pidx: 140 | return 141 | # Specify up/down trend lines using n_trend_point ZZPoint 142 | n_last_poke_points = [] 143 | n_last_peak_points = [] 144 | for zz_point in self.zz_points[-self.params["n_trend_point"] :]: 145 | if zz_point.ptype == mta.POINT_TYPE.POKE_POINT: 146 | n_last_poke_points.append((zz_point.pidx, zz_point.pline.low)) 147 | else: 148 | n_last_peak_points.append((zz_point.pidx, zz_point.pline.high)) 149 | if len(n_last_peak_points) < 2 or len(n_last_poke_points) < 2: 150 | return 151 | self.up_trend_line = find_uptrend_line(n_last_poke_points) 152 | self.down_trend_line = find_downtrend_line(n_last_peak_points) 153 | 154 | up_pct = (self.up_trend_line[1][1] - self.up_trend_line[0][1]) / self.up_trend_line[0][1] 155 | down_pct = (self.down_trend_line[1][1] - self.down_trend_line[0][1]) / self.down_trend_line[0][1] 156 | y_up = get_y_on_line(self.up_trend_line, len(chart) - 1) 157 | y_down = get_y_on_line(self.down_trend_line, len(chart) - 1) 158 | up_close_ratio = abs((last_kline["Close"] - y_up) / (y_down - y_up)) 159 | down_close_ratio = abs((last_kline["Close"] - y_down) / (y_down - y_up)) 160 | 161 | if last_zz_point.ptype == mta.POINT_TYPE.POKE_POINT: 162 | # check for higher low, lower low 163 | # if chart.iloc[last_zz_point.pidx + 1 :]["Close"].min() < last_zz_point.pline.high: 164 | # self.checked_pidx.append(last_zz_point.pidx) 165 | # return 166 | # check trend 167 | if down_pct < 0: 168 | self.checked_pidx.append(last_zz_point.pidx) 169 | return 170 | # check updown_ratio 171 | if up_close_ratio > self.params["min_updown_ratio"]: 172 | return 173 | # get n_last_point poke points lower low 174 | n_zz_points = [] 175 | i = 1 176 | while 2 * i + 1 <= len(self.zz_points): 177 | current_zz_point = self.zz_points[-(2 * i + 1)] 178 | if current_zz_point.pline.low < last_zz_point.pline.low: 179 | n_zz_points.append(current_zz_point) 180 | if len(n_zz_points) >= self.params["n_last_point"]: 181 | break 182 | i += 1 183 | 184 | valid_points_idx = self.filter_poke_points(n_zz_points, last_zz_point) 185 | for valid_point_idx in valid_points_idx: 186 | zz_point = n_zz_points[valid_point_idx] 187 | if self.rsi[zz_point.pidx] > self.rsi[last_zz_point.pidx] + self.delta_rsi: 188 | if (last_zz_point.pline.low - zz_point.pline.low) < self.delta_price_ratio * zz_point.pline.low: 189 | continue 190 | if not self.check_rsi(self.rsi.iloc[zz_point.pidx : last_zz_point.pidx + 1], above=True): 191 | continue 192 | sl = min(chart.iloc[last_zz_point.pidx :]["Low"].min(), last_zz_point.pline.low) 193 | sl = (1 - self.delta_price_ratio) * sl 194 | tp = get_y_on_line(self.down_trend_line, len(chart) + 10) 195 | order = Order( 196 | OrderType.MARKET, 197 | OrderSide.BUY, 198 | last_kline["Close"], 199 | tp=tp, 200 | sl=sl, 201 | status=OrderStatus.FILLED, 202 | ) 203 | order = self.trader.fix_order(order, self.params["sl_fix_mode"], self.max_sl_pct) 204 | if order is None: 205 | continue 206 | if ( 207 | order.rr >= self.params["min_rr"] 208 | and order.reward_ratio > self.min_reward_ratio 209 | and order.is_valid() 210 | ): 211 | if order.type == OrderType.MARKET: 212 | order["FILL_TIME"] = last_kline["Open time"] 213 | order["strategy"] = self.name 214 | order["description"] = self.description 215 | self.trader.create_trade(order, self.volume) 216 | order["desc"] = { 217 | "type": "higher_low_lower_low", 218 | "zz_point_1": zz_point, 219 | "zz_point_2": last_zz_point, 220 | "up_trend_line": self.up_trend_line, 221 | "down_trend_line": self.down_trend_line, 222 | "up_pct": up_pct, 223 | "down_pct": down_pct, 224 | "updown_close_ratio": up_close_ratio, 225 | "rsi_1": self.rsi[zz_point.pidx], 226 | "rsi_2": self.rsi[last_zz_point.pidx], 227 | } 228 | self.orders_opening.append(order) 229 | self.checked_pidx.append(last_zz_point.pidx) 230 | break 231 | 232 | if last_zz_point.ptype == mta.POINT_TYPE.PEAK_POINT: 233 | # check for lower high, higher high 234 | # if chart.iloc[last_zz_point.pidx + 1 :]["Close"].max() > last_zz_point.pline.low: 235 | # self.checked_pidx.append(last_zz_point.pidx) 236 | # return 237 | # check trend 238 | if up_pct > 0: 239 | self.checked_pidx.append(last_zz_point.pidx) 240 | return 241 | # check updown_ratio 242 | if down_close_ratio > self.params["min_updown_ratio"]: 243 | return 244 | # get n_last_point peak points higher high 245 | n_zz_points = [] 246 | i = 1 247 | while 2 * i + 1 <= len(self.zz_points): 248 | current_zz_point = self.zz_points[-(2 * i + 1)] 249 | if current_zz_point.pline.high > last_zz_point.pline.high: 250 | n_zz_points.append(current_zz_point) 251 | if len(n_zz_points) >= self.params["n_last_point"]: 252 | break 253 | i += 1 254 | 255 | valid_points_idx = self.filter_peak_points(n_zz_points, last_zz_point) 256 | for valid_point_idx in valid_points_idx: 257 | zz_point = n_zz_points[valid_point_idx] 258 | if self.rsi[zz_point.pidx] < self.rsi[last_zz_point.pidx] - self.delta_rsi: 259 | if (zz_point.pline.high - last_zz_point.pline.high) < self.delta_price_ratio * zz_point.pline.high: 260 | continue 261 | if not self.check_rsi(self.rsi.iloc[zz_point.pidx : last_zz_point.pidx + 1], above=False): 262 | continue 263 | sl = max(chart.iloc[last_zz_point.pidx :]["High"].max(), last_zz_point.pline.high) 264 | sl = (1 + self.delta_price_ratio) * sl 265 | tp = get_y_on_line(self.up_trend_line, len(chart) + 10) 266 | order = Order( 267 | OrderType.MARKET, 268 | OrderSide.SELL, 269 | last_kline["Close"], 270 | tp=tp, 271 | sl=sl, 272 | status=OrderStatus.FILLED, 273 | ) 274 | order = self.trader.fix_order(order, self.params["sl_fix_mode"], self.max_sl_pct) 275 | if order is None: 276 | continue 277 | if ( 278 | order.rr >= self.params["min_rr"] 279 | and order.reward_ratio > self.min_reward_ratio 280 | and order.is_valid() 281 | ): 282 | if order.type == OrderType.MARKET: 283 | order["FILL_TIME"] = last_kline["Open time"] 284 | order["strategy"] = self.name 285 | order["description"] = self.description 286 | self.trader.create_trade(order, self.volume) 287 | order["desc"] = { 288 | "type": "lower_high_higher_high", 289 | "zz_point_1": zz_point, 290 | "zz_point_2": last_zz_point, 291 | "up_trend_line": self.up_trend_line, 292 | "down_trend_line": self.down_trend_line, 293 | "up_pct": up_pct, 294 | "down_pct": down_pct, 295 | "updown_close_ratio": down_close_ratio, 296 | "rsi_1": self.rsi[zz_point.pidx], 297 | "rsi_2": self.rsi[last_zz_point.pidx], 298 | } 299 | self.orders_opening.append(order) 300 | self.checked_pidx.append(last_zz_point.pidx) 301 | break 302 | 303 | def plot_orders(self): 304 | fig = make_subplots(3, 1, vertical_spacing=0.02, shared_xaxes=True, row_heights=[0.6, 0.2, 0.2]) 305 | fig.update_layout( 306 | xaxis_rangeslider_visible=False, 307 | xaxis2_rangeslider_visible=False, 308 | xaxis=dict(showgrid=False), 309 | xaxis2=dict(showgrid=False), 310 | yaxis=dict(showgrid=False), 311 | yaxis2=dict(showgrid=False), 312 | plot_bgcolor="#2E4053", 313 | paper_bgcolor="#797D7F", 314 | font=dict(color="#F7F9F9"), 315 | ) 316 | df = self.tfs_chart[self.tf] 317 | dt2idx = dict(zip(df["Open time"], list(range(len(df))))) 318 | tmp_ot = df["Open time"] 319 | df["Open time"] = list(range(len(df))) 320 | super().plot_orders(fig, self.tf, 1, 1, dt2idx=dt2idx) 321 | df = self.tfs_chart[self.tf] 322 | 323 | fig.add_trace( 324 | go.Scatter( 325 | x=[df.iloc[ad.pidx]["Open time"] for ad in self.zz_points], 326 | y=[ad.pline.low if ad.ptype == mta.POINT_TYPE.POKE_POINT else ad.pline.high for ad in self.zz_points], 327 | mode="lines", 328 | line=dict(color="orange", dash="dash"), 329 | name="ZigZag Point", 330 | ), 331 | row=1, 332 | col=1, 333 | ) 334 | fig.add_trace( 335 | go.Scatter( 336 | x=df["Open time"], 337 | y=self.rsi, 338 | mode="lines", 339 | line=dict(color="orange"), 340 | name="RSI_{}".format(self.params["rsi_len"]), 341 | ), 342 | row=2, 343 | col=1, 344 | ) 345 | fig.add_shape( 346 | type="line", 347 | x0=0, 348 | y0=30, 349 | x1=1, 350 | y1=30, 351 | xref="x domain", 352 | line=dict(color="magenta", width=1, dash="dash"), 353 | row=2, 354 | col=1, 355 | ) 356 | fig.add_shape( 357 | type="line", 358 | x0=0, 359 | y0=70, 360 | x1=1, 361 | y1=70, 362 | xref="x domain", 363 | line=dict(color="magenta", width=1, dash="dash"), 364 | row=2, 365 | col=1, 366 | ) 367 | for order in self.orders_closed: 368 | desc = order["desc"] 369 | zz_point_1 = desc["zz_point_1"] 370 | zz_point_2 = desc["zz_point_2"] 371 | up_trend = desc["up_trend_line"] 372 | down_trend = desc["down_trend_line"] 373 | fig.add_trace( 374 | go.Scatter( 375 | x=[df.iloc[zz_point_1.pidx]["Open time"], df.iloc[zz_point_2.pidx]["Open time"]], 376 | y=[desc["rsi_1"], desc["rsi_2"]], 377 | mode="markers+lines", 378 | line=dict(color="orange", width=1), 379 | hoverinfo="skip", 380 | showlegend=False, 381 | ), 382 | row=2, 383 | col=1, 384 | ) 385 | fig.add_trace( 386 | go.Scatter( 387 | x=[df.iloc[zz_point_1.pidx]["Open time"], df.iloc[zz_point_2.pidx]["Open time"]], 388 | y=[zz_point_1.pline.low, zz_point_2.pline.low] 389 | if zz_point_1.ptype == mta.POINT_TYPE.POKE_POINT 390 | else [zz_point_1.pline.high, zz_point_2.pline.high], 391 | mode="markers+lines", 392 | line=dict(color="orange", width=2), 393 | hoverinfo="skip", 394 | showlegend=False, 395 | ), 396 | row=1, 397 | col=1, 398 | ) 399 | fig.add_shape( 400 | type="line", 401 | x0=df.iloc[up_trend[0][0]]["Open time"], 402 | y0=up_trend[0][1], 403 | x1=df.iloc[up_trend[1][0]]["Open time"], 404 | y1=up_trend[1][1], 405 | line=dict(color="green"), 406 | row=1, 407 | col=1, 408 | ) 409 | 410 | fig.add_shape( 411 | type="line", 412 | x0=df.iloc[down_trend[0][0]]["Open time"], 413 | y0=down_trend[0][1], 414 | x1=df.iloc[down_trend[1][0]]["Open time"], 415 | y1=down_trend[1][1], 416 | line=dict(color="red"), 417 | row=1, 418 | col=1, 419 | ) 420 | 421 | colors = ["red" if kline["Open"] > kline["Close"] else "green" for i, kline in df.iterrows()] 422 | fig.add_trace(go.Bar(x=df["Open time"], y=df["Volume"], marker_color=colors), row=3, col=1) 423 | 424 | fig.update_xaxes(showspikes=True, spikesnap="data") 425 | fig.update_yaxes(showspikes=True, spikesnap="data") 426 | fig.update_layout(hovermode="x", spikedistance=-1) 427 | fig.update_layout(hoverlabel=dict(bgcolor="white", font_size=16)) 428 | 429 | fig.update_layout( 430 | title={ 431 | "text": "RSI Hidden Divergence Strategy (symbol: {}, tf: {})".format(self.trader.symbol_name, self.tf), 432 | "x": 0.5, 433 | "xanchor": "center", 434 | } 435 | ) 436 | df["Open time"] = tmp_ot 437 | return fig 438 | -------------------------------------------------------------------------------- /strategies/break_strategy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pandas as pd 3 | from datetime import datetime 4 | import talib as ta 5 | import plotly.graph_objects as go 6 | from plotly.subplots import make_subplots 7 | from .base_strategy import BaseStrategy 8 | import indicators as mta 9 | from order import Order, OrderType, OrderSide, OrderStatus 10 | from utils import get_line_coffs, find_uptrend_line, find_downtrend_line, get_y_on_line 11 | 12 | bot_logger = logging.getLogger("bot_logger") 13 | 14 | 15 | class BreakStrategy(BaseStrategy): 16 | """ 17 | - EMA HeikinAshi strategy 18 | """ 19 | 20 | def __init__(self, name, params, tfs): 21 | super().__init__(name, params, tfs) 22 | self.tf = self.tfs["tf"] 23 | self.state = None 24 | self.trend = None 25 | self.min_zz_ratio = 0.01 * self.params["min_zz_pct"] 26 | self.temp = [] 27 | 28 | def attach(self, tfs_chart): 29 | self.tfs_chart = tfs_chart 30 | self.init_indicators() 31 | 32 | def init_indicators(self): 33 | # calculate HA candelstick 34 | chart = self.tfs_chart[self.tf] 35 | self.ma_vol = ta.SMA(chart["Volume"], self.params["ma_vol"]) 36 | self.zz_points = mta.zigzag(chart, self.min_zz_ratio) 37 | self.init_main_zigzag() 38 | self.start_trading_time = chart.iloc[-1]["Open time"] 39 | 40 | def init_main_zigzag(self): 41 | self.main_zz_idx = [] 42 | if len(self.zz_points) > 0: 43 | self.main_zz_idx.append(0) 44 | else: 45 | return 46 | last_main_zz_idx = 0 47 | while last_main_zz_idx + 3 < len(self.zz_points): 48 | last_main_zz_type = self.zz_points[last_main_zz_idx].ptype 49 | if last_main_zz_type == mta.POINT_TYPE.PEAK_POINT: 50 | if ( 51 | self.zz_points[last_main_zz_idx + 2].pline.high < self.zz_points[last_main_zz_idx].pline.high 52 | and self.zz_points[last_main_zz_idx + 3].pline.low < self.zz_points[last_main_zz_idx + 1].pline.low 53 | ): 54 | last_main_zz_idx += 2 55 | else: 56 | last_main_zz_idx += 1 57 | self.main_zz_idx.append(last_main_zz_idx) 58 | else: 59 | if ( 60 | self.zz_points[last_main_zz_idx + 2].pline.low > self.zz_points[last_main_zz_idx].pline.low 61 | and self.zz_points[last_main_zz_idx + 3].pline.high > self.zz_points[last_main_zz_idx + 1].pline.low 62 | ): 63 | last_main_zz_idx += 2 64 | else: 65 | last_main_zz_idx += 1 66 | self.main_zz_idx.append(last_main_zz_idx) 67 | 68 | def update_main_zigzag(self): 69 | last_main_zz_idx = self.main_zz_idx[-1] 70 | while last_main_zz_idx + 3 < len(self.zz_points): 71 | last_main_zz_type = self.zz_points[last_main_zz_idx].ptype 72 | if last_main_zz_type == mta.POINT_TYPE.PEAK_POINT: 73 | if ( 74 | self.zz_points[last_main_zz_idx + 2].pline.high < self.zz_points[last_main_zz_idx].pline.high 75 | and self.zz_points[last_main_zz_idx + 3].pline.low < self.zz_points[last_main_zz_idx + 1].pline.low 76 | ): 77 | last_main_zz_idx += 2 78 | else: 79 | last_main_zz_idx += 1 80 | self.main_zz_idx.append(last_main_zz_idx) 81 | else: 82 | if ( 83 | self.zz_points[last_main_zz_idx + 2].pline.low > self.zz_points[last_main_zz_idx].pline.low 84 | and self.zz_points[last_main_zz_idx + 3].pline.high 85 | > self.zz_points[last_main_zz_idx + 1].pline.high 86 | ): 87 | last_main_zz_idx += 2 88 | else: 89 | last_main_zz_idx += 1 90 | self.main_zz_idx.append(last_main_zz_idx) 91 | 92 | last_main_zz_type = self.zz_points[last_main_zz_idx].ptype 93 | if last_main_zz_idx + 2 < len(self.zz_points): 94 | if last_main_zz_type == mta.POINT_TYPE.POKE_POINT: 95 | if self.zz_points[last_main_zz_idx + 2].pline.low < self.zz_points[last_main_zz_idx].pline.low: 96 | last_main_zz_idx += 1 97 | self.main_zz_idx.append(last_main_zz_idx) 98 | else: 99 | if self.zz_points[last_main_zz_idx + 2].pline.high > self.zz_points[last_main_zz_idx].pline.high: 100 | last_main_zz_idx += 1 101 | self.main_zz_idx.append(last_main_zz_idx) 102 | 103 | last_main_zz_type = self.zz_points[last_main_zz_idx].ptype 104 | if last_main_zz_idx + 1 < len(self.zz_points): 105 | last_kline = self.tfs_chart[self.tf].iloc[-1] 106 | if last_main_zz_type == mta.POINT_TYPE.POKE_POINT: 107 | if last_kline["Low"] < self.zz_points[last_main_zz_idx].pline.low: 108 | last_main_zz_idx += 1 109 | self.main_zz_idx.append(last_main_zz_idx) 110 | else: 111 | if last_kline["High"] > self.zz_points[last_main_zz_idx].pline.high: 112 | last_main_zz_idx += 1 113 | self.main_zz_idx.append(last_main_zz_idx) 114 | 115 | def update_indicators(self, tf): 116 | if tf != self.tf: 117 | return 118 | chart = self.tfs_chart[self.tf] 119 | self.ma_vol.loc[len(self.ma_vol)] = ta.stream.SMA(chart["Volume"], self.params["ma_vol"]) 120 | last_Zz_pidx = self.zz_points[-1].pidx 121 | mta.zigzag_stream(chart, self.min_zz_ratio, self.zz_points) 122 | last_main_idx = self.main_zz_idx[-1] 123 | self.update_main_zigzag() 124 | if last_main_idx != self.main_zz_idx[-1]: 125 | self.temp.append((self.zz_points[self.main_zz_idx[-1]].pidx, len(chart) - 1)) 126 | self.adjust_sl() 127 | 128 | def check_required_params(self): 129 | return all( 130 | [ 131 | key in self.params.keys() 132 | for key in [ 133 | "ma_vol", 134 | "vol_ratio_ma", 135 | "kline_body_ratio", 136 | "sl_fix_mode", 137 | ] 138 | ] 139 | ) 140 | 141 | def is_params_valid(self): 142 | if not self.check_required_params(): 143 | bot_logger.info(" [-] Missing required params") 144 | return False 145 | return True 146 | 147 | def update(self, tf): 148 | # update when new kline arrive 149 | super().update(tf) 150 | # check order signal 151 | self.check_close_signal() 152 | self.check_signal() 153 | 154 | def close_opening_orders(self): 155 | super().close_opening_orders(self.tfs_chart[self.tf].iloc[-1]) 156 | 157 | def check_signal(self): 158 | chart = self.tfs_chart[self.tf] 159 | last_kline = chart.iloc[-1] 160 | if last_kline["Volume"] < self.params["vol_ratio_ma"] * self.ma_vol.iloc[-1]: 161 | return 162 | idx = 1 163 | while idx < len(self.zz_points): 164 | zz_point_1 = self.zz_points[-idx] 165 | zz_point_2 = self.zz_points[-idx - 1] 166 | if zz_point_2.ptype == mta.POINT_TYPE.POKE_POINT: 167 | change = (zz_point_1.pline.high - zz_point_2.pline.low) / zz_point_2.pline.low 168 | else: 169 | change = (zz_point_2.pline.high - zz_point_1.pline.low) / zz_point_2.pline.high 170 | if change > self.params["zz_dev"] * self.min_zz_ratio: 171 | break 172 | idx += 1 173 | 174 | n_df = chart[self.zz_points[-idx].pidx : -1] 175 | if len(n_df) < self.params["min_num_cuml"]: 176 | return 177 | n_last_poke_points = [] 178 | n_last_peak_points = [] 179 | for i, kline in n_df.iterrows(): 180 | n_last_poke_points.append((i, kline["Low"])) 181 | n_last_peak_points.append((i, kline["High"])) 182 | kline_body_pct = n_df[["Open", "Close"]].max(axis=1) - n_df[["Open", "Close"]].min(axis=1) 183 | mean_kline_body = kline_body_pct.mean() 184 | if abs(last_kline["Close"] - last_kline["Open"]) < self.params["kline_body_ratio"] * mean_kline_body: 185 | return 186 | self.up_trend_line = find_uptrend_line(n_last_poke_points) 187 | self.down_trend_line = find_downtrend_line(n_last_peak_points) 188 | 189 | self.up_pct = (self.up_trend_line[1][1] - self.up_trend_line[0][1]) / self.up_trend_line[0][1] 190 | self.down_pct = (self.down_trend_line[1][1] - self.down_trend_line[0][1]) / self.down_trend_line[0][1] 191 | delta_end = abs(self.down_trend_line[1][1] - self.up_trend_line[1][1]) / self.up_trend_line[1][1] 192 | if delta_end > self.params["zz_dev"] * self.min_zz_ratio: 193 | return 194 | if last_kline["Close"] > last_kline["Open"]: 195 | # green kkline 196 | if (last_kline["High"] - last_kline["Close"]) > 0.5 * (last_kline["High"] - last_kline["Low"]): 197 | return 198 | y_down_pct = get_y_on_line(self.down_trend_line, self.down_trend_line[1][0] + 1) 199 | if last_kline["Close"] > y_down_pct and last_kline["Close"] > ta.stream.SMA(chart["Close"], 200): 200 | sl = self.up_trend_line[1][1] 201 | order = Order( 202 | OrderType.MARKET, 203 | OrderSide.BUY, 204 | last_kline["Close"], 205 | tp=None, 206 | sl=sl, 207 | status=OrderStatus.FILLED, 208 | ) 209 | order["FILL_TIME"] = last_kline["Open time"] 210 | order["strategy"] = self.name 211 | order["description"] = self.description 212 | order["desc"] = { 213 | "up_trend_line": self.up_trend_line, 214 | "down_trend_line": self.down_trend_line, 215 | } 216 | order = self.trader.fix_order(order, self.params["sl_fix_mode"], self.max_sl_pct) 217 | if order: 218 | self.trader.create_trade(order, self.volume) 219 | self.orders_opening.append(order) 220 | else: 221 | # red kkline 222 | if (last_kline["Close"] - last_kline["Low"]) > 0.5 * (last_kline["High"] - last_kline["Low"]): 223 | return 224 | y_up_pct = get_y_on_line(self.up_trend_line, self.up_trend_line[1][0] + 1) 225 | if last_kline["Close"] < y_up_pct and last_kline["Close"] < ta.stream.SMA(chart["Close"], 200): 226 | sl = self.down_trend_line[1][1] 227 | order = Order( 228 | OrderType.MARKET, 229 | OrderSide.SELL, 230 | last_kline["Close"], 231 | tp=None, 232 | sl=sl, 233 | status=OrderStatus.FILLED, 234 | ) 235 | order["FILL_TIME"] = last_kline["Open time"] 236 | order["strategy"] = self.name 237 | order["description"] = self.description 238 | order["desc"] = { 239 | "up_trend_line": self.up_trend_line, 240 | "down_trend_line": self.down_trend_line, 241 | } 242 | order = self.trader.fix_order(order, self.params["sl_fix_mode"], self.max_sl_pct) 243 | if order: 244 | self.trader.create_trade(order, self.volume) 245 | self.orders_opening.append(order) 246 | 247 | def check_close_signal(self): 248 | self.check_close_reverse() 249 | chart = self.tfs_chart[self.tf] 250 | last_kline = chart.iloc[-1] 251 | if last_kline["Volume"] < self.params["vol_ratio_ma"] * self.ma_vol.iloc[-1]: 252 | return 253 | if last_kline["Close"] < last_kline["Open"]: 254 | # red kline, check close buy orders 255 | if (last_kline["High"] - last_kline["Close"]) < 0.75 * (last_kline["High"] - last_kline["Low"]): 256 | return 257 | for i in range(len(self.orders_opening) - 1, -1, -1): 258 | order = self.orders_opening[i] 259 | if order.side == OrderSide.SELL: 260 | continue 261 | desc = order["desc"] 262 | up_trend_line = desc["up_trend_line"] 263 | down_trend_line = desc["down_trend_line"] 264 | y_up = get_y_on_line(up_trend_line, len(chart) - 1) 265 | if last_kline["Close"] < y_up: 266 | order["desc"]["stop_idx"] = len(chart) - 1 267 | order["desc"]["y"] = y_up 268 | order.close(last_kline) 269 | self.trader.close_trade(order) 270 | if order.is_closed(): 271 | self.orders_closed.append(order) 272 | del self.orders_opening[i] 273 | else: 274 | # green kline, check close sell orders 275 | if (last_kline["Close"] - last_kline["Low"]) < 0.75 * (last_kline["High"] - last_kline["Low"]): 276 | return 277 | for i in range(len(self.orders_opening) - 1, -1, -1): 278 | order = self.orders_opening[i] 279 | if order.side == OrderSide.BUY: 280 | continue 281 | desc = order["desc"] 282 | up_trend_line = desc["up_trend_line"] 283 | down_trend_line = desc["down_trend_line"] 284 | y_down = get_y_on_line(down_trend_line, len(chart) - 1) 285 | if last_kline["Close"] > y_down: 286 | order["desc"]["stop_idx"] = len(chart) - 1 287 | order["desc"]["y"] = y_down 288 | order.close(last_kline) 289 | self.trader.close_trade(order) 290 | if order.is_closed(): 291 | self.orders_closed.append(order) 292 | del self.orders_opening[i] 293 | 294 | def check_close_reverse(self): 295 | # check close reverse order (buy order has uptrend downward) 296 | if len(self.main_zz_idx) < 3: 297 | return 298 | chart = self.tfs_chart[self.tf] 299 | last_kline = chart.iloc[-1] 300 | if self.zz_points[self.main_zz_idx[-1]].ptype == mta.POINT_TYPE.PEAK_POINT: 301 | if self.zz_points[self.main_zz_idx[-1]].pline.high < self.zz_points[self.main_zz_idx[-3]].pline.high: 302 | for i in range(len(self.orders_opening) - 1, -1, -1): 303 | order = self.orders_opening[i] 304 | if order.side == OrderSide.SELL: 305 | continue 306 | desc = order["desc"] 307 | up_trend_line = desc["up_trend_line"] 308 | if up_trend_line[0][1] * 1.005 >= up_trend_line[1][1]: 309 | order["desc"]["stop_idx"] = len(chart) - 1 310 | order["desc"]["y"] = last_kline["Close"] 311 | order.close(last_kline) 312 | self.trader.close_trade(order) 313 | if order.is_closed(): 314 | self.orders_closed.append(order) 315 | del self.orders_opening[i] 316 | else: 317 | if self.zz_points[self.main_zz_idx[-1]].pline.low > self.zz_points[self.main_zz_idx[-3]].pline.low: 318 | for i in range(len(self.orders_opening) - 1, -1, -1): 319 | order = self.orders_opening[i] 320 | if order.side == OrderSide.BUY: 321 | continue 322 | desc = order["desc"] 323 | down_trend_line = desc["down_trend_line"] 324 | if down_trend_line[0][1] <= down_trend_line[1][1] * 1.005: 325 | order["desc"]["stop_idx"] = len(chart) - 1 326 | order["desc"]["y"] = last_kline["Close"] 327 | order.close(last_kline) 328 | self.trader.close_trade(order) 329 | if order.is_closed(): 330 | self.orders_closed.append(order) 331 | del self.orders_opening[i] 332 | 333 | def adjust_sl(self): 334 | last_main_zz = self.zz_points[self.main_zz_idx[-1]] 335 | if last_main_zz.ptype == mta.POINT_TYPE.PEAK_POINT: 336 | for i in range(len(self.orders_opening) - 1, -1, -1): 337 | order = self.orders_opening[i] 338 | if order.side == OrderSide.BUY: 339 | continue 340 | if not order.has_sl() or order.sl > last_main_zz.pline.high: 341 | order.adjust_sl(last_main_zz.pline.high) 342 | self.trader.adjust_sl(order, last_main_zz.pline.high) 343 | else: 344 | for i in range(len(self.orders_opening) - 1, -1, -1): 345 | order = self.orders_opening[i] 346 | if order.side == OrderSide.SELL: 347 | continue 348 | if not order.has_sl() or order.sl < last_main_zz.pline.low: 349 | order.adjust_sl(last_main_zz.pline.low) 350 | self.trader.adjust_sl(order, last_main_zz.pline.low) 351 | 352 | def plot_orders(self): 353 | fig = make_subplots(2, 1, vertical_spacing=0.02, shared_xaxes=True, row_heights=[0.8, 0.2]) 354 | fig.update_layout( 355 | xaxis_rangeslider_visible=False, 356 | xaxis2_rangeslider_visible=False, 357 | yaxis=dict(showgrid=False), 358 | yaxis2=dict(showgrid=False), 359 | xaxis=dict(showgrid=False), 360 | xaxis2=dict(showgrid=False), 361 | plot_bgcolor="rgb(19, 23, 34)", 362 | paper_bgcolor="rgb(121,125,127)", 363 | font=dict(color="rgb(247,249,249)"), 364 | ) 365 | df = self.tfs_chart[self.tf] 366 | dt2idx = dict(zip(df["Open time"], list(range(len(df))))) 367 | tmp_ot = df["Open time"] 368 | df["Open time"] = list(range(len(df))) 369 | super().plot_orders(fig, self.tf, 1, 1, dt2idx=dt2idx) 370 | 371 | fig.add_trace( 372 | go.Scatter( 373 | x=[df.iloc[ad.pidx]["Open time"] for ad in self.zz_points], 374 | y=[ad.pline.low if ad.ptype == mta.POINT_TYPE.POKE_POINT else ad.pline.high for ad in self.zz_points], 375 | mode="lines", 376 | line=dict(dash="dash"), 377 | marker_color=[ 378 | "rgba(255, 255, 0, 1)" if ad.ptype == mta.POINT_TYPE.POKE_POINT else "rgba(0, 255, 0, 1)" 379 | for ad in self.zz_points 380 | ], 381 | name="ZigZag Point", 382 | hoverinfo="skip", 383 | ), 384 | row=1, 385 | col=1, 386 | ) 387 | 388 | fig.add_trace( 389 | go.Scatter( 390 | x=df["Open time"], 391 | y=ta.SMA(df["Close"], 200), 392 | mode="lines", 393 | line=dict(color="orange"), 394 | name="SMA_200", 395 | hoverinfo="skip", 396 | ), 397 | row=1, 398 | col=1, 399 | ) 400 | 401 | main_zz_points = [self.zz_points[i] for i in self.main_zz_idx] 402 | fig.add_trace( 403 | go.Scatter( 404 | x=[df.iloc[ad.pidx]["Open time"] for ad in main_zz_points], 405 | y=[ad.pline.low if ad.ptype == mta.POINT_TYPE.POKE_POINT else ad.pline.high for ad in main_zz_points], 406 | mode="lines", 407 | name="Main ZigZag Point", 408 | hoverinfo="skip", 409 | ), 410 | row=1, 411 | col=1, 412 | ) 413 | for x, y in self.temp: 414 | fig.add_shape( 415 | type="line", 416 | x0=df.iloc[x]["Open time"], 417 | y0=df.iloc[x]["High"], 418 | x1=df.iloc[y]["Open time"], 419 | y1=df.iloc[x]["High"], 420 | line=dict(color="red"), 421 | row=1, 422 | col=1, 423 | ) 424 | 425 | for order in self.orders_closed: 426 | desc = order["desc"] 427 | up_trend = desc["up_trend_line"] 428 | down_trend = desc["down_trend_line"] 429 | fig.add_trace( 430 | go.Scatter( 431 | x=[ 432 | df.iloc[up_trend[0][0]]["Open time"], 433 | df.iloc[up_trend[1][0]]["Open time"], 434 | df.iloc[down_trend[1][0]]["Open time"], 435 | df.iloc[down_trend[0][0]]["Open time"], 436 | df.iloc[up_trend[0][0]]["Open time"], 437 | ], 438 | y=[up_trend[0][1], up_trend[1][1], down_trend[1][1], down_trend[0][1], up_trend[0][1]], 439 | mode="lines", 440 | line=dict(color="rgba(156, 39, 176, 0.8)", width=2), 441 | fill="toself", 442 | fillcolor="rgba(128, 0, 128, 0.2)", 443 | hoverinfo="skip", 444 | showlegend=False, 445 | ) 446 | ) 447 | 448 | colors = [ 449 | "rgb(242, 54, 69)" if kline["Open"] > kline["Close"] else "rgb(8, 153, 129)" for i, kline in df.iterrows() 450 | ] 451 | fig.add_trace( 452 | go.Bar(x=list(range(len(df))), y=df["Volume"], marker_color=colors, marker_line_width=0, name="Volume"), 453 | row=2, 454 | col=1, 455 | ) 456 | fig.add_trace( 457 | go.Scatter( 458 | x=df["Open time"], 459 | y=self.ma_vol, 460 | mode="lines", 461 | line=dict(color="rgba(0, 255, 0, 1)"), 462 | name="MA_VOL_{}".format(self.params["ma_vol"]), 463 | ), 464 | row=2, 465 | col=1, 466 | ) 467 | fig.update_xaxes(showspikes=True, spikesnap="data") 468 | fig.update_yaxes(showspikes=True, spikesnap="data") 469 | fig.update_layout(hovermode="x", spikedistance=-1) 470 | fig.update_layout(hoverlabel=dict(bgcolor="white", font_size=16)) 471 | fig.update_layout( 472 | title={ 473 | "text": "Breakout Strategy({}) (tf {})".format(self.trader.symbol_name, self.tf), 474 | "x": 0.5, 475 | "xanchor": "center", 476 | } 477 | ) 478 | df["Open time"] = tmp_ot 479 | return fig 480 | --------------------------------------------------------------------------------