bbstrader 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.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.

Potentially problematic release.


This version of bbstrader might be problematic. Click here for more details.

@@ -1,7 +1,10 @@
1
+ import concurrent.futures
2
+ import functools
1
3
  import multiprocessing as mp
2
4
  import sys
3
5
  import time
4
6
  from datetime import date, datetime
7
+ from multiprocessing.synchronize import Event
5
8
  from typing import Callable, Dict, List, Literal, Optional
6
9
 
7
10
  import pandas as pd
@@ -9,7 +12,7 @@ from loguru import logger as log
9
12
 
10
13
  from bbstrader.btengine.strategy import MT5Strategy, Strategy
11
14
  from bbstrader.config import BBSTRADER_DIR
12
- from bbstrader.metatrader.account import Account, check_mt5_connection
15
+ from bbstrader.metatrader.account import check_mt5_connection
13
16
  from bbstrader.metatrader.trade import Trade, TradeAction, TradingMode
14
17
  from bbstrader.trading.utils import send_message
15
18
 
@@ -37,6 +40,8 @@ _TF_MAPPING = {
37
40
  "D1": 1440,
38
41
  }
39
42
 
43
+ MT5_ENGINE_TIMEFRAMES = list(_TF_MAPPING.keys())
44
+
40
45
  TradingDays = ["monday", "tuesday", "wednesday", "thursday", "friday"]
41
46
  WEEK_DAYS = TradingDays + ["saturday", "sunday"]
42
47
  FRIDAY = "friday"
@@ -177,6 +182,8 @@ class Mt5ExecutionEngine:
177
182
  mm: bool = True,
178
183
  auto_trade: bool = True,
179
184
  prompt_callback: Callable = None,
185
+ multithread: bool = False,
186
+ shutdown_event: Event = None,
180
187
  optimizer: str = "equal",
181
188
  trail: bool = True,
182
189
  stop_trail: Optional[int] = None,
@@ -205,6 +212,10 @@ class Mt5ExecutionEngine:
205
212
  the user for confimation.
206
213
  prompt_callback : Callback function to prompt the user for confirmation.
207
214
  This is useful when integrating with GUI applications.
215
+ multithread : If True, use a thread pool to process signals in parallel.
216
+ If False, process them sequentially. Set this to True only if the engine
217
+ is running in a separate process. Default to False.
218
+ shutdown_event : Use to terminate the copy process when runs in a custum environment like web App or GUI.
208
219
  show_positions_orders : Print open positions and orders. Defaults to False.
209
220
  iter_time : Interval to check for signals and `mm`. Defaults to 5.
210
221
  use_trade_time : Open trades after the time is completed. Defaults to True.
@@ -249,6 +260,7 @@ class Mt5ExecutionEngine:
249
260
  self.mm = mm
250
261
  self.auto_trade = auto_trade
251
262
  self.prompt_callback = prompt_callback
263
+ self.multithread = multithread
252
264
  self.optimizer = optimizer
253
265
  self.trail = trail
254
266
  self.stop_trail = stop_trail
@@ -272,6 +284,9 @@ class Mt5ExecutionEngine:
272
284
 
273
285
  self._initialize_engine(**kwargs)
274
286
  self.strategy = self._init_strategy(**kwargs)
287
+ self.shutdown_event = (
288
+ shutdown_event if shutdown_event is not None else mp.Event()
289
+ )
275
290
  self._running = True
276
291
 
277
292
  def __repr__(self):
@@ -314,16 +329,17 @@ class Mt5ExecutionEngine:
314
329
  def _print_exc(self, msg: str, e: Exception):
315
330
  if isinstance(e, KeyboardInterrupt):
316
331
  logger.info("Stopping the Execution Engine ...")
332
+ self.stop()
317
333
  sys.exit(0)
318
334
  if self.debug_mode:
319
335
  raise ValueError(msg).with_traceback(e.__traceback__)
320
336
  else:
321
- logger.error(f"{msg, repr(e)}")
337
+ logger.error(f"{msg}: {type(e).__name__}: {str(e)}")
322
338
 
323
339
  def _max_trades(self, mtrades):
324
340
  max_trades = {
325
341
  symbol: mtrades[symbol]
326
- if mtrades is not None and symbol in mtrades
342
+ if mtrades is not None and isinstance(mtrades, dict) and symbol in mtrades
327
343
  else self.trades_instances[symbol].max_trade()
328
344
  for symbol in self.symbols
329
345
  }
@@ -338,7 +354,7 @@ class Mt5ExecutionEngine:
338
354
  expert_ids = [expert_ids]
339
355
  return expert_ids
340
356
 
341
- def _init_strategy(self, **kwargs) -> Strategy | MT5Strategy:
357
+ def _init_strategy(self, **kwargs) -> MT5Strategy:
342
358
  try:
