bbstrader 0.3.3__py3-none-any.whl → 0.3.5__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,19 +1,28 @@
1
1
  import os
2
2
  import re
3
3
  import urllib.request
4
- from datetime import datetime, timedelta
4
+ from datetime import datetime
5
5
  from typing import Any, Dict, List, Literal, Optional, Tuple, Union
6
6
 
7
+ import numpy as np
7
8
  import pandas as pd
9
+ from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
10
+ from numpy.typing import NDArray
11
+
8
12
  from bbstrader.metatrader.utils import (
13
+ TIMEFRAMES,
9
14
  AccountInfo,
10
15
  BookInfo,
11
16
  InvalidBroker,
12
17
  OrderCheckResult,
13
18
  OrderSentResult,
19
+ RateDtype,
20
+ RateInfo,
14
21
  SymbolInfo,
15
22
  SymbolType,
16
23
  TerminalInfo,
24
+ TickDtype,
25
+ TickFlag,
17
26
  TickInfo,
18
27
  TradeDeal,
19
28
  TradeOrder,
@@ -21,7 +30,6 @@ from bbstrader.metatrader.utils import (
21
30
  TradeRequest,
22
31
  raise_mt5_error,
23
32
  )
24
- from currency_converter import SINGLE_DAY_ECB_URL, CurrencyConverter
25
33
 
26
34
  try:
27
35
  import MetaTrader5 as mt5
@@ -170,7 +178,7 @@ def check_mt5_connection(
170
178
  if account_info is not None:
171
179
  if account_info.login == login and account_info.server == server:
172
180
  return True
173
-
181
+
174
182
  init = False
175
183
  if path is None and (login or password or server):
176
184
  raise ValueError(
@@ -517,7 +525,7 @@ class Account(object):
517
525
  """Helper function to print account info"""
518
526
  self._show_info(self.get_account_info, "account")
519
527
 
520
- def get_terminal_info(self, show=False) -> Union[TerminalInfo, None]:
528
+ def get_terminal_info(self, show=False) -> TerminalInfo | None:
521
529
  """
522
530
  Get the connected MetaTrader 5 client terminal status and settings.
523
531
 
@@ -965,7 +973,7 @@ class Account(object):
965
973
  futures_types["bonds"].append(info.name)
966
974
  return futures_types[category]
967
975
 
968
- def get_symbol_info(self, symbol: str) -> Union[SymbolInfo, None]:
976
+ def get_symbol_info(self, symbol: str) -> SymbolInfo | None:
969
977
  """Get symbol properties
970
978
 
971
979
  Args:
@@ -998,6 +1006,14 @@ class Account(object):
998
1006
  msg = self._symbol_info_msg(symbol)
999
1007
  raise_mt5_error(message=f"{str(e)} {msg}")
1000
1008
 
1009
+ def _symbol_info_msg(self, symbol):
1010
+ return (
1011
+ f"No history found for {symbol} in Market Watch.\n"
1012
+ f"* Ensure {symbol} is selected and displayed in the Market Watch window.\n"
1013
+ f"* See https://www.metatrader5.com/en/terminal/help/trading/market_watch\n"
1014
+ f"* Ensure the symbol name is correct.\n"
1015
+ )
1016
+
1001
1017
  def show_symbol_info(self, symbol: str):
1002
1018
  """
1003
1019
  Print symbol properties
@@ -1007,15 +1023,7 @@ class Account(object):
1007
1023
  """
1008
1024
  self._show_info(self.get_symbol_info, "symbol", symbol=symbol)
1009
1025
 
1010
- def _symbol_info_msg(self, symbol):
1011
- return (
1012
- f"No history found for {symbol} in Market Watch.\n"
1013
- f"* Ensure {symbol} is selected and displayed in the Market Watch window.\n"
1014
- f"* See https://www.metatrader5.com/en/terminal/help/trading/market_watch\n"
1015
- f"* Ensure the symbol name is correct.\n"
1016
- )
1017
-
1018
- def get_tick_info(self, symbol: str) -> Union[TickInfo, None]:
1026
+ def get_tick_info(self, symbol: str) -> TickInfo | None:
1019
1027
  """Get symbol tick properties
1020
1028
 
1021
1029
  Args:
@@ -1057,7 +1065,148 @@ class Account(object):
1057
1065
  """
1058
1066
  self._show_info(self.get_tick_info, "tick", symbol=symbol)
1059
1067
 
1060
- def get_market_book(self, symbol: str) -> Union[Tuple[BookInfo], None]:
1068
+ def get_rate_info(self, symbol: str, timeframe: str = "1m") -> RateInfo | None:
1069
+ """Get the most recent bar for a specified symbol and timeframe.
1070
+
1071
+ Args:
1072
+ symbol (str): The symbol for which to get the rate information.
1073
+ timeframe (str): The timeframe for the rate information. Default is '1m'.
1074
+ See ``bbstrader.metatrader.utils.TIMEFRAMES`` for supported timeframes.
1075
+ Returns:
1076
+ RateInfo: The most recent bar as a RateInfo named tuple.
1077
+ None: If no rates are found or an error occurs.
1078
+ Raises:
1079
+ MT5TerminalError: A specific exception based on the error code.
1080
+ """
1081
+ rates = mt5.copy_rates_from_pos(symbol, TIMEFRAMES[timeframe], 0, 1)
1082
+ if rates is None or len(rates) == 0:
1083
+ return None
1084
+ rate = rates[0]
1085
+ return RateInfo(*rate)
1086
+
1087
+ def get_rates_from_pos(
1088
+ self, symbol: str, timeframe: str, start_pos: int = 0, bars: int = 1
1089
+ ) -> NDArray[np.void]:
1090
+ """
1091
+ Get bars from the MetaTrader 5 terminal starting from the specified index.
1092
+
1093
+ Args:
1094
+ symbol: Financial instrument name, for example, "EURUSD"
1095
+ timeframe: Timeframe the bars are requested for.
1096
+ start_pos: Initial index of the bar the data are requested from.
1097
+ bars: Number of bars to receive.
1098
+
1099
+ Returns:
1100
+ bars as the numpy array with the named time, open, high, low, close, tick_volume, spread and real_volume columns.
1101
+ Returns an empty array in case of an error.
1102
+ """
1103
+ rates = mt5.copy_rates_from_pos(symbol, TIMEFRAMES[timeframe], start_pos, bars)
1104
+ if rates is None or len(rates) == 0:
1105
+ return np.array([], dtype=RateDtype)
1106
+ return rates
1107
+
1108
+ def get_rates_from_date(
1109
+ self,
1110
+ symbol: str,
1111
+ timeframe: str,
1112
+ date_from: datetime | pd.Timestamp,
1113
+ bars: int = 1,
1114
+ ) -> NDArray[np.void]:
1115
+ """
1116
+ Get bars from the MetaTrader 5 terminal starting from the specified date.
1117
+
1118
+ Args:
1119
+ symbol: Financial instrument name, for example, "EURUSD"
1120
+ timeframe: Timeframe the bars are requested for.
1121
+ date_from: Date of opening of the first bar from the requested sample.
1122
+ bars: Number of bars to receive.
1123
+
1124
+ Returns:
1125
+ bars as the numpy array with the named time, open, high, low, close, tick_volume, spread and real_volume columns.
1126
+ Returns an empty array in case of an error.
1127
+
1128
+ """
1129
+ rates = mt5.copy_rates_from(symbol, TIMEFRAMES[timeframe], date_from, bars)
1130
+ if rates is None or len(rates) == 0:
1131
+ return np.array([], dtype=RateDtype)
1132
+ return rates
1133
+
1134
+ def get_rates_range(
1135
+ self,
1136
+ symbol: str,
1137
+ timeframe: str,
1138
+ date_from: datetime | pd.Timestamp,
1139
+ date_to: datetime | pd.Timestamp = datetime.now(),
1140
+ ) -> NDArray[np.void]:
1141
+ """
1142
+ Get bars in the specified date range from the MetaTrader 5 terminal.
1143
+
1144
+ Args:
1145
+ symbol: Financial instrument name, for example, "EURUSD"
1146
+ timeframe: Timeframe the bars are requested for.
1147
+ date_from: Date the bars are requested from.
1148
+ date_to: Date, up to which the bars are requested.
1149
+
1150
+ Returns:
1151
+ bars as the numpy array with the named time, open, high, low, close, tick_volume, spread and real_volume columns.
1152
+ Returns an empty array in case of an error.
1153
+ """
1154
+ rates = mt5.copy_rates_range(symbol, TIMEFRAMES[timeframe], date_from, date_to)
1155
+ if rates is None or len(rates) == 0:
1156
+ return np.array([], dtype=RateDtype)
1157
+ return rates
1158
+
1159
+ def get_tick_from_date(
1160
+ self,
1161
+ symbol: str,
1162
+ date_from: datetime | pd.Timestamp,
1163
+ ticks: int = 1,
1164
+ flag: Literal["all", "info", "trade"] = "all",
1165
+ ) -> NDArray[np.void]:
1166
+ """
1167
+ Get ticks from the MetaTrader 5 terminal starting from the specified date.
1168
+
1169
+ Args:
1170
+ symbol: Financial instrument name, for example, "EURUSD"
1171
+ date_from: Date the ticks are requested from.
1172
+ ticks: Number of bars to receive.
1173
+ flag: A flag to define the type of the requested ticks ("all", "info", "trade").
1174
+
1175
+ Returns:
1176
+ Returns ticks as the numpy array with the named time, bid, ask, last and flags columns.
1177
+ Return an empty array in case of an error.
1178
+ """
1179
+ ticks_data = mt5.copy_ticks_from(symbol, date_from, ticks, TickFlag[flag])
1180
+ if ticks_data is None or len(ticks_data) == 0:
1181
+ return np.array([], dtype=TickDtype)
1182
+ return ticks_data
1183
+
1184
+ def get_tick_range(
1185
+ self,
1186
+ symbol: str,
1187
+ date_from: datetime | pd.Timestamp,
1188
+ date_to: datetime | pd.Timestamp = datetime.now(),
1189
+ flag: Literal["all", "info", "trade"] = "all",
1190
+ ) -> NDArray[np.void]:
1191
+ """
1192
+ Get ticks for the specified date range from the MetaTrader 5 terminal.
1193
+
1194
+ Args:
1195
+ symbol: Financial instrument name, for example, "EURUSD"
1196
+ date_from: Date the ticks are requested from.
1197
+ date_to: Date, up to which the ticks are requested.
1198
+ flag: A flag to define the type of the requested ticks ("all", "info", "trade").
1199
+
1200
+ Returns:
1201
+ Returns ticks as the numpy array with the named time, bid, ask, last and flags columns.
1202
+ Return an empty array in case of an error.
1203
+ """
1204
+ ticks_data = mt5.copy_ticks_range(symbol, date_from, date_to, TickFlag[flag])
1205
+ if ticks_data is None or len(ticks_data) == 0:
1206
+ return np.array([], dtype=TickDtype)
1207
+ return ticks_data
1208
+
1209
+ def get_market_book(self, symbol: str) -> Tuple[BookInfo]:
1061
1210
  """
1062
1211
  Get the Market Depth content for a specific symbol.
1063
1212
  Args:
@@ -1199,7 +1348,7 @@ class Account(object):
1199
1348
  group: Optional[str] = None,
1200
1349
  ticket: Optional[int] = None,
1201
1350
  to_df: bool = False,
1202
- ) -> Union[pd.DataFrame, Tuple[TradePosition], None]:
1351
+ ) -> Union[pd.DataFrame, Tuple[TradePosition]]:
1203
1352
  """
1204
1353
  Get open positions with the ability to filter by symbol or ticket.
1205
1354
  There are four call options:
@@ -1229,7 +1378,6 @@ class Account(object):
1229
1378
  Returns:
1230
1379
  Union[pd.DataFrame, Tuple[TradePosition], None]:
1231
1380
  - `TradePosition` in the form of a named tuple structure (namedtuple) or pd.DataFrame.
1232
- - `None` in case of an error.
1233
1381
 
1234
1382
  Notes:
1235
1383
  The method allows receiving all open positions within a specified period.
@@ -1285,7 +1433,7 @@ class Account(object):
1285
1433
  position: Optional[int] = None, # TradePosition.ticket
1286
1434
  to_df: bool = True,
1287
1435
  save: bool = False,
1288
- ) -> Union[pd.DataFrame, Tuple[TradeDeal], None]:
1436
+ ) -> Union[pd.DataFrame, Tuple[TradeDeal]]:
1289
1437
  """
1290
1438
  Get deals from trading history within the specified interval
1291
1439
  with the ability to filter by `ticket` or `position`.
@@ -1323,7 +1471,6 @@ class Account(object):
1323
1471
  Returns:
1324
1472
  Union[pd.DataFrame, Tuple[TradeDeal], None]:
1325
1473
  - `TradeDeal` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
1326
- - `None` in case of an error.
1327
1474
 
1328
1475
  Notes:
1329
1476
  The method allows receiving all history orders within a specified period.
@@ -1413,7 +1560,6 @@ class Account(object):
1413
1560
  Returns:
1414
1561
  Union[pd.DataFrame, Tuple[TradeOrder], None]:
1415
1562
  - `TradeOrder` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
1416
- - `None` in case of an error.
1417
1563
 
1418
1564
  Notes:
1419
1565
  The method allows receiving all history orders within a specified period.
@@ -1478,7 +1624,7 @@ class Account(object):
1478
1624
  position: Optional[int] = None, # position ticket
1479
1625
  to_df: bool = True,
1480
1626
  save: bool = False,
1481
- ) -> Union[pd.DataFrame, Tuple[TradeOrder], None]:
1627
+ ) -> Union[pd.DataFrame, Tuple[TradeOrder]]:
1482
1628
  """
1483
1629
  Get orders from trading history within the specified interval
1484
1630
  with the ability to filter by `ticket` or `position`.
@@ -1516,7 +1662,6 @@ class Account(object):
1516
1662
  Returns:
1517
1663
  Union[pd.DataFrame, Tuple[TradeOrder], None]
1518
1664
  - `TradeOrder` in the form of a named tuple structure (namedtuple) or pd.DataFrame().
1519
- - `None` in case of an error.
1520
1665
 
1521
1666
  Notes:
1522
1667
  The method allows receiving all history orders within a specified period.
@@ -1597,16 +1742,11 @@ class Account(object):
1597
1742
  Returns:
1598
1743
  List[TradeDeal]: List of today deals
1599
1744
  """
1600
- date_from = datetime.now() - timedelta(days=2)
1601
- history = (
1602
- self.get_trades_history(date_from=date_from, group=group, to_df=False) or []
1603
- )
1745
+ history = self.get_trades_history(group=group, to_df=False) or []
1604
1746
  positions_ids = set([deal.position_id for deal in history if deal.magic == id])
1605
1747
  today_deals = []
1606
1748
  for position in positions_ids:
1607
- deal = self.get_trades_history(
1608
- date_from=date_from, position=position, to_df=False
1609
- )
1749
+ deal = self.get_trades_history(position=position, to_df=False) or []
1610
1750
  if deal is not None and len(deal) == 2:
1611
1751
  deal_time = datetime.fromtimestamp(deal[1].time)
1612
1752
  if deal_time.date() == datetime.now().date():
@@ -4,17 +4,15 @@ import numpy as np
4
4
  import pandas as pd
5
5
  import seaborn as sns
6
6
 
7
- from bbstrader.metatrader.account import check_mt5_connection
7
+ from bbstrader.metatrader.account import check_mt5_connection, shutdown_mt5
8
8
  from bbstrader.metatrader.utils import TIMEFRAMES
9
9
 
10
10
  sns.set_theme()
11
11
 
12
12
 
13
- def _get_data(path, symbol, timeframe, bars):
14
- check_mt5_connection(path=path)
13
+ def _get_data(symbol, timeframe, bars):
15
14
  rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, bars)
16
15
  df = pd.DataFrame(rates)
17
- df["time"] = pd.to_datetime(df["time"], unit="s")
18
16
  return df
19
17
 
20
18
 
@@ -76,10 +74,14 @@ def display_volume_profile(
76
74
  Returns:
77
75
  None: Displays a matplotlib chart of the volume profile.
78
76
  """
79
- df = _get_data(path, symbol, TIMEFRAMES[timeframe], bars)
77
+ check_mt5_connection(path=path)
78
+ df = _get_data(symbol, TIMEFRAMES[timeframe], bars)
79
+ if df.empty:
80
+ raise ValueError(f"No data found for {symbol} in {path}")
80
81
  hist, bin_edges, bin_centers = volume_profile(df, bins)
81
82
  poc, vah, val = value_area(hist, bin_centers, va_percentage)
82
83
  current_price = mt5.symbol_info_tick(symbol).bid
84
+ shutdown_mt5()
83
85
 
84
86
  plt.figure(figsize=(6, 10))
85
87
  plt.barh(bin_centers, hist, height=bin_centers[1] - bin_centers[0], color="skyblue")
@@ -51,6 +51,31 @@ ORDER_TYPE = {
51
51
  7: (Mt5.ORDER_TYPE_SELL_STOP_LIMIT, "SELL STOP LIMIT"),
52
52
  }
53
53
 
54
+ STOP_RETCODES = [
55
+ Mt5.TRADE_RETCODE_TRADE_DISABLED,
56
+ Mt5.TRADE_RETCODE_NO_MONEY,
57
+ Mt5.TRADE_RETCODE_SERVER_DISABLES_AT,
58
+ Mt5.TRADE_RETCODE_CLIENT_DISABLES_AT,
59
+ Mt5.TRADE_RETCODE_ONLY_REAL,
60
+ ]
61
+
62
+ RETURN_RETCODE = [
63
+ Mt5.TRADE_RETCODE_MARKET_CLOSED,
64
+ Mt5.TRADE_RETCODE_CONNECTION,
65
+ Mt5.TRADE_RETCODE_LIMIT_ORDERS,
66
+ Mt5.TRADE_RETCODE_LIMIT_VOLUME,
67
+ Mt5.TRADE_RETCODE_LIMIT_POSITIONS,
68
+ Mt5.TRADE_RETCODE_LONG_ONLY,
69
+ Mt5.TRADE_RETCODE_SHORT_ONLY,
70
+ Mt5.TRADE_RETCODE_CLOSE_ONLY,
71
+ Mt5.TRADE_RETCODE_FIFO_CLOSE,
72
+ Mt5.TRADE_RETCODE_INVALID_VOLUME,
73
+ Mt5.TRADE_RETCODE_INVALID_PRICE,
74
+ Mt5.TRADE_RETCODE_INVALID_STOPS,
75
+ Mt5.TRADE_RETCODE_NO_CHANGES
76
+ ]
77
+
78
+
54
79
 
55
80
  class OrderAction(Enum):
56
81
  COPY_NEW = "COPY_NEW"
@@ -65,25 +90,23 @@ CopyMode = Literal["fix", "multiply", "percentage", "dynamic", "replicate"]
65
90
 
66
91
  def fix_lot(fixed):
67
92
  if fixed == 0 or fixed is None:
68
- raise ValueError("Fixed lot must be a number")
93
+ raise ValueError("Fixed lot must be a number > 0")
69
94
  return fixed
70
95
 
71
96
 
72
97
  def multiply_lot(lot, multiplier):
73
98
  if multiplier == 0 or multiplier is None:
74
- raise ValueError("Multiplier lot must be a number")
99
+ raise ValueError("Multiplier lot must be a number > 0")
75
100
  return lot * multiplier
76
101
 
77
102
 
78
103
  def percentage_lot(lot, percentage):
79
104
  if percentage == 0 or percentage is None:
80
- raise ValueError("Percentage lot must be a number")
105
+ raise ValueError("Percentage lot must be a number > 0")
81
106
  return round(lot * percentage / 100, 2)
82
107
 
83
108
 
84
109
  def dynamic_lot(source_lot, source_eqty: float, dest_eqty: float):
85
- if source_eqty == 0 or dest_eqty == 0:
86
- raise ValueError("Source or destination account equity is zero")
87
110
  try:
88
111
  ratio = dest_eqty / source_eqty
89
112
  return round(source_lot * ratio, 2)
@@ -118,6 +141,7 @@ def fixed_lot(lot, symbol, destination) -> float:
118
141
  else:
119
142
  return _check_lot(round(lot), s_info)
120
143
 
144
+
121
145
  def calculate_copy_lot(
122
146
  source_lot,
123
147
  symbol: str,
@@ -173,7 +197,7 @@ def get_symbols_from_string(symbols_string: str):
173
197
 
174
198
  def get_copy_symbols(destination: dict, source: dict) -> List[str] | Dict[str, str]:
175
199
  symbols = destination.get("symbols", "all")
176
- if symbols == "all" or symbols == "*" or isinstance(symbols, list) :
200
+ if symbols == "all" or symbols == "*" or isinstance(symbols, list):
177
201
  src_account = Account(**source)
178
202
  src_symbols = src_account.get_symbols()
179
203
  dest_account = Account(**destination)
@@ -360,7 +384,10 @@ class TradeCopier(object):
360
384
  for destination in self.destinations:
361
385
  destination["copy"] = destination.get("copy", True)
362
386
 
363
- def log_message(self, message, type="info"):
387
+ def log_message(
388
+ self, message, type: Literal["info", "error", "debug", "warning"] = "info"
389
+ ):
390
+ logger.trace
364
391
  if self.log_queue:
365
392
  try:
366
393
  now = datetime.now()
@@ -370,7 +397,7 @@ class TradeCopier(object):
370
397
  )
371
398
  space = len("exception") # longest log name
372
399
  self.log_queue.put(
373
- f"{formatted} |{type.upper()} {' '*(space - len(type))} | - {message}"
400
+ f"{formatted} |{type.upper()} {' ' * (space - len(type))} | - {message}"
374
401
  )
375
402
  except Exception:
376
403
  pass
@@ -384,8 +411,8 @@ class TradeCopier(object):
384
411
  error_msg = repr(e)
385
412
  if error_msg not in self.errors:
386
413
  self.errors.add(error_msg)
387
- add_msg = f"SYMBOL={symbol}" if symbol else ""
388
- message = f"Error encountered: {error_msg}, {add_msg}"
414
+ add_msg = f", SYMBOL={symbol}" if symbol else ""
415
+ message = f"Error encountered: {error_msg}{add_msg}"
389
416
  self.log_message(message, type="error")
390
417
 
391
418
  def _validate_source(self):
@@ -487,6 +514,14 @@ class TradeCopier(object):
487
514
  if new_result.retcode == Mt5.TRADE_RETCODE_DONE:
488
515
  break
489
516
  return new_result
517
+
518
+ def handle_retcode(self, retcode) -> int:
519
+ if retcode in STOP_RETCODES:
520
+ msg = trade_retcode_message(retcode)
521
+ self.log_error(f"Critical Error on @{self.source['login']}: {msg} ")
522
+ self.stop()
523
+ if retcode in RETURN_RETCODE:
524
+ return 1
490
525
 
491
526
  def copy_new_trade(self, trade: TradeOrder | TradePosition, destination: dict):
492
527
  if not self.iscopy_time():
@@ -508,7 +543,6 @@ class TradeCopier(object):
508
543
  trade_action = (
509
544
  Mt5.TRADE_ACTION_DEAL if trade.type in [0, 1] else Mt5.TRADE_ACTION_PENDING
510
545
  )
511
- action = ORDER_TYPE[trade.type][1]
512
546
  tick = Mt5.symbol_info_tick(symbol)
513
547
  price = tick.bid if trade.type == 0 else tick.ask
514
548
  try:
@@ -535,14 +569,18 @@ class TradeCopier(object):
535
569
  result = Mt5.order_send(request)
536
570
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
537
571
  result = self._update_filling_type(request, result)
572
+ action = ORDER_TYPE[trade.type][1]
573
+ copy_action = "Position" if trade.type in [0, 1] else "Order"
538
574
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
539
575
  self.log_message(
540
- f"Copy {action} Order #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
576
+ f"Copy {action} {copy_action} #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
541
577
  f"to @{destination.get('login')}::{symbol}",
542
578
  )
543
579
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
580
+ if self.handle_retcode(result.retcode) == 1:
581
+ return
544
582
  self.log_message(
545
- f"Error copying {action} Order #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
583
+ f"Error copying {action} {copy_action} #{trade.ticket} from @{self.source.get('login')}::{trade.symbol} "
546
584
  f"to @{destination.get('login')}::{symbol}, {trade_retcode_message(result.retcode)}",
547
585
  type="error",
548
586
  )
@@ -573,6 +611,8 @@ class TradeCopier(object):
573
611
  f"SOURCE=@{self.source.get('login')}::{source_order.symbol}"
574
612
  )
