bbstrader 0.1.6__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.

@@ -1,20 +1,18 @@
1
1
  import os
2
2
  import csv
3
3
  import time
4
- import logging
5
4
  import numpy as np
6
5
  from datetime import datetime
7
6
  import MetaTrader5 as Mt5
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
-
16
- # Configure the logger
17
- logger = config_logger('trade.log', console_log=False)
15
+ raise_mt5_error, trade_retcode_message, config_logger)
18
16
 
19
17
  class Trade(RiskManagement):
20
18
  """
@@ -27,8 +25,8 @@ class Trade(RiskManagement):
27
25
  >>> import time
28
26
  >>> # Initialize the Trade class with parameters
29
27
  >>> trade = Trade(
30
- ... symbol="#AAPL", # Symbol to trade
31
- ... expert_name="MyExpertAdvisor",# Name of the expert advisor
28
+ ... symbol="EURUSD", # Symbol to trade
29
+ ... expert_name="bbstrader", # Name of the expert advisor
32
30
  ... expert_id=12345, # Unique ID for the expert advisor
33
31
  ... version="1.0", # Version of the expert advisor
34
32
  ... target=5.0, # Daily profit target in percentage
@@ -77,10 +75,13 @@ class Trade(RiskManagement):
77
75
  expert_id: int = 9818,
78
76
  version: str = '1.0',
79
77
  target: float = 5.0,
78
+ be_on_trade_open: bool = True,
80
79
  start_time: str = "1:00",
81
80
  finishing_time: str = "23:00",
82
81
  ending_time: str = "23:30",
83
82
  verbose: Optional[bool] = None,
83
+ console_log: Optional[bool] = False,
84
+ logger: Logger | str = 'bbstrader.log',
84
85
  **kwargs,
85
86
  ):
86
87
  """
@@ -93,11 +94,14 @@ class Trade(RiskManagement):
93
94
  or the strategy used on the symbol.
94
95
  version (str): The `version` of the expert advisor.
95
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.
96
98
  start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
97
99
  finishing_time (str): The time after which no new position can be opened.
98
100
  ending_time (str): The time after which any open position will be closed.
99
101
  verbose (bool | None): If set to None (default), account summary and risk managment
100
102
  parameters are printed in the terminal.
103
+ console_log (bool): If set to True, log messages are displayed in the console.
104
+ logger (Logger | str): The logger object to use for logging messages could be a string or a logger object.
101
105
 
102
106
  Inherits:
103
107
  - max_risk
@@ -127,18 +131,15 @@ class Trade(RiskManagement):
127
131
  self.expert_id = expert_id
128
132
  self.version = version
129
133
  self.target = target
134
+ self.be_on_trade_open = be_on_trade_open
130
135
  self.verbose = verbose
131
136
  self.start = start_time
132
137
  self.end = ending_time
133
138
  self.finishing = finishing_time
139
+ self.console_log = console_log
140
+ self.logger = self._get_logger(logger, console_log)
134
141
  self.tf = kwargs.get("time_frame", 'D1')
135
142
 
136
- self.lot = self.get_lot()
137
- self.stop_loss = self.get_stop_loss()
138
- self.take_profit = self.get_take_profit()
139
- self.break_even_points = self.get_break_even()
140
- self.deviation = self.get_deviation()
141
-
142
143
  self.start_time_hour, self.start_time_minutes = self.start.split(":")
143
144
  self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
144
145
  ":")
@@ -149,6 +150,8 @@ class Trade(RiskManagement):
149
150
  self.opened_positions = []
150
151
  self.opened_orders = []
151
152
  self.break_even_status = []
153
+ self.break_even_points = {}
154
+ self.trail_after_points = {}
152
155
 
153
156
  self.initialize()
154
157
  self.select_symbol()
@@ -162,6 +165,12 @@ class Trade(RiskManagement):
162
165
  print(
163
166
  f">>> Everything is OK, @{self.expert_name} is Running ...>>>\n")
164
167
 
168
+ def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
169
+ """Get the logger object"""
170
+ if isinstance(logger, str):
171
+ return config_logger(logger, consol_log=consol_log)
172
+ return logger
173
+
165
174
  def initialize(self):
