bbstrader 0.1.7__py3-none-any.whl → 0.1.8__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
12
  from bbstrader.metatrader.account import 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
@@ -74,6 +75,7 @@ class Trade(RiskManagement):
74
75
  expert_id: int = 9818,
75
76
  version: str = '1.0',
76
77
  target: float = 5.0,
78
+ be_on_trade_open: bool = True,
77
79
  start_time: str = "1:00",
78
80
  finishing_time: str = "23:00",
79
81
  ending_time: str = "23:30",
@@ -92,6 +94,7 @@ class Trade(RiskManagement):
92
94
  or the strategy used on the symbol.
93
95
  version (str): The `version` of the expert advisor.
94
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.
95
98
  start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
96
99
  finishing_time (str): The time after which no new position can be opened.
97
100
  ending_time (str): The time after which any open position will be closed.
@@ -128,6 +131,7 @@ class Trade(RiskManagement):
128
131
  self.expert_id = expert_id
129
132
  self.version = version
130
133
  self.target = target
134
+ self.be_on_trade_open = be_on_trade_open
131
135
  self.verbose = verbose
132
136
  self.start = start_time
133
137
  self.end = ending_time
@@ -136,12 +140,6 @@ class Trade(RiskManagement):
136
140
  self.logger = self._get_logger(logger, console_log)
137
141
  self.tf = kwargs.get("time_frame", 'D1')
138
142
 
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
143
  self.start_time_hour, self.start_time_minutes = self.start.split(":")
146
144
  self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
147
145
  ":")
@@ -152,6 +150,8 @@ class Trade(RiskManagement):
152
150
  self.opened_positions = []
153
151
  self.opened_orders = []
154
152
  self.break_even_status = []
153
+ self.break_even_points = {}
154
+ self.trail_after_points = {}
155
155
 
156
156
  self.initialize()
157
157
  self.select_symbol()
@@ -168,7 +168,7 @@ class Trade(RiskManagement):
168
168
  def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
169
169
  """Get the logger object"""
170
170
  if isinstance(logger, str):
171
- return config_logger(logger, consol_log)
171
+ return config_logger(logger, consol_log=consol_log)
172
172
  return logger
173
173
 
174
174
  def initialize(self):
@@ -235,18 +235,22 @@ class Trade(RiskManagement):
235
235
 
236
236
  def summary(self):
