bbstrader 0.2.97__py3-none-any.whl → 0.2.99__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/__main__.py CHANGED
@@ -16,7 +16,7 @@ USAGE_TEXT = """
16
16
  Modules:
17
17
  copier: Copy trades from one MetaTrader account to another or multiple accounts
18
18
  backtest: Backtest a strategy, see bbstrader.btengine.backtest.run_backtest
19
- execution: Execute a strategy, see bbstrader.trading.execution.MT5ExecutionEngine
19
+ execution: Execute a strategy, see bbstrader.trading.execution.Mt5ExecutionEngine
20
20
 
21
21
  python -m bbstrader --run <module> --help for more information on the module
22
22
  """
@@ -4,7 +4,7 @@ from datetime import datetime
4
4
  from typing import List, Literal, Optional
5
5
 
6
6
  from tabulate import tabulate
7
-
7
+ from bbstrader.btengine.event import Events
8
8
  from bbstrader.btengine.data import DataHandler
9
9
  from bbstrader.btengine.execution import ExecutionHandler, SimExecutionHandler
10
10
  from bbstrader.btengine.portfolio import Portfolio
@@ -165,19 +165,19 @@ class BacktestEngine(Backtest):
165
165
  break
166
166
  else:
167
167
  if event is not None:
168
- if event.type == "MARKET":
168
+ if event.type == Events.MARKET:
169
169
  self.strategy.calculate_signals(event)
170
170
  self.portfolio.update_timeindex(event)
171
171
 
172
- elif event.type == "SIGNAL":
172
+ elif event.type == Events.SIGNAL:
173
173
  self.signals += 1
174
174
  self.portfolio.update_signal(event)
175
175
 
176
- elif event.type == "ORDER":
176
+ elif event.type == Events.ORDER:
177
177
  self.orders += 1
178
178
  self.execution_handler.execute_order(event)
179
179
 
180
- elif event.type == "FILL":
180
+ elif event.type == Events.FILL:
181
181
  self.fills += 1
182
182
  self.portfolio.update_fill(event)
183
183
  self.strategy.update_trades_from_fill(event)
@@ -354,7 +354,7 @@ def run_backtest_with(engine: Literal["bbstrader", "cerebro", "zipline"], **kwar
354
354
  )
355
355
  elif engine == "cerebro":
356
356
  # TODO:
357
- pass
357
+ raise NotImplementedError("cerebro engine is not supported yet")
358
358
  elif engine == "zipline":
359
359
  # TODO:
360
- pass
360
+ raise NotImplementedError("zipline engine is not supported yet")
@@ -1,4 +1,5 @@
1
1
  from datetime import datetime
2
+ from enum import Enum
2
3
  from typing import Literal
3
4
 
4
5
  __all__ = ["Event", "MarketEvent", "SignalEvent", "OrderEvent", "FillEvent"]
@@ -18,6 +19,13 @@ class Event(object):
18
19
  ...
19
20
 
20
21
 
22
+ class Events(Enum):
23
+ MARKET = "MARKET"
24
+ SIGNAL = "SIGNAL"
25
+ ORDER = "ORDER"
26
+ FILL = "FILL"
27
+
28
+
21
29
  class MarketEvent(Event):
22
30
  """
23
31
  Market Events are triggered when the outer while loop of the backtesting
@@ -32,7 +40,7 @@ class MarketEvent(Event):
32
40
  """
33
41
  Initialises the MarketEvent.
34
42
  """
35
- self.type = "MARKET"
43
+ self.type = Events.MARKET
36
44
 
37
45
 
38
46
  class SignalEvent(Event):
@@ -72,7 +80,7 @@ class SignalEvent(Event):
72
80
  price (int | float): An optional price to be used when the signal is generated.
73
81
  stoplimit (int | float): An optional stop-limit price for the signal
74
82
  """
75
- self.type = "SIGNAL"
83
+ self.type = Events.SIGNAL
76
84
  self.strategy_id = strategy_id
77
85
  self.symbol = symbol
78
86
  self.datetime = datetime
@@ -118,7 +126,7 @@ class OrderEvent(Event):
118
126
  price (int | float): The price at which to order.
119
127
  signal (str): The signal that generated the order.
