bbstrader 0.1.9__py3-none-any.whl → 0.1.92__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 check_mt5_connection, 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
  """
@@ -142,12 +166,12 @@ class Trade(RiskManagement):
142
166
  ":")
143
167
  self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
144
168
 
145
- self.buy_positions = []
146
- self.sell_positions = []
147
- self.opened_positions = []
148
- self.opened_orders = []
149
- self.break_even_status = []
150
- self.break_even_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 = {}
151
175
  self.trail_after_points = []
152
176
 
153
177
  self.initialize()
@@ -165,7 +189,9 @@ class Trade(RiskManagement):
165
189
  def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
166
190
  """Get the logger object"""
167
191
  if isinstance(logger, str):
168
- return config_logger(logger, 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)
169
195
  return logger
170
196
 
171
197
  def initialize(self):
@@ -302,14 +328,10 @@ class Trade(RiskManagement):
302
328
  """
303
329
  stats, additional_stats = self.get_stats()
304
330
 
305
- deals = stats["deals"]
306
- wins = stats["win_trades"]
307
- losses = stats["loss_trades"]
308
331
  profit = round(stats["profit"], 2)
309
332
  win_rate = stats["win_rate"]
310
333
  total_fees = round(stats["total_fees"], 3)
311
334
  average_fee = round(stats["average_fee"], 3)
312
- profitability = additional_stats["profitability"]
313
335
  currency = self.get_account_info().currency
314
336
  net_profit = round((profit + total_fees), 2)
315
337
  trade_risk = round(self.get_currency_risk() * -1, 2)
@@ -317,9 +339,9 @@ class Trade(RiskManagement):
317
339
 
318
340
  # Formatting the statistics output
319
341
  session_data = [
320
- ["Total Trades", deals],
321
- ["Winning Trades", wins],
322
- ["Losing Trades", losses],
342
+ ["Total Trades", stats["deals"]],
343
+ ["Winning Trades", stats["win_trades"]],
344
+ ["Losing Trades", stats["loss_trades"]],
323
345
  ["Session Profit", f"{profit} {currency}"],
324
346
  ["Total Fees", f"{total_fees} {currency}"],
325
347
  ["Average Fees", f"{average_fee} {currency}"],
@@ -329,9 +351,10 @@ class Trade(RiskManagement):
329
351
  ["Risk Reward Ratio", self.rr],
330
352
  ["Win Rate", f"{win_rate}%"],
331
353
  ["Sharpe Ratio", self.sharpe()],
332
- ["Trade Profitability", profitability],
354
+ ["Trade Profitability", additional_stats["profitability"]],
333
355
  ]
334
- session_table = tabulate(session_data, headers=["Statistics", "Values"], tablefmt="outline")
356
+ session_table = tabulate(
357
+ session_data, headers=["Statistics", "Values"], tablefmt="outline")
335
358
 
336
359
  # Print the formatted statistics
337
360
  if self.verbose:
@@ -342,24 +365,10 @@ class Trade(RiskManagement):
342
365
  if save:
343
366
  today_date = datetime.now().strftime('%Y%m%d%H%M%S')
344
367
  # Create a dictionary with the statistics
345
- statistics_dict = {
346
- "Total Trades": deals,
347
- "Winning Trades": wins,
348
- "Losing Trades": losses,
349
- "Session Profit": f"{profit} {currency}",
350
- "Total Fees": f"{total_fees} {currency}",
351
- "Average Fees": f"{average_fee} {currency}",
352
- "Net Profit": f"{net_profit} {currency}",
353
- "Risk per Trade": f"{trade_risk} {currency}",
354
- "Expected Profit per Trade": f"{expected_profit} {currency}",
355
- "Risk Reward Ratio": self.rr,
356
- "Win Rate": f"{win_rate}%",
357
- "Sharpe Ratio": self.sharpe(),
358
- "Trade Profitability": profitability,
359
- }
368
+ statistics_dict = {item[0]: item[1] for item in session_data}
369
+ stats_df = pd.DataFrame(statistics_dict, index=[0])
360
370
  # Create the directory if it doesn't exist
