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

@@ -2,17 +2,41 @@ import os
2
2
  import csv
3
3
  import time
4
4
  import numpy as np
5
+ import pandas as pd
5
6
  from datetime import datetime
6
7
  import MetaTrader5 as Mt5
7
8
  from logging import Logger
8
9
  from tabulate import tabulate
9
- from typing import List, Tuple, Dict, Any, Optional, Literal
10
+ from typing import (
11
+ List,
12
+ Tuple,
13
+ Dict,
14
+ Any,
15
+ Optional,
16
+ Literal,
17
+ Callable
18
+ )
10
19
  from bbstrader.btengine.performance import create_sharpe_ratio
11
20
  from bbstrader.metatrader.risk import RiskManagement
12
- from bbstrader.metatrader.account import INIT_MSG
21
+ from bbstrader.metatrader.account import(
22
+ check_mt5_connection,
23
+ INIT_MSG
24
+ )
13
25
  from bbstrader.metatrader.utils import (
14
- TimeFrame, TradePosition, TickInfo,
15
- raise_mt5_error, trade_retcode_message, config_logger)
26
+ TimeFrame,
27
+ TradePosition,
28
+ TickInfo,
29
+ raise_mt5_error,
30
+ trade_retcode_message
31
+ )
32
+ from bbstrader.config import config_logger, BBSTRADER_DIR
33
+
34
+
35
+ __all__ = [
36
+ 'Trade',
37
+ 'create_trade_instance',
38
+ ]
39
+
16
40
 
17
41
  class Trade(RiskManagement):
18
42
  """
@@ -75,7 +99,6 @@ class Trade(RiskManagement):
75
99
  expert_id: int = 9818,
76
100
  version: str = '1.0',
77
101
  target: float = 5.0,
78
- be_on_trade_open: bool = True,
79
102
  start_time: str = "1:00",
80
103
  finishing_time: str = "23:00",
81
104
  ending_time: str = "23:30",
@@ -93,8 +116,7 @@ class Trade(RiskManagement):
93
116
  expert_id (int): The `unique ID` used to identify the expert advisor
94
117
  or the strategy used on the symbol.
95
118
  version (str): The `version` of the expert advisor.
96
- target (float): `Trading period (day, week, month) profit target` in percentage
97
- be_on_trade_open (bool): Whether to check for break-even when opening a trade.
119
+ target (float): `Trading period (day, week, month) profit target` in percentage.
98
120
  start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
99
121
  finishing_time (str): The time after which no new position can be opened.
100
122
  ending_time (str): The time after which any open position will be closed.
@@ -131,7 +153,6 @@ class Trade(RiskManagement):
131
153
  self.expert_id = expert_id
132
154
  self.version = version
133
155
  self.target = target
134
- self.be_on_trade_open = be_on_trade_open
135
156
  self.verbose = verbose
136
157
  self.start = start_time
137
158
  self.end = ending_time
@@ -145,13 +166,13 @@ class Trade(RiskManagement):
145
166
  ":")
146
167
  self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
147
168
 
148
- self.buy_positions = []
149
- self.sell_positions = []
150
- self.opened_positions = []
151
- self.opened_orders = []
152
- self.break_even_status = []
153
- self.break_even_points = {}
154
- self.trail_after_points = {}
169
+ self.buy_positions = []
170
+ self.sell_positions = []
171
+ self.opened_positions = []
172
+ self.opened_orders = []
173
+ self.break_even_status = []
174
+ self.break_even_points = {}
175
+ self.trail_after_points = []
155
176
 
156
177
  self.initialize()
157
178
  self.select_symbol()
@@ -168,7 +189,9 @@ class Trade(RiskManagement):
168
189
  def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
169
190
  """Get the logger object"""
170
191
  if isinstance(logger, str):
171
- return config_logger(logger, consol_log=consol_log)
192
+ log_path = BBSTRADER_DIR / 'logs'
193
+ log_path.mkdir(exist_ok=True)
194
+ return config_logger(f'{log_path}/{logger}', consol_log)
172
195
  return logger
173
196
 
174
197
  def initialize(self):
@@ -184,8 +207,7 @@ class Trade(RiskManagement):
184
207
  try:
185
208
  if self.verbose:
186
209
  print("\nInitializing the basics.")
187
- if not Mt5.initialize():
188
- raise_mt5_error(message=INIT_MSG)
210
+ check_mt5_connection()
189
211
  if self.verbose:
190
212
  print(
191
213
  f"You are running the @{self.expert_name} Expert advisor,"
@@ -306,14 +328,10 @@ class Trade(RiskManagement):
306
328
  """
307
329
  stats, additional_stats = self.get_stats()
308
330
 
309
- deals = stats["deals"]
310
- wins = stats["win_trades"]
311
- losses = stats["loss_trades"]
312
331
  profit = round(stats["profit"], 2)
313
332
  win_rate = stats["win_rate"]
314
333
  total_fees = round(stats["total_fees"], 3)
315
334
  average_fee = round(stats["average_fee"], 3)