237
237
  """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
- )
238
+ summary_data = [
239
+ ["Expert Advisor Name", f"@{self.expert_name}"],
240
+ ["Expert Advisor Version", f"@{self.version}"],
241
+ ["Expert | Strategy ID", self.expert_id],
242
+ ["Trading Symbol", self.symbol],
243
+ ["Trading Time Frame", self.tf],
244
+ ["Start Trading Time", f"{self.start_time_hour}:{self.start_time_minutes}"],
245
+ ["Finishing Trading Time", f"{self.finishing_time_hour}:{self.finishing_time_minutes}"],
246
+ ["Closing Position After", f"{self.ending_time_hour}:{self.ending_time_minutes}"],
247
+ ]
248
+ # Custom table format
249
+ summary_table = tabulate(summary_data, headers=["Summary", "Values"], tablefmt="outline")
250
+
251
+ # Print the table
252
+ print("\n[======= Trade Account Summary =======]")
253
+ print(summary_table)
250
254
 
251
255
  def risk_managment(self):
252
256
  """Show the risk management parameters"""
@@ -259,36 +263,40 @@ class Trade(RiskManagement):
259
263
  currency = account_info.currency
260
264
  rates = self.get_currency_rates(self.symbol)
261
265
  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"):
266
+ account_data = [
267
+ ["Account Name", account_info.name],
268
+ ["Account Number", account_info.login],
269
+ ["Account Server", account_info.server],
270
+ ["Account Balance", f"{account_info.balance} {currency}"],
271
+ ["Account Profit", f"{_profit} {currency}"],
272
+ ["Account Equity", f"{account_info.equity} {currency}"],
273
+ ["Account Leverage", self.get_leverage(True)],
274
+ ["Account Margin", f"{round(account_info.margin, 2)} {currency}"],
275
+ ["Account Free Margin", f"{account_info.margin_free} {currency}"],
276
+ ["Maximum Drawdown", f"{self.max_risk}%"],
277
+ ["Risk Allowed", f"{round((self.max_risk - self.risk_level()), 2)}%"],
278
+ ["Volume", f"{self.volume()} {marging_currency}"],
279
+ ["Risk Per trade", f"{-self.get_currency_risk()} {currency}"],
280
+ ["Profit Expected Per trade", f"{self.expected_profit()} {currency}"],
281
+ ["Lot Size", f"{self.get_lot()} Lots"],
282
+ ["Stop Loss", f"{self.get_stop_loss()} Points"],
283
+ ["Loss Value Per Tick", f"{round(loss, 5)} {currency}"],
284
+ ["Take Profit", f"{self.get_take_profit()} Points"],
285
+ ["Profit Value Per Tick", f"{round(profit, 5)} {currency}"],
286
+ ["Break Even", f"{self.get_break_even()} Points"],
287
+ ["Deviation", f"{self.get_deviation()} Points"],
288
+ ["Trading Time Interval", f"{self.get_minutes()} Minutes"],
289
+ ["Risk Level", ok],
290
+ ["Maximum Trades", self.max_trade()],
291
+ ]
292
+ # Custom table format
293
+ print("\n[======= Account Risk Management Overview =======]")
294
+ table = tabulate(account_data, headers=["Risk Metrics", "Values"], tablefmt="outline")
295
+
296
+ # Print the table
297
+ print(table)
298
+
299
+ def statistics(self, save=True, dir=None):
292
300
  """
293
301
  Print some statistics for the trading session and save to CSV if specified.
294
302
 
@@ -312,27 +320,27 @@ class Trade(RiskManagement):
312
320
  expected_profit = round((trade_risk * self.rr * -1), 2)
313
321
 
314
322
  # 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
- )
323
+ session_data = [
324
+ ["Total Trades", deals],
325
+ ["Winning Trades", wins],
326
+ ["Losing Trades", losses],
327
+ ["Session Profit", f"{profit} {currency}"],
328
+ ["Total Fees", f"{total_fees} {currency}"],
329
+ ["Average Fees", f"{average_fee} {currency}"],
330
+ ["Net Profit", f"{net_profit} {currency}"],
331
+ ["Risk per Trade", f"{trade_risk} {currency}"],
332
+ ["Expected Profit per Trade", f"{self.expected_profit()} {currency}"],
333
+ ["Risk Reward Ratio", self.rr],
334
+ ["Win Rate", f"{win_rate}%"],
335
+ ["Sharpe Ratio", self.sharpe()],
336
+ ["Trade Profitability", profitability],
337
+ ]
338
+ session_table = tabulate(session_data, headers=["Statistics", "Values"], tablefmt="outline")
332
339
 
333
340
  # Print the formatted statistics
334
341
  if self.verbose:
335
- print(stats_output)
342
+ print("\n[======= Trading Session Statistics =======]")
343
+ print(session_table)
336
344
 
337
345
  # Save to CSV if specified
338
346
  if save:
@@ -354,13 +362,15 @@ class Trade(RiskManagement):
354
362
  "Trade Profitability": profitability,
355
363
  }
356
364
  # Create the directory if it doesn't exist
365
+ if dir is None:
366
+ dir = f"{self.expert_name}_session_stats"
357
367
  os.makedirs(dir, exist_ok=True)
358
368
  if '.' in self.symbol:
359
369
  symbol = self.symbol.split('.')[0]
360
370
  else:
361
371
  symbol = self.symbol
362
372
 
363
- filename = f"{symbol}_{today_date}@{self.expert_id}_session.csv"
373
+ filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
364
374
  filepath = os.path.join(dir, filename)