575
613
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
614
+ if self.handle_retcode(result.retcode) == 1:
615
+ return
576
616
  self.log_message(
577
617
  f"Error modifying {ORDER_TYPE[source_order.type][1]} Order #{ticket} on @{destination.get('login')}::{symbol},"
578
618
  f"SOURCE=@{self.source.get('login')}::{source_order.symbol}, {trade_retcode_message(result.retcode)}",
@@ -595,6 +635,8 @@ class TradeCopier(object):
595
635
  f"SOURCE=@{self.source.get('login')}::{src_symbol}"
596
636
  )
597
637
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
638
+ if self.handle_retcode(result.retcode) == 1:
639
+ return
598
640
  self.log_message(
599
641
  f"Error closing {ORDER_TYPE[order.type][1]} Order #{order.ticket} on @{destination.get('login')}::{order.symbol}, "
600
642
  f"SOURCE=@{self.source.get('login')}::{src_symbol}, {trade_retcode_message(result.retcode)}",
@@ -625,6 +667,8 @@ class TradeCopier(object):
625
667
  f"SOURCE=@{self.source.get('login')}::{source_pos.symbol}"
626
668
  )
627
669
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
670
+ if self.handle_retcode(result.retcode) == 1:
671
+ return
628
672
  self.log_message(
629
673
  f"Error modifying {ORDER_TYPE[source_pos.type][1]} Position #{ticket} on @{destination.get('login')}::{symbol}, "
630
674
  f"SOURCE=@{self.source.get('login')}::{source_pos.symbol}, {trade_retcode_message(result.retcode)}",
@@ -659,6 +703,8 @@ class TradeCopier(object):
659
703
  f"SOURCE=@{self.source.get('login')}::{src_symbol}"
660
704
  )
