bbstrader 0.1.7__py3-none-any.whl → 0.1.9__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 +20 -6
- bbstrader/metatrader/rates.py +6 -9
- bbstrader/metatrader/risk.py +2 -3
- bbstrader/metatrader/trade.py +306 -219
- bbstrader/metatrader/utils.py +37 -29
- bbstrader/trading/execution.py +139 -92
- bbstrader/trading/strategies.py +13 -9
- bbstrader/tseries.py +500 -494
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.9.dist-info}/METADATA +2 -1
- bbstrader-0.1.9.dist-info/RECORD +26 -0
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.9.dist-info}/WHEEL +1 -1
- bbstrader-0.1.7.dist-info/RECORD +0 -26
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.9.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.7.dist-info → bbstrader-0.1.9.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
|
-
from bbstrader.metatrader.account import INIT_MSG
|
|
12
|
+
from bbstrader.metatrader.account import check_mt5_connection, 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
|
|
@@ -91,7 +92,7 @@ class Trade(RiskManagement):
|
|
|
91
92
|
expert_id (int): The `unique ID` used to identify the expert advisor
|
|
92
93
|
or the strategy used on the symbol.
|
|
93
94
|
version (str): The `version` of the expert advisor.
|
|
94
|
-
target (float): `Trading period (day, week, month) profit target` in percentage
|
|
95
|
+
target (float): `Trading period (day, week, month) profit target` in percentage.
|
|
95
96
|
start_time (str): The` hour and minutes` that the expert advisor is able to start to run.
|
|
96
97
|
finishing_time (str): The time after which no new position can be opened.
|
|
97
98
|
ending_time (str): The time after which any open position will be closed.
|
|
@@ -136,12 +137,6 @@ class Trade(RiskManagement):
|
|
|
136
137
|
self.logger = self._get_logger(logger, console_log)
|
|
137
138
|
self.tf = kwargs.get("time_frame", 'D1')
|
|
138
139
|
|
|
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
140
|
self.start_time_hour, self.start_time_minutes = self.start.split(":")
|
|
146
141
|
self.finishing_time_hour, self.finishing_time_minutes = self.finishing.split(
|
|
147
142
|
":")
|
|
@@ -152,6 +147,8 @@ class Trade(RiskManagement):
|
|
|
152
147
|
self.opened_positions = []
|
|
153
148
|
self.opened_orders = []
|
|
154
149
|
self.break_even_status = []
|
|
150
|
+
self.break_even_points = {}
|
|
151
|
+
self.trail_after_points = []
|
|
155
152
|
|
|
156
153
|
self.initialize()
|
|
157
154
|
self.select_symbol()
|
|
@@ -184,8 +181,7 @@ class Trade(RiskManagement):
|
|
|
184
181
|
try:
|
|
185
182
|
if self.verbose:
|
|
186
183
|
print("\nInitializing the basics.")
|
|
187
|
-
|
|
188
|
-
raise_mt5_error(message=INIT_MSG)
|
|
184
|
+
check_mt5_connection()
|
|
189
185
|
if self.verbose:
|
|
190
186
|
print(
|
|
191
187
|
f"You are running the @{self.expert_name} Expert advisor,"
|
|
@@ -235,18 +231,22 @@ class Trade(RiskManagement):
|
|
|
235
231
|
|
|
236
232
|
def summary(self):
|
|
237
233
|
"""Show a brief description about the trading program"""
|
|
238
|
-
|
|
239
|
-
"
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
)
|
|
234
|
+
summary_data = [
|
|
235
|
+
["Expert Advisor Name", f"@{self.expert_name}"],
|
|
236
|
+
["Expert Advisor Version", f"@{self.version}"],
|
|
237
|
+
["Expert | Strategy ID", self.expert_id],
|
|
238
|
+
["Trading Symbol", self.symbol],
|
|
239
|
+
["Trading Time Frame", self.tf],
|
|
240
|
+
["Start Trading Time", f"{self.start_time_hour}:{self.start_time_minutes}"],
|
|
241
|
+
["Finishing Trading Time", f"{self.finishing_time_hour}:{self.finishing_time_minutes}"],
|
|
242
|
+
["Closing Position After", f"{self.ending_time_hour}:{self.ending_time_minutes}"],
|
|
243
|
+
]
|
|
244
|
+
# Custom table format
|
|
245
|
+
summary_table = tabulate(summary_data, headers=["Summary", "Values"], tablefmt="outline")
|
|
246
|
+
|
|
247
|
+
# Print the table
|
|
248
|
+
print("\n[======= Trade Account Summary =======]")
|
|
249
|
+
print(summary_table)
|
|
250
250
|
|
|
251
251
|
def risk_managment(self):
|
|
252
252
|
"""Show the risk management parameters"""
|
|
@@ -259,36 +259,40 @@ class Trade(RiskManagement):
|
|
|
259
259
|
currency = account_info.currency
|
|
260
260
|
rates = self.get_currency_rates(self.symbol)
|
|
261
261
|
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
|
-
|
|
262
|
+
account_data = [
|
|
263
|
+
["Account Name", account_info.name],
|
|
264
|
+
["Account Number", account_info.login],
|
|
265
|
+
["Account Server", account_info.server],
|
|
266
|
+
["Account Balance", f"{account_info.balance} {currency}"],
|
|
267
|
+
["Account Profit", f"{_profit} {currency}"],
|
|
268
|
+
["Account Equity", f"{account_info.equity} {currency}"],
|
|
269
|
+
["Account Leverage", self.get_leverage(True)],
|
|
270
|
+
["Account Margin", f"{round(account_info.margin, 2)} {currency}"],
|
|
271
|
+
["Account Free Margin", f"{account_info.margin_free} {currency}"],
|
|
272
|
+
["Maximum Drawdown", f"{self.max_risk}%"],
|
|
273
|
+
["Risk Allowed", f"{round((self.max_risk - self.risk_level()), 2)}%"],
|
|
274
|
+
["Volume", f"{self.volume()} {marging_currency}"],
|
|
275
|
+
["Risk Per trade", f"{-self.get_currency_risk()} {currency}"],
|
|
276
|
+
["Profit Expected Per trade", f"{self.expected_profit()} {currency}"],
|
|
277
|
+
["Lot Size", f"{self.get_lot()} Lots"],
|
|
278
|
+
["Stop Loss", f"{self.get_stop_loss()} Points"],
|
|
279
|
+
["Loss Value Per Tick", f"{round(loss, 5)} {currency}"],
|
|
280
|
+
["Take Profit", f"{self.get_take_profit()} Points"],
|
|
281
|
+
["Profit Value Per Tick", f"{round(profit, 5)} {currency}"],
|
|
282
|
+
["Break Even", f"{self.get_break_even()} Points"],
|
|
283
|
+
["Deviation", f"{self.get_deviation()} Points"],
|
|
284
|
+
["Trading Time Interval", f"{self.get_minutes()} Minutes"],
|
|
285
|
+
["Risk Level", ok],
|
|
286
|
+
["Maximum Trades", self.max_trade()],
|
|
287
|
+
]
|
|
288
|
+
# Custom table format
|
|
289
|
+
print("\n[======= Account Risk Management Overview =======]")
|
|
290
|
+
table = tabulate(account_data, headers=["Risk Metrics", "Values"], tablefmt="outline")
|
|
291
|
+
|
|
292
|
+
# Print the table
|
|
293
|
+
print(table)
|
|
294
|
+
|
|
295
|
+
def statistics(self, save=True, dir=None):
|
|
292
296
|
"""
|
|
293
297
|
Print some statistics for the trading session and save to CSV if specified.
|
|
294
298
|
|
|
@@ -312,27 +316,27 @@ class Trade(RiskManagement):
|
|
|
312
316
|
expected_profit = round((trade_risk * self.rr * -1), 2)
|
|
313
317
|
|
|
314
318
|
# Formatting the statistics output
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
)
|
|
319
|
+
session_data = [
|
|
320
|
+
["Total Trades", deals],
|
|
321
|
+
["Winning Trades", wins],
|
|
322
|
+
["Losing Trades", losses],
|
|
323
|
+
["Session Profit", f"{profit} {currency}"],
|
|
324
|
+
["Total Fees", f"{total_fees} {currency}"],
|
|
325
|
+
["Average Fees", f"{average_fee} {currency}"],
|
|
326
|
+
["Net Profit", f"{net_profit} {currency}"],
|
|
327
|
+
["Risk per Trade", f"{trade_risk} {currency}"],
|
|
328
|
+
["Expected Profit per Trade", f"{self.expected_profit()} {currency}"],
|
|
329
|
+
["Risk Reward Ratio", self.rr],
|
|
330
|
+
["Win Rate", f"{win_rate}%"],
|
|
331
|
+
["Sharpe Ratio", self.sharpe()],
|
|
332
|
+
["Trade Profitability", profitability],
|
|
333
|
+
]
|
|
334
|
+
session_table = tabulate(session_data, headers=["Statistics", "Values"], tablefmt="outline")
|
|
332
335
|
|
|
333
336
|
# Print the formatted statistics
|
|
334
337
|
if self.verbose:
|
|
335
|
-
print(
|
|
338
|
+
print("\n[======= Trading Session Statistics =======]")
|
|
339
|
+
print(session_table)
|
|
336
340
|
|
|
337
341
|
# Save to CSV if specified
|
|
338
342
|
if save:
|
|
@@ -354,13 +358,15 @@ class Trade(RiskManagement):
|
|
|
354
358
|
"Trade Profitability": profitability,
|
|
355
359
|
}
|
|
356
360
|
# Create the directory if it doesn't exist
|
|
361
|
+
if dir is None:
|
|
362
|
+
dir = f".{self.expert_name}_session_stats"
|
|
357
363
|
os.makedirs(dir, exist_ok=True)
|
|
358
364
|
if '.' in self.symbol:
|
|
359
365
|
symbol = self.symbol.split('.')[0]
|
|
360
366
|
else:
|
|
361
367
|
symbol = self.symbol
|
|
362
368
|
|
|
363
|
-
filename = f"{symbol}_{today_date}@{self.expert_id}
|
|
369
|
+
filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
|
|
364
370
|
filepath = os.path.join(dir, filename)
|
|
365
371
|
|
|
366
372
|
# Updated code to write to CSV
|
|
@@ -373,9 +379,10 @@ class Trade(RiskManagement):
|
|
|
373
379
|
writer.writerow([stat, value])
|
|
374
380
|
self.logger.info(f"Session statistics saved to {filepath}")
|
|
375
381
|
|
|
382
|
+
Buys = Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
|
|
376
383
|
def open_buy_position(
|
|
377
384
|
self,
|
|
378
|
-
action:
|
|
385
|
+
action: Buys = 'BMKT',
|
|
379
386
|
price: Optional[float] = None,
|
|
380
387
|
mm: bool = True,
|
|
381
388
|
id: Optional[int] = None,
|
|
@@ -424,8 +431,7 @@ class Trade(RiskManagement):
|
|
|
424
431
|
if action != 'BMKT':
|
|
425
432
|
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
426
433
|
request["type"] = self._order_type()[action][0]
|
|
427
|
-
|
|
428
|
-
self.break_even(mm=mm)
|
|
434
|
+
self.break_even(mm=mm, id=Id)
|
|
429
435
|
if self.check(comment):
|
|
430
436
|
self.request_result(_price, request, action),
|
|
431
437
|
|
|
@@ -442,9 +448,10 @@ class Trade(RiskManagement):
|
|
|
442
448
|
}
|
|
443
449
|
return type
|
|
444
450
|
|
|
451
|
+
Sells = Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
445
452
|
def open_sell_position(
|
|
446
453
|
self,
|
|
447
|
-
action:
|
|
454
|
+
action: Sells = 'SMKT',
|
|
448
455
|
price: Optional[float] = None,
|
|
449
456
|
mm: bool = True,
|
|
450
457
|
id: Optional[int] = None,
|
|
@@ -493,8 +500,7 @@ class Trade(RiskManagement):
|
|
|
493
500
|
if action != 'SMKT':
|
|
494
501
|
request["action"] = Mt5.TRADE_ACTION_PENDING
|
|
495
502
|
request["type"] = self._order_type()[action][0]
|
|
496
|
-
|
|
497
|
-
self.break_even(comment)
|
|
503
|
+
self.break_even(mm=mm, id=Id)
|
|
498
504
|
if self.check(comment):
|
|
499
505
|
self.request_result(_price, request, action)
|
|
500
506
|
|
|
@@ -542,8 +548,7 @@ class Trade(RiskManagement):
|
|
|
542
548
|
self,
|
|
543
549
|
price: float,
|
|
544
550
|
request: Dict[str, Any],
|
|
545
|
-
type:
|
|
546
|
-
'SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
551
|
+
type: Buys | Sells
|
|
547
552
|
):
|
|
548
553
|
"""
|
|
549
554
|
Check if a trading order has been sent correctly
|
|
@@ -553,8 +558,7 @@ class Trade(RiskManagement):
|
|
|
553
558
|
request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
|
|
554
559
|
all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py
|
|
555
560
|
|
|
556
|
-
type (str): The type of the order
|
|
557
|
-
`(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
|
|
561
|
+
type (str): The type of the order `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
|
|
558
562
|
"""
|
|
559
563
|
# Send a trading request
|
|
560
564
|
# Check the execution result
|
|
@@ -621,14 +625,10 @@ class Trade(RiskManagement):
|
|
|
621
625
|
f"{self.get_account_info().currency}]\n"
|
|
622
626
|
)
|
|
623
627
|
self.logger.info(pos_info)
|
|
624
|
-
|
|
628
|
+
|
|
625
629
|
def open_position(
|
|
626
630
|
self,
|
|
627
|
-
action:
|
|
628
|
-
'BMKT', 'BLMT', 'BSTP', 'BSTPLMT',
|
|
629
|
-
'SMKT', 'SLMT', 'SSTP', 'SSTPLMT'],
|
|
630
|
-
buy: bool = False,
|
|
631
|
-
sell: bool = False,
|
|
631
|
+
action: Buys | Sells,
|
|
632
632
|
price: Optional[float] = None,
|
|
633
633
|
id: Optional[int] = None,
|
|
634
634
|
mm: bool = True,
|
|
@@ -640,18 +640,20 @@ class Trade(RiskManagement):
|
|
|
640
640
|
Args:
|
|
641
641
|
action (str): (`'BMKT'`, `'SMKT'`) for Market orders
|
|
642
642
|
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
643
|
id (int): The strategy id or expert Id
|
|
646
644
|
mm (bool): Weither to put stop loss and tp or not
|
|
647
645
|
comment (str): The comment for the closing position
|
|
648
646
|
"""
|
|
649
|
-
|
|
647
|
+
BUYS = ['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
|
|
648
|
+
SELLS = ['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
649
|
+
if action in BUYS:
|
|
650
650
|
self.open_buy_position(
|
|
651
651
|
action=action, price=price, id=id, mm=mm, comment=comment)
|
|
652
|
-
|
|
652
|
+
elif action in SELLS:
|
|
653
653
|
self.open_sell_position(
|
|
654
654
|
action=action, price=price, id=id, mm=mm, comment=comment)
|
|
655
|
+
else:
|
|
656
|
+
raise ValueError(f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}")
|
|
655
657
|
|
|
656
658
|
@property
|
|
657
659
|
def get_opened_orders(self):
|
|
@@ -699,13 +701,14 @@ class Trade(RiskManagement):
|
|
|
699
701
|
|
|
700
702
|
Args:
|
|
701
703
|
id (int): The strategy id or expert Id
|
|
702
|
-
filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', '
|
|
704
|
+
filter_type (str): Filter type ('orders', 'positions', 'buys', 'sells', 'profitables')
|
|
703
705
|
- `orders` are current open orders
|
|
704
706
|
- `positions` are all current open positions
|
|
705
707
|
- `buys` and `sells` are current buy or sell open positions
|
|
706
|
-
- `
|
|
708
|
+
- `profitables` are current open position that have a profit greater than a threshold
|
|
709
|
+
- `losings` are current open position that have a negative profit
|
|
707
710
|
th (bool): the minimum treshold for winning position
|
|
708
|
-
(only relevant when filter_type is '
|
|
711
|
+
(only relevant when filter_type is 'profitables')
|
|
709
712
|
|
|
710
713
|
Returns:
|
|
711
714
|
List[int] | None: A list of filtered tickets
|
|
@@ -727,7 +730,9 @@ class Trade(RiskManagement):
|
|
|
727
730
|
continue
|
|
728
731
|
if filter_type == 'sells' and item.type != 1:
|
|
729
732
|
continue
|
|
730
|
-
if filter_type == '
|
|
733
|
+
if filter_type == 'profitables' and not self.win_trade(item, th=th):
|
|
734
|
+
continue
|
|
735
|
+
if filter_type == 'losings' and item.profit > 0:
|
|
731
736
|
continue
|
|
732
737
|
filtered_tickets.append(item.ticket)
|
|
733
738
|
return filtered_tickets if filtered_tickets else None
|
|
@@ -739,8 +744,11 @@ class Trade(RiskManagement):
|
|
|
739
744
|
def get_current_open_positions(self, id: Optional[int] = None) -> List[int] | None:
|
|
740
745
|
return self.get_filtered_tickets(id=id, filter_type='positions')
|
|
741
746
|
|
|
742
|
-
def
|
|
743
|
-
return self.get_filtered_tickets(id=id, filter_type='
|
|
747
|
+
def get_current_profitables(self, id: Optional[int] = None, th=None) -> List[int] | None:
|
|
748
|
+
return self.get_filtered_tickets(id=id, filter_type='profitables', th=th)
|
|
749
|
+
|
|
750
|
+
def get_current_losings(self, id: Optional[int] = None) -> List[int] | None:
|
|
751
|
+
return self.get_filtered_tickets(id=id, filter_type='losings')
|
|
744
752
|
|
|
745
753
|
def get_current_buys(self, id: Optional[int] = None) -> List[int] | None:
|
|
746
754
|
return self.get_filtered_tickets(id=id, filter_type='buys')
|
|
@@ -748,8 +756,9 @@ class Trade(RiskManagement):
|
|
|
748
756
|
def get_current_sells(self, id: Optional[int] = None) -> List[int] | None:
|
|
749
757
|
return self.get_filtered_tickets(id=id, filter_type='sells')
|
|
750
758
|
|
|
751
|
-
def positive_profit(self, th: Optional[float] = None
|
|
752
|
-
|
|
759
|
+
def positive_profit(self, th: Optional[float] = None,
|
|
760
|
+
id: Optional[int] = None,
|
|
761
|
+
account: bool = True) -> bool:
|
|
753
762
|
"""
|
|
754
763
|
Check is the total profit on current open positions
|
|
755
764
|
Is greater than a minimum profit express as percentage
|
|
@@ -757,35 +766,56 @@ class Trade(RiskManagement):
|
|
|
757
766
|
|
|
758
767
|
Args:
|
|
759
768
|
th (float): The minimum profit target on current positions
|
|
769
|
+
id (int): The strategy id or expert Id
|
|
770
|
+
account (bool): Weither to check positions on the account or on the symbol
|
|
760
771
|
"""
|
|
761
|
-
|
|
772
|
+
if account and id is None:
|
|
773
|
+
# All open positions no matter the symbol or strategy or expert
|
|
774
|
+
positions = self.get_positions()
|
|
775
|
+
elif account and id is not None:
|
|
776
|
+
# All open positions for a specific strategy or expert no matter the symbol
|
|
777
|
+
positions = self.get_positions()
|
|
778
|
+
positions = [position for position in positions if position.magic == id]
|
|
779
|
+
elif not account and id is None:
|
|
780
|
+
# All open positions for the current symbol no matter the strategy or expert
|
|
781
|
+
positions = self.get_positions(symbol=self.symbol)
|
|
782
|
+
elif not account and id is not None:
|
|
783
|
+
# All open positions for the current symbol and a specific strategy or expert
|
|
784
|
+
positions = self.get_positions(symbol=self.symbol)
|
|
785
|
+
positions = [position for position in positions if position.magic == id]
|
|
762
786
|
profit = 0.0
|
|
763
787
|
balance = self.get_account_info().balance
|
|
764
788
|
target = round((balance * self.target)/100, 2)
|
|
765
|
-
if positions is not None:
|
|
789
|
+
if positions is not None or len(positions) != 0:
|
|
766
790
|
for position in positions:
|
|
767
|
-
|
|
768
|
-
history = self.get_positions(
|
|
769
|
-
ticket=position
|
|
770
|
-
)
|
|
771
|
-
profit += history[0].profit
|
|
791
|
+
profit += position.profit
|
|
772
792
|
fees = self.get_stats()[0]["average_fee"] * len(positions)
|
|
773
793
|
current_profit = profit + fees
|
|
774
794
|
th_profit = (target*th)/100 if th is not None else (target*0.01)
|
|
775
|
-
|
|
776
|
-
return True
|
|
795
|
+
return current_profit >= th_profit
|
|
777
796
|
return False
|
|
778
797
|
|
|
779
|
-
def break_even(self, mm=True,
|
|
798
|
+
def break_even(self, mm=True,
|
|
799
|
+
id: Optional[int] = None,
|
|
800
|
+
trail: Optional[bool] = True,
|
|
801
|
+
stop_trail: Optional[int] = None,
|
|
802
|
+
trail_after_points: Optional[int] = None,
|
|
803
|
+
be_plus_points: Optional[int] = None
|
|
804
|
+
):
|
|
780
805
|
"""
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
it checks if the price has moved in favorable direction,
|
|
784
|
-
|
|
806
|
+
This function checks if it's time to set the break-even level for a trading position.
|
|
807
|
+
If it is, it sets the break-even level. If the break-even level has already been set,
|
|
808
|
+
it checks if the price has moved in a favorable direction. If it has, and the trail parameter is set to True,
|
|
809
|
+
it updates the break-even level based on the trail_after_points and stop_trail parameters.
|
|
785
810
|
|
|
786
811
|
Args:
|
|
787
|
-
id (int): The strategy
|
|
788
|
-
mm (bool):
|
|
812
|
+
id (int): The strategy ID or expert ID.
|
|
813
|
+
mm (bool): Whether to manage the position or not.
|
|
814
|
+
trail (bool): Whether to trail the stop loss or not.
|
|
815
|
+
stop_trail (int): Number of points to trail the stop loss by.
|
|
816
|
+
It represent the distance from the current price to the stop loss.
|
|
817
|
+
trail_after_points (int): Number of points in profit from where the strategy will start to trail the stop loss.
|
|
818
|
+
be_plus_points (int): Number of points to add to the break-even level. Represents the minimum profit to secure.
|
|
789
819
|
"""
|
|
790
820
|
time.sleep(0.1)
|
|
791
821
|
if not mm:
|
|
@@ -793,6 +823,10 @@ class Trade(RiskManagement):
|
|
|
793
823
|
Id = id if id is not None else self.expert_id
|
|
794
824
|
positions = self.get_positions(symbol=self.symbol)
|
|
795
825
|
be = self.get_break_even()
|
|
826
|
+
if trail_after_points is not None:
|
|
827
|
+
assert trail_after_points > be, \
|
|
828
|
+
"trail_after_points must be greater than break even"\
|
|
829
|
+
" or set to None"
|
|
796
830
|
if positions is not None:
|
|
797
831
|
for position in positions:
|
|
798
832
|
if position.magic == Id:
|
|
@@ -805,30 +839,39 @@ class Trade(RiskManagement):
|
|
|
805
839
|
if break_even:
|
|
806
840
|
# Check if break-even has already been set for this position
|
|
807
841
|
if position.ticket not in self.break_even_status:
|
|
808
|
-
|
|
842
|
+
price = None
|
|
843
|
+
if be_plus_points is not None:
|
|
844
|
+
price = position.price_open + (be_plus_points * point)
|
|
845
|
+
self.set_break_even(position, be, price=price)
|
|
809
846
|
self.break_even_status.append(position.ticket)
|
|
847
|
+
self.break_even_points[position.ticket] = be
|
|
810
848
|
else:
|
|
849
|
+
# Skip this if the trail is not set to True
|
|
850
|
+
if not trail:
|
|
851
|
+
continue
|
|
811
852
|
# Check if the price has moved favorably
|
|
812
|
-
new_be = be * 0.
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
(position.
|
|
819
|
-
|
|
853
|
+
new_be = round(be * 0.10) if be_plus_points is None else be_plus_points
|
|
854
|
+
if trail_after_points is not None:
|
|
855
|
+
if position.ticket not in self.trail_after_points:
|
|
856
|
+
# This ensures that the position rich the minimum points required
|
|
857
|
+
# before the trail can be set
|
|
858
|
+
new_be = trail_after_points - be
|
|
859
|
+
self.trail_after_points.append(position.ticket)
|
|
860
|
+
new_be_points = self.break_even_points[position.ticket] + new_be
|
|
861
|
+
favorable_move = float(points/point) >= new_be_points
|
|
820
862
|
if favorable_move:
|
|
863
|
+
# This allows the position to go to take profit in case of a swing trade
|
|
864
|
+
# If is a scalping position, we can set the stop_trail close to the current price.
|
|
865
|
+
trail_points = round(be * 0.50) if stop_trail is None else stop_trail
|
|
821
866
|
# Calculate the new break-even level and price
|
|
822
867
|
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)
|
|
868
|
+
# This level validate the favorable move of the price
|
|
869
|
+
new_level = round(position.price_open + (new_be_points * point), digits)
|
|
870
|
+
# This price is set away from the current price by the trail_points
|
|
871
|
+
new_price = round(position.price_current - (trail_points * point), digits)
|
|
872
|
+
elif position.type == 1:
|
|
873
|
+
new_level = round(position.price_open - (new_be_points * point), digits)
|
|
874
|
+
new_price = round(position.price_current + (trail_points * point), digits)
|
|
832
875
|
self.set_break_even(
|
|
833
876
|
position, be, price=new_price, level=new_level
|
|
834
877
|
)
|
|
@@ -842,14 +885,10 @@ class Trade(RiskManagement):
|
|
|
842
885
|
Sets the break-even level for a given trading position.
|
|
843
886
|
|
|
844
887
|
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()`
|
|
888
|
+
position (TradePosition): The trading position for which the break-even is to be set. This is the value return by `mt5.positions_get()`.
|
|
848
889
|
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.
|
|
890
|
+
level (float): The break-even level in price, if set to None , it will be calated automaticaly.
|
|
891
|
+
price (float): The break-even price, if set to None , it will be calated automaticaly.
|
|
853
892
|
"""
|
|
854
893
|
point = self.get_symbol_info(self.symbol).point
|
|
855
894
|
digits = self.get_symbol_info(self.symbol).digits
|
|
@@ -863,7 +902,9 @@ class Trade(RiskManagement):
|
|
|
863
902
|
break_even_level = position.price_open + (be * point)
|
|
864
903
|
break_even_price = position.price_open + \
|
|
865
904
|
((fees_points + spread) * point)
|
|
866
|
-
|
|
905
|
+
# Check if the price specified is greater or lower than the calculated price
|
|
906
|
+
_price = break_even_price if price is None or \
|
|
907
|
+
price < break_even_price else price
|
|
867
908
|
_level = break_even_level if level is None else level
|
|
868
909
|
|
|
869
910
|
if self.get_tick_info(self.symbol).ask > _level:
|
|
@@ -882,7 +923,8 @@ class Trade(RiskManagement):
|
|
|
882
923
|
break_even_level = position.price_open - (be * point)
|
|
883
924
|
break_even_price = position.price_open - \
|
|
884
925
|
((fees_points + spread) * point)
|
|
885
|
-
_price = break_even_price if price is None
|
|
926
|
+
_price = break_even_price if price is None or \
|
|
927
|
+
price > break_even_price else price
|
|
886
928
|
_level = break_even_level if level is None else level
|
|
887
929
|
|
|
888
930
|
if self.get_tick_info(self.symbol).bid < _level:
|
|
@@ -987,7 +1029,7 @@ class Trade(RiskManagement):
|
|
|
987
1029
|
for position in self.opened_positions:
|
|
988
1030
|
time.sleep(0.1)
|
|
989
1031
|
# This return two TradeDeal Object,
|
|
990
|
-
# The first one is the
|
|
1032
|
+
# The first one is the opening order
|
|
991
1033
|
# The second is the closing order
|
|
992
1034
|
history = self.get_trades_history(
|
|
993
1035
|
position=position, to_df=False
|
|
@@ -1023,98 +1065,103 @@ class Trade(RiskManagement):
|
|
|
1023
1065
|
# get all Actives positions
|
|
1024
1066
|
time.sleep(0.1)
|
|
1025
1067
|
Id = id if id is not None else self.expert_id
|
|
1026
|
-
positions = self.get_positions(
|
|
1068
|
+
positions = self.get_positions(ticket=ticket)
|
|
1027
1069
|
buy_price = self.get_tick_info(self.symbol).ask
|
|
1028
1070
|
sell_price = self.get_tick_info(self.symbol).bid
|
|
1029
1071
|
digits = self.get_symbol_info(self.symbol).digits
|
|
1030
1072
|
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
|
-
|
|
1073
|
+
if positions is not None and len(positions) == 1:
|
|
1074
|
+
position = positions[0]
|
|
1075
|
+
if (position.ticket == ticket
|
|
1076
|
+
and position.magic == Id
|
|
1077
|
+
):
|
|
1078
|
+
buy = position.type == 0
|
|
1079
|
+
sell = position.type == 1
|
|
1080
|
+
request = {
|
|
1081
|
+
"action": Mt5.TRADE_ACTION_DEAL,
|
|
1082
|
+
"symbol": self.symbol,
|
|
1083
|
+
"volume": (position.volume*pct),
|
|
1084
|
+
"type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
|
|
1085
|
+
"position": ticket,
|
|
1086
|
+
"price": sell_price if buy else buy_price,
|
|
1087
|
+
"deviation": deviation,
|
|
1088
|
+
"magic": Id,
|
|
1089
|
+
"comment": f"@{self.expert_name}" if comment is None else comment,
|
|
1090
|
+
"type_time": Mt5.ORDER_TIME_GTC,
|
|
1091
|
+
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
1092
|
+
}
|
|
1093
|
+
addtionnal = f", SYMBOL={self.symbol}"
|
|
1094
|
+
try:
|
|
1095
|
+
check_result = self.check_order(request)
|
|
1096
|
+
result = self.send_order(request)
|
|
1097
|
+
except Exception as e:
|
|
1098
|
+
print(f"{self.current_datetime()} -", end=' ')
|
|
1099
|
+
trade_retcode_message(
|
|
1100
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
1101
|
+
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1102
|
+
msg = trade_retcode_message(result.retcode)
|
|
1103
|
+
self.logger.error(
|
|
1104
|
+
f"Closing Order Request, Position: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}")
|
|
1105
|
+
tries = 0
|
|
1106
|
+
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
1107
|
+
time.sleep(1)
|
|
1108
|
+
try:
|
|
1109
|
+
check_result = self.check_order(request)
|
|
1110
|
+
result = self.send_order(request)
|
|
1111
|
+
except Exception as e:
|
|
1112
|
+
print(f"{self.current_datetime()} -", end=' ')
|
|
1113
|
+
trade_retcode_message(
|
|
1114
|
+
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
1115
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1116
|
+
break
|
|
1117
|
+
tries += 1
|
|
1118
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1119
|
+
msg = trade_retcode_message(result.retcode)
|
|
1120
|
+
self.logger.info(
|
|
1121
|
+
f"Closing Order {msg}{addtionnal}")
|
|
1122
|
+
info = (
|
|
1123
|
+
f"Position #{ticket} closed, Symbol: {self.symbol}, Price: @{request['price']}")
|
|
1124
|
+
self.logger.info(info)
|
|
1125
|
+
return True
|
|
1126
|
+
else:
|
|
1127
|
+
return False
|
|
1086
1128
|
|
|
1129
|
+
Positions = Literal["all", "buy", "sell", "profitable", "losing"]
|
|
1087
1130
|
def close_positions(
|
|
1088
1131
|
self,
|
|
1089
|
-
position_type:
|
|
1132
|
+
position_type: Positions,
|
|
1090
1133
|
id: Optional[int] = None,
|
|
1091
1134
|
comment: Optional[str] = None):
|
|
1092
1135
|
"""
|
|
1093
1136
|
Args:
|
|
1094
|
-
position_type (str): Type of positions to close (
|
|
1137
|
+
position_type (str): Type of positions to close ('all', 'buy', 'sell', 'profitable', 'losing')
|
|
1095
1138
|
id (int): The unique ID of the Expert or Strategy
|
|
1096
1139
|
comment (str): Comment for the closing position
|
|
1097
1140
|
"""
|
|
1098
1141
|
if position_type == "all":
|
|
1099
1142
|
positions = self.get_positions(symbol=self.symbol)
|
|
1100
1143
|
elif position_type == "buy":
|
|
1101
|
-
positions = self.get_current_buys()
|
|
1144
|
+
positions = self.get_current_buys(id=id)
|
|
1102
1145
|
elif position_type == "sell":
|
|
1103
|
-
positions = self.get_current_sells()
|
|
1146
|
+
positions = self.get_current_sells(id=id)
|
|
1147
|
+
elif position_type == "profitable":
|
|
1148
|
+
positions = self.get_current_profitables(id=id)
|
|
1149
|
+
elif position_type == "losing":
|
|
1150
|
+
positions = self.get_current_losings(id=id)
|
|
1104
1151
|
else:
|
|
1105
|
-
logger.error(f"Invalid position type: {position_type}")
|
|
1152
|
+
self.logger.error(f"Invalid position type: {position_type}")
|
|
1106
1153
|
return
|
|
1107
1154
|
|
|
1108
1155
|
if positions is not None:
|
|
1109
1156
|
if position_type == 'all':
|
|
1110
|
-
tickets = [position.ticket for position in positions]
|
|
1157
|
+
tickets = [position.ticket for position in positions if position.magic == id]
|
|
1111
1158
|
else:
|
|
1112
1159
|
tickets = positions
|
|
1113
1160
|
else:
|
|
1114
1161
|
tickets = []
|
|
1115
1162
|
|
|
1116
1163
|
if position_type == 'all':
|
|
1117
|
-
pos_type = ''
|
|
1164
|
+
pos_type = 'open'
|
|
1118
1165
|
else:
|
|
1119
1166
|
pos_type = position_type
|
|
1120
1167
|
|
|
@@ -1224,12 +1271,11 @@ class Trade(RiskManagement):
|
|
|
1224
1271
|
return 0.0
|
|
1225
1272
|
df = df2.iloc[1:]
|
|
1226
1273
|
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)
|
|
1274
|
+
returns = profit.pct_change(fill_method=None)
|
|
1275
|
+
periods = self.max_trade() * 252
|
|
1276
|
+
sharpe = create_sharpe_ratio(returns, periods=periods)
|
|
1231
1277
|
|
|
1232
|
-
return round(
|
|
1278
|
+
return round(sharpe, 3)
|
|
1233
1279
|
|
|
1234
1280
|
def days_end(self) -> bool:
|
|
1235
1281
|
"""Check if it is the end of the trading day."""
|
|
@@ -1290,27 +1336,68 @@ class Trade(RiskManagement):
|
|
|
1290
1336
|
def create_trade_instance(
|
|
1291
1337
|
symbols: List[str],
|
|
1292
1338
|
params: Dict[str, Any],
|
|
1293
|
-
|
|
1339
|
+
daily_risk: Optional[Dict[str, float]] = None,
|
|
1340
|
+
max_risk: Optional[Dict[str, float]] = None,
|
|
1341
|
+
pchange_sl: Optional[Dict[str, float] | float] = None,
|
|
1342
|
+
logger: Logger = None) -> Dict[str, Trade]:
|
|
1294
1343
|
"""
|
|
1295
1344
|
Creates Trade instances for each symbol provided.
|
|
1296
1345
|
|
|
1297
1346
|
Args:
|
|
1298
1347
|
symbols: A list of trading symbols (e.g., ['AAPL', 'MSFT']).
|
|
1299
1348
|
params: A dictionary containing parameters for the Trade instance.
|
|
1349
|
+
daily_risk: A dictionary containing daily risk weight for each symbol.
|
|
1350
|
+
max_risk: A dictionary containing maximum risk weight for each symbol.
|
|
1351
|
+
logger: A logger instance.
|
|
1300
1352
|
|
|
1301
1353
|
Returns:
|
|
1302
1354
|
A dictionary where keys are symbols and values are corresponding Trade instances.
|
|
1303
1355
|
|
|
1304
1356
|
Raises:
|
|
1305
1357
|
ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
|
|
1358
|
+
|
|
1359
|
+
Note:
|
|
1360
|
+
`daily_risk` and `max_risk` can be used to manage the risk of each symbol
|
|
1361
|
+
based on the importance of the symbol in the portfolio or strategy.
|
|
1306
1362
|
"""
|
|
1307
1363
|
instances = {}
|
|
1308
1364
|
if not symbols:
|
|
1309
1365
|
raise ValueError("The 'symbols' list cannot be empty.")
|
|
1366
|
+
if not params:
|
|
1367
|
+
raise ValueError("The 'params' dictionary cannot be empty.")
|
|
1368
|
+
|
|
1369
|
+
if daily_risk is not None:
|
|
1370
|
+
for symbol in symbols:
|
|
1371
|
+
if symbol not in daily_risk:
|
|
1372
|
+
raise ValueError(f"Missing daily risk weight for symbol '{symbol}'.")
|
|
1373
|
+
if max_risk is not None:
|
|
1374
|
+
for symbol in symbols:
|
|
1375
|
+
if symbol not in max_risk:
|
|
1376
|
+
raise ValueError(f"Missing maximum risk percentage for symbol '{symbol}'.")
|
|
1377
|
+
if pchange_sl is not None:
|
|
1378
|
+
if isinstance(pchange_sl, dict):
|
|
1379
|
+
for symbol in symbols:
|
|
1380
|
+
if symbol not in pchange_sl:
|
|
1381
|
+
raise ValueError(f"Missing percentage change for symbol '{symbol}'.")
|
|
1382
|
+
|
|
1310
1383
|
for symbol in symbols:
|
|
1311
1384
|
try:
|
|
1312
|
-
|
|
1385
|
+
params['symbol'] = symbol
|
|
1386
|
+
params['pchange_sl'] = (
|
|
1387
|
+
pchange_sl[symbol] if pchange_sl is not None
|
|
1388
|
+
and isinstance(pchange_sl, dict) else pchange_sl
|
|
1389
|
+
)
|
|
1390
|
+
params['daily_risk'] = daily_risk[symbol] if daily_risk is not None else params['daily_risk']
|
|
1391
|
+
params['max_risk'] = max_risk[symbol] if max_risk is not None else params['max_risk']
|
|
1392
|
+
instances[symbol] = Trade(**params)
|
|
1313
1393
|
except Exception as e:
|
|
1314
1394
|
logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
|
|
1315
|
-
|
|
1316
|
-
|
|
1395
|
+
|
|
1396
|
+
if len(instances) != len(symbols):
|
|
1397
|
+
for symbol in symbols:
|
|
1398
|
+
if symbol not in instances:
|
|
1399
|
+
if logger is not None:
|
|
1400
|
+
logger.error(f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1401
|
+
else:
|
|
1402
|
+
raise ValueError(f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1403
|
+
return instances
|