316
- profitability = additional_stats["profitability"]
317
335
  currency = self.get_account_info().currency
318
336
  net_profit = round((profit + total_fees), 2)
319
337
  trade_risk = round(self.get_currency_risk() * -1, 2)
@@ -321,9 +339,9 @@ class Trade(RiskManagement):
321
339
 
322
340
  # Formatting the statistics output
323
341
  session_data = [
324
- ["Total Trades", deals],
325
- ["Winning Trades", wins],
326
- ["Losing Trades", losses],
342
+ ["Total Trades", stats["deals"]],
343
+ ["Winning Trades", stats["win_trades"]],
344
+ ["Losing Trades", stats["loss_trades"]],
327
345
  ["Session Profit", f"{profit} {currency}"],
328
346
  ["Total Fees", f"{total_fees} {currency}"],
329
347
  ["Average Fees", f"{average_fee} {currency}"],
@@ -333,9 +351,10 @@ class Trade(RiskManagement):
333
351
  ["Risk Reward Ratio", self.rr],
334
352
  ["Win Rate", f"{win_rate}%"],
335
353
  ["Sharpe Ratio", self.sharpe()],
336
- ["Trade Profitability", profitability],
354
+ ["Trade Profitability", additional_stats["profitability"]],
337
355
  ]
338
- session_table = tabulate(session_data, headers=["Statistics", "Values"], tablefmt="outline")
356
+ session_table = tabulate(
357
+ session_data, headers=["Statistics", "Values"], tablefmt="outline")
339
358
 
340
359
  # Print the formatted statistics
341
360
  if self.verbose:
@@ -346,24 +365,10 @@ class Trade(RiskManagement):
346
365
  if save:
347
366
  today_date = datetime.now().strftime('%Y%m%d%H%M%S')
348
367
  # Create a dictionary with the statistics
349
- statistics_dict = {
350
- "Total Trades": deals,
351
- "Winning Trades": wins,
352
- "Losing Trades": losses,
353
- "Session Profit": f"{profit} {currency}",
354
- "Total Fees": f"{total_fees} {currency}",
355
- "Average Fees": f"{average_fee} {currency}",
356
- "Net Profit": f"{net_profit} {currency}",
357
- "Risk per Trade": f"{trade_risk} {currency}",
358
- "Expected Profit per Trade": f"{expected_profit} {currency}",
359
- "Risk Reward Ratio": self.rr,
360
- "Win Rate": f"{win_rate}%",
361
- "Sharpe Ratio": self.sharpe(),
362
- "Trade Profitability": profitability,
363
- }
368
+ statistics_dict = {item[0]: item[1] for item in session_data}
369
+ stats_df = pd.DataFrame(statistics_dict, index=[0])
364
370
  # Create the directory if it doesn't exist
365
- if dir is None:
366
- dir = f"{self.expert_name}_session_stats"
371
+ dir = dir or '.sessions'
367
372
  os.makedirs(dir, exist_ok=True)
368
373
  if '.' in self.symbol:
369
374
  symbol = self.symbol.split('.')[0]
@@ -372,15 +377,7 @@ class Trade(RiskManagement):
372
377
 
373
378
  filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
374
379
  filepath = os.path.join(dir, filename)
375
-
376
- # Updated code to write to CSV
377
- with open(filepath, mode="w", newline='', encoding='utf-8') as csv_file:
378
- writer = csv.writer(
379
- csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL
380
- )
381
- writer.writerow(["Statistic", "Value"])
382
- for stat, value in statistics_dict.items():
383
- writer.writerow([stat, value])
380
+ stats_df.to_csv(filepath, index=False)
384
381
  self.logger.info(f"Session statistics saved to {filepath}")
385
382
 
386
383
  Buys = Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
@@ -388,6 +385,7 @@ class Trade(RiskManagement):
388
385
  self,
389
386
  action: Buys = 'BMKT',
390
387
  price: Optional[float] = None,
388
+ stoplimit: Optional[float] = None,
391
389
  mm: bool = True,
392
390
  id: Optional[int] = None,
393
391
  comment: Optional[str] = None
@@ -399,6 +397,8 @@ class Trade(RiskManagement):
399
397
  action (str): `'BMKT'` for Market orders or `'BLMT',
400
398
  'BSTP','BSTPLMT'` for pending orders
401
399
  price (float): The price at which to open an order
400
+ stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
401
+ The pending order is not passed to the trading system until that moment
402
402
  id (int): The strategy id or expert Id
403
403
  mm (bool): Weither to put stop loss and tp or not
404
404
  comment (str): The comment for the opening position
@@ -406,9 +406,11 @@ class Trade(RiskManagement):
406
406
  Id = id if id is not None else self.expert_id
407
407
  point = self.get_symbol_info(self.symbol).point
408
408
  if action != 'BMKT':
409
- assert price is not None, \
410
- "You need to set a price for pending orders"
411
- _price = price
409
+ if price is not None:
410
+ _price = price
411
+ else:
412
+ raise ValueError(
413
+ "You need to set a price for pending orders")
412
414
  else:
413
415
  _price = self.get_tick_info(self.symbol).ask