361
- if dir is None:
362
- dir = f".{self.expert_name}_session_stats"
371
+ dir = dir or '.sessions'
363
372
  os.makedirs(dir, exist_ok=True)
364
373
  if '.' in self.symbol:
365
374
  symbol = self.symbol.split('.')[0]
@@ -368,15 +377,7 @@ class Trade(RiskManagement):
368
377
 
369
378
  filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
370
379
  filepath = os.path.join(dir, filename)
371
-
372
- # Updated code to write to CSV
373
- with open(filepath, mode="w", newline='', encoding='utf-8') as csv_file:
374
- writer = csv.writer(
375
- csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL
376
- )
377
- writer.writerow(["Statistic", "Value"])
378
- for stat, value in statistics_dict.items():
379
- writer.writerow([stat, value])
380
+ stats_df.to_csv(filepath, index=False)
380
381
  self.logger.info(f"Session statistics saved to {filepath}")
381
382
 
382
383
  Buys = Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
@@ -384,6 +385,7 @@ class Trade(RiskManagement):
384
385
  self,
385
386
  action: Buys = 'BMKT',
386
387
  price: Optional[float] = None,
388
+ stoplimit: Optional[float] = None,
387
389
  mm: bool = True,
388
390
  id: Optional[int] = None,
389
391
  comment: Optional[str] = None
@@ -395,6 +397,8 @@ class Trade(RiskManagement):
395
397
  action (str): `'BMKT'` for Market orders or `'BLMT',
396
398
  'BSTP','BSTPLMT'` for pending orders
397
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
398
402
  id (int): The strategy id or expert Id
399
403
  mm (bool): Weither to put stop loss and tp or not
400
404
  comment (str): The comment for the opening position
@@ -402,9 +406,11 @@ class Trade(RiskManagement):
402
406
  Id = id if id is not None else self.expert_id
403
407
  point = self.get_symbol_info(self.symbol).point
404
408
  if action != 'BMKT':
405
- assert price is not None, \
406
- "You need to set a price for pending orders"
407
- _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")
408
414
  else:
409
415
  _price = self.get_tick_info(self.symbol).ask
410
416
  digits = self.get_symbol_info(self.symbol).digits
@@ -425,12 +431,22 @@ class Trade(RiskManagement):
425
431
  "type_time": Mt5.ORDER_TIME_GTC,
426
432
  "type_filling": Mt5.ORDER_FILLING_FOK,
427
433
  }
428
- if mm:
429
- request['sl'] = (_price - stop_loss * point)
430
- request['tp'] = (_price + take_profit * point)
434
+ mm_price = _price
431
435
  if action != 'BMKT':
432
436
  request["action"] = Mt5.TRADE_ACTION_PENDING
433
437
  request["type"] = self._order_type()[action][0]
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)
434
450
  self.break_even(mm=mm, id=Id)
435
451
  if self.check(comment):
436
452
  self.request_result(_price, request, action),
@@ -453,6 +469,7 @@ class Trade(RiskManagement):
453
469
  self,
454
470
  action: Sells = 'SMKT',
455
471
  price: Optional[float] = None,
472
+ stoplimit: Optional[float] = None,
456
473
  mm: bool = True,
457
474
  id: Optional[int] = None,
458
475
  comment: Optional[str] = None
@@ -464,6 +481,8 @@ class Trade(RiskManagement):
464
481
  action (str): `'SMKT'` for Market orders
465
482
  or `'SLMT', 'SSTP','SSTPLMT'` for pending orders
466
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
467
486
  id (int): The strategy id or expert Id
468
487
  mm (bool): Weither to put stop loss and tp or not
469
488
  comment (str): The comment for the closing position
@@ -471,9 +490,11 @@ class Trade(RiskManagement):
471
490
  Id = id if id is not None else self.expert_id
472
491
  point = self.get_symbol_info(self.symbol).point
473
492
  if action != 'SMKT':
474
- assert price is not None, \
475
- "You need to set a price for pending orders"
476
- _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")
477
498
  else:
