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

@@ -5,13 +5,14 @@ import numpy as np
5
5
  from datetime import datetime
6
6
  import MetaTrader5 as Mt5
7
7
  from logging import Logger
8
+ from tabulate import tabulate
8
9
  from typing import List, Tuple, Dict, Any, Optional, Literal
10
+ from bbstrader.btengine.performance import create_sharpe_ratio
9
11
  from bbstrader.metatrader.risk import RiskManagement
10
- from bbstrader.metatrader.account import INIT_MSG
12
+ from bbstrader.metatrader.account import check_mt5_connection, INIT_MSG
11
13
  from bbstrader.metatrader.utils import (
12
14
  TimeFrame, TradePosition, TickInfo,
13
- raise_mt5_error, trade_retcode_message, config_logger
14
- )
15
+ raise_mt5_error, trade_retcode_message, config_logger)
15
16
 
16
17
  class Trade(RiskManagement):
17
18
  """
@@ -24,8 +25,8 @@ class Trade(RiskManagement):
24
25
  >>> import time
25
26
  >>> # Initialize the Trade class with parameters
26
27
  >>> trade = Trade(
27
- ... symbol="#AAPL", # Symbol to trade
28
- ... expert_name="MyExpertAdvisor",# Name of the expert advisor
28
+ ... symbol="EURUSD", # Symbol to trade
29
+ ... expert_name="bbstrader", # Name of the expert advisor
29
30
  ... expert_id=12345, # Unique ID for the expert advisor
30
31
  ... version="1.0", # Version of the expert advisor
31
32
  ... target=5.0, # Daily profit target in percentage
@@ -91,7 +92,7 @@ class Trade(RiskManagement):
91
92
  expert_id (int): The `unique ID` used to identify the expert advisor
92
93
  or the strategy used on the symbol.
93
94
  version (str): The `version` of the expert advisor.
94
- target (float): `Trading period (day, week, month) profit target` in percentage
95
+ target (float): `Trading period (day, week, month) profit target` in percentage.
95
96
  start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
96
97
  finishing_time (str): The time after which no new position can be opened.
97
98
  ending_time (str): The time after which any open position will be closed.
@@ -136,12 +137,6 @@ class Trade(RiskManagement):
136
137
  self.logger = self._get_logger(logger, console_log)
137
138
  self.tf = kwargs.get("time_frame", 'D1')
138
139
 
139
- self.lot = self.get_lot()
140
- self.stop_loss = self.get_stop_loss()
141
- self.take_profit = self.get_take_profit()
142
- self.break_even_points = self.get_break_even()
143
- self.deviation = self.get_deviation()
144
-
145
140
  self.start_time_hour, self.start_time_minutes = self.start.split(":")
146
141
  self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
147
142
  ":")
@@ -152,6 +147,8 @@ class Trade(RiskManagement):
152
147
  self.opened_positions = []
153
148
  self.opened_orders = []
154
149
  self.break_even_status = []
150
+ self.break_even_points = {}
151
+ self.trail_after_points = []
155
152
 
156
153
  self.initialize()
157
154
  self.select_symbol()
@@ -184,8 +181,7 @@ class Trade(RiskManagement):
184
181
  try:
185
182
  if self.verbose:
186
183
  print("\nInitializing the basics.")
187
- if not Mt5.initialize():
188
- raise_mt5_error(message=INIT_MSG)
184
+ check_mt5_connection()
189
185
  if self.verbose:
190
186
  print(
191
187
  f"You are running the @{self.expert_name} Expert advisor,"
@@ -235,18 +231,22 @@ class Trade(RiskManagement):
235
231
 
236
232
  def summary(self):
237
233
  """Show a brief description about the trading program"""
238
- print(
239
- "╔═════════════════ Summary ════════════════════╗\n"
240
- f"Expert Advisor Name @{self.expert_name}\n"
241
- f"Expert Advisor Version @{self.version}\n"
242
- f" Expert | Strategy ID {self.expert_id}\n"
243
- f"Trading Symbol {self.symbol}\n"
244
- f" Trading Time Frame {self.tf}\n"
245
- f" Start Trading Time {self.start_time_hour}:{self.start_time_minutes}\n"
246
- f" Finishing Trading Time {self.finishing_time_hour}:{self.finishing_time_minutes}\n"
247
- f"║ Closing Position After {self.ending_time_hour}:{self.ending_time_minutes}\n"
248
- "╚═══════════════════════════════════════════════╝\n"
249
- )
234
+ summary_data = [
235
+ ["Expert Advisor Name", f"@{self.expert_name}"],
236
+ ["Expert Advisor Version", f"@{self.version}"],
237
+ ["Expert | Strategy ID", self.expert_id],
238
+ ["Trading Symbol", self.symbol],
239
+ ["Trading Time Frame", self.tf],
240
+ ["Start Trading Time", f"{self.start_time_hour}:{self.start_time_minutes}"],
241
+ ["Finishing Trading Time", f"{self.finishing_time_hour}:{self.finishing_time_minutes}"],
242
+ ["Closing Position After", f"{self.ending_time_hour}:{self.ending_time_minutes}"],
243
+ ]
244
+ # Custom table format
245
+ summary_table = tabulate(summary_data, headers=["Summary", "Values"], tablefmt="outline")
246
+
247
+ # Print the table
248
+ print("\n[======= Trade Account Summary =======]")
249
+ print(summary_table)
250
250
 
251
251
  def risk_managment(self):
252
252
  """Show the risk management parameters"""
@@ -259,36 +259,40 @@ class Trade(RiskManagement):
259
259
  currency = account_info.currency
260
260
  rates = self.get_currency_rates(self.symbol)
261
261
  marging_currency = rates['mc']
262
- print(
263
- "╔═════════════════ Risk Management ═════════════════════╗\n"
264
- f"Account Name {account_info.name}\n"
265
- f"Account Number {account_info.login}\n"
266
- f"Account Server {account_info.server}\n"
267
- f"Account Balance {account_info.balance} {currency}\n"
268
- f"Account Profit {_profit} {currency}\n"
269
- f"Account Equity {account_info.equity} {currency}\n"
270
- f"Account Leverage {self.get_leverage(True)}\n"
271
- f"Account Margin {round(account_info.margin, 2)} {currency}\n"
272
- f" Account Free Margin {account_info.margin_free} {currency}\n"
273
- f" Maximum Drawdown {self.max_risk}%\n"
274
- f"║ Risk Allowed {round((self.max_risk - self.risk_level()), 2)}%\n"
275
- f"║ Volume {self.volume()} {marging_currency}\n"
276
- f" Risk Per trade {-self.get_currency_risk()} {currency}\n"
277
- f" Profit Expected Per trade {self.expected_profit()} {currency}\n"
278
- f" Lot Size {self.lot} Lots\n"
279
- f" Stop Loss {self.stop_loss} Points\n"
280
- f" Loss Value Per Tick {round(loss, 5)} {currency}\n"
281
- f" Take Profit {self.take_profit} Points\n"
282
- f" Profit Value Per Tick {round(profit, 5)} {currency}\n"
283
- f"║ Break Even {self.break_even_points} Points\n"
284
- f"║ Deviation {self.deviation} Points\n"
285
- f" Trading Time Interval {self.get_minutes()} Minutes\n"
286
- f" Risk Level {ok}\n"
287
- f"║ Maximum Trades {self.max_trade()}\n"
288
- "╚══════════════════════════════════════════════════════╝\n"
289
- )
290
-
291
- def statistics(self, save=True, dir="stats"):
262
+ account_data = [
263
+ ["Account Name", account_info.name],
264
+ ["Account Number", account_info.login],
265
+ ["Account Server", account_info.server],
266
+ ["Account Balance", f"{account_info.balance} {currency}"],
267
+ ["Account Profit", f"{_profit} {currency}"],
268
+ ["Account Equity", f"{account_info.equity} {currency}"],
269
+ ["Account Leverage", self.get_leverage(True)],
270
+ ["Account Margin", f"{round(account_info.margin, 2)} {currency}"],
271
+ ["Account Free Margin", f"{account_info.margin_free} {currency}"],
272
+ ["Maximum Drawdown", f"{self.max_risk}%"],
273
+ ["Risk Allowed", f"{round((self.max_risk - self.risk_level()), 2)}%"],
274
+ ["Volume", f"{self.volume()} {marging_currency}"],
275
+ ["Risk Per trade", f"{-self.get_currency_risk()} {currency}"],
276
+ ["Profit Expected Per trade", f"{self.expected_profit()} {currency}"],
277
+ ["Lot Size", f"{self.get_lot()} Lots"],
278
+ ["Stop Loss", f"{self.get_stop_loss()} Points"],
279
+ ["Loss Value Per Tick", f"{round(loss, 5)} {currency}"],
280
+ ["Take Profit", f"{self.get_take_profit()} Points"],
281
+ ["Profit Value Per Tick", f"{round(profit, 5)} {currency}"],
282
+ ["Break Even", f"{self.get_break_even()} Points"],
283
+ ["Deviation", f"{self.get_deviation()} Points"],
284
+ ["Trading Time Interval", f"{self.get_minutes()} Minutes"],
285
+ ["Risk Level", ok],
286
+ ["Maximum Trades", self.max_trade()],
287
+ ]
288
+ # Custom table format
289
+ print("\n[======= Account Risk Management Overview =======]")
290
+ table = tabulate(account_data, headers=["Risk Metrics", "Values"], tablefmt="outline")
291
+
292
+ # Print the table
293
+ print(table)
294
+
295
+ def statistics(self, save=True, dir=None):
292
296
  """