120
128
  """
121
- self.type = "ORDER"
129
+ self.type = Events.ORDER
122
130
  self.symbol = symbol
123
131
  self.order_type = order_type
124
132
  self.quantity = quantity
@@ -191,7 +199,7 @@ class FillEvent(Event):
191
199
  commission (float | None): An optional commission sent from IB.
192
200
  order (str): The order that this fill is related
193
201
  """
194
- self.type = "FILL"
202
+ self.type = Events.FILL
195
203
  self.timeindex = timeindex
196
204
  self.symbol = symbol
197
205
  self.exchange = exchange
@@ -4,7 +4,7 @@ from queue import Queue
4
4
  from loguru import logger
5
5
 
6
6
  from bbstrader.btengine.data import DataHandler
7
- from bbstrader.btengine.event import FillEvent, OrderEvent
7
+ from bbstrader.btengine.event import Events, FillEvent, OrderEvent
8
8
  from bbstrader.config import BBSTRADER_DIR
9
9
  from bbstrader.metatrader.account import Account
10
10
 
@@ -80,7 +80,7 @@ class SimExecutionHandler(ExecutionHandler):
80
80
  Args:
81
81
  event (OrderEvent): Contains an Event object with order information.
82
82
  """
83
- if event.type == "ORDER":
83
+ if event.type == Events.ORDER:
84
84
  dtime = self.bardata.get_latest_bar_datetime(event.symbol)
85
85
  fill_event = FillEvent(
86
86
  timeindex=dtime,
@@ -233,7 +233,7 @@ class MT5ExecutionHandler(ExecutionHandler):
233
233
  Args:
234
234
  event (OrderEvent): Contains an Event object with order information.
235
235
  """
236
- if event.type == "ORDER":
236
+ if event.type == Events.ORDER:
237
237
  symbol = event.symbol
238
238
  direction = event.direction
239
239
  quantity = event.quantity
@@ -6,7 +6,7 @@ import pandas as pd
6
6
  import quantstats as qs
7
7
 
8
8
  from bbstrader.btengine.data import DataHandler