478
499
  _price = self.get_tick_info(self.symbol).bid
479
500
  digits = self.get_symbol_info(self.symbol).digits
@@ -494,23 +515,26 @@ class Trade(RiskManagement):
494
515
  "type_time": Mt5.ORDER_TIME_GTC,
495
516
  "type_filling": Mt5.ORDER_FILLING_FOK,
496
517
  }
497
- if mm:
498
- request["sl"] = (_price + stop_loss * point)
499
- request["tp"] = (_price - take_profit * point)
518
+ mm_price = _price
500
519
  if action != 'SMKT':
501
520
  request["action"] = Mt5.TRADE_ACTION_PENDING
502
521
  request["type"] = self._order_type()[action][0]
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)
503
534
  self.break_even(mm=mm, id=Id)
504
535
  if self.check(comment):
505
536
  self.request_result(_price, request, action)
506
537
 
507
- def _risk_free(self):
508
- max_trade = self.max_trade()
509
- loss_trades = self.get_stats()[0]['loss_trades']
510
- if loss_trades >= max_trade:
511
- return False
512
- return True
513
-
514
538
  def check(self, comment):
515
539
  """
516
540
  Verify if all conditions for taking a position are valide,
@@ -529,16 +553,14 @@ class Trade(RiskManagement):
529
553
  self.logger.error(f"Risk not allowed, SYMBOL={self.symbol}")
530
554
  self._check(comment)
531
555
  return False
532
- elif not self._risk_free():
533
- self.logger.error(f"Maximum trades Reached, SYMBOL={self.symbol}")
534
- self._check(comment)
535
- return False
536
556
  elif self.profit_target():
537
557
  self._check(f'Profit target Reached !!! SYMBOL={self.symbol}')
538
558
  return True
539
559
 
540
560
  def _check(self, txt: str = ""):
541
- 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
+ ):
542
564
  self.close_positions(position_type='all')
543
565
  self.logger.info(txt)
544
566
  time.sleep(5)
@@ -565,7 +587,7 @@ class Trade(RiskManagement):
565
587
  pos = self._order_type()[type][1]
566
588
  addtionnal = f", SYMBOL={self.symbol}"
567
589
  try:
568
- check_result = self.check_order(request)
590
+ check = self.check_order(request)
569
591
  result = self.send_order(request)
570
592
  except Exception as e:
571
593
  print(f"{self.current_datetime()} -", end=' ')
@@ -581,7 +603,7 @@ class Trade(RiskManagement):
581
603
  while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
582
604
  time.sleep(1)
583
605
  try:
584
- check_result = self.check_order(request)
606
+ check = self.check_order(request)
585
607
  result = self.send_order(request)
586
608
  except Exception as e:
587
609
  print(f"{self.current_datetime()} -", end=' ')
@@ -630,6 +652,7 @@ class Trade(RiskManagement):
630
652
  self,
631
653
  action: Buys | Sells,
632
654
  price: Optional[float] = None,
655
+ stoplimit: Optional[float] = None,
633
656
  id: Optional[int] = None,
634
657
  mm: bool = True,
635
658
  comment: Optional[str] = None
@@ -640,6 +663,9 @@ class Trade(RiskManagement):
640
663
  Args:
641
664
  action (str): (`'BMKT'`, `'SMKT'`) for Market orders
642
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
643
669
  id (int): The strategy id or expert Id
644
670
  mm (bool): Weither to put stop loss and tp or not
645
671
  comment (str): The comment for the closing position
@@ -648,43 +674,43 @@ class Trade(RiskManagement):
648
674
  SELLS = ['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
649
675
  if action in BUYS:
650
676
  self.open_buy_position(
651
- action=action, price=price, id=id, mm=mm, comment=comment)
677
+ action=action, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
652
678
  elif action in SELLS:
653
679
  self.open_sell_position(
654
- action=action, price=price, id=id, mm=mm, comment=comment)
680
+ action=action, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
655
681
  else:
656
682
  raise ValueError(f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}")
657
683
 
658
684
  @property
659
- def get_opened_orders(self):
685
+ def orders(self):
660
686
  """ Return all opened order's tickets"""
