bbstrader 0.1.94__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 +9 -9
- bbstrader/btengine/__init__.py +7 -7
- bbstrader/btengine/backtest.py +30 -26
- bbstrader/btengine/data.py +100 -79
- 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 +119 -94
- bbstrader/config.py +14 -8
- bbstrader/core/__init__.py +0 -0
- bbstrader/core/data.py +22 -0
- bbstrader/core/utils.py +57 -0
- bbstrader/ibkr/__init__.py +0 -0
- bbstrader/ibkr/utils.py +0 -0
- bbstrader/metatrader/__init__.py +5 -5
- bbstrader/metatrader/account.py +117 -121
- bbstrader/metatrader/rates.py +83 -80
- bbstrader/metatrader/risk.py +23 -37
- bbstrader/metatrader/trade.py +169 -140
- bbstrader/metatrader/utils.py +3 -3
- bbstrader/models/__init__.py +5 -5
- bbstrader/models/factors.py +280 -0
- bbstrader/models/ml.py +1092 -0
- bbstrader/models/optimization.py +31 -28
- bbstrader/models/{portfolios.py → portfolio.py} +64 -46
- bbstrader/models/risk.py +15 -9
- bbstrader/trading/__init__.py +2 -2
- bbstrader/trading/execution.py +252 -164
- bbstrader/trading/scripts.py +8 -4
- bbstrader/trading/strategies.py +79 -66
- bbstrader/tseries.py +482 -107
- {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/LICENSE +1 -1
- {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/METADATA +6 -1
- bbstrader-0.2.1.dist-info/RECORD +37 -0
- bbstrader-0.1.94.dist-info/RECORD +0 -32
- {bbstrader-0.1.94.dist-info → bbstrader-0.2.1.dist-info}/WHEEL +0 -0
- {bbstrader-0.1.94.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,
|
|
@@ -874,8 +879,9 @@ class Trade(RiskManagement):
|
|
|
874
879
|
"""
|
|
875
880
|
This function checks if it's time to set the break-even level for a trading position.
|
|
876
881
|
If it is, it sets the break-even level. If the break-even level has already been set,
|
|
877
|
-
it checks if the price has moved in a favorable direction.
|
|
878
|
-
it
|
|
882
|
+
it checks if the price has moved in a favorable direction.
|
|
883
|
+
If it has, and the trail parameter is set to True, it updates
|
|
884
|
+
the break-even level based on the trail_after_points and stop_trail parameters.
|
|
879
885
|
|
|
880
886
|
Args:
|
|
881
887
|
id (int): The strategy ID or expert ID.
|
|
@@ -883,8 +889,10 @@ class Trade(RiskManagement):
|
|
|
883
889
|
trail (bool): Whether to trail the stop loss or not.
|
|
884
890
|
stop_trail (int): Number of points to trail the stop loss by.
|
|
885
891
|
It represent the distance from the current price to the stop loss.
|
|
886
|
-
trail_after_points (int): Number of points in profit
|
|
887
|
-
|
|
892
|
+
trail_after_points (int): Number of points in profit
|
|
893
|
+
from where the strategy will start to trail the stop loss.
|
|
894
|
+
be_plus_points (int): Number of points to add to the break-even level.
|
|
895
|
+
Represents the minimum profit to secure.
|
|
888
896
|
"""
|
|
889
897
|
time.sleep(0.1)
|
|
890
898
|
if not mm:
|
|
@@ -910,7 +918,8 @@ class Trade(RiskManagement):
|
|
|
910
918
|
if position.ticket not in self.break_even_status:
|
|
911
919
|
price = None
|
|
912
920
|
if be_plus_points is not None:
|
|
913
|
-
price = position.price_open +
|
|
921
|
+
price = position.price_open + \
|
|
922
|
+
(be_plus_points * point)
|
|
914
923
|
self.set_break_even(position, be, price=price)
|
|
915
924
|
self.break_even_status.append(position.ticket)
|
|
916
925
|
self.break_even_points[position.ticket] = be
|
|
@@ -919,32 +928,40 @@ class Trade(RiskManagement):
|
|
|
919
928
|
if not trail:
|
|
920
929
|
continue
|
|
921
930
|
# Check if the price has moved favorably
|
|
922
|
-
new_be = round(
|
|
931
|
+
new_be = round(
|
|
932
|
+
be * 0.10) if be_plus_points is None else be_plus_points
|
|
923
933
|
if trail_after_points is not None:
|
|
924
934
|
if position.ticket not in self.trail_after_points:
|
|
925
935
|
# This ensures that the position rich the minimum points required
|
|
926
936
|
# before the trail can be set
|
|
927
937
|
new_be = trail_after_points - be
|
|
928
|
-
self.trail_after_points.append(
|
|
938
|
+
self.trail_after_points.append(
|
|
939
|
+
position.ticket)
|
|
929
940
|
new_be_points = self.break_even_points[position.ticket] + new_be
|
|
930
|
-
favorable_move = float(
|
|
941
|
+
favorable_move = float(
|
|
942
|
+
points/point) >= new_be_points
|
|
931
943
|
if favorable_move:
|
|
932
944
|
# This allows the position to go to take profit in case of a swing trade
|
|
933
945
|
# If is a scalping position, we can set the stop_trail close to the current price.
|
|
934
|
-
trail_points = round(
|
|
946
|
+
trail_points = round(
|
|
947
|
+
be * 0.50) if stop_trail is None else stop_trail
|
|
935
948
|
# Calculate the new break-even level and price
|
|
936
949
|
if position.type == 0:
|
|
937
950
|
# This level validate the favorable move of the price
|
|
938
|
-
new_level = round(
|
|
939
|
-
|
|
940
|
-
|
|
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)
|
|
941
956
|
if new_price < position.sl:
|
|
942
|
-
new_price = position.sl
|
|
957
|
+
new_price = position.sl
|
|
943
958
|
elif position.type == 1:
|
|
944
|
-
new_level = round(
|
|
945
|
-
|
|
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)
|
|
946
963
|
if new_price > position.sl:
|
|
947
|
-
new_price = position.sl
|
|
964
|
+
new_price = position.sl
|
|
948
965
|
self.set_break_even(
|
|
949
966
|
position, be, price=new_price, level=new_level
|
|
950
967
|
)
|
|
@@ -1024,7 +1041,7 @@ class Trade(RiskManagement):
|
|
|
1024
1041
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
1025
1042
|
time.sleep(0.1)
|
|
1026
1043
|
try:
|
|
1027
|
-
|
|
1044
|
+
self.check_order(request)
|
|
1028
1045
|
result = self.send_order(request)
|
|
1029
1046
|
except Exception as e:
|
|
1030
1047
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -1042,7 +1059,7 @@ class Trade(RiskManagement):
|
|
|
1042
1059
|
else:
|
|
1043
1060
|
time.sleep(1)
|
|
1044
1061
|
try:
|
|
1045
|
-
|
|
1062
|
+
self.check_order(request)
|
|
1046
1063
|
result = self.send_order(request)
|
|
1047
1064
|
except Exception as e:
|
|
1048
1065
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -1075,7 +1092,6 @@ class Trade(RiskManagement):
|
|
|
1075
1092
|
value = self.get_symbol_info(self.symbol).trade_tick_value
|
|
1076
1093
|
points = position.profit * (size / value / position.volume)
|
|
1077
1094
|
|
|
1078
|
-
spread = self.get_symbol_info(self.symbol).spread
|
|
1079
1095
|
point = self.get_symbol_info(self.symbol).point
|
|
1080
1096
|
fees = self.get_stats()[0]["average_fee"] * -1
|
|
1081
1097
|
risk = self.currency_risk()["trade_profit"]
|
|
@@ -1085,7 +1101,7 @@ class Trade(RiskManagement):
|
|
|
1085
1101
|
win_be = th
|
|
1086
1102
|
else:
|
|
1087
1103
|
win_be = max(min_be, round((0.1 * be)))
|
|
1088
|
-
win_trade = float(points/point) >=
|
|
1104
|
+
win_trade = float(points/point) >= win_be
|
|
1089
1105
|
# Check if the positon is in profit
|
|
1090
1106
|
if win_trade:
|
|
1091
1107
|
# Check if break-even has already been set for this position
|
|
@@ -1130,7 +1146,7 @@ class Trade(RiskManagement):
|
|
|
1130
1146
|
ticket = request[type]
|
|
1131
1147
|
addtionnal = f", SYMBOL={self.symbol}"
|
|
1132
1148
|
try:
|
|
1133
|
-
|
|
1149
|
+
self.check_order(request)
|
|
1134
1150
|
result = self.send_order(request)
|
|
1135
1151
|
except Exception as e:
|
|
1136
1152
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -1144,7 +1160,7 @@ class Trade(RiskManagement):
|
|
|
1144
1160
|
while result.retcode != Mt5.TRADE_RETCODE_DONE and tries < 5:
|
|
1145
1161
|
time.sleep(1)
|
|
1146
1162
|
try:
|
|
1147
|
-
|
|
1163
|
+
self.check_order(request)
|
|
1148
1164
|
result = self.send_order(request)
|
|
1149
1165
|
except Exception as e:
|
|
1150
1166
|
print(f"{self.current_datetime()} -", end=' ')
|
|
@@ -1163,10 +1179,10 @@ class Trade(RiskManagement):
|
|
|
1163
1179
|
return True
|
|
1164
1180
|
else:
|
|
1165
1181
|
return False
|
|
1166
|
-
|
|
1167
|
-
def close_order(self,
|
|
1168
|
-
ticket: int,
|
|
1169
|
-
id: Optional[int] = None,
|
|
1182
|
+
|
|
1183
|
+
def close_order(self,
|
|
1184
|
+
ticket: int,
|
|
1185
|
+
id: Optional[int] = None,
|
|
1170
1186
|
comment: Optional[str] = None):
|
|
1171
1187
|
"""
|
|
1172
1188
|
Close an open order by it ticket
|
|
@@ -1180,14 +1196,14 @@ class Trade(RiskManagement):
|
|
|
1180
1196
|
- True if order closed, False otherwise
|
|
1181
1197
|
"""
|
|
1182
1198
|
request = {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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
|
+
}
|
|
1189
1205
|
return self.close_request(request, type="order")
|
|
1190
|
-
|
|
1206
|
+
|
|
1191
1207
|
def close_position(self,
|
|
1192
1208
|
ticket: int,
|
|
1193
1209
|
id: Optional[int] = None,
|
|
@@ -1212,15 +1228,13 @@ class Trade(RiskManagement):
|
|
|
1212
1228
|
positions = self.get_positions(ticket=ticket)
|
|
1213
1229
|
buy_price = self.get_tick_info(self.symbol).ask
|
|
1214
1230
|
sell_price = self.get_tick_info(self.symbol).bid
|
|
1215
|
-
digits = self.get_symbol_info(self.symbol).digits
|
|
1216
1231
|
deviation = self.get_deviation()
|
|
1217
1232
|
if positions is not None and len(positions) == 1:
|
|
1218
1233
|
position = positions[0]
|
|
1219
1234
|
if (position.ticket == ticket
|
|
1220
|
-
|
|
1221
|
-
|
|
1235
|
+
and position.magic == Id
|
|
1236
|
+
):
|
|
1222
1237
|
buy = position.type == 0
|
|
1223
|
-
sell = position.type == 1
|
|
1224
1238
|
request = {
|
|
1225
1239
|
"action": Mt5.TRADE_ACTION_DEAL,
|
|
1226
1240
|
"symbol": self.symbol,
|
|
@@ -1235,15 +1249,14 @@ class Trade(RiskManagement):
|
|
|
1235
1249
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
1236
1250
|
}
|
|
1237
1251
|
return self.close_request(request, type="position")
|
|
1238
|
-
|
|
1239
|
-
def bulk_close(self,
|
|
1240
|
-
tickets: List,
|
|
1252
|
+
|
|
1253
|
+
def bulk_close(self,
|
|
1254
|
+
tickets: List,
|
|
1241
1255
|
tikets_type: Literal["positions", "orders"],
|
|
1242
|
-
close_func: Callable,
|
|
1243
|
-
order_type: str,
|
|
1256
|
+
close_func: Callable,
|
|
1257
|
+
order_type: str,
|
|
1244
1258
|
id: Optional[int] = None,
|
|
1245
1259
|
comment: Optional[str] = None):
|
|
1246
|
-
|
|
1247
1260
|
"""
|
|
1248
1261
|
Close multiple orders or positions at once.
|
|
1249
1262
|
|
|
@@ -1257,13 +1270,12 @@ class Trade(RiskManagement):
|
|
|
1257
1270
|
"""
|
|
1258
1271
|
if order_type == 'all':
|
|
1259
1272
|
order_type = 'open'
|
|
1260
|
-
if len(tickets) > 0:
|
|
1273
|
+
if tickets is not None and len(tickets) > 0:
|
|
1261
1274
|
for ticket in tickets.copy():
|
|
1262
1275
|
if close_func(ticket, id=id, comment=comment):
|
|
1263
1276
|
tickets.remove(ticket)
|
|
1264
1277
|
time.sleep(1)
|
|
1265
|
-
|
|
1266
|
-
if len(tickets) == 0:
|
|
1278
|
+
if tickets is not None and len(tickets) == 0:
|
|
1267
1279
|
self.logger.info(
|
|
1268
1280
|
f"ALL {order_type.upper()} {tikets_type.upper()} closed, SYMBOL={self.symbol}.")
|
|
1269
1281
|
else:
|
|
@@ -1273,11 +1285,12 @@ class Trade(RiskManagement):
|
|
|
1273
1285
|
self.logger.info(
|
|
1274
1286
|
f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}.")
|
|
1275
1287
|
|
|
1276
|
-
Orders = Literal["all", "buy_stops", "sell_stops", "buy_limits",
|
|
1288
|
+
Orders = Literal["all", "buy_stops", "sell_stops", "buy_limits",
|
|
1277
1289
|
"sell_limits", "buy_stop_limits", "sell_stop_limits"]
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1290
|
+
|
|
1291
|
+
def close_orders(self,
|
|
1292
|
+
order_type: Orders,
|
|
1293
|
+
id: Optional[int] = None,
|
|
1281
1294
|
comment: Optional[str] = None):
|
|
1282
1295
|
"""
|
|
1283
1296
|
Args:
|
|
@@ -1307,6 +1320,7 @@ class Trade(RiskManagement):
|
|
|
1307
1320
|
orders, "orders", self.close_order, order_type, id=id, comment=comment)
|
|
1308
1321
|
|
|
1309
1322
|
Positions = Literal["all", "buy", "sell", "profitable", "losing"]
|
|
1323
|
+
|
|
1310
1324
|
def close_positions(
|
|
1311
1325
|
self,
|
|
1312
1326
|
position_type: Positions,
|
|
@@ -1347,7 +1361,6 @@ class Trade(RiskManagement):
|
|
|
1347
1361
|
loss_trades = 0
|
|
1348
1362
|
win_trades = 0
|
|
1349
1363
|
balance = self.get_account_info().balance
|
|
1350
|
-
target = round((balance * self.target)/100)
|
|
1351
1364
|
deals = len(self.opened_positions)
|
|
1352
1365
|
if deals != 0:
|
|
1353
1366
|
for position in self.opened_positions:
|
|
@@ -1474,27 +1487,28 @@ class Trade(RiskManagement):
|
|
|
1474
1487
|
else:
|
|
1475
1488
|
# claculate number of minute from the end to the start
|
|
1476
1489
|
start = datetime.strptime(self.start, '%H:%M')
|
|
1477
|
-
end =
|
|
1490
|
+
end = datetime.strptime(self.current_time(), '%H:%M')
|
|
1478
1491
|
minutes = (end - start).total_seconds() // 60
|
|
1479
1492
|
sleep_time = (24*60) - minutes
|
|
1480
1493
|
return sleep_time
|
|
1481
1494
|
|
|
1482
1495
|
def current_datetime(self):
|
|
1483
1496
|
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1484
|
-
|
|
1497
|
+
|
|
1485
1498
|
def current_time(self, seconds=False):
|
|
1486
1499
|
if seconds:
|
|
1487
1500
|
return datetime.now().strftime("%H:%M:%S")
|
|
1488
1501
|
return datetime.now().strftime("%H:%M")
|
|
1489
1502
|
|
|
1503
|
+
|
|
1490
1504
|
def create_trade_instance(
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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]:
|
|
1498
1512
|
"""
|
|
1499
1513
|
Creates Trade instances for each symbol provided.
|
|
1500
1514
|
|
|
@@ -1509,59 +1523,74 @@ def create_trade_instance(
|
|
|
1509
1523
|
|
|
1510
1524
|
Raises:
|
|
1511
1525
|
ValueError: If the 'symbols' list is empty or the 'params' dictionary is missing required keys.
|
|
1512
|
-
|
|
1526
|
+
|
|
1513
1527
|
Note:
|
|
1514
1528
|
`daily_risk` and `max_risk` can be used to manage the risk of each symbol
|
|
1515
1529
|
based on the importance of the symbol in the portfolio or strategy.
|
|
1516
1530
|
"""
|
|
1517
1531
|
logger = params.get('logger', None)
|
|
1532
|
+
ids = params.get('expert_id', None)
|
|
1518
1533
|
trade_instances = {}
|
|
1519
1534
|
if not symbols:
|
|
1520
1535
|
raise ValueError("The 'symbols' list cannot be empty.")
|
|
1521
1536
|
if not params:
|
|
1522
1537
|
raise ValueError("The 'params' dictionary cannot be empty.")
|
|
1523
|
-
|
|
1538
|
+
|
|
1524
1539
|
if daily_risk is not None:
|
|
1525
1540
|
for symbol in symbols:
|
|
1526
1541
|
if symbol not in daily_risk:
|
|
1527
|
-
raise ValueError(
|
|
1542
|
+
raise ValueError(
|
|
1543
|
+
f"Missing daily risk weight for symbol '{symbol}'.")
|
|
1528
1544
|
if max_risk is not None:
|
|
1529
1545
|
for symbol in symbols:
|
|
1530
1546
|
if symbol not in max_risk:
|
|
1531
|
-
raise ValueError(
|
|
1547
|
+
raise ValueError(
|
|
1548
|
+
f"Missing maximum risk percentage for symbol '{symbol}'.")
|
|
1532
1549
|
if pchange_sl is not None:
|
|
1533
1550
|
if isinstance(pchange_sl, dict):
|
|
1534
1551
|
for symbol in symbols:
|
|
1535
1552
|
if symbol not in pchange_sl:
|
|
1536
|
-
raise ValueError(
|
|
1537
|
-
|
|
1553
|
+
raise ValueError(
|
|
1554
|
+
f"Missing percentage change for symbol '{symbol}'.")
|
|
1555
|
+
if isinstance(ids, dict):
|
|
1556
|
+
for symbol in symbols:
|
|
1557
|
+
if symbol not in ids:
|
|
1558
|
+
raise ValueError(f"Missing expert ID for symbol '{symbol}'.")
|
|
1559
|
+
|
|
1538
1560
|
for symbol in symbols:
|
|
1539
1561
|
try:
|
|
1540
1562
|
params['symbol'] = symbol
|
|
1563
|
+
params['expert_id'] = (
|
|
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
|
|
1566
|
+
params['expert_id'] if 'expert_id' in params else None
|
|
1567
|
+
)
|
|
1541
1568
|
params['pchange_sl'] = (
|
|
1542
1569
|
pchange_sl[symbol] if pchange_sl is not None
|
|
1543
|
-
and isinstance(pchange_sl, dict) else
|
|
1544
|
-
pchange_sl if pchange_sl is not None
|
|
1545
|
-
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
|
|
1546
1573
|
params['pchange_sl'] if 'pchange_sl' in params else None
|
|
1547
1574
|
)
|
|
1548
1575
|
params['daily_risk'] = (
|
|
1549
|
-
daily_risk[symbol] if daily_risk is not None else
|
|
1576
|
+
daily_risk[symbol] if daily_risk is not None else
|
|
1550
1577
|
params['daily_risk'] if 'daily_risk' in params else None
|
|
1551
1578
|
)
|
|
1552
1579
|
params['max_risk'] = (
|
|
1553
|
-
max_risk[symbol] if max_risk is not None else
|
|
1580
|
+
max_risk[symbol] if max_risk is not None else
|
|
1554
1581
|
params['max_risk'] if 'max_risk' in params else 10.0
|
|
1555
1582
|
)
|
|
1556
1583
|
trade_instances[symbol] = Trade(**params)
|
|
1557
1584
|
except Exception as e:
|
|
1558
1585
|
logger.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
|
|
1559
|
-
|
|
1586
|
+
|
|
1560
1587
|
if len(trade_instances) != len(symbols):
|
|
1561
1588
|
for symbol in symbols:
|
|
1562
1589
|
if symbol not in trade_instances:
|
|
1563
1590
|
if logger is not None and isinstance(logger, Logger):
|
|
1564
|
-
logger.error(
|
|
1591
|
+
logger.error(
|
|
1592
|
+
f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1565
1593
|
else:
|
|
1566
|
-
raise ValueError(
|
|
1567
|
-
|
|
1594
|
+
raise ValueError(
|
|
1595
|
+
f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1596
|
+
return trade_instances
|