bbstrader 2.0.3__cp312-cp312-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bbstrader/__init__.py +27 -0
- bbstrader/__main__.py +92 -0
- bbstrader/api/__init__.py +96 -0
- bbstrader/api/handlers.py +245 -0
- bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
- bbstrader/api/metatrader_client.pyi +624 -0
- bbstrader/assets/bbs_.png +0 -0
- bbstrader/assets/bbstrader.ico +0 -0
- bbstrader/assets/bbstrader.png +0 -0
- bbstrader/assets/qs_metrics_1.png +0 -0
- bbstrader/btengine/__init__.py +54 -0
- bbstrader/btengine/backtest.py +358 -0
- bbstrader/btengine/data.py +737 -0
- bbstrader/btengine/event.py +229 -0
- bbstrader/btengine/execution.py +287 -0
- bbstrader/btengine/performance.py +408 -0
- bbstrader/btengine/portfolio.py +393 -0
- bbstrader/btengine/strategy.py +588 -0
- bbstrader/compat.py +28 -0
- bbstrader/config.py +100 -0
- bbstrader/core/__init__.py +27 -0
- bbstrader/core/data.py +628 -0
- bbstrader/core/strategy.py +466 -0
- bbstrader/metatrader/__init__.py +48 -0
- bbstrader/metatrader/_copier.py +720 -0
- bbstrader/metatrader/account.py +865 -0
- bbstrader/metatrader/broker.py +418 -0
- bbstrader/metatrader/copier.py +1487 -0
- bbstrader/metatrader/rates.py +495 -0
- bbstrader/metatrader/risk.py +667 -0
- bbstrader/metatrader/trade.py +1692 -0
- bbstrader/metatrader/utils.py +402 -0
- bbstrader/models/__init__.py +39 -0
- bbstrader/models/nlp.py +932 -0
- bbstrader/models/optimization.py +182 -0
- bbstrader/scripts.py +665 -0
- bbstrader/trading/__init__.py +33 -0
- bbstrader/trading/execution.py +1159 -0
- bbstrader/trading/strategy.py +362 -0
- bbstrader/trading/utils.py +69 -0
- bbstrader-2.0.3.dist-info/METADATA +396 -0
- bbstrader-2.0.3.dist-info/RECORD +45 -0
- bbstrader-2.0.3.dist-info/WHEEL +5 -0
- bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
- bbstrader-2.0.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1159 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import functools
|
|
3
|
+
import multiprocessing as mp
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from datetime import date, datetime
|
|
7
|
+
from multiprocessing.synchronize import Event
|
|
8
|
+
from typing import Callable, Dict, List, Literal, Optional
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from loguru import logger as log
|
|
12
|
+
|
|
13
|
+
from bbstrader.config import BBSTRADER_DIR
|
|
14
|
+
from bbstrader.core.strategy import Strategy, TradeAction
|
|
15
|
+
from bbstrader.metatrader.account import check_mt5_connection
|
|
16
|
+
from bbstrader.metatrader.trade import Trade
|
|
17
|
+
from bbstrader.trading.strategy import LiveStrategy
|
|
18
|
+
from bbstrader.trading.utils import send_message
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import MetaTrader5 as MT5
|
|
22
|
+
except ImportError:
|
|
23
|
+
import bbstrader.compat # noqa: F401
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["Mt5ExecutionEngine", "RunMt5Engine", "RunMt5Engines"]
|
|
27
|
+
|
|
28
|
+
_TF_MAPPING = {
|
|
29
|
+
"1m": 1,
|
|
30
|
+
"3m": 3,
|
|
31
|
+
"5m": 5,
|
|
32
|
+
"10m": 10,
|
|
33
|
+
"15m": 15,
|
|
34
|
+
"30m": 30,
|
|
35
|
+
"1h": 60,
|
|
36
|
+
"2h": 120,
|
|
37
|
+
"4h": 240,
|
|
38
|
+
"6h": 360,
|
|
39
|
+
"8h": 480,
|
|
40
|
+
"12h": 720,
|
|
41
|
+
"D1": 1440,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
MT5_ENGINE_TIMEFRAMES = list(_TF_MAPPING.keys())
|
|
45
|
+
|
|
46
|
+
TradingDays = ["monday", "tuesday", "wednesday", "thursday", "friday"]
|
|
47
|
+
WEEK_DAYS = TradingDays + ["saturday", "sunday"]
|
|
48
|
+
FRIDAY = "friday"
|
|
49
|
+
WEEK_ENDS = ["friday", "saturday", "sunday"]
|
|
50
|
+
|
|
51
|
+
BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
|
|
52
|
+
SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
|
|
53
|
+
|
|
54
|
+
ORDERS_TYPES = [
|
|
55
|
+
"orders",
|
|
56
|
+
"buy_stops",
|
|
57
|
+
"sell_stops",
|
|
58
|
+
"buy_limits",
|
|
59
|
+
"sell_limits",
|
|
60
|
+
"buy_stop_limits",
|
|
61
|
+
"sell_stop_limits",
|
|
62
|
+
]
|
|
63
|
+
POSITIONS_TYPES = ["positions", "buys", "sells", "profitables", "losings"]
|
|
64
|
+
|
|
65
|
+
ACTIONS = ["buys", "sells"]
|
|
66
|
+
STOPS = ["buy_stops", "sell_stops"]
|
|
67
|
+
LIMITS = ["buy_limits", "sell_limits"]
|
|
68
|
+
STOP_LIMITS = ["buy_stop_limits", "sell_stop_limits"]
|
|
69
|
+
|
|
70
|
+
EXIT_SIGNAL_ACTIONS = {
|
|
71
|
+
"EXIT": {a: a[:-1] for a in ACTIONS},
|
|
72
|
+
"EXIT_LONG": {"buys": "buy"},
|
|
73
|
+
"EXIT_SHORT": {"sells": "sell"},
|
|
74
|
+
"EXIT_STOP": {stop: stop for stop in STOPS},
|
|
75
|
+
"EXIT_LONG_STOP": {"buy_stops": "buy_stops"},
|
|
76
|
+
"EXIT_SHORT_STOP": {"sell_stops": "sell_stops"},
|
|
77
|
+
"EXIT_LIMIT": {limit: limit for limit in LIMITS},
|
|
78
|
+
"EXIT_LONG_LIMIT": {"buy_limits": "buy_limits"},
|
|
79
|
+
"EXIT_SHORT_LIMIT": {"sell_limits": "sell_limits"},
|
|
80
|
+
"EXIT_STOP_LIMIT": {sl: sl for sl in STOP_LIMITS},
|
|
81
|
+
"EXIT_LONG_STOP_LIMIT": {STOP_LIMITS[0]: STOP_LIMITS[0]},
|
|
82
|
+
"EXIT_SHORT_STOP_LIMIT": {STOP_LIMITS[1]: STOP_LIMITS[1]},
|
|
83
|
+
"EXIT_PROFITABLES": {"profitables": "profitable"},
|
|
84
|
+
"EXIT_LOSINGS": {"losings": "losing"},
|
|
85
|
+
"EXIT_ALL_POSITIONS": {"positions": "all"},
|
|
86
|
+
"EXIT_ALL_ORDERS": {"orders": "all"},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
COMMON_RETCODES = [MT5.TRADE_RETCODE_MARKET_CLOSED, MT5.TRADE_RETCODE_CLOSE_ONLY]
|
|
90
|
+
|
|
91
|
+
NON_EXEC_RETCODES = {
|
|
92
|
+
"BMKT": [MT5.TRADE_RETCODE_SHORT_ONLY] + COMMON_RETCODES,
|
|
93
|
+
"SMKT": [MT5.TRADE_RETCODE_LONG_ONLY] + COMMON_RETCODES,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
log.add(
|
|
97
|
+
f"{BBSTRADER_DIR}/logs/execution.log",
|
|
98
|
+
enqueue=True,
|
|
99
|
+
level="DEBUG",
|
|
100
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Mt5ExecutionEngine:
|
|
105
|
+
"""
|
|
106
|
+
The `Mt5ExecutionEngine` class serves as the central hub for executing your trading strategies within the `bbstrader` framework.
|
|
107
|
+
It orchestrates the entire trading process, ensuring seamless interaction between your strategies, market data, and your chosen
|
|
108
|
+
trading platform.
|
|
109
|
+
|
|
110
|
+
Key Features
|
|
111
|
+
------------
|
|
112
|
+
|
|
113
|
+
- **Strategy Execution:** The `Mt5ExecutionEngine` is responsible for running your strategy, retrieving signals, and executing trades based on those signals.
|
|
114
|
+
- **Time Management:** You can define a specific time frame for your trades and set the frequency with which the engine checks for signals and manages trades.
|
|
115
|
+
- **Trade Period Control:** Define whether your strategy runs for a day, a week, or a month, allowing for flexible trading durations.
|
|
116
|
+
- **Money Management:** The engine supports optional money management features, allowing you to control risk and optimize your trading performance.
|
|
117
|
+
- **Trading Day Configuration:** You can customize the days of the week your strategy will execute, providing granular control over your trading schedule.
|
|
118
|
+
- **Platform Integration:** The `Mt5ExecutionEngine` is currently designed to work with MT5.
|
|
119
|
+
|
|
120
|
+
Examples
|
|
121
|
+
--------
|
|
122
|
+
|
|
123
|
+
>>> from bbstrader.metatrader import create_trade_instance
|
|
124
|
+
>>> from bbstrader.trading.execution import Mt5ExecutionEngine
|
|
125
|
+
>>> from examples.strategies import StockIndexSTBOTrading
|
|
126
|
+
>>> from bbstrader.config import config_logger
|
|
127
|
+
>>>
|
|
128
|
+
>>> if __name__ == '__main__':
|
|
129
|
+
>>> logger = config_logger(index_trade.log, console_log=True)
|
|
130
|
+
>>> # Define symbols
|
|
131
|
+
>>> ndx = '[NQ100]'
|
|
132
|
+
>>> spx = '[SP500]'
|
|
133
|
+
>>> dji = '[DJI30]'
|
|
134
|
+
>>> dax = 'GERMANY40'
|
|
135
|
+
>>>
|
|
136
|
+
>>> symbol_list = [spx, dax, dji, ndx]
|
|
137
|
+
>>>
|
|
138
|
+
>>> trade_kwargs = {
|
|
139
|
+
... 'expert_id': 5134,
|
|
140
|
+
... 'version': 2.0,
|
|
141
|
+
... 'time_frame': '15m',
|
|
142
|
+
... 'var_level': 0.99,
|
|
143
|
+
... 'start_time': '8:30',
|
|
144
|
+
... 'finishing_time': '19:30',
|
|
145
|
+
... 'ending_time': '21:30',
|
|
146
|
+
... 'max_risk': 5.0,
|
|
147
|
+
... 'daily_risk': 0.10,
|
|
148
|
+
... 'pchange_sl': 1.5,
|
|
149
|
+
... 'rr': 3.0,
|
|
150
|
+
... 'logger': logger
|
|
151
|
+
... }
|
|
152
|
+
>>> strategy_kwargs = {
|
|
153
|
+
... 'max_trades': {ndx: 3, spx: 3, dji: 3, dax: 3},
|
|
154
|
+
... 'expected_returns': {ndx: 1.5, spx: 1.5, dji: 1.0, dax: 1.0},
|
|
155
|
+
... 'strategy_name': 'SISTBO',
|
|
156
|
+
... 'logger': logger,
|
|
157
|
+
... 'expert_id': 5134
|
|
158
|
+
... }
|
|
159
|
+
>>> trades_instances = create_trade_instance(
|
|
160
|
+
... symbol_list, trade_kwargs,
|
|
161
|
+
... logger=logger,
|
|
162
|
+
... )
|
|
163
|
+
>>>
|
|
164
|
+
>>> engine = Mt5ExecutionEngine(
|
|
165
|
+
... symbol_list,
|
|
166
|
+
... trades_instances,
|
|
167
|
+
... StockIndexCFDTrading,
|
|
168
|
+
... time_frame='15m',
|
|
169
|
+
... iter_time=5,
|
|
170
|
+
... mm=True,
|
|
171
|
+
... period='week',
|
|
172
|
+
... comment='bbs_SISTBO_@2.0',
|
|
173
|
+
... **strategy_kwargs
|
|
174
|
+
... )
|
|
175
|
+
>>> engine.run()
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
symbol_list: List[str],
|
|
181
|
+
trades_instances: Dict[str, Trade],
|
|
182
|
+
strategy_cls: Strategy | LiveStrategy,
|
|
183
|
+
/,
|
|
184
|
+
mm: bool = True,
|
|
185
|
+
auto_trade: bool = True,
|
|
186
|
+
prompt_callback: Callable = None,
|
|
187
|
+
multithread: bool = False,
|
|
188
|
+
shutdown_event: Event = None,
|
|
189
|
+
optimizer: str = "equal",
|
|
190
|
+
trail: bool = True,
|
|
191
|
+
stop_trail: Optional[int] = None,
|
|
192
|
+
trail_after_points: int | str = None,
|
|
193
|
+
be_plus_points: Optional[int] = None,
|
|
194
|
+
show_positions_orders: bool = False,
|
|
195
|
+
iter_time: int | float = 5,
|
|
196
|
+
use_trade_time: bool = True,
|
|
197
|
+
period: Literal["24/7", "day", "week", "month"] = "month",
|
|
198
|
+
period_end_action: Literal["break", "sleep"] = "sleep",
|
|
199
|
+
closing_pnl: Optional[float] = None,
|
|
200
|
+
trading_days: Optional[List[str]] = None,
|
|
201
|
+
comment: Optional[str] = None,
|
|
202
|
+
**kwargs,
|
|
203
|
+
):
|
|
204
|
+
"""
|
|
205
|
+
Args:
|
|
206
|
+
symbol_list : List of symbols to trade
|
|
207
|
+
trades_instances : Dictionary of Trade instances
|
|
208
|
+
strategy_cls : Strategy class to use for trading
|
|
209
|
+
mm : Enable Money Management. Defaults to True.
|
|
210
|
+
optimizer : Risk management optimizer. Defaults to 'equal'.
|
|
211
|
+
See `bbstrader.models.optimization` module for more information.
|
|
212
|
+
auto_trade : If set to true, when signal are generated by the strategy class,
|
|
213
|
+
the Execution engine will automaticaly open position in other whise it will prompt
|
|
214
|
+
the user for confimation.
|
|
215
|
+
prompt_callback : Callback function to prompt the user for confirmation.
|
|
216
|
+
This is useful when integrating with GUI applications.
|
|
217
|
+
multithread : If True, use a thread pool to process signals in parallel.
|
|
218
|
+
If False, process them sequentially. Set this to True only if the engine
|
|
219
|
+
is running in a separate process. Default to False.
|
|
220
|
+
shutdown_event : Use to terminate the copy process when runs in a custum environment like web App or GUI.
|
|
221
|
+
show_positions_orders : Print open positions and orders. Defaults to False.
|
|
222
|
+
iter_time : Interval to check for signals and `mm`. Defaults to 5.
|
|
223
|
+
use_trade_time : Open trades after the time is completed. Defaults to True.
|
|
224
|
+
period : Period to trade ("24/7", "day", "week", "month"). Defaults to 'week'.
|
|
225
|
+
period_end_action : Action to take at the end of the period ("break", "sleep"). Defaults to 'break',
|
|
226
|
+
this only applies when period is 'day', 'week'.
|
|
227
|
+
closing_pnl : Minimum profit in percentage of target profit to close positions. Defaults to -0.001.
|
|
228
|
+
trading_days : Trading days in a week. Defaults to monday to friday.
|
|
229
|
+
comment: Comment for trades. Defaults to None.
|
|
230
|
+
**kwargs: Additional keyword arguments
|
|
231
|
+
_ time_frame : Time frame to trade. Defaults to '15m'.
|
|
232
|
+
- strategy_name (Optional[str]): Strategy name. Defaults to None.
|
|
233
|
+
- max_trades (Dict[str, int]): Maximum trades per symbol. Defaults to None.
|
|
234
|
+
- notify (bool): Enable notifications. Defaults to False.
|
|
235
|
+
- telegram (bool): Enable telegram notifications. Defaults to False.
|
|
236
|
+
- bot_token (str): Telegram bot token. Defaults to None.
|
|
237
|
+
- chat_id (Union[int, str, List] ): Telegram chat id. Defaults to None.
|
|
238
|
+
- MT5 connection arguments.
|
|
239
|
+
|
|
240
|
+
Note:
|
|
241
|
+
1. For `trail` , `stop_trail` , `trail_after_points` , `be_plus_points` see `bbstrader.metatrader.trade.Trade.break_even()` .
|
|
242
|
+
2. All Strategies must inherit from `bbstrader.btengine.strategy.MT5Strategy` class
|
|
243
|
+
and have a `calculate_signals` method that returns a List of ``bbstrader.metatrader.trade.TradingSignal``.
|
|
244
|
+
|
|
245
|
+
3. All strategies must have the following arguments in their `__init__` method:
|
|
246
|
+
- bars (DataHandler): DataHandler instance default to None
|
|
247
|
+
- events (Queue): Queue instance default to None
|
|
248
|
+
- symbol_list (List[str]): List of symbols to trade can be none for backtesting
|
|
249
|
+
- mode (str): Mode of the strategy. Must be either 'live' or 'backtest'
|
|
250
|
+
- **kwargs: Additional keyword arguments
|
|
251
|
+
The keyword arguments are all the additional arguments passed to the `Mt5ExecutionEngine` class,
|
|
252
|
+
the `Strategy` class, the `DataHandler` class, the `Portfolio` class and the `ExecutionHandler` class.
|
|
253
|
+
- The `bars` and `events` arguments are used for backtesting only.
|
|
254
|
+
|
|
255
|
+
4. All strategies must generate signals for backtesting and live trading.
|
|
256
|
+
See the `bbstrader.trading.strategies` module for more information on how to create custom strategies.
|
|
257
|
+
See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
|
|
258
|
+
"""
|
|
259
|
+
self.symbols = symbol_list.copy()
|
|
260
|
+
self.trades_instances = trades_instances
|
|
261
|
+
self.strategy_cls = strategy_cls
|
|
262
|
+
self.mm = mm
|
|
263
|
+
self.auto_trade = auto_trade
|
|
264
|
+
self.prompt_callback = prompt_callback
|
|
265
|
+
self.multithread = multithread
|
|
266
|
+
self.optimizer = optimizer
|
|
267
|
+
self.trail = trail
|
|
268
|
+
self.stop_trail = stop_trail
|
|
269
|
+
self.trail_after_points = trail_after_points
|
|
270
|
+
self.be_plus_points = be_plus_points
|
|
271
|
+
self.show_positions_orders = show_positions_orders
|
|
272
|
+
self.iter_time = iter_time
|
|
273
|
+
self.use_trade_time = use_trade_time
|
|
274
|
+
self.period = period.strip()
|
|
275
|
+
self.period_end_action = period_end_action
|
|
276
|
+
self.closing_pnl = closing_pnl
|
|
277
|
+
self.comment = comment
|
|
278
|
+
self.kwargs = kwargs
|
|
279
|
+
|
|
280
|
+
self.time_intervals = 0
|
|
281
|
+
self.time_frame = kwargs.get("time_frame", "15m")
|
|
282
|
+
self.trade_time = _TF_MAPPING[self.time_frame]
|
|
283
|
+
|
|
284
|
+
self.long_market = {symbol: False for symbol in self.symbols}
|
|
285
|
+
self.short_market = {symbol: False for symbol in self.symbols}
|
|
286
|
+
|
|
287
|
+
self._initialize_engine(**kwargs)
|
|
288
|
+
self.strategy = self._init_strategy(**kwargs)
|
|
289
|
+
self.shutdown_event = (
|
|
290
|
+
shutdown_event if shutdown_event is not None else mp.Event()
|
|
291
|
+
)
|
|
292
|
+
self._running = True
|
|
293
|
+
|
|
294
|
+
def __repr__(self):
|
|
295
|
+
symbols = self.trades_instances.keys()
|
|
296
|
+
strategy = self.strategy_cls.__name__
|
|
297
|
+
return (
|
|
298
|
+
f"{self.__class__.__name__}(Symbols={list(symbols)}, Strategy={strategy})"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def _initialize_engine(self, **kwargs):
|
|
302
|
+
global logger
|
|
303
|
+
logger = kwargs.get("logger", log)
|
|
304
|
+
try:
|
|
305
|
+
self.daily_risk = kwargs.get("daily_risk")
|
|
306
|
+
self.notify = kwargs.get("notify", False)
|
|
307
|
+
self.debug_mode = kwargs.get("debug_mode", False)
|
|
308
|
+
self.delay = kwargs.get("delay", 0)
|
|
309
|
+
|
|
310
|
+
self.STRATEGY = kwargs.get("strategy_name")
|
|
311
|
+
self.ACCOUNT = kwargs.get("account", "MT5 Account")
|
|
312
|
+
self.signal_tickers = kwargs.get("signal_tickers", self.symbols)
|
|
313
|
+
|
|
314
|
+
self.expert_ids = self._expert_ids(kwargs.get("expert_ids"))
|
|
315
|
+
self.max_trades = self._max_trades(kwargs.get("max_trades"))
|
|
316
|
+
if self.comment is None:
|
|
317
|
+
trade = self.trades_instances[self.symbols[0]]
|
|
318
|
+
self.comment = f"{trade.expert_name}@{trade.version}"
|
|
319
|
+
if kwargs.get("trading_days") is None:
|
|
320
|
+
if self.period.lower() == "24/7":
|
|
321
|
+
self.trading_days = WEEK_DAYS
|
|
322
|
+
else:
|
|
323
|
+
self.trading_days = TradingDays
|
|
324
|
+
else:
|
|
325
|
+
self.trading_days = kwargs.get("trading_days")
|
|
326
|
+
except Exception as e:
|
|
327
|
+
self._print_exc(
|
|
328
|
+
f"Initializing Execution Engine, STRATEGY={self.STRATEGY}, ACCOUNT={self.ACCOUNT}",
|
|
329
|
+
e,
|
|
330
|
+
)
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
def _print_exc(self, msg: str, e: Exception):
|
|
334
|
+
if isinstance(e, KeyboardInterrupt):
|
|
335
|
+
logger.info("Stopping the Execution Engine ...")
|
|
336
|
+
self.stop()
|
|
337
|
+
sys.exit(0)
|
|
338
|
+
if self.debug_mode:
|
|
339
|
+
raise ValueError(msg).with_traceback(e.__traceback__)
|
|
340
|
+
else:
|
|
341
|
+
logger.error(f"{msg}: {type(e).__name__}: {str(e)}")
|
|
342
|
+
|
|
343
|
+
def _max_trades(self, mtrades):
|
|
344
|
+
max_trades = {
|
|
345
|
+
symbol: mtrades[symbol]
|
|
346
|
+
if mtrades is not None and isinstance(mtrades, dict) and symbol in mtrades
|
|
347
|
+
else self.trades_instances[symbol].rm.max_trade()
|
|
348
|
+
for symbol in self.symbols
|
|
349
|
+
}
|
|
350
|
+
return max_trades
|
|
351
|
+
|
|
352
|
+
def _expert_ids(self, expert_ids):
|
|
353
|
+
if expert_ids is None:
|
|
354
|
+
expert_ids = list(
|
|
355
|
+
set([trade.expert_id for trade in self.trades_instances.values()])
|
|
356
|
+
)
|
|
357
|
+
elif isinstance(expert_ids, int):
|
|
358
|
+
expert_ids = [expert_ids]
|
|
359
|
+
return expert_ids
|
|
360
|
+
|
|
361
|
+
def _init_strategy(self, **kwargs) -> LiveStrategy:
|
|
362
|
+
try:
|
|
363
|
+
check_mt5_connection(**kwargs)
|
|
364
|
+
strategy = self.strategy_cls(self.symbols, **kwargs)
|
|
365
|
+
except Exception as e:
|
|
366
|
+
self._print_exc(
|
|
367
|
+
f"Initializing strategy, STRATEGY={self.STRATEGY}, ACCOUNT={self.ACCOUNT}",
|
|
368
|
+
e,
|
|
369
|
+
)
|
|
370
|
+
return
|
|
371
|
+
logger.info(
|
|
372
|
+
f"Running {self.STRATEGY} Strategy in {self.time_frame} Interval ..., ACCOUNT={self.ACCOUNT}"
|
|
373
|
+
)
|
|
374
|
+
return strategy
|
|
375
|
+
|
|
376
|
+
def _get_signal_info(self, signal, symbol, price, stoplimit, sl, tp):
|
|
377
|
+
account = self.strategy.account
|
|
378
|
+
symbol_info = account.get_symbol_info(symbol)
|
|
379
|
+
|
|
380
|
+
common_data = {
|
|
381
|
+
"signal": signal,
|
|
382
|
+
"symbol": symbol,
|
|
383
|
+
"strategy": self.STRATEGY,
|
|
384
|
+
"timeframe": self.time_frame,
|
|
385
|
+
"account": self.ACCOUNT,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
info = (
|
|
389
|
+
"SIGNAL={signal}, SYMBOL={symbol}, STRATEGY={strategy}, "
|
|
390
|
+
"TIMEFRAME={timeframe}, ACCOUNT={account}"
|
|
391
|
+
).format(**common_data)
|
|
392
|
+
|
|
393
|
+
sigmsg = (
|
|
394
|
+
"SIGNAL={signal}\n"
|
|
395
|
+
"SYMBOL={symbol}\n"
|
|
396
|
+
"TYPE={symbol_type}\n"
|
|
397
|
+
"DESCRIPTION={description}\n"
|
|
398
|
+
"PRICE={price}\n"
|
|
399
|
+
"STOPLIMIT={stoplimit}\n"
|
|
400
|
+
"STOP_LOSS={sl}\n"
|
|
401
|
+
"TAKE_PROFIT={tp}\n"
|
|
402
|
+
"STRATEGY={strategy}\n"
|
|
403
|
+
"TIMEFRAME={timeframe}\n"
|
|
404
|
+
"BROKER={broker}\n"
|
|
405
|
+
"TIMESTAMP={timestamp}"
|
|
406
|
+
).format(
|
|
407
|
+
**common_data,
|
|
408
|
+
symbol_type=account.get_symbol_type(symbol).value,
|
|
409
|
+
description=symbol_info.description if symbol_info else "N/A",
|
|
410
|
+
price=round(price, 5) if price else "MARKET",
|
|
411
|
+
stoplimit=round(stoplimit, 5) if stoplimit else None,
|
|
412
|
+
sl=round(sl, 5) if sl else "AUTO",
|
|
413
|
+
tp=round(tp, 5) if tp else "AUTO",
|
|
414
|
+
broker=account.broker.name,
|
|
415
|
+
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
msg_template = "SYMBOL={symbol}, STRATEGY={strategy}, ACCOUNT={account}"
|
|
419
|
+
msg = f"Sending {signal} Order ... " + msg_template.format(**common_data)
|
|
420
|
+
tfmsg = "Time Frame Not completed !!! " + msg_template.format(**common_data)
|
|
421
|
+
riskmsg = "Risk not allowed !!! " + msg_template.format(**common_data)
|
|
422
|
+
|
|
423
|
+
return info, sigmsg, msg, tfmsg, riskmsg
|
|
424
|
+
|
|
425
|
+
def _check_retcode(self, trade: Trade, position):
|
|
426
|
+
if len(trade.retcodes) > 0:
|
|
427
|
+
for retcode in trade.retcodes:
|
|
428
|
+
if retcode in NON_EXEC_RETCODES[position]:
|
|
429
|
+
return True
|
|
430
|
+
return False
|
|
431
|
+
|
|
432
|
+
def _check_positions_orders(self):
|
|
433
|
+
positions_orders = {}
|
|
434
|
+
try:
|
|
435
|
+
check_mt5_connection(**self.kwargs)
|
|
436
|
+
for order_type in POSITIONS_TYPES + ORDERS_TYPES:
|
|
437
|
+
positions_orders[order_type] = {}
|
|
438
|
+
for symbol in self.symbols:
|
|
439
|
+
positions_orders[order_type][symbol] = None
|
|
440
|
+
for id in self.expert_ids:
|
|
441
|
+
func = getattr(
|
|
442
|
+
self.trades_instances[symbol], f"get_current_{order_type}"
|
|
443
|
+
)
|
|
444
|
+
func_value = func(id=id)
|
|
445
|
+
if func_value is not None:
|
|
446
|
+
if positions_orders[order_type][symbol] is None:
|
|
447
|
+
positions_orders[order_type][symbol] = func_value
|
|
448
|
+
else:
|
|
449
|
+
positions_orders[order_type][symbol] += func_value
|
|
450
|
+
return positions_orders
|
|
451
|
+
except Exception as e:
|
|
452
|
+
self._print_exc(
|
|
453
|
+
f"Checking positions and orders, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}",
|
|
454
|
+
e,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
def _long_short_market(self, buys, sells):
|
|
458
|
+
long_market = {
|
|
459
|
+
symbol: buys[symbol] is not None
|
|
460
|
+
and len(buys[symbol]) >= self.max_trades[symbol]
|
|
461
|
+
for symbol in self.symbols
|
|
462
|
+
}
|
|
463
|
+
short_market = {
|
|
464
|
+
symbol: sells[symbol] is not None
|
|
465
|
+
and len(sells[symbol]) >= self.max_trades[symbol]
|
|
466
|
+
for symbol in self.symbols
|
|
467
|
+
}
|
|
468
|
+
return long_market, short_market
|
|
469
|
+
|
|
470
|
+
def _display_positions_orders(self, positions_orders):
|
|
471
|
+
for symbol in self.symbols:
|
|
472
|
+
for order_type in POSITIONS_TYPES + ORDERS_TYPES:
|
|
473
|
+
if positions_orders[order_type][symbol] is not None:
|
|
474
|
+
logger.info(
|
|
475
|
+
f"Current {order_type.upper()} SYMBOL={symbol}: \
|
|
476
|
+
{positions_orders[order_type][symbol]}, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def _send_notification(self, signal, symbol):
|
|
480
|
+
telegram = self.kwargs.get("telegram", False)
|
|
481
|
+
bot_token = self.kwargs.get("bot_token")
|
|
482
|
+
chat_id = self.kwargs.get("chat_id")
|
|
483
|
+
notify = self.kwargs.get("notify", False)
|
|
484
|
+
if symbol in self.signal_tickers:
|
|
485
|
+
send_message(
|
|
486
|
+
message=signal,
|
|
487
|
+
notify_me=notify,
|
|
488
|
+
telegram=telegram,
|
|
489
|
+
token=bot_token,
|
|
490
|
+
chat_id=chat_id,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
def _logmsg(self, period, symbol):
|
|
494
|
+
logger.info(
|
|
495
|
+
f"End of the {period} !!! SYMBOL={symbol}, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
def _logmsgif(self, period, symbol):
|
|
499
|
+
if len(self.symbols) <= 10:
|
|
500
|
+
self._logmsg(period, symbol)
|
|
501
|
+
elif len(self.symbols) > 10 and symbol == self.symbols[-1]:
|
|
502
|
+
logger.info(
|
|
503
|
+
f"End of the {period} !!! STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
def _sleepmsg(self, sleep_time):
|
|
507
|
+
logger.info(f"{self.ACCOUNT} Sleeping for {sleep_time} minutes ...\n")
|
|
508
|
+
|
|
509
|
+
def _sleep_over_night(self, sessionmsg):
|
|
510
|
+
sleep_time = self.trades_instances[self.symbols[-1]].sleep_time()
|
|
511
|
+
self._sleepmsg(sleep_time + self.delay)
|
|
512
|
+
time.sleep(60 * sleep_time + self.delay)
|
|
513
|
+
logger.info(sessionmsg)
|
|
514
|
+
|
|
515
|
+
def _sleep_over_weekend(self, sessionmsg):
|
|
516
|
+
sleep_time = self.trades_instances[self.symbols[-1]].sleep_time(weekend=True)
|
|
517
|
+
self._sleepmsg(sleep_time + self.delay)
|
|
518
|
+
time.sleep(60 * sleep_time + self.delay)
|
|
519
|
+
logger.info(sessionmsg)
|
|
520
|
+
|
|
521
|
+
def _check_is_day_ends(self, trade: Trade, symbol, period_type, today, closing):
|
|
522
|
+
if trade.days_end() or (today in WEEK_ENDS and today != FRIDAY):
|
|
523
|
+
self._logmsgif("Day", symbol) if today not in WEEK_ENDS else self._logmsgif(
|
|
524
|
+
"Week", symbol
|
|
525
|
+
)
|
|
526
|
+
if (
|
|
527
|
+
(period_type == "month" and self._is_month_ends() and closing)
|
|
528
|
+
or (period_type == "week" and today == FRIDAY and closing)
|
|
529
|
+
or (period_type == "day" and closing)
|
|
530
|
+
or (period_type == "24/7" and closing)
|
|
531
|
+
):
|
|
532
|
+
logger.info(
|
|
533
|
+
f"{self.ACCOUNT} Closing all positions and orders for {symbol} ..."
|
|
534
|
+
)
|
|
535
|
+
for id in self.expert_ids:
|
|
536
|
+
trade.close_positions(
|
|
537
|
+
position_type="all", id=id, comment=self.comment
|
|
538
|
+
)
|
|
539
|
+
trade.statistics(save=True)
|
|
540
|
+
|
|
541
|
+
def _is_month_ends(self):
|
|
542
|
+
today = pd.Timestamp(date.today())
|
|
543
|
+
last_business_day = today + pd.tseries.offsets.BMonthEnd(0)
|
|
544
|
+
return today == last_business_day
|
|
545
|
+
|
|
546
|
+
def _daily_end_checks(self, today, closing, sessionmsg):
|
|
547
|
+
self.strategy.perform_period_end_checks()
|
|
548
|
+
if self.period_end_action == "break" and closing:
|
|
549
|
+
sys.exit(0)
|
|
550
|
+
elif self.period_end_action == "sleep" and today not in WEEK_ENDS:
|
|
551
|
+
self._sleep_over_night(sessionmsg)
|
|
552
|
+
elif self.period_end_action == "sleep" and today in WEEK_ENDS:
|
|
553
|
+
self._sleep_over_weekend(sessionmsg)
|
|
554
|
+
|
|
555
|
+
def _weekly_end_checks(self, today, closing, sessionmsg):
|
|
556
|
+
if today not in WEEK_ENDS:
|
|
557
|
+
self._sleep_over_night(sessionmsg)
|
|
558
|
+
else:
|
|
559
|
+
self.strategy.perform_period_end_checks()
|
|
560
|
+
if self.period_end_action == "break" and closing:
|
|
561
|
+
sys.exit(0)
|
|
562
|
+
elif self.period_end_action == "sleep" or not closing:
|
|
563
|
+
self._sleep_over_weekend(sessionmsg)
|
|
564
|
+
|
|
565
|
+
def _monthly_end_cheks(self, today, closing, sessionmsg):
|
|
566
|
+
if today not in WEEK_ENDS and not self._is_month_ends():
|
|
567
|
+
self._sleep_over_night(sessionmsg)
|
|
568
|
+
elif self._is_month_ends() and closing:
|
|
569
|
+
self.strategy.perform_period_end_checks()
|
|
570
|
+
sys.exit(0)
|
|
571
|
+
else:
|
|
572
|
+
self._sleep_over_weekend(sessionmsg)
|
|
573
|
+
|
|
574
|
+
def _perform_period_end_actions(
|
|
575
|
+
self,
|
|
576
|
+
today,
|
|
577
|
+
day_end,
|
|
578
|
+
closing,
|
|
579
|
+
sessionmsg,
|
|
580
|
+
):
|
|
581
|
+
period = self.period.lower()
|
|
582
|
+
for symbol, trade in self.trades_instances.items():
|
|
583
|
+
self._check_is_day_ends(trade, symbol, period, today, closing)
|
|
584
|
+
|
|
585
|
+
if day_end:
|
|
586
|
+
self.time_intervals = 0
|
|
587
|
+
match period:
|
|
588
|
+
case "24/7":
|
|
589
|
+
self.strategy.perform_period_end_checks()
|
|
590
|
+
self._sleep_over_night(sessionmsg)
|
|
591
|
+
|
|
592
|
+
case "day":
|
|
593
|
+
self._daily_end_checks(today, closing, sessionmsg)
|
|
594
|
+
|
|
595
|
+
case "week":
|
|
596
|
+
self._weekly_end_checks(today, closing, sessionmsg)
|
|
597
|
+
|
|
598
|
+
case "month":
|
|
599
|
+
self._monthly_end_cheks(today, closing, sessionmsg)
|
|
600
|
+
case _:
|
|
601
|
+
raise ValueError(f"Invalid period {period}")
|
|
602
|
+
|
|
603
|
+
def _check(self, buys, sells, symbol):
|
|
604
|
+
if not self.mm:
|
|
605
|
+
return
|
|
606
|
+
if buys is not None or sells is not None:
|
|
607
|
+
self.trades_instances[symbol].break_even(
|
|
608
|
+
mm=self.mm,
|
|
609
|
+
trail=self.trail,
|
|
610
|
+
stop_trail=self.stop_trail,
|
|
611
|
+
trail_after_points=self.trail_after_points,
|
|
612
|
+
be_plus_points=self.be_plus_points,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
def _get_signals_and_weights(self):
|
|
616
|
+
try:
|
|
617
|
+
check_mt5_connection(**self.kwargs)
|
|
618
|
+
signals = self.strategy.calculate_signals()
|
|
619
|
+
weights = (
|
|
620
|
+
self.strategy.apply_risk_management(self.optimizer)
|
|
621
|
+
if hasattr(self.strategy, "apply_risk_management")
|
|
622
|
+
else None
|
|
623
|
+
)
|
|
624
|
+
return signals, weights
|
|
625
|
+
except Exception as e:
|
|
626
|
+
self._print_exc(
|
|
627
|
+
f"Calculating Signals, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}",
|
|
628
|
+
e,
|
|
629
|
+
)
|
|
630
|
+
pass
|
|
631
|
+
|
|
632
|
+
def _update_risk(self, weights):
|
|
633
|
+
try:
|
|
634
|
+
check_mt5_connection(**self.kwargs)
|
|
635
|
+
if weights is not None and not all(v == 0 for v in weights.values()):
|
|
636
|
+
assert self.daily_risk is not None
|
|
637
|
+
for symbol in self.symbols:
|
|
638
|
+
if symbol not in weights:
|
|
639
|
+
continue
|
|
640
|
+
trade = self.trades_instances[symbol]
|
|
641
|
+
dailydd = round(weights[symbol] * self.daily_risk, 5)
|
|
642
|
+
trade.dailydd = dailydd
|
|
643
|
+
except Exception as e:
|
|
644
|
+
self._print_exc(
|
|
645
|
+
f"Updating Risk, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}",
|
|
646
|
+
e,
|
|
647
|
+
)
|
|
648
|
+
pass
|
|
649
|
+
|
|
650
|
+
def _auto_trade(self, sigmsg, symbol) -> bool:
|
|
651
|
+
if self.notify:
|
|
652
|
+
self._send_notification(sigmsg, symbol)
|
|
653
|
+
if self.auto_trade:
|
|
654
|
+
return True
|
|
655
|
+
if not self.auto_trade:
|
|
656
|
+
prompt = (
|
|
657
|
+
f"{sigmsg} \n Enter Y/Yes to accept or N/No to reject this order : "
|
|
658
|
+
)
|
|
659
|
+
if self.prompt_callback is not None:
|
|
660
|
+
auto_trade = self.prompt_callback(prompt)
|
|
661
|
+
else:
|
|
662
|
+
auto_trade = input(prompt)
|
|
663
|
+
if not auto_trade.upper().startswith("Y"):
|
|
664
|
+
info = f"Order Rejected !!! SYMBOL={symbol}, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
665
|
+
logger.info(info)
|
|
666
|
+
if self.notify:
|
|
667
|
+
self._send_notification(info, symbol)
|
|
668
|
+
return False
|
|
669
|
+
return True
|
|
670
|
+
|
|
671
|
+
def _open_buy(
|
|
672
|
+
self,
|
|
673
|
+
signal,
|
|
674
|
+
symbol,
|
|
675
|
+
id,
|
|
676
|
+
trade: Trade,
|
|
677
|
+
price,
|
|
678
|
+
stoplimit,
|
|
679
|
+
sl,
|
|
680
|
+
tp,
|
|
681
|
+
sigmsg,
|
|
682
|
+
msg,
|
|
683
|
+
comment,
|
|
684
|
+
):
|
|
685
|
+
if not self._auto_trade(sigmsg, symbol):
|
|
686
|
+
return
|
|
687
|
+
if not self._check_retcode(trade, "BMKT"):
|
|
688
|
+
logger.info(msg)
|
|
689
|
+
trade.open_buy_position(
|
|
690
|
+
action=signal,
|
|
691
|
+
price=price,
|
|
692
|
+
stoplimit=stoplimit,
|
|
693
|
+
sl=sl,
|
|
694
|
+
tp=tp,
|
|
695
|
+
id=id,
|
|
696
|
+
mm=self.mm,
|
|
697
|
+
trail=self.trail,
|
|
698
|
+
comment=comment,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
def _open_sell(
|
|
702
|
+
self,
|
|
703
|
+
signal,
|
|
704
|
+
symbol,
|
|
705
|
+
id,
|
|
706
|
+
trade: Trade,
|
|
707
|
+
price,
|
|
708
|
+
stoplimit,
|
|
709
|
+
sl,
|
|
710
|
+
tp,
|
|
711
|
+
sigmsg,
|
|
712
|
+
msg,
|
|
713
|
+
comment,
|
|
714
|
+
):
|
|
715
|
+
if not self._auto_trade(sigmsg, symbol):
|
|
716
|
+
return
|
|
717
|
+
if not self._check_retcode(trade, "SMKT"):
|
|
718
|
+
logger.info(msg)
|
|
719
|
+
trade.open_sell_position(
|
|
720
|
+
action=signal,
|
|
721
|
+
price=price,
|
|
722
|
+
stoplimit=stoplimit,
|
|
723
|
+
sl=sl,
|
|
724
|
+
tp=tp,
|
|
725
|
+
id=id,
|
|
726
|
+
mm=self.mm,
|
|
727
|
+
trail=self.trail,
|
|
728
|
+
comment=comment,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
def _handle_exit_signals(self, signal, symbol, id, trade: Trade, sigmsg, comment):
|
|
732
|
+
for exit_signal, actions in EXIT_SIGNAL_ACTIONS.items():
|
|
733
|
+
if signal == exit_signal:
|
|
734
|
+
for signal_attr, order_type in actions.items():
|
|
735
|
+
clos_func = getattr(
|
|
736
|
+
self.trades_instances[symbol], f"get_current_{signal_attr}"
|
|
737
|
+
)
|
|
738
|
+
if clos_func(id=id) is not None:
|
|
739
|
+
if self.notify:
|
|
740
|
+
self._send_notification(sigmsg, symbol)
|
|
741
|
+
close_method = (
|
|
742
|
+
trade.close_positions
|
|
743
|
+
if signal_attr in POSITIONS_TYPES
|
|
744
|
+
else trade.close_orders
|
|
745
|
+
)
|
|
746
|
+
close_method(order_type, id=id, comment=comment)
|
|
747
|
+
|
|
748
|
+
def _handle_buy_signal(
|
|
749
|
+
self,
|
|
750
|
+
signal,
|
|
751
|
+
symbol,
|
|
752
|
+
id,
|
|
753
|
+
trade,
|
|
754
|
+
price,
|
|
755
|
+
stoplimit,
|
|
756
|
+
sl,
|
|
757
|
+
tp,
|
|
758
|
+
buys,
|
|
759
|
+
sells,
|
|
760
|
+
sigmsg,
|
|
761
|
+
msg,
|
|
762
|
+
tfmsg,
|
|
763
|
+
riskmsg,
|
|
764
|
+
comment,
|
|
765
|
+
):
|
|
766
|
+
if not self.long_market[symbol]:
|
|
767
|
+
if self.use_trade_time:
|
|
768
|
+
if self.time_intervals % self.trade_time == 0 or buys[symbol] is None:
|
|
769
|
+
self._open_buy(
|
|
770
|
+
signal,
|
|
771
|
+
symbol,
|
|
772
|
+
id,
|
|
773
|
+
trade,
|
|
774
|
+
price,
|
|
775
|
+
stoplimit,
|
|
776
|
+
sl,
|
|
777
|
+
tp,
|
|
778
|
+
sigmsg,
|
|
779
|
+
msg,
|
|
780
|
+
comment,
|
|
781
|
+
)
|
|
782
|
+
else:
|
|
783
|
+
logger.info(tfmsg)
|
|
784
|
+
self._check(buys[symbol], sells[symbol], symbol)
|
|
785
|
+
else:
|
|
786
|
+
self._open_buy(
|
|
787
|
+
signal,
|
|
788
|
+
symbol,
|
|
789
|
+
id,
|
|
790
|
+
trade,
|
|
791
|
+
price,
|
|
792
|
+
stoplimit,
|
|
793
|
+
sl,
|
|
794
|
+
tp,
|
|
795
|
+
sigmsg,
|
|
796
|
+
msg,
|
|
797
|
+
comment,
|
|
798
|
+
)
|
|
799
|
+
else:
|
|
800
|
+
logger.info(riskmsg)
|
|
801
|
+
|
|
802
|
+
def _handle_sell_signal(
|
|
803
|
+
self,
|
|
804
|
+
signal,
|
|
805
|
+
symbol,
|
|
806
|
+
id,
|
|
807
|
+
trade,
|
|
808
|
+
price,
|
|
809
|
+
stoplimit,
|
|
810
|
+
sl,
|
|
811
|
+
tp,
|
|
812
|
+
buys,
|
|
813
|
+
sells,
|
|
814
|
+
sigmsg,
|
|
815
|
+
msg,
|
|
816
|
+
tfmsg,
|
|
817
|
+
riskmsg,
|
|
818
|
+
comment,
|
|
819
|
+
):
|
|
820
|
+
if not self.short_market[symbol]:
|
|
821
|
+
if self.use_trade_time:
|
|
822
|
+
if self.time_intervals % self.trade_time == 0 or sells[symbol] is None:
|
|
823
|
+
self._open_sell(
|
|
824
|
+
signal,
|
|
825
|
+
symbol,
|
|
826
|
+
id,
|
|
827
|
+
trade,
|
|
828
|
+
price,
|
|
829
|
+
stoplimit,
|
|
830
|
+
sl,
|
|
831
|
+
tp,
|
|
832
|
+
sigmsg,
|
|
833
|
+
msg,
|
|
834
|
+
comment,
|
|
835
|
+
)
|
|
836
|
+
else:
|
|
837
|
+
logger.info(tfmsg)
|
|
838
|
+
self._check(buys[symbol], sells[symbol], symbol)
|
|
839
|
+
else:
|
|
840
|
+
self._open_sell(
|
|
841
|
+
signal,
|
|
842
|
+
symbol,
|
|
843
|
+
id,
|
|
844
|
+
trade,
|
|
845
|
+
price,
|
|
846
|
+
stoplimit,
|
|
847
|
+
sl,
|
|
848
|
+
tp,
|
|
849
|
+
sigmsg,
|
|
850
|
+
msg,
|
|
851
|
+
comment,
|
|
852
|
+
)
|
|
853
|
+
else:
|
|
854
|
+
logger.info(riskmsg)
|
|
855
|
+
|
|
856
|
+
def _run_trade_algorithm(
|
|
857
|
+
self,
|
|
858
|
+
signal,
|
|
859
|
+
symbol,
|
|
860
|
+
id,
|
|
861
|
+
trade,
|
|
862
|
+
price,
|
|
863
|
+
stoplimit,
|
|
864
|
+
sl,
|
|
865
|
+
tp,
|
|
866
|
+
buys,
|
|
867
|
+
sells,
|
|
868
|
+
comment,
|
|
869
|
+
):
|
|
870
|
+
signal = {"LONG": "BMKT", "BUY": "BMKT", "SHORT": "SMKT", "SELL": "SMKT"}.get(
|
|
871
|
+
signal, signal
|
|
872
|
+
)
|
|
873
|
+
if (
|
|
874
|
+
self.trades_instances[symbol].dailydd == 0
|
|
875
|
+
and signal not in EXIT_SIGNAL_ACTIONS
|
|
876
|
+
):
|
|
877
|
+
logger.info(
|
|
878
|
+
f"Daily Risk is set to 0 !!! No trades allowed for SYMBOL={symbol}, "
|
|
879
|
+
f"STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
880
|
+
)
|
|
881
|
+
return
|
|
882
|
+
info, sigmsg, msg, tfmsg, riskmsg = self._get_signal_info(
|
|
883
|
+
signal, symbol, price, stoplimit, sl, tp
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
if signal not in EXIT_SIGNAL_ACTIONS:
|
|
887
|
+
if signal in NON_EXEC_RETCODES and not self._check_retcode(trade, signal):
|
|
888
|
+
logger.info(info)
|
|
889
|
+
elif signal not in NON_EXEC_RETCODES:
|
|
890
|
+
logger.info(info)
|
|
891
|
+
|
|
892
|
+
signal_handler = None
|
|
893
|
+
if signal in EXIT_SIGNAL_ACTIONS:
|
|
894
|
+
self._handle_exit_signals(signal, symbol, id, trade, sigmsg, comment)
|
|
895
|
+
elif signal in BUYS:
|
|
896
|
+
signal_handler = self._handle_buy_signal
|
|
897
|
+
elif signal in SELLS:
|
|
898
|
+
signal_handler = self._handle_sell_signal
|
|
899
|
+
|
|
900
|
+
if signal_handler is not None:
|
|
901
|
+
signal_handler(
|
|
902
|
+
signal,
|
|
903
|
+
symbol,
|
|
904
|
+
id,
|
|
905
|
+
trade,
|
|
906
|
+
price,
|
|
907
|
+
stoplimit,
|
|
908
|
+
sl,
|
|
909
|
+
tp,
|
|
910
|
+
buys,
|
|
911
|
+
sells,
|
|
912
|
+
sigmsg,
|
|
913
|
+
msg,
|
|
914
|
+
tfmsg,
|
|
915
|
+
riskmsg,
|
|
916
|
+
comment,
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
def _is_closing(self):
|
|
920
|
+
closing = True
|
|
921
|
+
if self.closing_pnl is not None:
|
|
922
|
+
closing = all(
|
|
923
|
+
trade.positive_profit(id=trade.expert_id, th=self.closing_pnl)
|
|
924
|
+
for trade in self.trades_instances.values()
|
|
925
|
+
)
|
|
926
|
+
return closing
|
|
927
|
+
|
|
928
|
+
def _sleep(self):
|
|
929
|
+
time.sleep((60 * self.iter_time) - 1.0)
|
|
930
|
+
if self.iter_time == 1:
|
|
931
|
+
self.time_intervals += 1
|
|
932
|
+
elif self.trade_time % self.iter_time == 0:
|
|
933
|
+
self.time_intervals += self.iter_time
|
|
934
|
+
else:
|
|
935
|
+
if self.use_trade_time:
|
|
936
|
+
raise ValueError(
|
|
937
|
+
f"iter_time must be a multiple of the {self.time_frame} !!!"
|
|
938
|
+
f"(e.g., if time_frame is 15m, iter_time must be 1.5, 3, 5, 15 etc)"
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
def _handle_one_signal(self, signal, today, buys, sells):
|
|
942
|
+
try:
|
|
943
|
+
symbol = signal.symbol
|
|
944
|
+
trade: Trade = self.trades_instances[symbol]
|
|
945
|
+
if trade.trading_time() and today in self.trading_days:
|
|
946
|
+
if signal.action is not None:
|
|
947
|
+
action = (
|
|
948
|
+
signal.action.value
|
|
949
|
+
if isinstance(signal.action, TradeAction)
|
|
950
|
+
else signal.action
|
|
951
|
+
)
|
|
952
|
+
self._run_trade_algorithm(
|
|
953
|
+
action,
|
|
954
|
+
symbol,
|
|
955
|
+
signal.id,
|
|
956
|
+
trade,
|
|
957
|
+
signal.price,
|
|
958
|
+
signal.stoplimit,
|
|
959
|
+
signal.sl,
|
|
960
|
+
signal.tp,
|
|
961
|
+
buys,
|
|
962
|
+
sells,
|
|
963
|
+
signal.comment or self.comment,
|
|
964
|
+
)
|
|
965
|
+
else:
|
|
966
|
+
if len(self.symbols) >= 10:
|
|
967
|
+
if symbol == self.symbols[-1]:
|
|
968
|
+
logger.info(
|
|
969
|
+
f"Not trading Time !!!, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
970
|
+
)
|
|
971
|
+
else:
|
|
972
|
+
logger.info(
|
|
973
|
+
f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
974
|
+
)
|
|
975
|
+
self._check(buys[symbol], sells[symbol], symbol)
|
|
976
|
+
|
|
977
|
+
except Exception as e:
|
|
978
|
+
msg = (
|
|
979
|
+
f"Error handling signal for SYMBOL={signal.symbol} (SIGNAL: {action}), "
|
|
980
|
+
f"STRATEGY={self.STRATEGY}, ACCOUNT={self.ACCOUNT}"
|
|
981
|
+
)
|
|
982
|
+
self._print_exc(msg, e)
|
|
983
|
+
|
|
984
|
+
def _handle_all_signals(self, today, signals, buys, sells, max_workers=50):
|
|
985
|
+
try:
|
|
986
|
+
check_mt5_connection(**self.kwargs)
|
|
987
|
+
except Exception as e:
|
|
988
|
+
msg = "Initial MT5 connection check failed. Aborting signal processing."
|
|
989
|
+
self._print_exc(msg, e)
|
|
990
|
+
return
|
|
991
|
+
|
|
992
|
+
if not signals:
|
|
993
|
+
return
|
|
994
|
+
|
|
995
|
+
# We want to create a temporary function that
|
|
996
|
+
# already has the 'today', 'buys', and 'sells' arguments filled in.
|
|
997
|
+
# This is necessary because executor.map only iterates over one sequence (signals).
|
|
998
|
+
signal_processor = functools.partial(
|
|
999
|
+
self._handle_one_signal, today=today, buys=buys, sells=sells
|
|
1000
|
+
)
|
|
1001
|
+
if self.multithread:
|
|
1002
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
1003
|
+
max_workers=max_workers
|
|
1004
|
+
) as executor:
|
|
1005
|
+
# 'map' will apply our worker function to every item in the 'signals' list.
|
|
1006
|
+
# It will automatically manage the distribution of tasks to the worker threads.
|
|
1007
|
+
# We wrap it in list() to ensure all tasks are complete before moving on.
|
|
1008
|
+
list(executor.map(signal_processor, signals))
|
|
1009
|
+
else:
|
|
1010
|
+
for signal in signals:
|
|
1011
|
+
try:
|
|
1012
|
+
signal_processor(signal)
|
|
1013
|
+
except Exception as e:
|
|
1014
|
+
self._print_exc(f"Failed to process signal {signal}: ", e)
|
|
1015
|
+
|
|
1016
|
+
def _handle_period_end_actions(self, today):
|
|
1017
|
+
try:
|
|
1018
|
+
check_mt5_connection(**self.kwargs)
|
|
1019
|
+
day_end = (
|
|
1020
|
+
all(trade.days_end() for trade in self.trades_instances.values())
|
|
1021
|
+
or (today in WEEK_ENDS and today != FRIDAY)
|
|
1022
|
+
and self.period != "24/7"
|
|
1023
|
+
)
|
|
1024
|
+
closing = self._is_closing()
|
|
1025
|
+
sessionmsg = f"{self.ACCOUNT} STARTING NEW TRADING SESSION ...\n"
|
|
1026
|
+
self._perform_period_end_actions(
|
|
1027
|
+
today,
|
|
1028
|
+
day_end,
|
|
1029
|
+
closing,
|
|
1030
|
+
sessionmsg,
|
|
1031
|
+
)
|
|
1032
|
+
except Exception as e:
|
|
1033
|
+
msg = f"Handling period end actions, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
1034
|
+
self._print_exc(msg, e)
|
|
1035
|
+
pass
|
|
1036
|
+
|
|
1037
|
+
def _select_symbols(self):
|
|
1038
|
+
for symbol in self.symbols:
|
|
1039
|
+
if not MT5.symbol_select(symbol, True):
|
|
1040
|
+
logger.error(
|
|
1041
|
+
f"Failed to select symbol {symbol} error = {MT5.last_error()}"
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
def run(self):
|
|
1045
|
+
while self._running and not self.shutdown_event.is_set():
|
|
1046
|
+
try:
|
|
1047
|
+
check_mt5_connection(**self.kwargs)
|
|
1048
|
+
self._select_symbols()
|
|
1049
|
+
positions_orders = self._check_positions_orders()
|
|
1050
|
+
if self.show_positions_orders:
|
|
1051
|
+
self._display_positions_orders(positions_orders)
|
|
1052
|
+
buys = positions_orders.get("buys")
|
|
1053
|
+
sells = positions_orders.get("sells")
|
|
1054
|
+
self.long_market, self.short_market = self._long_short_market(
|
|
1055
|
+
buys, sells
|
|
1056
|
+
)
|
|
1057
|
+
today = datetime.now().strftime("%A").lower()
|
|
1058
|
+
signals, weights = self._get_signals_and_weights()
|
|
1059
|
+
if len(signals) == 0:
|
|
1060
|
+
for symbol in self.symbols:
|
|
1061
|
+
self._check(buys[symbol], sells[symbol], symbol)
|
|
1062
|
+
else:
|
|
1063
|
+
self._update_risk(weights)
|
|
1064
|
+
self._handle_all_signals(today, signals, buys, sells)
|
|
1065
|
+
self._sleep()
|
|
1066
|
+
self._handle_period_end_actions(today)
|
|
1067
|
+
except KeyboardInterrupt:
|
|
1068
|
+
self.stop()
|
|
1069
|
+
sys.exit(0)
|
|
1070
|
+
except Exception as e:
|
|
1071
|
+
msg = f"Running Execution Engine, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
1072
|
+
self._print_exc(msg, e)
|
|
1073
|
+
self._sleep()
|
|
1074
|
+
|
|
1075
|
+
def stop(self):
|
|
1076
|
+
"""Stops the execution engine."""
|
|
1077
|
+
if self._running:
|
|
1078
|
+
logger.info(
|
|
1079
|
+
f"Stopping Execution Engine for {self.STRATEGY} STRATEGY on {self.ACCOUNT} Account"
|
|
1080
|
+
)
|
|
1081
|
+
self._running = False
|
|
1082
|
+
self.shutdown_event.set()
|
|
1083
|
+
logger.info("Execution Engine stopped successfully.")
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def RunMt5Engine(account_id: str, **kwargs):
|
|
1087
|
+
"""
|
|
1088
|
+
Start an MT5 execution engine for a given account.
|
|
1089
|
+
|
|
1090
|
+
Parameters
|
|
1091
|
+
----------
|
|
1092
|
+
account_id : str
|
|
1093
|
+
Account ID to run the execution engine on.
|
|
1094
|
+
|
|
1095
|
+
**kwargs : dict
|
|
1096
|
+
Additional keyword arguments. Possible keys include:
|
|
1097
|
+
|
|
1098
|
+
* symbol_list : list
|
|
1099
|
+
List of symbols to trade.
|
|
1100
|
+
* trades_instances : dict
|
|
1101
|
+
Dictionary of Trade instances.
|
|
1102
|
+
* strategy_cls : class
|
|
1103
|
+
Strategy class to use for trading.
|
|
1104
|
+
|
|
1105
|
+
Returns
|
|
1106
|
+
-------
|
|
1107
|
+
None
|
|
1108
|
+
Initializes and runs the MT5 execution engine.
|
|
1109
|
+
"""
|
|
1110
|
+
log.info(f"Starting execution engine for {account_id}")
|
|
1111
|
+
|
|
1112
|
+
symbol_list = kwargs.pop("symbol_list")
|
|
1113
|
+
trades_instances = kwargs.pop("trades_instances")
|
|
1114
|
+
strategy_cls = kwargs.pop("strategy_cls")
|
|
1115
|
+
|
|
1116
|
+
if symbol_list is None or trades_instances is None or strategy_cls is None:
|
|
1117
|
+
log.error(f"Missing required arguments for account {account_id}")
|
|
1118
|
+
raise ValueError(f"Missing required arguments for account {account_id}")
|
|
1119
|
+
|
|
1120
|
+
try:
|
|
1121
|
+
engine = Mt5ExecutionEngine(
|
|
1122
|
+
symbol_list, trades_instances, strategy_cls, **kwargs
|
|
1123
|
+
)
|
|
1124
|
+
engine.run()
|
|
1125
|
+
except KeyboardInterrupt:
|
|
1126
|
+
log.info(f"Execution engine for {account_id} interrupted by user")
|
|
1127
|
+
engine.stop()
|
|
1128
|
+
sys.exit(0)
|
|
1129
|
+
except Exception as e:
|
|
1130
|
+
log.exception(f"Error running execution engine for {account_id}: {e}")
|
|
1131
|
+
finally:
|
|
1132
|
+
log.info(f"Execution for {account_id} completed")
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def RunMt5Engines(accounts: Dict[str, Dict], start_delay: float = 1.0):
|
|
1136
|
+
"""Runs multiple MT5 execution engines in parallel using multiprocessing.
|
|
1137
|
+
|
|
1138
|
+
Args:
|
|
1139
|
+
accounts: Dictionary of accounts to run the execution engines on.
|
|
1140
|
+
Keys are the account names or IDs and values are the parameters for the execution engine.
|
|
1141
|
+
The parameters are the same as the ones passed to the `Mt5ExecutionEngine` class.
|
|
1142
|
+
start_delay: Delay in seconds between starting the processes. Defaults to 1.0.
|
|
1143
|
+
"""
|
|
1144
|
+
|
|
1145
|
+
processes = {}
|
|
1146
|
+
|
|
1147
|
+
for account_id, params in accounts.items():
|
|
1148
|
+
log.info(f"Starting process for {account_id}")
|
|
1149
|
+
params["multithread"] = True
|
|
1150
|
+
process = mp.Process(target=RunMt5Engine, args=(account_id,), kwargs=params)
|
|
1151
|
+
process.start()
|
|
1152
|
+
processes[process] = account_id
|
|
1153
|
+
|
|
1154
|
+
if start_delay:
|
|
1155
|
+
time.sleep(start_delay)
|
|
1156
|
+
|
|
1157
|
+
for process, account_id in processes.items():
|
|
1158
|
+
process.join()
|
|
1159
|
+
log.info(f"Process for {account_id} joined")
|