661
687
  if len(self.opened_orders) != 0:
662
688
  return self.opened_orders
663
689
  return None
664
690
 
665
691
  @property
666
- def get_opened_positions(self):
692
+ def positions(self):
667
693
  """Return all opened position's tickets"""
668
694
  if len(self.opened_positions) != 0:
669
695
  return self.opened_positions
670
696
  return None
671
697
 
672
698
  @property
673
- def get_buy_positions(self):
699
+ def buypos(self):
674
700
  """Return all buy opened position's tickets"""
675
701
  if len(self.buy_positions) != 0:
676
702
  return self.buy_positions
677
703
  return None
678
704
 
679
705
  @property
680
- def get_sell_positions(self):
706
+ def sellpos(self):
681
707
  """Return all sell opened position's tickets"""
682
708
  if len(self.sell_positions) != 0:
683
709
  return self.sell_positions
684
710
  return None
685
711
 
686
712
  @property
687
- def get_be_positions(self):
713
+ def bepos(self):
688
714
  """Return All positon's tickets
689
715
  for which a break even has been set"""
690
716
  if len(self.break_even_status) != 0:
@@ -701,8 +727,14 @@ class Trade(RiskManagement):
701
727
 
702
728
  Args:
703
729
  id (int): The strategy id or expert Id
704
- filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'profitables')
730
+ filter_type (str): Filter type to apply on the tickets,
705
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
706
738
  - `positions` are all current open positions
707
739
  - `buys` and `sells` are current buy or sell open positions
708
740
  - `profitables` are current open position that have a profit greater than a threshold
@@ -715,8 +747,9 @@ class Trade(RiskManagement):
715
747
  or None if no tickets match the criteria.
716
748
  """
717
749
  Id = id if id is not None else self.expert_id
750
+ POSITIONS = ['positions', 'buys', 'sells', 'profitables', 'losings']
718
751
 
719
- if filter_type == 'orders':
752
+ if filter_type not in POSITIONS:
720
753
  items = self.get_orders(symbol=self.symbol)
721
754
  else:
722
755
  items = self.get_positions(symbol=self.symbol)
@@ -730,18 +763,48 @@ class Trade(RiskManagement):
730
763
  continue
731
764
  if filter_type == 'sells' and item.type != 1:
732
765
  continue
766
+ if filter_type == 'losings' and item.profit > 0:
767
+ continue
733
768
  if filter_type == 'profitables' and not self.win_trade(item, th=th):
734
769
  continue
735
- 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]:
736
781
  continue
737
782
  filtered_tickets.append(item.ticket)
738
783
  return filtered_tickets if filtered_tickets else None
739
784
  return None
740
785
 
741
- 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:
742
787
  return self.get_filtered_tickets(id=id, filter_type='orders')
743
788
 
744
- 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:
745
808
  return self.get_filtered_tickets(id=id, filter_type='positions')
746
809
 
747
810
  def get_current_profitables(self, id: Optional[int] = None, th=None) -> List[int] | None:
@@ -775,14 +838,16 @@ class Trade(RiskManagement):
775
838
  elif account and id is not None:
776
839
  # All open positions for a specific strategy or expert no matter the symbol
777
840
  positions = self.get_positions()
778
- positions = [position for position in positions if position.magic == id]
841
+ if positions is not None:
842
+ positions = [position for position in positions if position.magic == id]
779
843
  elif not account and id is None:
780
844
  # All open positions for the current symbol no matter the strategy or expert
781
845
  positions = self.get_positions(symbol=self.symbol)
782
846
  elif not account and id is not None:
783
847
  # All open positions for the current symbol and a specific strategy or expert
784
848
  positions = self.get_positions(symbol=self.symbol)
785
- positions = [position for position in positions if position.magic == id]
849
+ if positions is not None:
850
+ positions = [position for position in positions if position.magic == id]
786
851
  profit = 0.0
787
852
  balance = self.get_account_info().balance
788
853
  target = round((balance * self.target)/100, 2)
@@ -868,10 +933,14 @@ class Trade(RiskManagement):
868
933
  # This level validate the favorable move of the price
869
934
  new_level = round(position.price_open + (new_be_points * point), digits)
870
935
  # This price is set away from the current price by the trail_points
871
- new_price = round(position.price_current - (trail_points * point), digits)
936
+ new_price = round(position.price_current - (trail_points * point), digits)
937
+ if new_price < position.sl:
938
+ new_price = position.sl
872
939
  elif position.type == 1:
873
940
  new_level = round(position.price_open - (new_be_points * point), digits)
874
- new_price = round(position.price_current + (trail_points * point), digits)
941
+ new_price = round(position.price_current + (trail_points * point), digits)
942
+ if new_price > position.sl:
943
+ new_price = position.sl
875
944
  self.set_break_even(
876
945
  position, be, price=new_price, level=new_level
877
946
  )
@@ -916,7 +985,7 @@ class Trade(RiskManagement):
916
985
  "sl": round(_price, digits),
917
986
  "tp": position.tp
918
987
  }
919
- self._break_even_request(
988
+ self.break_even_request(
920
989
  position.ticket, round(_price, digits), request)
921
990
  # If Sell
922
991
  elif position.type == 1 and position.price_current < position.price_open:
@@ -936,10 +1005,10 @@ class Trade(RiskManagement):
936
1005
  "sl": round(_price, digits),
937
1006
  "tp": position.tp
938
1007
  }
939
- self._break_even_request(
1008
+ self.break_even_request(
940
1009
  position.ticket, round(_price, digits), request)
941
1010
 
942
- def _break_even_request(self, tiket, price, request):
1011
+ def break_even_request(self, tiket, price, request):
943
1012
  """
