bbstrader 0.2.93__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 (35) hide show
  1. bbstrader/__ini__.py +20 -20
  2. bbstrader/__main__.py +50 -50
  3. bbstrader/btengine/__init__.py +54 -54
  4. bbstrader/btengine/scripts.py +157 -157
  5. bbstrader/compat.py +19 -19
  6. bbstrader/config.py +137 -137
  7. bbstrader/core/data.py +22 -22
  8. bbstrader/core/utils.py +146 -146
  9. bbstrader/metatrader/__init__.py +6 -6
  10. bbstrader/metatrader/account.py +1516 -1516
  11. bbstrader/metatrader/copier.py +750 -745
  12. bbstrader/metatrader/rates.py +584 -584
  13. bbstrader/metatrader/risk.py +749 -748
  14. bbstrader/metatrader/scripts.py +81 -81
  15. bbstrader/metatrader/trade.py +1836 -1836
  16. bbstrader/metatrader/utils.py +645 -645
  17. bbstrader/models/__init__.py +10 -10
  18. bbstrader/models/factors.py +312 -312
  19. bbstrader/models/ml.py +1272 -1272
  20. bbstrader/models/optimization.py +182 -182
  21. bbstrader/models/portfolio.py +223 -223
  22. bbstrader/models/risk.py +398 -398
  23. bbstrader/trading/__init__.py +11 -11
  24. bbstrader/trading/execution.py +846 -846
  25. bbstrader/trading/script.py +155 -155
  26. bbstrader/trading/scripts.py +69 -69
  27. bbstrader/trading/strategies.py +860 -860
  28. bbstrader/tseries.py +1842 -1842
  29. {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/LICENSE +21 -21
  30. {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/METADATA +188 -187
  31. bbstrader-0.2.94.dist-info/RECORD +44 -0
  32. bbstrader-0.2.93.dist-info/RECORD +0 -44
  33. {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/WHEEL +0 -0
  34. {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/entry_points.txt +0 -0
  35. {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/top_level.txt +0 -0
@@ -1,1836 +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
- 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
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