166
175
  """
167
176
  Initializes the MetaTrader 5 (MT5) terminal for trading operations.
@@ -183,7 +192,7 @@ class Trade(RiskManagement):
183
192
  f" Version @{self.version}, on {self.symbol}."
184
193
  )
185
194
  except Exception as e:
186
- logger.error(f"During initialization: {e}")
195
+ self.logger.error(f"During initialization: {e}")
187
196
 
188
197
  def select_symbol(self):
189
198
  """
@@ -200,7 +209,7 @@ class Trade(RiskManagement):
200
209
  if not Mt5.symbol_select(self.symbol, True):
201
210
  raise_mt5_error(message=INIT_MSG)
202
211
  except Exception as e:
203
- logger.error(f"Selecting symbol '{self.symbol}': {e}")
212
+ self.logger.error(f"Selecting symbol '{self.symbol}': {e}")
204
213
 
205
214
  def prepare_symbol(self):
206
215
  """
@@ -222,22 +231,26 @@ class Trade(RiskManagement):
222
231
  if self.verbose:
223
232
  print("Initialization successfully completed.")
224
233
  except Exception as e:
225
- logger.error(f"Preparing symbol '{self.symbol}': {e}")
234
+ self.logger.error(f"Preparing symbol '{self.symbol}': {e}")
226
235
 
227
236
  def summary(self):
228
237
  """Show a brief description about the trading program"""
229
- print(
230
- "╔═════════════════ Summary ════════════════════╗\n"
231
- f"Expert Advisor Name @{self.expert_name}\n"
232
- f"Expert Advisor Version @{self.version}\n"
233
- f" Expert | Strategy ID {self.expert_id}\n"
234
- f"Trading Symbol {self.symbol}\n"
235
- f" Trading Time Frame {self.tf}\n"
236
- f" Start Trading Time {self.start_time_hour}:{self.start_time_minutes}\n"
237
- f" Finishing Trading Time {self.finishing_time_hour}:{self.finishing_time_minutes}\n"
238
- f"║ Closing Position After {self.ending_time_hour}:{self.ending_time_minutes}\n"
239
- "╚═══════════════════════════════════════════════╝\n"
240
- )
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)
241
254
 
242
255
  def risk_managment(self):
243
256
  """Show the risk management parameters"""
@@ -250,36 +263,40 @@ class Trade(RiskManagement):
250
263
  currency = account_info.currency
251
264
  rates = self.get_currency_rates(self.symbol)
252
265
  marging_currency = rates['mc']
253
- print(
254
- "╔═════════════════ Risk Management ═════════════════════╗\n"
255
- f"Account Name {account_info.name}\n"
256
- f"Account Number {account_info.login}\n"
257
- f"Account Server {account_info.server}\n"
258
- f"Account Balance {account_info.balance} {currency}\n"
259
- f"Account Profit {_profit} {currency}\n"
260
- f"Account Equity {account_info.equity} {currency}\n"
261
- f"Account Leverage {self.get_leverage(True)}\n"
262
- f"Account Margin {round(account_info.margin, 2)} {currency}\n"
263
- f" Account Free Margin {account_info.margin_free} {currency}\n"
264
- f" Maximum Drawdown {self.max_risk}%\n"
265
- f"║ Risk Allowed {round((self.max_risk - self.risk_level()), 2)}%\n"
266
- f"║ Volume {self.volume()} {marging_currency}\n"
267
- f" Risk Per trade {-self.get_currency_risk()} {currency}\n"
268
- f" Profit Expected Per trade {self.expected_profit()} {currency}\n"
269
- f" Lot Size {self.lot} Lots\n"
270
- f" Stop Loss {self.stop_loss} Points\n"
271
- f" Loss Value Per Tick {round(loss, 5)} {currency}\n"
272
- f" Take Profit {self.take_profit} Points\n"
273
- f" Profit Value Per Tick {round(profit, 5)} {currency}\n"
274
- f"║ Break Even {self.break_even_points} Points\n"
275
- f"║ Deviation {self.deviation} Points\n"
276
- f" Trading Time Interval {self.get_minutes()} Minutes\n"
277
- f" Risk Level {ok}\n"
278
- f"║ Maximum Trades {self.max_trade()}\n"
279
- "╚══════════════════════════════════════════════════════╝\n"
280
- )
281
-
282
- 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):
283
300
  """