944
1013
  Send a request to set the stop loss to break even for a given trading position.
945
1014
 
@@ -959,8 +1028,9 @@ class Trade(RiskManagement):
959
1028
  result.retcode, display=True, add_msg=f"{e}{addtionnal}")
960
1029
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
961
1030
  msg = trade_retcode_message(result.retcode)
962
- self.logger.error(
963
- f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}")
1031
+ if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
1032
+ self.logger.error(
1033
+ f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}")
964
1034
  tries = 0
965
1035
  while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
966
1036
  if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
@@ -994,6 +1064,7 @@ class Trade(RiskManagement):
994
1064
  wen it is closed before be level , tp or sl.
995
1065
 
996
1066
  Args:
1067
+ position (TradePosition): The trading position to check.
997
1068
  th (int): The minimum profit for a position in point
998
1069
  """
999
1070
  size = self.get_symbol_info(self.symbol).trade_tick_size
@@ -1044,6 +1115,75 @@ class Trade(RiskManagement):
1044
1115
  return True
1045
1116
  return False
1046
1117
 
1118
+ def close_request(self, request: dict, type: str):
1119
+ """
1120
+ Close a trading order or position
1121
+
1122
+ Args:
1123
+ request (dict): The request to close a trading order or position
1124
+ type (str): Type of the request ('order', 'position')
1125
+ """
1126
+ ticket = request[type]
1127
+ addtionnal = f", SYMBOL={self.symbol}"
1128
+ try:
1129
+ check_result = self.check_order(request)
1130
+ result = self.send_order(request)
1131
+ except Exception as e:
1132
+ print(f"{self.current_datetime()} -", end=' ')
1133
+ trade_retcode_message(
1134
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1135
+ if result.retcode != Mt5.TRADE_RETCODE_DONE:
1136
+ msg = trade_retcode_message(result.retcode)
1137
+ self.logger.error(
1138
+ f"Closing Order Request, {type.capitalize()}: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}")
1139
+ tries = 0
1140
+ while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
1141
+ time.sleep(1)
1142
+ try:
1143
+ check_result = self.check_order(request)
1144
+ result = self.send_order(request)
1145
+ except Exception as e:
1146
+ print(f"{self.current_datetime()} -", end=' ')
1147
+ trade_retcode_message(
1148
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1149
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
1150
+ break
1151
+ tries += 1
1152
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
1153
+ msg = trade_retcode_message(result.retcode)
1154
+ self.logger.info(
1155
+ f"Closing Order {msg}{addtionnal}")
1156
+ info = (
1157
+ f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol}, Price: @{request.get('price', 0.0)}")
1158
+ self.logger.info(info)
1159
+ return True
1160
+ else:
1161
+ return False
1162
+
1163
+ def close_order(self,
1164
+ ticket: int,
1165
+ id: Optional[int] = None,
1166
+ comment: Optional[str] = None):
1167
+ """
1168
+ Close an open order by it ticket
1169
+
1170
+ Args:
1171
+ ticket (int): Order ticket to close (e.g TradeOrder.ticket)
1172
+ id (int): The unique ID of the Expert or Strategy
1173
+ comment (str): Comment for the closing position
1174
+
1175
+ Returns:
1176
+ - True if order closed, False otherwise
1177
+ """
1178
+ request = {
1179
+ "action": Mt5.TRADE_ACTION_REMOVE,
1180
+ "symbol": self.symbol,
1181
+ "order": ticket,
1182
+ "magic": id if id is not None else self.expert_id,
1183
+ "comment": f"@{self.expert_name}" if comment is None else comment,
1184
+ }
1185
+ return self.close_request(request, type="order")
1186
+
1047
1187
  def close_position(self,
1048
1188
  ticket: int,
1049
1189
  id: Optional[int] = None,
@@ -1090,41 +1230,77 @@ class Trade(RiskManagement):
1090
1230
  "type_time": Mt5.ORDER_TIME_GTC,
1091
1231
  "type_filling": Mt5.ORDER_FILLING_FOK,
1092
1232
  }
1093
- addtionnal = f", SYMBOL={self.symbol}"
1094
- try:
1095
- check_result = self.check_order(request)
1096
- result = self.send_order(request)
1097
- except Exception as e:
1098
- print(f"{self.current_datetime()} -", end=' ')
1099
- trade_retcode_message(
1100
- result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1101
- if result.retcode != Mt5.TRADE_RETCODE_DONE:
1102
- msg = trade_retcode_message(result.retcode)
1103
- self.logger.error(
1104
- f"Closing Order Request, Position: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}")
1105
- tries = 0
1106
- while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
1107
- time.sleep(1)
1108
- try:
1109
- check_result = self.check_order(request)
1110
- result = self.send_order(request)
1111
- except Exception as e:
1112
- print(f"{self.current_datetime()} -", end=' ')
1113
- trade_retcode_message(
1114
- result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1115
- if result.retcode == Mt5.TRADE_RETCODE_DONE:
1116
- break
1117
- tries += 1
1118
- if result.retcode == Mt5.TRADE_RETCODE_DONE:
1119
- msg = trade_retcode_message(result.retcode)
1120
- self.logger.info(
1121
- f"Closing Order {msg}{addtionnal}")
1122
- info = (
1123
- f"Position #{ticket} closed, Symbol: {self.symbol}, Price: @{request['price']}")
1124
- self.logger.info(info)
1125
- return True
1126
- else:
1127
- return False
1233
+ return self.close_request(request, type="position")
1234
+
1235
+ def bulk_close(self,
1236
+ tickets: List,
1237
+ tikets_type: Literal["positions", "orders"],
1238
+ close_func: Callable,
1239
+ order_type: str,
1240
+ id: Optional[int] = None,
1241
+ comment: Optional[str] = None):
1242
+
1243
+ """
1244
+ Close multiple orders or positions at once.
1245
+
1246
+ Args:
1247
+ tickets (List): List of tickets to close
1248
+ tikets_type (str): Type of tickets to close ('positions', 'orders')
1249
+ close_func (Callable): The function to close the tickets
1250
+ order_type (str): Type of orders or positions to close
1251
+ id (int): The unique ID of the Expert or Strategy
1252
+ comment (str): Comment for the closing position
1253
+ """
1254
+ if order_type == 'all':
1255
+ order_type = 'open'
1256
+ if len(tickets) > 0:
1257
+ for ticket in tickets.copy():
1258
+ if close_func(ticket, id=id, comment=comment):
1259
+ tickets.remove(ticket)
1260
+ time.sleep(1)
1261
+
1262
+ if len(tickets) == 0:
1263
+ self.logger.info(
1264
+ f"ALL {order_type.upper()} {tikets_type.upper()} closed, SYMBOL={self.symbol}.")
1265
+ else:
1266
+ self.logger.info(
1267
+ f"{len(tickets)} {order_type.upper()} {tikets_type.upper()} not closed, SYMBOL={self.symbol}")
1268
+ else:
1269
+ self.logger.info(
1270
+ f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}.")
1271
+
1272
+ Orders = Literal["all", "buy_stops", "sell_stops", "buy_limits",
1273
+ "sell_limits", "buy_stop_limits", "sell_stop_limits"]
1274
+ def close_orders(self,
1275
+ order_type: Orders,
1276
+ id: Optional[int] = None,
1277
+ comment: Optional[str] = None):
1278
+ """
1279
+ Args:
1280
+ order_type (str): Type of orders to close ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
1281
+ id (int): The unique ID of the Expert or Strategy
1282
+ comment (str): Comment for the closing position
1283
+ """
1284
+ id = id if id is not None else self.expert_id
1285
+ if order_type == "all":
1286
+ orders = self.get_current_orders(id=id)
1287
+ elif order_type == "buy_stops":
1288
+ orders = self.get_current_buy_stops(id=id)
1289
+ elif order_type == "sell_stops":
1290
+ orders = self.get_current_sell_stops(id=id)
1291
+ elif order_type == "buy_limits":
1292
+ orders = self.get_current_buy_limits(id=id)
1293
+ elif order_type == "sell_limits":
1294
+ orders = self.get_current_sell_limits(id=id)
1295
+ elif order_type == "buy_stop_limits":
1296
+ orders = self.get_current_buy_stop_limits(id=id)
1297
+ elif order_type == "sell_stop_limits":
1298
+ orders = self.get_current_sell_stop_limits(id=id)
1299
+ else:
1300
+ self.logger.error(f"Invalid order type: {order_type}")
1301
+ return
1302
+ self.bulk_close(
1303
+ orders, "orders", self.close_order, order_type, id=id, comment=comment)
1128
1304
 
1129
1305
  Positions = Literal["all", "buy", "sell", "profitable", "losing"]
1130
1306
  def close_positions(
@@ -1138,8 +1314,9 @@ class Trade(RiskManagement):
1138
1314
  id (int): The unique ID of the Expert or Strategy
1139
1315
  comment (str): Comment for the closing position
1140
1316
  """
