bbstrader 0.2.98__tar.gz → 0.2.99__tar.gz

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.

Files changed (53) hide show
  1. {bbstrader-0.2.98/bbstrader.egg-info → bbstrader-0.2.99}/PKG-INFO +1 -1
  2. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/backtest.py +7 -7
  3. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/event.py +12 -4
  4. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/execution.py +3 -3
  5. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/portfolio.py +3 -3
  6. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/strategy.py +8 -2
  7. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/account.py +1 -1
  8. bbstrader-0.2.99/bbstrader/metatrader/analysis.py +98 -0
  9. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/copier.py +71 -46
  10. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/trade.py +46 -54
  11. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/factors.py +97 -97
  12. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/trading/execution.py +144 -157
  13. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/trading/strategies.py +13 -5
  14. {bbstrader-0.2.98 → bbstrader-0.2.99/bbstrader.egg-info}/PKG-INFO +1 -1
  15. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader.egg-info/SOURCES.txt +1 -0
  16. {bbstrader-0.2.98 → bbstrader-0.2.99}/setup.py +1 -1
  17. {bbstrader-0.2.98 → bbstrader-0.2.99}/LICENSE +0 -0
  18. {bbstrader-0.2.98 → bbstrader-0.2.99}/MANIFEST.in +0 -0
  19. {bbstrader-0.2.98 → bbstrader-0.2.99}/README.md +0 -0
  20. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/__ini__.py +0 -0
  21. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/__main__.py +0 -0
  22. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/__init__.py +0 -0
  23. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/data.py +0 -0
  24. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/performance.py +0 -0
  25. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/btengine/scripts.py +0 -0
  26. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/compat.py +0 -0
  27. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/config.py +0 -0
  28. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/core/__init__.py +0 -0
  29. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/core/data.py +0 -0
  30. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/core/utils.py +0 -0
  31. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/ibkr/__init__.py +0 -0
  32. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/ibkr/utils.py +0 -0
  33. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/__init__.py +0 -0
  34. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/rates.py +0 -0
  35. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/risk.py +0 -0
  36. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/scripts.py +0 -0
  37. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/metatrader/utils.py +0 -0
  38. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/__init__.py +0 -0
  39. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/ml.py +0 -0
  40. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/nlp.py +0 -0
  41. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/optimization.py +0 -0
  42. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/portfolio.py +0 -0
  43. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/models/risk.py +0 -0
  44. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/trading/__init__.py +0 -0
  45. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/trading/scripts.py +0 -0
  46. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/trading/utils.py +0 -0
  47. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader/tseries.py +0 -0
  48. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader.egg-info/dependency_links.txt +0 -0
  49. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader.egg-info/entry_points.txt +0 -0
  50. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader.egg-info/requires.txt +0 -0
  51. {bbstrader-0.2.98 → bbstrader-0.2.99}/bbstrader.egg-info/top_level.txt +0 -0
  52. {bbstrader-0.2.98 → bbstrader-0.2.99}/requirements.txt +0 -0
  53. {bbstrader-0.2.98 → bbstrader-0.2.99}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bbstrader
3
- Version: 0.2.98
3
+ Version: 0.2.99
4
4
  Summary: Simplified Investment & Trading Toolkit
5
5
  Home-page: https://github.com/bbalouki/bbstrader
6
6
  Download-URL: https://pypi.org/project/bbstrader/
@@ -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,7 +10,7 @@ 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
15
  from bbstrader.metatrader.account import (
16
16
  Account,
@@ -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":
@@ -678,6 +678,12 @@ class MT5Strategy(Strategy):
678
678
  if period_count == 0 or period_count is None:
679
679
  return True
680
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
681
687
 
682
688
  def ispositions(
683
689
  self, symbol, strategy_id, position, max_trades, one_true=False, account=None
@@ -1059,7 +1059,7 @@ class Account(object):
1059
1059
  if book is None:
1060
1060
  return None
1061
1061
  else:
1062
- return Tuple([BookInfo(**entry._asdict()) for entry in book])
1062
+ return tuple([BookInfo(**entry._asdict()) for entry in book])
1063
1063
  except Exception as e:
1064
1064
  raise_mt5_error(e)
1065
1065
 
@@ -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()
@@ -486,12 +486,7 @@ class TradeCopier(object):
486
486
  )
487
487
  return source_orders, dest_orders
488
488
 
489
- def copy_orders(self, destination: dict):
490
- assert destination.get("copy", False), "Destination account not set to copy"
491
- what = destination.get("copy_what", "all")
492
- if what not in ["all", "orders"]:
493
- return
494
- check_mt5_connection(**destination)
489
+ def _copy_new_orders(self, destination):
495
490
  source_orders, destination_orders = self.get_orders(destination)
496
491
  # Check for new orders
497
492
  dest_ids = [order.magic for order in destination_orders]
@@ -500,6 +495,7 @@ class TradeCopier(object):
500
495
  if not self.slippage(source_order, destination):
501
496
  self.copy_new_order(source_order, destination)
502
497
 
498
+ def _copy_modified_orders(self, destination):
503
499
  # Check for modified orders
504
500
  source_orders, destination_orders = self.get_orders(destination)
505
501
  for source_order in source_orders:
@@ -509,6 +505,8 @@ class TradeCopier(object):
509
505
  ticket = destination_order.ticket
510
506
  symbol = destination_order.symbol
511
507
  self.modify_order(ticket, symbol, source_order, destination)
508
+
509
+ def _copy_closed_orders(self, destination):
512
510
  # Check for closed orders
513
511
  source_orders, destination_orders = self.get_orders(destination)
514
512
  source_ids = [order.ticket for order in source_orders]
@@ -519,8 +517,8 @@ class TradeCopier(object):
519
517
  )
