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

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