1317
+ id = id if id is not None else self.expert_id
1141
1318
  if position_type == "all":
1142
- positions = self.get_positions(symbol=self.symbol)
1319
+ positions = self.get_current_positions(id=id)
1143
1320
  elif position_type == "buy":
1144
1321
  positions = self.get_current_buys(id=id)
1145
1322
  elif position_type == "sell":
@@ -1151,35 +1328,8 @@ class Trade(RiskManagement):
1151
1328
  else:
1152
1329
  self.logger.error(f"Invalid position type: {position_type}")
1153
1330
  return
1154
-
1155
- if positions is not None:
1156
- if position_type == 'all':
1157
- tickets = [position.ticket for position in positions if position.magic == id]
1158
- else:
1159
- tickets = positions
1160
- else:
1161
- tickets = []
1162
-
1163
- if position_type == 'all':
1164
- pos_type = 'open'
1165
- else:
1166
- pos_type = position_type
1167
-
1168
- if len(tickets) != 0:
1169
- for ticket in tickets.copy():
1170
- if self.close_position(ticket, id=id, comment=comment):
1171
- tickets.remove(ticket)
1172
- time.sleep(1)
1173
-
1174
- if len(tickets) == 0:
1175
- self.logger.info(
1176
- f"ALL {pos_type.upper()} Positions closed, SYMBOL={self.symbol}.")
1177
- else:
1178
- self.logger.info(
1179
- f"{len(tickets)} {pos_type.upper()} Positions not closed, SYMBOL={self.symbol}")
1180
- else:
1181
- self.logger.info(
1182
- f"No {pos_type.upper()} Positions to close, SYMBOL={self.symbol}.")
1331
+ self.bulk_close(
1332
+ positions, "positions", self.close_position, position_type, id=id, comment=comment)
1183
1333
 