293
297
  Print some statistics for the trading session and save to CSV if specified.
294
298
 
@@ -312,27 +316,27 @@ class Trade(RiskManagement):
312
316
  expected_profit = round((trade_risk * self.rr * -1), 2)
313
317
 
314
318
  # Formatting the statistics output
315
- stats_output = (
316
- f"╔═══════════════ Session Statistics ═════════════╗\n"
317
- f" Total Trades {deals}\n"
318
- f" Winning Trades {wins}\n"
319
- f" Losing Trades {losses}\n"
320
- f" Session Profit {profit} {currency}\n"
321
- f" Total Fees {total_fees} {currency}\n"
322
- f" Average Fees {average_fee} {currency}\n"
323
- f" Net Profit {net_profit} {currency}\n"
324
- f" Risk per Trade {trade_risk} {currency}\n"
325
- f" Expected Profit per Trade {self.expected_profit()} {currency}\n"
326
- f" Risk Reward Ratio {self.rr}\n"
327
- f" Win Rate {win_rate}%\n"
328
- f" Sharpe Ratio {self.sharpe()}\n"
329
- f"║ Trade Profitability {profitability}\n"
330
- "╚═════════════════════════════════════════════════╝\n"
331
- )
319
+ session_data = [
320
+ ["Total Trades", deals],
321
+ ["Winning Trades", wins],
322
+ ["Losing Trades", losses],
323
+ ["Session Profit", f"{profit} {currency}"],
324
+ ["Total Fees", f"{total_fees} {currency}"],
325
+ ["Average Fees", f"{average_fee} {currency}"],
326
+ ["Net Profit", f"{net_profit} {currency}"],
327
+ ["Risk per Trade", f"{trade_risk} {currency}"],
328
+ ["Expected Profit per Trade", f"{self.expected_profit()} {currency}"],
329
+ ["Risk Reward Ratio", self.rr],
330
+ ["Win Rate", f"{win_rate}%"],
331
+ ["Sharpe Ratio", self.sharpe()],
332
+ ["Trade Profitability", profitability],
333
+ ]
334
+ session_table = tabulate(session_data, headers=["Statistics", "Values"], tablefmt="outline")
332
335
 
