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

Files changed (38) hide show
  1. bbstrader/__ini__.py +9 -9
  2. bbstrader/btengine/__init__.py +7 -7
  3. bbstrader/btengine/backtest.py +30 -26
  4. bbstrader/btengine/data.py +100 -79
  5. bbstrader/btengine/event.py +2 -1
  6. bbstrader/btengine/execution.py +18 -16
  7. bbstrader/btengine/performance.py +11 -7
  8. bbstrader/btengine/portfolio.py +35 -36
  9. bbstrader/btengine/strategy.py +119 -94
  10. bbstrader/config.py +14 -8
  11. bbstrader/core/__init__.py +0 -0
  12. bbstrader/core/data.py +22 -0
  13. bbstrader/core/utils.py +57 -0
  14. bbstrader/ibkr/__init__.py +0 -0
  15. bbstrader/ibkr/utils.py +0 -0
  16. bbstrader/metatrader/__init__.py +5 -5
  17. bbstrader/metatrader/account.py +117 -121
  18. bbstrader/metatrader/rates.py +83 -80
  19. bbstrader/metatrader/risk.py +23 -37
  20. bbstrader/metatrader/trade.py +169 -140
  21. bbstrader/metatrader/utils.py +3 -3
  22. bbstrader/models/__init__.py +5 -5
  23. bbstrader/models/factors.py +280 -0
  24. bbstrader/models/ml.py +1092 -0
  25. bbstrader/models/optimization.py +31 -28
  26. bbstrader/models/{portfolios.py → portfolio.py} +64 -46
  27. bbstrader/models/risk.py +15 -9
  28. bbstrader/trading/__init__.py +2 -2
  29. bbstrader/trading/execution.py +252 -164
  30. bbstrader/trading/scripts.py +8 -4
  31. bbstrader/trading/strategies.py +79 -66
  32. bbstrader/tseries.py +482 -107
  33. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/LICENSE +1 -1
  34. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/METADATA +6 -1
  35. bbstrader-0.2.1.dist-info/RECORD +37 -0
  36. bbstrader-0.1.94.dist-info/RECORD +0 -32
  37. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/WHEEL +0 -0
  38. {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/top_level.txt +0 -0
@@ -1,36 +1,22 @@
1
1
  import os
2
- import csv
3
2
  import time
4
- import numpy as np
5
- import pandas as pd
6
3
  from datetime import datetime
7
- import MetaTrader5 as Mt5
8
4
  from logging import Logger
5
+ from typing import Any, Callable, Dict, List, Literal, Optional, Tuple
6
+
7
+ import MetaTrader5 as Mt5
8
+ import pandas as pd
9
9
  from tabulate import tabulate
10
- from typing import (
11
- List,
12
- Tuple,
13
- Dict,
14
- Any,
15
- Optional,
16
- Literal,
17
- Callable
18
- )
10
+
19
11
  from bbstrader.btengine.performance import create_sharpe_ratio
12
+ from bbstrader.config import BBSTRADER_DIR, config_logger
13
+ from bbstrader.metatrader.account import INIT_MSG, check_mt5_connection
20
14
  from bbstrader.metatrader.risk import RiskManagement
21
- from bbstrader.metatrader.account import(
22
- check_mt5_connection,
23
- INIT_MSG
24
- )
25
15
  from bbstrader.metatrader.utils import (
26
- TimeFrame,
27
- TradePosition,
28
- TickInfo,
29
- raise_mt5_error,
30
- trade_retcode_message
16
+ TradePosition,
17
+ raise_mt5_error,
18
+ trade_retcode_message,
31
19
  )
32
- from bbstrader.config import config_logger, BBSTRADER_DIR
33
-
34
20
 
35
21
  __all__ = [
36
22
  'Trade',
@@ -168,13 +154,14 @@ class Trade(RiskManagement):
168
154
  ":")
169
155
  self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
170
156
 
171
- self.buy_positions = []
172
- self.sell_positions = []
173
- self.opened_positions = []
174
- self.opened_orders = []
175
- self.break_even_status = []
176
- self.break_even_points = {}
157
+ self.buy_positions = []
158
+ self.sell_positions = []
159
+ self.opened_positions = []
160
+ self.opened_orders = []
161
+ self.break_even_status = []
162
+ self.break_even_points = {}
177
163
  self.trail_after_points = []
164
+ self._retcodes = []
178
165
 
179
166
  self.initialize(**kwargs)
180
167
  self.select_symbol(**kwargs)
@@ -188,14 +175,19 @@ class Trade(RiskManagement):
188
175
  print(
189
176
  f">>> Everything is OK, @{self.expert_name} is Running ...>>>\n")
190
177
 
191
- def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
178
+ @property
179
+ def retcodes(self) -> List[int]:
180
+ """Return all the retcodes"""
181
+ return self._retcodes
182
+
183
+ def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
192
184
  """Get the logger object"""
193
185
  if isinstance(logger, str):
194
186
  log_path = BBSTRADER_DIR / 'logs'
195
187
  log_path.mkdir(exist_ok=True)
196
188
  return config_logger(f'{log_path}/{logger}', consol_log)
197
189
  return logger
198
-
190
+
199
191
  def initialize(self, **kwargs):
200
192
  """
201
193
  Initializes the MetaTrader 5 (MT5) terminal for trading operations.
@@ -266,12 +258,16 @@ class Trade(RiskManagement):
266
258
  ["Expert | Strategy ID", self.expert_id],
267
259
  ["Trading Symbol", self.symbol],
268
260
  ["Trading Time Frame", self.tf],
269
- ["Start Trading Time", f"{self.start_time_hour}:{self.start_time_minutes}"],
270
- ["Finishing Trading Time", f"{self.finishing_time_hour}:{self.finishing_time_minutes}"],
271
- ["Closing Position After", f"{self.ending_time_hour}:{self.ending_time_minutes}"],
261
+ ["Start Trading Time",
262
+ f"{self.start_time_hour}:{self.start_time_minutes}"],
263
+ ["Finishing Trading Time",
264
+ f"{self.finishing_time_hour}:{self.finishing_time_minutes}"],
265
+ ["Closing Position After",
266
+ f"{self.ending_time_hour}:{self.ending_time_minutes}"],
272
267
  ]
273
268
  # Custom table format
274
- summary_table = tabulate(summary_data, headers=["Summary", "Values"], tablefmt="outline")
269
+ summary_table = tabulate(summary_data, headers=[
270
+ "Summary", "Values"], tablefmt="outline")
275
271
 
276
272
  # Print the table
277
273
  print("\n[======= Trade Account Summary =======]")
@@ -299,10 +295,12 @@ class Trade(RiskManagement):
299
295
  ["Account Margin", f"{round(account_info.margin, 2)} {currency}"],
300
296
  ["Account Free Margin", f"{account_info.margin_free} {currency}"],
301
297
  ["Maximum Drawdown", f"{self.max_risk}%"],
302
- ["Risk Allowed", f"{round((self.max_risk - self.risk_level()), 2)}%"],
298
+ ["Risk Allowed",
299
+ f"{round((self.max_risk - self.risk_level()), 2)}%"],
303
300
  ["Volume", f"{self.volume()} {marging_currency}"],
304
301
  ["Risk Per trade", f"{-self.get_currency_risk()} {currency}"],
305
- ["Profit Expected Per trade", f"{self.expected_profit()} {currency}"],
302
+ ["Profit Expected Per trade",
303
+ f"{self.expected_profit()} {currency}"],
306
304
  ["Lot Size", f"{self.get_lot()} Lots"],
307
305
  ["Stop Loss", f"{self.get_stop_loss()} Points"],
308
306
  ["Loss Value Per Tick", f"{round(loss, 5)} {currency}"],
@@ -316,7 +314,8 @@ class Trade(RiskManagement):
316
314
  ]
317
315
  # Custom table format
318
316
  print("\n[======= Account Risk Management Overview =======]")
319
- table = tabulate(account_data, headers=["Risk Metrics", "Values"], tablefmt="outline")
317
+ table = tabulate(account_data, headers=[
318
+ "Risk Metrics", "Values"], tablefmt="outline")
320
319
 
321
320
  # Print the table
322
321
  print(table)
@@ -338,7 +337,6 @@ class Trade(RiskManagement):
338
337
  currency = self.get_account_info().currency
339
338
  net_profit = round((profit + total_fees), 2)
340
339
  trade_risk = round(self.get_currency_risk() * -1, 2)
341
- expected_profit = round((trade_risk * self.rr * -1), 2)
342
340
 
343
341
  # Formatting the statistics output
344
342
  session_data = [
@@ -350,7 +348,8 @@ class Trade(RiskManagement):
350
348
  ["Average Fees", f"{average_fee} {currency}"],
351
349
  ["Net Profit", f"{net_profit} {currency}"],
352
350
  ["Risk per Trade", f"{trade_risk} {currency}"],
353
- ["Expected Profit per Trade", f"{self.expected_profit()} {currency}"],
351
+ ["Expected Profit per Trade",
352
+ f"{self.expected_profit()} {currency}"],
354
353
  ["Risk Reward Ratio", self.rr],
355
354
  ["Win Rate", f"{win_rate}%"],
356
355
  ["Sharpe Ratio", self.sharpe()],
@@ -384,6 +383,7 @@ class Trade(RiskManagement):
384
383
  self.logger.info(f"Session statistics saved to {filepath}")
385
384
 
386
385
  Buys = Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
386
+
387
387
  def open_buy_position(
388
388
  self,
389
389
  action: Buys = 'BMKT',
@@ -416,7 +416,6 @@ class Trade(RiskManagement):
416
416
  "You need to set a price for pending orders")
417
417
  else:
418
418
  _price = self.get_tick_info(self.symbol).ask
419
- digits = self.get_symbol_info(self.symbol).digits
420
419
 
421
420
  lot = self.get_lot()
422
421
  stop_loss = self.get_stop_loss()
@@ -468,6 +467,7 @@ class Trade(RiskManagement):
468
467
  return type
469
468
 
470
469
  Sells = Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
470
+
471
471
  def open_sell_position(
472
472
  self,
473
473
  action: Sells = 'SMKT',
@@ -500,7 +500,6 @@ class Trade(RiskManagement):
500
500
  "You need to set a price for pending orders")
501
501
  else:
502
502
  _price = self.get_tick_info(self.symbol).bid
503
- digits = self.get_symbol_info(self.symbol).digits
504
503
 
505
504
  lot = self.get_lot()
506
505
  stop_loss = self.get_stop_loss()
@@ -553,7 +552,8 @@ class Trade(RiskManagement):
553
552
  self.logger.info(f"Not Trading time, SYMBOL={self.symbol}")
554
553
  return False
555
554
  elif not self.is_risk_ok():
556
- self.logger.error(f"Risk not allowed, SYMBOL={self.symbol}")
555
+ self.logger.error(
556
+ f"Account Risk not allowed, SYMBOL={self.symbol}")
557
557
  self._check(comment)
558
558
  return False
559
559
  elif self.profit_target():
@@ -561,9 +561,9 @@ class Trade(RiskManagement):
561
561
  return True
562
562
 
563
563
  def _check(self, txt: str = ""):
564
- if (self.positive_profit(id=self.expert_id)
565
- or self.get_current_positions() is None
566
- ):
564
+ if (self.positive_profit(id=self.expert_id)
565
+ or self.get_current_positions() is None
566
+ ):
567
567
  self.close_positions(position_type='all')
568
568
  self.logger.info(txt)
569
569
  time.sleep(5)
@@ -582,7 +582,7 @@ class Trade(RiskManagement):
582
582
  price (float): Price for opening the position
583
583
  request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
584
584
  all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py
585
-
585
+
586
586
  type (str): The type of the order `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
587
587
  """
588
588
  # Send a trading request
@@ -590,23 +590,25 @@ class Trade(RiskManagement):
590
590
  pos = self._order_type()[type][1]
591
591
  addtionnal = f", SYMBOL={self.symbol}"
592
592
  try:
593
- check = self.check_order(request)
593
+ self.check_order(request)
594
594
  result = self.send_order(request)
595
595
  except Exception as e:
596
596
  print(f"{self.current_datetime()} -", end=' ')
597
597
  trade_retcode_message(
598
598
  result.retcode, display=True, add_msg=f"{e}{addtionnal}")
599
599
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
600
- msg = trade_retcode_message(result.retcode)
601
- self.logger.error(
602
- f"Trade Order Request, RETCODE={result.retcode}: {msg}{addtionnal}")
600
+ if result.retcode not in self._retcodes:
601
+ self._retcodes.append(result.retcode)
602
+ msg = trade_retcode_message(result.retcode)
603
+ self.logger.error(
604
+ f"Trade Order Request, RETCODE={result.retcode}: {msg}{addtionnal}")
603
605
  if result.retcode in [
604
606
  Mt5.TRADE_RETCODE_CONNECTION, Mt5.TRADE_RETCODE_TIMEOUT]:
605
607
  tries = 0
606
608
  while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
607
609
  time.sleep(1)
608
610
  try:
609
- check = self.check_order(request)
611
+ self.check_order(request)
610
612
  result = self.send_order(request)
611
613
  except Exception as e:
612
614
  print(f"{self.current_datetime()} -", end=' ')
@@ -650,7 +652,7 @@ class Trade(RiskManagement):
650
652
  f"{self.get_account_info().currency}]\n"
651
653
  )