520
518
  self.remove_order(src_symbol, destination_order, destination)
521
519
 
522
- # Check if order are triggered on source account
523
- # and not on destination account or vice versa
520
+ def _sync_positions(self, what, destination):
521
+ # Update postions
524
522
  source_positions, _ = self.get_positions(destination)
525
523
  _, destination_orders = self.get_orders(destination)
526
524
  for source_position in source_positions:
@@ -533,6 +531,8 @@ class TradeCopier(object):
533
531
  if not self.slippage(source_position, destination):
534
532
  self.copy_new_position(source_position, destination)
535
533
 
534
+ def _sync_orders(self, destination):
535
+ # Update orders
536
536
  _, destination_positions = self.get_positions(destination)
537
537
  source_orders, _ = self.get_orders(destination)
538
538
  for destination_position in destination_positions:
@@ -543,16 +543,25 @@ class TradeCopier(object):
543
543
  )
544
544
  if not self.slippage(source_order, destination):
545
545
  self.copy_new_order(source_order, destination)
546
- Mt5.shutdown()
547
546
 
548
- def copy_positions(self, destination: dict):
549
- assert destination.get("copy", False), "Destination account not set to copy"
550
- what = destination.get("copy_what", "all")
551
- 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"]:
552
555
  return
553
556
  check_mt5_connection(**destination)
554
- 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)
555
562
 
563
+ def _copy_new_positions(self, destination):
564
+ source_positions, destination_positions = self.get_positions(destination)
556
565
  # Check for new positions
557
566
  dest_ids = [pos.magic for pos in destination_positions]
558
567
  for source_position in source_positions:
@@ -560,6 +569,7 @@ class TradeCopier(object):
560
569
  if not self.slippage(source_position, destination):
561
570
  self.copy_new_position(source_position, destination)
562
571
 
572
+ def _copy_modified_positions(self, destination):
563
573
  # Check for modified positions
564
574
  source_positions, destination_positions = self.get_positions(destination)
565
575
  for source_position in source_positions:
@@ -571,6 +581,8 @@ class TradeCopier(object):
571
581
  self.modify_position(
572
582
  ticket, symbol, source_position, destination
573
583
  )
584
+
585
+ def _copy_closed_position(self, destination):
574
586
  # Check for closed positions
575
587
  source_positions, destination_positions = self.get_positions(destination)
576
588
  source_ids = [pos.ticket for pos in source_positions]
@@ -580,7 +592,15 @@ class TradeCopier(object):
580
592
  destination_position.symbol, destination, type="source"
581
593
  )
582
594
  self.remove_position(src_symbol, destination_position, destination)
583
- 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)
584
604
 
585
605
  def log_error(self, e, symbol=None):
586
606
  error_msg = repr(e)
@@ -602,8 +622,10 @@ class TradeCopier(object):
602
622
  continue
603
623
  self.copy_orders(destination)
604
624
  self.copy_positions(destination)
625
+ Mt5.shutdown()
605
626
  time.sleep(0.1)
606
627
  except KeyboardInterrupt:
628
+ logger.info("Stopping the Trade Copier ...")
607
629
  break
608
630
  except Exception as e:
609
631
  self.log_error(e)
@@ -656,6 +678,39 @@ def RunMultipleCopier(
656
678
  process.join()
657
679
 
658
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
+
659
714
  def config_copier(
660
715
  source_section: str = None,
661
716
  dest_sections: str | List[str] = None,
@@ -681,10 +736,6 @@ def config_copier(
681
736
  """
682
737
  from bbstrader.core.utils import dict_from_ini
683
738
 
684
- def strtodict(string: str) -> dict:
685
- string = string.strip().replace("\n", "").replace(" ", "").replace('"""', "")
686
- return dict(item.split(":") for item in string.split(","))
687
-
688
739
  if not inifile:
689
740
  inifile = Path().home() / ".bbstrader" / "copier" / "copier.ini"
690
741
  if not inifile.exists() or not inifile.is_file():
@@ -714,33 +765,7 @@ def config_copier(
714
765
  raise ValueError(
715
766
  f"Destination section {dest_section} not found in {inifile}"
716
767
  )
717
- symbols: str = section.get("symbols")
718
- symbols = symbols.strip().replace("\n", " ").replace('"""', "")
719
- if symbols in ["all", "*"]:
720
- section["symbols"] = symbols
721
- elif ":" in symbols:
722
- symbols = strtodict(symbols)
723
- section["symbols"] = symbols
724
- elif " " in symbols and "," not in symbols:
725
- symbols = symbols.split()
726
- section["symbols"] = symbols
727
- elif "," in symbols:
728
- symbols = symbols.replace(" ", "").split(",")
729
- section["symbols"] = symbols
730
- else:
731
- err_msg = """
732
- Invalid symbols format.
733
- You can use space or comma separated symbols in one line or multiple lines using triple quotes.
734
- You can also use a dictionary to map source symbols to destination symbols as shown below.
735
- Or if you want to copy all symbols, use "all" or "*".
736
-
737
- symbols = EURUSD, GBPUSD, USDJPY (space separated)
738
- symbols = EURUSD,GBPUSD,USDJPY (comma separated)
739
- symbols = EURUSD.s:EURUSD_i, GBPUSD.s:GBPUSD_i, USDJPY.s:USDJPY_i (dictionary)
740
- symbols = all (copy all symbols)
741
- symbols = * (copy all symbols) """
742
- raise ValueError(err_msg)
743
-
768
+ _parse_symbols(section)
744
769
  destinations.append(section)
745
770
 
746
771
  return source, destinations