414
416
  digits = self.get_symbol_info(self.symbol).digits
@@ -429,14 +431,23 @@ class Trade(RiskManagement):
429
431
  "type_time": Mt5.ORDER_TIME_GTC,
430
432
  "type_filling": Mt5.ORDER_FILLING_FOK,
431
433
  }
432
- if mm:
433
- request['sl'] = (_price - stop_loss * point)
434
- request['tp'] = (_price + take_profit * point)
434
+ mm_price = _price
435
435
  if action != 'BMKT':
436
436
  request["action"] = Mt5.TRADE_ACTION_PENDING
437
437
  request["type"] = self._order_type()[action][0]
438
- if self.be_on_trade_open:
439
- self.break_even(mm=mm, id=Id)
438
+ if action == 'BSTPLMT':
439
+ if stoplimit is None:
440
+ raise ValueError(
441
+ "You need to set a stoplimit price for BSTPLMT orders")
442
+ if stoplimit > _price:
443
+ raise ValueError(
444
+ "Stoplimit price must be less than the price and greater than the current price")
445
+ request["stoplimit"] = stoplimit
446
+ mm_price = stoplimit
447
+ if mm:
448
+ request["sl"] = (mm_price - stop_loss * point)
449
+ request["tp"] = (mm_price + take_profit * point)
450
+ self.break_even(mm=mm, id=Id)
440
451
  if self.check(comment):
441
452
  self.request_result(_price, request, action),
442
453
 
@@ -458,6 +469,7 @@ class Trade(RiskManagement):
458
469
  self,
459
470
  action: Sells = 'SMKT',
460
471
  price: Optional[float] = None,
472
+ stoplimit: Optional[float] = None,
461
473
  mm: bool = True,
462
474
  id: Optional[int] = None,
463
475
  comment: Optional[str] = None
@@ -469,6 +481,8 @@ class Trade(RiskManagement):
469
481
  action (str): `'SMKT'` for Market orders
470
482
  or `'SLMT', 'SSTP','SSTPLMT'` for pending orders
471
483
  price (float): The price at which to open an order
484
+ stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
485
+ The pending order is not passed to the trading system until that moment
472
486
  id (int): The strategy id or expert Id
473
487
  mm (bool): Weither to put stop loss and tp or not
474
488
  comment (str): The comment for the closing position
@@ -476,9 +490,11 @@ class Trade(RiskManagement):
476
490
  Id = id if id is not None else self.expert_id
477
491
  point = self.get_symbol_info(self.symbol).point
478
492
  if action != 'SMKT':
479
- assert price is not None, \
480
- "You need to set a price for pending orders"
481
- _price = price
493
+ if price is not None:
494
+ _price = price
495
+ else:
496
+ raise ValueError(
497
+ "You need to set a price for pending orders")
482
498
  else:
483
499
  _price = self.get_tick_info(self.symbol).bid
484
500
  digits = self.get_symbol_info(self.symbol).digits
@@ -499,24 +515,26 @@ class Trade(RiskManagement):
499
515
  "type_time": Mt5.ORDER_TIME_GTC,
500
516
  "type_filling": Mt5.ORDER_FILLING_FOK,
501
517
  }
502
- if mm:
503
- request["sl"] = (_price + stop_loss * point)
504
- request["tp"] = (_price - take_profit * point)
518
+ mm_price = _price
505
519
  if action != 'SMKT':
506
520
  request["action"] = Mt5.TRADE_ACTION_PENDING
507
521
  request["type"] = self._order_type()[action][0]
508
- if self.be_on_trade_open:
509
- self.break_even(mm=mm, id=Id)
522
+ if action == 'SSTPLMT':
523
+ if stoplimit is None:
524
+ raise ValueError(
525
+ "You need to set a stoplimit price for SSTPLMT orders")
526
+ if stoplimit < _price:
527
+ raise ValueError(
528
+ "Stoplimit price must be greater than the price and less than the current price")
529
+ request["stoplimit"] = stoplimit
530
+ mm_price = stoplimit
531
+ if mm:
532
+ request["sl"] = (mm_price + stop_loss * point)
533
+ request["tp"] = (mm_price - take_profit * point)
534
+ self.break_even(mm=mm, id=Id)
510
535
  if self.check(comment):
511
536
  self.request_result(_price, request, action)
512
537
 
513
- def _risk_free(self):
514
- max_trade = self.max_trade()
515
- loss_trades = self.get_stats()[0]['loss_trades']
516
- if loss_trades >= max_trade:
517
- return False
518
- return True
519
-
520
538
  def check(self, comment):