1184
1334
  def get_stats(self) -> Tuple[Dict[str, Any]]:
1185
1335
  """
@@ -1339,7 +1489,7 @@ def create_trade_instance(
1339
1489
  daily_risk: Optional[Dict[str, float]] = None,
1340
1490
  max_risk: Optional[Dict[str, float]] = None,
1341
1491
  pchange_sl: Optional[Dict[str, float] | float] = None,
1342
- logger: Logger = None) -> Dict[str, Trade]:
1492
+ ) -> Dict[str, Trade]:
1343
1493
  """
1344
1494
  Creates Trade instances for each symbol provided.
1345
1495
 
@@ -1348,7 +1498,6 @@ def create_trade_instance(
1348
1498
  params: A dictionary containing parameters for the Trade instance.
1349
1499
  daily_risk: A dictionary containing daily risk weight for each symbol.
1350
1500
  max_risk: A dictionary containing maximum risk weight for each symbol.
1351
- logger: A logger instance.
1352
1501
 
1353
1502
  Returns:
1354
1503
  A dictionary where keys are symbols and values are corresponding Trade instances.
@@ -1360,7 +1509,8 @@ def create_trade_instance(
1360
1509
  `daily_risk` and `max_risk` can be used to manage the risk of each symbol
1361
1510
  based on the importance of the symbol in the portfolio or strategy.
1362
1511
  """
1363
- instances = {}
1512
+ logger = params.get('logger', None)
1513
+ trade_instances = {}
1364
1514
  if not symbols:
1365
1515
  raise ValueError("The 'symbols' list cannot be empty.")
1366
1516
  if not params:
@@ -1385,19 +1535,28 @@ def create_trade_instance(
1385
1535
  params['symbol'] = symbol
1386
1536
  params['pchange_sl'] = (
1387
1537
  pchange_sl[symbol] if pchange_sl is not None
1388
- and isinstance(pchange_sl, dict) else pchange_sl
1538
+ and isinstance(pchange_sl, dict) else
1539
+ pchange_sl if pchange_sl is not None
1540
+ and isinstance(pchange_sl, (int, float)) else
1541
+ params['pchange_sl'] if 'pchange_sl' in params else None
1542
+ )
1543
+ params['daily_risk'] = (
1544
+ daily_risk[symbol] if daily_risk is not None else
1545
+ params['daily_risk'] if 'daily_risk' in params else None
1546
+ )
1547
+ params['max_risk'] = (
1548
+ max_risk[symbol] if max_risk is not None else
1549
+ params['max_risk'] if 'max_risk' in params else 10.0
1389
1550
  )
1390
- params['daily_risk'] = daily_risk[symbol] if daily_risk is not None else params['daily_risk']
1391
- params['max_risk'] = max_risk[symbol] if max_risk is not None else params['max_risk']
1392
- instances[symbol] = Trade(**params)
1551
+ trade_instances[symbol] = Trade(**params)
1393
1552
  except Exception as e:
1394
1553
  logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
1395
1554
 
1396
- if len(instances) != len(symbols):
1555
+ if len(trade_instances) != len(symbols):
1397
1556
  for symbol in symbols:
1398
- if symbol not in instances:
1399
- if logger is not None:
1557
+ if symbol not in trade_instances:
1558
+ if logger is not None and isinstance(logger, Logger):
1400
1559
  logger.error(f"Failed to create Trade instance for SYMBOL={symbol}")
1401
1560
  else:
1402
1561
  raise ValueError(f"Failed to create Trade instance for SYMBOL={symbol}")
1403
- return instances
1562
+ return trade_instances