333
336
  # Print the formatted statistics
334
337
  if self.verbose:
335
- print(stats_output)
338
+ print("\n[======= Trading Session Statistics =======]")
339
+ print(session_table)
336
340
 
337
341
  # Save to CSV if specified
338
342
  if save:
@@ -354,13 +358,15 @@ class Trade(RiskManagement):
354
358
  "Trade Profitability": profitability,
355
359
  }
356
360
  # Create the directory if it doesn't exist
361
+ if dir is None:
362
+ dir = f".{self.expert_name}_session_stats"
357
363
  os.makedirs(dir, exist_ok=True)
358
364
  if '.' in self.symbol:
359
365
  symbol = self.symbol.split('.')[0]
360
366
  else:
361
367
  symbol = self.symbol
362
368
 
363
- filename = f"{symbol}_{today_date}@{self.expert_id}_session.csv"
369
+ filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
364
370
  filepath = os.path.join(dir, filename)
365
371
 
366
372
  # Updated code to write to CSV
@@ -373,9 +379,10 @@ class Trade(RiskManagement):
373
379
  writer.writerow([stat, value])
374
380
  self.logger.info(f"Session statistics saved to {filepath}")
375
381
 
382
+ Buys = Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
376
383
  def open_buy_position(
377
384
  self,
378
- action: Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT'] = 'BMKT',
385
+ action: Buys = 'BMKT',
379
386
  price: Optional[float] = None,
380
387
  mm: bool = True,
381
388
  id: Optional[int] = None,
@@ -424,8 +431,7 @@ class Trade(RiskManagement):
424
431
  if action != 'BMKT':
425
432
  request["action"] = Mt5.TRADE_ACTION_PENDING
426
433
  request["type"] = self._order_type()[action][0]
427
-
428
- self.break_even(mm=mm)
434
+ self.break_even(mm=mm, id=Id)
429
435
  if self.check(comment):
430
436
  self.request_result(_price, request, action),
431
437
 
@@ -442,9 +448,10 @@ class Trade(RiskManagement):
442
448
  }
443
449
  return type
444
450
 
451
+ Sells = Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
445
452
  def open_sell_position(
446
453
  self,
447
- action: Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT'] = 'SMKT',
454
+ action: Sells = 'SMKT',
448
455
  price: Optional[float] = None,
449
456
  mm: bool = True,
450
457
  id: Optional[int] = None,
@@ -493,8 +500,7 @@ class Trade(RiskManagement):
493
500
  if action != 'SMKT':
494
501
  request["action"] = Mt5.TRADE_ACTION_PENDING
495
502
  request["type"] = self._order_type()[action][0]
496
-
497
- self.break_even(comment)
503
+ self.break_even(mm=mm, id=Id)
498
504
  if self.check(comment):
499
505
  self.request_result(_price, request, action)
500
506
 
@@ -542,8 +548,7 @@ class Trade(RiskManagement):
542
548
  self,
543
549
  price: float,
544
550
  request: Dict[str, Any],
545
- type: Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
546
- 'SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
551
+ type: Buys | Sells
547
552
  ):