521
539
  """
522
540
  Verify if all conditions for taking a position are valide,
@@ -535,16 +553,14 @@ class Trade(RiskManagement):
535
553
  self.logger.error(f"Risk not allowed, SYMBOL={self.symbol}")
536
554
  self._check(comment)
537
555
  return False
538
- elif not self._risk_free():
539
- self.logger.error(f"Maximum trades Reached, SYMBOL={self.symbol}")
540
- self._check(comment)
541
- return False
542
556
  elif self.profit_target():
543
557
  self._check(f'Profit target Reached !!! SYMBOL={self.symbol}')
544
558
  return True
545
559
 
546
560
  def _check(self, txt: str = ""):
547
- if self.positive_profit() or self.get_current_open_positions() is None:
561
+ if (self.positive_profit(id=self.expert_id)
562
+ or self.get_current_positions() is None
563
+ ):
548
564
  self.close_positions(position_type='all')
549
565
  self.logger.info(txt)
550
566
  time.sleep(5)
@@ -571,7 +587,7 @@ class Trade(RiskManagement):
571
587
  pos = self._order_type()[type][1]
572
588
  addtionnal = f", SYMBOL={self.symbol}"
573
589
  try:
574
- check_result = self.check_order(request)
590
+ check = self.check_order(request)
575
591
  result = self.send_order(request)
576
592
  except Exception as e:
577
593
  print(f"{self.current_datetime()} -", end=' ')
@@ -587,7 +603,7 @@ class Trade(RiskManagement):
587
603
  while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
588
604
  time.sleep(1)
589
605
  try:
590
- check_result = self.check_order(request)
606
+ check = self.check_order(request)
591
607
  result = self.send_order(request)
592
608
  except Exception as e:
593
609
  print(f"{self.current_datetime()} -", end=' ')
@@ -636,6 +652,7 @@ class Trade(RiskManagement):
636
652
  self,
637
653
  action: Buys | Sells,
638
654
  price: Optional[float] = None,
655
+ stoplimit: Optional[float] = None,
639
656
  id: Optional[int] = None,
640
657
  mm: bool = True,
641
658
  comment: Optional[str] = None
@@ -646,6 +663,9 @@ class Trade(RiskManagement):
646
663
  Args:
647
664
  action (str): (`'BMKT'`, `'SMKT'`) for Market orders
648
665
  or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
666
+ price (float): The price at which to open an order
667
+ stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
668
+ The pending order is not passed to the trading system until that moment
649
669
  id (int): The strategy id or expert Id
650
670
  mm (bool): Weither to put stop loss and tp or not
651
671
  comment (str): The comment for the closing position
@@ -654,43 +674,43 @@ class Trade(RiskManagement):
654
674
  SELLS = ['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
655
675
  if action in BUYS:
656
676
  self.open_buy_position(
657
- action=action, price=price, id=id, mm=mm, comment=comment)
677
+ action=action, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
658
678
  elif action in SELLS:
659
679
  self.open_sell_position(
660
- action=action, price=price, id=id, mm=mm, comment=comment)
680
+ action=action, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
661
681
  else:
662
682
  raise ValueError(f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}")
663
683
 
664
684
  @property
665
- def get_opened_orders(self):
685
+ def orders(self):
666
686
  """ Return all opened order's tickets"""
667
687
  if len(self.opened_orders) != 0:
668
688
  return self.opened_orders
669
689
  return None
670
690
 
671
691
  @property
672
- def get_opened_positions(self):
692
+ def positions(self):
673
693
  """Return all opened position's tickets"""
674
694
  if len(self.opened_positions) != 0:
675
695
  return self.opened_positions
676
696
  return None
677
697
 
678
698
  @property
679
- def get_buy_positions(self):
699
+ def buypos(self):
680
700
  """Return all buy opened position's tickets"""
681
701
  if len(self.buy_positions) != 0:
682
702
  return self.buy_positions
683
703
  return None
684
704
 
685
705
  @property
686
- def get_sell_positions(self):
706
+ def sellpos(self):
687
707
  """Return all sell opened position's tickets"""
688
708
  if len(self.sell_positions) != 0:
689
709
  return self.sell_positions
690
710
  return None
691
711
 
692
712
  @property
693
- def get_be_positions(self):
713
+ def bepos(self):
694
714
  """Return All positon's tickets
695
715
  for which a break even has been set"""
696
716
  if len(self.break_even_status) != 0:
@@ -707,8 +727,14 @@ class Trade(RiskManagement):
707
727
 
708
728
  Args:
709
729
  id (int): The strategy id or expert Id
710
- filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'profitables')
730
+ filter_type (str): Filter type to apply on the tickets,
711
731
  - `orders` are current open orders
732
+ - `buy_stops` are current buy stop orders
733
+ - `sell_stops` are current sell stop orders
734
+ - `buy_limits` are current buy limit orders
735
+ - `sell_limits` are current sell limit orders
736
+ - `buy_stop_limits` are current buy stop limit orders
737
+ - `sell_stop_limits` are current sell stop limit orders
712
738
  - `positions` are all current open positions
713
739
  - `buys` and `sells` are current buy or sell open positions
714
740
  - `profitables` are current open position that have a profit greater than a threshold
@@ -721,8 +747,9 @@ class Trade(RiskManagement):
721
747
  or None if no tickets match the criteria.