661
705
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
706
+ if self.handle_retcode(result.retcode) == 1:
707
+ return
662
708
  self.log_message(
663
709
  f"Error closing {ORDER_TYPE[position.type][1]} Position #{position.ticket} "
664
710
  f"on @{destination.get('login')}::{position.symbol}, "
@@ -1019,7 +1065,8 @@ class TradeCopier(object):
1019
1065
  self.destinations
1020
1066
  ):
1021
1067
  self.log_message(
1022
- "Two or more destination accounts have the same Terminal path, which is not allowed."
1068
+ "Two or more destination accounts have the same Terminal path, which is not allowed.",
1069
+ type="error",
1023
1070
  )
1024
1071
  return
1025
1072
 
@@ -2,7 +2,7 @@ import argparse
2
2
  import multiprocessing
3
3
  import sys
4
4
 
5
- from bbstrader.apps._copier import main as RunCopyAPP
5
+ from bbstrader.apps._copier import main as RunCopyApp
6
6
  from bbstrader.metatrader.copier import RunCopier, config_copier, copier_worker_process
7
7
 
8
8
 
@@ -18,6 +18,15 @@ def copier_args(parser: argparse.ArgumentParser):
18
18
  parser.add_argument(
19
19
  "-s", "--source", type=str, nargs="?", default=None, help="Source section name"
20
20
  )