284
301
  Print some statistics for the trading session and save to CSV if specified.
285
302
 
@@ -303,31 +320,31 @@ class Trade(RiskManagement):
303
320
  expected_profit = round((trade_risk * self.rr * -1), 2)
304
321
 
305
322
  # Formatting the statistics output
306
- stats_output = (
307
- f"╔═══════════════ Session Statistics ═════════════╗\n"
308
- f" Total Trades {deals}\n"
309
- f" Winning Trades {wins}\n"
310
- f" Losing Trades {losses}\n"
311
- f" Session Profit {profit} {currency}\n"
312
- f" Total Fees {total_fees} {currency}\n"
313
- f" Average Fees {average_fee} {currency}\n"
314
- f" Net Profit {net_profit} {currency}\n"
315
- f" Risk per Trade {trade_risk} {currency}\n"
316
- f" Expected Profit per Trade {self.expected_profit()} {currency}\n"
317
- f" Risk Reward Ratio {self.rr}\n"
318
- f" Win Rate {win_rate}%\n"
319
- f" Sharpe Ratio {self.sharpe()}\n"
320
- f"║ Trade Profitability {profitability}\n"
321
- "╚═════════════════════════════════════════════════╝\n"
322
- )
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")
323
339
 
324
340
  # Print the formatted statistics
325
341
  if self.verbose:
326
- print(stats_output)
342
+ print("\n[======= Trading Session Statistics =======]")
343
+ print(session_table)
327
344
 
328
345
  # Save to CSV if specified
329
346
  if save:
330
- today_date = datetime.now().strftime("%Y-%m-%d")
347
+ today_date = datetime.now().strftime('%Y%m%d%H%M%S')
331
348
  # Create a dictionary with the statistics
332
349
  statistics_dict = {
333
350
  "Total Trades": deals,
@@ -345,13 +362,15 @@ class Trade(RiskManagement):
345
362
  "Trade Profitability": profitability,
346
363
  }
347
364
  # Create the directory if it doesn't exist
365
+ if dir is None:
366
+ dir = f"{self.expert_name}_session_stats"
348
367
  os.makedirs(dir, exist_ok=True)
349
368
  if '.' in self.symbol:
350
369
  symbol = self.symbol.split('.')[0]
351
370
  else:
352
371
  symbol = self.symbol
353
372
 
354
- filename = f"{symbol}_{today_date}_session.csv"
373
+ filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
355
374
  filepath = os.path.join(dir, filename)
356
375
 
357
376
  # Updated code to write to CSV
@@ -362,11 +381,12 @@ class Trade(RiskManagement):
362
381
  writer.writerow(["Statistic", "Value"])
363
382
  for stat, value in statistics_dict.items():
364
383
  writer.writerow([stat, value])
365
- logger.info(f"Session statistics saved to {filepath}")
384
+ self.logger.info(f"Session statistics saved to {filepath}")
366
385
 
386
+ Buys = Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
367
387
  def open_buy_position(
368
388
  self,
369
- action: Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT'] = 'BMKT',
389
+ action: Buys = 'BMKT',
370
390
  price: Optional[float] = None,
371
391
  mm: bool = True,
372
392
  id: Optional[int] = None,
@@ -415,8 +435,8 @@ class Trade(RiskManagement):
415
435
  if action != 'BMKT':
416
436
  request["action"] = Mt5.TRADE_ACTION_PENDING
417
437
  request["type"] = self._order_type()[action][0]
418
-
419
- self.break_even(mm=mm)
438
+ if self.be_on_trade_open:
439
+ self.break_even(mm=mm, id=Id)
420
440
  if self.check(comment):
421
441
  self.request_result(_price, request, action),
422
442
 
@@ -433,9 +453,10 @@ class Trade(RiskManagement):
433
453
  }
434
454
  return type
435
455
 