722
748
  """
723
749
  Id = id if id is not None else self.expert_id
750
+ POSITIONS = ['positions', 'buys', 'sells', 'profitables', 'losings']
724
751
 
725
- if filter_type == 'orders':
752
+ if filter_type not in POSITIONS:
726
753
  items = self.get_orders(symbol=self.symbol)
727
754
  else:
728
755
  items = self.get_positions(symbol=self.symbol)
@@ -736,18 +763,48 @@ class Trade(RiskManagement):
736
763
  continue
737
764
  if filter_type == 'sells' and item.type != 1:
738
765
  continue
766
+ if filter_type == 'losings' and item.profit > 0:
767
+ continue
739
768
  if filter_type == 'profitables' and not self.win_trade(item, th=th):
740
769
  continue
741
- if filter_type == 'losings' and item.profit > 0:
770
+ if filter_type == 'buy_stops' and item.type != self._order_type()['BSTP'][0]:
771
+ continue
772
+ if filter_type == 'sell_stops' and item.type != self._order_type()['SSTP'][0]:
773
+ continue
774
+ if filter_type == 'buy_limits' and item.type != self._order_type()['BLMT'][0]:
775
+ continue
776
+ if filter_type == 'sell_limits' and item.type != self._order_type()['SLMT'][0]:
777
+ continue
778
+ if filter_type == 'buy_stop_limits' and item.type != self._order_type()['BSTPLMT'][0]:
779
+ continue
780
+ if filter_type == 'sell_stop_limits' and item.type != self._order_type()['SSTPLMT'][0]:
742
781
  continue
743
782
  filtered_tickets.append(item.ticket)
744
783
  return filtered_tickets if filtered_tickets else None
745
784
  return None
746
785
 
747
- def get_current_open_orders(self, id: Optional[int] = None) -> List[int] | None:
786
+ def get_current_orders(self, id: Optional[int] = None) -> List[int] | None:
748
787
  return self.get_filtered_tickets(id=id, filter_type='orders')
749
788
 
750
- def get_current_open_positions(self, id: Optional[int] = None) -> List[int] | None:
789
+ def get_current_buy_stops(self, id: Optional[int] = None) -> List[int] | None:
790
+ return self.get_filtered_tickets(id=id, filter_type='buy_stops')
791
+
792
+ def get_current_sell_stops(self, id: Optional[int] = None) -> List[int] | None:
793
+ return self.get_filtered_tickets(id=id, filter_type='sell_stops')
794
+
795
+ def get_current_buy_limits(self, id: Optional[int] = None) -> List[int] | None:
796
+ return self.get_filtered_tickets(id=id, filter_type='buy_limits')
797
+
798
+ def get_current_sell_limits(self, id: Optional[int] = None) -> List[int] | None:
799
+ return self.get_filtered_tickets(id=id, filter_type='sell_limits')
800
+
801
+ def get_current_buy_stop_limits(self, id: Optional[int] = None) -> List[int] | None:
802
+ return self.get_filtered_tickets(id=id, filter_type='buy_stop_limits')
803
+
804
+ def get_current_sell_stop_limits(self, id: Optional[int] = None) -> List[int] | None:
805
+ return self.get_filtered_tickets(id=id, filter_type='sell_stop_limits')
806
+
807
+ def get_current_positions(self, id: Optional[int] = None) -> List[int] | None:
751
808
  return self.get_filtered_tickets(id=id, filter_type='positions')
752
809
 
753
810
  def get_current_profitables(self, id: Optional[int] = None, th=None) -> List[int] | None:
@@ -862,7 +919,7 @@ class Trade(RiskManagement):
862
919
  # This ensures that the position rich the minimum points required
863
920
  # before the trail can be set
864
921
  new_be = trail_after_points - be
865
- self.trail_after_points[position.ticket] = True
922
+ self.trail_after_points.append(position.ticket)
866
923
  new_be_points = self.break_even_points[position.ticket] + new_be
867
924
  favorable_move = float(points/point) >= new_be_points
868
925
  if favorable_move:
@@ -874,10 +931,14 @@ class Trade(RiskManagement):
874
931
  # This level validate the favorable move of the price
875
932
  new_level = round(position.price_open + (new_be_points * point), digits)
876
933
  # This price is set away from the current price by the trail_points
877
- new_price = round(position.price_current - (trail_points * point), digits)
934
+ new_price = round(position.price_current - (trail_points * point), digits)
935
+ if new_price < position.sl:
936
+ new_price = position.sl
878
937
  elif position.type == 1:
879
938
  new_level = round(position.price_open - (new_be_points * point), digits)
880
- new_price = round(position.price_current + (trail_points * point), digits)
939
+ new_price = round(position.price_current + (trail_points * point), digits)
940
+ if new_price > position.sl:
941
+ new_price = position.sl
881
942
  self.set_break_even(
882
943
  position, be, price=new_price, level=new_level
883
944
  )
@@ -922,7 +983,7 @@ class Trade(RiskManagement):
922
983
  "sl": round(_price, digits),
923
984
  "tp": position.tp
924
985
  }
925
- self._break_even_request(
986
+ self.break_even_request(
926
987
  position.ticket, round(_price, digits), request)
927
988
  # If Sell
928
989
  elif position.type == 1 and position.price_current < position.price_open:
@@ -942,10 +1003,10 @@ class Trade(RiskManagement):
942
1003
  "sl": round(_price, digits),
943
1004
  "tp": position.tp
944
1005
  }
945
- self._break_even_request(
1006
+ self.break_even_request(
946
1007
  position.ticket, round(_price, digits), request)
947
1008
 
948
- def _break_even_request(self, tiket, price, request):
1009
+ def break_even_request(self, tiket, price, request):
949
1010
  """
