bbstrader 0.2.91__py3-none-any.whl → 0.2.93__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/__main__.py +50 -0
- bbstrader/btengine/data.py +12 -9
- bbstrader/btengine/execution.py +13 -2
- bbstrader/btengine/performance.py +50 -1
- bbstrader/btengine/scripts.py +157 -0
- bbstrader/btengine/strategy.py +12 -2
- bbstrader/compat.py +2 -2
- bbstrader/config.py +2 -4
- bbstrader/core/utils.py +90 -1
- bbstrader/metatrader/__init__.py +2 -1
- bbstrader/metatrader/account.py +29 -39
- bbstrader/metatrader/copier.py +745 -0
- bbstrader/metatrader/rates.py +6 -3
- bbstrader/metatrader/risk.py +19 -8
- bbstrader/metatrader/scripts.py +81 -0
- bbstrader/metatrader/trade.py +178 -66
- bbstrader/metatrader/utils.py +5 -2
- bbstrader/models/ml.py +20 -12
- bbstrader/trading/execution.py +150 -33
- bbstrader/trading/script.py +155 -0
- bbstrader/trading/scripts.py +2 -0
- bbstrader/tseries.py +33 -7
- {bbstrader-0.2.91.dist-info → bbstrader-0.2.93.dist-info}/METADATA +6 -2
- bbstrader-0.2.93.dist-info/RECORD +44 -0
- {bbstrader-0.2.91.dist-info → bbstrader-0.2.93.dist-info}/WHEEL +1 -1
- bbstrader-0.2.93.dist-info/entry_points.txt +2 -0
- bbstrader-0.2.91.dist-info/RECORD +0 -38
- {bbstrader-0.2.91.dist-info → bbstrader-0.2.93.dist-info}/LICENSE +0 -0
- {bbstrader-0.2.91.dist-info → bbstrader-0.2.93.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/trade.py
CHANGED
|
@@ -2,15 +2,15 @@ import os
|
|
|
2
2
|
import time
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from logging import Logger
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple
|
|
6
7
|
|
|
7
|
-
from bbstrader import compat # noqa: F401
|
|
8
|
-
import MetaTrader5 as Mt5
|
|
9
8
|
import pandas as pd
|
|
9
|
+
from loguru import logger as log
|
|
10
10
|
from tabulate import tabulate
|
|
11
11
|
|
|
12
12
|
from bbstrader.btengine.performance import create_sharpe_ratio
|
|
13
|
-
from bbstrader.config import config_logger
|
|
13
|
+
from bbstrader.config import BBSTRADER_DIR, config_logger
|
|
14
14
|
from bbstrader.metatrader.account import INIT_MSG, check_mt5_connection
|
|
15
15
|
from bbstrader.metatrader.risk import RiskManagement
|
|
16
16
|
from bbstrader.metatrader.utils import (
|
|
@@ -19,6 +19,12 @@ from bbstrader.metatrader.utils import (
|
|
|
19
19
|
trade_retcode_message,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
+
try:
|
|
23
|
+
import MetaTrader5 as Mt5
|
|
24
|
+
except ImportError:
|
|
25
|
+
import bbstrader.compat # noqa: F401
|
|
26
|
+
|
|
27
|
+
|
|
22
28
|
__all__ = [
|
|
23
29
|
"Trade",
|
|
24
30
|
"create_trade_instance",
|
|
@@ -30,6 +36,16 @@ FILLING_TYPE = [
|
|
|
30
36
|
Mt5.ORDER_FILLING_BOC,
|
|
31
37
|
]
|
|
32
38
|
|
|
39
|
+
log.add(
|
|
40
|
+
f"{BBSTRADER_DIR}/logs/trade.log",
|
|
41
|
+
enqueue=True,
|
|
42
|
+
level="INFO",
|
|
43
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
global LOGGER
|
|
47
|
+
LOGGER = log
|
|
48
|
+
|
|
33
49
|
|
|
34
50
|
class Trade(RiskManagement):
|
|
35
51
|
"""
|
|
@@ -92,9 +108,9 @@ class Trade(RiskManagement):
|
|
|
92
108
|
expert_id: int = 9818,
|
|
93
109
|
version: str = "2.0",
|
|
94
110
|
target: float = 5.0,
|
|
95
|
-
start_time: str = "
|
|
96
|
-
finishing_time: str = "23:
|
|
97
|
-
ending_time: str = "23:
|
|
111
|
+
start_time: str = "0:00",
|
|
112
|
+
finishing_time: str = "23:59",
|
|
113
|
+
ending_time: str = "23:59",
|
|
98
114
|
verbose: Optional[bool] = None,
|
|
99
115
|
console_log: Optional[bool] = False,
|
|
100
116
|
logger: Logger | str = "bbstrader.log",
|
|
@@ -152,7 +168,6 @@ class Trade(RiskManagement):
|
|
|
152
168
|
self.end = ending_time
|
|
153
169
|
self.finishing = finishing_time
|
|
154
170
|
self.console_log = console_log
|
|
155
|
-
self.logger = self._get_logger(logger, console_log)
|
|
156
171
|
self.tf = kwargs.get("time_frame", "D1")
|
|
157
172
|
self.kwargs = kwargs
|
|
158
173
|
|
|
@@ -171,6 +186,7 @@ class Trade(RiskManagement):
|
|
|
171
186
|
self.trail_after_points = []
|
|
172
187
|
self._retcodes = []
|
|
173
188
|
|
|
189
|
+
self._get_logger(logger, console_log)
|
|
174
190
|
self.initialize(**kwargs)
|
|
175
191
|
self.select_symbol(**kwargs)
|
|
176
192
|
self.prepare_symbol()
|
|
@@ -187,11 +203,19 @@ class Trade(RiskManagement):
|
|
|
187
203
|
"""Return all the retcodes"""
|
|
188
204
|
return self._retcodes
|
|
189
205
|
|
|
190
|
-
|
|
206
|
+
@property
|
|
207
|
+
def logger(self):
|
|
208
|
+
return LOGGER
|
|
209
|
+
|
|
210
|
+
def _get_logger(self, loger: Any, consol_log: bool):
|
|
191
211
|
"""Get the logger object"""
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
212
|
+
global LOGGER
|
|
213
|
+
if loger is None:
|
|
214
|
+
... # Do nothing
|
|
215
|
+
elif isinstance(loger, (str, Path)):
|
|
216
|
+
LOGGER = config_logger(f"{BBSTRADER_DIR}/logs/{loger}", consol_log)
|
|
217
|
+
elif isinstance(loger, (Logger, type(log))):
|
|
218
|
+
LOGGER = loger
|
|
195
219
|
|
|
196
220
|
def initialize(self, **kwargs):
|
|
197
221
|
"""
|
|
@@ -213,7 +237,7 @@ class Trade(RiskManagement):
|
|
|
213
237
|
f" Version @{self.version}, on {self.symbol}."
|
|
214
238
|
)
|
|
215
239
|
except Exception as e:
|
|
216
|
-
|
|
240
|
+
LOGGER.error(f"During initialization: {e}")
|
|
217
241
|
|
|
218
242
|
def select_symbol(self, **kwargs):
|
|
219
243
|
"""
|
|
@@ -231,7 +255,7 @@ class Trade(RiskManagement):
|
|
|
231
255
|
if not Mt5.symbol_select(self.symbol, True):
|
|
232
256
|
raise_mt5_error(message=INIT_MSG)
|
|
233
257
|
except Exception as e:
|
|
234
|
-
|
|
258
|
+
LOGGER.error(f"Selecting symbol '{self.symbol}': {e}")
|
|
235
259
|
|
|
236
260
|
def prepare_symbol(self):
|
|
237
261
|
"""
|
|
@@ -253,7 +277,7 @@ class Trade(RiskManagement):
|
|
|
253
277
|
if self.verbose:
|
|
254
278
|
print("Initialization successfully completed.")
|
|
255
279
|
except Exception as e:
|
|
256
|
-
|
|
280
|
+
LOGGER.error(f"Preparing symbol '{self.symbol}': {e}")
|
|
257
281
|
|
|
258
282
|
def summary(self):
|
|
259
283
|
"""Show a brief description about the trading program"""
|
|
@@ -388,7 +412,7 @@ class Trade(RiskManagement):
|
|
|
388
412
|
filename = f"{symbol}_{today_date}@{self.expert_id}.csv"
|
|
389
413
|
filepath = os.path.join(dir, filename)
|
|
390
414
|
stats_df.to_csv(filepath, index=False)
|
|
391
|
-
|
|
415
|
+
LOGGER.info(f"Session statistics saved to {filepath}")
|
|
392
416
|
|
|
393
417
|
Buys = Literal["BMKT", "BLMT", "BSTP", "BSTPLMT"]
|
|
394
418
|
|
|
@@ -398,8 +422,13 @@ class Trade(RiskManagement):
|
|
|
398
422
|
price: Optional[float] = None,
|
|
399
423
|
stoplimit: Optional[float] = None,
|
|
400
424
|
mm: bool = True,
|
|
425
|
+
trail: bool = True,
|
|
401
426
|
id: Optional[int] = None,
|
|
402
427
|
comment: Optional[str] = None,
|
|
428
|
+
symbol: Optional[str] = None,
|
|
429
|
+
volume: Optional[float] = None,
|
|
430
|
+
sl: Optional[float] = None,
|
|
431
|
+
tp: Optional[float] = None,
|
|
403
432
|
):
|
|
404
433
|
"""
|
|
405
434
|
Open a Buy positin
|
|
@@ -412,6 +441,7 @@ class Trade(RiskManagement):
|
|
|
412
441
|
The pending order is not passed to the trading system until that moment
|
|
413
442
|
id (int): The strategy id or expert Id
|
|
414
443
|
mm (bool): Weither to put stop loss and tp or not
|
|
444
|
+
trail (bool): Weither to trail the stop loss or not
|
|
415
445
|
comment (str): The comment for the opening position
|
|
416
446
|
"""
|
|
417
447
|
Id = id if id is not None else self.expert_id
|
|
@@ -424,19 +454,19 @@ class Trade(RiskManagement):
|
|
|
424
454
|
else:
|
|
425
455
|
_price = self.get_tick_info(self.symbol).ask
|
|
426
456
|
|
|
427
|
-
lot = self.get_lot()
|
|
457
|
+
lot = volume or self.get_lot()
|
|
428
458
|
stop_loss = self.get_stop_loss()
|
|
429
459
|
take_profit = self.get_take_profit()
|
|
430
460
|
deviation = self.get_deviation()
|
|
431
461
|
request = {
|
|
432
462
|
"action": Mt5.TRADE_ACTION_DEAL,
|
|
433
|
-
"symbol": self.symbol,
|
|
463
|
+
"symbol": symbol or self.symbol,
|
|
434
464
|
"volume": float(lot),
|
|
435
465
|
"type": Mt5.ORDER_TYPE_BUY,
|
|
436
466
|
"price": _price,
|
|
437
467
|
"deviation": deviation,
|
|
438
468
|
"magic": Id,
|
|
439
|
-
"comment": f"@{self.expert_name}"
|
|
469
|
+
"comment": comment or f"@{self.expert_name}",
|
|
440
470
|
"type_time": Mt5.ORDER_TIME_GTC,
|
|
441
471
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
442
472
|
}
|
|
@@ -454,11 +484,12 @@ class Trade(RiskManagement):
|
|
|
454
484
|
request["stoplimit"] = stoplimit
|
|
455
485
|
mm_price = stoplimit
|
|
456
486
|
if mm:
|
|
457
|
-
request["sl"] = mm_price - stop_loss * point
|
|
458
|
-
request["tp"] = mm_price + take_profit * point
|
|
459
|
-
self.break_even(mm=mm, id=Id)
|
|
487
|
+
request["sl"] = sl or mm_price - stop_loss * point
|
|
488
|
+
request["tp"] = tp or mm_price + take_profit * point
|
|
489
|
+
self.break_even(mm=mm, id=Id, trail=trail)
|
|
460
490
|
if self.check(comment):
|
|
461
|
-
self.request_result(_price, request, action)
|
|
491
|
+
return self.request_result(_price, request, action)
|
|
492
|
+
return False
|
|
462
493
|
|
|
463
494
|
def _order_type(self):
|
|
464
495
|
type = {
|
|
@@ -481,8 +512,13 @@ class Trade(RiskManagement):
|
|
|
481
512
|
price: Optional[float] = None,
|
|
482
513
|
stoplimit: Optional[float] = None,
|
|
483
514
|
mm: bool = True,
|
|
515
|
+
trail: bool = True,
|
|
484
516
|
id: Optional[int] = None,
|
|
485
517
|
comment: Optional[str] = None,
|
|
518
|
+
symbol: Optional[str] = None,
|
|
519
|
+
volume: Optional[float] = None,
|
|
520
|
+
sl: Optional[float] = None,
|
|
521
|
+
tp: Optional[float] = None,
|
|
486
522
|
):
|
|
487
523
|
"""
|
|
488
524
|
Open a sell positin
|
|
@@ -495,7 +531,12 @@ class Trade(RiskManagement):
|
|
|
495
531
|
The pending order is not passed to the trading system until that moment
|
|
496
532
|
id (int): The strategy id or expert Id
|
|
497
533
|
mm (bool): Weither to put stop loss and tp or not
|
|
534
|
+
trail (bool): Weither to trail the stop loss or not
|
|
498
535
|
comment (str): The comment for the closing position
|
|
536
|
+
symbol (str): The symbol to trade
|
|
537
|
+
volume (float): The volume (lot) to trade
|
|
538
|
+
sl (float): The stop loss in points
|
|
539
|
+
tp (float): The take profit in points
|
|
499
540
|
"""
|
|
500
541
|
Id = id if id is not None else self.expert_id
|
|
501
542
|
point = self.get_symbol_info(self.symbol).point
|
|
@@ -507,19 +548,19 @@ class Trade(RiskManagement):
|
|
|
507
548
|
else:
|
|
508
549
|
_price = self.get_tick_info(self.symbol).bid
|
|
509
550
|
|
|
510
|
-
lot = self.get_lot()
|
|
551
|
+
lot = volume or self.get_lot()
|
|
511
552
|
stop_loss = self.get_stop_loss()
|
|
512
553
|
take_profit = self.get_take_profit()
|
|
513
554
|
deviation = self.get_deviation()
|
|
514
555
|
request = {
|
|
515
556
|
"action": Mt5.TRADE_ACTION_DEAL,
|
|
516
|
-
"symbol": self.symbol,
|
|
557
|
+
"symbol": symbol or self.symbol,
|
|
517
558
|
"volume": float(lot),
|
|
518
559
|
"type": Mt5.ORDER_TYPE_SELL,
|
|
519
560
|
"price": _price,
|
|
520
561
|
"deviation": deviation,
|
|
521
562
|
"magic": Id,
|
|
522
|
-
"comment": f"@{self.expert_name}"
|
|
563
|
+
"comment": comment or f"@{self.expert_name}",
|
|
523
564
|
"type_time": Mt5.ORDER_TIME_GTC,
|
|
524
565
|
"type_filling": Mt5.ORDER_FILLING_FOK,
|
|
525
566
|
}
|
|
@@ -537,11 +578,12 @@ class Trade(RiskManagement):
|
|
|
537
578
|
request["stoplimit"] = stoplimit
|
|
538
579
|
mm_price = stoplimit
|
|
539
580
|
if mm:
|
|
540
|
-
request["sl"] = mm_price + stop_loss * point
|
|
541
|
-
request["tp"] = mm_price - take_profit * point
|
|
542
|
-
self.break_even(mm=mm, id=Id)
|
|
581
|
+
request["sl"] = sl or mm_price + stop_loss * point
|
|
582
|
+
request["tp"] = tp or mm_price - take_profit * point
|
|
583
|
+
self.break_even(mm=mm, id=Id, trail=trail)
|
|
543
584
|
if self.check(comment):
|
|
544
|
-
self.request_result(_price, request, action)
|
|
585
|
+
return self.request_result(_price, request, action)
|
|
586
|
+
return False
|
|
545
587
|
|
|
546
588
|
def check(self, comment):
|
|
547
589
|
"""
|
|
@@ -555,10 +597,10 @@ class Trade(RiskManagement):
|
|
|
555
597
|
if self.days_end():
|
|
556
598
|
return False
|
|
557
599
|
elif not self.trading_time():
|
|
558
|
-
|
|
600
|
+
LOGGER.info(f"Not Trading time, SYMBOL={self.symbol}")
|
|
559
601
|
return False
|
|
560
602
|
elif not self.is_risk_ok():
|
|
561
|
-
|
|
603
|
+
LOGGER.error(f"Account Risk not allowed, SYMBOL={self.symbol}")
|
|
562
604
|
self._check(comment)
|
|
563
605
|
return False
|
|
564
606
|
elif self.profit_target():
|
|
@@ -571,7 +613,7 @@ class Trade(RiskManagement):
|
|
|
571
613
|
or self.get_current_positions() is None
|
|
572
614
|
):
|
|
573
615
|
self.close_positions(position_type="all")
|
|
574
|
-
|
|
616
|
+
LOGGER.info(txt)
|
|
575
617
|
time.sleep(5)
|
|
576
618
|
self.statistics(save=True)
|
|
577
619
|
|
|
@@ -613,7 +655,7 @@ class Trade(RiskManagement):
|
|
|
613
655
|
elif result.retcode not in self._retcodes:
|
|
614
656
|
self._retcodes.append(result.retcode)
|
|
615
657
|
msg = trade_retcode_message(result.retcode)
|
|
616
|
-
|
|
658
|
+
LOGGER.error(
|
|
617
659
|
f"Trade Order Request, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
618
660
|
)
|
|
619
661
|
elif result.retcode in [
|
|
@@ -637,7 +679,7 @@ class Trade(RiskManagement):
|
|
|
637
679
|
# Print the result
|
|
638
680
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
639
681
|
msg = trade_retcode_message(result.retcode)
|
|
640
|
-
|
|
682
|
+
LOGGER.info(f"Trade Order {msg}{addtionnal}")
|
|
641
683
|
if type != "BMKT" or type != "SMKT":
|
|
642
684
|
self.opened_orders.append(result.order)
|
|
643
685
|
long_msg = (
|
|
@@ -645,7 +687,7 @@ class Trade(RiskManagement):
|
|
|
645
687
|
f"Lot(s): {result.volume}, Sl: {self.get_stop_loss()}, "
|
|
646
688
|
f"Tp: {self.get_take_profit()}"
|
|
647
689
|
)
|
|
648
|
-
|
|
690
|
+
LOGGER.info(long_msg)
|
|
649
691
|
time.sleep(0.1)
|
|
650
692
|
if type == "BMKT" or type == "SMKT":
|
|
651
693
|
self.opened_positions.append(result.order)
|
|
@@ -664,17 +706,19 @@ class Trade(RiskManagement):
|
|
|
664
706
|
f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open, 5)}, "
|
|
665
707
|
f"Sl: @{position.sl} Tp: @{position.tp}"
|
|
666
708
|
)
|
|
667
|
-
|
|
709
|
+
LOGGER.info(order_info)
|
|
668
710
|
pos_info = (
|
|
669
711
|
f"3. [OPEN POSITIONS ON {self.symbol} = {len(positions)}, ACCOUNT OPEN PnL = {profit} "
|
|
670
712
|
f"{self.get_account_info().currency}]\n"
|
|
671
713
|
)
|
|
672
|
-
|
|
714
|
+
LOGGER.info(pos_info)
|
|
715
|
+
return True
|
|
673
716
|
else:
|
|
674
717
|
msg = trade_retcode_message(result.retcode)
|
|
675
|
-
|
|
718
|
+
LOGGER.error(
|
|
676
719
|
f"Unable to Open Position, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
677
720
|
)
|
|
721
|
+
return False
|
|
678
722
|
|
|
679
723
|
def open_position(
|
|
680
724
|
self,
|
|
@@ -683,7 +727,12 @@ class Trade(RiskManagement):
|
|
|
683
727
|
stoplimit: Optional[float] = None,
|
|
684
728
|
id: Optional[int] = None,
|
|
685
729
|
mm: bool = True,
|
|
730
|
+
trail: bool = True,
|
|
686
731
|
comment: Optional[str] = None,
|
|
732
|
+
symbol: Optional[str] = None,
|
|
733
|
+
volume: Optional[float] = None,
|
|
734
|
+
sl: Optional[float] = None,
|
|
735
|
+
tp: Optional[float] = None,
|
|
687
736
|
):
|
|
688
737
|
"""
|
|
689
738
|
Open a buy or sell position.
|
|
@@ -696,27 +745,42 @@ class Trade(RiskManagement):
|
|
|
696
745
|
The pending order is not passed to the trading system until that moment
|
|
697
746
|
id (int): The strategy id or expert Id
|
|
698
747
|
mm (bool): Weither to put stop loss and tp or not
|
|
748
|
+
trail (bool): Weither to trail the stop loss or not
|
|
699
749
|
comment (str): The comment for the closing position
|
|
750
|
+
symbol (str): The symbol to trade
|
|
751
|
+
volume (float): The volume (lot) to trade
|
|
752
|
+
sl (float): The stop loss in points
|
|
753
|
+
tp (float): The take profit in points
|
|
700
754
|
"""
|
|
701
755
|
BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
|
|
702
756
|
SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
|
|
703
757
|
if action in BUYS:
|
|
704
|
-
self.open_buy_position(
|
|
758
|
+
return self.open_buy_position(
|
|
705
759
|
action=action,
|
|
706
760
|
price=price,
|
|
707
761
|
stoplimit=stoplimit,
|
|
708
762
|
id=id,
|
|
709
763
|
mm=mm,
|
|
764
|
+
trail=trail,
|
|
710
765
|
comment=comment,
|
|
766
|
+
symbol=symbol,
|
|
767
|
+
volume=volume,
|
|
768
|
+
sl=sl,
|
|
769
|
+
tp=tp,
|
|
711
770
|
)
|
|
712
771
|
elif action in SELLS:
|
|
713
|
-
self.open_sell_position(
|
|
772
|
+
return self.open_sell_position(
|
|
714
773
|
action=action,
|
|
715
774
|
price=price,
|
|
716
775
|
stoplimit=stoplimit,
|
|
717
776
|
id=id,
|
|
718
777
|
mm=mm,
|
|
778
|
+
trail=trail,
|
|
719
779
|
comment=comment,
|
|
780
|
+
symbol=symbol,
|
|
781
|
+
volume=volume,
|
|
782
|
+
sl=sl,
|
|
783
|
+
tp=tp,
|
|
720
784
|
)
|
|
721
785
|
else:
|
|
722
786
|
raise ValueError(
|
|
@@ -939,7 +1003,7 @@ class Trade(RiskManagement):
|
|
|
939
1003
|
mm=True,
|
|
940
1004
|
id: Optional[int] = None,
|
|
941
1005
|
trail: Optional[bool] = True,
|
|
942
|
-
stop_trail:
|
|
1006
|
+
stop_trail: int | str = None,
|
|
943
1007
|
trail_after_points: int | str = None,
|
|
944
1008
|
be_plus_points: Optional[int] = None,
|
|
945
1009
|
):
|
|
@@ -1096,7 +1160,6 @@ class Trade(RiskManagement):
|
|
|
1096
1160
|
# Set the stop loss to break even
|
|
1097
1161
|
request = {
|
|
1098
1162
|
"action": Mt5.TRADE_ACTION_SLTP,
|
|
1099
|
-
"type": Mt5.ORDER_TYPE_SELL_STOP,
|
|
1100
1163
|
"position": position.ticket,
|
|
1101
1164
|
"sl": round(_price, digits),
|
|
1102
1165
|
"tp": position.tp,
|
|
@@ -1115,7 +1178,6 @@ class Trade(RiskManagement):
|
|
|
1115
1178
|
# Set the stop loss to break even
|
|
1116
1179
|
request = {
|
|
1117
1180
|
"action": Mt5.TRADE_ACTION_SLTP,
|
|
1118
|
-
"type": Mt5.ORDER_TYPE_BUY_STOP,
|
|
1119
1181
|
"position": position.ticket,
|
|
1120
1182
|
"sl": round(_price, digits),
|
|
1121
1183
|
"tp": position.tp,
|
|
@@ -1144,7 +1206,7 @@ class Trade(RiskManagement):
|
|
|
1144
1206
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1145
1207
|
msg = trade_retcode_message(result.retcode)
|
|
1146
1208
|
if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
1147
|
-
|
|
1209
|
+
LOGGER.error(
|
|
1148
1210
|
f"Break-Even Order Request, Position: #{tiket}, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
1149
1211
|
)
|
|
1150
1212
|
tries = 0
|
|
@@ -1166,9 +1228,9 @@ class Trade(RiskManagement):
|
|
|
1166
1228
|
tries += 1
|
|
1167
1229
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1168
1230
|
msg = trade_retcode_message(result.retcode)
|
|
1169
|
-
|
|
1231
|
+
LOGGER.info(f"Break-Even Order {msg}{addtionnal}")
|
|
1170
1232
|
info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{price}"
|
|
1171
|
-
|
|
1233
|
+
LOGGER.info(info)
|
|
1172
1234
|
self.break_even_status.append(tiket)
|
|
1173
1235
|
|
|
1174
1236
|
def win_trade(self, position: TradePosition, th: Optional[int] = None) -> bool:
|
|
@@ -1190,7 +1252,7 @@ class Trade(RiskManagement):
|
|
|
1190
1252
|
try:
|
|
1191
1253
|
min_be = round((fees / risk)) + 2
|
|
1192
1254
|
except ZeroDivisionError:
|
|
1193
|
-
min_be = self.symbol_info
|
|
1255
|
+
min_be = self.symbol_info.spread
|
|
1194
1256
|
be = self.get_break_even()
|
|
1195
1257
|
if th is not None:
|
|
1196
1258
|
win_be = th
|
|
@@ -1256,7 +1318,7 @@ class Trade(RiskManagement):
|
|
|
1256
1318
|
elif result.retcode not in self._retcodes:
|
|
1257
1319
|
self._retcodes.append(result.retcode)
|
|
1258
1320
|
msg = trade_retcode_message(result.retcode)
|
|
1259
|
-
|
|
1321
|
+
LOGGER.error(
|
|
1260
1322
|
f"Closing Order Request, {type.capitalize()}: #{ticket}, RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
1261
1323
|
)
|
|
1262
1324
|
else:
|
|
@@ -1276,13 +1338,59 @@ class Trade(RiskManagement):
|
|
|
1276
1338
|
tries += 1
|
|
1277
1339
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1278
1340
|
msg = trade_retcode_message(result.retcode)
|
|
1279
|
-
|
|
1341
|
+
LOGGER.info(f"Closing Order {msg}{addtionnal}")
|
|
1280
1342
|
info = f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol}, Price: @{request.get('price', 0.0)}"
|
|
1281
|
-
|
|
1343
|
+
LOGGER.info(info)
|
|
1282
1344
|
return True
|
|
1283
1345
|
else:
|
|
1284
1346
|
return False
|
|
1285
1347
|
|
|
1348
|
+
def modify_order(
|
|
1349
|
+
self,
|
|
1350
|
+
ticket: int,
|
|
1351
|
+
price: Optional[float] = None,
|
|
1352
|
+
stoplimit: Optional[float] = None,
|
|
1353
|
+
sl: Optional[float] = None,
|
|
1354
|
+
tp: Optional[float] = None,
|
|
1355
|
+
):
|
|
1356
|
+
"""
|
|
1357
|
+
Modify an open order by it ticket
|
|
1358
|
+
|
|
1359
|
+
Args:
|
|
1360
|
+
ticket (int): Order ticket to modify (e.g TradeOrder.ticket)
|
|
1361
|
+
price (float): The price at which to modify the order
|
|
1362
|
+
stoplimit (float): A price a pending Limit order is set at when the price reaches the 'price' value (this condition is mandatory).
|
|
1363
|
+
The pending order is not passed to the trading system until that moment
|
|
1364
|
+
sl (float): The stop loss in points
|
|
1365
|
+
tp (float): The take profit in points
|
|
1366
|
+
"""
|
|
1367
|
+
orders = self.get_orders(ticket=ticket) or []
|
|
1368
|
+
if len(orders) == 0:
|
|
1369
|
+
LOGGER.error(
|
|
1370
|
+
f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={price}"
|
|
1371
|
+
)
|
|
1372
|
+
return
|
|
1373
|
+
order = orders[0]
|
|
1374
|
+
request = {
|
|
1375
|
+
"action": Mt5.TRADE_ACTION_MODIFY,
|
|
1376
|
+
"order": ticket,
|
|
1377
|
+
"price": price or order.price_open,
|
|
1378
|
+
"sl": sl or order.sl,
|
|
1379
|
+
"tp": tp or order.tp,
|
|
1380
|
+
"stoplimit": stoplimit or order.price_stoplimit,
|
|
1381
|
+
}
|
|
1382
|
+
self.check_order(request)
|
|
1383
|
+
result = self.send_order(request)
|
|
1384
|
+
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1385
|
+
LOGGER.info(
|
|
1386
|
+
f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={price}, SL={sl}, TP={tp}, STOP_LIMIT={stoplimit}"
|
|
1387
|
+
)
|
|
1388
|
+
else:
|
|
1389
|
+
msg = trade_retcode_message(result.retcode)
|
|
1390
|
+
LOGGER.error(
|
|
1391
|
+
f"Unable to modify Order #{ticket}, RETCODE={result.retcode}: {msg}, SYMBOL={self.symbol}"
|
|
1392
|
+
)
|
|
1393
|
+
|
|
1286
1394
|
def close_order(
|
|
1287
1395
|
self, ticket: int, id: Optional[int] = None, comment: Optional[str] = None
|
|
1288
1396
|
):
|
|
@@ -1312,6 +1420,7 @@ class Trade(RiskManagement):
|
|
|
1312
1420
|
id: Optional[int] = None,
|
|
1313
1421
|
pct: Optional[float] = 1.0,
|
|
1314
1422
|
comment: Optional[str] = None,
|
|
1423
|
+
symbol: Optional[str] = None,
|
|
1315
1424
|
) -> bool:
|
|
1316
1425
|
"""
|
|
1317
1426
|
Close an open position by it ticket
|
|
@@ -1327,10 +1436,11 @@ class Trade(RiskManagement):
|
|
|
1327
1436
|
"""
|
|
1328
1437
|
# get all Actives positions
|
|
1329
1438
|
time.sleep(0.1)
|
|
1439
|
+
symbol = symbol or self.symbol
|
|
1330
1440
|
Id = id if id is not None else self.expert_id
|
|
1331
1441
|
positions = self.get_positions(ticket=ticket)
|
|
1332
|
-
buy_price = self.get_tick_info(
|
|
1333
|
-
sell_price = self.get_tick_info(
|
|
1442
|
+
buy_price = self.get_tick_info(symbol).ask
|
|
1443
|
+
sell_price = self.get_tick_info(symbol).bid
|
|
1334
1444
|
deviation = self.get_deviation()
|
|
1335
1445
|
if positions is not None and len(positions) == 1:
|
|
1336
1446
|
position = positions[0]
|
|
@@ -1338,7 +1448,7 @@ class Trade(RiskManagement):
|
|
|
1338
1448
|
buy = position.type == 0
|
|
1339
1449
|
request = {
|
|
1340
1450
|
"action": Mt5.TRADE_ACTION_DEAL,
|
|
1341
|
-
"symbol":
|
|
1451
|
+
"symbol": symbol,
|
|
1342
1452
|
"volume": (position.volume * pct),
|
|
1343
1453
|
"type": Mt5.ORDER_TYPE_SELL if buy else Mt5.ORDER_TYPE_BUY,
|
|
1344
1454
|
"position": ticket,
|
|
@@ -1379,15 +1489,15 @@ class Trade(RiskManagement):
|
|
|
1379
1489
|
tickets.remove(ticket)
|
|
1380
1490
|
time.sleep(1)
|
|
1381
1491
|
if tickets is not None and len(tickets) == 0:
|
|
1382
|
-
|
|
1492
|
+
LOGGER.info(
|
|
1383
1493
|
f"ALL {order_type.upper()} {tikets_type.upper()} closed, SYMBOL={self.symbol}."
|
|
1384
1494
|
)
|
|
1385
1495
|
else:
|
|
1386
|
-
|
|
1496
|
+
LOGGER.info(
|
|
1387
1497
|
f"{len(tickets)} {order_type.upper()} {tikets_type.upper()} not closed, SYMBOL={self.symbol}"
|
|
1388
1498
|
)
|
|
1389
1499
|
else:
|
|
1390
|
-
|
|
1500
|
+
LOGGER.info(
|
|
1391
1501
|
f"No {order_type.upper()} {tikets_type.upper()} to close, SYMBOL={self.symbol}."
|
|
1392
1502
|
)
|
|
1393
1503
|
|
|
@@ -1429,7 +1539,7 @@ class Trade(RiskManagement):
|
|
|
1429
1539
|
elif order_type == "sell_stop_limits":
|
|
1430
1540
|
orders = self.get_current_sell_stop_limits(id=id)
|
|
1431
1541
|
else:
|
|
1432
|
-
|
|
1542
|
+
LOGGER.error(f"Invalid order type: {order_type}")
|
|
1433
1543
|
return
|
|
1434
1544
|
self.bulk_close(
|
|
1435
1545
|
orders, "orders", self.close_order, order_type, id=id, comment=comment
|
|
@@ -1461,7 +1571,7 @@ class Trade(RiskManagement):
|
|
|
1461
1571
|
elif position_type == "losing":
|
|
1462
1572
|
positions = self.get_current_losings(id=id)
|
|
1463
1573
|
else:
|
|
1464
|
-
|
|
1574
|
+
LOGGER.error(f"Invalid position type: {position_type}")
|
|
1465
1575
|
return
|
|
1466
1576
|
self.bulk_close(
|
|
1467
1577
|
positions,
|
|
@@ -1642,8 +1752,12 @@ def create_trade_instance(
|
|
|
1642
1752
|
Note:
|
|
1643
1753
|
`daily_risk` and `max_risk` can be used to manage the risk of each symbol
|
|
1644
1754
|
based on the importance of the symbol in the portfolio or strategy.
|
|
1755
|
+
See bbstrader.metatrader.trade.Trade for more details.
|
|
1645
1756
|
"""
|
|
1646
|
-
|
|
1757
|
+
if not isinstance(params.get("logger"), (Logger, type(log))):
|
|
1758
|
+
loggr = log
|
|
1759
|
+
else:
|
|
1760
|
+
loggr = params.get("logger")
|
|
1647
1761
|
ids = params.get("expert_id", None)
|
|
1648
1762
|
trade_instances = {}
|
|
1649
1763
|
if not symbols:
|
|
@@ -1710,15 +1824,13 @@ def create_trade_instance(
|
|
|
1710
1824
|
)
|
|
1711
1825
|
trade_instances[symbol] = Trade(**params)
|
|
1712
1826
|
except Exception as e:
|
|
1713
|
-
|
|
1827
|
+
loggr.error(f"Creating Trade instance, SYMBOL={symbol} {e}")
|
|
1714
1828
|
|
|
1715
1829
|
if len(trade_instances) != len(symbols):
|
|
1716
1830
|
for symbol in symbols:
|
|
1717
1831
|
if symbol not in trade_instances:
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
f"Failed to create Trade instance for SYMBOL={symbol}"
|
|
1723
|
-
)
|
|
1832
|
+
loggr.error(f"Failed to create Trade instance for SYMBOL={symbol}")
|
|
1833
|
+
loggr.info(
|
|
1834
|
+
f"Trade instances created successfully for {len(trade_instances)} symbols."
|
|
1835
|
+
)
|
|
1724
1836
|
return trade_instances
|
bbstrader/metatrader/utils.py
CHANGED
|
@@ -2,8 +2,11 @@ from datetime import datetime
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from typing import NamedTuple, Optional
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
import MetaTrader5 as MT5
|
|
5
|
+
try:
|
|
6
|
+
import MetaTrader5 as MT5
|
|
7
|
+
except ImportError:
|
|
8
|
+
import bbstrader.compat # noqa: F401
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
__all__ = [
|
|
9
12
|
"TIMEFRAMES",
|