bbstrader 0.3.1__py3-none-any.whl → 0.3.3__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/__init__.py +1 -1
- bbstrader/__main__.py +7 -5
- bbstrader/btengine/backtest.py +7 -8
- bbstrader/btengine/data.py +3 -3
- bbstrader/btengine/execution.py +2 -2
- bbstrader/btengine/strategy.py +70 -17
- bbstrader/config.py +2 -2
- bbstrader/core/data.py +3 -1
- bbstrader/core/scripts.py +62 -19
- bbstrader/metatrader/account.py +108 -23
- bbstrader/metatrader/copier.py +753 -280
- bbstrader/metatrader/rates.py +2 -2
- bbstrader/metatrader/risk.py +1 -0
- bbstrader/metatrader/scripts.py +35 -9
- bbstrader/metatrader/trade.py +60 -43
- bbstrader/metatrader/utils.py +3 -5
- bbstrader/models/__init__.py +0 -1
- bbstrader/models/ml.py +55 -26
- bbstrader/models/nlp.py +159 -89
- bbstrader/models/optimization.py +1 -1
- bbstrader/models/risk.py +16 -386
- bbstrader/trading/execution.py +109 -50
- bbstrader/trading/strategies.py +9 -592
- bbstrader/tseries.py +39 -711
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.3.dist-info}/METADATA +36 -41
- bbstrader-0.3.3.dist-info/RECORD +47 -0
- bbstrader-0.3.1.dist-info/RECORD +0 -47
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.3.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.3.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.3.1.dist-info → bbstrader-0.3.3.dist-info}/top_level.txt +0 -0
bbstrader/trading/execution.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import functools
|
|
1
3
|
import multiprocessing as mp
|
|
2
4
|
import sys
|
|
3
5
|
import time
|
|
4
6
|
from datetime import date, datetime
|
|
7
|
+
from multiprocessing.synchronize import Event
|
|
5
8
|
from typing import Callable, Dict, List, Literal, Optional
|
|
6
9
|
|
|
7
10
|
import pandas as pd
|
|
@@ -9,7 +12,7 @@ from loguru import logger as log
|
|
|
9
12
|
|
|
10
13
|
from bbstrader.btengine.strategy import MT5Strategy, Strategy
|
|
11
14
|
from bbstrader.config import BBSTRADER_DIR
|
|
12
|
-
from bbstrader.metatrader.account import
|
|
15
|
+
from bbstrader.metatrader.account import check_mt5_connection
|
|
13
16
|
from bbstrader.metatrader.trade import Trade, TradeAction, TradingMode
|
|
14
17
|
from bbstrader.trading.utils import send_message
|
|
15
18
|
|
|
@@ -37,6 +40,8 @@ _TF_MAPPING = {
|
|
|
37
40
|
"D1": 1440,
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
MT5_ENGINE_TIMEFRAMES = list(_TF_MAPPING.keys())
|
|
44
|
+
|
|
40
45
|
TradingDays = ["monday", "tuesday", "wednesday", "thursday", "friday"]
|
|
41
46
|
WEEK_DAYS = TradingDays + ["saturday", "sunday"]
|
|
42
47
|
FRIDAY = "friday"
|
|
@@ -177,6 +182,8 @@ class Mt5ExecutionEngine:
|
|
|
177
182
|
mm: bool = True,
|
|
178
183
|
auto_trade: bool = True,
|
|
179
184
|
prompt_callback: Callable = None,
|
|
185
|
+
multithread: bool = False,
|
|
186
|
+
shutdown_event: Event = None,
|
|
180
187
|
optimizer: str = "equal",
|
|
181
188
|
trail: bool = True,
|
|
182
189
|
stop_trail: Optional[int] = None,
|
|
@@ -205,6 +212,10 @@ class Mt5ExecutionEngine:
|
|
|
205
212
|
the user for confimation.
|
|
206
213
|
prompt_callback : Callback function to prompt the user for confirmation.
|
|
207
214
|
This is useful when integrating with GUI applications.
|
|
215
|
+
multithread : If True, use a thread pool to process signals in parallel.
|
|
216
|
+
If False, process them sequentially. Set this to True only if the engine
|
|
217
|
+
is running in a separate process. Default to False.
|
|
218
|
+
shutdown_event : Use to terminate the copy process when runs in a custum environment like web App or GUI.
|
|
208
219
|
show_positions_orders : Print open positions and orders. Defaults to False.
|
|
209
220
|
iter_time : Interval to check for signals and `mm`. Defaults to 5.
|
|
210
221
|
use_trade_time : Open trades after the time is completed. Defaults to True.
|
|
@@ -249,6 +260,7 @@ class Mt5ExecutionEngine:
|
|
|
249
260
|
self.mm = mm
|
|
250
261
|
self.auto_trade = auto_trade
|
|
251
262
|
self.prompt_callback = prompt_callback
|
|
263
|
+
self.multithread = multithread
|
|
252
264
|
self.optimizer = optimizer
|
|
253
265
|
self.trail = trail
|
|
254
266
|
self.stop_trail = stop_trail
|
|
@@ -272,6 +284,9 @@ class Mt5ExecutionEngine:
|
|
|
272
284
|
|
|
273
285
|
self._initialize_engine(**kwargs)
|
|
274
286
|
self.strategy = self._init_strategy(**kwargs)
|
|
287
|
+
self.shutdown_event = (
|
|
288
|
+
shutdown_event if shutdown_event is not None else mp.Event()
|
|
289
|
+
)
|
|
275
290
|
self._running = True
|
|
276
291
|
|
|
277
292
|
def __repr__(self):
|
|
@@ -314,16 +329,17 @@ class Mt5ExecutionEngine:
|
|
|
314
329
|
def _print_exc(self, msg: str, e: Exception):
|
|
315
330
|
if isinstance(e, KeyboardInterrupt):
|
|
316
331
|
logger.info("Stopping the Execution Engine ...")
|
|
332
|
+
self.stop()
|
|
317
333
|
sys.exit(0)
|
|
318
334
|
if self.debug_mode:
|
|
319
335
|
raise ValueError(msg).with_traceback(e.__traceback__)
|
|
320
336
|
else:
|
|
321
|
-
logger.error(f"{msg
|
|
337
|
+
logger.error(f"{msg}: {type(e).__name__}: {str(e)}")
|
|
322
338
|
|
|
323
339
|
def _max_trades(self, mtrades):
|
|
324
340
|
max_trades = {
|
|
325
341
|
symbol: mtrades[symbol]
|
|
326
|
-
if mtrades is not None and symbol in mtrades
|
|
342
|
+
if mtrades is not None and isinstance(mtrades, dict) and symbol in mtrades
|
|
327
343
|
else self.trades_instances[symbol].max_trade()
|
|
328
344
|
for symbol in self.symbols
|
|
329
345
|
}
|
|
@@ -338,7 +354,7 @@ class Mt5ExecutionEngine:
|
|
|
338
354
|
expert_ids = [expert_ids]
|
|
339
355
|
return expert_ids
|
|
340
356
|
|
|
341
|
-
def _init_strategy(self, **kwargs) ->
|
|
357
|
+
def _init_strategy(self, **kwargs) -> MT5Strategy:
|
|
342
358
|
try:
|
|
343
359
|
check_mt5_connection(**kwargs)
|
|
344
360
|
strategy = self.strategy_cls(
|
|
@@ -356,7 +372,7 @@ class Mt5ExecutionEngine:
|
|
|
356
372
|
return strategy
|
|
357
373
|
|
|
358
374
|
def _get_signal_info(self, signal, symbol, price, stoplimit):
|
|
359
|
-
account =
|
|
375
|
+
account = self.strategy.account
|
|
360
376
|
symbol_info = account.get_symbol_info(symbol)
|
|
361
377
|
|
|
362
378
|
common_data = {
|
|
@@ -512,6 +528,9 @@ class Mt5ExecutionEngine:
|
|
|
512
528
|
or (period_type == "day" and closing)
|
|
513
529
|
or (period_type == "24/7" and closing)
|
|
514
530
|
):
|
|
531
|
+
logger.info(
|
|
532
|
+
f"{self.ACCOUNT} Closing all positions and orders for {symbol} ..."
|
|
533
|
+
)
|
|
515
534
|
for id in self.expert_ids:
|
|
516
535
|
trade.close_positions(
|
|
517
536
|
position_type="all", id=id, comment=self.comment
|
|
@@ -627,13 +646,15 @@ class Mt5ExecutionEngine:
|
|
|
627
646
|
)
|
|
628
647
|
pass
|
|
629
648
|
|
|
630
|
-
def
|
|
649
|
+
def _auto_trade(self, sigmsg, symbol) -> bool:
|
|
631
650
|
if self.notify:
|
|
632
651
|
self._send_notification(sigmsg, symbol)
|
|
633
652
|
if self.auto_trade:
|
|
634
653
|
return True
|
|
635
654
|
if not self.auto_trade:
|
|
636
|
-
prompt =
|
|
655
|
+
prompt = (
|
|
656
|
+
f"{sigmsg} \n Enter Y/Yes to accept or N/No to reject this order : "
|
|
657
|
+
)
|
|
637
658
|
if self.prompt_callback is not None:
|
|
638
659
|
auto_trade = self.prompt_callback(prompt)
|
|
639
660
|
else:
|
|
@@ -649,7 +670,7 @@ class Mt5ExecutionEngine:
|
|
|
649
670
|
def _open_buy(
|
|
650
671
|
self, signal, symbol, id, trade: Trade, price, stoplimit, sigmsg, msg, comment
|
|
651
672
|
):
|
|
652
|
-
if not self.
|
|
673
|
+
if not self._auto_trade(sigmsg, symbol):
|
|
653
674
|
return
|
|
654
675
|
if not self._check_retcode(trade, "BMKT"):
|
|
655
676
|
logger.info(msg)
|
|
@@ -666,7 +687,7 @@ class Mt5ExecutionEngine:
|
|
|
666
687
|
def _open_sell(
|
|
667
688
|
self, signal, symbol, id, trade: Trade, price, stoplimit, sigmsg, msg, comment
|
|
668
689
|
):
|
|
669
|
-
if not self.
|
|
690
|
+
if not self._auto_trade(sigmsg, symbol):
|
|
670
691
|
return
|
|
671
692
|
if not self._check_retcode(trade, "SMKT"):
|
|
672
693
|
logger.info(msg)
|
|
@@ -859,46 +880,79 @@ class Mt5ExecutionEngine:
|
|
|
859
880
|
f"(e.g., if time_frame is 15m, iter_time must be 1.5, 3, 5, 15 etc)"
|
|
860
881
|
)
|
|
861
882
|
|
|
862
|
-
def
|
|
883
|
+
def _handle_one_signal(self, signal, today, buys, sells):
|
|
863
884
|
try:
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
action
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
if len(self.symbols) >= 10:
|
|
888
|
-
if symbol == self.symbols[-1]:
|
|
889
|
-
logger.info(
|
|
890
|
-
f"Not trading Time !!!, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
891
|
-
)
|
|
892
|
-
else:
|
|
885
|
+
symbol = signal.symbol
|
|
886
|
+
trade: Trade = self.trades_instances[symbol]
|
|
887
|
+
if trade.trading_time() and today in self.trading_days:
|
|
888
|
+
if signal.action is not None:
|
|
889
|
+
action = (
|
|
890
|
+
signal.action.value
|
|
891
|
+
if isinstance(signal.action, TradeAction)
|
|
892
|
+
else signal.action
|
|
893
|
+
)
|
|
894
|
+
self._run_trade_algorithm(
|
|
895
|
+
action,
|
|
896
|
+
symbol,
|
|
897
|
+
signal.id,
|
|
898
|
+
trade,
|
|
899
|
+
signal.price,
|
|
900
|
+
signal.stoplimit,
|
|
901
|
+
buys,
|
|
902
|
+
sells,
|
|
903
|
+
signal.comment or self.comment,
|
|
904
|
+
)
|
|
905
|
+
else:
|
|
906
|
+
if len(self.symbols) >= 10:
|
|
907
|
+
if symbol == self.symbols[-1]:
|
|
893
908
|
logger.info(
|
|
894
|
-
f"Not trading Time
|
|
909
|
+
f"Not trading Time !!!, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
895
910
|
)
|
|
896
|
-
|
|
911
|
+
else:
|
|
912
|
+
logger.info(
|
|
913
|
+
f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
914
|
+
)
|
|
915
|
+
self._check(buys[symbol], sells[symbol], symbol)
|
|
897
916
|
|
|
898
917
|
except Exception as e:
|
|
899
|
-
msg =
|
|
918
|
+
msg = (
|
|
919
|
+
f"Error handling signal for SYMBOL={signal.symbol} (SIGNAL: {action}), "
|
|
920
|
+
f"STRATEGY={self.STRATEGY}, ACCOUNT={self.ACCOUNT}"
|
|
921
|
+
)
|
|
900
922
|
self._print_exc(msg, e)
|
|
901
|
-
|
|
923
|
+
|
|
924
|
+
def _handle_all_signals(self, today, signals, buys, sells, max_workers=50):
|
|
925
|
+
try:
|
|
926
|
+
check_mt5_connection(**self.kwargs)
|
|
927
|
+
except Exception as e:
|
|
928
|
+
msg = "Initial MT5 connection check failed. Aborting signal processing."
|
|
929
|
+
self._print_exc(msg, e)
|
|
930
|
+
return
|
|
931
|
+
|
|
932
|
+
if not signals:
|
|
933
|
+
logger.info("No signals to process.")
|
|
934
|
+
return
|
|
935
|
+
|
|
936
|
+
# We want to create a temporary function that
|
|
937
|
+
# already has the 'today', 'buys', and 'sells' arguments filled in.
|
|
938
|
+
# This is necessary because executor.map only iterates over one sequence (signals).
|
|
939
|
+
signal_processor = functools.partial(
|
|
940
|
+
self._handle_one_signal, today=today, buys=buys, sells=sells
|
|
941
|
+
)
|
|
942
|
+
if self.multithread:
|
|
943
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
944
|
+
max_workers=max_workers
|
|
945
|
+
) as executor:
|
|
946
|
+
# 'map' will apply our worker function to every item in the 'signals' list.
|
|
947
|
+
# It will automatically manage the distribution of tasks to the worker threads.
|
|
948
|
+
# We wrap it in list() to ensure all tasks are complete before moving on.
|
|
949
|
+
list(executor.map(signal_processor, signals))
|
|
950
|
+
else:
|
|
951
|
+
for signal in signals:
|
|
952
|
+
try:
|
|
953
|
+
signal_processor(signal)
|
|
954
|
+
except Exception as e:
|
|
955
|
+
self._print_exc(f"Failed to process signal {signal}: ", e)
|
|
902
956
|
|
|
903
957
|
def _handle_period_end_actions(self, today):
|
|
904
958
|
try:
|
|
@@ -918,7 +972,7 @@ class Mt5ExecutionEngine:
|
|
|
918
972
|
pass
|
|
919
973
|
|
|
920
974
|
def run(self):
|
|
921
|
-
while self._running:
|
|
975
|
+
while self._running and not self.shutdown_event.is_set():
|
|
922
976
|
try:
|
|
923
977
|
check_mt5_connection(**self.kwargs)
|
|
924
978
|
positions_orders = self._check_positions_orders()
|
|
@@ -936,23 +990,27 @@ class Mt5ExecutionEngine:
|
|
|
936
990
|
self._check(buys[symbol], sells[symbol], symbol)
|
|
937
991
|
else:
|
|
938
992
|
self._update_risk(weights)
|
|
939
|
-
self.
|
|
993
|
+
self._handle_all_signals(today, signals, buys, sells)
|
|
940
994
|
self._sleep()
|
|
941
995
|
self._handle_period_end_actions(today)
|
|
942
996
|
except KeyboardInterrupt:
|
|
943
997
|
logger.info(
|
|
944
|
-
f"Stopping Execution Engine for {self.STRATEGY} on {self.ACCOUNT}"
|
|
998
|
+
f"Stopping Execution Engine for {self.STRATEGY} STRATEGY on {self.ACCOUNT} Account"
|
|
945
999
|
)
|
|
946
|
-
|
|
1000
|
+
self.stop()
|
|
1001
|
+
sys.exit(0)
|
|
947
1002
|
except Exception as e:
|
|
948
1003
|
msg = f"Running Execution Engine, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
|
|
949
1004
|
self._print_exc(msg, e)
|
|
950
|
-
continue
|
|
951
1005
|
|
|
952
1006
|
def stop(self):
|
|
953
1007
|
"""Stops the execution engine."""
|
|
954
|
-
self._running
|
|
955
|
-
|
|
1008
|
+
if self._running:
|
|
1009
|
+
logger.info(
|
|
1010
|
+
f"Stopping Execution Engine for {self.STRATEGY} STRATEGY on {self.ACCOUNT} Account"
|
|
1011
|
+
)
|
|
1012
|
+
self._running = False
|
|
1013
|
+
self.shutdown_event.set()
|
|
956
1014
|
logger.info("Execution Engine stopped successfully.")
|
|
957
1015
|
|
|
958
1016
|
|
|
@@ -1000,6 +1058,7 @@ def RunMt5Engines(accounts: Dict[str, Dict], start_delay: float = 1.0):
|
|
|
1000
1058
|
|
|
1001
1059
|
for account_id, params in accounts.items():
|
|
1002
1060
|
log.info(f"Starting process for {account_id}")
|
|
1061
|
+
params["multithread"] = True
|
|
1003
1062
|
process = mp.Process(target=RunMt5Engine, args=(account_id,), kwargs=params)
|
|
1004
1063
|
process.start()
|
|
1005
1064
|
processes[process] = account_id
|