950
1011
  Send a request to set the stop loss to break even for a given trading position.
951
1012
 
@@ -1000,6 +1061,7 @@ class Trade(RiskManagement):
1000
1061
  wen it is closed before be level , tp or sl.
1001
1062
 
1002
1063
  Args:
1064
+ position (TradePosition): The trading position to check.
1003
1065
  th (int): The minimum profit for a position in point
1004
1066
  """
1005
1067
  size = self.get_symbol_info(self.symbol).trade_tick_size
@@ -1050,6 +1112,75 @@ class Trade(RiskManagement):
1050
1112
  return True
1051
1113
  return False
1052
1114
 
1115
+ def close_request(self, request: dict, type: str):
1116
+ """
1117
+ Close a trading order or position
1118
+
1119
+ Args:
1120
+ request (dict): The request to close a trading order or position
1121
+ type (str): Type of the request ('order', 'position')
1122
+ """
1123
+ ticket = request[type]
1124
+ addtionnal = f", SYMBOL={self.symbol}"
1125
+ try:
1126
+ check_result = self.check_order(request)
1127
+ result = self.send_order(request)
1128
+ except Exception as e:
1129
+ print(f"{self.current_datetime()} -", end=' ')
1130
+ trade_retcode_message(
1131
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1132
+ if result.retcode != Mt5.TRADE_RETCODE_DONE:
1133
+ msg = trade_retcode_message(result.retcode)
1134
+ self.logger.error(
1135
+ f"Closing Order Request, {type.capitalize()}: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}")
1136
+ tries = 0
1137
+ while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
1138
+ time.sleep(1)
1139
+ try:
1140
+ check_result = self.check_order(request)
1141
+ result = self.send_order(request)
1142
+ except Exception as e:
1143
+ print(f"{self.current_datetime()} -", end=' ')
1144
+ trade_retcode_message(
1145
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1146
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
1147
+ break
1148
+ tries += 1
1149
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
1150
+ msg = trade_retcode_message(result.retcode)
1151
+ self.logger.info(
1152
+ f"Closing Order {msg}{addtionnal}")
1153
+ info = (
1154
+ f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol}, Price: @{request.get('price', 0.0)}")
1155
+ self.logger.info(info)
1156
+ return True
1157
+ else:
1158
+ return False
1159
+
1160
+ def close_order(self,
1161
+ ticket: int,
1162
+ id: Optional[int] = None,
1163
+ comment: Optional[str] = None):
1164
+ """
1165
+ Close an open order by it ticket
1166
+
1167
+ Args:
1168
+ ticket (int): Order ticket to close (e.g TradeOrder.ticket)
1169
+ id (int): The unique ID of the Expert or Strategy
1170
+ comment (str): Comment for the closing position
1171
+
1172
+ Returns:
1173
+ - True if order closed, False otherwise
1174
+ """
1175
+ request = {
1176
+ "action": Mt5.TRADE_ACTION_REMOVE,
1177
+ "symbol": self.symbol,
1178
+ "order": ticket,
1179
+ "magic": id if id is not None else self.expert_id,
1180
+ "comment": f"@{self.expert_name}" if comment is None else comment,
1181
+ }
1182
+ return self.close_request(request, type="order")
1183
+
1053
1184
  def close_position(self,
1054
1185
  ticket: int,
1055
1186
  id: Optional[int] = None,
@@ -1096,41 +1227,77 @@ class Trade(RiskManagement):
1096
1227
  "type_time": Mt5.ORDER_TIME_GTC,
1097
1228
  "type_filling": Mt5.ORDER_FILLING_FOK,
1098
1229
  }
1099
- addtionnal = f", SYMBOL={self.symbol}"
1100
- try:
1101
- check_result = self.check_order(request)
1102
- result = self.send_order(request)
1103
- except Exception as e:
1104
- print(f"{self.current_datetime()} -", end=' ')
1105
- trade_retcode_message(
1106
- result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1107
- if result.retcode != Mt5.TRADE_RETCODE_DONE:
1108
- msg = trade_retcode_message(result.retcode)
1109
- self.logger.error(
1110
- f"Closing Order Request, Position: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}")
1111
- tries = 0
1112
- while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
1113
- time.sleep(1)
1114
- try:
1115
- check_result = self.check_order(request)
1116
- result = self.send_order(request)
1117
- except Exception as e:
1118
- print(f"{self.current_datetime()} -", end=' ')
1119
- trade_retcode_message(
1120
- result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1121
- if result.retcode == Mt5.TRADE_RETCODE_DONE:
1122
- break
1123
- tries += 1
1124
- if result.retcode == Mt5.TRADE_RETCODE_DONE:
1125
- msg = trade_retcode_message(result.retcode)
1126
- self.logger.info(
1127
- f"Closing Order {msg}{addtionnal}")
1128
- info = (
1129
- f"Position #{ticket} closed, Symbol: {self.symbol}, Price: @{request['price']}")
1130
- self.logger.info(info)
1131
- return True
1132
- else:
1133
- return False
1230
+ return self.close_request(request, type="position")
1231
+
1232
+ def bulk_close(self,
1233
+ tickets: List,
1234
+ tikets_type: Literal["positions", "orders"],
1235
+ close_func: Callable,
1236
+ order_type: str,
1237
+ id: Optional[int] = None,
1238
+ comment: Optional[str] = None):
1239
+
1240
+ """
1241
+ Close multiple orders or positions at once.
1242
+
1243
+ Args:
1244
+ tickets (List): List of tickets to close
1245
+ tikets_type (str): Type of tickets to close ('positions', 'orders')
1246
+ close_func (Callable): The function to close the tickets
1247
+ order_type (str): Type of orders or positions to close
1248
+ id (int): The unique ID of the Expert or Strategy
1249
+ comment (str): Comment for the closing position
1250
+ """
1251
+ if order_type == 'all':
1252
+ order_type = 'open'
1253
+ if len(tickets) > 0:
1254
+ for ticket in tickets.copy():
1255
+ if close_func(ticket, id=id, comment=comment):
1256
+ tickets.remove(ticket)
1257
+ time.sleep(1)
1258
+
1259
+ if len(tickets) == 0:
1260
+ self.logger.info(
1261
+ f"ALL {order_type.upper()} {tikets_type.upper()} closed, SYMBOL={self.symbol}.")
1262
+ else:
1263
+ self.logger.info(
1264
+ f"{len(tickets)} {order_type.upper()} {tikets_type.upper()} not closed, SYMBOL={self.symbol}")
1265
+ else:
1266
+ self.logger.info(
1267
+ f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}.")
1268
+
1269
+ Orders = Literal["all", "buy_stops", "sell_stops", "buy_limits",
1270
+ "sell_limits", "buy_stop_limits", "sell_stop_limits"]
1271
+ def close_orders(self,
1272
+ order_type: Orders,
1273
+ id: Optional[int] = None,
1274
+ comment: Optional[str] = None):
1275
+ """
1276
+ Args:
1277
+ order_type (str): Type of orders to close ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
1278
+ id (int): The unique ID of the Expert or Strategy
1279
+ comment (str): Comment for the closing position
1280
+ """
1281
+ id = id if id is not None else self.expert_id
1282
+ if order_type == "all":
1283
+ orders = self.get_current_orders(id=id)
1284
+ elif order_type == "buy_stops":
1285
+ orders = self.get_current_buy_stops(id=id)
1286
+ elif order_type == "sell_stops":
1287
+ orders = self.get_current_sell_stops(id=id)
1288
+ elif order_type == "buy_limits":
1289
+ orders = self.get_current_buy_limits(id=id)
1290
+ elif order_type == "sell_limits":
1291
+ orders = self.get_current_sell_limits(id=id)
1292
+ elif order_type == "buy_stop_limits":
1293
+ orders = self.get_current_buy_stop_limits(id=id)
1294
+ elif order_type == "sell_stop_limits":
1295
+ orders = self.get_current_sell_stop_limits(id=id)
1296
+ else:
1297
+ self.logger.error(f"Invalid order type: {order_type}")
1298
+ return
1299
+ self.bulk_close(
1300
+ orders, "orders", self.close_order, order_type, id=id, comment=comment)
1134
1301
 
1135
1302
  Positions = Literal["all", "buy", "sell", "profitable", "losing"]
1136
1303
  def close_positions(
@@ -1144,8 +1311,9 @@ class Trade(RiskManagement):
1144
1311
  id (int): The unique ID of the Expert or Strategy
1145
1312
  comment (str): Comment for the closing position
1146
1313
  """
