bbstrader 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of bbstrader might be problematic. Click here for more details.
- bbstrader/__ini__.py +7 -7
- bbstrader/btengine/__init__.py +7 -7
- bbstrader/btengine/backtest.py +30 -26
- bbstrader/btengine/data.py +92 -81
- bbstrader/btengine/event.py +2 -1
- bbstrader/btengine/execution.py +18 -16
- bbstrader/btengine/performance.py +11 -7
- bbstrader/btengine/portfolio.py +35 -36
- bbstrader/btengine/strategy.py +113 -92
- bbstrader/config.py +12 -10
- bbstrader/core/data.py +4 -5
- bbstrader/core/utils.py +57 -0
- bbstrader/ibkr/utils.py +0 -0
- bbstrader/metatrader/__init__.py +5 -5
- bbstrader/metatrader/account.py +117 -121
- bbstrader/metatrader/rates.py +81 -78
- bbstrader/metatrader/risk.py +23 -37
- bbstrader/metatrader/trade.py +154 -138
- bbstrader/metatrader/utils.py +3 -3
- bbstrader/models/__init__.py +5 -5
- bbstrader/models/factors.py +17 -12
- bbstrader/models/ml.py +371 -305
- bbstrader/models/optimization.py +14 -12
- bbstrader/models/portfolio.py +44 -35
- bbstrader/models/risk.py +15 -9
- bbstrader/trading/__init__.py +2 -2
- bbstrader/trading/execution.py +245 -179
- bbstrader/trading/scripts.py +8 -4
- bbstrader/trading/strategies.py +78 -65
- bbstrader/tseries.py +124 -98
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/LICENSE +1 -1
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/METADATA +2 -1
- bbstrader-0.2.1.dist-info/RECORD +37 -0
- bbstrader-0.2.0.dist-info/RECORD +0 -36
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/WHEEL +0 -0
- {bbstrader-0.2.0.dist-info → bbstrader-0.2.1.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/trade.py
CHANGED
|
@@ -1,36 +1,22 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import csv
|
|
3
2
|
import time
|
|
4
|
-
import numpy as np
|
|
5
|
-
import pandas as pd
|
|
6
3
|
from datetime import datetime
|
|
7
|
-
import MetaTrader5 as Mt5
|
|
8
4
|
from logging import Logger
|
|
5
|
+
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import MetaTrader5 as Mt5
|
|
8
|
+
import pandas as pd
|
|
9
9
|
from tabulate import tabulate
|
|
10
|
-
|
|
11
|
-
List,
|
|
12
|
-
Tuple,
|
|
13
|
-
Dict,
|
|
14
|
-
Any,
|
|
15
|
-
Optional,
|
|
16
|
-
Literal,
|
|
17
|
-
Callable
|
|
18
|
-
)
|
|
10
|
+
|
|
19
11
|
from bbstrader.btengine.performance import create_sharpe_ratio
|
|
12
|
+
from bbstrader.config import BBSTRADER_DIR, config_logger
|
|
13
|
+
from bbstrader.metatrader.account import INIT_MSG, check_mt5_connection
|
|
20
14
|
from bbstrader.metatrader.risk import RiskManagement
|
|
21
|
-
from bbstrader.metatrader.account import(
|
|
22
|
-
check_mt5_connection,
|
|
23
|
-
INIT_MSG
|
|
24
|
-
)
|
|
25
15
|
from bbstrader.metatrader.utils import (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
raise_mt5_error,
|
|
30
|
-
trade_retcode_message
|
|
16
|
+
TradePosition,
|
|
17
|
+
raise_mt5_error,
|
|
18
|
+
trade_retcode_message,
|
|
31
19
|
)
|
|
32
|
-
from bbstrader.config import config_logger, BBSTRADER_DIR
|
|
33
|
-
|
|
34
20
|
|
|
35
21
|
__all__ = [
|
|
36
22
|
'Trade',
|
|
@@ -168,13 +154,14 @@ class Trade(RiskManagement):
|
|
|
168
154
|
":")
|
|
169
155
|
self.ending_time_hour, self.ending_time_minutes = self.end.split(":")
|
|
170
156
|
|
|
171
|
-
self.buy_positions
|
|
172
|
-
self.sell_positions
|
|
173
|
-
self.opened_positions
|
|
174
|
-
self.opened_orders
|
|
175
|
-
self.break_even_status
|
|
176
|
-
self.break_even_points
|
|
157
|
+
self.buy_positions = []
|
|
158
|
+
self.sell_positions = []
|
|
159
|
+
self.opened_positions = []
|
|
160
|
+
self.opened_orders = []
|
|
161
|
+
self.break_even_status = []
|
|
162
|
+
self.break_even_points = {}
|
|
177
163
|
self.trail_after_points = []
|
|
164
|
+
self._retcodes = []
|
|
178
165
|
|
|
179
166
|
self.initialize(**kwargs)
|
|
180
167
|
self.select_symbol(**kwargs)
|
|
@@ -188,14 +175,19 @@ class Trade(RiskManagement):
|
|
|
188
175
|
print(
|
|
189
176
|
f">>> Everything is OK, @{self.expert_name} is Running ...>>>\n")
|
|
190
177
|
|
|
191
|
-
|
|
178
|
+
@property
|
|
179
|
+
def retcodes(self) -> List[int]:
|
|
180
|
+
"""Return all the retcodes"""
|
|
181
|
+
return self._retcodes
|
|
182
|
+
|
|
183
|
+
def _get_logger(self, logger: str | Logger, consol_log: bool) -> Logger:
|
|
192
184
|
"""Get the logger object"""
|
|
193
185
|
if isinstance(logger, str):
|
|
194
186
|
log_path = BBSTRADER_DIR / 'logs'
|
|
195
187
|
log_path.mkdir(exist_ok=True)
|
|
196
188
|
return config_logger(f'{log_path}/{logger}', consol_log)
|
|
197
189
|
return logger
|
|
198
|
-
|
|
190
|
+
|
|
199
191
|
def initialize(self, **kwargs):
|
|
200
192
|
"""
|
|
201
193
|
Initializes the MetaTrader 5 (MT5) terminal for trading operations.
|
|
@@ -266,12 +258,16 @@ class Trade(RiskManagement):
|
|
|
266
258
|
["Expert | Strategy ID", self.expert_id],
|
|
267
259
|
["Trading Symbol", self.symbol],
|
|
268
260
|
["Trading Time Frame", self.tf],
|
|
269
|
-
["Start Trading Time",
|
|
270
|
-
|
|
271
|
-
["
|
|
261
|
+
["Start Trading Time",
|
|
262
|
+
f"{self.start_time_hour}:{self.start_time_minutes}"],
|
|
263
|
+
["Finishing Trading Time",
|
|
264
|
+
f"{self.finishing_time_hour}:{self.finishing_time_minutes}"],
|
|
265
|
+
["Closing Position After",
|
|
266
|
+
f"{self.ending_time_hour}:{self.ending_time_minutes}"],
|
|
272
267
|
]
|
|
273
268
|
# Custom table format
|
|
274
|
-
summary_table = tabulate(summary_data, headers=[
|
|
269
|
+
summary_table = tabulate(summary_data, headers=[
|
|
270
|
+
"Summary", "Values"], tablefmt="outline")
|
|
275
271
|
|
|
276
272
|
# Print the table
|
|
277
273
|
print("\n[======= Trade Account Summary =======]")
|
|
@@ -299,10 +295,12 @@ class Trade(RiskManagement):
|
|
|
299
295
|
["Account Margin", f"{round(account_info.margin, 2)} {currency}"],
|
|
300
296
|
["Account Free Margin", f"{account_info.margin_free} {currency}"],
|
|
301
297
|
["Maximum Drawdown", f"{self.max_risk}%"],
|
|
302
|
-
["Risk Allowed",
|
|
298
|
+
["Risk Allowed",
|
|
299
|
+
f"{round((self.max_risk - self.risk_level()), 2)}%"],
|
|
303
300
|
["Volume", f"{self.volume()} {marging_currency}"],
|
|
304
301
|
["Risk Per trade", f"{-self.get_currency_risk()} {currency}"],
|
|
305
|
-
["Profit Expected Per trade",
|
|
302
|
+
["Profit Expected Per trade",
|
|
303
|
+
f"{self.expected_profit()} {currency}"],
|
|
306
304
|
["Lot Size", f"{self.get_lot()} Lots"],
|
|
307
305
|
["Stop Loss", f"{self.get_stop_loss()} Points"],
|
|
308
306
|
["Loss Value Per Tick", f"{round(loss, 5)} {currency}"],
|
|
@@ -316,7 +314,8 @@ class Trade(RiskManagement):
|
|
|
316
314
|
]
|
|
317
315
|
# Custom table format
|
|
318
316
|
print("\n[======= Account Risk Management Overview =======]")
|
|
319
|
-
table = tabulate(account_data, headers=[
|
|
317
|
+
table = tabulate(account_data, headers=[
|
|
318
|
+
"Risk Metrics", "Values"], tablefmt="outline")
|
|
320
319
|
|
|
321
320
|
# Print the table
|
|
322
321
|
print(table)
|
|
@@ -338,7 +337,6 @@ class Trade(RiskManagement):
|
|
|
338
337
|
currency = self.get_account_info().currency
|
|
339
338
|
net_profit = round((profit + total_fees), 2)
|
|
340
339
|
trade_risk = round(self.get_currency_risk() * -1, 2)
|
|
341
|
-
expected_profit = round((trade_risk * self.rr * -1), 2)
|
|
342
340
|
|
|
343
341
|
# Formatting the statistics output
|
|
344
342
|
session_data = [
|
|
@@ -350,7 +348,8 @@ class Trade(RiskManagement):
|
|
|
350
348
|
["Average Fees", f"{average_fee} {currency}"],
|
|
351
349
|
["Net Profit", f"{net_profit} {currency}"],
|
|
352
350
|
["Risk per Trade", f"{trade_risk} {currency}"],
|
|
353
|
-
["Expected Profit per Trade",
|
|
351
|
+
["Expected Profit per Trade",
|
|
352
|
+
f"{self.expected_profit()} {currency}"],
|
|
354
353
|
["Risk Reward Ratio", self.rr],
|
|
355
354
|
["Win Rate", f"{win_rate}%"],
|
|
356
355
|
["Sharpe Ratio", self.sharpe()],
|
|
@@ -384,6 +383,7 @@ class Trade(RiskManagement):
|
|
|
384
383
|
self.logger.info(f"Session statistics saved to {filepath}")
|
|
385
384
|
|
|
386
385
|
Buys = Literal['BMKT', 'BLMT', 'BSTP', 'BSTPLMT']
|
|
386
|
+
|
|
387
387
|
def open_buy_position(
|
|
388
388
|
self,
|
|
389
389
|
action: Buys = 'BMKT',
|
|
@@ -416,7 +416,6 @@ class Trade(RiskManagement):
|
|
|
416
416
|
"You need to set a price for pending orders")
|
|
417
417
|
else:
|
|
418
418
|
_price = self.get_tick_info(self.symbol).ask
|
|
419
|
-
digits = self.get_symbol_info(self.symbol).digits
|
|
420
419
|
|
|
421
420
|
lot = self.get_lot()
|
|
422
421
|
stop_loss = self.get_stop_loss()
|
|
@@ -468,6 +467,7 @@ class Trade(RiskManagement):
|
|
|
468
467
|
return type
|
|
469
468
|
|
|
470
469
|
Sells = Literal['SMKT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
470
|
+
|
|
471
471
|
def open_sell_position(
|
|
472
472
|
self,
|
|
473
473
|
action: Sells = 'SMKT',
|
|
@@ -500,7 +500,6 @@ class Trade(RiskManagement):
|
|
|
500
500
|
"You need to set a price for pending orders")
|
|
501
501
|
else:
|
|
502
502
|
_price = self.get_tick_info(self.symbol).bid
|
|
503
|
-
digits = self.get_symbol_info(self.symbol).digits
|
|
504
503
|
|
|
505
504
|
lot = self.get_lot()
|
|
506
505
|
stop_loss = self.get_stop_loss()
|
|
@@ -553,7 +552,8 @@ class Trade(RiskManagement):
|
|
|
553
552
|
self.logger.info(f"Not Trading time, SYMBOL={self.symbol}")
|
|
554
553
|
return False
|
|
555
554
|
elif not self.is_risk_ok():
|
|
556
|
-
self.logger.error(
|
|
555
|
+
self.logger.error(
|
|
556
|
+
f"Account Risk not allowed, SYMBOL={self.symbol}")
|
|
557
557
|
self._check(comment)
|
|
558
558
|
return False
|
|
559
559
|
elif self.profit_target():
|
|
@@ -561,9 +561,9 @@ class Trade(RiskManagement):
|
|
|
561
561
|
return True
|
|
562
562
|
|
|
563
563
|
def _check(self, txt: str = ""):
|
|
564
|
-
if (self.positive_profit(id=self.expert_id)
|
|
565
|
-
|
|
566
|
-
|
|
564
|
+
if (self.positive_profit(id=self.expert_id)
|
|
565
|
+
or self.get_current_positions() is None
|
|
566
|
+
):
|
|
567
567
|
self.close_positions(position_type='all')
|
|
568
568
|
self.logger.info(txt)
|
|
569
569
|
time.sleep(5)
|
|
@@ -582,7 +582,7 @@ class Trade(RiskManagement):
|
|
|
582
582
|
price (float): Price for opening the position
|
|
583
583
|
request (Dict[str, Any]): A trade request to sent to Mt5.order_sent()
|
|
584
584
|
all detail in request can be found here https://www.mql5.com/en/docs/python_metatrader5/mt5ordersend_py
|
|
585
|
-
|
|
585
|
+
|
|
586
586
|
type (str): The type of the order `(BMKT, SMKT, BLMT, SLMT, BSTP, SSTP, BSTPLMT, SSTPLMT)`
|
|
587
587
|
"""
|
|
588
588
|
# Send a trading request
|
|
@@ -590,23 +590,25 @@ class Trade(RiskManagement):
|
|
|
590
590
|
pos = self._order_type()[type][1]
|
|
591
591
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
592
592
|
try:
|
|
593
|
-
|
|
593
|
+
self.check_order(request)
|
|
594
594
|
result = self.send_order(request)
|
|
595
595
|
except Exception as e:
|
|
596
596
|
print(f"{self.current_datetime()} -", end=' ')
|
|
597
597
|
trade_retcode_message(
|
|
598
598
|
result.retcode, display=True, add_msg=f"{e}{addtionnal}")
|
|
599
599
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
600
|
+
if result.retcode not in self._retcodes:
|
|
601
|
+
self._retcodes.append(result.retcode)
|
|
602
|
+
msg = trade_retcode_message(result.retcode)
|
|
603
|
+
self.logger.error(
|
|
604
|
+
f"Trade Order Request, RETCODE={result.retcode}: {msg}{addtionnal}")
|
|
603
605
|
if result.retcode in [
|
|
604
606
|
Mt5.TRADE_RETCODE_CONNECTION, Mt5.TRADE_RETCODE_TIMEOUT]:
|
|
605
607
|
tries = 0
|
|
606
608
|
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
607
609
|
time.sleep(1)
|
|
608
610
|
try:
|
|
609
|
-
|
|
611
|
+
self.check_order(request)
|
|
610
612
|
result = self.send_order(request)
|
|
611
613
|
except Exception as e:
|
|
612
614
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -650,7 +652,7 @@ class Trade(RiskManagement):
|
|
|
650
652
|
f"{self.get_account_info().currency}]\n"
|
|
651
653
|
)
|
|
652
654
|
self.logger.info(pos_info)
|
|
653
|
-
|
|
655
|
+
|
|
654
656
|
def open_position(
|
|
655
657
|
self,
|
|
656
658
|
action: Buys | Sells,
|
|
@@ -682,7 +684,8 @@ class Trade(RiskManagement):
|
|
|
682
684
|
self.open_sell_position(
|
|
683
685
|
action=action, price=price, stoplimit=stoplimit, id=id, mm=mm, comment=comment)
|
|
684
686
|
else:
|
|
685
|
-
raise ValueError(
|
|
687
|
+
raise ValueError(
|
|
688
|
+
f"Invalid action type '{action}', must be {', '.join(BUYS + SELLS)}")
|
|
686
689
|
|
|
687
690
|
@property
|
|
688
691
|
def orders(self):
|
|
@@ -766,7 +769,7 @@ class Trade(RiskManagement):
|
|
|
766
769
|
continue
|
|
767
770
|
if filter_type == 'sells' and item.type != 1:
|
|
768
771
|
continue
|
|
769
|
-
if filter_type == 'losings' and
|
|
772
|
+
if filter_type == 'losings' and item.profit > 0:
|
|
770
773
|
continue
|
|
771
774
|
if filter_type == 'profitables' and not self.win_trade(item, th=th):
|
|
772
775
|
continue
|
|
@@ -791,19 +794,19 @@ class Trade(RiskManagement):
|
|
|
791
794
|
|
|
792
795
|
def get_current_buy_stops(self, id: Optional[int] = None) -> List[int] | None:
|
|
793
796
|
return self.get_filtered_tickets(id=id, filter_type='buy_stops')
|
|
794
|
-
|
|
797
|
+
|
|
795
798
|
def get_current_sell_stops(self, id: Optional[int] = None) -> List[int] | None:
|
|
796
799
|
return self.get_filtered_tickets(id=id, filter_type='sell_stops')
|
|
797
|
-
|
|
800
|
+
|
|
798
801
|
def get_current_buy_limits(self, id: Optional[int] = None) -> List[int] | None:
|
|
799
802
|
return self.get_filtered_tickets(id=id, filter_type='buy_limits')
|
|
800
|
-
|
|
803
|
+
|
|
801
804
|
def get_current_sell_limits(self, id: Optional[int] = None) -> List[int] | None:
|
|
802
805
|
return self.get_filtered_tickets(id=id, filter_type='sell_limits')
|
|
803
|
-
|
|
806
|
+
|
|
804
807
|
def get_current_buy_stop_limits(self, id: Optional[int] = None) -> List[int] | None:
|
|
805
808
|
return self.get_filtered_tickets(id=id, filter_type='buy_stop_limits')
|
|
806
|
-
|
|
809
|
+
|
|
807
810
|
def get_current_sell_stop_limits(self, id: Optional[int] = None) -> List[int] | None:
|
|
808
811
|
return self.get_filtered_tickets(id=id, filter_type='sell_stop_limits')
|
|
809
812
|
|
|
@@ -842,7 +845,8 @@ class Trade(RiskManagement):
|
|
|
842
845
|
# All open positions for a specific strategy or expert no matter the symbol
|
|
843
846
|
positions = self.get_positions()
|
|
844
847
|
if positions is not None:
|
|
845
|
-
positions = [
|
|
848
|
+
positions = [
|
|
849
|
+
position for position in positions if position.magic == id]
|
|
846
850
|
elif not account and id is None:
|
|
847
851
|
# All open positions for the current symbol no matter the strategy or expert
|
|
848
852
|
positions = self.get_positions(symbol=self.symbol)
|
|
@@ -850,8 +854,9 @@ class Trade(RiskManagement):
|
|
|
850
854
|
# All open positions for the current symbol and a specific strategy or expert
|
|
851
855
|
positions = self.get_positions(symbol=self.symbol)
|
|
852
856
|
if positions is not None:
|
|
853
|
-
positions = [
|
|
854
|
-
|
|
857
|
+
positions = [
|
|
858
|
+
position for position in positions if position.magic == id]
|
|
859
|
+
|
|
855
860
|
if positions is not None:
|
|
856
861
|
profit = 0.0
|
|
857
862
|
balance = self.get_account_info().balance
|
|
@@ -864,7 +869,7 @@ class Trade(RiskManagement):
|
|
|
864
869
|
return current_profit >= th_profit
|
|
865
870
|
return False
|
|
866
871
|
|
|
867
|
-
def break_even(self, mm=True,
|
|
872
|
+
def break_even(self, mm=True,
|
|
868
873
|
id: Optional[int] = None,
|
|
869
874
|
trail: Optional[bool] = True,
|
|
870
875
|
stop_trail: Optional[int] = None,
|
|
@@ -913,7 +918,8 @@ class Trade(RiskManagement):
|
|
|
913
918
|
if position.ticket not in self.break_even_status:
|
|
914
919
|
price = None
|
|
915
920
|
if be_plus_points is not None:
|
|
916
|
-
price = position.price_open +
|
|
921
|
+
price = position.price_open + \
|
|
922
|
+
(be_plus_points * point)
|
|
917
923
|
self.set_break_even(position, be, price=price)
|
|
918
924
|
self.break_even_status.append(position.ticket)
|
|
919
925
|
self.break_even_points[position.ticket] = be
|
|
@@ -922,32 +928,40 @@ class Trade(RiskManagement):
|
|
|
922
928
|
if not trail:
|
|
923
929
|
continue
|
|
924
930
|
# Check if the price has moved favorably
|
|
925
|
-
new_be = round(
|
|
931
|
+
new_be = round(
|
|
932
|
+
be * 0.10) if be_plus_points is None else be_plus_points
|
|
926
933
|
if trail_after_points is not None:
|
|
927
934
|
if position.ticket not in self.trail_after_points:
|
|
928
935
|
# This ensures that the position rich the minimum points required
|
|
929
936
|
# before the trail can be set
|
|
930
937
|
new_be = trail_after_points - be
|
|
931
|
-
self.trail_after_points.append(
|
|
938
|
+
self.trail_after_points.append(
|
|
939
|
+
position.ticket)
|
|
932
940
|
new_be_points = self.break_even_points[position.ticket] + new_be
|
|
933
|
-
favorable_move = float(
|
|
941
|
+
favorable_move = float(
|
|
942
|
+
points/point) >= new_be_points
|
|
934
943
|
if favorable_move:
|
|
935
944
|
# This allows the position to go to take profit in case of a swing trade
|
|
936
945
|
# If is a scalping position, we can set the stop_trail close to the current price.
|
|
937
|
-
trail_points = round(
|
|
946
|
+
trail_points = round(
|
|
947
|
+
be * 0.50) if stop_trail is None else stop_trail
|
|
938
948
|
# Calculate the new break-even level and price
|
|
939
949
|
if position.type == 0:
|
|
940
950
|
# This level validate the favorable move of the price
|
|
941
|
-
new_level = round(
|
|
942
|
-
|
|
943
|
-
|
|
951
|
+
new_level = round(
|
|
952
|
+
position.price_open + (new_be_points * point), digits)
|
|
953
|
+
# This price is set away from the current price by the trail_points
|
|
954
|
+
new_price = round(
|
|
955
|
+
position.price_current - (trail_points * point), digits)
|
|
944
956
|
if new_price < position.sl:
|
|
945
|
-
new_price = position.sl
|
|
957
|
+
new_price = position.sl
|
|
946
958
|
elif position.type == 1:
|
|
947
|
-
new_level = round(
|
|
948
|
-
|
|
959
|
+
new_level = round(
|
|
960
|
+
position.price_open - (new_be_points * point), digits)
|
|
961
|
+
new_price = round(
|
|
962
|
+
position.price_current + (trail_points * point), digits)
|
|
949
963
|
if new_price > position.sl:
|
|
950
|
-
new_price = position.sl
|
|
964
|
+
new_price = position.sl
|
|
951
965
|
self.set_break_even(
|
|
952
966
|
position, be, price=new_price, level=new_level
|
|
953
967
|
)
|
|
@@ -1027,7 +1041,7 @@ class Trade(RiskManagement):
|
|
|
1027
1041
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
1028
1042
|
time.sleep(0.1)
|
|
1029
1043
|
try:
|
|
1030
|
-
|
|
1044
|
+
self.check_order(request)
|
|
1031
1045
|
result = self.send_order(request)
|
|
1032
1046
|
except Exception as e:
|
|
1033
1047
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -1045,7 +1059,7 @@ class Trade(RiskManagement):
|
|
|
1045
1059
|
else:
|
|
1046
1060
|
time.sleep(1)
|
|
1047
1061
|
try:
|
|
1048
|
-
|
|
1062
|
+
self.check_order(request)
|
|
1049
1063
|
result = self.send_order(request)
|
|
1050
1064
|
except Exception as e:
|
|
1051
1065
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -1078,7 +1092,6 @@ class Trade(RiskManagement):
|
|
|
1078
1092
|
value = self.get_symbol_info(self.symbol).trade_tick_value
|
|
1079
1093
|
points = position.profit * (size / value / position.volume)
|
|
1080
1094
|
|
|
1081
|
-
spread = self.get_symbol_info(self.symbol).spread
|
|
1082
1095
|
point = self.get_symbol_info(self.symbol).point
|
|
1083
1096
|
fees = self.get_stats()[0]["average_fee"] * -1
|
|
1084
1097
|
risk = self.currency_risk()["trade_profit"]
|
|
@@ -1088,7 +1101,7 @@ class Trade(RiskManagement):
|
|
|
1088
1101
|
win_be = th
|
|
1089
1102
|
else:
|
|
1090
1103
|
win_be = max(min_be, round((0.1 * be)))
|
|
1091
|
-
win_trade = float(points/point) >=
|
|
1104
|
+
win_trade = float(points/point) >= win_be
|
|
1092
1105
|
# Check if the positon is in profit
|
|
1093
1106
|
if win_trade:
|
|
1094
1107
|
# Check if break-even has already been set for this position
|
|
@@ -1133,7 +1146,7 @@ class Trade(RiskManagement):
|
|
|
1133
1146
|
ticket = request[type]
|
|
1134
1147
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
1135
1148
|
try:
|
|
1136
|
-
|
|
1149
|
+
self.check_order(request)
|
|
1137
1150
|
result = self.send_order(request)
|
|
1138
1151
|
except Exception as e:
|
|
1139
1152
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -1147,7 +1160,7 @@ class Trade(RiskManagement):
|
|
|
1147
1160
|
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
1148
1161
|
time.sleep(1)
|
|
1149
1162
|
try:
|
|
1150
|
-
|
|
1163
|
+
self.check_order(request)
|
|
1151
1164
|
result = self.send_order(request)
|
|
1152
1165
|
except Exception as e:
|
|
1153
1166
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -1166,10 +1179,10 @@ class Trade(RiskManagement):
|
|
|
1166
1179
|
return True
|
|
1167
1180
|
else:
|
|
1168
1181
|
return False
|
|
1169
|
-
|
|
1170
|
-
def close_order(self,
|
|
1171
|
-
ticket: int,
|
|
1172
|
-
id: Optional[int] = None,
|
|
1182
|
+
|
|
1183
|
+
def close_order(self,
|
|
1184
|
+
ticket: int,
|
|
1185
|
+
id: Optional[int] = None,
|
|
1173
1186
|
comment: Optional[str] = None):
|
|
1174
1187
|
"""
|
|
1175
1188
|
Close an open order by it ticket
|
|
@@ -1183,14 +1196,14 @@ class Trade(RiskManagement):
|
|
|
1183
1196
|
- True if order closed, False otherwise
|
|
1184
1197
|
"""
|
|
1185
1198
|
request = {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1199
|
+
"action": Mt5.TRADE_ACTION_REMOVE,
|
|
1200
|
+
"symbol": self.symbol,
|
|
1201
|
+
"order": ticket,
|
|
1202
|
+
"magic": id if id is not None else self.expert_id,
|
|
1203
|
+
"comment": f"@{self.expert_name}" if comment is None else comment,
|
|
1204
|
+
}
|
|
1192
1205
|
return self.close_request(request, type="order")
|
|
1193
|
-
|
|
1206
|
+
|
|
1194
1207
|
def close_position(self,
|
|
1195
1208
|
ticket: int,
|
|
1196
1209
|
id: Optional[int] = None,
|
|
@@ -1215,15 +1228,13 @@ class Trade(RiskManagement):
|
|
|
1215
1228
|
positions = self.get_positions(ticket=ticket)
|
|
1216
1229
|
buy_price = self.get_tick_info(self.symbol).ask
|
|
1217
1230
|
sell_price = self.get_tick_info(self.symbol).bid
|
|
1218
|
-
digits = self.get_symbol_info(self.symbol).digits
|
|
1219
1231
|
deviation = self.get_deviation()
|
|
1220
1232
|
if positions is not None and len(positions) == 1:
|
|
1221
1233
|
position = positions[0]
|
|
1222
1234
|
if (position.ticket == ticket
|
|
1223
|
-
|
|
1224
|
-
|
|
1235
|
+
and position.magic == Id
|
|
1236
|
+
):
|
|
1225
1237
|
buy = position.type == 0
|
|
1226
|
-
sell = position.type == 1
|
|
1227
1238
|
request = {
|
|
1228
1239
|
"action": Mt5.TRADE_ACTION_DEAL,
|
|
1229
1240
|
"symbol": self.symbol,
|
|
@@ -1238,15 +1249,14 @@ class Trade(RiskManagement):
|
|
|
1238
1249
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
1239
1250
|
}
|
|
1240
1251
|
return self.close_request(request, type="position")
|
|
1241
|
-
|
|
1242
|
-
def bulk_close(self,
|
|
1243
|
-
tickets: List,
|
|
1252
|
+
|
|
1253
|
+
def bulk_close(self,
|
|
1254
|
+
tickets: List,
|
|
1244
1255
|
tikets_type: Literal["positions", "orders"],
|
|
1245
|
-
close_func: Callable,
|
|
1246
|
-
order_type: str,
|
|
1256
|
+
close_func: Callable,
|
|
1257
|
+
order_type: str,
|
|
1247
1258
|
id: Optional[int] = None,
|
|
1248
1259
|
comment: Optional[str] = None):
|
|
1249
|
-
|
|
1250
1260
|
"""
|
|
1251
1261
|
Close multiple orders or positions at once.
|
|
1252
1262
|
|
|
@@ -1260,13 +1270,12 @@ class Trade(RiskManagement):
|
|
|
1260
1270
|
"""
|
|
1261
1271
|
if order_type == 'all':
|
|
1262
1272
|
order_type = 'open'
|
|
1263
|
-
if len(tickets) > 0:
|
|
1273
|
+
if tickets is not None and len(tickets) > 0:
|
|
1264
1274
|
for ticket in tickets.copy():
|
|
1265
1275
|
if close_func(ticket, id=id, comment=comment):
|
|
1266
1276
|
tickets.remove(ticket)
|
|
1267
1277
|
time.sleep(1)
|
|
1268
|
-
|
|
1269
|
-
if len(tickets) == 0:
|
|
1278
|
+
if tickets is not None and len(tickets) == 0:
|
|
1270
1279
|
self.logger.info(
|
|
1271
1280
|
f"ALL {order_type.upper()} {tikets_type.upper()} closed, SYMBOL={self.symbol}.")
|
|
1272
1281
|
else:
|
|
@@ -1276,11 +1285,12 @@ class Trade(RiskManagement):
|
|
|
1276
1285
|
self.logger.info(
|
|
1277
1286
|
f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}.")
|
|
1278
1287
|
|
|
1279
|
-
Orders = Literal["all", "buy_stops", "sell_stops", "buy_limits",
|
|
1288
|
+
Orders = Literal["all", "buy_stops", "sell_stops", "buy_limits",
|
|
1280
1289
|
"sell_limits", "buy_stop_limits", "sell_stop_limits"]
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1290
|
+
|
|
1291
|
+
def close_orders(self,
|
|
1292
|
+
order_type: Orders,
|
|
1293
|
+
id: Optional[int] = None,
|
|
1284
1294
|
comment: Optional[str] = None):
|
|
1285
1295
|
"""
|
|
1286
1296
|
Args:
|
|
@@ -1310,6 +1320,7 @@ class Trade(RiskManagement):
|
|
|
1310
1320
|
orders, "orders", self.close_order, order_type, id=id, comment=comment)
|
|
1311
1321
|
|
|
1312
1322
|
Positions = Literal["all", "buy", "sell", "profitable", "losing"]
|
|
1323
|
+
|
|
1313
1324
|
def close_positions(
|
|
1314
1325
|
self,
|
|
1315
1326
|
position_type: Positions,
|
|
@@ -1350,7 +1361,6 @@ class Trade(RiskManagement):
|
|
|
1350
1361
|
loss_trades = 0
|
|
1351
1362
|
win_trades = 0
|
|
1352
1363
|
balance = self.get_account_info().balance
|
|
1353
|
-
target = round((balance * self.target)/100)
|
|
1354
1364
|
deals = len(self.opened_positions)
|
|
1355
1365
|
if deals != 0:
|
|
1356
1366
|
for position in self.opened_positions:
|
|
@@ -1477,27 +1487,28 @@ class Trade(RiskManagement):
|
|
|
1477
1487
|
else:
|
|
1478
1488
|
# claculate number of minute from the end to the start
|
|
1479
1489
|
start = datetime.strptime(self.start, '%H:%M')
|
|
1480
|
-
end =
|
|
1490
|
+
end = datetime.strptime(self.current_time(), '%H:%M')
|
|
1481
1491
|
minutes = (end - start).total_seconds() // 60
|
|
1482
1492
|
sleep_time = (24*60) - minutes
|
|
1483
1493
|
return sleep_time
|
|
1484
1494
|
|
|
1485
1495
|
def current_datetime(self):
|
|
1486
1496
|
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1487
|
-
|
|
1497
|
+
|
|
1488
1498
|
def current_time(self, seconds=False):
|
|
1489
1499
|
if seconds:
|
|
1490
1500
|
return datetime.now().strftime("%H:%M:%S")
|
|
1491
1501
|
return datetime.now().strftime("%H:%M")
|
|
1492
1502
|
|
|
1503
|
+
|
|
1493
1504
|
def create_trade_instance(
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1505
|
+
symbols: List[str],
|
|
1506
|
+
params: Dict[str, Any],
|
|
1507
|
+
daily_risk: Optional[Dict[str, float]] = None,
|
|
1508
|
+
max_risk: Optional[Dict[str, float]] = None,
|
|
1509
|
+
pchange_sl: Optional[Dict[str, float] | float] = None,
|
|
1510
|
+
**kwargs
|
|
1511
|
+
) -> Dict[str, Trade]:
|
|
1501
1512
|
"""
|
|
1502
1513
|
Creates Trade instances for each symbol provided.
|
|
1503
1514
|
|
|
@@ -1512,7 +1523,7 @@ def create_trade_instance(
|
|
|
1512
1523
|
|
|
1513
1524
|
Raises:
|
|
1514
1525
|
ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
|
|
1515
|
-
|
|
1526
|
+
|
|
1516
1527
|
Note:
|
|
1517
1528
|
`daily_risk` and `max_risk` can be used to manage the risk of each symbol
|
|
1518
1529
|
based on the importance of the symbol in the portfolio or strategy.
|
|
@@ -1524,57 +1535,62 @@ def create_trade_instance(
|
|
|
1524
1535
|
raise ValueError("The 'symbols' list cannot be empty.")
|
|
1525
1536
|
if not params:
|
|
1526
1537
|
raise ValueError("The 'params' dictionary cannot be empty.")
|
|
1527
|
-
|
|
1538
|
+
|
|
1528
1539
|
if daily_risk is not None:
|
|
1529
1540
|
for symbol in symbols:
|
|
1530
1541
|
if symbol not in daily_risk:
|
|
1531
|
-
raise ValueError(
|
|
1542
|
+
raise ValueError(
|
|
1543
|
+
f"Missing daily risk weight for symbol '{symbol}'.")
|
|
1532
1544
|
if max_risk is not None:
|
|
1533
1545
|
for symbol in symbols:
|
|
1534
1546
|
if symbol not in max_risk:
|
|
1535
|
-
raise ValueError(
|
|
1547
|
+
raise ValueError(
|
|
1548
|
+
f"Missing maximum risk percentage for symbol '{symbol}'.")
|
|
1536
1549
|
if pchange_sl is not None:
|
|
1537
1550
|
if isinstance(pchange_sl, dict):
|
|
1538
1551
|
for symbol in symbols:
|
|
1539
1552
|
if symbol not in pchange_sl:
|
|
1540
|
-
raise ValueError(
|
|
1553
|
+
raise ValueError(
|
|
1554
|
+
f"Missing percentage change for symbol '{symbol}'.")
|
|
1541
1555
|
if isinstance(ids, dict):
|
|
1542
1556
|
for symbol in symbols:
|
|
1543
1557
|
if symbol not in ids:
|
|
1544
1558
|
raise ValueError(f"Missing expert ID for symbol '{symbol}'.")
|
|
1545
|
-
|
|
1559
|
+
|
|
1546
1560
|
for symbol in symbols:
|
|
1547
1561
|
try:
|
|
1548
1562
|
params['symbol'] = symbol
|
|
1549
1563
|
params['expert_id'] = (
|
|
1550
|
-
ids[symbol] if ids is not None and isinstance(ids, dict) else
|
|
1551
|
-
ids if ids is not None and isinstance(ids, (int, float)) else
|
|
1564
|
+
ids[symbol] if ids is not None and isinstance(ids, dict) else
|
|
1565
|
+
ids if ids is not None and isinstance(ids, (int, float)) else
|
|
1552
1566
|
params['expert_id'] if 'expert_id' in params else None
|
|
1553
1567
|
)
|
|
1554
1568
|
params['pchange_sl'] = (
|
|
1555
1569
|
pchange_sl[symbol] if pchange_sl is not None
|
|
1556
|
-
and isinstance(pchange_sl, dict) else
|
|
1557
|
-
pchange_sl if pchange_sl is not None
|
|
1558
|
-
and isinstance(pchange_sl, (int, float)) else
|
|
1570
|
+
and isinstance(pchange_sl, dict) else
|
|
1571
|
+
pchange_sl if pchange_sl is not None
|
|
1572
|
+
and isinstance(pchange_sl, (int, float)) else
|
|
1559
1573
|
params['pchange_sl'] if 'pchange_sl' in params else None
|
|
1560
1574
|
)
|
|
1561
1575
|
params['daily_risk'] = (
|
|
1562
|
-
daily_risk[symbol] if daily_risk is not None else
|
|
1576
|
+
daily_risk[symbol] if daily_risk is not None else
|
|
1563
1577
|
params['daily_risk'] if 'daily_risk' in params else None
|
|
1564
1578
|
)
|
|
1565
1579
|
params['max_risk'] = (
|
|
1566
|
-
max_risk[symbol] if max_risk is not None else
|
|
1580
|
+
max_risk[symbol] if max_risk is not None else
|
|
1567
1581
|
params['max_risk'] if 'max_risk' in params else 10.0
|
|
1568
1582
|
)
|
|
1569
1583
|
trade_instances[symbol] = Trade(**params)
|
|
1570
1584
|
except Exception as e:
|
|
1571
1585
|
logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
|
|
1572
|
-
|
|
1586
|
+
|
|
1573
1587
|
if len(trade_instances) != len(symbols):
|
|
1574
1588
|
for symbol in symbols:
|
|
1575
1589
|
if symbol not in trade_instances:
|
|
1576
1590
|
if logger is not None and isinstance(logger, Logger):
|
|
1577
|
-
logger.error(
|
|
1591
|
+
logger.error(
|
|
1592
|
+
f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1578
1593
|
else:
|
|
1579
|
-
raise ValueError(
|
|
1580
|
-
|
|
1594
|
+
raise ValueError(
|
|
1595
|
+
f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1596
|
+
return trade_instances
|