bbstrader 0.2.91__py3-none-any.whl → 0.2.93__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.

bbstrader/models/ml.py CHANGED
@@ -243,6 +243,7 @@ class LightGBModel(object):
243
243
  end=end,
244
244
  progress=False,
245
245
  multi_level_index=False,
246
+ auto_adjust=True,
246
247
  )
247
248
  prices["symbol"] = ticker
248
249
  data.append(prices)
@@ -633,14 +634,13 @@ class LightGBModel(object):
633
634
 
634
635
  # set up cross-validation
635
636
  n_splits = int(2 * YEAR / test_length)
636
- if verbose:
637
- print(
638
- f"Lookahead: {lookahead:2.0f} | "
639
- f"Train: {train_length:3.0f} | "
640
- f"Test: {test_length:2.0f} | "
641
- f"Params: {len(cv_params_):3.0f} | "
642
- f"Train configs: {len(test_params)}"
643
- )
637
+ print(
638
+ f"Lookahead: {lookahead:2.0f} | "
639
+ f"Train: {train_length:3.0f} | "
640
+ f"Test: {test_length:2.0f} | "
641
+ f"Params: {len(cv_params_):3.0f} | "
642
+ f"Train configs: {len(test_params)}"
643
+ )
644
644
 
645
645
  # time-series cross-validation
646
646
  cv = MultipleTimeSeriesCV(
@@ -1234,7 +1234,10 @@ class LightGBModel(object):
1234
1234
  .to_frame("prediction")
1235
1235
  )
1236
1236
  tickers = predictions.index.get_level_values("symbol").unique().tolist()
1237
- return (predictions.unstack("symbol").prediction.tz_convert("UTC")), tickers
1237
+ try:
1238
+ return (predictions.unstack("symbol").prediction.tz_convert("UTC")), tickers
1239
+ except TypeError:
1240
+ return (predictions.unstack("symbol").prediction.tz_localize("UTC")), tickers
1238
1241
 
1239
1242
  def assert_last_date(self, predictions: pd.DataFrame):
1240
1243
  """
@@ -1242,9 +1245,14 @@ class LightGBModel(object):
1242
1245
  is the previous day, so it predicts today's returns.
1243
1246
  """
1244
1247
  last_date = predictions.index.get_level_values("date").max()
1245
- if last_date.tzinfo is None:
1246
- last_date = last_date.tz_localize("UTC")
1247
- last_date = last_date.normalize()
1248
+ try:
1249
+ if last_date.tzinfo is None:
1250
+ last_date = last_date.tz_localize("UTC")
1251
+ else:
1252
+ last_date = last_date.tz_convert("UTC")
1253
+ last_date = last_date.normalize()
1254
+ except Exception as e:
1255
+ print(f"Error getting last date: {e}")
1248
1256
  try:
1249
1257
  days = 3 if datetime.now().strftime("%A") == "Monday" else 1