456
+ Sells = Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
436
457
  def open_sell_position(
437
458
  self,
438
- action: Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT'] = 'SMKT',
459
+ action: Sells = 'SMKT',
439
460
  price: Optional[float] = None,
440
461
  mm: bool = True,
441
462
  id: Optional[int] = None,
@@ -484,8 +505,8 @@ class Trade(RiskManagement):
484
505
  if action != 'SMKT':
485
506
  request["action"] = Mt5.TRADE_ACTION_PENDING
486
507
  request["type"] = self._order_type()[action][0]
487
-
488
- self.break_even(comment)
508
+ if self.be_on_trade_open:
509
+ self.break_even(mm=mm, id=Id)
489
510
  if self.check(comment):
490
511
  self.request_result(_price, request, action)
491
512
 
@@ -508,14 +529,14 @@ class Trade(RiskManagement):
508
529
  if self.days_end():
509
530
  return False
510
531
  elif not self.trading_time():
511
- logger.info(f"Not Trading time, SYMBOL={self.symbol}")
532
+ self.logger.info(f"Not Trading time, SYMBOL={self.symbol}")
512
533
  return False
513
534
  elif not self.is_risk_ok():
514
- logger.error(f"Risk not allowed, SYMBOL={self.symbol}")
535
+ self.logger.error(f"Risk not allowed, SYMBOL={self.symbol}")
515
536
  self._check(comment)
516
537
  return False
517
538
  elif not self._risk_free():
518
- logger.error(f"Maximum trades Reached, SYMBOL={self.symbol}")
539
+ self.logger.error(f"Maximum trades Reached, SYMBOL={self.symbol}")
519
540
  self._check(comment)
520
541
  return False
521
542
  elif self.profit_target():
@@ -525,7 +546,7 @@ class Trade(RiskManagement):
525
546
  def _check(self, txt: str = ""):
526
547
  if self.positive_profit() or self.get_current_open_positions() is None:
527
548
  self.close_positions(position_type='all')
528
- logger.info(txt)
549
+ self.logger.info(txt)
529
550
  time.sleep(5)
530
551
  self.statistics(save=True)
531
552
 
@@ -533,8 +554,7 @@ class Trade(RiskManagement):
533
554
  self,
534
555
  price: float,
535
556
  request: Dict[str, Any],
536
- type: Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
537
- 'SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
557
+ type: Buys | Sells
538
558
  ):
539
559
  """
540
560
  Check if a trading order has been sent correctly
@@ -544,8 +564,7 @@ class Trade(RiskManagement):
544
564
  request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
545
565
  all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py
546
566
 
547
- type (str): The type of the order
548
- `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
567
+ type (str): The type of the order `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
549
568
  """
550
569
  # Send a trading request
551
570
  # Check the execution result
@@ -560,7 +579,7 @@ class Trade(RiskManagement):
560
579
  result.retcode, display=True, add_msg=f"{e}{addtionnal}")
561
580
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
562
581
  msg = trade_retcode_message(result.retcode)
563
- logger.error(
582
+ self.logger.error(
564
583
  f"Trade Order Request, RETCODE={result.retcode}: {msg}{addtionnal}")
565
584
  if result.retcode in [
566
585
  Mt5.TRADE_RETCODE_CONNECTION, Mt5.TRADE_RETCODE_TIMEOUT]:
@@ -580,7 +599,7 @@ class Trade(RiskManagement):
580
599
  # Print the result
581
600
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
582
601
  msg = trade_retcode_message(result.retcode)
583
- logger.info(f"Trade Order {msg}{addtionnal}")
602
+ self.logger.info(f"Trade Order {msg}{addtionnal}")
584
603
  if type != "BMKT" or type != "SMKT":
585
604
  self.opened_orders.append(result.order)
586
605
  long_msg = (
@@ -588,7 +607,7 @@ class Trade(RiskManagement):
588
607
  f"Lot(s): {result.volume}, Sl: {self.get_stop_loss()}, "
589
608
  f"Tp: {self.get_take_profit()}"
590
609
  )
591
- logger.info(long_msg)
610
+ self.logger.info(long_msg)
592
611
  time.sleep(0.1)
593
612
  if type == "BMKT" or type == "SMKT":
594
613
  self.opened_positions.append(result.order)
@@ -606,20 +625,16 @@ class Trade(RiskManagement):
606
625
  f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open,5)}, "
607
626
  f"Sl: @{position.sl} Tp: @{position.tp}"
