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.
Files changed (45) hide show
  1. bbstrader/__init__.py +27 -0
  2. bbstrader/__main__.py +92 -0
  3. bbstrader/api/__init__.py +96 -0
  4. bbstrader/api/handlers.py +245 -0
  5. bbstrader/api/metatrader_client.cpython-312-darwin.so +0 -0
  6. bbstrader/api/metatrader_client.pyi +624 -0
  7. bbstrader/assets/bbs_.png +0 -0
  8. bbstrader/assets/bbstrader.ico +0 -0
  9. bbstrader/assets/bbstrader.png +0 -0
  10. bbstrader/assets/qs_metrics_1.png +0 -0
  11. bbstrader/btengine/__init__.py +54 -0
  12. bbstrader/btengine/backtest.py +358 -0
  13. bbstrader/btengine/data.py +737 -0
  14. bbstrader/btengine/event.py +229 -0
  15. bbstrader/btengine/execution.py +287 -0
  16. bbstrader/btengine/performance.py +408 -0
  17. bbstrader/btengine/portfolio.py +393 -0
  18. bbstrader/btengine/strategy.py +588 -0
  19. bbstrader/compat.py +28 -0
  20. bbstrader/config.py +100 -0
  21. bbstrader/core/__init__.py +27 -0
  22. bbstrader/core/data.py +628 -0
  23. bbstrader/core/strategy.py +466 -0
  24. bbstrader/metatrader/__init__.py +48 -0
  25. bbstrader/metatrader/_copier.py +720 -0
  26. bbstrader/metatrader/account.py +865 -0
  27. bbstrader/metatrader/broker.py +418 -0
  28. bbstrader/metatrader/copier.py +1487 -0
  29. bbstrader/metatrader/rates.py +495 -0
  30. bbstrader/metatrader/risk.py +667 -0
  31. bbstrader/metatrader/trade.py +1692 -0
  32. bbstrader/metatrader/utils.py +402 -0
  33. bbstrader/models/__init__.py +39 -0
  34. bbstrader/models/nlp.py +932 -0
  35. bbstrader/models/optimization.py +182 -0
  36. bbstrader/scripts.py +665 -0
  37. bbstrader/trading/__init__.py +33 -0
  38. bbstrader/trading/execution.py +1159 -0
  39. bbstrader/trading/strategy.py +362 -0
  40. bbstrader/trading/utils.py +69 -0
  41. bbstrader-2.0.3.dist-info/METADATA +396 -0
  42. bbstrader-2.0.3.dist-info/RECORD +45 -0
  43. bbstrader-2.0.3.dist-info/WHEEL +5 -0
  44. bbstrader-2.0.3.dist-info/entry_points.txt +3 -0
  45. 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")