1250
1258
  td = (
@@ -1,16 +1,23 @@
1
+ import multiprocessing as mp
1
2
  import time
2
3
  import traceback
3
- import MetaTrader5 as MT5
4
4
  from datetime import datetime
5
- from logging import Logger
6
5
  from typing import Dict, List, Literal, Optional
7
6
 
7
+ from loguru import logger as log
8
+
8
9
  from bbstrader.btengine.strategy import MT5Strategy, Strategy
9
- from bbstrader.metatrader.account import check_mt5_connection
10
+ from bbstrader.config import BBSTRADER_DIR
11
+ from bbstrader.core.utils import TradeAction
12
+ from bbstrader.metatrader.account import Account, check_mt5_connection
10
13
  from bbstrader.metatrader.trade import Trade
11
- from bbstrader.metatrader.account import Account
12
14
  from bbstrader.trading.scripts import send_message
13
- from bbstrader.core.utils import TradeAction
15
+
16
+ try:
17
+ import MetaTrader5 as MT5
18
+ except ImportError:
19
+ import bbstrader.compat # noqa: F401
20
+
14
21
 
15
22
  __all__ = ["MT5ExecutionEngine", "TWSExecutionEngine"]
16
23
 
@@ -31,6 +38,7 @@ _TF_MAPPING = {
31
38
  }
32
39
 
33
40
  TradingDays = ["monday", "tuesday", "wednesday", "thursday", "friday"]
41
+ WEEK_DAYS = TradingDays + ["saturday", "sunday"]
34
42
 
35
43
  BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
36
44
  SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
@@ -77,6 +85,13 @@ NON_EXEC_RETCODES = {
77
85
  "SMKT": [MT5.TRADE_RETCODE_LONG_ONLY] + COMMON_RETCODES,
78
86
  }
79
87
 
88
+ log.add(
89
+ f"{BBSTRADER_DIR}/logs/execution.log",
90
+ enqueue=True,
91
+ level="INFO",
92
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
93
+ )
94
+
80
95
 
81
96
  def _mt5_execution(
82
97
  symbol_list,
@@ -99,18 +114,22 @@ def _mt5_execution(
99
114
  comment,
100
115
  **kwargs,
101
116
  ):
117
+ logger = kwargs.get("logger", log)
118
+
102
119
  def _print_exc(dm, msg):
103
120
  traceback.print_exc() if dm else logger.error(msg)
104
121
 
105
122
  try:
106
123
  symbols = symbol_list.copy()
107
124
  time_frame = kwargs.get("time_frame", "15m")
125
+ daily_risk = kwargs.get("daily_risk")
108
126
  STRATEGY = kwargs.get("strategy_name")
109
127
  mtrades = kwargs.get("max_trades")
110
128
  notify = kwargs.get("notify", False)
111
129
  signal_tickers = kwargs.get("signal_tickers", symbols)
112
130
  debug_mode = kwargs.get("debug_mode", False)
113
131
  delay = kwargs.get("delay", 0)
132
+ ACCOUNT = kwargs.get("account", "MT5 Account")
114
133
  if notify:
115
134
  telegram = kwargs.get("telegram", False)
116
135
  bot_token = kwargs.get("bot_token")
@@ -124,9 +143,6 @@ def _mt5_execution(
124
143
  elif isinstance(expert_ids, int):
125
144
  expert_ids = [expert_ids]
126
145
 
127
- logger: Logger = kwargs.get("logger")
128
- if logger is None:
129
- logger: Logger = trades_instances[symbols[0]].logger
130
146
  max_trades = {
131
147
  symbol: mtrades[symbol]
132
148
  if mtrades is not None and symbol in mtrades
@@ -136,8 +152,14 @@ def _mt5_execution(
136
152
  if comment is None:
137
153
  trade = trades_instances[symbols[0]]
138
154
  comment = f"{trade.expert_name}@{trade.version}"
155
+
156
+ if period.lower() == "24/7":
157
+ trading_days = WEEK_DAYS
139
158
  except Exception:
140
- _print_exc(debug_mode, f"Initializing Execution Engine, STRATEGY={STRATEGY}")
159
+ _print_exc(
160
+ debug_mode,
161
+ f"Initializing Execution Engine, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}",
162
+ )
141
163
  return
142
164
 
143
165
  def update_risk(weights):
@@ -146,7 +168,8 @@ def _mt5_execution(
146
168
  if symbol not in weights:
147
169
  continue
148
170
  trade = trades_instances[symbol]
149
- trade.dailydd = round(weights[symbol], 5)
171
+ dailydd = round(weights[symbol] * daily_risk, 5)
172
+ trade.dailydd = dailydd
150
173
 
151
174
  def check_retcode(trade: Trade, position):
152
175
  if len(trade.retcodes) > 0:
@@ -181,25 +204,29 @@ def _mt5_execution(
181
204
  check_mt5_connection(**kwargs)
182
205
  strategy: MT5Strategy = strategy_cls(symbol_list=symbols, mode="live", **kwargs)
183
206
  except Exception:
184
- _print_exc(debug_mode, f"Initializing strategy, STRATEGY={STRATEGY}")
207
+ _print_exc(
208
+ debug_mode, f"Initializing strategy, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}"
209
+ )
185
210
  return
186
- logger.info(f"Running {STRATEGY} Strategy in {time_frame} Interval ...")
211
+ logger.info(
212
+ f"Running {STRATEGY} Strategy in {time_frame} Interval ..., ACCOUNT={ACCOUNT}"
213
+ )
187
214
 
188
215
  def run_trade_algorithm(signal, symbol, id, trade: Trade, price, stoplimit):
189
216
  signal = "BMKT" if signal == "LONG" or signal == "BUY" else signal
190
217
  signal = "SMKT" if signal == "SHORT" or signal == "SELL" else signal
191
- info = f"SIGNAL = {signal}, SYMBOL={symbol}, STRATEGY={STRATEGY}, TIMEFRAME={time_frame}"
218
+ info = f"SIGNAL = {signal}, SYMBOL={symbol}, STRATEGY={STRATEGY}, TIMEFRAME={time_frame} , ACCOUNT={ACCOUNT}"
192
219
  account = Account(**kwargs)
193
220
  symbol_type = account.get_symbol_type(symbol)
194
221
  desc = account.get_symbol_info(symbol).description
195
222
  sigmsg = (
196
223
  f"SIGNAL = {signal}, \nSYMBOL={symbol}, \nTYPE={symbol_type}, \nDESCRIPTION={desc}, "
197
- f"\nPRICE={price}, \nSTOPLIMIT={stoplimit}, \nSTRATEGY={STRATEGY}, \nTIMEFRAME={time_frame}"
224
+ f"\nPRICE={price}, \nSTOPLIMIT={stoplimit}, \nSTRATEGY={STRATEGY}, \nTIMEFRAME={time_frame},"
198
225
  f"\nBROKER={account.broker.name}, \nTIMESTAMP={datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
199
226
  )
200
- msg = f"Sending {signal} Order ... SYMBOL={symbol}, STRATEGY={STRATEGY}"
201
- tfmsg = f"Time Frame Not completed !!! SYMBOL={symbol}, STRATEGY={STRATEGY}"
202
- riskmsg = f"Risk not allowed !!! SYMBOL={symbol}, STRATEGY={STRATEGY}"
227
+ msg = f"Sending {signal} Order ... SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
228
+ tfmsg = f"Time Frame Not completed !!! SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
229
+ riskmsg = f"Risk not allowed !!! SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
203
230
  if signal not in EXIT_SIGNAL_ACTIONS:
204
231
  if signal in NON_EXEC_RETCODES and not check_retcode(trade, signal):
205
232
  logger.info(info)
@@ -231,6 +258,7 @@ def _mt5_execution(
231
258
  stoplimit=stoplimit,
232
259
  id=id,
233
260
  mm=mm,
261
+ trail=trail,
234
262
  comment=comment,
235
263
  )
236
264
  else:
@@ -247,6 +275,7 @@ def _mt5_execution(
247
275
  stoplimit=stoplimit,
248
276
  id=id,
249
277
  mm=mm,
278
+ trail=trail,
250
279
  comment=comment,
251
280
  )
252
281
 
@@ -266,6 +295,7 @@ def _mt5_execution(
266
295
  stoplimit=stoplimit,
267
296
  id=id,
268
297
  mm=mm,
298
+ trail=trail,
269
299
  comment=comment,
270
300
  )
271
301
  else:
@@ -282,6 +312,7 @@ def _mt5_execution(
282
312
  stoplimit=stoplimit,
283
313
  id=id,
284
314
  mm=mm,
315
+ trail=trail,
285
316
  comment=comment,
286
317
  )
287
318
 
@@ -324,7 +355,7 @@ def _mt5_execution(
324
355
  if show_positions_orders:
325
356
  logger.info(
326
357
  f"Current {type.upper()} SYMBOL={symbol}: \
327
- {positions_orders[type][symbol]}, STRATEGY={STRATEGY}"
358
+ {positions_orders[type][symbol]}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
328
359
  )
329
360
  long_market = {
330
361
  symbol: buys[symbol] is not None
@@ -338,7 +369,8 @@ def _mt5_execution(
338
369
  }
339
370
  except Exception:
340
371
  _print_exc(
341
- debug_mode, f"Checking positions and orders, STRATEGY={STRATEGY}"
372
+ debug_mode,
373
+ f"Checking positions and orders, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}",
342
374
  )
343
375
  continue
344
376
  time.sleep(0.5)
@@ -352,7 +384,10 @@ def _mt5_execution(
352
384
  )
353
385
  update_risk(weights)
354
386
  except Exception:
355
- _print_exc(debug_mode, f"Calculating Signals, STRATEGY={STRATEGY}")
387
+ _print_exc(
388
+ debug_mode,
389
+ f"Calculating Signals, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}",
390
+ )
356
391
  continue
357
392
  if len(signals) == 0:
358
393
  for symbol in symbols:
@@ -382,16 +417,16 @@ def _mt5_execution(
382
417
  if len(symbols) >= 10:
383
418
  if symbol == symbols[-1]:
384
419
  logger.info(
385
- f"Not trading Time !!!, STRATEGY={STRATEGY}"
420
+ f"Not trading Time !!!, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
386
421
  )
387
422
  else:
388
423
  logger.info(
389
- f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
424
+ f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
390
425
  )
391
426
  check(buys[symbol], sells[symbol], symbol)
392
427
 
393
428
  except Exception:
394
- msg = f"Handling Signals, SYMBOL={symbol}, STRATEGY={STRATEGY}"
429
+ msg = f"Handling Signals, SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
395
430
  _print_exc(debug_mode, msg)
396
431
  continue
397
432
 
@@ -420,20 +455,40 @@ def _mt5_execution(
420
455
 
421
456
  def logmsg(period, symbol):
422
457
  logger.info(
423
- f"End of the {period} !!! SYMBOL={symbol}, STRATEGY={STRATEGY}"
458
+ f"End of the {period} !!! SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
424
459
  )
425
460
 
426
461
  def logmsgif(period, symbol):
427
462
  if len(symbols) <= 10:
428
463
  logmsg(period, symbol)
429
464
  elif len(symbols) > 10 and symbol == symbols[-1]:
430
- logger.info(f"End of the {period} !!! STRATEGY={STRATEGY}")
465
+ logger.info(
466
+ f"End of the {period} !!! STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
467
+ )
431
468
 
432
469
  def sleepmsg(sleep_time):
433
- logger.info(f"Sleeping for {sleep_time} minutes ...\n")
470
+ logger.info(f"{ACCOUNT} Sleeping for {sleep_time} minutes ...\n")
434
471
 
435
- sessionmsg = "STARTING NEW TRADING SESSION ...\n"
436
- if period.lower() == "day":
472
+ sessionmsg = f"{ACCOUNT} STARTING NEW TRADING SESSION ...\n"
473
+ if period.lower() == "24/7": # For Cryptos
474
+ for symbol in symbols:
475
+ trade = trades_instances[symbol]
476
+ if trade.days_end() and closing:
477
+ for id in expert_ids:
478
+ trade.close_positions(
479
+ position_type="all", id=id, comment=comment
480
+ )
481
+ logmsgif("Day", symbol)
482
+ trade.statistics(save=True)
483
+ if day_end:
484
+ strategy.perform_period_end_checks()
485
+ sleep_time = trades_instances[symbols[-1]].sleep_time()
486
+ sleepmsg(sleep_time + delay)
487
+ time.sleep(60 * sleep_time + delay)
488
+ logger.info(sessionmsg)
489
+ time_intervals = 0
490
+
491
+ elif period.lower() == "day":
437
492
  for symbol in symbols:
438
493
  trade = trades_instances[symbol]
439
494
  if trade.days_end() and closing:
@@ -455,6 +510,7 @@ def _mt5_execution(
455
510
  sleepmsg(sleep_time + delay)
456
511
  time.sleep(60 * sleep_time + delay)
457
512
  logger.info(sessionmsg)
513
+ time_intervals = 0
458
514
  elif period_end_action == "sleep" and today == FRIDAY:
459
515
  sleep_time = trades_instances[symbols[-1]].sleep_time(
460
516
  weekend=True
@@ -462,6 +518,7 @@ def _mt5_execution(
462
518
  sleepmsg(sleep_time + delay)
463
519
  time.sleep(60 * sleep_time + delay)
464
520
  logger.info(sessionmsg)
521
+ time_intervals = 0
465
522
 
466
523
  elif period.lower() == "week":
467
524
  for symbol in symbols:
@@ -482,6 +539,7 @@ def _mt5_execution(
482
539
  sleepmsg(sleep_time + delay)
483
540
  time.sleep(60 * sleep_time + delay)
484
541
  logger.info(sessionmsg)
542
+ time_intervals = 0
485
543
  elif day_end and today == FRIDAY:
486
544
  strategy.perform_period_end_checks()
487
545
  if period_end_action == "break" and closing:
@@ -493,6 +551,7 @@ def _mt5_execution(
493
551
  sleepmsg(sleep_time + delay)
494
552
  time.sleep(60 * sleep_time + delay)
495
553
  logger.info(sessionmsg)
554
+ time_intervals = 0
496
555
 
497
556
  elif period.lower() == "month":
498
557
  for symbol in symbols:
@@ -502,9 +561,7 @@ def _mt5_execution(
502
561
  elif trade.days_end() and today == FRIDAY:
503
562
  logmsgif("Week", symbol)
504
563
  elif (
505
- trade.days_end()
506
- and today == FRIDAY
507
- and num_days >= 20
564
+ trade.days_end() and today == FRIDAY and num_days >= 20
508
565
  ) and closing:
509
566
  for id in expert_ids:
510
567
  trade.close_positions(
@@ -517,18 +574,22 @@ def _mt5_execution(
517
574
  sleepmsg(sleep_time + delay)
518
575
  time.sleep(60 * sleep_time + delay)
519
576
  logger.info(sessionmsg)
577
+ time_intervals = 0
520
578
  num_days += 1
521
579
  elif day_end and today == FRIDAY:
522
580
  sleep_time = trades_instances[symbols[-1]].sleep_time(weekend=True)
523
581
  sleepmsg(sleep_time + delay)
524
582
  time.sleep(60 * sleep_time + delay)
525
583
  logger.info(sessionmsg)
584
+ time_intervals = 0
526
585
  num_days += 1
527
586
  elif day_end and today == FRIDAY and num_days >= 20:
528
587
  strategy.perform_period_end_checks()
529
588
  break
530
589
  except Exception:
531
- msg = f"Handling period end actions, STRATEGY={STRATEGY}"
590
+ msg = (
591
+ f"Handling period end actions, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
592
+ )
532
593
  _print_exc(debug_mode, msg)
533
594
  continue
534
595
 
@@ -638,7 +699,7 @@ class MT5ExecutionEngine:
638
699
  symbol_list : List of symbols to trade
639
700
  trades_instances : Dictionary of Trade instances
640
701
  strategy_cls : Strategy class to use for trading
641
- mm : Enable Money Management. Defaults to False.
702
+ mm : Enable Money Management. Defaults to True.
642
703
  optimizer : Risk management optimizer. Defaults to 'equal'.
643
704
  See `bbstrader.models.optimization` module for more information.
644
705
  show_positions_orders : Print open positions and orders. Defaults to False.
@@ -726,4 +787,60 @@ class MT5ExecutionEngine:
726
787
  )
727
788
 
728
789
 
790
+ def mt5_engine(account_id: str, **kwargs):
791
+ """Starts an MT5 execution engine for a given account.
792
+ Args:
793
+ account_id: Account ID to run the execution engine on.
794
+ **kwargs: Additional keyword arguments
795
+ _ symbol_list : List of symbols to trade.
796
+ - trades_instances : Dictionary of Trade instances.
797
+ - strategy_cls : Strategy class to use for trading.
798
+ """
799
+ log.info(f"Starting execution engine for {account_id}")
800
+
801
+ symbol_list = kwargs.pop("symbol_list")
802
+ trades_instances = kwargs.pop("trades_instances")
803
+ strategy_cls = kwargs.pop("strategy_cls")
804
+
805
+ if symbol_list is None or trades_instances is None or strategy_cls is None:
806
+ log.error(f"Missing required arguments for account {account_id}")
807
+ raise ValueError(f"Missing required arguments for account {account_id}")
808
+
809
+ try:
810
+ engine = MT5ExecutionEngine(
811
+ symbol_list, trades_instances, strategy_cls, **kwargs
812
+ )
813
+ engine.run()
814
+ except Exception as e:
815
+ log.exception(f"Error running execution engine for {account_id}: {e}")
816
+ finally:
817
+ log.info(f"Execution engine for {account_id} completed")
818
+
819
+
820
+ def RunMt5Engines(accounts: Dict[str, Dict], start_delay: float = 1.0):
821
+ """Runs multiple MT5 execution engines in parallel using multiprocessing.
822
+
823
+ Args:
824
+ accounts: Dictionary of accounts to run the execution engines on.
825
+ Keys are the account names or IDs and values are the parameters for the execution engine.
826
+ The parameters are the same as the ones passed to the `MT5ExecutionEngine` class.
827
+ start_delay: Delay in seconds between starting the processes. Defaults to 1.0.
828
+ """
829
+
830
+ processes = {}
831
+
832
+ for account_id, params in accounts.items():
833
+ log.info(f"Starting process for {account_id}")
834
+ process = mp.Process(target=mt5_engine, args=(account_id,), kwargs=params)
835
+ process.start()
836
+ processes[process] = account_id
837
+
838
+ if start_delay:
839
+ time.sleep(start_delay)
840
+
841
+ for process, account_id in processes.items():
842
+ process.join()
843
+ log.info(f"Process for {account_id} joined")
844
+
845
+
729
846
  class TWSExecutionEngine: ...
@@ -0,0 +1,155 @@
1
+ import argparse
2
+ import json
3
+ import multiprocessing as mp
4
+ import os
5
+ import sys
6
+
7
+ from bbstrader.btengine import MT5Strategy, Strategy
8
+ from bbstrader.core.utils import load_class, load_module
9
+ from bbstrader.metatrader.trade import create_trade_instance
10
+ from bbstrader.trading.execution import mt5_engine
11
+
12
+ EXECUTION_PATH = os.path.expanduser("~/.bbstrader/execution/execution.py")
13
+ CONFIG_PATH = os.path.expanduser("~/.bbstrader/execution/execution.json")
14
+
15
+
16
+ def load_config(config_path, strategy_name, account=None):
17
+ if not os.path.exists(config_path):
18
+ raise FileNotFoundError(f"Configuration file not found at {config_path}")
19
+ with open(config_path, "r") as f:
20
+ config = json.load(f)
21
+ try:
22
+ config = config[strategy_name]
23
+ except KeyError:
24
+ raise ValueError(
25
+ f"Strategy {strategy_name} not found in the configuration file."
26
+ )
27
+ if account is not None:
28
+ try:
29
+ config = config[account]
30
+ except KeyError:
31
+ raise ValueError(f"Account {account} not found in the configuration file.")
32
+ if config.get("symbol_list") is None:
33
+ raise ValueError("symbol_list is required in the configuration file.")
34
+ if config.get("trades_kwargs") is None:
35
+ raise ValueError("trades_kwargs is required in the configuration file.")
36
+ return config
37
+
38
+
39
+ def worker_function(account, args):
40
+ strategy_module = load_module(args.path)
41
+ strategy_class = load_class(strategy_module, args.strategy, (MT5Strategy, Strategy))
42
+
43
+ config = load_config(args.config, args.strategy, account)
44
+ symbol_list = config.pop("symbol_list")
45
+ trades_kwargs = config.pop("trades_kwargs")
46
+ trades = create_trade_instance(symbol_list, trades_kwargs)
47
+
48
+ kwargs = {
49
+ "symbol_list": symbol_list,
50
+ "trades_instances": trades,
51
+ "strategy_cls": strategy_class,
52
+ "account": account,
53
+ **config,
54
+ }
55
+ mt5_engine(account, **kwargs)
56
+
57
+
58
+ def mt5_terminal(args):
59
+ if args.parallel:
60
+ if len(args.account) == 0:
61
+ raise ValueError(
62
+ "account or accounts are required when running in parallel"
63
+ )
64
+
65
+ processes = []
66
+ try:
67
+ for account in args.account:
68
+ p = mp.Process(target=worker_function, args=(account, args))
69
+ p.start()
70
+ processes.append(p)
71
+
72
+ for p in processes:
73
+ p.join()
74
+ except Exception as e:
75
+ print(f"Error in parallel execution: {e}")
76
+ raise e
77
+ except KeyboardInterrupt:
78
+ print("\nTerminating Execution...")
79
+ for p in processes:
80
+ p.terminate()
81
+ for p in processes:
82
+ p.join()
83
+ print("Execution terminated")
84
+ else:
85
+ worker_function(None, args)
86
+
87
+
88
+ def tws_terminal(args):
89
+ raise NotImplementedError("TWS terminal is not implemented yet")
90
+
91
+
92
+ def execute_strategy(unknown):
93
+ HELP_MSG = """
94
+ Execute a strategy on one or multiple MT5 accounts.
95
+
96
+ Usage:
97
+ python -m bbstrader --run execution [options]
98
+
99
+ Options:
100
+ -s, --strategy: Strategy class name to run
101
+ -a, --account: Account(s) name(s) or ID(s) to run the strategy on (must be the same as in the configuration file)
102
+ -p, --path: Path to the execution file (default: ~/.bbstrader/execution/execution.py)
103
+ -c, --config: Path to the configuration file (default: ~/.bbstrader/execution/execution.json)
104
+ -l, --parallel: Run the strategy in parallel (default: False)
105
+ -t, --terminal: Terminal to use (default: MT5)
106
+ -h, --help: Show this help message and exit
107
+
108
+ Note:
109
+ The configuration file must contain all the required parameters
110
+ to create trade instances for each account and strategy.
111
+ The configuration file must be a dictionary with the following structure:
112
+ If parallel is True:
113
+ {
114
+ "strategy_name": {
115
+ "account_name": {
116
+ "symbol_list": ["symbol1", "symbol2"],
117
+ "trades_kwargs": {"param1": "value1", "param2": "value2"}
118
+ **other_parameters (for the strategy and the execution engine)
119
+ }
120
+ }
121
+ }
122
+ If parallel is False:
123
+ {
124
+ "strategy_name": {
125
+ "symbol_list": ["symbol1", "symbol2"],
126
+ "trades_kwargs": {"param1": "value1", "param2": "value2"}
127
+ **other_parameters (for the strategy and the execution engine)
128
+ }
129
+ }
130
+ See bbstrader.metatrader.trade.create_trade_instance for more details on the trades_kwargs.
131
+ See bbstrader.trading.execution.MT5ExecutionEngine for more details on the other parameters.
132
+
133
+ All other paramaters must be python built-in types.
134
+ If you have custom type you must set them in your strategy class
135
+ or run the MT5ExecutionEngine directly, don't run on CLI.
136
+ """
137
+ if "-h" in unknown or "--help" in unknown:
138
+ print(HELP_MSG)
139
+ sys.exit(0)
140
+
141
+ parser = argparse.ArgumentParser()
142
+ parser.add_argument("-s", "--strategy", type=str, required=True)
143
+ parser.add_argument("-a", "--account", type=str, nargs="*", default=[])
144
+ parser.add_argument("-p", "--path", type=str, default=EXECUTION_PATH)
145
+ parser.add_argument("-c", "--config", type=str, default=CONFIG_PATH)
146
+ parser.add_argument("-l", "--parallel", action="store_true")
147
+ parser.add_argument(
148
+ "-t", "--terminal", type=str, default="MT5", choices=["MT5", "TWS"]
149
+ )
150
+ args = parser.parse_args(unknown)
151
+
152
+ if args.terminal == "MT5":
153
+ mt5_terminal(args)
154
+ elif args.terminal == "TWS":
155
+ tws_terminal(args)
@@ -4,6 +4,7 @@ from notifypy import Notify
4
4
  from telegram import Bot
5
5
  from telegram.error import TelegramError
6
6
 
7
+
7
8
  __all__ = ["send_telegram_message", "send_notification", "send_message"]
8
9
 
9
10
 
@@ -65,3 +66,4 @@ def send_message(
65
66
  if token is None or chat_id is None:
66
67
  raise ValueError("Token and chat_id must be provided")
67
68
  asyncio.run(send_telegram_message(token, chat_id, text=message))
69
+