343
359
  check_mt5_connection(**kwargs)
344
360
  strategy = self.strategy_cls(
@@ -356,7 +372,7 @@ class Mt5ExecutionEngine:
356
372
  return strategy
357
373
 
358
374
  def _get_signal_info(self, signal, symbol, price, stoplimit):
359
- account = Account(**self.kwargs)
375
+ account = self.strategy.account
360
376
  symbol_info = account.get_symbol_info(symbol)
361
377
 
362
378
  common_data = {
@@ -512,6 +528,9 @@ class Mt5ExecutionEngine:
512
528
  or (period_type == "day" and closing)
513
529
  or (period_type == "24/7" and closing)
514
530
  ):
531
+ logger.info(
532
+ f"{self.ACCOUNT} Closing all positions and orders for {symbol} ..."
533
+ )
515
534
  for id in self.expert_ids:
516
535
  trade.close_positions(
517
536
  position_type="all", id=id, comment=self.comment
@@ -627,13 +646,15 @@ class Mt5ExecutionEngine:
627
646
  )
628
647
  pass
629
648
 
630
- def _handle_auto_trade(self, sigmsg, symbol) -> bool:
649
+ def _auto_trade(self, sigmsg, symbol) -> bool:
631
650
  if self.notify:
632
651
  self._send_notification(sigmsg, symbol)
633
652
  if self.auto_trade:
634
653
  return True
635
654
  if not self.auto_trade:
636
- prompt = f"{sigmsg} \n Enter Y/Yes to accept or N/No to reject this order : "
655
+ prompt = (
656
+ f"{sigmsg} \n Enter Y/Yes to accept or N/No to reject this order : "
657
+ )
637
658
  if self.prompt_callback is not None:
638
659
  auto_trade = self.prompt_callback(prompt)
639
660
  else:
@@ -649,7 +670,7 @@ class Mt5ExecutionEngine:
649
670
  def _open_buy(
650
671
  self, signal, symbol, id, trade: Trade, price, stoplimit, sigmsg, msg, comment
651
672
  ):
652
- if not self._handle_auto_trade(sigmsg, symbol):
673
+ if not self._auto_trade(sigmsg, symbol):
653
674
  return
654
675
  if not self._check_retcode(trade, "BMKT"):
655
676
  logger.info(msg)
@@ -666,7 +687,7 @@ class Mt5ExecutionEngine:
666
687
  def _open_sell(
667
688
  self, signal, symbol, id, trade: Trade, price, stoplimit, sigmsg, msg, comment
668
689
  ):
669
- if not self._handle_auto_trade(sigmsg, symbol):
690
+ if not self._auto_trade(sigmsg, symbol):
670
691
  return
671
692
  if not self._check_retcode(trade, "SMKT"):
672
693
  logger.info(msg)
@@ -859,46 +880,79 @@ class Mt5ExecutionEngine:
859
880
  f"(e.g., if time_frame is 15m, iter_time must be 1.5, 3, 5, 15 etc)"
860
881
  )
861
882
 
862
- def _handle_signals(self, today, signals, buys, sells):
883
+ def _handle_one_signal(self, signal, today, buys, sells):
863
884
  try:
864
- check_mt5_connection(**self.kwargs)
865
- for signal in signals:
866
- symbol = signal.symbol
867
- trade: Trade = self.trades_instances[symbol]
868
- if trade.trading_time() and today in self.trading_days:
869
- if signal.action is not None:
870
- action = (
871
- signal.action.value
872
- if isinstance(signal.action, TradeAction)
873
- else signal.action
874
- )
875
- self._run_trade_algorithm(
876
- action,
877
- symbol,
878
- signal.id,
879
- trade,
880
- signal.price,
881
- signal.stoplimit,
882
- buys,
883
- sells,
884
- signal.comment or self.comment,
885
- )
886
- else:
887
- if len(self.symbols) >= 10:
888
- if symbol == self.symbols[-1]:
889
- logger.info(
890
- f"Not trading Time !!!, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
891
- )
892
- else:
885
+ symbol = signal.symbol
886
+ trade: Trade = self.trades_instances[symbol]
887
+ if trade.trading_time() and today in self.trading_days:
888
+ if signal.action is not None:
889
+ action = (
890
+ signal.action.value
891
+ if isinstance(signal.action, TradeAction)
892
+ else signal.action
893
+ )
894
+ self._run_trade_algorithm(
895
+ action,
896
+ symbol,
897
+ signal.id,
898
+ trade,
899
+ signal.price,
900
+ signal.stoplimit,
901
+ buys,
902
+ sells,
903
+ signal.comment or self.comment,
904
+ )
905
+ else:
906
+ if len(self.symbols) >= 10:
907
+ if symbol == self.symbols[-1]:
893
908
  logger.info(
894
- f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
909
+ f"Not trading Time !!!, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
895
910
  )