652
654
  self.logger.info(pos_info)
653
-
655
+
654
656
  def open_position(
655
657
  self,
656
658
  action: Buys | Sells,
@@ -682,7 +684,8 @@ class Trade(RiskManagement):
682
684
  self.open_sell_position(
683
685
  action=action, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
684
686
  else:
685
- raise ValueError(f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}")
687
+ raise ValueError(
688
+ f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}")
686
689
 
687
690
  @property
688
691
  def orders(self):
@@ -766,7 +769,7 @@ class Trade(RiskManagement):
766
769
  continue
767
770
  if filter_type == 'sells' and item.type != 1:
768
771
  continue
769
- if filter_type == 'losings' and item.profit > 0:
772
+ if filter_type == 'losings' and item.profit > 0:
770
773
  continue
771
774
  if filter_type == 'profitables' and not self.win_trade(item, th=th):
772
775
  continue
@@ -791,19 +794,19 @@ class Trade(RiskManagement):
791
794
 
792
795
  def get_current_buy_stops(self, id: Optional[int] = None) -> List[int] | None:
793
796
  return self.get_filtered_tickets(id=id, filter_type='buy_stops')
794
-
797
+
795
798
  def get_current_sell_stops(self, id: Optional[int] = None) -> List[int] | None:
796
799
  return self.get_filtered_tickets(id=id, filter_type='sell_stops')
797
-
800
+
798
801
  def get_current_buy_limits(self, id: Optional[int] = None) -> List[int] | None:
799
802
  return self.get_filtered_tickets(id=id, filter_type='buy_limits')
800
-
803
+
801
804
  def get_current_sell_limits(self, id: Optional[int] = None) -> List[int] | None:
802
805
  return self.get_filtered_tickets(id=id, filter_type='sell_limits')
803
-
806
+
804
807
  def get_current_buy_stop_limits(self, id: Optional[int] = None) -> List[int] | None:
805
808
  return self.get_filtered_tickets(id=id, filter_type='buy_stop_limits')
806
-
809
+
807
810
  def get_current_sell_stop_limits(self, id: Optional[int] = None) -> List[int] | None:
808
811
  return self.get_filtered_tickets(id=id, filter_type='sell_stop_limits')
809
812
 
@@ -842,7 +845,8 @@ class Trade(RiskManagement):
842
845
  # All open positions for a specific strategy or expert no matter the symbol
843
846
  positions = self.get_positions()
844
847
  if positions is not None:
845
- positions = [position for position in positions if position.magic == id]
848
+ positions = [
849
+ position for position in positions if position.magic == id]
846
850
  elif not account and id is None:
847
851
  # All open positions for the current symbol no matter the strategy or expert
848
852
  positions = self.get_positions(symbol=self.symbol)
@@ -850,8 +854,9 @@ class Trade(RiskManagement):
850
854
  # All open positions for the current symbol and a specific strategy or expert
851
855
  positions = self.get_positions(symbol=self.symbol)
852
856
  if positions is not None:
853
- positions = [position for position in positions if position.magic == id]
854
-
857
+ positions = [
858
+ position for position in positions if position.magic == id]
859
+
855
860
  if positions is not None:
856
861
  profit = 0.0
857
862
  balance = self.get_account_info().balance
@@ -864,7 +869,7 @@ class Trade(RiskManagement):
864
869
  return current_profit >= th_profit
865
870
  return False
866
871
 
867
- def break_even(self, mm=True,
872
+ def break_even(self, mm=True,
868
873
  id: Optional[int] = None,
869
874
  trail: Optional[bool] = True,
870
875
  stop_trail: Optional[int] = None,
@@ -874,8 +879,9 @@ class Trade(RiskManagement):
874
879
  """
875
880
  This function checks if it's time to set the break-even level for a trading position.
876
881
  If it is, it sets the break-even level. If the break-even level has already been set,
877
- it checks if the price has moved in a favorable direction. If it has, and the trail parameter is set to True,
878
- it updates the break-even level based on the trail_after_points and stop_trail parameters.
882
+ it checks if the price has moved in a favorable direction.
883
+ If it has, and the trail parameter is set to True, it updates
884
+ the break-even level based on the trail_after_points and stop_trail parameters.
879
885
 
880
886
  Args:
881
887
  id (int): The strategy ID or expert ID.
@@ -883,8 +889,10 @@ class Trade(RiskManagement):
883
889
  trail (bool): Whether to trail the stop loss or not.
884
890
  stop_trail (int): Number of points to trail the stop loss by.
885
891
  It represent the distance from the current price to the stop loss.
886
- trail_after_points (int): Number of points in profit from where the strategy will start to trail the stop loss.
887
- be_plus_points (int): Number of points to add to the break-even level. Represents the minimum profit to secure.
892
+ trail_after_points (int): Number of points in profit
893
+ from where the strategy will start to trail the stop loss.
894
+ be_plus_points (int): Number of points to add to the break-even level.
895
+ Represents the minimum profit to secure.
888
896
  """
889
897
  time.sleep(0.1)
890
898
  if not mm:
@@ -910,7 +918,8 @@ class Trade(RiskManagement):
910
918
  if position.ticket not in self.break_even_status:
911
919
  price = None
912
920
  if be_plus_points is not None:
913
- price = position.price_open + (be_plus_points * point)
921
+ price = position.price_open + \
922
+ (be_plus_points * point)
914
923
  self.set_break_even(position, be, price=price)
915
924
  self.break_even_status.append(position.ticket)
916
925
  self.break_even_points[position.ticket] = be
@@ -919,32 +928,40 @@ class Trade(RiskManagement):
919
928
  if not trail:
920
929
  continue
921
930
  # Check if the price has moved favorably
922
- new_be = round(be * 0.10) if be_plus_points is None else be_plus_points
931
+ new_be = round(
932
+ be * 0.10) if be_plus_points is None else be_plus_points
923
933
  if trail_after_points is not None:
924
934
  if position.ticket not in self.trail_after_points:
925
935
  # This ensures that the position rich the minimum points required
926
936
  # before the trail can be set
927
937
  new_be = trail_after_points - be
928
- self.trail_after_points.append(position.ticket)
938
+ self.trail_after_points.append(
939
+ position.ticket)
929
940
  new_be_points = self.break_even_points[position.ticket] + new_be
930
- favorable_move = float(points/point) >= new_be_points
941
+ favorable_move = float(
942
+ points/point) >= new_be_points
931
943
  if favorable_move:
932
944
  # This allows the position to go to take profit in case of a swing trade
933
945
  # If is a scalping position, we can set the stop_trail close to the current price.
934
- trail_points = round(be * 0.50) if stop_trail is None else stop_trail
946
+ trail_points = round(
947
+ be * 0.50) if stop_trail is None else stop_trail
935
948
  # Calculate the new break-even level and price
936
949
  if position.type == 0:
937
950
  # This level validate the favorable move of the price
938
- new_level = round(position.price_open + (new_be_points * point), digits)
939
- # This price is set away from the current price by the trail_points
940
- new_price = round(position.price_current - (trail_points * point), digits)
951
+ new_level = round(
952
+ position.price_open + (new_be_points * point), digits)
953
+ # This price is set away from the current price by the trail_points
954
+ new_price = round(
955
+ position.price_current - (trail_points * point), digits)
941
956
  if new_price < position.sl:
942
- new_price = position.sl
957
+ new_price = position.sl
943
958
  elif position.type == 1:
944
- new_level = round(position.price_open - (new_be_points * point), digits)
945
- new_price = round(position.price_current + (trail_points * point), digits)
959
+ new_level = round(
960
+ position.price_open - (new_be_points * point), digits)
961
+ new_price = round(
962
+ position.price_current + (trail_points * point), digits)
946
963
  if new_price > position.sl:
947
- new_price = position.sl
964
+ new_price = position.sl
948
965
  self.set_break_even(
949
966
  position, be, price=new_price, level=new_level
950
967
  )
@@ -1024,7 +1041,7 @@ class Trade(RiskManagement):
1024
1041
  addtionnal = f", SYMBOL={self.symbol}"
1025
1042
  time.sleep(0.1)
1026
1043
  try:
1027
- check_result = self.check_order(request)
1044
+ self.check_order(request)
1028
1045
  result = self.send_order(request)
1029
1046
  except Exception as e:
1030
1047
  print(f"{self.current_datetime()} -", end=' ')
@@ -1042,7 +1059,7 @@ class Trade(RiskManagement):
1042
1059
  else:
1043
1060
  time.sleep(1)
1044
1061
  try:
1045
- check_result = self.check_order(request)
1062
+ self.check_order(request)
1046
1063
  result = self.send_order(request)
1047
1064
  except Exception as e:
1048
1065
  print(f"{self.current_datetime()} -", end=' ')
@@ -1075,7 +1092,6 @@ class Trade(RiskManagement):
1075
1092
  value = self.get_symbol_info(self.symbol).trade_tick_value
1076
1093
  points = position.profit * (size / value / position.volume)
1077
1094
 
1078
- spread = self.get_symbol_info(self.symbol).spread
1079
1095
  point = self.get_symbol_info(self.symbol).point
1080
1096
  fees = self.get_stats()[0]["average_fee"] * -1
1081
1097
  risk = self.currency_risk()["trade_profit"]
@@ -1085,7 +1101,7 @@ class Trade(RiskManagement):
1085
1101
  win_be = th
1086
1102
  else:
1087
1103
  win_be = max(min_be, round((0.1 * be)))
1088
- win_trade = float(points/point) >= be
1104
+ win_trade = float(points/point) >= win_be
1089
1105
  # Check if the positon is in profit
1090
1106
  if win_trade:
1091
1107
  # Check if break-even has already been set for this position
@@ -1130,7 +1146,7 @@ class Trade(RiskManagement):
1130
1146
  ticket = request[type]
1131
1147
  addtionnal = f", SYMBOL={self.symbol}"
1132
1148
  try:
1133
- check_result = self.check_order(request)
1149
+ self.check_order(request)
1134
1150
  result = self.send_order(request)
1135
1151
  except Exception as e:
1136
1152
  print(f"{self.current_datetime()} -", end=' ')
@@ -1144,7 +1160,7 @@ class Trade(RiskManagement):
1144
1160
  while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
1145
1161
  time.sleep(1)
1146
1162
  try:
1147
- check_result = self.check_order(request)
1163
+ self.check_order(request)
1148
1164
  result = self.send_order(request)
1149
1165
  except Exception as e:
1150
1166
  print(f"{self.current_datetime()} -", end=' ')
@@ -1163,10 +1179,10 @@ class Trade(RiskManagement):
1163
1179
  return True
1164
1180
  else:
1165
1181
  return False
1166
-
1167
- def close_order(self,
1168
- ticket: int,
1169
- id: Optional[int] = None,
1182
+
1183
+ def close_order(self,
1184
+ ticket: int,
1185
+ id: Optional[int] = None,
1170
1186
  comment: Optional[str] = None):
1171
1187
  """
1172
1188
  Close an open order by it ticket
@@ -1180,14 +1196,14 @@ class Trade(RiskManagement):
1180
1196
  - True if order closed, False otherwise
1181
1197
  """
1182
1198
  request = {
1183
- "action": Mt5.TRADE_ACTION_REMOVE,
1184
- "symbol": self.symbol,
1185
- "order": ticket,
1186
- "magic": id if id is not None else self.expert_id,
1187
- "comment": f"@{self.expert_name}" if comment is None else comment,
1188
- }
1199
+ "action": Mt5.TRADE_ACTION_REMOVE,
1200
+ "symbol": self.symbol,
1201
+ "order": ticket,
1202
+ "magic": id if id is not None else self.expert_id,
1203
+ "comment": f"@{self.expert_name}" if comment is None else comment,
1204
+ }
1189
1205
  return self.close_request(request, type="order")
1190
-
1206
+
1191
1207
  def close_position(self,
1192
1208
  ticket: int,
1193
1209
  id: Optional[int] = None,
@@ -1212,15 +1228,13 @@ class Trade(RiskManagement):
1212
1228
  positions = self.get_positions(ticket=ticket)
1213
1229
  buy_price = self.get_tick_info(self.symbol).ask
1214
1230
  sell_price = self.get_tick_info(self.symbol).bid
1215
- digits = self.get_symbol_info(self.symbol).digits
1216
1231
  deviation = self.get_deviation()
1217
1232
  if positions is not None and len(positions) == 1:
1218
1233
  position = positions[0]
1219
1234
  if (position.ticket == ticket
1220
- and position.magic == Id
1221
- ):
1235
+ and position.magic == Id
1236
+ ):
1222
1237
  buy = position.type == 0
1223
- sell = position.type == 1
1224
1238
  request = {
1225
1239
  "action": Mt5.TRADE_ACTION_DEAL,
1226
1240
  "symbol": self.symbol,
@@ -1235,15 +1249,14 @@ class Trade(RiskManagement):
1235
1249
  "type_filling": Mt5.ORDER_FILLING_FOK,
1236
1250
  }
1237
1251
  return self.close_request(request, type="position")
1238
-
1239
- def bulk_close(self,
1240
- tickets: List,
1252
+
1253
+ def bulk_close(self,
1254
+ tickets: List,
1241
1255
  tikets_type: Literal["positions", "orders"],
1242
- close_func: Callable,
1243
- order_type: str,
1256
+ close_func: Callable,
1257
+ order_type: str,
1244
1258
  id: Optional[int] = None,
1245
1259
  comment: Optional[str] = None):
1246
-
1247
1260
  """
1248
1261
  Close multiple orders or positions at once.
1249
1262
 
@@ -1257,13 +1270,12 @@ class Trade(RiskManagement):
1257
1270
  """
1258
1271
  if order_type == 'all':
1259
1272
  order_type = 'open'
1260
- if len(tickets) > 0:
1273
+ if tickets is not None and len(tickets) > 0:
1261
1274
  for ticket in tickets.copy():
1262
1275
  if close_func(ticket, id=id, comment=comment):
1263
1276
  tickets.remove(ticket)
1264
1277
  time.sleep(1)
1265
-
1266
- if len(tickets) == 0:
1278
+ if tickets is not None and len(tickets) == 0:
1267
1279
  self.logger.info(
1268
1280
  f"ALL {order_type.upper()} {tikets_type.upper()} closed, SYMBOL={self.symbol}.")
1269
1281
  else:
@@ -1273,11 +1285,12 @@ class Trade(RiskManagement):
1273
1285
  self.logger.info(
1274
1286
  f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}.")
1275
1287
 
1276
- Orders = Literal["all", "buy_stops", "sell_stops", "buy_limits",
1288
+ Orders = Literal["all", "buy_stops", "sell_stops", "buy_limits",
1277
1289
  "sell_limits", "buy_stop_limits", "sell_stop_limits"]
1278
- def close_orders(self,
1279
- order_type: Orders,
1280
- id: Optional[int] = None,
1290
+
1291
+ def close_orders(self,
1292
+ order_type: Orders,
1293
+ id: Optional[int] = None,
1281
1294
  comment: Optional[str] = None):
1282
1295
  """
1283
1296
  Args:
@@ -1307,6 +1320,7 @@ class Trade(RiskManagement):
1307
1320
  orders, "orders", self.close_order, order_type, id=id, comment=comment)
1308
1321
 
1309
1322
  Positions = Literal["all", "buy", "sell", "profitable", "losing"]
1323
+
1310
1324
  def close_positions(
1311
1325
  self,
1312
1326
  position_type: Positions,
@@ -1347,7 +1361,6 @@ class Trade(RiskManagement):
1347
1361
  loss_trades = 0
1348
1362
  win_trades = 0
1349
1363
  balance = self.get_account_info().balance
1350
- target = round((balance * self.target)/100)
1351
1364
  deals = len(self.opened_positions)
1352
1365
  if deals != 0:
1353
1366
  for position in self.opened_positions:
@@ -1474,27 +1487,28 @@ class Trade(RiskManagement):
1474
1487
  else:
1475
1488
  # claculate number of minute from the end to the start
1476
1489
  start = datetime.strptime(self.start, '%H:%M')
1477
- end = datetime.strptime(self.current_time(), '%H:%M')
1490
+ end = datetime.strptime(self.current_time(), '%H:%M')
1478
1491
  minutes = (end - start).total_seconds() // 60
1479
1492
  sleep_time = (24*60) - minutes
1480
1493
  return sleep_time
1481
1494
 
1482
1495
  def current_datetime(self):
1483
1496
  return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1484
-
1497
+
1485
1498
  def current_time(self, seconds=False):
1486
1499
  if seconds:
1487
1500
  return datetime.now().strftime("%H:%M:%S")
1488
1501
  return datetime.now().strftime("%H:%M")
1489
1502
 
1503
+
1490
1504
  def create_trade_instance(
1491
- symbols: List[str],
1492
- params: Dict[str, Any],
1493
- daily_risk: Optional[Dict[str, float]] = None,
1494
- max_risk: Optional[Dict[str, float]] = None,
1495
- pchange_sl: Optional[Dict[str, float] | float] = None,
1496
- **kwargs
1497
- ) -> Dict[str, Trade]:
1505
+ symbols: List[str],
1506
+ params: Dict[str, Any],
1507
+ daily_risk: Optional[Dict[str, float]] = None,
1508
+ max_risk: Optional[Dict[str, float]] = None,
1509
+ pchange_sl: Optional[Dict[str, float] | float] = None,
1510
+ **kwargs
1511
+ ) -> Dict[str, Trade]:
1498
1512
  """
1499
1513
  Creates Trade instances for each symbol provided.
1500
1514
 
@@ -1509,59 +1523,74 @@ def create_trade_instance(
1509
1523
 
1510
1524
  Raises:
1511
1525
  ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
1512
-
1526
+
1513
1527
  Note:
1514
1528
  `daily_risk` and `max_risk` can be used to manage the risk of each symbol
1515
1529
  based on the importance of the symbol in the portfolio or strategy.
1516
1530
  """
1517
1531
  logger = params.get('logger', None)
1532
+ ids = params.get('expert_id', None)
1518
1533
  trade_instances = {}
1519
1534
  if not symbols:
1520
1535
  raise ValueError("The 'symbols' list cannot be empty.")
1521
1536
  if not params:
1522
1537
  raise ValueError("The 'params' dictionary cannot be empty.")
1523
-
1538
+
1524
1539
  if daily_risk is not None:
1525
1540
  for symbol in symbols:
1526
1541
  if symbol not in daily_risk:
1527
- raise ValueError(f"Missing daily risk weight for symbol '{symbol}'.")
1542
+ raise ValueError(
1543
+ f"Missing daily risk weight for symbol '{symbol}'.")
1528
1544
  if max_risk is not None:
1529
1545
  for symbol in symbols:
1530
1546
  if symbol not in max_risk:
1531
- raise ValueError(f"Missing maximum risk percentage for symbol '{symbol}'.")
1547
+ raise ValueError(
1548
+ f"Missing maximum risk percentage for symbol '{symbol}'.")
1532
1549
  if pchange_sl is not None:
1533
1550
  if isinstance(pchange_sl, dict):
1534
1551
  for symbol in symbols:
1535
1552
  if symbol not in pchange_sl:
1536
- raise ValueError(f"Missing percentage change for symbol '{symbol}'.")
1537
-
1553
+ raise ValueError(
1554
+ f"Missing percentage change for symbol '{symbol}'.")
1555
+ if isinstance(ids, dict):
1556
+ for symbol in symbols:
1557
+ if symbol not in ids:
1558
+ raise ValueError(f"Missing expert ID for symbol '{symbol}'.")
1559
+
1538
1560
  for symbol in symbols:
1539
1561
  try:
1540
1562
  params['symbol'] = symbol
1563
+ params['expert_id'] = (
1564
+ ids[symbol] if ids is not None and isinstance(ids, dict) else
1565
+ ids if ids is not None and isinstance(ids, (int, float)) else
1566
+ params['expert_id'] if 'expert_id' in params else None
1567
+ )
1541
1568
  params['pchange_sl'] = (
1542
1569
  pchange_sl[symbol] if pchange_sl is not None
1543
- and isinstance(pchange_sl, dict) else
1544
- pchange_sl if pchange_sl is not None
1545
- and isinstance(pchange_sl, (int, float)) else
1570
+ and isinstance(pchange_sl, dict) else
1571
+ pchange_sl if pchange_sl is not None
1572
+ and isinstance(pchange_sl, (int, float)) else
1546
1573
  params['pchange_sl'] if 'pchange_sl' in params else None
1547
1574
  )
1548
1575
  params['daily_risk'] = (
1549
- daily_risk[symbol] if daily_risk is not None else
1576
+ daily_risk[symbol] if daily_risk is not None else
1550
1577
  params['daily_risk'] if 'daily_risk' in params else None
1551
1578
  )
1552
1579
  params['max_risk'] = (
1553
- max_risk[symbol] if max_risk is not None else
1580
+ max_risk[symbol] if max_risk is not None else
1554
1581
  params['max_risk'] if 'max_risk' in params else 10.0
1555
1582
  )
1556
1583
  trade_instances[symbol] = Trade(**params)
1557
1584
  except Exception as e:
1558
1585
  logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
1559
-
1586
+
1560
1587
  if len(trade_instances) != len(symbols):
1561
1588
  for symbol in symbols:
1562
1589
  if symbol not in trade_instances:
1563
1590
  if logger is not None and isinstance(logger, Logger):
1564
- logger.error(f"Failed to create Trade instance for SYMBOL={symbol}")
1591
+ logger.error(
1592
+ f"Failed to create Trade instance for SYMBOL={symbol}")
1565
1593
  else:
1566
- raise ValueError(f"Failed to create Trade instance for SYMBOL={symbol}")
1567
- return trade_instances
1594
+ raise ValueError(
1595
+ f"Failed to create Trade instance for SYMBOL={symbol}")
1596
+ return trade_instances