9
- from bbstrader.btengine.event import FillEvent, MarketEvent, OrderEvent, SignalEvent
9
+ from bbstrader.btengine.event import Events, FillEvent, MarketEvent, OrderEvent, SignalEvent
10
10
  from bbstrader.btengine.performance import (
11
11
  create_drawdowns,
12
12
  create_sharpe_ratio,
@@ -282,7 +282,7 @@ class Portfolio(object):
282
282
  Updates the portfolio current positions and holdings
283
283
  from a FillEvent.
284
284
  """
285
- if event.type == "FILL":
285
+ if event.type == Events.FILL:
286
286
  self.update_positions_from_fill(event)
287
287
  self.update_holdings_from_fill(event)
288
288
 
@@ -337,7 +337,7 @@ class Portfolio(object):
337
337
  Acts on a SignalEvent to generate new orders
338
338
  based on the portfolio logic.
339
339
  """
340
- if event.type == "SIGNAL":
340
+ if event.type == Events.SIGNAL:
341
341
  order_event = self.generate_order(event)
342
342
  self.events.put(order_event)
343
343
 
@@ -10,15 +10,15 @@ import pytz
10
10
  from loguru import logger
11
11
 
12
12
  from bbstrader.btengine.data import DataHandler
13
- from bbstrader.btengine.event import FillEvent, SignalEvent
13
+ from bbstrader.btengine.event import Events, FillEvent, SignalEvent
14
14
  from bbstrader.config import BBSTRADER_DIR
15
- from bbstrader.metatrader.trade import TradeSignal
16
15
  from bbstrader.metatrader.account import (
17
16
  Account,
18
17
  AdmiralMarktsGroup,
19
18
  PepperstoneGroupLimited,
20
19
  )
21
20
  from bbstrader.metatrader.rates import Rates
21
+ from bbstrader.metatrader.trade import TradeSignal
22
22
  from bbstrader.models.optimization import optimized_weights
23
23
 
24
24
  __all__ = ["Strategy", "MT5Strategy"]
@@ -67,7 +67,7 @@ class MT5Strategy(Strategy):
67
67
  """
68
68
  A `MT5Strategy()` object is a subclass of `Strategy` that is used to
69
69
  calculate signals for the MetaTrader 5 trading platform. The signals
70
- are generated by the `MT5Strategy` object and sent to the the `MT5ExecutionEngine`
70
+ are generated by the `MT5Strategy` object and sent to the the `Mt5ExecutionEngine`
71
71
  for live trading and `MT5BacktestEngine` objects for backtesting.
72
72
  """
73
73
 
@@ -146,9 +146,9 @@ class MT5Strategy(Strategy):
146
146
  def _initialize_portfolio(self):
147
147
  positions = ["LONG", "SHORT"]
148
148
  orders = ["BLMT", "BSTP", "BSTPLMT", "SLMT", "SSTP", "SSTPLMT"]
149
+ self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
149
150
  self._positions: Dict[str, Dict[str, int | float]] = {}
150
151
  self._trades: Dict[str, Dict[str, int]] = {}
151
- self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
152
152
  for symbol in self.symbols:
153
153
  self._positions[symbol] = {}
154
154
  self._orders[symbol] = {}
@@ -188,7 +188,7 @@ class MT5Strategy(Strategy):
188
188
  This method updates the trades for the strategy based on the fill event.
189
189
  It is used to keep track of the number of trades executed for each order.
190
190
  """
191
- if event.type == "FILL":
191
+ if event.type == Events.FILL:
192
192
  if event.order != "EXIT":
193
193
  self._trades[event.symbol][event.order] += 1
194
194
  elif event.order == "EXIT" and event.direction == "BUY":
@@ -206,6 +206,7 @@ class MT5Strategy(Strategy):
206
206
  - ``price``: The price at which to execute the action, used for pending orders.
207
207
  - ``stoplimit``: The stop-limit price for STOP-LIMIT orders, used for pending stop limit orders.
208
208
  - ``id``: The unique identifier for the strategy or order.
209
+ - ``comment``: An optional comment or description related to the trade signal.
209
210
  """
210
211
  pass
211
212
 
@@ -677,6 +678,12 @@ class MT5Strategy(Strategy):
677
678
  if period_count == 0 or period_count is None:
678
679
  return True
679
680
  return period_count % signal_inverval == 0
681
+
682
+ @staticmethod
683
+ def stop_time(time_zone: str, stop_time: str) -> bool:
684
+ now = datetime.now(pytz.timezone(time_zone)).time()
685
+ stop_time = datetime.strptime(stop_time, "%H:%M").time()
686
+ return now >= stop_time
680
687
 
681
688
  def ispositions(
682
689
  self, symbol, strategy_id, position, max_trades, one_true=False, account=None
@@ -9,6 +9,7 @@ from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
9
9
 
10
10
  from bbstrader.metatrader.utils import (
11
11
  AccountInfo,
12
+ BookInfo,
12
13
  InvalidBroker,
13
14
  OrderCheckResult,
14
15
  OrderSentResult,
@@ -188,10 +189,12 @@ def check_mt5_connection(**kwargs):
188
189
  except Exception:
189
190
  raise_mt5_error(INIT_MSG)
190
191
 
192
+
191
193
  def shutdown_mt5():
192
194
  """Close the connection to the MetaTrader 5 terminal."""
193
195
  mt5.shutdown()
194
-
196
+
197
+
195
198
  class Broker(object):
196
199
  def __init__(self, name: str = None, **kwargs):
197
200
  if name is None:
@@ -323,7 +326,7 @@ class Account(object):
323
326
  f"For {supported['FTMO'].name}, See [{ftmo_url}]\n"
324
327
  )
325
328
  raise InvalidBroker(message=msg)
326
-
329
+
327
330
  def shutdown(self):
328
331
  """Close the connection to the MetaTrader 5 terminal."""
329
332
  shutdown_mt5()
@@ -1037,6 +1040,29 @@ class Account(object):
1037
1040
  """
1038
1041
  self._show_info(self.get_tick_info, "tick", symbol=symbol)
1039
1042
 
1043
+ def get_market_book(self, symbol: str) -> Union[Tuple[BookInfo], None]:
1044
+ """
1045
+ Get the Market Depth content for a specific symbol.
1046
+ Args:
1047
+ symbol (str): Financial instrument name. Required unnamed parameter.
1048
+ The symbol name should be specified in the same format as in the Market Watch window.
1049
+
1050
+ Returns:
1051
+ The Market Depth content as a tuple from BookInfo entries featuring order type, price and volume in lots.
1052
+ Return None in case of an error.
1053
+
1054
+ Raises:
1055
+ MT5TerminalError: A specific exception based on the error code.
1056
+ """
1057
+ try:
1058
+ book = mt5.market_book_get(symbol)
1059
+ if book is None:
1060
+ return None
1061
+ else:
1062
+ return tuple([BookInfo(**entry._asdict()) for entry in book])
1063
+ except Exception as e:
1064
+ raise_mt5_error(e)
1065
+
1040
1066
  def calculate_margin(
1041
1067
  self, action: Literal["buy", "sell"], symbol: str, lot: float, price: float
1042
1068
  ) -> float:
@@ -1299,8 +1325,7 @@ class Account(object):
1299
1325
  df.drop(["time_msc", "external_id"], axis=1, inplace=True)
1300
1326
  df.set_index("time", inplace=True)
1301
1327
  if save:
1302
- file = "trade_history.csv"
1303
- df.to_csv(file)
1328
+ df.to_csv("trades_history.csv")
1304
1329
  if to_df:
1305
1330
  return df
1306
1331
  else:
@@ -1507,8 +1532,7 @@ class Account(object):
1507
1532
  df["time_done"] = pd.to_datetime(df["time_done"], unit="s")
1508
1533
 
1509
1534
  if save:
1510
- file = "trade_history.csv"
1511
- df.to_csv(file)
1535
+ df.to_csv("orders_history.csv")
1512
1536
  if to_df:
1513
1537
  return df
1514
1538
  else:
@@ -0,0 +1,98 @@
1
+ import matplotlib.pyplot as plt
2
+ import MetaTrader5 as mt5
3
+ import numpy as np
4
+ import pandas as pd
5
+ import seaborn as sns
6
+
7
+ from bbstrader.metatrader.account import check_mt5_connection
8
+ from bbstrader.metatrader.utils import TIMEFRAMES
9
+
10
+ sns.set_theme()
11
+
12
+
13
+ def _get_data(path, symbol, timeframe, bars):
14
+ check_mt5_connection(path=path)
15
+ rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, bars)
16
+ df = pd.DataFrame(rates)
17
+ df["time"] = pd.to_datetime(df["time"], unit="s")
18
+ return df
19
+
20
+
21
+ def volume_profile(df, bins):
22
+ prices = (df["high"] + df["low"]) / 2
23
+ volumes = df["tick_volume"]
24
+ hist, bin_edges = np.histogram(prices, bins=bins, weights=volumes)
25
+ bin_centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])
26
+ return hist, bin_edges, bin_centers
27
+
28
+
29
+ def value_area(hist, bin_centers, percentage):
30
+ total_volume = np.sum(hist)
31
+ poc_index = np.argmax(hist)
32
+ poc = bin_centers[poc_index]
33
+
34
+ sorted_indices = np.argsort(hist)[::-1]
35
+ volume_accum = 0
36
+ value_area_indices = []
37
+
38
+ for idx in sorted_indices:
39
+ volume_accum += hist[idx]
40
+ value_area_indices.append(idx)
41
+ if volume_accum >= percentage * total_volume:
42
+ break
43
+
44
+ vah = max(bin_centers[i] for i in value_area_indices)
45
+ val = min(bin_centers[i] for i in value_area_indices)
46
+ return poc, vah, val
47
+
48
+
49
+ def display_volume_profile(
50
+ symbol,
51
+ path,
52
+ timeframe: str = "1m",
53
+ bars: int = 1440,
54
+ bins: int = 100,
55
+ va_percentage: float = 0.7,
56
+ ):
57
+ """
58
+ Display a volume profile chart for a given market symbol using historical data.
59
+
60
+ This function retrieves historical price and volume data for a given symbol and
61
+ plots a vertical volume profile chart showing the volume distribution across
62
+ price levels. It highlights key levels such as:
63
+ - Point of Control (POC): Price level with the highest traded volume.
64
+ - Value Area High (VAH): Upper bound of the value area.
65
+ - Value Area Low (VAL): Lower bound of the value area.
66
+ - Current Price: Latest bid price from MetaTrader 5.
67
+
68
+ Args:
69
+ symbol (str): Market symbol (e.g., "AAPL", "EURUSD").
70
+ path (str): Path to the historical data see ``bbstrader.metatrader.account.check_mt5_connection()``.
71
+ timeframe (str, optional): Timeframe for each candle (default is "1m").
72
+ bars (int, optional): Number of historical bars to fetch (default is 1440).
73
+ bins (int, optional): Number of price bins for volume profile calculation (default is 100).
74
+ va_percentage (float, optional): Percentage of total volume to define the value area (default is 0.7).
75
+
76
+ Returns:
77
+ None: Displays a matplotlib chart of the volume profile.
78
+ """
79
+ df = _get_data(path, symbol, TIMEFRAMES[timeframe], bars)
80
+ hist, bin_edges, bin_centers = volume_profile(df, bins)
81
+ poc, vah, val = value_area(hist, bin_centers, va_percentage)
82
+ current_price = mt5.symbol_info_tick(symbol).bid
83
+
84
+ plt.figure(figsize=(6, 10))
85
+ plt.barh(bin_centers, hist, height=bin_centers[1] - bin_centers[0], color="skyblue")
86
+ plt.axhline(poc, color="red", linestyle="--", label=f"POC: {poc:.5f}")
87
+ plt.axhline(vah, color="green", linestyle="--", label=f"VAH: {vah:.5f}")
88
+ plt.axhline(val, color="orange", linestyle="--", label=f"VAL: {val:.5f}")
89
+ plt.axhline(
90
+ current_price, color="black", linestyle=":", label=f"Price: {current_price:.5f}"
91
+ )
92
+ plt.legend()
93
+ plt.title("Volume Profile")
94
+ plt.xlabel("Volume")
95
+ plt.ylabel("Price")
96
+ plt.grid(True)
97
+ plt.tight_layout()
98
+ plt.show()
@@ -302,16 +302,11 @@ class TradeCopier(object):
302
302
  if self.start_time is None or self.end_time is None:
303
303
  return True
304
304
  else:
305
- start_hour, start_minutes = self.start_time.split(":")
306
- end_hour, end_minutes = self.end_time.split(":")
307
- if int(start_hour) < datetime.now().hour < int(end_hour):
305
+ now = datetime.now()
306
+ start_time = datetime.strptime(self.start_time, "%H:%M").time()
307
+ end_time = datetime.strptime(self.end_time, "%H:%M").time()
308
+ if start_time <= now.time() <= end_time:
308
309
  return True
309
- elif datetime.now().hour == int(start_hour):
310
- if datetime.now().minute >= int(start_minutes):
311
- return True
312
- elif datetime.now().hour == int(end_hour):
313
- if datetime.now().minute < int(end_minutes):
314
- return True
315
310
  return False
316
311
 
317
312
  def copy_new_trade(
@@ -491,12 +486,7 @@ class TradeCopier(object):
491
486
  )
492
487
  return source_orders, dest_orders
493
488
 
494
- def copy_orders(self, destination: dict):
495
- assert destination.get("copy", False), "Destination account not set to copy"
496
- what = destination.get("copy_what", "all")
497
- if what not in ["all", "orders"]:
498
- return
499
- check_mt5_connection(**destination)
489
+ def _copy_new_orders(self, destination):
500
490
  source_orders, destination_orders = self.get_orders(destination)
501
491
  # Check for new orders
502
492
  dest_ids = [order.magic for order in destination_orders]
@@ -505,6 +495,7 @@ class TradeCopier(object):
505
495
  if not self.slippage(source_order, destination):
506
496
  self.copy_new_order(source_order, destination)
507
497
 
498
+ def _copy_modified_orders(self, destination):
508
499
  # Check for modified orders
509
500
  source_orders, destination_orders = self.get_orders(destination)
510
501
  for source_order in source_orders:
@@ -514,6 +505,8 @@ class TradeCopier(object):
514
505
  ticket = destination_order.ticket
515
506
  symbol = destination_order.symbol
516
507
  self.modify_order(ticket, symbol, source_order, destination)
508
+
509
+ def _copy_closed_orders(self, destination):
517
510
  # Check for closed orders
518
511
  source_orders, destination_orders = self.get_orders(destination)
519
512
  source_ids = [order.ticket for order in source_orders]
@@ -524,8 +517,8 @@ class TradeCopier(object):
524
517
  )
525
518
  self.remove_order(src_symbol, destination_order, destination)
526
519
 
527
- # Check if order are triggered on source account
528
- # and not on destination account or vice versa
520
+ def _sync_positions(self, what, destination):
521
+ # Update postions
529
522
  source_positions, _ = self.get_positions(destination)
530
523
  _, destination_orders = self.get_orders(destination)
531
524
  for source_position in source_positions:
@@ -538,6 +531,8 @@ class TradeCopier(object):
538
531
  if not self.slippage(source_position, destination):
539
532
  self.copy_new_position(source_position, destination)
540
533
 
534
+ def _sync_orders(self, destination):
535
+ # Update orders
541
536
  _, destination_positions = self.get_positions(destination)
542
537
  source_orders, _ = self.get_orders(destination)
543
538
  for destination_position in destination_positions:
@@ -548,16 +543,25 @@ class TradeCopier(object):
548
543
  )
549
544
  if not self.slippage(source_order, destination):
550
545
  self.copy_new_order(source_order, destination)
551
- Mt5.shutdown()
552
546
 
553
- def copy_positions(self, destination: dict):
554
- assert destination.get("copy", False), "Destination account not set to copy"
555
- what = destination.get("copy_what", "all")
556
- if what not in ["all", "positions"]:
547
+ def _copy_what(self, destination):
548
+ if not destination.get("copy", False):
549
+ raise ValueError("Destination account not set to copy mode")
550
+ return destination.get("copy_what", "all")
551
+
552
+ def copy_orders(self, destination: dict):
553
+ what = self._copy_what(destination)
554
+ if what not in ["all", "orders"]:
557
555
  return
558
556
  check_mt5_connection(**destination)
559
- source_positions, destination_positions = self.get_positions(destination)
557
+ self._copy_new_orders(destination)
558
+ self._copy_modified_orders(destination)
559
+ self._copy_closed_orders(destination)
560
+ self._sync_positions(what, destination)
561
+ self._sync_orders(destination)
560
562
 
563
+ def _copy_new_positions(self, destination):
564
+ source_positions, destination_positions = self.get_positions(destination)
561
565
  # Check for new positions
562
566
  dest_ids = [pos.magic for pos in destination_positions]
563
567
  for source_position in source_positions:
@@ -565,6 +569,7 @@ class TradeCopier(object):
565
569
  if not self.slippage(source_position, destination):
566
570
  self.copy_new_position(source_position, destination)
567
571
 
572
+ def _copy_modified_positions(self, destination):
568
573
  # Check for modified positions
569
574
  source_positions, destination_positions = self.get_positions(destination)
570
575
  for source_position in source_positions:
@@ -576,6 +581,8 @@ class TradeCopier(object):
576
581
  self.modify_position(
577
582
  ticket, symbol, source_position, destination
578
583
  )
584
+
585
+ def _copy_closed_position(self, destination):
579
586
  # Check for closed positions
580
587
  source_positions, destination_positions = self.get_positions(destination)
581
588
  source_ids = [pos.ticket for pos in source_positions]
@@ -585,7 +592,15 @@ class TradeCopier(object):
585
592
  destination_position.symbol, destination, type="source"
586
593
  )
587
594
  self.remove_position(src_symbol, destination_position, destination)
588
- Mt5.shutdown()
595
+
596
+ def copy_positions(self, destination: dict):
597
+ what = self._copy_what(destination)
598
+ if what not in ["all", "positions"]:
599
+ return
600
+ check_mt5_connection(**destination)
601
+ self._copy_new_positions(destination)
602
+ self._copy_modified_positions(destination)
603
+ self._copy_closed_position(destination)
589
604
 
590
605
  def log_error(self, e, symbol=None):
591
606
  error_msg = repr(e)
@@ -607,11 +622,13 @@ class TradeCopier(object):
607
622
  continue
608
623
  self.copy_orders(destination)
609
624
  self.copy_positions(destination)
625
+ Mt5.shutdown()
610
626
  time.sleep(0.1)
611
- except Exception as e:
612
- self.log_error(e)
613
627
  except KeyboardInterrupt:
628
+ logger.info("Stopping the Trade Copier ...")
614
629
  break
630
+ except Exception as e:
631
+ self.log_error(e)
615
632
  time.sleep(self.sleeptime)
616
633
 
617
634
 
@@ -661,6 +678,39 @@ def RunMultipleCopier(
661
678
  process.join()
662
679
 
663
680
 
681
+ def _strtodict(string: str) -> dict:
682
+ string = string.strip().replace("\n", "").replace(" ", "").replace('"""', "")
683
+ return dict(item.split(":") for item in string.split(","))
684
+
685
+
686
+ def _parse_symbols(section):
687
+ symbols: str = section.get("symbols")
688
+ symbols = symbols.strip().replace("\n", " ").replace('"""', "")
689
+ if symbols in ["all", "*"]:
690
+ section["symbols"] = symbols
691
+ elif ":" in symbols:
692
+ symbols = _strtodict(symbols)
693
+ section["symbols"] = symbols
694
+ elif " " in symbols and "," not in symbols:
695
+ symbols = symbols.split()
696
+ section["symbols"] = symbols
697
+ elif "," in symbols:
698
+ symbols = symbols.replace(" ", "").split(",")
699
+ section["symbols"] = symbols
700
+ else:
701
+ raise ValueError("""
702
+ Invalid symbols format.
703
+ You can use space or comma separated symbols in one line or multiple lines using triple quotes.
704
+ You can also use a dictionary to map source symbols to destination symbols as shown below.
705
+ Or if you want to copy all symbols, use "all" or "*".
706
+
707
+ symbols = EURUSD, GBPUSD, USDJPY (space separated)
708
+ symbols = EURUSD,GBPUSD,USDJPY (comma separated)
709
+ symbols = EURUSD.s:EURUSD_i, GBPUSD.s:GBPUSD_i, USDJPY.s:USDJPY_i (dictionary)
710
+ symbols = all (copy all symbols)
711
+ symbols = * (copy all symbols) """)
712
+
713
+
664
714
  def config_copier(
665
715
  source_section: str = None,
666
716
  dest_sections: str | List[str] = None,
@@ -686,10 +736,6 @@ def config_copier(
686
736
  """
687
737
  from bbstrader.core.utils import dict_from_ini
688
738
 
689
- def strtodict(string: str) -> dict:
690
- string = string.strip().replace("\n", "").replace(" ", "").replace('"""', "")
691
- return dict(item.split(":") for item in string.split(","))
692
-
693
739
  if not inifile:
694
740
  inifile = Path().home() / ".bbstrader" / "copier" / "copier.ini"
695
741
  if not inifile.exists() or not inifile.is_file():
@@ -719,33 +765,7 @@ def config_copier(
719
765
  raise ValueError(
720
766
  f"Destination section {dest_section} not found in {inifile}"
721
767
  )
722
- symbols: str = section.get("symbols")
723
- symbols = symbols.strip().replace("\n", " ").replace('"""', "")
724
- if symbols in ["all", "*"]:
725
- section["symbols"] = symbols
726
- elif ":" in symbols:
727
- symbols = strtodict(symbols)
728
- section["symbols"] = symbols
729
- elif " " in symbols and "," not in symbols:
730
- symbols = symbols.split()
731
- section["symbols"] = symbols
732
- elif "," in symbols:
733
- symbols = symbols.replace(" ", "").split(",")
734
- section["symbols"] = symbols
735
- else:
736
- err_msg = """
737
- Invalid symbols format.
738
- You can use space or comma separated symbols in one line or multiple lines using triple quotes.
739
- You can also use a dictionary to map source symbols to destination symbols as shown below.
740
- Or if you want to copy all symbols, use "all" or "*".
741
-
742
- symbols = EURUSD, GBPUSD, USDJPY (space separated)
743
- symbols = EURUSD,GBPUSD,USDJPY (comma separated)
744
- symbols = EURUSD.s:EURUSD_i, GBPUSD.s:GBPUSD_i, USDJPY.s:USDJPY_i (dictionary)
745
- symbols = all (copy all symbols)
746
- symbols = * (copy all symbols) """
747
- raise ValueError(err_msg)
748
-
768
+ _parse_symbols(section)
749
769
  destinations.append(section)
750
770
 
751
771
  return source, destinations