548
553
  """
549
554
  Check if a trading order has been sent correctly
@@ -553,8 +558,7 @@ class Trade(RiskManagement):
553
558
  request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
554
559
  all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py
555
560
 
556
- type (str): The type of the order
557
- `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
561
+ type (str): The type of the order `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
558
562
  """
559
563
  # Send a trading request
560
564
  # Check the execution result
@@ -621,14 +625,10 @@ class Trade(RiskManagement):
621
625
  f"{self.get_account_info().currency}]\n"
622
626
  )
623
627
  self.logger.info(pos_info)
624
-
628
+
625
629
  def open_position(
626
630
  self,
627
- action: Literal[
628
- 'BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
629
- 'SMKT', 'SLMT', 'SSTP', 'SSTPLMT'],
630
- buy: bool = False,
631
- sell: bool = False,
631
+ action: Buys | Sells,
632
632
  price: Optional[float] = None,
633
633
  id: Optional[int] = None,
634
634
  mm: bool = True,
@@ -640,18 +640,20 @@ class Trade(RiskManagement):
640
640
  Args:
641
641
  action (str): (`'BMKT'`, `'SMKT'`) for Market orders
642
642
  or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
643
- buy (bool): A boolean True or False
644
- sell (bool): A boolean True or False
645
643
  id (int): The strategy id or expert Id
646
644
  mm (bool): Weither to put stop loss and tp or not
647
645
  comment (str): The comment for the closing position
648
646
  """
649
- if buy:
647
+ BUYS = ['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
648
+ SELLS = ['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
649
+ if action in BUYS:
650
650
  self.open_buy_position(
651
651
  action=action, price=price, id=id, mm=mm, comment=comment)
652
- if sell:
652
+ elif action in SELLS:
653
653
  self.open_sell_position(
654
654
  action=action, price=price, id=id, mm=mm, comment=comment)
655
+ else:
656
+ raise ValueError(f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}")
655
657
 
656
658
  @property
657
659
  def get_opened_orders(self):
@@ -699,13 +701,14 @@ class Trade(RiskManagement):
699
701
 
700
702
  Args:
701
703
  id (int): The strategy id or expert Id
702
- filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'win_trades')
704
+ filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'profitables')
703
705
  - `orders` are current open orders
704
706
  - `positions` are all current open positions
705
707
  - `buys` and `sells` are current buy or sell open positions
706
- - `win_trades` are current open position that have a profit greater than a threshold
708
+ - `profitables` are current open position that have a profit greater than a threshold
709
+ - `losings` are current open position that have a negative profit
707
710
  th (bool): the minimum treshold for winning position
708
- (only relevant when filter_type is 'win_trades')
711
+ (only relevant when filter_type is 'profitables')
709
712
 
710
713
  Returns:
711
714
  List[int] | None: A list of filtered tickets
@@ -727,7 +730,9 @@ class Trade(RiskManagement):
727
730
  continue
728
731
  if filter_type == 'sells' and item.type != 1:
729
732
  continue
730
- if filter_type == 'win_trades' and not self.win_trade(item, th=th):
733
+ if filter_type == 'profitables' and not self.win_trade(item, th=th):
734
+ continue
735
+ if filter_type == 'losings' and item.profit > 0:
731
736
  continue
732
737
  filtered_tickets.append(item.ticket)
733
738
  return filtered_tickets if filtered_tickets else None
@@ -739,8 +744,11 @@ class Trade(RiskManagement):
739
744
  def get_current_open_positions(self, id: Optional[int] = None) -> List[int] | None:
740
745
  return self.get_filtered_tickets(id=id, filter_type='positions')
741
746
 
742
- def get_current_win_trades(self, id: Optional[int] = None, th=None) -> List[int] | None:
743
- return self.get_filtered_tickets(id=id, filter_type='win_trades', th=th)
747
+ def get_current_profitables(self, id: Optional[int] = None, th=None) -> List[int] | None:
748
+ return self.get_filtered_tickets(id=id, filter_type='profitables', th=th)
749
+
750
+ def get_current_losings(self, id: Optional[int] = None) -> List[int] | None:
751
+ return self.get_filtered_tickets(id=id, filter_type='losings')
744
752
 
745
753
  def get_current_buys(self, id: Optional[int] = None) -> List[int] | None:
746
754
  return self.get_filtered_tickets(id=id, filter_type='buys')
@@ -748,8 +756,9 @@ class Trade(RiskManagement):
748
756
  def get_current_sells(self, id: Optional[int] = None) -> List[int] | None:
749
757
  return self.get_filtered_tickets(id=id, filter_type='sells')
750
758
 
751
- def positive_profit(self, th: Optional[float] = None
752
- ) -> bool:
759
+ def positive_profit(self, th: Optional[float] = None,
760
+ id: Optional[int] = None,
761
+ account: bool = True) -> bool:
753
762
  """
754
763
  Check is the total profit on current open positions
755
764
  Is greater than a minimum profit express as percentage
@@ -757,35 +766,56 @@ class Trade(RiskManagement):
757
766
 
758
767
  Args:
759
768
  th (float): The minimum profit target on current positions
769
+ id (int): The strategy id or expert Id
770
+ account (bool): Weither to check positions on the account or on the symbol
760
771
  """
761
- positions = self.get_current_open_positions()
772
+ if account and id is None:
773
+ # All open positions no matter the symbol or strategy or expert
774
+ positions = self.get_positions()
775
+ elif account and id is not None:
776
+ # All open positions for a specific strategy or expert no matter the symbol
777
+ positions = self.get_positions()
778
+ positions = [position for position in positions if position.magic == id]
779
+ elif not account and id is None:
780
+ # All open positions for the current symbol no matter the strategy or expert
781
+ positions = self.get_positions(symbol=self.symbol)
782
+ elif not account and id is not None:
783
+ # All open positions for the current symbol and a specific strategy or expert
784
+ positions = self.get_positions(symbol=self.symbol)
785
+ positions = [position for position in positions if position.magic == id]
762
786
  profit = 0.0
763
787
  balance = self.get_account_info().balance
764
788
  target = round((balance * self.target)/100, 2)
765
- if positions is not None:
789
+ if positions is not None or len(positions) != 0:
766
790
  for position in positions:
767
- time.sleep(0.1)
768
- history = self.get_positions(
769
- ticket=position
770
- )
771
- profit += history[0].profit
791
+ profit += position.profit
772
792
  fees = self.get_stats()[0]["average_fee"] * len(positions)
773
793
  current_profit = profit + fees
774
794
  th_profit = (target*th)/100 if th is not None else (target*0.01)
775
- if current_profit > th_profit:
776
- return True
795
+ return current_profit >= th_profit
777
796
  return False
778
797
 
779
- def break_even(self, mm=True, id: Optional[int] = None):
798
+ def break_even(self, mm=True,
799
+ id: Optional[int] = None,
800
+ trail: Optional[bool] = True,
801
+ stop_trail: Optional[int] = None,
802
+ trail_after_points: Optional[int] = None,
803
+ be_plus_points: Optional[int] = None
804
+ ):
780
805
  """
781
- Checks if it's time to put the break even,
782
- if so , it will sets the break even ,and if the break even was already set,
783
- it checks if the price has moved in favorable direction,
784
- if so , it set the new break even.
806
+ This function checks if it's time to set the break-even level for a trading position.
807
+ If it is, it sets the break-even level. If the break-even level has already been set,
808
+ it checks if the price has moved in a favorable direction. If it has, and the trail parameter is set to True,
809
+ it updates the break-even level based on the trail_after_points and stop_trail parameters.
785
810
 
786
811
  Args:
787
- id (int): The strategy Id or Expert Id
788
- mm (bool): Weither to manage the position or not
812
+ id (int): The strategy ID or expert ID.
813
+ mm (bool): Whether to manage the position or not.
814
+ trail (bool): Whether to trail the stop loss or not.
815
+ stop_trail (int): Number of points to trail the stop loss by.
816
+ It represent the distance from the current price to the stop loss.
817
+ trail_after_points (int): Number of points in profit from where the strategy will start to trail the stop loss.
818
+ be_plus_points (int): Number of points to add to the break-even level. Represents the minimum profit to secure.
789
819
  """
790
820
  time.sleep(0.1)
791
821
  if not mm:
@@ -793,6 +823,10 @@ class Trade(RiskManagement):
793
823
  Id = id if id is not None else self.expert_id
794
824
  positions = self.get_positions(symbol=self.symbol)
795
825
  be = self.get_break_even()
826
+ if trail_after_points is not None:
827
+ assert trail_after_points > be, \
828
+ "trail_after_points must be greater than break even"\
829
+ " or set to None"
796
830
  if positions is not None:
797
831
  for position in positions:
798
832
  if position.magic == Id:
@@ -805,30 +839,39 @@ class Trade(RiskManagement):
805
839
  if break_even:
806
840
  # Check if break-even has already been set for this position
807
841
  if position.ticket not in self.break_even_status:
808
- self.set_break_even(position, be)
842
+ price = None
843
+ if be_plus_points is not None:
844
+ price = position.price_open + (be_plus_points * point)
845
+ self.set_break_even(position, be, price=price)
809
846
  self.break_even_status.append(position.ticket)
847
+ self.break_even_points[position.ticket] = be
810
848
  else:
849
+ # Skip this if the trail is not set to True
850
+ if not trail:
851
+ continue
811
852
  # Check if the price has moved favorably
812
- new_be = be * 0.50
813
- favorable_move = (
814
- (position.type == 0 and (
815
- (position.price_current - position.sl) / point) > new_be)
816
- or
817
- (position.type == 1 and (
818
- (position.sl - position.price_current) / point) > new_be)
819
- )
853
+ new_be = round(be * 0.10) if be_plus_points is None else be_plus_points
854
+ if trail_after_points is not None:
855
+ if position.ticket not in self.trail_after_points:
856
+ # This ensures that the position rich the minimum points required
857
+ # before the trail can be set
858
+ new_be = trail_after_points - be
859
+ self.trail_after_points.append(position.ticket)
860
+ new_be_points = self.break_even_points[position.ticket] + new_be
861
+ favorable_move = float(points/point) >= new_be_points
820
862
  if favorable_move:
863
+ # This allows the position to go to take profit in case of a swing trade
864
+ # If is a scalping position, we can set the stop_trail close to the current price.
865
+ trail_points = round(be * 0.50) if stop_trail is None else stop_trail
821
866
  # Calculate the new break-even level and price
822
867
  if position.type == 0:
823
- new_level = round(
824
- position.sl + (new_be * point), digits)
825
- new_price = round(
826
- position.sl + ((0.25 * be) * point), digits)
827
- else:
828
- new_level = round(
829
- position.sl - (new_be * point), digits)
830
- new_price = round(
831
- position.sl - ((0.25 * be) * point), digits)
868
+ # This level validate the favorable move of the price
869
+ new_level = round(position.price_open + (new_be_points * point), digits)
870
+ # This price is set away from the current price by the trail_points
871
+ new_price = round(position.price_current - (trail_points * point), digits)
872
+ elif position.type == 1:
873
+ new_level = round(position.price_open - (new_be_points * point), digits)
874
+ new_price = round(position.price_current + (trail_points * point), digits)
832
875
  self.set_break_even(
833
876
  position, be, price=new_price, level=new_level
834
877
  )
@@ -842,14 +885,10 @@ class Trade(RiskManagement):
842
885
  Sets the break-even level for a given trading position.
843
886
 
844
887
  Args:
845
- position (TradePosition):
846
- The trading position for which the break-even is to be set
847
- This is the value return by `mt5.positions_get()`
888
+ position (TradePosition): The trading position for which the break-even is to be set. This is the value return by `mt5.positions_get()`.
848
889
  be (int): The break-even level in points.
849
- level (float): The break-even level in price
850
- if set to None , it will be calated automaticaly.
851
- price (float): The break-even price
852
- if set to None , it will be calated automaticaly.
890
+ level (float): The break-even level in price, if set to None , it will be calated automaticaly.
891
+ price (float): The break-even price, if set to None , it will be calated automaticaly.
853
892
  """
854
893
  point = self.get_symbol_info(self.symbol).point
855
894
  digits = self.get_symbol_info(self.symbol).digits
@@ -863,7 +902,9 @@ class Trade(RiskManagement):
863
902
  break_even_level = position.price_open + (be * point)
864
903
  break_even_price = position.price_open + \
865
904
  ((fees_points + spread) * point)
866
- _price = break_even_price if price is None else price
905
+ # Check if the price specified is greater or lower than the calculated price
906
+ _price = break_even_price if price is None or \
907
+ price < break_even_price else price
867
908
  _level = break_even_level if level is None else level
868
909
 
869
910
  if self.get_tick_info(self.symbol).ask > _level:
@@ -882,7 +923,8 @@ class Trade(RiskManagement):
882
923
  break_even_level = position.price_open - (be * point)
883
924
  break_even_price = position.price_open - \
884
925
  ((fees_points + spread) * point)
885
- _price = break_even_price if price is None else price
926
+ _price = break_even_price if price is None or \
927
+ price > break_even_price else price
886
928
  _level = break_even_level if level is None else level
887
929
 
888
930
  if self.get_tick_info(self.symbol).bid < _level:
@@ -987,7 +1029,7 @@ class Trade(RiskManagement):
987
1029
  for position in self.opened_positions:
988
1030
  time.sleep(0.1)
989
1031
  # This return two TradeDeal Object,
990
- # The first one is the one the opening order
1032
+ # The first one is the opening order
991
1033
  # The second is the closing order
992
1034
  history = self.get_trades_history(
993
1035
  position=position, to_df=False
@@ -1023,98 +1065,103 @@ class Trade(RiskManagement):
1023
1065
  # get all Actives positions
1024
1066
  time.sleep(0.1)
1025
1067
  Id = id if id is not None else self.expert_id
1026
- positions = self.get_positions(symbol=self.symbol)
1068
+ positions = self.get_positions(ticket=ticket)
1027
1069
  buy_price = self.get_tick_info(self.symbol).ask
1028
1070
  sell_price = self.get_tick_info(self.symbol).bid
1029
1071
  digits = self.get_symbol_info(self.symbol).digits
1030
1072
  deviation = self.get_deviation()
1031
- if positions is not None:
1032
- for position in positions:
1033
- if (position.ticket == ticket
1034
- and position.magic == Id
1035
- ):
1036
- buy = position.type == 0
1037
- sell = position.type == 1
1038
- request = {
1039
- "action": Mt5.TRADE_ACTION_DEAL,
1040
- "symbol": self.symbol,
1041
- "volume": (position.volume*pct),
1042
- "type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
1043
- "position": ticket,
1044
- "price": sell_price if buy else buy_price,
1045
- "deviation": deviation,
1046
- "magic": Id,
1047
- "comment": f"@{self.expert_name}" if comment is None else comment,
1048
- "type_time": Mt5.ORDER_TIME_GTC,
1049
- "type_filling": Mt5.ORDER_FILLING_FOK,
1050
- }
1051
- addtionnal = f", SYMBOL={self.symbol}"
1052
- try:
1053
- check_result = self.check_order(request)
1054
- result = self.send_order(request)
1055
- except Exception as e:
1056
- print(f"{self.current_datetime()} -", end=' ')
1057
- trade_retcode_message(
1058
- result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1059
- if result.retcode != Mt5.TRADE_RETCODE_DONE:
1060
- msg = trade_retcode_message(result.retcode)
1061
- self.logger.error(
1062
- f"Closing Order Request, Position: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}")
1063
- tries = 0
1064
- while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
1065
- time.sleep(1)
1066
- try:
1067
- check_result = self.check_order(request)
1068
- result = self.send_order(request)
1069
- except Exception as e:
1070
- print(f"{self.current_datetime()} -", end=' ')
1071
- trade_retcode_message(
1072
- result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1073
- if result.retcode == Mt5.TRADE_RETCODE_DONE:
1074
- break
1075
- tries += 1
1076
- if result.retcode == Mt5.TRADE_RETCODE_DONE:
1077
- msg = trade_retcode_message(result.retcode)
1078
- self.logger.info(
1079
- f"Closing Order {msg}{addtionnal}")
1080
- info = (
1081
- f"Position #{ticket} closed, Symbol: {self.symbol}, Price: @{request['price']}")
1082
- self.logger.info(info)
1083
- return True
1084
- else:
1085
- return False
1073
+ if positions is not None and len(positions) == 1:
1074
+ position = positions[0]
1075
+ if (position.ticket == ticket
1076
+ and position.magic == Id
1077
+ ):
1078
+ buy = position.type == 0
1079
+ sell = position.type == 1
1080
+ request = {
1081
+ "action": Mt5.TRADE_ACTION_DEAL,
1082
+ "symbol": self.symbol,
1083
+ "volume": (position.volume*pct),
1084
+ "type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
1085
+ "position": ticket,
1086
+ "price": sell_price if buy else buy_price,
1087
+ "deviation": deviation,
1088
+ "magic": Id,
1089
+ "comment": f"@{self.expert_name}" if comment is None else comment,
1090
+ "type_time": Mt5.ORDER_TIME_GTC,
1091
+ "type_filling": Mt5.ORDER_FILLING_FOK,
1092
+ }
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
1086
1128
 
1129
+ Positions = Literal["all", "buy", "sell", "profitable", "losing"]
1087
1130
  def close_positions(
1088
1131
  self,
1089
- position_type: Literal["all", "buy", "sell"] = "all",
1132
+ position_type: Positions,
1090
1133
  id: Optional[int] = None,
1091
1134
  comment: Optional[str] = None):
1092
1135
  """
1093
1136
  Args:
1094
- position_type (str): Type of positions to close ("all", "buy", "sell")
1137
+ position_type (str): Type of positions to close ('all', 'buy', 'sell', 'profitable', 'losing')
1095
1138
  id (int): The unique ID of the Expert or Strategy
1096
1139
  comment (str): Comment for the closing position
1097
1140
  """
1098
1141
  if position_type == "all":
1099
1142
  positions = self.get_positions(symbol=self.symbol)
1100
1143
  elif position_type == "buy":
1101
- positions = self.get_current_buys()
1144
+ positions = self.get_current_buys(id=id)
1102
1145
  elif position_type == "sell":
1103
- positions = self.get_current_sells()
1146
+ positions = self.get_current_sells(id=id)
1147
+ elif position_type == "profitable":
1148
+ positions = self.get_current_profitables(id=id)
1149
+ elif position_type == "losing":
1150
+ positions = self.get_current_losings(id=id)
1104
1151
  else:
1105
- logger.error(f"Invalid position type: {position_type}")
1152
+ self.logger.error(f"Invalid position type: {position_type}")
1106
1153
  return
1107
1154
 
1108
1155
  if positions is not None:
1109
1156
  if position_type == 'all':
1110
- tickets = [position.ticket for position in positions]
1157
+ tickets = [position.ticket for position in positions if position.magic == id]
1111
1158
  else:
1112
1159
  tickets = positions
1113
1160
  else:
1114
1161
  tickets = []
1115
1162
 
1116
1163
  if position_type == 'all':
1117
- pos_type = ''
1164
+ pos_type = 'open'
1118
1165
  else:
1119
1166
  pos_type = position_type
1120
1167
 
@@ -1224,12 +1271,11 @@ class Trade(RiskManagement):
1224
1271
  return 0.0
1225
1272
  df = df2.iloc[1:]
1226
1273
  profit = df[["profit", "commission", "fee", "swap"]].sum(axis=1)
1227
- returns = profit.values
1228
- returns = np.diff(returns, prepend=0.0)
1229
- N = self.max_trade() * 252
1230
- sharp = np.sqrt(N) * np.mean(returns) / (np.std(returns) + 1e-10)
1274
+ returns = profit.pct_change(fill_method=None)
1275
+ periods = self.max_trade() * 252
1276
+ sharpe = create_sharpe_ratio(returns, periods=periods)
1231
1277
 
1232
- return round(sharp, 3)
1278
+ return round(sharpe, 3)
1233
1279
 
1234
1280
  def days_end(self) -> bool:
1235
1281
  """Check if it is the end of the trading day."""
@@ -1290,27 +1336,68 @@ class Trade(RiskManagement):
1290
1336
  def create_trade_instance(
1291
1337
  symbols: List[str],
1292
1338
  params: Dict[str, Any],
1293
- logger: Logger = ...) -> Dict[str, Trade]:
1339
+ daily_risk: Optional[Dict[str, float]] = None,
1340
+ max_risk: Optional[Dict[str, float]] = None,
1341
+ pchange_sl: Optional[Dict[str, float] | float] = None,
1342
+ logger: Logger = None) -> Dict[str, Trade]:
1294
1343
  """
1295
1344
  Creates Trade instances for each symbol provided.
1296
1345
 
1297
1346
  Args:
1298
1347
  symbols: A list of trading symbols (e.g., ['AAPL', 'MSFT']).
1299
1348
  params: A dictionary containing parameters for the Trade instance.
1349
+ daily_risk: A dictionary containing daily risk weight for each symbol.
1350
+ max_risk: A dictionary containing maximum risk weight for each symbol.
1351
+ logger: A logger instance.
1300
1352
 
1301
1353
  Returns:
1302
1354
  A dictionary where keys are symbols and values are corresponding Trade instances.
1303
1355
 
1304
1356
  Raises:
1305
1357
  ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
1358
+
1359
+ Note:
1360
+ `daily_risk` and `max_risk` can be used to manage the risk of each symbol
1361
+ based on the importance of the symbol in the portfolio or strategy.
1306
1362
  """
1307
1363
  instances = {}
1308
1364
  if not symbols:
1309
1365
  raise ValueError("The 'symbols' list cannot be empty.")
1366
+ if not params:
1367
+ raise ValueError("The 'params' dictionary cannot be empty.")
1368
+
1369
+ if daily_risk is not None:
1370
+ for symbol in symbols:
1371
+ if symbol not in daily_risk:
1372
+ raise ValueError(f"Missing daily risk weight for symbol '{symbol}'.")
1373
+ if max_risk is not None:
1374
+ for symbol in symbols:
1375
+ if symbol not in max_risk:
1376
+ raise ValueError(f"Missing maximum risk percentage for symbol '{symbol}'.")
1377
+ if pchange_sl is not None:
1378
+ if isinstance(pchange_sl, dict):
1379
+ for symbol in symbols:
1380
+ if symbol not in pchange_sl:
1381
+ raise ValueError(f"Missing percentage change for symbol '{symbol}'.")
1382
+
1310
1383
  for symbol in symbols:
1311
1384
  try:
1312
- instances[symbol] = Trade(**params, symbol=symbol)
1385
+ params['symbol'] = symbol
1386
+ params['pchange_sl'] = (
1387
+ pchange_sl[symbol] if pchange_sl is not None
1388
+ and isinstance(pchange_sl, dict) else pchange_sl
1389
+ )
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)
1313
1393
  except Exception as e:
1314
1394
  logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
1315
- assert len(instances) == len(symbols), "Failed to create Trade instances for all symbols."
1316
- return instances
1395
+
1396
+ if len(instances) != len(symbols):
1397
+ for symbol in symbols:
1398
+ if symbol not in instances:
1399
+ if logger is not None:
1400
+ logger.error(f"Failed to create Trade instance for SYMBOL={symbol}")
1401
+ else:
1402
+ raise ValueError(f"Failed to create Trade instance for SYMBOL={symbol}")
1403
+ return instances