21
+ parser.add_argument(
22
+ "-I", "--id", type=int, default=0, help="Source Account unique ID"
23
+ )
24
+ parser.add_argument(
25
+ "-U",
26
+ "--unique",
27
+ action="store_true",
28
+ help="Specify if the source account is only master",
29
+ )
21
30
  parser.add_argument(
22
31
  "-d",
23
32
  "--destinations",
@@ -70,6 +79,8 @@ def copy_trades(unknown):
70
79
  Options:
71
80
  -m, --mode: CLI for terminal app and GUI for Desktop app
72
81
  -s, --source: Source Account section name
82
+ -I, --id: Source Account unique ID
83
+ -U, --unique: Specify if the source account is only master
73
84
  -d, --destinations: Destination Account section names (multiple allowed)
74
85
  -i, --interval: Update interval in seconds
75
86
  -M, --multiprocess: When set to True, each destination account runs in a separate process.
@@ -86,7 +97,7 @@ def copy_trades(unknown):
86
97
  copy_args = copy_parser.parse_args(unknown)
87
98
 
88
99
  if copy_args.mode == "GUI":
89
- RunCopyAPP()
100
+ RunCopyApp()
90
101
 
91
102
  elif copy_args.mode == "CLI":
92
103
  source, destinations = config_copier(
@@ -94,6 +105,8 @@ def copy_trades(unknown):
94
105
  dest_sections=copy_args.destinations,
95
106
  inifile=copy_args.config,
96
107
  )
108
+ source["id"] = copy_args.id
109
+ source["unique"] = copy_args.unique
97
110
  if copy_args.multiprocess:
98
111
  copier_processes = []
99
112
  for dest_config in destinations: