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