365
375
 
366
376
  # Updated code to write to CSV
@@ -373,9 +383,10 @@ class Trade(RiskManagement):
373
383
  writer.writerow([stat, value])
374
384
  self.logger.info(f"Session statistics saved to {filepath}")
375
385
 
386
+ Buys = Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
376
387
  def open_buy_position(
377
388
  self,
378
- action: Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT'] = 'BMKT',
389
+ action: Buys = 'BMKT',
379
390
  price: Optional[float] = None,
380
391
  mm: bool = True,
381
392
  id: Optional[int] = None,
@@ -424,8 +435,8 @@ class Trade(RiskManagement):
424
435
  if action != 'BMKT':
425
436
  request["action"] = Mt5.TRADE_ACTION_PENDING
426
437
  request["type"] = self._order_type()[action][0]
427
-
428
- self.break_even(mm=mm)
438
+ if self.be_on_trade_open:
439
+ self.break_even(mm=mm, id=Id)
429
440
  if self.check(comment):
430
441
  self.request_result(_price, request, action),
431
442
 
@@ -442,9 +453,10 @@ class Trade(RiskManagement):
442
453
  }
443
454
  return type
444
455
 
456
+ Sells = Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
445
457
  def open_sell_position(
446
458
  self,
447
- action: Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT'] = 'SMKT',
459
+ action: Sells = 'SMKT',
448
460
  price: Optional[float] = None,
449
461
  mm: bool = True,
450
462
  id: Optional[int] = None,
@@ -493,8 +505,8 @@ class Trade(RiskManagement):
493
505
  if action != 'SMKT':
494
506
  request["action"] = Mt5.TRADE_ACTION_PENDING
495
507
  request["type"] = self._order_type()[action][0]
496
-
497
- self.break_even(comment)
508
+ if self.be_on_trade_open:
509
+ self.break_even(mm=mm, id=Id)
498
510
  if self.check(comment):
499
511
  self.request_result(_price, request, action)
500
512
 
@@ -542,8 +554,7 @@ class Trade(RiskManagement):
542
554
  self,
543
555
  price: float,
544
556
  request: Dict[str, Any],
545
- type: Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
546
- 'SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
557
+ type: Buys | Sells
547
558
  ):
548
559
  """
549
560
  Check if a trading order has been sent correctly
@@ -553,8 +564,7 @@ class Trade(RiskManagement):
553
564
  request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
554
565
  all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py
555
566
 
556
- type (str): The type of the order
557
- `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
567
+ type (str): The type of the order `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
558
568
  """
559
569
  # Send a trading request
560
570
  # Check the execution result
@@ -621,14 +631,10 @@ class Trade(RiskManagement):
621
631
  f"{self.get_account_info().currency}]\n"
622
632
  )
623
633
  self.logger.info(pos_info)
