bbstrader 0.1.7__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/btengine/__init__.py +1 -0
- bbstrader/btengine/backtest.py +11 -9
- bbstrader/btengine/performance.py +9 -30
- bbstrader/btengine/portfolio.py +18 -10
- bbstrader/metatrader/account.py +11 -4
- bbstrader/metatrader/rates.py +4 -5
- bbstrader/metatrader/risk.py +1 -2
- bbstrader/metatrader/trade.py +269 -214
- bbstrader/metatrader/utils.py +37 -29
- bbstrader/trading/execution.py +55 -18
- bbstrader/trading/strategies.py +5 -3
- bbstrader/tseries.py +500 -494
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.8.dist-info}/METADATA +2 -1
- bbstrader-0.1.8.dist-info/RECORD +26 -0
- bbstrader-0.1.7.dist-info/RECORD +0 -26
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.8.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.8.dist-info}/WHEEL +0 -0
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.8.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/trade.py
CHANGED
|
@@ -5,13 +5,14 @@ import numpy as np
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
import MetaTrader5 as Mt5
|
|
7
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
|
+
raise_mt5_error, trade_retcode_message, config_logger)
|
|
15
16
|
|
|
16
17
|
class Trade(RiskManagement):
|
|
17
18
|
"""
|
|
@@ -24,8 +25,8 @@ class Trade(RiskManagement):
|
|
|
24
25
|
>>> import time
|
|
25
26
|
>>> # Initialize the Trade class with parameters
|
|
26
27
|
>>> trade = Trade(
|
|
27
|
-
... symbol="
|
|
28
|
-
... expert_name="
|
|
28
|
+
... symbol="EURUSD", # Symbol to trade
|
|
29
|
+
... expert_name="bbstrader", # Name of the expert advisor
|
|
29
30
|
... expert_id=12345, # Unique ID for the expert advisor
|
|
30
31
|
... version="1.0", # Version of the expert advisor
|
|
31
32
|
... target=5.0, # Daily profit target in percentage
|
|
@@ -74,6 +75,7 @@ class Trade(RiskManagement):
|
|
|
74
75
|
expert_id: int = 9818,
|
|
75
76
|
version: str = '1.0',
|
|
76
77
|
target: float = 5.0,
|
|
78
|
+
be_on_trade_open: bool = True,
|
|
77
79
|
start_time: str = "1:00",
|
|
78
80
|
finishing_time: str = "23:00",
|
|
79
81
|
ending_time: str = "23:30",
|
|
@@ -92,6 +94,7 @@ class Trade(RiskManagement):
|
|
|
92
94
|
or the strategy used on the symbol.
|
|
93
95
|
version (str): The `version` of the expert advisor.
|
|
94
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.
|
|
95
98
|
start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
|
|
96
99
|
finishing_time (str): The time after which no new position can be opened.
|
|
97
100
|
ending_time (str): The time after which any open position will be closed.
|
|
@@ -128,6 +131,7 @@ class Trade(RiskManagement):
|
|
|
128
131
|
self.expert_id = expert_id
|
|
129
132
|
self.version = version
|
|
130
133
|
self.target = target
|
|
134
|
+
self.be_on_trade_open = be_on_trade_open
|
|
131
135
|
self.verbose = verbose
|
|
132
136
|
self.start = start_time
|
|
133
137
|
self.end = ending_time
|
|
@@ -136,12 +140,6 @@ class Trade(RiskManagement):
|
|
|
136
140
|
self.logger = self._get_logger(logger, console_log)
|
|
137
141
|
self.tf = kwargs.get("time_frame", 'D1')
|
|
138
142
|
|
|
139
|
-
self.lot = self.get_lot()
|
|
140
|
-
self.stop_loss = self.get_stop_loss()
|
|
141
|
-
self.take_profit = self.get_take_profit()
|
|
142
|
-
self.break_even_points = self.get_break_even()
|
|
143
|
-
self.deviation = self.get_deviation()
|
|
144
|
-
|
|
145
143
|
self.start_time_hour, self.start_time_minutes = self.start.split(":")
|
|
146
144
|
self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
|
|
147
145
|
":")
|
|
@@ -152,6 +150,8 @@ class Trade(RiskManagement):
|
|
|
152
150
|
self.opened_positions = []
|
|
153
151
|
self.opened_orders = []
|
|
154
152
|
self.break_even_status = []
|
|
153
|
+
self.break_even_points = {}
|
|
154
|
+
self.trail_after_points = {}
|
|
155
155
|
|
|
156
156
|
self.initialize()
|
|
157
157
|
self.select_symbol()
|
|
@@ -168,7 +168,7 @@ class Trade(RiskManagement):
|
|
|
168
168
|
def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
|
|
169
169
|
"""Get the logger object"""
|
|
170
170
|
if isinstance(logger, str):
|
|
171
|
-
return config_logger(logger, consol_log)
|
|
171
|
+
return config_logger(logger, consol_log=consol_log)
|
|
172
172
|
return logger
|
|
173
173
|
|
|
174
174
|
def initialize(self):
|
|
@@ -235,18 +235,22 @@ class Trade(RiskManagement):
|
|
|
235
235
|
|
|
236
236
|
def summary(self):
|
|
237
237
|
"""Show a brief description about the trading program"""
|
|
238
|
-
|
|
239
|
-
"
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
)
|
|
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)
|
|
250
254
|
|
|
251
255
|
def risk_managment(self):
|
|
252
256
|
"""Show the risk management parameters"""
|
|
@@ -259,36 +263,40 @@ class Trade(RiskManagement):
|
|
|
259
263
|
currency = account_info.currency
|
|
260
264
|
rates = self.get_currency_rates(self.symbol)
|
|
261
265
|
marging_currency = rates['mc']
|
|
262
|
-
|
|
263
|
-
"
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
f"
|
|
275
|
-
f"
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
f"
|
|
280
|
-
|
|
281
|
-
f"
|
|
282
|
-
|
|
283
|
-
f"
|
|
284
|
-
f"
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
|
|
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):
|
|
292
300
|
"""
|
|
293
301
|
Print some statistics for the trading session and save to CSV if specified.
|
|
294
302
|
|
|
@@ -312,27 +320,27 @@ class Trade(RiskManagement):
|
|
|
312
320
|
expected_profit = round((trade_risk * self.rr * -1), 2)
|
|
313
321
|
|
|
314
322
|
# Formatting the statistics output
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
)
|
|
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")
|
|
332
339
|
|
|
333
340
|
# Print the formatted statistics
|
|
334
341
|
if self.verbose:
|
|
335
|
-
print(
|
|
342
|
+
print("\n[======= Trading Session Statistics =======]")
|
|
343
|
+
print(session_table)
|
|
336
344
|
|
|
337
345
|
# Save to CSV if specified
|
|
338
346
|
if save:
|
|
@@ -354,13 +362,15 @@ class Trade(RiskManagement):
|
|
|
354
362
|
"Trade Profitability": profitability,
|
|
355
363
|
}
|
|
356
364
|
# Create the directory if it doesn't exist
|
|
365
|
+
if dir is None:
|
|
366
|
+
dir = f"{self.expert_name}_session_stats"
|
|
357
367
|
os.makedirs(dir, exist_ok=True)
|
|
358
368
|
if '.' in self.symbol:
|
|
359
369
|
symbol = self.symbol.split('.')[0]
|
|
360
370
|
else:
|
|
361
371
|
symbol = self.symbol
|
|
362
372
|
|
|
363
|
-
filename = f"{symbol}_{today_date}@{self.expert_id}
|
|
373
|
+
filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
|
|
364
374
|
filepath = os.path.join(dir, filename)
|
|
365
375
|
|
|
366
376
|
# Updated code to write to CSV
|
|
@@ -373,9 +383,10 @@ class Trade(RiskManagement):
|
|
|
373
383
|
writer.writerow([stat, value])
|
|
374
384
|
self.logger.info(f"Session statistics saved to {filepath}")
|
|
375
385
|
|
|
386
|
+
Buys = Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
|
|
376
387
|
def open_buy_position(
|
|
377
388
|
self,
|
|
378
|
-
action:
|
|
389
|
+
action: Buys = 'BMKT',
|
|
379
390
|
price: Optional[float] = None,
|
|
380
391
|
mm: bool = True,
|
|
381
392
|
id: Optional[int] = None,
|
|
@@ -424,8 +435,8 @@ class Trade(RiskManagement):
|
|
|
424
435
|
if action != 'BMKT':
|
|
425
436
|
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
426
437
|
request["type"] = self._order_type()[action][0]
|
|
427
|
-
|
|
428
|
-
|
|
438
|
+
if self.be_on_trade_open:
|
|
439
|
+
self.break_even(mm=mm, id=Id)
|
|
429
440
|
if self.check(comment):
|
|
430
441
|
self.request_result(_price, request, action),
|
|
431
442
|
|
|
@@ -442,9 +453,10 @@ class Trade(RiskManagement):
|
|
|
442
453
|
}
|
|
443
454
|
return type
|
|
444
455
|
|
|
456
|
+
Sells = Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
445
457
|
def open_sell_position(
|
|
446
458
|
self,
|
|
447
|
-
action:
|
|
459
|
+
action: Sells = 'SMKT',
|
|
448
460
|
price: Optional[float] = None,
|
|
449
461
|
mm: bool = True,
|
|
450
462
|
id: Optional[int] = None,
|
|
@@ -493,8 +505,8 @@ class Trade(RiskManagement):
|
|
|
493
505
|
if action != 'SMKT':
|
|
494
506
|
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
495
507
|
request["type"] = self._order_type()[action][0]
|
|
496
|
-
|
|
497
|
-
|
|
508
|
+
if self.be_on_trade_open:
|
|
509
|
+
self.break_even(mm=mm, id=Id)
|
|
498
510
|
if self.check(comment):
|
|
499
511
|
self.request_result(_price, request, action)
|
|
500
512
|
|
|
@@ -542,8 +554,7 @@ class Trade(RiskManagement):
|
|
|
542
554
|
self,
|
|
543
555
|
price: float,
|
|
544
556
|
request: Dict[str, Any],
|
|
545
|
-
type:
|
|
546
|
-
'SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
557
|
+
type: Buys | Sells
|
|
547
558
|
):
|
|
548
559
|
"""
|
|
549
560
|
Check if a trading order has been sent correctly
|
|
@@ -553,8 +564,7 @@ class Trade(RiskManagement):
|
|
|
553
564
|
request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
|
|
554
565
|
all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py
|
|
555
566
|
|
|
556
|
-
type (str): The type of the order
|
|
557
|
-
`(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
|
|
567
|
+
type (str): The type of the order `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
|
|
558
568
|
"""
|
|
559
569
|
# Send a trading request
|
|
560
570
|
# Check the execution result
|
|
@@ -621,14 +631,10 @@ class Trade(RiskManagement):
|
|
|
621
631
|
f"{self.get_account_info().currency}]\n"
|
|
622
632
|
)
|
|
623
633
|
self.logger.info(pos_info)
|
|
624
|
-
|
|
634
|
+
|
|
625
635
|
def open_position(
|
|
626
636
|
self,
|
|
627
|
-
action:
|
|
628
|
-
'BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
|
|
629
|
-
'SMKT', 'SLMT', 'SSTP', 'SSTPLMT'],
|
|
630
|
-
buy: bool = False,
|
|
631
|
-
sell: bool = False,
|
|
637
|
+
action: Buys | Sells,
|
|
632
638
|
price: Optional[float] = None,
|
|
633
639
|
id: Optional[int] = None,
|
|
634
640
|
mm: bool = True,
|
|
@@ -640,18 +646,20 @@ class Trade(RiskManagement):
|
|
|
640
646
|
Args:
|
|
641
647
|
action (str): (`'BMKT'`, `'SMKT'`) for Market orders
|
|
642
648
|
or (`'BLMT', 'SLMT', 'BSTP', 'SSTP', 'BSTPLMT', 'SSTPLMT'`) for pending orders
|
|
643
|
-
buy (bool): A boolean True or False
|
|
644
|
-
sell (bool): A boolean True or False
|
|
645
649
|
id (int): The strategy id or expert Id
|
|
646
650
|
mm (bool): Weither to put stop loss and tp or not
|
|
647
651
|
comment (str): The comment for the closing position
|
|
648
652
|
"""
|
|
649
|
-
|
|
653
|
+
BUYS = ['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
|
|
654
|
+
SELLS = ['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
655
|
+
if action in BUYS:
|
|
650
656
|
self.open_buy_position(
|
|
651
657
|
action=action, price=price, id=id, mm=mm, comment=comment)
|
|
652
|
-
|
|
658
|
+
elif action in SELLS:
|
|
653
659
|
self.open_sell_position(
|
|
654
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)}")
|
|
655
663
|
|
|
656
664
|
@property
|
|
657
665
|
def get_opened_orders(self):
|
|
@@ -699,13 +707,14 @@ class Trade(RiskManagement):
|
|
|
699
707
|
|
|
700
708
|
Args:
|
|
701
709
|
id (int): The strategy id or expert Id
|
|
702
|
-
filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', '
|
|
710
|
+
filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'profitables')
|
|
703
711
|
- `orders` are current open orders
|
|
704
712
|
- `positions` are all current open positions
|
|
705
713
|
- `buys` and `sells` are current buy or sell open positions
|
|
706
|
-
- `
|
|
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
|
|
707
716
|
th (bool): the minimum treshold for winning position
|
|
708
|
-
(only relevant when filter_type is '
|
|
717
|
+
(only relevant when filter_type is 'profitables')
|
|
709
718
|
|
|
710
719
|
Returns:
|
|
711
720
|
List[int] | None: A list of filtered tickets
|
|
@@ -727,7 +736,9 @@ class Trade(RiskManagement):
|
|
|
727
736
|
continue
|
|
728
737
|
if filter_type == 'sells' and item.type != 1:
|
|
729
738
|
continue
|
|
730
|
-
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:
|
|
731
742
|
continue
|
|
732
743
|
filtered_tickets.append(item.ticket)
|
|
733
744
|
return filtered_tickets if filtered_tickets else None
|
|
@@ -739,8 +750,11 @@ class Trade(RiskManagement):
|
|
|
739
750
|
def get_current_open_positions(self, id: Optional[int] = None) -> List[int] | None:
|
|
740
751
|
return self.get_filtered_tickets(id=id, filter_type='positions')
|
|
741
752
|
|
|
742
|
-
def
|
|
743
|
-
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')
|
|
744
758
|
|
|
745
759
|
def get_current_buys(self, id: Optional[int] = None) -> List[int] | None:
|
|
746
760
|
return self.get_filtered_tickets(id=id, filter_type='buys')
|
|
@@ -748,8 +762,9 @@ class Trade(RiskManagement):
|
|
|
748
762
|
def get_current_sells(self, id: Optional[int] = None) -> List[int] | None:
|
|
749
763
|
return self.get_filtered_tickets(id=id, filter_type='sells')
|
|
750
764
|
|
|
751
|
-
def positive_profit(self, th: Optional[float] = None
|
|
752
|
-
|
|
765
|
+
def positive_profit(self, th: Optional[float] = None,
|
|
766
|
+
id: Optional[int] = None,
|
|
767
|
+
account: bool = True) -> bool:
|
|
753
768
|
"""
|
|
754
769
|
Check is the total profit on current open positions
|
|
755
770
|
Is greater than a minimum profit express as percentage
|
|
@@ -757,35 +772,56 @@ class Trade(RiskManagement):
|
|
|
757
772
|
|
|
758
773
|
Args:
|
|
759
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
|
|
760
777
|
"""
|
|
761
|
-
|
|
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]
|
|
762
792
|
profit = 0.0
|
|
763
793
|
balance = self.get_account_info().balance
|
|
764
794
|
target = round((balance * self.target)/100, 2)
|
|
765
|
-
if positions is not None:
|
|
795
|
+
if positions is not None or len(positions) != 0:
|
|
766
796
|
for position in positions:
|
|
767
|
-
|
|
768
|
-
history = self.get_positions(
|
|
769
|
-
ticket=position
|
|
770
|
-
)
|
|
771
|
-
profit += history[0].profit
|
|
797
|
+
profit += position.profit
|
|
772
798
|
fees = self.get_stats()[0]["average_fee"] * len(positions)
|
|
773
799
|
current_profit = profit + fees
|
|
774
800
|
th_profit = (target*th)/100 if th is not None else (target*0.01)
|
|
775
|
-
|
|
776
|
-
return True
|
|
801
|
+
return current_profit >= th_profit
|
|
777
802
|
return False
|
|
778
803
|
|
|
779
|
-
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
|
+
):
|
|
780
811
|
"""
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
it checks if the price has moved in favorable direction,
|
|
784
|
-
|
|
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.
|
|
785
816
|
|
|
786
817
|
Args:
|
|
787
|
-
id (int): The strategy
|
|
788
|
-
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.
|
|
789
825
|
"""
|
|
790
826
|
time.sleep(0.1)
|
|
791
827
|
if not mm:
|
|
@@ -793,6 +829,10 @@ class Trade(RiskManagement):
|
|
|
793
829
|
Id = id if id is not None else self.expert_id
|
|
794
830
|
positions = self.get_positions(symbol=self.symbol)
|
|
795
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"
|
|
796
836
|
if positions is not None:
|
|
797
837
|
for position in positions:
|
|
798
838
|
if position.magic == Id:
|
|
@@ -805,30 +845,39 @@ class Trade(RiskManagement):
|
|
|
805
845
|
if break_even:
|
|
806
846
|
# Check if break-even has already been set for this position
|
|
807
847
|
if position.ticket not in self.break_even_status:
|
|
808
|
-
|
|
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)
|
|
809
852
|
self.break_even_status.append(position.ticket)
|
|
853
|
+
self.break_even_points[position.ticket] = be
|
|
810
854
|
else:
|
|
855
|
+
# Skip this if the trail is not set to True
|
|
856
|
+
if not trail:
|
|
857
|
+
continue
|
|
811
858
|
# Check if the price has moved favorably
|
|
812
|
-
new_be = be * 0.
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
|
820
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
|
|
821
872
|
# Calculate the new break-even level and price
|
|
822
873
|
if position.type == 0:
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
new_level = round(
|
|
829
|
-
|
|
830
|
-
new_price = round(
|
|
831
|
-
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)
|
|
832
881
|
self.set_break_even(
|
|
833
882
|
position, be, price=new_price, level=new_level
|
|
834
883
|
)
|
|
@@ -842,14 +891,10 @@ class Trade(RiskManagement):
|
|
|
842
891
|
Sets the break-even level for a given trading position.
|
|
843
892
|
|
|
844
893
|
Args:
|
|
845
|
-
position (TradePosition):
|
|
846
|
-
The trading position for which the break-even is to be set
|
|
847
|
-
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()`.
|
|
848
895
|
be (int): The break-even level in points.
|
|
849
|
-
level (float): The break-even level in price
|
|
850
|
-
|
|
851
|
-
price (float): The break-even price
|
|
852
|
-
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.
|
|
853
898
|
"""
|
|
854
899
|
point = self.get_symbol_info(self.symbol).point
|
|
855
900
|
digits = self.get_symbol_info(self.symbol).digits
|
|
@@ -863,7 +908,9 @@ class Trade(RiskManagement):
|
|
|
863
908
|
break_even_level = position.price_open + (be * point)
|
|
864
909
|
break_even_price = position.price_open + \
|
|
865
910
|
((fees_points + spread) * point)
|
|
866
|
-
|
|
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
|
|
867
914
|
_level = break_even_level if level is None else level
|
|
868
915
|
|
|
869
916
|
if self.get_tick_info(self.symbol).ask > _level:
|
|
@@ -882,7 +929,8 @@ class Trade(RiskManagement):
|
|
|
882
929
|
break_even_level = position.price_open - (be * point)
|
|
883
930
|
break_even_price = position.price_open - \
|
|
884
931
|
((fees_points + spread) * point)
|
|
885
|
-
_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
|
|
886
934
|
_level = break_even_level if level is None else level
|
|
887
935
|
|
|
888
936
|
if self.get_tick_info(self.symbol).bid < _level:
|
|
@@ -987,7 +1035,7 @@ class Trade(RiskManagement):
|
|
|
987
1035
|
for position in self.opened_positions:
|
|
988
1036
|
time.sleep(0.1)
|
|
989
1037
|
# This return two TradeDeal Object,
|
|
990
|
-
# The first one is the
|
|
1038
|
+
# The first one is the opening order
|
|
991
1039
|
# The second is the closing order
|
|
992
1040
|
history = self.get_trades_history(
|
|
993
1041
|
position=position, to_df=False
|
|
@@ -1023,98 +1071,103 @@ class Trade(RiskManagement):
|
|
|
1023
1071
|
# get all Actives positions
|
|
1024
1072
|
time.sleep(0.1)
|
|
1025
1073
|
Id = id if id is not None else self.expert_id
|
|
1026
|
-
positions = self.get_positions(
|
|
1074
|
+
positions = self.get_positions(ticket=ticket)
|
|
1027
1075
|
buy_price = self.get_tick_info(self.symbol).ask
|
|
1028
1076
|
sell_price = self.get_tick_info(self.symbol).bid
|
|
1029
1077
|
digits = self.get_symbol_info(self.symbol).digits
|
|
1030
1078
|
deviation = self.get_deviation()
|
|
1031
|
-
if positions is not None:
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
|
1086
1134
|
|
|
1135
|
+
Positions = Literal["all", "buy", "sell", "profitable", "losing"]
|
|
1087
1136
|
def close_positions(
|
|
1088
1137
|
self,
|
|
1089
|
-
position_type:
|
|
1138
|
+
position_type: Positions,
|
|
1090
1139
|
id: Optional[int] = None,
|
|
1091
1140
|
comment: Optional[str] = None):
|
|
1092
1141
|
"""
|
|
1093
1142
|
Args:
|
|
1094
|
-
position_type (str): Type of positions to close (
|
|
1143
|
+
position_type (str): Type of positions to close ('all', 'buy', 'sell', 'profitable', 'losing')
|
|
1095
1144
|
id (int): The unique ID of the Expert or Strategy
|
|
1096
1145
|
comment (str): Comment for the closing position
|
|
1097
1146
|
"""
|
|
1098
1147
|
if position_type == "all":
|
|
1099
1148
|
positions = self.get_positions(symbol=self.symbol)
|
|
1100
1149
|
elif position_type == "buy":
|
|
1101
|
-
positions = self.get_current_buys()
|
|
1150
|
+
positions = self.get_current_buys(id=id)
|
|
1102
1151
|
elif position_type == "sell":
|
|
1103
|
-
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)
|
|
1104
1157
|
else:
|
|
1105
|
-
logger.error(f"Invalid position type: {position_type}")
|
|
1158
|
+
self.logger.error(f"Invalid position type: {position_type}")
|
|
1106
1159
|
return
|
|
1107
1160
|
|
|
1108
1161
|
if positions is not None:
|
|
1109
1162
|
if position_type == 'all':
|
|
1110
|
-
tickets = [position.ticket for position in positions]
|
|
1163
|
+
tickets = [position.ticket for position in positions if position.magic == id]
|
|
1111
1164
|
else:
|
|
1112
1165
|
tickets = positions
|
|
1113
1166
|
else:
|
|
1114
1167
|
tickets = []
|
|
1115
1168
|
|
|
1116
1169
|
if position_type == 'all':
|
|
1117
|
-
pos_type = ''
|
|
1170
|
+
pos_type = 'open'
|
|
1118
1171
|
else:
|
|
1119
1172
|
pos_type = position_type
|
|
1120
1173
|
|
|
@@ -1224,12 +1277,11 @@ class Trade(RiskManagement):
|
|
|
1224
1277
|
return 0.0
|
|
1225
1278
|
df = df2.iloc[1:]
|
|
1226
1279
|
profit = df[["profit", "commission", "fee", "swap"]].sum(axis=1)
|
|
1227
|
-
returns = profit.
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
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)
|
|
1231
1283
|
|
|
1232
|
-
return round(
|
|
1284
|
+
return round(sharpe, 3)
|
|
1233
1285
|
|
|
1234
1286
|
def days_end(self) -> bool:
|
|
1235
1287
|
"""Check if it is the end of the trading day."""
|
|
@@ -1309,8 +1361,11 @@ def create_trade_instance(
|
|
|
1309
1361
|
raise ValueError("The 'symbols' list cannot be empty.")
|
|
1310
1362
|
for symbol in symbols:
|
|
1311
1363
|
try:
|
|
1312
|
-
instances[symbol] = Trade(
|
|
1364
|
+
instances[symbol] = Trade(symbol=symbol, **params)
|
|
1313
1365
|
except Exception as e:
|
|
1314
1366
|
logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
|
|
1315
|
-
|
|
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}")
|
|
1316
1371
|
return instances
|