bbstrader 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bbstrader might be problematic. Click here for more details.

@@ -0,0 +1,1296 @@
1
+ import os
2
+ import csv
3
+ import time
4
+ import logging
5
+ import numpy as np
6
+ from datetime import datetime
7
+ import MetaTrader5 as Mt5
8
+ from typing import List, Tuple, Dict, Any, Optional, Literal
9
+ from bbstrader.metatrader.risk import RiskManagement
10
+ from bbstrader.metatrader.account import INIT_MSG
11
+ from bbstrader.metatrader.utils import (
12
+ TimeFrame, TradePosition, TickInfo,
13
+ raise_mt5_error, trade_retcode_message, config_logger
14
+ )
15
+
16
+ # Configure the logger
17
+ logger = config_logger('trade.log', console_log=True)
18
+
19
+ class Trade(RiskManagement):
20
+ """
21
+ Extends the `RiskManagement` class to include specific trading operations,
22
+ incorporating risk management strategies directly into trade executions.
23
+ It offers functionalities to execute trades while managing risks
24
+ according to the inherited RiskManagement parameters and methods.
25
+
26
+ Exemple:
27
+ >>> import time
28
+ >>> # Initialize the Trade class with parameters
29
+ >>> trade = Trade(
30
+ ... symbol="#AAPL", # Symbol to trade
31
+ ... expert_name="MyExpertAdvisor",# Name of the expert advisor
32
+ ... expert_id=12345, # Unique ID for the expert advisor
33
+ ... version="1.0", # Version of the expert advisor
34
+ ... target=5.0, # Daily profit target in percentage
35
+ ... start_time="09:00", # Start time for trading
36
+ ... finishing_time="17:00", # Time to stop opening new positions
37
+ ... ending_time="17:30", # Time to close any open positions
38
+ ... max_risk=2.0, # Maximum risk allowed on the account in percentage
39
+ ... daily_risk=1.0, # Daily risk allowed in percentage
40
+ ... max_trades=5, # Maximum number of trades per session
41
+ ... rr=2.0, # Risk-reward ratio
42
+ ... account_leverage=True, # Use account leverage in calculations
43
+ ... std_stop=True, # Use standard deviation for stop loss calculation
44
+ ... sl=20, # Stop loss in points (optional)
45
+ ... tp=30, # Take profit in points (optional)
46
+ ... be=10 # Break-even in points (optional)
47
+ ... )
48
+
49
+ >>> # Example to open a buy position
50
+ >>> trade.open_buy_position(mm=True, comment="Opening Buy Position")
51
+
52
+ >>> # Example to open a sell position
53
+ >>> trade.open_sell_position(mm=True, comment="Opening Sell Position")
54
+
55
+ >>> # Check current open positions
56
+ >>> opened_positions = trade.get_opened_positions
57
+ >>> if opened_positions is not None:
58
+ ... print(f"Current open positions: {opened_positions}")
59
+
60
+ >>> # Close all open positions at the end of the trading session
61
+ >>> if trade.days_end():
62
+ ... trade.close_all_positions(comment="Closing all positions at day's end")
63
+
64
+ >>> # Print trading session statistics
65
+ >>> trade.statistics(save=True, dir="my_trading_stats")
66
+
67
+ >>> # Sleep until the next trading session if needed (example usage)
68
+ >>> sleep_time = trade.sleep_time()
69
+ >>> print(f"Sleeping for {sleep_time} minutes until the next trading session.")
70
+ >>> time.sleep(sleep_time * 60)
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ symbol: str = 'EURUSD',
76
+ expert_name: str = 'bbstrader',
77
+ expert_id: int = 9818,
78
+ version: str = '1.0',
79
+ target: float = 5.0,
80
+ start_time: str = "1:00",
81
+ finishing_time: str = "23:00",
82
+ ending_time: str = "23:30",
83
+ verbose: Optional[bool] = None,
84
+ **kwargs,
85
+ ):
86
+ """
87
+ Initializes the Trade class with the specified parameters.
88
+
89
+ Args:
90
+ symbol (str): The `symbol` that the expert advisor will trade.
91
+ expert_name (str): The name of the `expert advisor`.
92
+ expert_id (int): The `unique ID` used to identify the expert advisor
93
+ or the strategy used on the symbol.
94
+ version (str): The `version` of the expert advisor.
95
+ target (float): `Trading period (day, week, month) profit target` in percentage
96
+ start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
97
+ finishing_time (str): The time after which no new position can be opened.
98
+ ending_time (str): The time after which any open position will be closed.
99
+ verbose (bool | None): If set to None (default), account summary and risk managment
100
+ parameters are printed in the terminal.
101
+
102
+ Inherits:
103
+ - max_risk
104
+ - max_trades
105
+ - rr
106
+ - daily_risk
107
+ - time_frame
108
+ - account_leverage
109
+ - std_stop
110
+ - pchange_sl
111
+ - sl
112
+ - tp
113
+ - be
114
+ See the RiskManagement class for more details on these parameters.
115
+ """
116
+ # Call the parent class constructor first
117
+ super().__init__(
118
+ symbol=symbol,
119
+ start_time=start_time,
120
+ finishing_time=finishing_time,
121
+ **kwargs, # Pass kwargs to the parent constructor
122
+ )
123
+
124
+ # Initialize Trade-specific attributes
125
+ self.symbol = symbol
126
+ self.expert_name = expert_name
127
+ self.expert_id = expert_id
128
+ self.version = version
129
+ self.target = target
130
+ self.verbose = verbose
131
+ self.start = start_time
132
+ self.end = ending_time
133
+ self.finishing = finishing_time
134
+ self.tf = kwargs.get("time_frame", 'D1')
135
+
136
+ self.lot = self.get_lot()
137
+ self.stop_loss = self.get_stop_loss()
138
+ self.take_profit = self.get_take_profit()
139
+ self.break_even_points = self.get_break_even()
140
+ self.deviation = self.get_deviation()
141
+
142
+ self.start_time_hour, self.start_time_minutes = self.start.split(":")
143
+ self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
144
+ ":")
145
+ self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
146
+
147
+ self.buy_positions = []
148
+ self.sell_positions = []
149
+ self.opened_positions = []
150
+ self.opened_orders = []
151
+ self.break_even_status = []
152
+
153
+ self.initialize()
154
+ self.select_symbol()
155
+ self.prepare_symbol()
156
+
157
+ if self.verbose:
158
+ self.summary()
159
+ time.sleep(1)
160
+ print()
161
+ self.risk_managment()
162
+ print(
163
+ f">>> Everything is OK, @{self.expert_name} is Running ....>>>\n")
164
+
165
+ def initialize(self):
166
+ """
167
+ Initializes the MetaTrader 5 (MT5) terminal for trading operations.
168
+ This method attempts to establish a connection with the MT5 terminal.
169
+ If the initial connection attempt fails due to a timeout, it retries after a specified delay.
170
+ Successful initialization is crucial for the execution of trading operations.
171
+
172
+ Raises:
173
+ MT5TerminalError: If initialization fails.
174
+ """
175
+ try:
176
+ if self.verbose:
177
+ print("\nInitializing the basics.")
178
+ if not Mt5.initialize():
179
+ raise_mt5_error(message=INIT_MSG)
180
+ if self.verbose:
181
+ print(
182
+ f"You are running the @{self.expert_name} Expert advisor,"
183
+ f" Version @{self.version}, on {self.symbol}."
184
+ )
185
+ except Exception as e:
186
+ logger.error(f"During initialization: {e}")
187
+
188
+ def select_symbol(self):
189
+ """
190
+ Selects the trading symbol in the MetaTrader 5 (MT5) terminal.
191
+ This method ensures that the specified trading
192
+ symbol is selected and visible in the MT5 terminal,
193
+ allowing subsequent trading operations such as opening and
194
+ closing positions on this symbol.
195
+
196
+ Raises:
197
+ MT5TerminalError: If symbole selection fails.
198
+ """
199
+ try:
200
+ if not Mt5.symbol_select(self.symbol, True):
201
+ raise_mt5_error(message=INIT_MSG)
202
+ except Exception as e:
203
+ logger.error(f"Selecting symbol '{self.symbol}': {e}")
204
+
205
+ def prepare_symbol(self):
206
+ """
207
+ Prepares the selected symbol for trading.
208
+ This method checks if the symbol is available and visible in the
209
+ MT5 terminal. If the symbol is not visible, it attempts to select the symbol again.
210
+ This step ensures that trading operations can be performed on the selected symbol without issues.
211
+
212
+ Raises:
213
+ MT5TerminalError: If the symbol cannot be made visible for trading operations.
214
+ """
215
+ try:
216
+ symbol_info = self.get_symbol_info(self.symbol)
217
+ if symbol_info is None:
218
+ raise_mt5_error(message=INIT_MSG)
219
+
220
+ if not symbol_info.visible:
221
+ raise_mt5_error(message=INIT_MSG)
222
+ if self.verbose:
223
+ print("Initialization successfully completed.")
224
+ except Exception as e:
225
+ logger.error(f"Preparing symbol '{self.symbol}': {e}")
226
+
227
+ def summary(self):
228
+ """Show a brief description about the trading program"""
229
+ print(
230
+ "╔═════════════════ Summary ════════════════════╗\n"
231
+ f"║ Expert Advisor Name @{self.expert_name}\n"
232
+ f"║ Expert Advisor Version @{self.version}\n"
233
+ f"║ Expert | Strategy ID {self.expert_id}\n"
234
+ f"║ Trading Symbol {self.symbol}\n"
235
+ f"║ Trading Time Frame {self.tf}\n"
236
+ f"║ Start Trading Time {self.start_time_hour}:{self.start_time_minutes}\n"
237
+ f"║ Finishing Trading Time {self.finishing_time_hour}:{self.finishing_time_minutes}\n"
238
+ f"║ Closing Position After {self.ending_time_hour}:{self.ending_time_minutes}\n"
239
+ "╚═══════════════════════════════════════════════╝\n"
240
+ )
241
+
242
+ def risk_managment(self):
243
+ """Show the risk management parameters"""
244
+
245
+ loss = self.currency_risk()["trade_loss"]
246
+ profit = self.currency_risk()["trade_profit"]
247
+ ok = "OK" if self.is_risk_ok() else "Not OK"
248
+ account_info = self.get_account_info()
249
+ _profit = round(self.get_stats()[1]["total_profit"], 2)
250
+ currency = account_info.currency
251
+ rates = self.get_currency_rates(self.symbol)
252
+ marging_currency = rates['mc']
253
+ print(
254
+ "╔═════════════════ Risk Management ═════════════════════╗\n"
255
+ f"║ Account Name {account_info.name}\n"
256
+ f"║ Account Number {account_info.login}\n"
257
+ f"║ Account Server {account_info.server}\n"
258
+ f"║ Account Balance {account_info.balance} {currency}\n"
259
+ f"║ Account Profit {_profit} {currency}\n"
260
+ f"║ Account Equity {account_info.equity} {currency}\n"
261
+ f"║ Account Leverage {self.get_leverage(True)}\n"
262
+ f"║ Account Margin {round(account_info.margin, 2)} {currency}\n"
263
+ f"║ Account Free Margin {account_info.margin_free} {currency}\n"
264
+ f"║ Maximum Drawdown {self.max_risk}%\n"
265
+ f"║ Risk Allowed {round((self.max_risk - self.risk_level()), 2)}%\n"
266
+ f"║ Volume {self.volume()} {marging_currency}\n"
267
+ f"║ Risk Per trade {-self.get_currency_risk()} {currency}\n"
268
+ f"║ Profit Expected Per trade {self.expected_profit()} {currency}\n"
269
+ f"║ Lot Size {self.lot} Lots\n"
270
+ f"║ Stop Loss {self.stop_loss} Points\n"
271
+ f"║ Loss Value Per Tick {round(loss, 5)} {currency}\n"
272
+ f"║ Take Profit {self.take_profit} Points\n"
273
+ f"║ Profit Value Per Tick {round(profit, 5)} {currency}\n"
274
+ f"║ Break Even {self.break_even_points} Points\n"
275
+ f"║ Deviation {self.deviation} Points\n"
276
+ f"║ Trading Time Interval {self.get_minutes()} Minutes\n"
277
+ f"║ Risk Level {ok}\n"
278
+ f"║ Maximum Trades {self.max_trade()}\n"
279
+ "╚══════════════════════════════════════════════════════╝\n"
280
+ )
281
+
282
+ def statistics(self, save=True, dir="stats"):
283
+ """
284
+ Print some statistics for the trading session and save to CSV if specified.
285
+
286
+ Args:
287
+ save (bool, optional): Whether to save the statistics to a CSV file.
288
+ dir (str, optional): The directory to save the CSV file.
289
+ """
290
+ stats, additional_stats = self.get_stats()
291
+
292
+ deals = stats["deals"]
293
+ wins = stats["win_trades"]
294
+ losses = stats["loss_trades"]
295
+ profit = round(stats["profit"], 2)
296
+ win_rate = stats["win_rate"]
297
+ total_fees = round(stats["total_fees"], 3)
298
+ average_fee = round(stats["average_fee"], 3)
299
+ profitability = additional_stats["profitability"]
300
+ currency = self.get_account_info().currency
301
+ net_profit = round((profit + total_fees), 2)
302
+ trade_risk = round(self.get_currency_risk() * -1, 2)
303
+ expected_profit = round((trade_risk * self.rr * -1), 2)
304
+
305
+ # Formatting the statistics output
306
+ stats_output = (
307
+ f"╔═══════════════ Session Statistics ═════════════╗\n"
308
+ f"║ Total Trades {deals}\n"
309
+ f"║ Winning Trades {wins}\n"
310
+ f"║ Losing Trades {losses}\n"
311
+ f"║ Session Profit {profit} {currency}\n"
312
+ f"║ Total Fees {total_fees} {currency}\n"
313
+ f"║ Average Fees {average_fee} {currency}\n"
314
+ f"║ Net Profit {net_profit} {currency}\n"
315
+ f"║ Risk per Trade {trade_risk} {currency}\n"
316
+ f"║ Expected Profit per Trade {self.expected_profit()} {currency}\n"
317
+ f"║ Risk Reward Ratio {self.rr}\n"
318
+ f"║ Win Rate {win_rate}%\n"
319
+ f"║ Sharpe Ratio {self.sharpe()}\n"
320
+ f"║ Trade Profitability {profitability}\n"
321
+ "╚═════════════════════════════════════════════════╝\n"
322
+ )
323
+
324
+ # Print the formatted statistics
325
+ if self.verbose:
326
+ print(stats_output)
327
+
328
+ # Save to CSV if specified
329
+ if save:
330
+ today_date = datetime.now().strftime("%Y-%m-%d")
331
+ # Create a dictionary with the statistics
332
+ statistics_dict = {
333
+ "Total Trades": deals,
334
+ "Winning Trades": wins,
335
+ "Losing Trades": losses,
336
+ "Session Profit": f"{profit} {currency}",
337
+ "Total Fees": f"{total_fees} {currency}",
338
+ "Average Fees": f"{average_fee} {currency}",
339
+ "Net Profit": f"{net_profit} {currency}",
340
+ "Risk per Trade": f"{trade_risk} {currency}",
341
+ "Expected Profit per Trade": f"{expected_profit} {currency}",
342
+ "Risk Reward Ratio": self.rr,
343
+ "Win Rate": f"{win_rate}%",
344
+ "Sharpe Ratio": self.sharpe(),
345
+ "Trade Profitability": profitability,
346
+ }
347
+ # Create the directory if it doesn't exist
348
+ os.makedirs(dir, exist_ok=True)
349
+ if '.' in self.symbol:
350
+ symbol = self.symbol.split('.')[0]
351
+ else:
352
+ symbol = self.symbol
353
+
354
+ filename = f"{symbol}_{today_date}_session.csv"
355
+ filepath = os.path.join(dir, filename)
356
+
357
+ # Updated code to write to CSV
358
+ with open(filepath, mode="w", newline='', encoding='utf-8') as csv_file:
359
+ writer = csv.writer(
360
+ csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL
361
+ )
362
+ writer.writerow(["Statistic", "Value"])
363
+ for stat, value in statistics_dict.items():
364
+ writer.writerow([stat, value])
365
+ logger.info(f"Session statistics saved to {filepath}")
366
+
367
+ def open_buy_position(
368
+ self,
369
+ action: Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT'] = 'BMKT',
370
+ price: Optional[float] = None,
371
+ mm: bool = True,
372
+ id: Optional[int] = None,
373
+ comment: Optional[str] = None
374
+ ):
375
+ """
376
+ Open a Buy positin
377
+
378
+ Args:
379
+ action (str): `'BMKT'` for Market orders or `'BLMT',
380
+ 'BSTP','BSTPLMT'` for pending orders
381
+ price (float): The price at which to open an order
382
+ id (int): The strategy id or expert Id
383
+ mm (bool): Weither to put stop loss and tp or not
384
+ comment (str): The comment for the opening position
385
+ """
386
+ Id = id if id is not None else self.expert_id
387
+ point = self.get_symbol_info(self.symbol).point
388
+ if action != 'BMKT':
389
+ assert price is not None, \
390
+ "You need to set a price for pending orders"
391
+ _price = price
392
+ else:
393
+ _price = self.get_tick_info(self.symbol).ask
394
+ digits = self.get_symbol_info(self.symbol).digits
395
+
396
+ lot = self.get_lot()
397
+ stop_loss = self.get_stop_loss()
398
+ take_profit = self.get_take_profit()
399
+ deviation = self.get_deviation()
400
+ request = {
401
+ "action": Mt5.TRADE_ACTION_DEAL,
402
+ "symbol": self.symbol,
403
+ "volume": float(lot),
404
+ "type": Mt5.ORDER_TYPE_BUY,
405
+ "price": _price,
406
+ "deviation": deviation,
407
+ "magic": Id,
408
+ "comment": f"@{self.expert_name}" if comment is None else comment,
409
+ "type_time": Mt5.ORDER_TIME_GTC,
410
+ "type_filling": Mt5.ORDER_FILLING_FOK,
411
+ }
412
+ if mm:
413
+ request['sl'] = (_price - stop_loss * point)
414
+ request['tp'] = (_price + take_profit * point)
415
+ if action != 'BMKT':
416
+ request["action"] = Mt5.TRADE_ACTION_PENDING
417
+ request["type"] = self._order_type()[action][0]
418
+
419
+ self.break_even(comment)
420
+ if self.check(comment):
421
+ self.request_result(_price, request, action),
422
+
423
+ def _order_type(self):
424
+ type = {
425
+ 'BMKT': (Mt5.ORDER_TYPE_BUY, 'BUY'),
426
+ 'SMKT': (Mt5.ORDER_TYPE_BUY, 'SELL'),
427
+ 'BLMT': (Mt5.ORDER_TYPE_BUY_LIMIT, 'BUY_LIMIT'),
428
+ 'SLMT': (Mt5.ORDER_TYPE_SELL_LIMIT, 'SELL_LIMIT'),
429
+ 'BSTP': (Mt5.ORDER_TYPE_BUY_STOP, 'BUY_STOP'),
430
+ 'SSTP': (Mt5.ORDER_TYPE_SELL_STOP, 'SELL_STOP'),
431
+ 'BSTPLMT': (Mt5.ORDER_TYPE_BUY_STOP_LIMIT, 'BUY_STOP_LIMIT'),
432
+ 'SSTPLMT': (Mt5.ORDER_TYPE_SELL_STOP_LIMIT, 'SELL_STOP_LIMIT')
433
+ }
434
+ return type
435
+
436
+ def open_sell_position(
437
+ self,
438
+ action: Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT'] = 'SMKT',
439
+ price: Optional[float] = None,
440
+ mm: bool = True,
441
+ id: Optional[int] = None,
442
+ comment: Optional[str] = None
443
+ ):
444
+ """
445
+ Open a sell positin
446
+
447
+ Args:
448
+ action (str): `'SMKT'` for Market orders
449
+ or `'SLMT', 'SSTP','SSTPLMT'` for pending orders
450
+ price (float): The price at which to open an order
451
+ id (int): The strategy id or expert Id
452
+ mm (bool): Weither to put stop loss and tp or not
453
+ comment (str): The comment for the closing position
454
+ """
455
+ Id = id if id is not None else self.expert_id
456
+ point = self.get_symbol_info(self.symbol).point
457
+ if action != 'SMKT':
458
+ assert price is not None, \
459
+ "You need to set a price for pending orders"
460
+ _price = price
461
+ else:
462
+ _price = self.get_tick_info(self.symbol).bid
463
+ digits = self.get_symbol_info(self.symbol).digits
464
+
465
+ lot = self.get_lot()
466
+ stop_loss = self.get_stop_loss()
467
+ take_profit = self.get_take_profit()
468
+ deviation = self.get_deviation()
469
+ request = {
470
+ "action": Mt5.TRADE_ACTION_DEAL,
471
+ "symbol": self.symbol,
472
+ "volume": float(lot),
473
+ "type": Mt5.ORDER_TYPE_SELL,
474
+ "price": _price,
475
+ "deviation": deviation,
476
+ "magic": Id,
477
+ "comment": f"@{self.expert_name}" if comment is None else comment,
478
+ "type_time": Mt5.ORDER_TIME_GTC,
479
+ "type_filling": Mt5.ORDER_FILLING_FOK,
480
+ }
481
+ if mm:
482
+ request["sl"] = (_price + stop_loss * point)
483
+ request["tp"] = (_price - take_profit * point)
484
+ if action != 'SMKT':
485
+ request["action"] = Mt5.TRADE_ACTION_PENDING
486
+ request["type"] = self._order_type()[action][0]
487
+
488
+ self.break_even(comment)
489
+ if self.check(comment):
490
+ self.request_result(_price, request, action)
491
+
492
+ def _risk_free(self):
493
+ max_trade = self.max_trade()
494
+ loss_trades = self.get_stats()[0]['loss_trades']
495
+ if loss_trades >= max_trade:
496
+ return False
497
+ return True
498
+
499
+ def check(self, comment):
500
+ """
501
+ Verify if all conditions for taking a position are valide,
502
+ These conditions are based on the Maximum risk ,daily risk,
503
+ the starting, the finishing, and ending trading time.
504
+
505
+ Args:
506
+ comment (str): The comment for the closing position
507
+ """
508
+ if self.days_end():
509
+ return False
510
+ elif not self.trading_time():
511
+ logger.info(f"Not Trading time, SYMBOL={self.symbol}")
512
+ return False
513
+ elif not self.is_risk_ok():
514
+ logger.error(f"Risk not allowed, SYMBOL={self.symbol}")
515
+ self._check(comment)
516
+ return False
517
+ elif not self._risk_free():
518
+ logger.error(f"Maximum trades Reached, SYMBOL={self.symbol}")
519
+ self._check(comment)
520
+ return False
521
+ elif self.profit_target():
522
+ self._check(f'Profit target Reached !!! SYMBOL={self.symbol}')
523
+ return True
524
+
525
+ def _check(self, txt: str = ""):
526
+ if self.positive_profit() or self.get_current_open_positions() is None:
527
+ self.close_positions(position_type='all')
528
+ logger.info(txt)
529
+ time.sleep(5)
530
+ self.statistics(save=True)
531
+
532
+ def request_result(
533
+ self,
534
+ price: float,
535
+ request: Dict[str, Any],
536
+ type: Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
537
+ 'SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
538
+ ):
539
+ """
540
+ Check if a trading order has been sent correctly
541
+
542
+ Args:
543
+ price (float): Price for opening the position
544
+ request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
545
+ all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py
546
+
547
+ type (str): The type of the order
548
+ `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
549
+ """
550
+ # Send a trading request
551
+ # Check the execution result
552
+ pos = self._order_type()[type][1]
553
+ addtionnal = f", SYMBOL={self.symbol}"
554
+ try:
555
+ check_result = self.check_order(request)
556
+ result = self.send_order(request)
557
+ except Exception as e:
558
+ print(f"{self.get_current_time()} -", end=' ')
559
+ trade_retcode_message(
560
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}")
561
+ if result.retcode != Mt5.TRADE_RETCODE_DONE:
562
+ msg = trade_retcode_message(result.retcode)
563
+ logger.error(
564
+ f"Trade Order Request, RETCODE={result.retcode}: {msg}{addtionnal}")
565
+ if result.retcode in [
566
+ Mt5.TRADE_RETCODE_CONNECTION, Mt5.TRADE_RETCODE_TIMEOUT]:
567
+ tries = 0
568
+ while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
569
+ time.sleep(1)
570
+ try:
571
+ check_result = self.check_order(request)
572
+ result = self.send_order(request)
573
+ except Exception as e:
574
+ print(f"{self.get_current_time()} -", end=' ')
575
+ trade_retcode_message(
576
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}")
577
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
578
+ break
579
+ tries += 1
580
+ # Print the result
581
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
582
+ msg = trade_retcode_message(result.retcode)
583
+ logger.info(f"Trade Order {msg}{addtionnal}")
584
+ if type != "BMKT" or type != "SMKT":
585
+ self.opened_orders.append(result.order)
586
+ long_msg = (
587
+ f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{price}, "
588
+ f"Lot(s): {result.volume}, Sl: {self.get_stop_loss()}, "
589
+ f"Tp: {self.get_take_profit()}"
590
+ )
591
+ logger.info(long_msg)
592
+ time.sleep(0.1)
593
+ if type == "BMKT" or type == "SMKT":
594
+ self.opened_positions.append(result.order)
595
+ positions = self.get_positions(symbol=self.symbol)
596
+ for position in positions:
597
+ if position.ticket == result.order:
598
+ if position.type == 0:
599
+ order_type = "BUY"
600
+ self.buy_positions.append(position.ticket)
601
+ else:
602
+ order_type = "SELL"
603
+ self.sell_positions.append(position.ticket)
604
+ profit = round(self.get_account_info().profit, 5)
605
+ order_info = (
606
+ f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open,5)}, "
607
+ f"Sl: @{position.sl} Tp: @{position.tp}"
608
+ )
609
+ logger.info(order_info)
610
+ pos_info = (
611
+ f"3. [OPEN POSITIONS ON {self.symbol} = {len(positions)}, ACCOUNT OPEN PnL = {profit} "
612
+ f"{self.get_account_info().currency}]\n"
613
+ )
614
+ logger.info(pos_info)
615
+
616
+ def open_position(
617
+ self,
618
+ action: Literal[
619
+ 'BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
620
+ 'SMKT', 'SLMT', 'SSTP', 'SSTPLMT'],
621
+ buy: bool = False,
622
+ sell: bool = False,
623
+ price: Optional[float] = None,
624
+ id: Optional[int] = None,
625
+ mm: bool = True,
626
+ comment: Optional[str] = None
627
+ ):
628
+ """
629
+ Open a buy or sell position.
630
+
631
+ Args:
632
+ action (str): (`'BMKT'`, `'SMKT'`) for Market orders
633
+ or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
634
+ buy (bool): A boolean True or False
635
+ sell (bool): A boolean True or False
636
+ id (int): The strategy id or expert Id
637
+ mm (bool): Weither to put stop loss and tp or not
638
+ comment (str): The comment for the closing position
639
+ """
640
+ if buy:
641
+ self.open_buy_position(
642
+ action=action, price=price, id=id, mm=mm, comment=comment)
643
+ if sell:
644
+ self.open_sell_position(
645
+ action=action, price=price, id=id, mm=mm, comment=comment)
646
+
647
+ @property
648
+ def get_opened_orders(self):
649
+ """ Return all opened order's tickets"""
650
+ if len(self.opened_orders) != 0:
651
+ return self.opened_orders
652
+ return None
653
+
654
+ @property
655
+ def get_opened_positions(self):
656
+ """Return all opened position's tickets"""
657
+ if len(self.opened_positions) != 0:
658
+ return self.opened_positions
659
+ return None
660
+
661
+ @property
662
+ def get_buy_positions(self):
663
+ """Return all buy opened position's tickets"""
664
+ if len(self.buy_positions) != 0:
665
+ return self.buy_positions
666
+ return None
667
+
668
+ @property
669
+ def get_sell_positions(self):
670
+ """Return all sell opened position's tickets"""
671
+ if len(self.sell_positions) != 0:
672
+ return self.sell_positions
673
+ return None
674
+
675
+ @property
676
+ def get_be_positions(self):
677
+ """Return All positon's tickets
678
+ for which a break even has been set"""
679
+ if len(self.break_even_status) != 0:
680
+ return self.break_even_status
681
+ return None
682
+
683
+ def get_filtered_tickets(self,
684
+ id: Optional[int] = None,
685
+ filter_type: Optional[str] = None,
686
+ th=None
687
+ ) -> List[int] | None:
688
+ """
689
+ Get tickets for positions or orders based on filters.
690
+
691
+ Args:
692
+ id (int): The strategy id or expert Id
693
+ filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'win_trades')
694
+ - `orders` are current open orders
695
+ - `positions` are all current open positions
696
+ - `buys` and `sells` are current buy or sell open positions
697
+ - `win_trades` are current open position that have a profit greater than a threshold
698
+ th (bool): the minimum treshold for winning position
699
+ (only relevant when filter_type is 'win_trades')
700
+
701
+ Returns:
702
+ List[int] | None: A list of filtered tickets
703
+ or None if no tickets match the criteria.
704
+ """
705
+ Id = id if id is not None else self.expert_id
706
+
707
+ if filter_type == 'orders':
708
+ items = self.get_orders(symbol=self.symbol)
709
+ else:
710
+ items = self.get_positions(symbol=self.symbol)
711
+
712
+ filtered_tickets = []
713
+
714
+ if items is not None:
715
+ for item in items:
716
+ if item.magic == Id:
717
+ if filter_type == 'buys' and item.type != 0:
718
+ continue
719
+ if filter_type == 'sells' and item.type != 1:
720
+ continue
721
+ if filter_type == 'win_trades' and not self.win_trade(item, th=th):
722
+ continue
723
+ filtered_tickets.append(item.ticket)
724
+ return filtered_tickets if filtered_tickets else None
725
+ return None
726
+
727
+ def get_current_open_orders(self, id: Optional[int] = None) -> List[int] | None:
728
+ return self.get_filtered_tickets(id=id, filter_type='orders')
729
+
730
+ def get_current_open_positions(self, id: Optional[int] = None) -> List[int] | None:
731
+ return self.get_filtered_tickets(id=id, filter_type='positions')
732
+
733
+ def get_current_win_trades(self, id: Optional[int] = None, th=None) -> List[int] | None:
734
+ return self.get_filtered_tickets(id=id, filter_type='win_trades', th=th)
735
+
736
+ def get_current_buys(self, id: Optional[int] = None) -> List[int] | None:
737
+ return self.get_filtered_tickets(id=id, filter_type='buys')
738
+
739
+ def get_current_sells(self, id: Optional[int] = None) -> List[int] | None:
740
+ return self.get_filtered_tickets(id=id, filter_type='sells')
741
+
742
+ def positive_profit(self, th: Optional[float] = None
743
+ ) -> bool:
744
+ """
745
+ Check is the total profit on current open positions
746
+ Is greater than a minimum profit express as percentage
747
+ of the profit target.
748
+
749
+ Args:
750
+ th (float): The minimum profit target on current positions
751
+ """
752
+ positions = self.get_current_open_positions()
753
+ profit = 0.0
754
+ balance = self.get_account_info().balance
755
+ target = round((balance * self.target)/100, 2)
756
+ if positions is not None:
757
+ for position in positions:
758
+ time.sleep(0.1)
759
+ history = self.get_positions(
760
+ ticket=position
761
+ )
762
+ profit += history[0].profit
763
+ fees = self.get_stats()[0]["average_fee"] * len(positions)
764
+ current_profit = profit + fees
765
+ th_profit = (target*th)/100 if th is not None else (target*0.01)
766
+ if current_profit > th_profit:
767
+ return True
768
+ return False
769
+
770
+ def break_even(self, id: Optional[int] = None):
771
+ """
772
+ Checks if it's time to put the break even,
773
+ if so , it will sets the break even ,and if the break even was already set,
774
+ it checks if the price has moved in favorable direction,
775
+ if so , it set the new break even.
776
+
777
+ Args:
778
+ id (int): The strategy Id or Expert Id
779
+ """
780
+ time.sleep(0.1)
781
+ Id = id if id is not None else self.expert_id
782
+ positions = self.get_positions(symbol=self.symbol)
783
+ be = self.get_break_even()
784
+ if positions is not None:
785
+ for position in positions:
786
+ if position.magic == Id:
787
+ size = self.get_symbol_info(self.symbol).trade_tick_size
788
+ value = self.get_symbol_info(self.symbol).trade_tick_value
789
+ point = self.get_symbol_info(self.symbol).point
790
+ digits = self.get_symbol_info(self.symbol).digits
791
+ points = position.profit * (size / value / position.volume)
792
+ break_even = float(points/point) >= be
793
+ if break_even:
794
+ # Check if break-even has already been set for this position
795
+ if position.ticket not in self.break_even_status:
796
+ self.set_break_even(position, be)
797
+ self.break_even_status.append(position.ticket)
798
+ else:
799
+ # Check if the price has moved favorably
800
+ new_be = be * 0.50
801
+ favorable_move = (
802
+ (position.type == 0 and (
803
+ (position.price_current - position.sl) / point) > new_be)
804
+ or
805
+ (position.type == 1 and (
806
+ (position.sl - position.price_current) / point) > new_be)
807
+ )
808
+ if favorable_move:
809
+ # Calculate the new break-even level and price
810
+ if position.type == 0:
811
+ new_level = round(
812
+ position.sl + (new_be * point), digits)
813
+ new_price = round(
814
+ position.sl + ((0.25 * be) * point), digits)
815
+ else:
816
+ new_level = round(
817
+ position.sl - (new_be * point), digits)
818
+ new_price = round(
819
+ position.sl - ((0.25 * be) * point), digits)
820
+ self.set_break_even(
821
+ position, be, price=new_price, level=new_level
822
+ )
823
+
824
+ def set_break_even(self,
825
+ position: TradePosition,
826
+ be: int,
827
+ price: Optional[float] = None,
828
+ level: Optional[float] = None):
829
+ """
830
+ Sets the break-even level for a given trading position.
831
+
832
+ Args:
833
+ position (TradePosition):
834
+ The trading position for which the break-even is to be set
835
+ This is the value return by `mt5.positions_get()`
836
+ be (int): The break-even level in points.
837
+ level (float): The break-even level in price
838
+ if set to None , it will be calated automaticaly.
839
+ price (float): The break-even price
840
+ if set to None , it will be calated automaticaly.
841
+ """
842
+ point = self.get_symbol_info(self.symbol).point
843
+ digits = self.get_symbol_info(self.symbol).digits
844
+ spread = self.get_symbol_info(self.symbol).spread
845
+ fees = self.get_stats()[0]["average_fee"] * -1
846
+ risk = self.currency_risk()["trade_profit"]
847
+ fees_points = round((fees / risk), 3)
848
+ # If Buy
849
+ if position.type == 0 and position.price_current > position.price_open:
850
+ # Calculate the break-even level and price
851
+ break_even_level = position.price_open + (be * point)
852
+ break_even_price = position.price_open + \
853
+ ((fees_points + spread) * point)
854
+ _price = break_even_price if price is None else price
855
+ _level = break_even_level if level is None else level
856
+
857
+ if self.get_tick_info(self.symbol).ask > _level:
858
+ # Set the stop loss to break even
859
+ request = {
860
+ "action": Mt5.TRADE_ACTION_SLTP,
861
+ "type": Mt5.ORDER_TYPE_SELL_STOP,
862
+ "position": position.ticket,
863
+ "sl": round(_price, digits),
864
+ "tp": position.tp
865
+ }
866
+ self._break_even_request(
867
+ position.ticket, round(_price, digits), request)
868
+ # If Sell
869
+ elif position.type == 1 and position.price_current < position.price_open:
870
+ break_even_level = position.price_open - (be * point)
871
+ break_even_price = position.price_open - \
872
+ ((fees_points + spread) * point)
873
+ _price = break_even_price if price is None else price
874
+ _level = break_even_level if level is None else level
875
+
876
+ if self.get_tick_info(self.symbol).bid < _level:
877
+ # Set the stop loss to break even
878
+ request = {
879
+ "action": Mt5.TRADE_ACTION_SLTP,
880
+ "type": Mt5.ORDER_TYPE_BUY_STOP,
881
+ "position": position.ticket,
882
+ "sl": round(_price, digits),
883
+ "tp": position.tp
884
+ }
885
+ self._break_even_request(
886
+ position.ticket, round(_price, digits), request)
887
+
888
+ def _break_even_request(self, tiket, price, request):
889
+ """
890
+ Send a request to set the stop loss to break even for a given trading position.
891
+
892
+ Args:
893
+ tiket (int): The ticket number of the trading position.
894
+ price (float): The price at which the stop loss is to be set.
895
+ request (dict): The request to set the stop loss to break even.
896
+ """
897
+ addtionnal = f", SYMBOL={self.symbol}"
898
+ time.sleep(0.1)
899
+ try:
900
+ check_result = self.check_order(request)
901
+ result = self.send_order(request)
902
+ except Exception as e:
903
+ print(f"{self.get_current_time()} -", end=' ')
904
+ trade_retcode_message(
905
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}")
906
+ if result.retcode != Mt5.TRADE_RETCODE_DONE:
907
+ msg = trade_retcode_message(result.retcode)
908
+ logger.error(
909
+ f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}")
910
+ tries = 0
911
+ while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 10:
912
+ if result.retcode == Mt5.TRADE_RETCODE_NO_CHANGES:
913
+ break
914
+ else:
915
+ time.sleep(1)
916
+ try:
917
+ check_result = self.check_order(request)
918
+ result = self.send_order(request)
919
+ except Exception as e:
920
+ print(f"{self.get_current_time()} -", end=' ')
921
+ trade_retcode_message(
922
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}")
923
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
924
+ break
925
+ tries += 1
926
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
927
+ msg = trade_retcode_message(result.retcode)
928
+ logger.info(f"Break-Even Order {msg}{addtionnal}")
929
+ info = (
930
+ f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{price}"
931
+ )
932
+ logger.info(info)
933
+ self.break_even_status.append(tiket)
934
+
935
+ def win_trade(self,
936
+ position: TradePosition,
937
+ th: Optional[int] = None) -> bool:
938
+ """
939
+ Check if a positon is wining or looing
940
+ wen it is closed before be level , tp or sl.
941
+
942
+ Args:
943
+ th (int): The minimum profit for a position in point
944
+ """
945
+ size = self.get_symbol_info(self.symbol).trade_tick_size
946
+ value = self.get_symbol_info(self.symbol).trade_tick_value
947
+ points = position.profit * (size / value / position.volume)
948
+
949
+ spread = self.get_symbol_info(self.symbol).spread
950
+ point = self.get_symbol_info(self.symbol).point
951
+ fees = self.get_stats()[0]["average_fee"] * -1
952
+ risk = self.currency_risk()["trade_profit"]
953
+ min_be = round((fees / risk)) + 2
954
+ be = self.get_break_even()
955
+ if th is not None:
956
+ win_be = th
957
+ else:
958
+ win_be = max(min_be, round((0.1 * be)))
959
+ win_trade = float(points/point) >= be
960
+ # Check if the positon is in profit
961
+ if win_trade:
962
+ # Check if break-even has already been set for this position
963
+ if position.ticket not in self.break_even_status:
964
+ return True
965
+ return False
966
+
967
+ def profit_target(self):
968
+ fee = 0.0
969
+ swap = 0.0
970
+ commission = 0.0
971
+ profit = 0.0
972
+ balance = self.get_account_info().balance
973
+ target = round((balance * self.target)/100, 2)
974
+ if len(self.opened_positions) != 0:
975
+ for position in self.opened_positions:
976
+ time.sleep(0.1)
977
+ # This return two TradeDeal Object,
978
+ # The first one is the one the opening order
979
+ # The second is the closing order
980
+ history = self.get_trades_history(
981
+ position=position, to_df=False
982
+ )
983
+ if len(history) == 2:
984
+ profit += history[1].profit
985
+ commission += history[0].commission
986
+ swap += history[0].swap
987
+ fee += history[0].fee
988
+ current_profit = profit + commission + fee + swap
989
+ if current_profit >= target:
990
+ return True
991
+ return False
992
+
993
+ def close_position(self,
994
+ ticket: int,
995
+ id: Optional[int] = None,
996
+ pct: Optional[float] = 1.0,
997
+ comment: Optional[str] = None
998
+ ) -> bool:
999
+ """
1000
+ Close an open position by it ticket
1001
+
1002
+ Args:
1003
+ ticket (int): Positon ticket to close (e.g TradePosition.ticket)
1004
+ id (int): The unique ID of the Expert or Strategy
1005
+ pct (float): Percentage of the position to close
1006
+ comment (str): Comment for the closing position
1007
+
1008
+ Returns:
1009
+ - True if position closed, False otherwise
1010
+ """
1011
+ # get all Actives positions
1012
+ time.sleep(0.1)
1013
+ Id = id if id is not None else self.expert_id
1014
+ positions = self.get_positions(symbol=self.symbol)
1015
+ buy_price = self.get_tick_info(self.symbol).ask
1016
+ sell_price = self.get_tick_info(self.symbol).bid
1017
+ digits = self.get_symbol_info(self.symbol).digits
1018
+ deviation = self.get_deviation()
1019
+ if positions is not None:
1020
+ for position in positions:
1021
+ if (position.ticket == ticket
1022
+ and position.magic == Id
1023
+ ):
1024
+ buy = position.type == 0
1025
+ sell = position.type == 1
1026
+ request = {
1027
+ "action": Mt5.TRADE_ACTION_DEAL,
1028
+ "symbol": self.symbol,
1029
+ "volume": (position.volume*pct),
1030
+ "type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
1031
+ "position": ticket,
1032
+ "price": sell_price if buy else buy_price,
1033
+ "deviation": deviation,
1034
+ "magic": Id,
1035
+ "comment": f"@{self.expert_name}" if comment is None else comment,
1036
+ "type_time": Mt5.ORDER_TIME_GTC,
1037
+ "type_filling": Mt5.ORDER_FILLING_FOK,
1038
+ }
1039
+ addtionnal = f", SYMBOL={self.symbol}"
1040
+ try:
1041
+ check_result = self.check_order(request)
1042
+ result = self.send_order(request)
1043
+ except Exception as e:
1044
+ print(f"{self.get_current_time()} -", end=' ')
1045
+ trade_retcode_message(
1046
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1047
+ if result.retcode != Mt5.TRADE_RETCODE_DONE:
1048
+ msg = trade_retcode_message(result.retcode)
1049
+ logger.error(
1050
+ f"Closing Order Request, Position: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}")
1051
+ tries = 0
1052
+ while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
1053
+ time.sleep(1)
1054
+ try:
1055
+ check_result = self.check_order(request)
1056
+ result = self.send_order(request)
1057
+ except Exception as e:
1058
+ print(f"{self.get_current_time()} -", end=' ')
1059
+ trade_retcode_message(
1060
+ result.retcode, display=True, add_msg=f"{e}{addtionnal}")
1061
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
1062
+ break
1063
+ tries += 1
1064
+ if result.retcode == Mt5.TRADE_RETCODE_DONE:
1065
+ msg = trade_retcode_message(result.retcode)
1066
+ logger.info(
1067
+ f"Closing Order {msg}{addtionnal}")
1068
+ info = (
1069
+ f"Position #{ticket} closed, Symbol: {self.symbol}, Price: @{request['price']}")
1070
+ logger.info(info)
1071
+ return True
1072
+ else:
1073
+ return False
1074
+
1075
+ def close_positions(
1076
+ self,
1077
+ position_type: Literal["all", "buy", "sell"] = "all",
1078
+ id: Optional[int] = None,
1079
+ comment: Optional[str] = None):
1080
+ """
1081
+ Args:
1082
+ position_type (str): Type of positions to close ("all", "buy", "sell")
1083
+ id (int): The unique ID of the Expert or Strategy
1084
+ comment (str): Comment for the closing position
1085
+ """
1086
+ if position_type == "all":
1087
+ positions = self.get_positions(symbol=self.symbol)
1088
+ elif position_type == "buy":
1089
+ positions = self.get_current_buys()
1090
+ elif position_type == "sell":
1091
+ positions = self.get_current_sells()
1092
+ else:
1093
+ logger.error(f"Invalid position type: {position_type}")
1094
+ return
1095
+
1096
+ if positions is not None:
1097
+ if position_type == 'all':
1098
+ pos_type = ""
1099
+ tickets = [position.ticket for position in positions]
1100
+ else:
1101
+ tickets = positions
1102
+ pos_type = position_type
1103
+ else:
1104
+ tickets = []
1105
+
1106
+ if len(tickets) != 0:
1107
+ for ticket in tickets.copy():
1108
+ if self.close_position(ticket, id=id, comment=comment):
1109
+ tickets.remove(ticket)
1110
+ time.sleep(1)
1111
+
1112
+ if len(tickets) == 0:
1113
+ logger.info(
1114
+ f"ALL {position_type.upper()} Positions closed, SYMBOL={self.symbol}.")
1115
+ else:
1116
+ logger.info(
1117
+ f"{len(tickets)} {position_type.upper()} Positions not closed, SYMBOL={self.symbol}")
1118
+ else:
1119
+ logger.info(
1120
+ f"No {position_type.upper()} Positions to close, SYMBOL={self.symbol}.")
1121
+
1122
+ def get_stats(self) -> Tuple[Dict[str, Any]]:
1123
+ """
1124
+ get some stats about the trading day and trading history
1125
+
1126
+ :return: tuple[Dict[str, Any]]
1127
+ """
1128
+ # get history of deals for one trading session
1129
+ profit = 0.0
1130
+ total_fees = 0.0
1131
+ loss_trades = 0
1132
+ win_trades = 0
1133
+ balance = self.get_account_info().balance
1134
+ target = round((balance * self.target)/100)
1135
+ deals = len(self.opened_positions)
1136
+ if deals != 0:
1137
+ for position in self.opened_positions:
1138
+ time.sleep(0.1)
1139
+ history = self.get_trades_history(
1140
+ position=position, to_df=False
1141
+ )
1142
+ if len(history) == 2:
1143
+ result = history[1].profit
1144
+ comm = history[0].commission
1145
+ swap = history[0].swap
1146
+ fee = history[0].fee
1147
+ if (result + comm + swap + fee) <= 0:
1148
+ loss_trades += 1
1149
+ else:
1150
+ win_trades += 1
1151
+ profit += result
1152
+ total_fees += (comm + swap + fee)
1153
+ average_fee = total_fees / deals
1154
+ win_rate = round((win_trades / deals) * 100, 2)
1155
+ stats1 = {
1156
+ "deals": deals,
1157
+ "profit": profit,
1158
+ "win_trades": win_trades,
1159
+ "loss_trades": loss_trades,
1160
+ "total_fees": total_fees,
1161
+ "average_fee": average_fee,
1162
+ "win_rate": win_rate
1163
+ }
1164
+ else:
1165
+ stats1 = {
1166
+ "deals": 0,
1167
+ "profit": 0,
1168
+ "win_trades": 0,
1169
+ "loss_trades": 0,
1170
+ "total_fees": 0,
1171
+ "average_fee": 0,
1172
+ "win_rate": 0,
1173
+ }
1174
+
1175
+ # Get total stats
1176
+ df = self.get_trades_history()
1177
+ if df is not None:
1178
+ df2 = df.iloc[1:]
1179
+ profit = df2["profit"].sum()
1180
+ commisions = df2["commission"].sum()
1181
+ _fees = df2["fee"].sum()
1182
+ _swap = df2["swap"].sum()
1183
+ total_profit = commisions + _fees + _swap + profit
1184
+ account_info = self.get_account_info()
1185
+ balance = account_info.balance
1186
+ initial_balance = balance - total_profit
1187
+ profittable = "Yes" if balance > initial_balance else "No"
1188
+ stats2 = {
1189
+ "total_profit": total_profit,
1190
+ "profitability": profittable
1191
+ }
1192
+ else:
1193
+ stats2 = {
1194
+ "total_profit": 0,
1195
+ "profitability": 0
1196
+ }
1197
+ return (stats1, stats2)
1198
+
1199
+ def sharpe(self):
1200
+ """
1201
+ Calculate the Sharpe ratio of a returns stream
1202
+ based on a number of trading periods.
1203
+ The function assumes that the returns are the excess of
1204
+ those compared to a benchmark.
1205
+ """
1206
+ # Get total history
1207
+ df2 = self.get_trades_history()
1208
+ if df2 is None:
1209
+ return 0.0
1210
+ df = df2.iloc[1:]
1211
+ profit = df[["profit", "commission", "fee", "swap"]].sum(axis=1)
1212
+ returns = profit.values
1213
+ returns = np.diff(returns, prepend=0.0)
1214
+ N = self.max_trade() * 252
1215
+ sharp = np.sqrt(N) * np.mean(returns) / (np.std(returns) + 1e-10)
1216
+
1217
+ return round(sharp, 3)
1218
+
1219
+ def days_end(self) -> bool:
1220
+ """Check if it is the end of the trading day."""
1221
+ current_hour = datetime.now().hour
1222
+ current_minute = datetime.now().minute
1223
+
1224
+ ending_hour = int(self.ending_time_hour)
1225
+ ending_minute = int(self.ending_time_minutes)
1226
+
1227
+ if current_hour > ending_hour or (
1228
+ current_hour == ending_hour and current_minute >= ending_minute
1229
+ ):
1230
+ return True
1231
+ else:
1232
+ return False
1233
+
1234
+ def trading_time(self):
1235
+ """Check if it is time to trade."""
1236
+ if (
1237
+ int(self.start_time_hour)
1238
+ < datetime.now().hour
1239
+ < int(self.finishing_time_hour)
1240
+ ):
1241
+ return True
1242
+ elif datetime.now().hour == int(self.start_time_hour):
1243
+ if datetime.now().minute >= int(self.start_time_minutes):
1244
+ return True
1245
+ elif datetime.now().hour == int(self.finishing_time_hour):
1246
+ if datetime.now().minute < int(self.finishing_time_minutes):
1247
+ return True
1248
+ return False
1249
+
1250
+ def sleep_time(self, weekend=False):
1251
+ if weekend:
1252
+ # claculate number of minute from the friday and to monday start
1253
+ friday_time = datetime.strptime(self.end, '%H:%M')
1254
+ monday_time = datetime.strptime(self.start, '%H:%M')
1255
+ intra_day_diff = (monday_time - friday_time).total_seconds() // 60
1256
+ inter_day_diff = 3 * 24 * 60
1257
+ total_minutes = inter_day_diff + intra_day_diff
1258
+ return total_minutes
1259
+ else:
1260
+ # claculate number of minute from the end to the start
1261
+ start = datetime.strptime(self.start, '%H:%M')
1262
+ end = datetime.strptime(self.end, '%H:%M')
1263
+ minutes = (end - start).total_seconds() // 60
1264
+ sleep_time = (24*60) - minutes
1265
+ return sleep_time
1266
+
1267
+ def get_current_time(self):
1268
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1269
+
1270
+
1271
+ def create_trade_instance(
1272
+ symbols: List[str],
1273
+ params: Dict[str, Any]) -> Dict[str, Trade]:
1274
+ """
1275
+ Creates Trade instances for each symbol provided.
1276
+
1277
+ Args:
1278
+ symbols: A list of trading symbols (e.g., ['AAPL', 'MSFT']).
1279
+ params: A dictionary containing parameters for the Trade instance.
1280
+
1281
+ Returns:
1282
+ A dictionary where keys are symbols and values are corresponding Trade instances.
1283
+
1284
+ Raises:
1285
+ ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
1286
+ """
1287
+ instances = {}
1288
+
1289
+ if not symbols:
1290
+ raise ValueError("The 'symbols' list cannot be empty.")
1291
+ for symbol in symbols:
1292
+ try:
1293
+ instances[symbol] = Trade(**params, symbol=symbol)
1294
+ except Exception as e:
1295
+ logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
1296
+ return instances