624
-
634
+
625
635
  def open_position(
626
636
  self,
627
- action: Literal[
628
- 'BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
629
- 'SMKT', 'SLMT', 'SSTP', 'SSTPLMT'],
630
- buy: bool = False,
631
- sell: bool = False,
637
+ action: Buys | Sells,
632
638
  price: Optional[float] = None,
633
639
  id: Optional[int] = None,
634
640
  mm: bool = True,
@@ -640,18 +646,20 @@ class Trade(RiskManagement):
640
646
  Args:
641
647
  action (str): (`'BMKT'`, `'SMKT'`) for Market orders
642
648
  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
649
  id (int): The strategy id or expert Id
646
650
  mm (bool): Weither to put stop loss and tp or not
647
651
  comment (str): The comment for the closing position
648
652
  """
649
- if buy:
653
+ BUYS = ['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
654
+ SELLS = ['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
655
+ if action in BUYS:
650
656
  self.open_buy_position(
651
657
  action=action, price=price, id=id, mm=mm, comment=comment)
652
- if sell:
658
+ elif action in SELLS:
653
659
  self.open_sell_position(
654
660
  action=action, price=price, id=id, mm=mm, comment=comment)
661
+ else:
662
+ raise ValueError(f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}")
655
663
 
656
664
  @property
657
665
  def get_opened_orders(self):
@@ -699,13 +707,14 @@ class Trade(RiskManagement):
699
707
 
700
708
  Args:
701
709
  id (int): The strategy id or expert Id
702
- filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'win_trades')
710
+ filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'profitables')
703
711
  - `orders` are current open orders
704
712
  - `positions` are all current open positions
705
713
  - `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
714
+ - `profitables` are current open position that have a profit greater than a threshold
715
+ - `losings` are current open position that have a negative profit
707
716
  th (bool): the minimum treshold for winning position
708
- (only relevant when filter_type is 'win_trades')
717
+ (only relevant when filter_type is 'profitables')
709
718
 
710
719
  Returns:
711
720
  List[int] | None: A list of filtered tickets
@@ -727,7 +736,9 @@ class Trade(RiskManagement):
727
736
  continue
728
737
  if filter_type == 'sells' and item.type != 1:
729
738
  continue
730
- if filter_type == 'win_trades' and not self.win_trade(item, th=th):
739
+ if filter_type == 'profitables' and not self.win_trade(item, th=th):
740
+ continue
741
+ if filter_type == 'losings' and item.profit > 0:
731
742
  continue
732
743
  filtered_tickets.append(item.ticket)
733
744
  return filtered_tickets if filtered_tickets else None
@@ -739,8 +750,11 @@ class Trade(RiskManagement):
739
750
  def get_current_open_positions(self, id: Optional[int] = None) -> List[int] | None:
740
751
  return self.get_filtered_tickets(id=id, filter_type='positions')
741
752
 
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)
753
+ def get_current_profitables(self, id: Optional[int] = None, th=None) -> List[int] | None:
754
+ return self.get_filtered_tickets(id=id, filter_type='profitables', th=th)
755
+
756
+ def get_current_losings(self, id: Optional[int] = None) -> List[int] | None:
757
+ return self.get_filtered_tickets(id=id, filter_type='losings')
744
758
 
745
759
  def get_current_buys(self, id: Optional[int] = None) -> List[int] | None:
746
760
  return self.get_filtered_tickets(id=id, filter_type='buys')
@@ -748,8 +762,9 @@ class Trade(RiskManagement):
748
762
  def get_current_sells(self, id: Optional[int] = None) -> List[int] | None:
749
763
  return self.get_filtered_tickets(id=id, filter_type='sells')
750
764
 
751
- def positive_profit(self, th: Optional[float] = None
752
- ) -> bool:
765
+ def positive_profit(self, th: Optional[float] = None,
766
+ id: Optional[int] = None,
767
+ account: bool = True) -> bool:
753
768
  """
754
769
  Check is the total profit on current open positions
755
770
  Is greater than a minimum profit express as percentage
@@ -757,35 +772,56 @@ class Trade(RiskManagement):
757
772
 
758
773
  Args:
759
774
  th (float): The minimum profit target on current positions
775
+ id (int): The strategy id or expert Id
776
+ account (bool): Weither to check positions on the account or on the symbol
760
777
  """
761
- positions = self.get_current_open_positions()
778
+ if account and id is None:
779
+ # All open positions no matter the symbol or strategy or expert
780
+ positions = self.get_positions()
781
+ elif account and id is not None:
782
+ # All open positions for a specific strategy or expert no matter the symbol
783
+ positions = self.get_positions()
784
+ positions = [position for position in positions if position.magic == id]
785
+ elif not account and id is None:
786
+ # All open positions for the current symbol no matter the strategy or expert
787
+ positions = self.get_positions(symbol=self.symbol)
788
+ elif not account and id is not None:
789
+ # All open positions for the current symbol and a specific strategy or expert
790
+ positions = self.get_positions(symbol=self.symbol)
791
+ positions = [position for position in positions if position.magic == id]
762
792
  profit = 0.0
763
793
  balance = self.get_account_info().balance
764
794
  target = round((balance * self.target)/100, 2)
765
- if positions is not None:
795
+ if positions is not None or len(positions) != 0:
766
796
  for position in positions:
767
- time.sleep(0.1)
768
- history = self.get_positions(
769
- ticket=position
770
- )
771
- profit += history[0].profit
797
+ profit += position.profit
772
798
  fees = self.get_stats()[0]["average_fee"] * len(positions)
773
799
  current_profit = profit + fees
774
800
  th_profit = (target*th)/100 if th is not None else (target*0.01)
775
- if current_profit > th_profit:
776
- return True
801
+ return current_profit >= th_profit
777
802
  return False
778
803
 
779
- def break_even(self, mm=True, id: Optional[int] = None):
804
+ def break_even(self, mm=True,
805
+ id: Optional[int] = None,
806
+ trail: Optional[bool] = True,
807
+ stop_trail: Optional[int] = None,
808
+ trail_after_points: Optional[int] = None,
809
+ be_plus_points: Optional[int] = None
810
+ ):
780
811
  """
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.
812
+ This function checks if it's time to set the break-even level for a trading position.
813
+ If it is, it sets the break-even level. If the break-even level has already been set,
814
+ it checks if the price has moved in a favorable direction. If it has, and the trail parameter is set to True,
815
+ it updates the break-even level based on the trail_after_points and stop_trail parameters.
785
816
 
786
817
  Args:
787
- id (int): The strategy Id or Expert Id
788
- mm (bool): Weither to manage the position or not
818
+ id (int): The strategy ID or expert ID.
819
+ mm (bool): Whether to manage the position or not.
820
+ trail (bool): Whether to trail the stop loss or not.
821
+ stop_trail (int): Number of points to trail the stop loss by.
822
+ It represent the distance from the current price to the stop loss.
823
+ trail_after_points (int): Number of points in profit from where the strategy will start to trail the stop loss.
824
+ be_plus_points (int): Number of points to add to the break-even level. Represents the minimum profit to secure.
789
825
  """
790
826
  time.sleep(0.1)
791
827
  if not mm:
@@ -793,6 +829,10 @@ class Trade(RiskManagement):
793
829
  Id = id if id is not None else self.expert_id
794
830
  positions = self.get_positions(symbol=self.symbol)
795
831
  be = self.get_break_even()
832
+ if trail_after_points is not None:
833
+ assert trail_after_points > be, \
834
+ "trail_after_points must be greater than break even"\
835
+ " or set to None"
796
836
  if positions is not None:
797
837
  for position in positions:
798
838
  if position.magic == Id:
@@ -805,30 +845,39 @@ class Trade(RiskManagement):
805
845
  if break_even:
806
846
  # Check if break-even has already been set for this position
807
847
  if position.ticket not in self.break_even_status:
808
- self.set_break_even(position, be)
848
+ price = None
849
+ if be_plus_points is not None:
850
+ price = position.price_open + (be_plus_points * point)
851
+ self.set_break_even(position, be, price=price)
809
852
  self.break_even_status.append(position.ticket)
853
+ self.break_even_points[position.ticket] = be
810
854
  else:
855
+ # Skip this if the trail is not set to True
856
+ if not trail:
857
+ continue
811
858
  # 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
- )
859
+ new_be = round(be * 0.10) if be_plus_points is None else be_plus_points
860
+ if trail_after_points is not None:
861
+ if position.ticket not in self.trail_after_points:
862
+ # This ensures that the position rich the minimum points required
863
+ # before the trail can be set
864
+ new_be = trail_after_points - be
865
+ self.trail_after_points[position.ticket] = True
866
+ new_be_points = self.break_even_points[position.ticket] + new_be
867
+ favorable_move = float(points/point) >= new_be_points
820
868
  if favorable_move:
869
+ # This allows the position to go to take profit in case of a swing trade
870
+ # If is a scalping position, we can set the stop_trail close to the current price.
871
+ trail_points = round(be * 0.50) if stop_trail is None else stop_trail
821
872
  # Calculate the new break-even level and price
822
873
  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)
874
+ # This level validate the favorable move of the price
875
+ new_level = round(position.price_open + (new_be_points * point), digits)
876
+ # This price is set away from the current price by the trail_points
877
+ new_price = round(position.price_current - (trail_points * point), digits)
878
+ elif position.type == 1:
879
+ new_level = round(position.price_open - (new_be_points * point), digits)
880
+ new_price = round(position.price_current + (trail_points * point), digits)
832
881
  self.set_break_even(
833
882
  position, be, price=new_price, level=new_level
834
883
  )
@@ -842,14 +891,10 @@ class Trade(RiskManagement):
842
891
  Sets the break-even level for a given trading position.
843
892
 
844
893
  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()`
894
+ position (TradePosition): The trading position for which the break-even is to be set. This is the value return by `mt5.positions_get()`.
848
895
  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.
896
+ level (float): The break-even level in price, if set to None , it will be calated automaticaly.
897
+ price (float): The break-even price, if set to None , it will be calated automaticaly.
853
898
  """
854
899
  point = self.get_symbol_info(self.symbol).point
855
900
  digits = self.get_symbol_info(self.symbol).digits
@@ -863,7 +908,9 @@ class Trade(RiskManagement):
863
908
  break_even_level = position.price_open + (be * point)
864
909
  break_even_price = position.price_open + \
865
910
  ((fees_points + spread) * point)
866
- _price = break_even_price if price is None else price
911
+ # Check if the price specified is greater or lower than the calculated price
912
+ _price = break_even_price if price is None or \
913
+ price < break_even_price else price
867
914
  _level = break_even_level if level is None else level
868
915
 
869
916
  if self.get_tick_info(self.symbol).ask > _level:
@@ -882,7 +929,8 @@ class Trade(RiskManagement):
882
929
  break_even_level = position.price_open - (be * point)
883
930
  break_even_price = position.price_open - \
884
931
  ((fees_points + spread) * point)
885
- _price = break_even_price if price is None else price
932
+ _price = break_even_price if price is None or \
933
+ price > break_even_price else price
886
934
  _level = break_even_level if level is None else level
887
935
 
888
936
  if self.get_tick_info(self.symbol).bid < _level:
@@ -987,7 +1035,7 @@ class Trade(RiskManagement):
987
1035
  for position in self.opened_positions:
988
1036
  time.sleep(0.1)
989
1037
  # This return two TradeDeal Object,
990
- # The first one is the one the opening order
1038
+ # The first one is the opening order
991
1039
  # The second is the closing order
992
1040
  history = self.get_trades_history(
993
1041
  position=position, to_df=False
@@ -1023,98 +1071,103 @@ class Trade(RiskManagement):
1023
1071
  # get all Actives positions
1024
1072
  time.sleep(0.1)
1025
1073
  Id = id if id is not None else self.expert_id
1026
- positions = self.get_positions(symbol=self.symbol)
1074
+ positions = self.get_positions(ticket=ticket)
1027
1075
  buy_price = self.get_tick_info(self.symbol).ask
1028
1076
  sell_price = self.get_tick_info(self.symbol).bid
1029
1077
  digits = self.get_symbol_info(self.symbol).digits
1030
1078
  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
1079
+ if positions is not None and len(positions) == 1:
1080
+ position = positions[0]
1081
+ if (position.ticket == ticket
1082
+ and position.magic == Id
1083
+ ):
1084
+ buy = position.type == 0
1085
+ sell = position.type == 1
1086
+ request = {
1087
+ "action": Mt5.TRADE_ACTION_DEAL,
1088
+ "symbol": self.symbol,
1089
+ "volume": (position.volume*pct),
1090
+ "type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
1091
+ "position": ticket,
1092
+ "price": sell_price if buy else buy_price,
1093
+ "deviation": deviation,
1094
+ "magic": Id,
1095
+ "comment": f"@{self.expert_name}" if comment is None else comment,
1096
+ "type_time": Mt5.ORDER_TIME_GTC,
1097
+ "type_filling": Mt5.ORDER_FILLING_FOK,
1098
+ }
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
1086
1134
 
1135
+ Positions = Literal["all", "buy", "sell", "profitable", "losing"]
1087
1136
  def close_positions(
1088
1137
  self,
1089
- position_type: Literal["all", "buy", "sell"] = "all",
1138
+ position_type: Positions,
1090
1139
  id: Optional[int] = None,
1091
1140
  comment: Optional[str] = None):
1092
1141
  """
1093
1142
  Args:
1094
- position_type (str): Type of positions to close ("all", "buy", "sell")
1143
+ position_type (str): Type of positions to close ('all', 'buy', 'sell', 'profitable', 'losing')
1095
1144
  id (int): The unique ID of the Expert or Strategy
1096
1145
  comment (str): Comment for the closing position
1097
1146
  """
1098
1147
  if position_type == "all":
1099
1148
  positions = self.get_positions(symbol=self.symbol)
1100
1149
  elif position_type == "buy":
1101
- positions = self.get_current_buys()
1150
+ positions = self.get_current_buys(id=id)
1102
1151
  elif position_type == "sell":
1103
- positions = self.get_current_sells()
1152
+ positions = self.get_current_sells(id=id)
1153
+ elif position_type == "profitable":
1154
+ positions = self.get_current_profitables(id=id)
1155
+ elif position_type == "losing":
1156
+ positions = self.get_current_losings(id=id)
1104
1157
  else:
1105
- logger.error(f"Invalid position type: {position_type}")
1158
+ self.logger.error(f"Invalid position type: {position_type}")
1106
1159
  return
1107
1160
 
1108
1161
  if positions is not None:
1109
1162
  if position_type == 'all':
1110
- tickets = [position.ticket for position in positions]
1163
+ tickets = [position.ticket for position in positions if position.magic == id]
1111
1164
  else:
1112
1165
  tickets = positions
1113
1166
  else:
1114
1167
  tickets = []
1115
1168
 
1116
1169
  if position_type == 'all':
1117
- pos_type = ''
1170
+ pos_type = 'open'
1118
1171
  else:
1119
1172
  pos_type = position_type
1120
1173
 
@@ -1224,12 +1277,11 @@ class Trade(RiskManagement):
1224
1277
  return 0.0
1225
1278
  df = df2.iloc[1:]
1226
1279
  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)
1280
+ returns = profit.pct_change(fill_method=None)
1281
+ periods = self.max_trade() * 252
1282
+ sharpe = create_sharpe_ratio(returns, periods=periods)
1231
1283
 
1232
- return round(sharp, 3)
1284
+ return round(sharpe, 3)
1233
1285
 
1234
1286
  def days_end(self) -> bool:
1235
1287
  """Check if it is the end of the trading day."""
@@ -1309,8 +1361,11 @@ def create_trade_instance(
1309
1361
  raise ValueError("The 'symbols' list cannot be empty.")
1310
1362
  for symbol in symbols:
1311
1363
  try:
1312
- instances[symbol] = Trade(**params, symbol=symbol)
1364
+ instances[symbol] = Trade(symbol=symbol, **params)
1313
1365
  except Exception as e:
1314
1366
  logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
1315
- assert len(instances) == len(symbols), "Failed to create Trade instances for all symbols."
1367
+ if len(instances) != len(symbols):
1368
+ for symbol in symbols:
1369
+ if symbol not in instances:
1370
+ logger.error(f"Failed to create Trade instance for SYMBOL={symbol}")
1316
1371
  return instances