1314
+ id = id if id is not None else self.expert_id
1147
1315
  if position_type == "all":
1148
- positions = self.get_positions(symbol=self.symbol)
1316
+ positions = self.get_current_positions(id=id)
1149
1317
  elif position_type == "buy":
1150
1318
  positions = self.get_current_buys(id=id)
1151
1319
  elif position_type == "sell":
@@ -1157,35 +1325,8 @@ class Trade(RiskManagement):
1157
1325
  else:
1158
1326
  self.logger.error(f"Invalid position type: {position_type}")
1159
1327
  return
1160
-
1161
- if positions is not None:
1162
- if position_type == 'all':
1163
- tickets = [position.ticket for position in positions if position.magic == id]
1164
- else:
1165
- tickets = positions
1166
- else:
1167
- tickets = []
1168
-
1169
- if position_type == 'all':
1170
- pos_type = 'open'
1171
- else:
1172
- pos_type = position_type
1173
-
1174
- if len(tickets) != 0:
1175
- for ticket in tickets.copy():
1176
- if self.close_position(ticket, id=id, comment=comment):
1177
- tickets.remove(ticket)
1178
- time.sleep(1)
1179
-
1180
- if len(tickets) == 0:
1181
- self.logger.info(
1182
- f"ALL {pos_type.upper()} Positions closed, SYMBOL={self.symbol}.")
1183
- else:
1184
- self.logger.info(
1185
- f"{len(tickets)} {pos_type.upper()} Positions not closed, SYMBOL={self.symbol}")
1186
- else:
1187
- self.logger.info(
1188
- f"No {pos_type.upper()} Positions to close, SYMBOL={self.symbol}.")
1328
+ self.bulk_close(
1329
+ positions, "positions", self.close_position, position_type, id=id, comment=comment)
1189
1330
 