608
627
  )
609
- logger.info(order_info)
628
+ self.logger.info(order_info)
610
629
  pos_info = (
611
630
  f"3. [OPEN POSITIONS ON {self.symbol} = {len(positions)}, ACCOUNT OPEN PnL = {profit} "
612
631
  f"{self.get_account_info().currency}]\n"
613
632
  )
614
- logger.info(pos_info)
615
-
633
+ self.logger.info(pos_info)
634
+
616
635
  def open_position(
617
636
  self,
618
- action: Literal[
619
- 'BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
620
- 'SMKT', 'SLMT', 'SSTP', 'SSTPLMT'],
621
- buy: bool = False,
622
- sell: bool = False,
637
+ action: Buys | Sells,
623
638
  price: Optional[float] = None,
624
639
  id: Optional[int] = None,
625
640
  mm: bool = True,
@@ -631,18 +646,20 @@ class Trade(RiskManagement):
631
646
  Args:
632
647
  action (str): (`'BMKT'`, `'SMKT'`) for Market orders
633
648
  or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
634
- buy (bool): A boolean True or False
635
- sell (bool): A boolean True or False
636
649
  id (int): The strategy id or expert Id
637
650
  mm (bool): Weither to put stop loss and tp or not
638
651
  comment (str): The comment for the closing position
639
652
  """
640
- if buy:
653
+ BUYS = ['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
654
+ SELLS = ['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
655
+ if action in BUYS:
641
656
  self.open_buy_position(
642
657
  action=action, price=price, id=id, mm=mm, comment=comment)
643
- if sell:
658
+ elif action in SELLS:
644
659
  self.open_sell_position(
645
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)}")
646
663
 
647
664
  @property
648
665
  def get_opened_orders(self):
@@ -690,13 +707,14 @@ class Trade(RiskManagement):
690
707
 
691
708
  Args:
692
709
  id (int): The strategy id or expert Id
693
- filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'win_trades')
710
+ filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'profitables')
694
711
  - `orders` are current open orders
695
712
  - `positions` are all current open positions
696
713
  - `buys` and `sells` are current buy or sell open positions
697
- - `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
698
716
  th (bool): the minimum treshold for winning position
699
- (only relevant when filter_type is 'win_trades')
717
+ (only relevant when filter_type is 'profitables')
700
718
 
701
719
  Returns:
702
720
  List[int] | None: A list of filtered tickets
@@ -718,7 +736,9 @@ class Trade(RiskManagement):
718
736
  continue
719
737
  if filter_type == 'sells' and item.type != 1:
720
738
  continue
721
- 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:
722
742
  continue
723
743
  filtered_tickets.append(item.ticket)
724
744
  return filtered_tickets if filtered_tickets else None
@@ -730,8 +750,11 @@ class Trade(RiskManagement):
730
750
  def get_current_open_positions(self, id: Optional[int] = None) -> List[int] | None:
731
751
  return self.get_filtered_tickets(id=id, filter_type='positions')
732
752
 
733
- def get_current_win_trades(self, id: Optional[int] = None, th=None) -> List[int] | None:
734
- 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')
735
758
 
736
759
  def get_current_buys(self, id: Optional[int] = None) -> List[int] | None:
737
760
  return self.get_filtered_tickets(id=id, filter_type='buys')
@@ -739,8 +762,9 @@ class Trade(RiskManagement):
739
762
  def get_current_sells(self, id: Optional[int] = None) -> List[int] | None:
740
763
  return self.get_filtered_tickets(id=id, filter_type='sells')
741
764
 
742
- def positive_profit(self, th: Optional[float] = None
743
- ) -> bool:
765
+ def positive_profit(self, th: Optional[float] = None,
766
+ id: Optional[int] = None,
767
+ account: bool = True) -> bool:
744
768
  """
745
769
  Check is the total profit on current open positions
746
770
  Is greater than a minimum profit express as percentage
@@ -748,35 +772,56 @@ class Trade(RiskManagement):
748
772
 
749
773
  Args:
750
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
751
777
  """
752
- 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]
753
792
  profit = 0.0
754
793
  balance = self.get_account_info().balance
755
794
  target = round((balance * self.target)/100, 2)
756
- if positions is not None:
795
+ if positions is not None or len(positions) != 0:
757
796
  for position in positions:
758
- time.sleep(0.1)
759
- history = self.get_positions(
760
- ticket=position
761
- )
762
- profit += history[0].profit
797
+ profit += position.profit
763
798
  fees = self.get_stats()[0]["average_fee"] * len(positions)
764
799
  current_profit = profit + fees
765
800
  th_profit = (target*th)/100 if th is not None else (target*0.01)
766
- if current_profit > th_profit:
767
- return True
801
+ return current_profit >= th_profit
768
802
  return False
769
803
 
770
- 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
+ ):
771
811
  """
772
- Checks if it's time to put the break even,
773
- if so , it will sets the break even ,and if the break even was already set,
774
- it checks if the price has moved in favorable direction,
775
- 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.
776
816
 
777
817
  Args:
778
- id (int): The strategy Id or Expert Id
779
- 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.
780
825
  """
781
826
  time.sleep(0.1)
782
827
  if not mm:
@@ -784,6 +829,10 @@ class Trade(RiskManagement):
784
829
  Id = id if id is not None else self.expert_id
785
830
  positions = self.get_positions(symbol=self.symbol)
786
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"
787
836
  if positions is not None:
788
837
  for position in positions:
789
838
  if position.magic == Id:
@@ -796,30 +845,39 @@ class Trade(RiskManagement):
796
845
  if break_even:
797
846
  # Check if break-even has already been set for this position
798
847
  if position.ticket not in self.break_even_status:
799
- 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)
800
852
  self.break_even_status.append(position.ticket)
853
+ self.break_even_points[position.ticket] = be
801
854
  else:
855
+ # Skip this if the trail is not set to True
856
+ if not trail:
857
+ continue
802
858
  # Check if the price has moved favorably
803
- new_be = be * 0.50
804
- favorable_move = (
805
- (position.type == 0 and (
806
- (position.price_current - position.sl) / point) > new_be)
807
- or
808
- (position.type == 1 and (
809
- (position.sl - position.price_current) / point) > new_be)
810
- )
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
811
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
812
872
  # Calculate the new break-even level and price
813
873
  if position.type == 0:
814
- new_level = round(
815
- position.sl + (new_be * point), digits)
816
- new_price = round(
817
- position.sl + ((0.25 * be) * point), digits)
818
- else:
819
- new_level = round(
820
- position.sl - (new_be * point), digits)
821
- new_price = round(
822
- 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)
823
881
  self.set_break_even(
824
882
  position, be, price=new_price, level=new_level
825
883
  )
@@ -833,14 +891,10 @@ class Trade(RiskManagement):
833
891
  Sets the break-even level for a given trading position.
834
892
 
835
893
  Args:
836
- position (TradePosition):
837
- The trading position for which the break-even is to be set
838
- 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()`.
839
895
  be (int): The break-even level in points.
840
- level (float): The break-even level in price
841
- if set to None , it will be calated automaticaly.
842
- price (float): The break-even price
843
- 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.
844
898
  """
845
899
  point = self.get_symbol_info(self.symbol).point
846
900
  digits = self.get_symbol_info(self.symbol).digits
@@ -854,7 +908,9 @@ class Trade(RiskManagement):
854
908
  break_even_level = position.price_open + (be * point)
855
909
  break_even_price = position.price_open + \
856
910
  ((fees_points + spread) * point)
857
- _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
858
914
  _level = break_even_level if level is None else level
859
915
 
860
916
  if self.get_tick_info(self.symbol).ask > _level:
@@ -873,7 +929,8 @@ class Trade(RiskManagement):
873
929
  break_even_level = position.price_open - (be * point)
874
930
  break_even_price = position.price_open - \
875
931
  ((fees_points + spread) * point)
876
- _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
877
934
  _level = break_even_level if level is None else level
878
935
 
879
936
  if self.get_tick_info(self.symbol).bid < _level:
@@ -908,7 +965,7 @@ class Trade(RiskManagement):
908
965
  result.retcode, display=True, add_msg=f"{e}{addtionnal}")
