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

@@ -0,0 +1,1690 @@
1
+ import os
2
+ import time
3
+ from datetime import datetime
4
+ from logging import Logger
5
+ from typing import Any, Callable, Dict, List, Literal, Optional, Tuple
6
+
7
+ import MetaTrader5 as Mt5
8
+ import pandas as pd
9
+ from tabulate import tabulate
10
+
11
+ from bbstrader.btengine.performance import create_sharpe_ratio
12
+ from bbstrader.config import config_logger
13
+ from bbstrader.metatrader.account import INIT_MSG, check_mt5_connection
14
+ from bbstrader.metatrader.risk import RiskManagement
15
+ from bbstrader.metatrader.utils import (
16
+ TradePosition,
17
+ raise_mt5_error,
18
+ trade_retcode_message,
19
+ )
20
+
21
+ __all__ = [
22
+ "Trade",
23
+ "create_trade_instance",
24
+ ]
25
+
26
+ FILLING_TYPE = [
27
+ Mt5.ORDER_FILLING_IOC,
28
+ Mt5.ORDER_FILLING_RETURN,
29
+ Mt5.ORDER_FILLING_BOC,
30
+ ]
31
+
32
+
33
+ class Trade(RiskManagement):
34
+ """
35
+ Extends the `RiskManagement` class to include specific trading operations,
36
+ incorporating risk management strategies directly into trade executions.
37
+ It offers functionalities to execute trades while managing risks
38
+ according to the inherited RiskManagement parameters and methods.
39
+
40
+ Exemple:
41
+ >>> import time
42
+ >>> # Initialize the Trade class with parameters
43
+ >>> trade = Trade(
44
+ ... symbol="EURUSD", # Symbol to trade
45
+ ... expert_name="bbstrader", # Name of the expert advisor
46
+ ... expert_id=12345, # Unique ID for the expert advisor
47
+ ... version="1.0", # Version of the expert advisor
48
+ ... target=5.0, # Daily profit target in percentage
49
+ ... start_time="09:00", # Start time for trading
50
+ ... finishing_time="17:00", # Time to stop opening new positions
51
+ ... ending_time="17:30", # Time to close any open positions
52
+ ... max_risk=2.0, # Maximum risk allowed on the account in percentage
53
+ ... daily_risk=1.0, # Daily risk allowed in percentage
54
+ ... max_trades=5, # Maximum number of trades per session
55
+ ... rr=2.0, # Risk-reward ratio
56
+ ... account_leverage=True, # Use account leverage in calculations
57
+ ... std_stop=True, # Use standard deviation for stop loss calculation
58
+ ... sl=20, # Stop loss in points (optional)
59
+ ... tp=30, # Take profit in points (optional)
60
+ ... be=10 # Break-even in points (optional)
61
+ ... )
62
+
63
+ >>> # Example to open a buy position
64
+ >>> trade.open_buy_position(mm=True, comment="Opening Buy Position")
65
+
66
+ >>> # Example to open a sell position
67
+ >>> trade.open_sell_position(mm=True, comment="Opening Sell Position")
68
+
69
+ >>> # Check current open positions
70
+ >>> opened_positions = trade.get_opened_positions
71
+ >>> if opened_positions is not None:
72
+ ... print(f"Current open positions: {opened_positions}")
73
+
74
+ >>> # Close all open positions at the end of the trading session
75
+ >>> if trade.days_end():
76
+ ... trade.close_all_positions(comment="Closing all positions at day's end")
77
+
78
+ >>> # Print trading session statistics
79
+ >>> trade.statistics(save=True, dir="my_trading_stats")
80
+
81
+ >>> # Sleep until the next trading session if needed (example usage)
82
+ >>> sleep_time = trade.sleep_time()
83
+ >>> print(f"Sleeping for {sleep_time} minutes until the next trading session.")
84
+ >>> time.sleep(sleep_time * 60)
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ symbol: str = "EURUSD",
90
+ expert_name: str = "bbstrader",
91
+ expert_id: int = 9818,
92
+ version: str = "1.0",
93
+ target: float = 5.0,
94
+ start_time: str = "1:00",
95
+ finishing_time: str = "23:00",
96
+ ending_time: str = "23:30",
97
+ verbose: Optional[bool] = None,
98
+ console_log: Optional[bool] = False,
99
+ logger: Logger | str = "bbstrader.log",
100
+ **kwargs,
101
+ ):
102
+ """
103
+ Initializes the Trade class with the specified parameters.
104
+
105
+ Args:
106
+ symbol (str): The `symbol` that the expert advisor will trade.
107
+ expert_name (str): The name of the `expert advisor`.
108
+ expert_id (int): The `unique ID` used to identify the expert advisor
109
+ or the strategy used on the symbol.
110
+ version (str): The `version` of the expert advisor.
111
+ target (float): `Trading period (day, week, month) profit target` in percentage.
112
+ start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
113
+ finishing_time (str): The time after which no new position can be opened.
114
+ ending_time (str): The time after which any open position will be closed.
115
+ verbose (bool | None): If set to None (default), account summary and risk managment
116
+ parameters are printed in the terminal.
117
+ console_log (bool): If set to True, log messages are displayed in the console.
118
+ logger (Logger | str): The logger object to use for logging messages could be a string or a logger object.
119
+
120
+ Inherits:
121
+ - max_risk
122
+ - max_trades
123
+ - rr
124
+ - daily_risk
125
+ - time_frame
126
+ - account_leverage
127
+ - std_stop
128
+ - pchange_sl
129
+ - sl
130
+ - tp
131
+ - be
132
+ See the RiskManagement class for more details on these parameters.
133
+ See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
134
+ """
135
+ # Call the parent class constructor first
136
+ super().__init__(
137
+ symbol=symbol,
138
+ start_time=start_time,
139
+ finishing_time=finishing_time,
140
+ **kwargs, # Pass kwargs to the parent constructor
141
+ )
142
+
143
+ # Initialize Trade-specific attributes
144
+ self.symbol = symbol
145
+ self.expert_name = expert_name
146
+ self.expert_id = expert_id
147
+ self.version = version
148
+ self.target = target
149
+ self.verbose = verbose
150
+ self.start = start_time
151
+ self.end = ending_time
152
+ self.finishing = finishing_time
153
+ self.console_log = console_log
154
+ self.logger = self._get_logger(logger, console_log)
155
+ self.tf = kwargs.get("time_frame", "D1")
156
+ self.kwargs = kwargs
157
+
158
+ self.start_time_hour, self.start_time_minutes = self.start.split(":")
159
+ self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
160
+ ":"
161
+ )
162
+ self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
163
+
164
+ self.buy_positions = []
165
+ self.sell_positions = []
166
+ self.opened_positions = []
167
+ self.opened_orders = []
168
+ self.break_even_status = []
169
+ self.break_even_points = {}
170
+ self.trail_after_points = []
171
+ self._retcodes = []
172
+
173
+ self.initialize(**kwargs)
174
+ self.select_symbol(**kwargs)
175
+ self.prepare_symbol()
176
+
177
+ if self.verbose:
178
+ self.summary()
179
+ time.sleep(1)
180
+ print()
181
+ self.risk_managment()
182
+ print(f">>> Everything is OK, @{self.expert_name} is Running ...>>>\n")
183
+
184
+ @property
185
+ def retcodes(self) -> List[int]:
186
+ """Return all the retcodes"""
187
+ return self._retcodes
188
+
189
+ def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
190
+ """Get the logger object"""
191
+ if isinstance(logger, str):
192
+ return config_logger(logger, consol_log)
193
+ return logger
194
+
195
+ def initialize(self, **kwargs):
196
+ """
197
+ Initializes the MetaTrader 5 (MT5) terminal for trading operations.
198
+ This method attempts to establish a connection with the MT5 terminal.
199
+ If the initial connection attempt fails due to a timeout, it retries after a specified delay.
200
+ Successful initialization is crucial for the execution of trading operations.
201
+
202
+ Raises:
203
+ MT5TerminalError: If initialization fails.
204
+ """
205
+ try:
206
+ if self.verbose:
207
+ print("\nInitializing the basics.")
208
+ check_mt5_connection(**kwargs)
209
+ if self.verbose:
210
+ print(
211
+ f"You are running the @{self.expert_name} Expert advisor,"
212
+ f" Version @{self.version}, on {self.symbol}."
213
+ )
214
+ except Exception as e:
215
+ self.logger.error(f"During initialization: {e}")
216
+
217
+ def select_symbol(self, **kwargs):
218
+ """
219
+ Selects the trading symbol in the MetaTrader 5 (MT5) terminal.
220
+ This method ensures that the specified trading
221
+ symbol is selected and visible in the MT5 terminal,
222
+ allowing subsequent trading operations such as opening and
223
+ closing positions on this symbol.
224
+
225
+ Raises:
226
+ MT5TerminalError: If symbole selection fails.
227
+ """
228
+ try:
229
+ check_mt5_connection(**kwargs)
230
+ if not Mt5.symbol_select(self.symbol, True):
231
+ raise_mt5_error(message=INIT_MSG)
232
+ except Exception as e:
233
+ self.logger.error(f"Selecting symbol '{self.symbol}': {e}")
234
+
235
+ def prepare_symbol(self):
236
+ """
237
+ Prepares the selected symbol for trading.
238
+ This method checks if the symbol is available and visible in the
239
+ MT5 terminal. If the symbol is not visible, it attempts to select the symbol again.
240
+ This step ensures that trading operations can be performed on the selected symbol without issues.
241
+
242
+ Raises:
243
+ MT5TerminalError: If the symbol cannot be made visible for trading operations.
244
+ """
245
+ try:
246
+ symbol_info = self.get_symbol_info(self.symbol)
247
+ if symbol_info is None:
248
+ raise_mt5_error(message=INIT_MSG)
249
+
250
+ if not symbol_info.visible:
251
+ raise_mt5_error(message=INIT_MSG)
252
+ if self.verbose:
253
+ print("Initialization successfully completed.")
254
+ except Exception as e:
255
+ self.logger.error(f"Preparing symbol '{self.symbol}': {e}")
256
+
257
+ def summary(self):
258
+ """Show a brief description about the trading program"""
259
+ summary_data = [
260
+ ["Expert Advisor Name", f"@{self.expert_name}"],
261
+ ["Expert Advisor Version", f"@{self.version}"],
262
+ ["Expert | Strategy ID", self.expert_id],
263
+ ["Trading Symbol", self.symbol],
264
+ ["Trading Time Frame", self.tf],
265
+ ["Start Trading Time", f"{self.start_time_hour}:{self.start_time_minutes}"],
266
+ [
267
+ "Finishing Trading Time",
268
+ f"{self.finishing_time_hour}:{self.finishing_time_minutes}",
269
+ ],
270
+ [
271
+ "Closing Position After",
272
+ f"{self.ending_time_hour}:{self.ending_time_minutes}",
273
+ ],
274
+ ]
275
+ # Custom table format
276
+ summary_table = tabulate(
277
+ summary_data, headers=["Summary", "Values"], tablefmt="outline"
278
+ )
279
+
280
+ # Print the table
281
+ print("\n[======= Trade Account Summary =======]")
282
+ print(summary_table)
283
+
284
+ def risk_managment(self):
285
+ """Show the risk management parameters"""
286
+
287
+ loss = self.currency_risk()["trade_loss"]
288
+ profit = self.currency_risk()["trade_profit"]
289
+ ok = "OK" if self.is_risk_ok() else "Not OK"
290
+ account_info = self.get_account_info()
291
+ _profit = round(self.get_stats()[1]["total_profit"], 2)
292
+ currency = account_info.currency
293
+ rates = self.get_currency_rates(self.symbol)
294
+ marging_currency = rates["mc"]
295
+ account_data = [
296
+ ["Account Name", account_info.name],
297
+ ["Account Number", account_info.login],
298
+ ["Account Server", account_info.server],
299
+ ["Account Balance", f"{account_info.balance} {currency}"],
300
+ ["Account Profit", f"{_profit} {currency}"],
301
+ ["Account Equity", f"{account_info.equity} {currency}"],
302
+ ["Account Leverage", self.get_leverage(True)],
303
+ ["Account Margin", f"{round(account_info.margin, 2)} {currency}"],
304
+ ["Account Free Margin", f"{account_info.margin_free} {currency}"],
305
+ ["Maximum Drawdown", f"{self.max_risk}%"],
306
+ ["Risk Allowed", f"{round((self.max_risk - self.risk_level()), 2)}%"],
307
+ ["Volume", f"{self.volume()} {marging_currency}"],
308
+ ["Risk Per trade", f"{-self.get_currency_risk()} {currency}"],
309
+ ["Profit Expected Per trade", f"{self.expected_profit()} {currency}"],
310
+ ["Lot Size", f"{self.get_lot()} Lots"],
311
+ ["Stop Loss", f"{self.get_stop_loss()} Points"],
312
+ ["Loss Value Per Tick", f"{round(loss, 5)} {currency}"],
313
+ ["Take Profit", f"{self.get_take_profit()} Points"],
314
+ ["Profit Value Per Tick", f"{round(profit, 5)} {currency}"],
315
+ ["Break Even", f"{self.get_break_even()} Points"],
316
+ ["Deviation", f"{self.get_deviation()} Points"],
317
+ ["Trading Time Interval", f"{self.get_minutes()} Minutes"],
318
+ ["Risk Level", ok],
319
+ ["Maximum Trades", self.max_trade()],
320
+ ]
321
+ # Custom table format
322
+ print("\n[======= Account Risk Management Overview =======]")
323
+ table = tabulate(
324
+ account_data, headers=["Risk Metrics", "Values"], tablefmt="outline"
325
+ )
326
+
327
+ # Print the table
328
+ print(table)
329
+
330
+ def statistics(self, save=True, dir=None):
331
+ """
332
+ Print some statistics for the trading session and save to CSV if specified.
333
+
334
+ Args:
335
+ save (bool, optional): Whether to save the statistics to a CSV file.
336
+ dir (str, optional): The directory to save the CSV file.
337
+ """
338
+ stats, additional_stats = self.get_stats()
339
+
340
+ profit = round(stats["profit"], 2)
341
+ win_rate = stats["win_rate"]
342
+ total_fees = round(stats["total_fees"], 3)
343
+ average_fee = round(stats["average_fee"], 3)
344
+ currency = self.get_account_info().currency
345
+ net_profit = round((profit + total_fees), 2)
346
+ trade_risk = round(self.get_currency_risk() * -1, 2)
347
+
348
+ # Formatting the statistics output
349
+ session_data = [
350
+ ["Total Trades", stats["deals"]],
351
+ ["Winning Trades", stats["win_trades"]],
352
+ ["Losing Trades", stats["loss_trades"]],
353
+ ["Session Profit", f"{profit} {currency}"],
354
+ ["Total Fees", f"{total_fees} {currency}"],
355
+ ["Average Fees", f"{average_fee} {currency}"],
356
+ ["Net Profit", f"{net_profit} {currency}"],
357
+ ["Risk per Trade", f"{trade_risk} {currency}"],
358
+ ["Expected Profit per Trade", f"{self.expected_profit()} {currency}"],
359
+ ["Risk Reward Ratio", self.rr],
360
+ ["Win Rate", f"{win_rate}%"],
361
+ ["Sharpe Ratio", self.sharpe()],
362
+ ["Trade Profitability", additional_stats["profitability"]],
363
+ ]
364
+ session_table = tabulate(
365
+ session_data, headers=["Statistics", "Values"], tablefmt="outline"
366
+ )
367
+
368
+ # Print the formatted statistics
369
+ if self.verbose:
370
+ print("\n[======= Trading Session Statistics =======]")
371
+ print(session_table)
372
+
373
+ # Save to CSV if specified
374
+ if save:
375
+ today_date = datetime.now().strftime("%Y%m%d%H%M%S")
376
+ # Create a dictionary with the statistics
377
+ statistics_dict = {item[0]: item[1] for item in session_data}
378
+ stats_df = pd.DataFrame(statistics_dict, index=[0])
379
+ # Create the directory if it doesn't exist
380
+ dir = dir or ".sessions"
381
+ os.makedirs(dir, exist_ok=True)
382
+ if "." in self.symbol:
383
+ symbol = self.symbol.split(".")[0]
384
+ else:
385
+ symbol = self.symbol
386
+
387
+ filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
388
+ filepath = os.path.join(dir, filename)
389
+ stats_df.to_csv(filepath, index=False)
390
+ self.logger.info(f"Session statistics saved to {filepath}")
391
+
392
+ Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
393
+
394
+ def open_buy_position(
395
+ self,
396
+ action: Buys = "BMKT",
397
+ price: Optional[float] = None,
398
+ stoplimit: Optional[float] = None,
399
+ mm: bool = True,
400
+ id: Optional[int] = None,
401
+ comment: Optional[str] = None,
402
+ ):
403
+ """
404
+ Open a Buy positin
405
+
406
+ Args:
407
+ action (str): `BMKT` for Market orders or `BLMT`,
408
+ `BSTP`,`BSTPLMT` for pending orders
409
+ price (float): The price at which to open an order
410
+ stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
411
+ The pending order is not passed to the trading system until that moment
412
+ id (int): The strategy id or expert Id
413
+ mm (bool): Weither to put stop loss and tp or not
414
+ comment (str): The comment for the opening position
415
+ """
416
+ Id = id if id is not None else self.expert_id
417
+ point = self.get_symbol_info(self.symbol).point
418
+ if action != "BMKT":
419
+ if price is not None:
420
+ _price = price
421
+ else:
422
+ raise ValueError("You need to set a price for pending orders")
423
+ else:
424
+ _price = self.get_tick_info(self.symbol).ask
425
+
426
+ lot = self.get_lot()
427
+ stop_loss = self.get_stop_loss()
428
+ take_profit = self.get_take_profit()
429
+ deviation = self.get_deviation()
430
+ request = {
431
+ "action": Mt5.TRADE_ACTION_DEAL,
432
+ "symbol": self.symbol,
433
+ "volume": float(lot),
434
+ "type": Mt5.ORDER_TYPE_BUY,
435
+ "price": _price,
436
+ "deviation": deviation,
437
+ "magic": Id,
438
+ "comment": f"@{self.expert_name}" if comment is None else comment,
439
+ "type_time": Mt5.ORDER_TIME_GTC,
440
+ "type_filling": Mt5.ORDER_FILLING_FOK,
441
+ }
442
+ mm_price = _price
443
+ if action != "BMKT":
444
+ request["action"] = Mt5.TRADE_ACTION_PENDING
445
+ request["type"] = self._order_type()[action][0]
446
+ if action == "BSTPLMT":
447
+ if stoplimit is None:
448
+ raise ValueError("You need to set a stoplimit price for BSTPLMT orders")
449
+ if stoplimit > _price:
450
+ raise ValueError(
451
+ "Stoplimit price must be less than the price and greater than the current price"
452
+ )
453
+ request["stoplimit"] = stoplimit
454
+ mm_price = stoplimit
455
+ if mm:
456
+ request["sl"] = mm_price - stop_loss * point
457
+ request["tp"] = mm_price + take_profit * point
458
+ self.break_even(mm=mm, id=Id)
459
+ if self.check(comment):
460
+ (self.request_result(_price, request, action),)
461
+
462
+ def _order_type(self):
463
+ type = {
464
+ "BMKT": (Mt5.ORDER_TYPE_BUY, "BUY"),
465
+ "SMKT": (Mt5.ORDER_TYPE_BUY, "SELL"),
466
+ "BLMT": (Mt5.ORDER_TYPE_BUY_LIMIT, "BUY_LIMIT"),
467
+ "SLMT": (Mt5.ORDER_TYPE_SELL_LIMIT, "SELL_LIMIT"),
468
+ "BSTP": (Mt5.ORDER_TYPE_BUY_STOP, "BUY_STOP"),
469
+ "SSTP": (Mt5.ORDER_TYPE_SELL_STOP, "SELL_STOP"),
470
+ "BSTPLMT": (Mt5.ORDER_TYPE_BUY_STOP_LIMIT, "BUY_STOP_LIMIT"),
471
+ "SSTPLMT": (Mt5.ORDER_TYPE_SELL_STOP_LIMIT, "SELL_STOP_LIMIT"),
472
+ }
473
+ return type
474
+
475
+ Sells = Literal["SMKT", "SLMT", "SSTP", "SSTPLMT"]
476
+
477
+ def open_sell_position(
478
+ self,
479
+ action: Sells = "SMKT",
480
+ price: Optional[float] = None,
481
+ stoplimit: Optional[float] = None,
482
+ mm: bool = True,
483
+ id: Optional[int] = None,
484
+ comment: Optional[str] = None,
485
+ ):
486
+ """
487
+ Open a sell positin
488
+
489
+ Args:
490
+ action (str): `SMKT` for Market orders
491
+ or ``SLMT``, ``SSTP``,``SSTPLMT`` for pending orders
492
+ price (float): The price at which to open an order
493
+ stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
494
+ The pending order is not passed to the trading system until that moment
495
+ id (int): The strategy id or expert Id
496
+ mm (bool): Weither to put stop loss and tp or not
497
+ comment (str): The comment for the closing position
498
+ """
499
+ Id = id if id is not None else self.expert_id
500
+ point = self.get_symbol_info(self.symbol).point
501
+ if action != "SMKT":
502
+ if price is not None:
503
+ _price = price
504
+ else:
505
+ raise ValueError("You need to set a price for pending orders")
506
+ else:
507
+ _price = self.get_tick_info(self.symbol).bid
508
+
509
+ lot = self.get_lot()
510
+ stop_loss = self.get_stop_loss()
511
+ take_profit = self.get_take_profit()
512
+ deviation = self.get_deviation()
513
+ request = {
514
+ "action": Mt5.TRADE_ACTION_DEAL,
515
+ "symbol": self.symbol,
516
+ "volume": float(lot),
517
+ "type": Mt5.ORDER_TYPE_SELL,
518
+ "price": _price,
519
+ "deviation": deviation,
520
+ "magic": Id,
521
+ "comment": f"@{self.expert_name}" if comment is None else comment,
522
+ "type_time": Mt5.ORDER_TIME_GTC,
523
+ "type_filling": Mt5.ORDER_FILLING_FOK,
524
+ }
525
+ mm_price = _price
526
+ if action != "SMKT":
527
+ request["action"] = Mt5.TRADE_ACTION_PENDING
528
+ request["type"] = self._order_type()[action][0]
529
+ if action == "SSTPLMT":
530
+ if stoplimit is None:
531
+ raise ValueError("You need to set a stoplimit price for SSTPLMT orders")
532
+ if stoplimit < _price:
533
+ raise ValueError(
534
+ "Stoplimit price must be greater than the price and less than the current price"
535
+ )
536
+ request["stoplimit"] = stoplimit
537
+ mm_price = stoplimit
538
+ if mm:
539
+ request["sl"] = mm_price + stop_loss * point
540
+ request["tp"] = mm_price - take_profit * point
541
+ self.break_even(mm=mm, id=Id)
542
+ if self.check(comment):
543
+ self.request_result(_price, request, action)
544
+
545
+ def check(self, comment):
546
+ """
547
+ Verify if all conditions for taking a position are valide,
548
+ These conditions are based on the Maximum risk ,daily risk,
549
+ the starting, the finishing, and ending trading time.
550
+
551
+ Args:
552
+ comment (str): The comment for the closing position
553
+ """
554
+ if self.days_end():
555
+ return False
556
+ elif not self.trading_time():
557
+ self.logger.info(f"Not Trading time, SYMBOL={self.symbol}")
558
+ return False
559
+ elif not self.is_risk_ok():
560
+ self.logger.error(f"Account Risk not allowed, SYMBOL={self.symbol}")
561
+ self._check(comment)
562
+ return False
563
+ elif self.profit_target():
564
+ self._check(f"Profit target Reached !!! SYMBOL={self.symbol}")
565
+ return True
566
+
567
+ def _check(self, txt: str = ""):
568
+ if (
569
+ self.positive_profit(id=self.expert_id)
570
+ or self.get_current_positions() is None
571
+ ):
572
+ self.close_positions(position_type="all")
573
+ self.logger.info(txt)
574
+ time.sleep(5)
575
+ self.statistics(save=True)
576
+
577
+ def request_result(self, price: float, request: Dict[str, Any], type: Buys | Sells):
578
+ """
579
+ Check if a trading order has been sent correctly
580
+
581
+ Args:
582
+ price (float): Price for opening the position
583
+ request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
584
+ all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py
585
+
586
+ type (str): The type of the order `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
587
+ """
588
+ # Send a trading request
589
+ # Check the execution result
590
+ pos = self._order_type()[type][1]
591
+ addtionnal = f", SYMBOL={self.symbol}"
592
+ try:
593
+ self.check_order(request)
594
+ result = self.send_order(request)
595
+ except Exception as e:
596
+ print(f"{self.current_datetime()} -", end=" ")
597
+ trade_retcode_message(
598
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}"
599
+ )
600
+ if result.retcode != Mt5.TRADE_RETCODE_DONE:
601
+ if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
602
+ for fill in FILLING_TYPE:
603
+ request["type_filling"] = fill
604
+ result = self.send_order(request)
605
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
606
+ break
607
+ elif result.retcode not in self._retcodes:
608
+ self._retcodes.append(result.retcode)
609
+ msg = trade_retcode_message(result.retcode)
610
+ self.logger.error(
611
+ f"Trade Order Request, RETCODE={result.retcode}: {msg}{addtionnal}"
612
+ )
613
+ elif result.retcode in [
614
+ Mt5.TRADE_RETCODE_CONNECTION,
615
+ Mt5.TRADE_RETCODE_TIMEOUT,
616
+ ]:
617
+ tries = 0
618
+ while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
619
+ time.sleep(1)
620
+ try:
621
+ self.check_order(request)
622
+ result = self.send_order(request)
623
+ except Exception as e:
624
+ print(f"{self.current_datetime()} -", end=" ")
625
+ trade_retcode_message(
626
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}"
627
+ )
628
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
629
+ break
630
+ tries += 1
631
+ # Print the result
632
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
633
+ msg = trade_retcode_message(result.retcode)
634
+ self.logger.info(f"Trade Order {msg}{addtionnal}")
635
+ if type != "BMKT" or type != "SMKT":
636
+ self.opened_orders.append(result.order)
637
+ long_msg = (
638
+ f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{price}, "
639
+ f"Lot(s): {result.volume}, Sl: {self.get_stop_loss()}, "
640
+ f"Tp: {self.get_take_profit()}"
641
+ )
642
+ self.logger.info(long_msg)
643
+ time.sleep(0.1)
644
+ if type == "BMKT" or type == "SMKT":
645
+ self.opened_positions.append(result.order)
646
+ positions = self.get_positions(symbol=self.symbol)
647
+ for position in positions:
648
+ if position.ticket == result.order:
649
+ if position.type == 0:
650
+ order_type = "BUY"
651
+ self.buy_positions.append(position.ticket)
652
+ else:
653
+ order_type = "SELL"
654
+ self.sell_positions.append(position.ticket)
655
+ profit = round(self.get_account_info().profit, 5)
656
+ order_info = (
657
+ f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open,5)}, "
658
+ f"Sl: @{position.sl} Tp: @{position.tp}"
659
+ )
660
+ self.logger.info(order_info)
661
+ pos_info = (
662
+ f"3. [OPEN POSITIONS ON {self.symbol} = {len(positions)}, ACCOUNT OPEN PnL = {profit} "
663
+ f"{self.get_account_info().currency}]\n"
664
+ )
665
+ self.logger.info(pos_info)
666
+ else:
667
+ msg = trade_retcode_message(result.retcode)
668
+ self.logger.error(
669
+ f"Unable to Open Position, RETCODE={result.retcode}: {msg}{addtionnal}"
670
+ )
671
+
672
+ def open_position(
673
+ self,
674
+ action: Buys | Sells,
675
+ price: Optional[float] = None,
676
+ stoplimit: Optional[float] = None,
677
+ id: Optional[int] = None,
678
+ mm: bool = True,
679
+ comment: Optional[str] = None,
680
+ ):
681
+ """
682
+ Open a buy or sell position.
683
+
684
+ Args:
685
+ action (str): (`'BMKT'`, `'SMKT'`) for Market orders
686
+ or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
687
+ price (float): The price at which to open an order
688
+ stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
689
+ The pending order is not passed to the trading system until that moment
690
+ id (int): The strategy id or expert Id
691
+ mm (bool): Weither to put stop loss and tp or not
692
+ comment (str): The comment for the closing position
693
+ """
694
+ BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
695
+ SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
696
+ if action in BUYS:
697
+ self.open_buy_position(
698
+ action=action,
699
+ price=price,
700
+ stoplimit=stoplimit,
701
+ id=id,
702
+ mm=mm,
703
+ comment=comment,
704
+ )
705
+ elif action in SELLS:
706
+ self.open_sell_position(
707
+ action=action,
708
+ price=price,
709
+ stoplimit=stoplimit,
710
+ id=id,
711
+ mm=mm,
712
+ comment=comment,
713
+ )
714
+ else:
715
+ raise ValueError(
716
+ f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}"
717
+ )
718
+
719
+ @property
720
+ def orders(self):
721
+ """Return all opened order's tickets"""
722
+ if len(self.opened_orders) != 0:
723
+ return self.opened_orders
724
+ return None
725
+
726
+ @property
727
+ def positions(self):
728
+ """Return all opened position's tickets"""
729
+ if len(self.opened_positions) != 0:
730
+ return self.opened_positions
731
+ return None
732
+
733
+ @property
734
+ def buypos(self):
735
+ """Return all buy opened position's tickets"""
736
+ if len(self.buy_positions) != 0:
737
+ return self.buy_positions
738
+ return None
739
+
740
+ @property
741
+ def sellpos(self):
742
+ """Return all sell opened position's tickets"""
743
+ if len(self.sell_positions) != 0:
744
+ return self.sell_positions
745
+ return None
746
+
747
+ @property
748
+ def bepos(self):
749
+ """Return All positon's tickets
750
+ for which a break even has been set"""
751
+ if len(self.break_even_status) != 0:
752
+ return self.break_even_status
753
+ return None
754
+
755
+ def get_filtered_tickets(
756
+ self, id: Optional[int] = None, filter_type: Optional[str] = None, th=None
757
+ ) -> List[int] | None:
758
+ """
759
+ Get tickets for positions or orders based on filters.
760
+
761
+ Args:
762
+ id (int): The strategy id or expert Id
763
+ filter_type (str): Filter type to apply on the tickets,
764
+ - `orders` are current open orders
765
+ - `buy_stops` are current buy stop orders
766
+ - `sell_stops` are current sell stop orders
767
+ - `buy_limits` are current buy limit orders
768
+ - `sell_limits` are current sell limit orders
769
+ - `buy_stop_limits` are current buy stop limit orders
770
+ - `sell_stop_limits` are current sell stop limit orders
771
+ - `positions` are all current open positions
772
+ - `buys` and `sells` are current buy or sell open positions
773
+ - `profitables` are current open position that have a profit greater than a threshold
774
+ - `losings` are current open position that have a negative profit
775
+ th (bool): the minimum treshold for winning position
776
+ (only relevant when filter_type is 'profitables')
777
+
778
+ Returns:
779
+ List[int] | None: A list of filtered tickets
780
+ or None if no tickets match the criteria.
781
+ """
782
+ Id = id if id is not None else self.expert_id
783
+ POSITIONS = ["positions", "buys", "sells", "profitables", "losings"]
784
+
785
+ if filter_type not in POSITIONS:
786
+ items = self.get_orders(symbol=self.symbol)
787
+ else:
788
+ items = self.get_positions(symbol=self.symbol)
789
+
790
+ filtered_tickets = []
791
+
792
+ if items is not None:
793
+ for item in items:
794
+ if item.magic == Id:
795
+ if filter_type == "buys" and item.type != 0:
796
+ continue
797
+ if filter_type == "sells" and item.type != 1:
798
+ continue
799
+ if filter_type == "losings" and item.profit > 0:
800
+ continue
801
+ if filter_type == "profitables" and not self.win_trade(item, th=th):
802
+ continue
803
+ if (
804
+ filter_type == "buy_stops"
805
+ and item.type != self._order_type()["BSTP"][0]
806
+ ):
807
+ continue
808
+ if (
809
+ filter_type == "sell_stops"
810
+ and item.type != self._order_type()["SSTP"][0]
811
+ ):
812
+ continue
813
+ if (
814
+ filter_type == "buy_limits"
815
+ and item.type != self._order_type()["BLMT"][0]
816
+ ):
817
+ continue
818
+ if (
819
+ filter_type == "sell_limits"
820
+ and item.type != self._order_type()["SLMT"][0]
821
+ ):
822
+ continue
823
+ if (
824
+ filter_type == "buy_stop_limits"
825
+ and item.type != self._order_type()["BSTPLMT"][0]
826
+ ):
827
+ continue
828
+ if (
829
+ filter_type == "sell_stop_limits"
830
+ and item.type != self._order_type()["SSTPLMT"][0]
831
+ ):
832
+ continue
833
+ filtered_tickets.append(item.ticket)
834
+ return filtered_tickets if filtered_tickets else None
835
+ return None
836
+
837
+ def get_current_orders(self, id: Optional[int] = None) -> List[int] | None:
838
+ return self.get_filtered_tickets(id=id, filter_type="orders")
839
+
840
+ def get_current_buy_stops(self, id: Optional[int] = None) -> List[int] | None:
841
+ return self.get_filtered_tickets(id=id, filter_type="buy_stops")
842
+
843
+ def get_current_sell_stops(self, id: Optional[int] = None) -> List[int] | None:
844
+ return self.get_filtered_tickets(id=id, filter_type="sell_stops")
845
+
846
+ def get_current_buy_limits(self, id: Optional[int] = None) -> List[int] | None:
847
+ return self.get_filtered_tickets(id=id, filter_type="buy_limits")
848
+
849
+ def get_current_sell_limits(self, id: Optional[int] = None) -> List[int] | None:
850
+ return self.get_filtered_tickets(id=id, filter_type="sell_limits")
851
+
852
+ def get_current_buy_stop_limits(self, id: Optional[int] = None) -> List[int] | None:
853
+ return self.get_filtered_tickets(id=id, filter_type="buy_stop_limits")
854
+
855
+ def get_current_sell_stop_limits(
856
+ self, id: Optional[int] = None
857
+ ) -> List[int] | None:
858
+ return self.get_filtered_tickets(id=id, filter_type="sell_stop_limits")
859
+
860
+ def get_current_positions(self, id: Optional[int] = None) -> List[int] | None:
861
+ return self.get_filtered_tickets(id=id, filter_type="positions")
862
+
863
+ def get_current_profitables(
864
+ self, id: Optional[int] = None, th=None
865
+ ) -> List[int] | None:
866
+ return self.get_filtered_tickets(id=id, filter_type="profitables", th=th)
867
+
868
+ def get_current_losings(self, id: Optional[int] = None) -> List[int] | None:
869
+ return self.get_filtered_tickets(id=id, filter_type="losings")
870
+
871
+ def get_current_buys(self, id: Optional[int] = None) -> List[int] | None:
872
+ return self.get_filtered_tickets(id=id, filter_type="buys")
873
+
874
+ def get_current_sells(self, id: Optional[int] = None) -> List[int] | None:
875
+ return self.get_filtered_tickets(id=id, filter_type="sells")
876
+
877
+ def positive_profit(
878
+ self, th: Optional[float] = None, id: Optional[int] = None, account: bool = True
879
+ ) -> bool:
880
+ """
881
+ Check is the total profit on current open positions
882
+ Is greater than a minimum profit express as percentage
883
+ of the profit target.
884
+
885
+ Args:
886
+ th (float): The minimum profit target on current positions
887
+ id (int): The strategy id or expert Id
888
+ account (bool): Weither to check positions on the account or on the symbol
889
+ """
890
+ if account and id is None:
891
+ # All open positions no matter the symbol or strategy or expert
892
+ positions = self.get_positions()
893
+ elif account and id is not None:
894
+ # All open positions for a specific strategy or expert no matter the symbol
895
+ positions = self.get_positions()
896
+ if positions is not None:
897
+ positions = [position for position in positions if position.magic == id]
898
+ elif not account and id is None:
899
+ # All open positions for the current symbol no matter the strategy or expert
900
+ positions = self.get_positions(symbol=self.symbol)
901
+ elif not account and id is not None:
902
+ # All open positions for the current symbol and a specific strategy or expert
903
+ positions = self.get_positions(symbol=self.symbol)
904
+ if positions is not None:
905
+ positions = [position for position in positions if position.magic == id]
906
+
907
+ if positions is not None:
908
+ profit = 0.0
909
+ balance = self.get_account_info().balance
910
+ target = round((balance * self.target) / 100, 2)
911
+ for position in positions:
912
+ profit += position.profit
913
+ fees = self.get_stats()[0]["average_fee"] * len(positions)
914
+ current_profit = profit + fees
915
+ th_profit = (target * th) / 100 if th is not None else (target * 0.01)
916
+ return current_profit >= th_profit
917
+ return False
918
+
919
+ def break_even(
920
+ self,
921
+ mm=True,
922
+ id: Optional[int] = None,
923
+ trail: Optional[bool] = True,
924
+ stop_trail: Optional[int] = None,
925
+ trail_after_points: Optional[int] = None,
926
+ be_plus_points: Optional[int] = None,
927
+ ):
928
+ """
929
+ This function checks if it's time to set the break-even level for a trading position.
930
+ If it is, it sets the break-even level. If the break-even level has already been set,
931
+ it checks if the price has moved in a favorable direction.
932
+ If it has, and the trail parameter is set to True, it updates
933
+ the break-even level based on the trail_after_points and stop_trail parameters.
934
+
935
+ Args:
936
+ id (int): The strategy ID or expert ID.
937
+ mm (bool): Whether to manage the position or not.
938
+ trail (bool): Whether to trail the stop loss or not.
939
+ stop_trail (int): Number of points to trail the stop loss by.
940
+ It represent the distance from the current price to the stop loss.
941
+ trail_after_points (int): Number of points in profit
942
+ from where the strategy will start to trail the stop loss.
943
+ be_plus_points (int): Number of points to add to the break-even level.
944
+ Represents the minimum profit to secure.
945
+ """
946
+ time.sleep(0.1)
947
+ if not mm:
948
+ return
949
+ Id = id if id is not None else self.expert_id
950
+ positions = self.get_positions(symbol=self.symbol)
951
+ be = self.get_break_even()
952
+ if trail_after_points is not None:
953
+ assert trail_after_points > be, (
954
+ "trail_after_points must be greater than break even" " or set to None"
955
+ )
956
+ if positions is not None:
957
+ for position in positions:
958
+ if position.magic == Id:
959
+ size = self.get_symbol_info(self.symbol).trade_tick_size
960
+ value = self.get_symbol_info(self.symbol).trade_tick_value
961
+ point = self.get_symbol_info(self.symbol).point
962
+ digits = self.get_symbol_info(self.symbol).digits
963
+ points = position.profit * (size / value / position.volume)
964
+ break_even = float(points / point) >= be
965
+ if break_even:
966
+ # Check if break-even has already been set for this position
967
+ if position.ticket not in self.break_even_status:
968
+ price = None
969
+ if be_plus_points is not None:
970
+ price = position.price_open + (be_plus_points * point)
971
+ self.set_break_even(position, be, price=price)
972
+ self.break_even_status.append(position.ticket)
973
+ self.break_even_points[position.ticket] = be
974
+ else:
975
+ # Skip this if the trail is not set to True
976
+ if not trail:
977
+ continue
978
+ # Check if the price has moved favorably
979
+ new_be = (
980
+ round(be * 0.10)
981
+ if be_plus_points is None
982
+ else be_plus_points
983
+ )
984
+ if trail_after_points is not None:
985
+ if position.ticket not in self.trail_after_points:
986
+ # This ensures that the position rich the minimum points required
987
+ # before the trail can be set
988
+ new_be = trail_after_points - be
989
+ self.trail_after_points.append(position.ticket)
990
+ new_be_points = (
991
+ self.break_even_points[position.ticket] + new_be
992
+ )
993
+ favorable_move = float(points / point) >= new_be_points
994
+ if favorable_move:
995
+ # This allows the position to go to take profit in case of a swing trade
996
+ # If is a scalping position, we can set the stop_trail close to the current price.
997
+ trail_points = (
998
+ round(be * 0.50)
999
+ if stop_trail is None
1000
+ else stop_trail
1001
+ )
1002
+ # Calculate the new break-even level and price
1003
+ if position.type == 0:
1004
+ # This level validate the favorable move of the price
1005
+ new_level = round(
1006
+ position.price_open + (new_be_points * point),
1007
+ digits,
1008
+ )
1009
+ # This price is set away from the current price by the trail_points
1010
+ new_price = round(
1011
+ position.price_current - (trail_points * point),
1012
+ digits,
1013
+ )
1014
+ if new_price < position.sl:
1015
+ new_price = position.sl
1016
+ elif position.type == 1:
1017
+ new_level = round(
1018
+ position.price_open - (new_be_points * point),
1019
+ digits,
1020
+ )
1021
+ new_price = round(
1022
+ position.price_current + (trail_points * point),
1023
+ digits,
1024
+ )
1025
+ if new_price > position.sl:
1026
+ new_price = position.sl
1027
+ self.set_break_even(
1028
+ position, be, price=new_price, level=new_level
1029
+ )
1030
+
1031
+ def set_break_even(
1032
+ self,
1033
+ position: TradePosition,
1034
+ be: int,
1035
+ price: Optional[float] = None,
1036
+ level: Optional[float] = None,
1037
+ ):
1038
+ """
1039
+ Sets the break-even level for a given trading position.
1040
+
1041
+ Args:
1042
+ position (TradePosition): The trading position for which the break-even is to be set. This is the value return by `mt5.positions_get()`.
1043
+ be (int): The break-even level in points.
1044
+ level (float): The break-even level in price, if set to None , it will be calated automaticaly.
1045
+ price (float): The break-even price, if set to None , it will be calated automaticaly.
1046
+ """
1047
+ point = self.get_symbol_info(self.symbol).point
1048
+ digits = self.get_symbol_info(self.symbol).digits
1049
+ spread = self.get_symbol_info(self.symbol).spread
1050
+ fees = self.get_stats()[0]["average_fee"] * -1
1051
+ risk = self.currency_risk()["trade_profit"]
1052
+ fees_points = round((fees / risk), 3)
1053
+ # If Buy
1054
+ if position.type == 0 and position.price_current > position.price_open:
1055
+ # Calculate the break-even level and price
1056
+ break_even_level = position.price_open + (be * point)
1057
+ break_even_price = position.price_open + ((fees_points + spread) * point)
1058
+ # Check if the price specified is greater or lower than the calculated price
1059
+ _price = (
1060
+ break_even_price if price is None or price < break_even_price else price
1061
+ )
1062
+ _level = break_even_level if level is None else level
1063
+
1064
+ if self.get_tick_info(self.symbol).ask > _level:
1065
+ # Set the stop loss to break even
1066
+ request = {
1067
+ "action": Mt5.TRADE_ACTION_SLTP,
1068
+ "type": Mt5.ORDER_TYPE_SELL_STOP,
1069
+ "position": position.ticket,
1070
+ "sl": round(_price, digits),
1071
+ "tp": position.tp,
1072
+ }
1073
+ self.break_even_request(position.ticket, round(_price, digits), request)
1074
+ # If Sell
1075
+ elif position.type == 1 and position.price_current < position.price_open:
1076
+ break_even_level = position.price_open - (be * point)
1077
+ break_even_price = position.price_open - ((fees_points + spread) * point)
1078
+ _price = (
1079
+ break_even_price if price is None or price > break_even_price else price
1080
+ )
1081
+ _level = break_even_level if level is None else level
1082
+
1083
+ if self.get_tick_info(self.symbol).bid < _level:
1084
+ # Set the stop loss to break even
1085
+ request = {
1086
+ "action": Mt5.TRADE_ACTION_SLTP,
1087
+ "type": Mt5.ORDER_TYPE_BUY_STOP,
1088
+ "position": position.ticket,
1089
+ "sl": round(_price, digits),
1090
+ "tp": position.tp,
1091
+ }
1092
+ self.break_even_request(position.ticket, round(_price, digits), request)
1093
+
1094
+ def break_even_request(self, tiket, price, request):
1095
+ """
1096
+ Send a request to set the stop loss to break even for a given trading position.
1097
+
1098
+ Args:
1099
+ tiket (int): The ticket number of the trading position.
1100
+ price (float): The price at which the stop loss is to be set.
1101
+ request (dict): The request to set the stop loss to break even.
1102
+ """
1103
+ addtionnal = f", SYMBOL={self.symbol}"
1104
+ time.sleep(0.1)
1105
+ try:
1106
+ self.check_order(request)
1107
+ result = self.send_order(request)
1108
+ except Exception as e:
1109
+ print(f"{self.current_datetime()} -", end=" ")
1110
+ trade_retcode_message(
1111
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}"
1112
+ )
1113
+ if result.retcode != Mt5.TRADE_RETCODE_DONE:
1114
+ msg = trade_retcode_message(result.retcode)
1115
+ if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
1116
+ self.logger.error(
1117
+ f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}"
1118
+ )
1119
+ tries = 0
1120
+ while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
1121
+ if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
1122
+ break
1123
+ else:
1124
+ time.sleep(1)
1125
+ try:
1126
+ self.check_order(request)
1127
+ result = self.send_order(request)
1128
+ except Exception as e:
1129
+ print(f"{self.current_datetime()} -", end=" ")
1130
+ trade_retcode_message(
1131
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}"
1132
+ )
1133
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
1134
+ break
1135
+ tries += 1
1136
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
1137
+ msg = trade_retcode_message(result.retcode)
1138
+ self.logger.info(f"Break-Even Order {msg}{addtionnal}")
1139
+ info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{price}"
1140
+ self.logger.info(info)
1141
+ self.break_even_status.append(tiket)
1142
+
1143
+ def win_trade(self, position: TradePosition, th: Optional[int] = None) -> bool:
1144
+ """
1145
+ Check if a positon is wining or looing
1146
+ wen it is closed before be level , tp or sl.
1147
+
1148
+ Args:
1149
+ position (TradePosition): The trading position to check.
1150
+ th (int): The minimum profit for a position in point
1151
+ """
1152
+ size = self.get_symbol_info(self.symbol).trade_tick_size
1153
+ value = self.get_symbol_info(self.symbol).trade_tick_value
1154
+ points = position.profit * (size / value / position.volume)
1155
+
1156
+ point = self.get_symbol_info(self.symbol).point
1157
+ fees = self.get_stats()[0]["average_fee"] * -1
1158
+ risk = self.currency_risk()["trade_profit"]
1159
+ min_be = round((fees / risk)) + 2
1160
+ be = self.get_break_even()
1161
+ if th is not None:
1162
+ win_be = th
1163
+ else:
1164
+ win_be = max(min_be, round((0.1 * be)))
1165
+ win_trade = float(points / point) >= win_be
1166
+ # Check if the positon is in profit
1167
+ if win_trade:
1168
+ # Check if break-even has already been set for this position
1169
+ if position.ticket not in self.break_even_status:
1170
+ return True
1171
+ return False
1172
+
1173
+ def profit_target(self):
1174
+ fee = 0.0
1175
+ swap = 0.0
1176
+ commission = 0.0
1177
+ profit = 0.0
1178
+ balance = self.get_account_info().balance
1179
+ target = round((balance * self.target) / 100, 2)
1180
+ if len(self.opened_positions) != 0:
1181
+ for position in self.opened_positions:
1182
+ time.sleep(0.1)
1183
+ # This return two TradeDeal Object,
1184
+ # The first one is the opening order
1185
+ # The second is the closing order
1186
+ history = self.get_trades_history(position=position, to_df=False)
1187
+ if len(history) == 2:
1188
+ profit += history[1].profit
1189
+ commission += history[0].commission
1190
+ swap += history[0].swap
1191
+ fee += history[0].fee
1192
+ current_profit = profit + commission + fee + swap
1193
+ if current_profit >= target:
1194
+ return True
1195
+ return False
1196
+
1197
+ def close_request(self, request: dict, type: str):
1198
+ """
1199
+ Close a trading order or position
1200
+
1201
+ Args:
1202
+ request (dict): The request to close a trading order or position
1203
+ type (str): Type of the request ('order', 'position')
1204
+ """
1205
+ ticket = request[type]
1206
+ addtionnal = f", SYMBOL={self.symbol}"
1207
+ try:
1208
+ self.check_order(request)
1209
+ result = self.send_order(request)
1210
+ except Exception as e:
1211
+ print(f"{self.current_datetime()} -", end=" ")
1212
+ trade_retcode_message(
1213
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}"
1214
+ )
1215
+ if result.retcode != Mt5.TRADE_RETCODE_DONE:
1216
+ if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
1217
+ for fill in FILLING_TYPE:
1218
+ request["type_filling"] = fill
1219
+ result = self.send_order(request)
1220
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
1221
+ break
1222
+ elif result.retcode not in self._retcodes:
1223
+ self._retcodes.append(result.retcode)
1224
+ msg = trade_retcode_message(result.retcode)
1225
+ self.logger.error(
1226
+ f"Closing Order Request, {type.capitalize()}: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}"
1227
+ )
1228
+ else:
1229
+ tries = 0
1230
+ while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
1231
+ time.sleep(1)
1232
+ try:
1233
+ self.check_order(request)
1234
+ result = self.send_order(request)
1235
+ except Exception as e:
1236
+ print(f"{self.current_datetime()} -", end=" ")
1237
+ trade_retcode_message(
1238
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}"
1239
+ )
1240
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
1241
+ break
1242
+ tries += 1
1243
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
1244
+ msg = trade_retcode_message(result.retcode)
1245
+ self.logger.info(f"Closing Order {msg}{addtionnal}")
1246
+ info = f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol}, Price: @{request.get('price', 0.0)}"
1247
+ self.logger.info(info)
1248
+ return True
1249
+ else:
1250
+ return False
1251
+
1252
+ def close_order(
1253
+ self, ticket: int, id: Optional[int] = None, comment: Optional[str] = None
1254
+ ):
1255
+ """
1256
+ Close an open order by it ticket
1257
+
1258
+ Args:
1259
+ ticket (int): Order ticket to close (e.g TradeOrder.ticket)
1260
+ id (int): The unique ID of the Expert or Strategy
1261
+ comment (str): Comment for the closing position
1262
+
1263
+ Returns:
1264
+ - True if order closed, False otherwise
1265
+ """
1266
+ request = {
1267
+ "action": Mt5.TRADE_ACTION_REMOVE,
1268
+ "symbol": self.symbol,
1269
+ "order": ticket,
1270
+ "magic": id if id is not None else self.expert_id,
1271
+ "comment": f"@{self.expert_name}" if comment is None else comment,
1272
+ }
1273
+ return self.close_request(request, type="order")
1274
+
1275
+ def close_position(
1276
+ self,
1277
+ ticket: int,
1278
+ id: Optional[int] = None,
1279
+ pct: Optional[float] = 1.0,
1280
+ comment: Optional[str] = None,
1281
+ ) -> bool:
1282
+ """
1283
+ Close an open position by it ticket
1284
+
1285
+ Args:
1286
+ ticket (int): Positon ticket to close (e.g TradePosition.ticket)
1287
+ id (int): The unique ID of the Expert or Strategy
1288
+ pct (float): Percentage of the position to close
1289
+ comment (str): Comment for the closing position
1290
+
1291
+ Returns:
1292
+ - True if position closed, False otherwise
1293
+ """
1294
+ # get all Actives positions
1295
+ time.sleep(0.1)
1296
+ Id = id if id is not None else self.expert_id
1297
+ positions = self.get_positions(ticket=ticket)
1298
+ buy_price = self.get_tick_info(self.symbol).ask
1299
+ sell_price = self.get_tick_info(self.symbol).bid
1300
+ deviation = self.get_deviation()
1301
+ if positions is not None and len(positions) == 1:
1302
+ position = positions[0]
1303
+ if position.ticket == ticket and position.magic == Id:
1304
+ buy = position.type == 0
1305
+ request = {
1306
+ "action": Mt5.TRADE_ACTION_DEAL,
1307
+ "symbol": self.symbol,
1308
+ "volume": (position.volume * pct),
1309
+ "type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
1310
+ "position": ticket,
1311
+ "price": sell_price if buy else buy_price,
1312
+ "deviation": deviation,
1313
+ "magic": Id,
1314
+ "comment": f"@{self.expert_name}" if comment is None else comment,
1315
+ "type_time": Mt5.ORDER_TIME_GTC,
1316
+ "type_filling": Mt5.ORDER_FILLING_FOK,
1317
+ }
1318
+ return self.close_request(request, type="position")
1319
+
1320
+ def bulk_close(
1321
+ self,
1322
+ tickets: List,
1323
+ tikets_type: Literal["positions", "orders"],
1324
+ close_func: Callable,
1325
+ order_type: str,
1326
+ id: Optional[int] = None,
1327
+ comment: Optional[str] = None,
1328
+ ):
1329
+ """
1330
+ Close multiple orders or positions at once.
1331
+
1332
+ Args:
1333
+ tickets (List): List of tickets to close
1334
+ tikets_type (str): Type of tickets to close ('positions', 'orders')
1335
+ close_func (Callable): The function to close the tickets
1336
+ order_type (str): Type of orders or positions to close
1337
+ id (int): The unique ID of the Expert or Strategy
1338
+ comment (str): Comment for the closing position
1339
+ """
1340
+ if order_type == "all":
1341
+ order_type = "open"
1342
+ if tickets is not None and len(tickets) > 0:
1343
+ for ticket in tickets.copy():
1344
+ if close_func(ticket, id=id, comment=comment):
1345
+ tickets.remove(ticket)
1346
+ time.sleep(1)
1347
+ if tickets is not None and len(tickets) == 0:
1348
+ self.logger.info(
1349
+ f"ALL {order_type.upper()} {tikets_type.upper()} closed, SYMBOL={self.symbol}."
1350
+ )
1351
+ else:
1352
+ self.logger.info(
1353
+ f"{len(tickets)} {order_type.upper()} {tikets_type.upper()} not closed, SYMBOL={self.symbol}"
1354
+ )
1355
+ else:
1356
+ self.logger.info(
1357
+ f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}."
1358
+ )
1359
+
1360
+ Orders = Literal[
1361
+ "all",
1362
+ "buy_stops",
1363
+ "sell_stops",
1364
+ "buy_limits",
1365
+ "sell_limits",
1366
+ "buy_stop_limits",
1367
+ "sell_stop_limits",
1368
+ ]
1369
+
1370
+ def close_orders(
1371
+ self,
1372
+ order_type: Orders,
1373
+ id: Optional[int] = None,
1374
+ comment: Optional[str] = None,
1375
+ ):
1376
+ """
1377
+ Args:
1378
+ order_type (str): Type of orders to close ('all', 'buy_stops', 'sell_stops', 'buy_limits', 'sell_limits', 'buy_stop_limits', 'sell_stop_limits')
1379
+ id (int): The unique ID of the Expert or Strategy
1380
+ comment (str): Comment for the closing position
1381
+ """
1382
+ id = id if id is not None else self.expert_id
1383
+ if order_type == "all":
1384
+ orders = self.get_current_orders(id=id)
1385
+ elif order_type == "buy_stops":
1386
+ orders = self.get_current_buy_stops(id=id)
1387
+ elif order_type == "sell_stops":
1388
+ orders = self.get_current_sell_stops(id=id)
1389
+ elif order_type == "buy_limits":
1390
+ orders = self.get_current_buy_limits(id=id)
1391
+ elif order_type == "sell_limits":
1392
+ orders = self.get_current_sell_limits(id=id)
1393
+ elif order_type == "buy_stop_limits":
1394
+ orders = self.get_current_buy_stop_limits(id=id)
1395
+ elif order_type == "sell_stop_limits":
1396
+ orders = self.get_current_sell_stop_limits(id=id)
1397
+ else:
1398
+ self.logger.error(f"Invalid order type: {order_type}")
1399
+ return
1400
+ self.bulk_close(
1401
+ orders, "orders", self.close_order, order_type, id=id, comment=comment
1402
+ )
1403
+
1404
+ Positions = Literal["all", "buy", "sell", "profitable", "losing"]
1405
+
1406
+ def close_positions(
1407
+ self,
1408
+ position_type: Positions,
1409
+ id: Optional[int] = None,
1410
+ comment: Optional[str] = None,
1411
+ ):
1412
+ """
1413
+ Args:
1414
+ position_type (str): Type of positions to close ('all', 'buy', 'sell', 'profitable', 'losing')
1415
+ id (int): The unique ID of the Expert or Strategy
1416
+ comment (str): Comment for the closing position
1417
+ """
1418
+ id = id if id is not None else self.expert_id
1419
+ if position_type == "all":
1420
+ positions = self.get_current_positions(id=id)
1421
+ elif position_type == "buy":
1422
+ positions = self.get_current_buys(id=id)
1423
+ elif position_type == "sell":
1424
+ positions = self.get_current_sells(id=id)
1425
+ elif position_type == "profitable":
1426
+ positions = self.get_current_profitables(id=id)
1427
+ elif position_type == "losing":
1428
+ positions = self.get_current_losings(id=id)
1429
+ else:
1430
+ self.logger.error(f"Invalid position type: {position_type}")
1431
+ return
1432
+ self.bulk_close(
1433
+ positions,
1434
+ "positions",
1435
+ self.close_position,
1436
+ position_type,
1437
+ id=id,
1438
+ comment=comment,
1439
+ )
1440
+
1441
+ def get_stats(self) -> Tuple[Dict[str, Any]]:
1442
+ """
1443
+ get some stats about the trading day and trading history
1444
+
1445
+ :return: tuple[Dict[str, Any]]
1446
+ """
1447
+ # get history of deals for one trading session
1448
+ profit = 0.0
1449
+ total_fees = 0.0
1450
+ loss_trades = 0
1451
+ win_trades = 0
1452
+ balance = self.get_account_info().balance
1453
+ deals = len(self.opened_positions)
1454
+ if deals != 0:
1455
+ for position in self.opened_positions:
1456
+ time.sleep(0.1)
1457
+ history = self.get_trades_history(position=position, to_df=False)
1458
+ if len(history) == 2:
1459
+ result = history[1].profit
1460
+ comm = history[0].commission
1461
+ swap = history[0].swap
1462
+ fee = history[0].fee
1463
+ if (result + comm + swap + fee) <= 0:
1464
+ loss_trades += 1
1465
+ else:
1466
+ win_trades += 1
1467
+ profit += result
1468
+ total_fees += comm + swap + fee
1469
+ average_fee = total_fees / deals
1470
+ win_rate = round((win_trades / deals) * 100, 2)
1471
+ stats1 = {
1472
+ "deals": deals,
1473
+ "profit": profit,
1474
+ "win_trades": win_trades,
1475
+ "loss_trades": loss_trades,
1476
+ "total_fees": total_fees,
1477
+ "average_fee": average_fee,
1478
+ "win_rate": win_rate,
1479
+ }
1480
+ else:
1481
+ stats1 = {
1482
+ "deals": 0,
1483
+ "profit": 0,
1484
+ "win_trades": 0,
1485
+ "loss_trades": 0,
1486
+ "total_fees": 0,
1487
+ "average_fee": 0,
1488
+ "win_rate": 0,
1489
+ }
1490
+
1491
+ # Get total stats
1492
+ df = self.get_trades_history()
1493
+ if df is not None:
1494
+ df2 = df.iloc[1:]
1495
+ profit = df2["profit"].sum()
1496
+ commisions = df2["commission"].sum()
1497
+ _fees = df2["fee"].sum()
1498
+ _swap = df2["swap"].sum()
1499
+ total_profit = commisions + _fees + _swap + profit
1500
+ account_info = self.get_account_info()
1501
+ balance = account_info.balance
1502
+ initial_balance = balance - total_profit
1503
+ profittable = "Yes" if balance > initial_balance else "No"
1504
+ stats2 = {"total_profit": total_profit, "profitability": profittable}
1505
+ else:
1506
+ stats2 = {"total_profit": 0, "profitability": 0}
1507
+ return (stats1, stats2)
1508
+
1509
+ def sharpe(self):
1510
+ """
1511
+ Calculate the Sharpe ratio of a returns stream
1512
+ based on a number of trading periods.
1513
+ The function assumes that the returns are the excess of
1514
+ those compared to a benchmark.
1515
+ """
1516
+ # Get total history
1517
+ df2 = self.get_trades_history()
1518
+ if df2 is None:
1519
+ return 0.0
1520
+ df = df2.iloc[1:]
1521
+ profit = df[["profit", "commission", "fee", "swap"]].sum(axis=1)
1522
+ returns = profit.pct_change(fill_method=None)
1523
+ periods = self.max_trade() * 252
1524
+ sharpe = create_sharpe_ratio(returns, periods=periods)
1525
+
1526
+ return round(sharpe, 3)
1527
+
1528
+ def days_end(self) -> bool:
1529
+ """Check if it is the end of the trading day."""
1530
+ current_hour = datetime.now().hour
1531
+ current_minute = datetime.now().minute
1532
+
1533
+ ending_hour = int(self.ending_time_hour)
1534
+ ending_minute = int(self.ending_time_minutes)
1535
+
1536
+ if current_hour > ending_hour or (
1537
+ current_hour == ending_hour and current_minute >= ending_minute
1538
+ ):
1539
+ return True
1540
+ else:
1541
+ return False
1542
+
1543
+ def trading_time(self):
1544
+ """Check if it is time to trade."""
1545
+ if (
1546
+ int(self.start_time_hour)
1547
+ < datetime.now().hour
1548
+ < int(self.finishing_time_hour)
1549
+ ):
1550
+ return True
1551
+ elif datetime.now().hour == int(self.start_time_hour):
1552
+ if datetime.now().minute >= int(self.start_time_minutes):
1553
+ return True
1554
+ elif datetime.now().hour == int(self.finishing_time_hour):
1555
+ if datetime.now().minute < int(self.finishing_time_minutes):
1556
+ return True
1557
+ return False
1558
+
1559
+ def sleep_time(self, weekend=False):
1560
+ if weekend:
1561
+ # claculate number of minute from the friday and to monday start
1562
+ friday_time = datetime.strptime(self.current_time(), "%H:%M")
1563
+ monday_time = datetime.strptime(self.start, "%H:%M")
1564
+ intra_day_diff = (monday_time - friday_time).total_seconds() // 60
1565
+ inter_day_diff = 3 * 24 * 60
1566
+ total_minutes = inter_day_diff + intra_day_diff
1567
+ return total_minutes
1568
+ else:
1569
+ # claculate number of minute from the end to the start
1570
+ start = datetime.strptime(self.start, "%H:%M")
1571
+ end = datetime.strptime(self.current_time(), "%H:%M")
1572
+ minutes = (end - start).total_seconds() // 60
1573
+ sleep_time = (24 * 60) - minutes
1574
+ return sleep_time
1575
+
1576
+ def current_datetime(self):
1577
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1578
+
1579
+ def current_time(self, seconds=False):
1580
+ if seconds:
1581
+ return datetime.now().strftime("%H:%M:%S")
1582
+ return datetime.now().strftime("%H:%M")
1583
+
1584
+
1585
+ def create_trade_instance(
1586
+ symbols: List[str],
1587
+ params: Dict[str, Any],
1588
+ daily_risk: Optional[Dict[str, float]] = None,
1589
+ max_risk: Optional[Dict[str, float]] = None,
1590
+ pchange_sl: Optional[Dict[str, float] | float] = None,
1591
+ **kwargs,
1592
+ ) -> Dict[str, Trade]:
1593
+ """
1594
+ Creates Trade instances for each symbol provided.
1595
+
1596
+ Args:
1597
+ symbols: A list of trading symbols (e.g., ['AAPL', 'MSFT']).
1598
+ params: A dictionary containing parameters for the Trade instance.
1599
+ daily_risk: A dictionary containing daily risk weight for each symbol.
1600
+ max_risk: A dictionary containing maximum risk weight for each symbol.
1601
+
1602
+ Returns:
1603
+ A dictionary where keys are symbols and values are corresponding Trade instances.
1604
+
1605
+ Raises:
1606
+ ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
1607
+
1608
+ Note:
1609
+ `daily_risk` and `max_risk` can be used to manage the risk of each symbol
1610
+ based on the importance of the symbol in the portfolio or strategy.
1611
+ """
1612
+ logger = params.get("logger", None)
1613
+ ids = params.get("expert_id", None)
1614
+ trade_instances = {}
1615
+ if not symbols:
1616
+ raise ValueError("The 'symbols' list cannot be empty.")
1617
+ if not params:
1618
+ raise ValueError("The 'params' dictionary cannot be empty.")
1619
+
1620
+ if daily_risk is not None:
1621
+ for symbol in symbols:
1622
+ if symbol not in daily_risk:
1623
+ raise ValueError(f"Missing daily risk weight for symbol '{symbol}'.")
1624
+ if max_risk is not None:
1625
+ for symbol in symbols:
1626
+ if symbol not in max_risk:
1627
+ raise ValueError(
1628
+ f"Missing maximum risk percentage for symbol '{symbol}'."
1629
+ )
1630
+ if pchange_sl is not None:
1631
+ if isinstance(pchange_sl, dict):
1632
+ for symbol in symbols:
1633
+ if symbol not in pchange_sl:
1634
+ raise ValueError(
1635
+ f"Missing percentage change for symbol '{symbol}'."
1636
+ )
1637
+ if isinstance(ids, dict):
1638
+ for symbol in symbols:
1639
+ if symbol not in ids:
1640
+ raise ValueError(f"Missing expert ID for symbol '{symbol}'.")
1641
+
1642
+ for symbol in symbols:
1643
+ try:
1644
+ params["symbol"] = symbol
1645
+ params["expert_id"] = (
1646
+ ids[symbol]
1647
+ if ids is not None and isinstance(ids, dict)
1648
+ else ids
1649
+ if ids is not None and isinstance(ids, (int, float))
1650
+ else params["expert_id"]
1651
+ if "expert_id" in params
1652
+ else None
1653
+ )
1654
+ params["pchange_sl"] = (
1655
+ pchange_sl[symbol]
1656
+ if pchange_sl is not None and isinstance(pchange_sl, dict)
1657
+ else pchange_sl
1658
+ if pchange_sl is not None and isinstance(pchange_sl, (int, float))
1659
+ else params["pchange_sl"]
1660
+ if "pchange_sl" in params
1661
+ else None
1662
+ )
1663
+ params["daily_risk"] = (
1664
+ daily_risk[symbol]
1665
+ if daily_risk is not None
1666
+ else params["daily_risk"]
1667
+ if "daily_risk" in params
1668
+ else None
1669
+ )
1670
+ params["max_risk"] = (
1671
+ max_risk[symbol]
1672
+ if max_risk is not None
1673
+ else params["max_risk"]
1674
+ if "max_risk" in params
1675
+ else 10.0
1676
+ )
1677
+ trade_instances[symbol] = Trade(**params)
1678
+ except Exception as e:
1679
+ logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
1680
+
1681
+ if len(trade_instances) != len(symbols):
1682
+ for symbol in symbols:
1683
+ if symbol not in trade_instances:
1684
+ if logger is not None and isinstance(logger, Logger):
1685
+ logger.error(f"Failed to create Trade instance for SYMBOL={symbol}")
1686
+ else:
1687
+ raise ValueError(
1688
+ f"Failed to create Trade instance for SYMBOL={symbol}"
1689
+ )
1690
+ return trade_instances