1190
1331
  def get_stats(self) -> Tuple[Dict[str, Any]]:
1191
1332
  """
@@ -1342,30 +1483,77 @@ class Trade(RiskManagement):
1342
1483
  def create_trade_instance(
1343
1484
  symbols: List[str],
1344
1485
  params: Dict[str, Any],
1345
- logger: Logger = ...) -> Dict[str, Trade]:
1486
+ daily_risk: Optional[Dict[str, float]] = None,
1487
+ max_risk: Optional[Dict[str, float]] = None,
1488
+ pchange_sl: Optional[Dict[str, float] | float] = None,
1489
+ ) -> Dict[str, Trade]:
1346
1490
  """
1347
1491
  Creates Trade instances for each symbol provided.
1348
1492
 
1349
1493
  Args:
1350
1494
  symbols: A list of trading symbols (e.g., ['AAPL', 'MSFT']).
1351
1495
  params: A dictionary containing parameters for the Trade instance.
1496
+ daily_risk: A dictionary containing daily risk weight for each symbol.
1497
+ max_risk: A dictionary containing maximum risk weight for each symbol.
1352
1498
 
1353
1499
  Returns:
1354
1500
  A dictionary where keys are symbols and values are corresponding Trade instances.
1355
1501
 
1356
1502
  Raises:
1357
1503
  ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
1504
+
1505
+ Note:
1506
+ `daily_risk` and `max_risk` can be used to manage the risk of each symbol
1507
+ based on the importance of the symbol in the portfolio or strategy.
1358
1508
  """
1359
- instances = {}
1509
+ logger = params.get('logger', None)
1510
+ trade_instances = {}
1360
1511
  if not symbols:
1361
1512
  raise ValueError("The 'symbols' list cannot be empty.")
1513
+ if not params:
1514
+ raise ValueError("The 'params' dictionary cannot be empty.")
1515
+
1516
+ if daily_risk is not None:
1517
+ for symbol in symbols:
1518
+ if symbol not in daily_risk:
1519
+ raise ValueError(f"Missing daily risk weight for symbol '{symbol}'.")
1520
+ if max_risk is not None:
1521
+ for symbol in symbols:
1522
+ if symbol not in max_risk:
1523
+ raise ValueError(f"Missing maximum risk percentage for symbol '{symbol}'.")
1524
+ if pchange_sl is not None:
1525
+ if isinstance(pchange_sl, dict):
1526
+ for symbol in symbols:
1527
+ if symbol not in pchange_sl:
1528
+ raise ValueError(f"Missing percentage change for symbol '{symbol}'.")
1529
+
1362
1530
  for symbol in symbols:
1363
1531
  try:
1364
- instances[symbol] = Trade(symbol=symbol, **params)
1532
+ params['symbol'] = symbol
1533
+ params['pchange_sl'] = (
1534
+ pchange_sl[symbol] if pchange_sl is not None
1535
+ and isinstance(pchange_sl, dict) else
1536
+ pchange_sl if pchange_sl is not None
1537
+ and isinstance(pchange_sl, (int, float)) else
1538
+ params['pchange_sl'] if 'pchange_sl' in params else None
1539
+ )
1540
+ params['daily_risk'] = (
1541
+ daily_risk[symbol] if daily_risk is not None else
1542
+ params['daily_risk'] if 'daily_risk' in params else None
1543
+ )
1544
+ params['max_risk'] = (
1545
+ max_risk[symbol] if max_risk is not None else
1546
+ params['max_risk'] if 'max_risk' in params else 10.0
1547
+ )
1548
+ trade_instances[symbol] = Trade(**params)
1365
1549
  except Exception as e:
1366
1550
  logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
1367
- if len(instances) != len(symbols):
1551
+
1552
+ if len(trade_instances) != len(symbols):
1368
1553
  for symbol in symbols:
1369
- if symbol not in instances:
1370
- logger.error(f"Failed to create Trade instance for SYMBOL={symbol}")
1371
- return instances
1554
+ if symbol not in trade_instances:
1555
+ if logger is not None and isinstance(logger, Logger):
1556
+ logger.error(f"Failed to create Trade instance for SYMBOL={symbol}")
1557
+ else:
1558
+ raise ValueError(f"Failed to create Trade instance for SYMBOL={symbol}")
1559
+ return trade_instances