909
966
  if result.retcode != Mt5.TRADE_RETCODE_DONE:
910
967
  msg = trade_retcode_message(result.retcode)
911
- logger.error(
968
+ self.logger.error(
912
969
  f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}")
913
970
  tries = 0
914
971
  while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
@@ -928,11 +985,11 @@ class Trade(RiskManagement):
928
985
  tries += 1
929
986
  if result.retcode == Mt5.TRADE_RETCODE_DONE:
930
987
  msg = trade_retcode_message(result.retcode)
931
- logger.info(f"Break-Even Order {msg}{addtionnal}")
988
+ self.logger.info(f"Break-Even Order {msg}{addtionnal}")
932
989
  info = (
933
990
  f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{price}"
934
991
  )
935
- logger.info(info)
992
+ self.logger.info(info)
936
993
  self.break_even_status.append(tiket)
937
994
 
938
995
  def win_trade(self,
@@ -978,7 +1035,7 @@ class Trade(RiskManagement):
978
1035
  for position in self.opened_positions:
979
1036
  time.sleep(0.1)
980
1037
  # This return two TradeDeal Object,
981
- # The first one is the one the opening order
1038
+ # The first one is the opening order
982
1039
  # The second is the closing order
983
1040
  history = self.get_trades_history(
984
1041
  position=position, to_df=False
@@ -1014,97 +1071,105 @@ class Trade(RiskManagement):
1014
1071
  # get all Actives positions
1015
1072
  time.sleep(0.1)
1016
1073
  Id = id if id is not None else self.expert_id
1017
- positions = self.get_positions(symbol=self.symbol)
1074
+ positions = self.get_positions(ticket=ticket)
1018
1075
  buy_price = self.get_tick_info(self.symbol).ask
1019
1076
  sell_price = self.get_tick_info(self.symbol).bid
1020
1077
  digits = self.get_symbol_info(self.symbol).digits
1021
1078
  deviation = self.get_deviation()
1022
- if positions is not None:
1023
- for position in positions:
1024
- if (position.ticket == ticket
1025
- and position.magic == Id
1026
- ):
1027
- buy = position.type == 0
1028
- sell = position.type == 1
1029
- request = {
1030
- "action": Mt5.TRADE_ACTION_DEAL,
1031
- "symbol": self.symbol,
1032
- "volume": (position.volume*pct),
1033
- "type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
1034
- "position": ticket,
1035
- "price": sell_price if buy else buy_price,
1036
- "deviation": deviation,
1037
- "magic": Id,
1038
- "comment": f"@{self.expert_name}" if comment is None else comment,
1039
- "type_time": Mt5.ORDER_TIME_GTC,
1040
- "type_filling": Mt5.ORDER_FILLING_FOK,
1041
- }
1042
- addtionnal = f", SYMBOL={self.symbol}"
1043
- try:
1044
- check_result = self.check_order(request)
1045
- result = self.send_order(request)
1046
- except Exception as e:
1047
- print(f"{self.current_datetime()} -", end=' ')
1048
- trade_retcode_message(
1049
- result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1050
- if result.retcode != Mt5.TRADE_RETCODE_DONE:
1051
- msg = trade_retcode_message(result.retcode)
1052
- logger.error(
1053
- f"Closing Order Request, Position: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}")
1054
- tries = 0
1055
- while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
1056
- time.sleep(1)
1057
- try:
1058
- check_result = self.check_order(request)
1059
- result = self.send_order(request)
1060
- except Exception as e:
1061
- print(f"{self.current_datetime()} -", end=' ')
1062
- trade_retcode_message(
1063
- result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1064
- if result.retcode == Mt5.TRADE_RETCODE_DONE:
1065
- break
1066
- tries += 1
1067
- if result.retcode == Mt5.TRADE_RETCODE_DONE:
1068
- msg = trade_retcode_message(result.retcode)
1069
- logger.info(
1070
- f"Closing Order {msg}{addtionnal}")
1071
- info = (
1072
- f"Position #{ticket} closed, Symbol: {self.symbol}, Price: @{request['price']}")
1073
- logger.info(info)
1074
- return True
1075
- else:
1076
- 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
1077
1134
 
1135
+ Positions = Literal["all", "buy", "sell", "profitable", "losing"]
1078
1136
  def close_positions(
1079
1137
  self,
1080
- position_type: Literal["all", "buy", "sell"] = "all",
1138
+ position_type: Positions,
1081
1139
  id: Optional[int] = None,
1082
1140
  comment: Optional[str] = None):
1083
1141
  """
1084
1142
  Args:
1085
- 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')
1086
1144
  id (int): The unique ID of the Expert or Strategy
1087
1145
  comment (str): Comment for the closing position
1088
1146
  """
1089
1147
  if position_type == "all":
1090
1148
  positions = self.get_positions(symbol=self.symbol)
1091
1149
  elif position_type == "buy":
1092
- positions = self.get_current_buys()
1150
+ positions = self.get_current_buys(id=id)
1093
1151
  elif position_type == "sell":
1094
- 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)
1095
1157
  else:
1096
- logger.error(f"Invalid position type: {position_type}")
1158
+ self.logger.error(f"Invalid position type: {position_type}")
1097
1159
  return
1098
1160
 
1099
1161
  if positions is not None:
1100
1162
  if position_type == 'all':
1101
- pos_type = ""
1102
- tickets = [position.ticket for position in positions]
1163
+ tickets = [position.ticket for position in positions if position.magic == id]
1103
1164
  else:
1104
1165
  tickets = positions
1105
- pos_type = position_type
1106
1166
  else:
1107
1167
  tickets = []
1168
+
1169
+ if position_type == 'all':
1170
+ pos_type = 'open'
1171
+ else:
1172
+ pos_type = position_type
1108
1173
 
1109
1174
  if len(tickets) != 0:
1110
1175
  for ticket in tickets.copy():
@@ -1113,13 +1178,13 @@ class Trade(RiskManagement):
1113
1178
  time.sleep(1)
1114
1179
 
1115
1180
  if len(tickets) == 0:
1116
- logger.info(
1181
+ self.logger.info(
1117
1182
  f"ALL {pos_type.upper()} Positions closed, SYMBOL={self.symbol}.")
1118
1183
  else:
1119
- logger.info(
1184
+ self.logger.info(
1120
1185
  f"{len(tickets)} {pos_type.upper()} Positions not closed, SYMBOL={self.symbol}")
1121
1186
  else:
1122
- logger.info(
1187
+ self.logger.info(
1123
1188
  f"No {pos_type.upper()} Positions to close, SYMBOL={self.symbol}.")
1124
1189
 
1125
1190
  def get_stats(self) -> Tuple[Dict[str, Any]]:
@@ -1212,12 +1277,11 @@ class Trade(RiskManagement):
1212
1277
  return 0.0
1213
1278
  df = df2.iloc[1:]
1214
1279
  profit = df[["profit", "commission", "fee", "swap"]].sum(axis=1)
1215
- returns = profit.values
1216
- returns = np.diff(returns, prepend=0.0)
1217
- N = self.max_trade() * 252
1218
- 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)
1219
1283
 
1220
- return round(sharp, 3)
1284
+ return round(sharpe, 3)
1221
1285
 
1222
1286
  def days_end(self) -> bool:
1223
1287
  """Check if it is the end of the trading day."""
@@ -1277,7 +1341,8 @@ class Trade(RiskManagement):
1277
1341
 
1278
1342
  def create_trade_instance(
1279
1343
  symbols: List[str],
1280
- params: Dict[str, Any]) -> Dict[str, Trade]:
1344
+ params: Dict[str, Any],
1345
+ logger: Logger = ...) -> Dict[str, Trade]:
1281
1346
  """
1282
1347
  Creates Trade instances for each symbol provided.
1283
1348
 
@@ -1292,12 +1357,15 @@ def create_trade_instance(
1292
1357
  ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
1293
1358
  """
1294
1359
  instances = {}
1295
-
1296
1360
  if not symbols:
1297
1361
  raise ValueError("The 'symbols' list cannot be empty.")
1298
1362
  for symbol in symbols:
1299
1363
  try:
1300
- instances[symbol] = Trade(**params, symbol=symbol)
1364
+ instances[symbol] = Trade(symbol=symbol, **params)
1301
1365
  except Exception as e:
1302
1366
  logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
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}")
1303
1371
  return instances