896
- self._check(buys[symbol], sells[symbol], symbol)
911
+ else:
912
+ logger.info(
913
+ f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
914
+ )
915
+ self._check(buys[symbol], sells[symbol], symbol)
897
916
 
898
917
  except Exception as e:
899
- msg = f"Handling Signals, SYMBOL={symbol}, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
918
+ msg = (
919
+ f"Error handling signal for SYMBOL={signal.symbol} (SIGNAL: {action}), "
920
+ f"STRATEGY={self.STRATEGY}, ACCOUNT={self.ACCOUNT}"
921
+ )
900
922
  self._print_exc(msg, e)
901
- pass
923
+
924
+ def _handle_all_signals(self, today, signals, buys, sells, max_workers=50):
925
+ try:
926
+ check_mt5_connection(**self.kwargs)
927
+ except Exception as e:
928
+ msg = "Initial MT5 connection check failed. Aborting signal processing."
929
+ self._print_exc(msg, e)
930
+ return
931
+
932
+ if not signals:
933
+ logger.info("No signals to process.")
934
+ return
935
+
936
+ # We want to create a temporary function that
937
+ # already has the 'today', 'buys', and 'sells' arguments filled in.
938
+ # This is necessary because executor.map only iterates over one sequence (signals).
939
+ signal_processor = functools.partial(
940
+ self._handle_one_signal, today=today, buys=buys, sells=sells
941
+ )
942
+ if self.multithread:
943
+ with concurrent.futures.ThreadPoolExecutor(
944
+ max_workers=max_workers
945
+ ) as executor:
946
+ # 'map' will apply our worker function to every item in the 'signals' list.
947
+ # It will automatically manage the distribution of tasks to the worker threads.
948
+ # We wrap it in list() to ensure all tasks are complete before moving on.
949
+ list(executor.map(signal_processor, signals))
950
+ else:
951
+ for signal in signals:
952
+ try:
953
+ signal_processor(signal)
954
+ except Exception as e:
955
+ self._print_exc(f"Failed to process signal {signal}: ", e)
902
956
 
903
957
  def _handle_period_end_actions(self, today):
904
958
  try:
@@ -918,7 +972,7 @@ class Mt5ExecutionEngine:
918
972
  pass
919
973
 
920
974
  def run(self):
921
- while self._running:
975
+ while self._running and not self.shutdown_event.is_set():
922
976
  try:
923
977
  check_mt5_connection(**self.kwargs)
924
978
  positions_orders = self._check_positions_orders()
@@ -936,23 +990,27 @@ class Mt5ExecutionEngine:
936
990
  self._check(buys[symbol], sells[symbol], symbol)
937
991
  else:
938
992
  self._update_risk(weights)
939
- self._handle_signals(today, signals, buys, sells)
993
+ self._handle_all_signals(today, signals, buys, sells)
940
994
  self._sleep()
941
995
  self._handle_period_end_actions(today)
942
996
  except KeyboardInterrupt:
943
997
  logger.info(
944
- f"Stopping Execution Engine for {self.STRATEGY} on {self.ACCOUNT}"
998
+ f"Stopping Execution Engine for {self.STRATEGY} STRATEGY on {self.ACCOUNT} Account"
945
999
  )
946
- break
1000
+ self.stop()
1001
+ sys.exit(0)
947
1002
  except Exception as e:
948
1003
  msg = f"Running Execution Engine, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
949
1004
  self._print_exc(msg, e)
950
- continue
951
1005
 
952
1006
  def stop(self):
953
1007
  """Stops the execution engine."""
954
- self._running = False
955
- logger.info(f"Stopping Execution Engine for {self.STRATEGY} on {self.ACCOUNT}")
1008
+ if self._running:
1009
+ logger.info(
1010
+ f"Stopping Execution Engine for {self.STRATEGY} STRATEGY on {self.ACCOUNT} Account"
1011
+ )
1012
+ self._running = False
1013
+ self.shutdown_event.set()
956
1014
  logger.info("Execution Engine stopped successfully.")
957
1015
 
958
1016
 
@@ -1000,6 +1058,7 @@ def RunMt5Engines(accounts: Dict[str, Dict], start_delay: float = 1.0):
1000
1058
 
1001
1059
  for account_id, params in accounts.items():
1002
1060
  log.info(f"Starting process for {account_id}")
1061
+ params["multithread"] = True
1003
1062
  process = mp.Process(target=RunMt5Engine, args=(account_id,), kwargs=params)
1004
1063
  process.start()
1005
1064
  processes[process] = account_id