bbstrader 0.2.9__py3-none-any.whl → 0.2.92__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 +1 -0
- 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 +1 -1
- bbstrader/config.py +2 -4
- bbstrader/core/utils.py +90 -1
- bbstrader/metatrader/__init__.py +2 -1
- bbstrader/metatrader/account.py +27 -37
- bbstrader/metatrader/copier.py +735 -0
- bbstrader/metatrader/rates.py +6 -3
- bbstrader/metatrader/risk.py +19 -8
- bbstrader/metatrader/scripts.py +81 -0
- bbstrader/metatrader/trade.py +165 -63
- bbstrader/metatrader/utils.py +5 -2
- bbstrader/models/ml.py +1 -0
- bbstrader/trading/execution.py +145 -32
- bbstrader/trading/script.py +155 -0
- bbstrader/trading/scripts.py +2 -0
- bbstrader/tseries.py +33 -7
- {bbstrader-0.2.9.dist-info → bbstrader-0.2.92.dist-info}/METADATA +9 -3
- bbstrader-0.2.92.dist-info/RECORD +44 -0
- bbstrader-0.2.92.dist-info/entry_points.txt +2 -0
- bbstrader-0.2.9.dist-info/RECORD +0 -38
- {bbstrader-0.2.9.dist-info → bbstrader-0.2.92.dist-info}/LICENSE +0 -0
- {bbstrader-0.2.9.dist-info → bbstrader-0.2.92.dist-info}/WHEEL +0 -0
- {bbstrader-0.2.9.dist-info → bbstrader-0.2.92.dist-info}/top_level.txt +0 -0
bbstrader/trading/execution.py
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
1
2
|
import time
|
|
2
3
|
import traceback
|
|
3
|
-
import MetaTrader5 as MT5
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
from logging import Logger
|
|
6
5
|
from typing import Dict, List, Literal, Optional
|
|
7
6
|
|
|
7
|
+
from loguru import logger as log
|
|
8
|
+
|
|
8
9
|
from bbstrader.btengine.strategy import MT5Strategy, Strategy
|
|
9
|
-
from bbstrader.
|
|
10
|
+
from bbstrader.config import BBSTRADER_DIR
|
|
11
|
+
from bbstrader.core.utils import TradeAction
|
|
12
|
+
from bbstrader.metatrader.account import Account, check_mt5_connection
|
|
10
13
|
from bbstrader.metatrader.trade import Trade
|
|
11
|
-
from bbstrader.metatrader.account import Account
|
|
12
14
|
from bbstrader.trading.scripts import send_message
|
|
13
|
-
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import MetaTrader5 as MT5
|
|
18
|
+
except ImportError:
|
|
19
|
+
import bbstrader.compat # noqa: F401
|
|
20
|
+
|
|
14
21
|
|
|
15
22
|
__all__ = ["MT5ExecutionEngine", "TWSExecutionEngine"]
|
|
16
23
|
|
|
@@ -31,6 +38,7 @@ _TF_MAPPING = {
|
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
TradingDays = ["monday", "tuesday", "wednesday", "thursday", "friday"]
|
|
41
|
+
WEEK_DAYS = TradingDays + ["saturday", "sunday"]
|
|
34
42
|
|
|
35
43
|
BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
|
|
36
44
|
SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
|
|
@@ -77,6 +85,13 @@ NON_EXEC_RETCODES = {
|
|
|
77
85
|
"SMKT": [MT5.TRADE_RETCODE_LONG_ONLY] + COMMON_RETCODES,
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
log.add(
|
|
89
|
+
f"{BBSTRADER_DIR}/logs/execution.log",
|
|
90
|
+
enqueue=True,
|
|
91
|
+
level="INFO",
|
|
92
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
93
|
+
)
|
|
94
|
+
|
|
80
95
|
|
|
81
96
|
def _mt5_execution(
|
|
82
97
|
symbol_list,
|
|
@@ -99,18 +114,22 @@ def _mt5_execution(
|
|
|
99
114
|
comment,
|
|
100
115
|
**kwargs,
|
|
101
116
|
):
|
|
117
|
+
logger = kwargs.get("logger", log)
|
|
118
|
+
|
|
102
119
|
def _print_exc(dm, msg):
|
|
103
120
|
traceback.print_exc() if dm else logger.error(msg)
|
|
104
121
|
|
|
105
122
|
try:
|
|
106
123
|
symbols = symbol_list.copy()
|
|
107
124
|
time_frame = kwargs.get("time_frame", "15m")
|
|
125
|
+
daily_risk = kwargs.get("daily_risk")
|
|
108
126
|
STRATEGY = kwargs.get("strategy_name")
|
|
109
127
|
mtrades = kwargs.get("max_trades")
|
|
110
128
|
notify = kwargs.get("notify", False)
|
|
111
129
|
signal_tickers = kwargs.get("signal_tickers", symbols)
|
|
112
130
|
debug_mode = kwargs.get("debug_mode", False)
|
|
113
131
|
delay = kwargs.get("delay", 0)
|
|
132
|
+
ACCOUNT = kwargs.get("account", "MT5 Account")
|
|
114
133
|
if notify:
|
|
115
134
|
telegram = kwargs.get("telegram", False)
|
|
116
135
|
bot_token = kwargs.get("bot_token")
|
|
@@ -124,9 +143,6 @@ def _mt5_execution(
|
|
|
124
143
|
elif isinstance(expert_ids, int):
|
|
125
144
|
expert_ids = [expert_ids]
|
|
126
145
|
|
|
127
|
-
logger: Logger = kwargs.get("logger")
|
|
128
|
-
if logger is None:
|
|
129
|
-
logger: Logger = trades_instances[symbols[0]].logger
|
|
130
146
|
max_trades = {
|
|
131
147
|
symbol: mtrades[symbol]
|
|
132
148
|
if mtrades is not None and symbol in mtrades
|
|
@@ -136,8 +152,14 @@ def _mt5_execution(
|
|
|
136
152
|
if comment is None:
|
|
137
153
|
trade = trades_instances[symbols[0]]
|
|
138
154
|
comment = f"{trade.expert_name}@{trade.version}"
|
|
155
|
+
|
|
156
|
+
if period.lower() == "24/7":
|
|
157
|
+
trading_days = WEEK_DAYS
|
|
139
158
|
except Exception:
|
|
140
|
-
_print_exc(
|
|
159
|
+
_print_exc(
|
|
160
|
+
debug_mode,
|
|
161
|
+
f"Initializing Execution Engine, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}",
|
|
162
|
+
)
|
|
141
163
|
return
|
|
142
164
|
|
|
143
165
|
def update_risk(weights):
|
|
@@ -146,7 +168,8 @@ def _mt5_execution(
|
|
|
146
168
|
if symbol not in weights:
|
|
147
169
|
continue
|
|
148
170
|
trade = trades_instances[symbol]
|
|
149
|
-
|
|
171
|
+
dailydd = round(weights[symbol] * daily_risk, 5)
|
|
172
|
+
trade.dailydd = dailydd
|
|
150
173
|
|
|
151
174
|
def check_retcode(trade: Trade, position):
|
|
152
175
|
if len(trade.retcodes) > 0:
|
|
@@ -181,14 +204,18 @@ def _mt5_execution(
|
|
|
181
204
|
check_mt5_connection(**kwargs)
|
|
182
205
|
strategy: MT5Strategy = strategy_cls(symbol_list=symbols, mode="live", **kwargs)
|
|
183
206
|
except Exception:
|
|
184
|
-
_print_exc(
|
|
207
|
+
_print_exc(
|
|
208
|
+
debug_mode, f"Initializing strategy, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}"
|
|
209
|
+
)
|
|
185
210
|
return
|
|
186
|
-
logger.info(
|
|
211
|
+
logger.info(
|
|
212
|
+
f"Running {STRATEGY} Strategy in {time_frame} Interval ..., ACCOUNT={ACCOUNT}"
|
|
213
|
+
)
|
|
187
214
|
|
|
188
215
|
def run_trade_algorithm(signal, symbol, id, trade: Trade, price, stoplimit):
|
|
189
216
|
signal = "BMKT" if signal == "LONG" or signal == "BUY" else signal
|
|
190
217
|
signal = "SMKT" if signal == "SHORT" or signal == "SELL" else signal
|
|
191
|
-
info = f"SIGNAL = {signal}, SYMBOL={symbol}, STRATEGY={STRATEGY}, TIMEFRAME={time_frame}"
|
|
218
|
+
info = f"SIGNAL = {signal}, SYMBOL={symbol}, STRATEGY={STRATEGY}, TIMEFRAME={time_frame} , ACCOUNT={ACCOUNT}"
|
|
192
219
|
account = Account(**kwargs)
|
|
193
220
|
symbol_type = account.get_symbol_type(symbol)
|
|
194
221
|
desc = account.get_symbol_info(symbol).description
|
|
@@ -197,9 +224,9 @@ def _mt5_execution(
|
|
|
197
224
|
f"\nPRICE={price}, \nSTOPLIMIT={stoplimit}, \nSTRATEGY={STRATEGY}, \nTIMEFRAME={time_frame}"
|
|
198
225
|
f"\nBROKER={account.broker.name}, \nTIMESTAMP={datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
|
199
226
|
)
|
|
200
|
-
msg = f"Sending {signal} Order ... SYMBOL={symbol}, STRATEGY={STRATEGY}"
|
|
201
|
-
tfmsg = f"Time Frame Not completed !!! SYMBOL={symbol}, STRATEGY={STRATEGY}"
|
|
202
|
-
riskmsg = f"Risk not allowed !!! SYMBOL={symbol}, STRATEGY={STRATEGY}"
|
|
227
|
+
msg = f"Sending {signal} Order ... SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
228
|
+
tfmsg = f"Time Frame Not completed !!! SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
229
|
+
riskmsg = f"Risk not allowed !!! SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
203
230
|
if signal not in EXIT_SIGNAL_ACTIONS:
|
|
204
231
|
if signal in NON_EXEC_RETCODES and not check_retcode(trade, signal):
|
|
205
232
|
logger.info(info)
|
|
@@ -324,7 +351,7 @@ def _mt5_execution(
|
|
|
324
351
|
if show_positions_orders:
|
|
325
352
|
logger.info(
|
|
326
353
|
f"Current {type.upper()} SYMBOL={symbol}: \
|
|
327
|
-
{positions_orders[type][symbol]}, STRATEGY={STRATEGY}"
|
|
354
|
+
{positions_orders[type][symbol]}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
328
355
|
)
|
|
329
356
|
long_market = {
|
|
330
357
|
symbol: buys[symbol] is not None
|
|
@@ -338,7 +365,8 @@ def _mt5_execution(
|
|
|
338
365
|
}
|
|
339
366
|
except Exception:
|
|
340
367
|
_print_exc(
|
|
341
|
-
debug_mode,
|
|
368
|
+
debug_mode,
|
|
369
|
+
f"Checking positions and orders, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}",
|
|
342
370
|
)
|
|
343
371
|
continue
|
|
344
372
|
time.sleep(0.5)
|
|
@@ -352,7 +380,10 @@ def _mt5_execution(
|
|
|
352
380
|
)
|
|
353
381
|
update_risk(weights)
|
|
354
382
|
except Exception:
|
|
355
|
-
_print_exc(
|
|
383
|
+
_print_exc(
|
|
384
|
+
debug_mode,
|
|
385
|
+
f"Calculating Signals, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}",
|
|
386
|
+
)
|
|
356
387
|
continue
|
|
357
388
|
if len(signals) == 0:
|
|
358
389
|
for symbol in symbols:
|
|
@@ -382,16 +413,16 @@ def _mt5_execution(
|
|
|
382
413
|
if len(symbols) >= 10:
|
|
383
414
|
if symbol == symbols[-1]:
|
|
384
415
|
logger.info(
|
|
385
|
-
f"Not trading Time !!!, STRATEGY={STRATEGY}"
|
|
416
|
+
f"Not trading Time !!!, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
386
417
|
)
|
|
387
418
|
else:
|
|
388
419
|
logger.info(
|
|
389
|
-
f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
|
|
420
|
+
f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
390
421
|
)
|
|
391
422
|
check(buys[symbol], sells[symbol], symbol)
|
|
392
423
|
|
|
393
424
|
except Exception:
|
|
394
|
-
msg = f"Handling Signals, SYMBOL={symbol}, STRATEGY={STRATEGY}"
|
|
425
|
+
msg = f"Handling Signals, SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
395
426
|
_print_exc(debug_mode, msg)
|
|
396
427
|
continue
|
|
397
428
|
|
|
@@ -420,20 +451,40 @@ def _mt5_execution(
|
|
|
420
451
|
|
|
421
452
|
def logmsg(period, symbol):
|
|
422
453
|
logger.info(
|
|
423
|
-
f"End of the {period} !!! SYMBOL={symbol}, STRATEGY={STRATEGY}"
|
|
454
|
+
f"End of the {period} !!! SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
424
455
|
)
|
|
425
456
|
|
|
426
457
|
def logmsgif(period, symbol):
|
|
427
458
|
if len(symbols) <= 10:
|
|
428
459
|
logmsg(period, symbol)
|
|
429
460
|
elif len(symbols) > 10 and symbol == symbols[-1]:
|
|
430
|
-
logger.info(
|
|
461
|
+
logger.info(
|
|
462
|
+
f"End of the {period} !!! STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
463
|
+
)
|
|
431
464
|
|
|
432
465
|
def sleepmsg(sleep_time):
|
|
433
|
-
logger.info(f"Sleeping for {sleep_time} minutes ...\n")
|
|
466
|
+
logger.info(f"{ACCOUNT} Sleeping for {sleep_time} minutes ...\n")
|
|
434
467
|
|
|
435
|
-
sessionmsg = "STARTING NEW TRADING SESSION ...\n"
|
|
436
|
-
if period.lower() == "
|
|
468
|
+
sessionmsg = f"{ACCOUNT} STARTING NEW TRADING SESSION ...\n"
|
|
469
|
+
if period.lower() == "24/7": # For Cryptos
|
|
470
|
+
for symbol in symbols:
|
|
471
|
+
trade = trades_instances[symbol]
|
|
472
|
+
if trade.days_end() and closing:
|
|
473
|
+
for id in expert_ids:
|
|
474
|
+
trade.close_positions(
|
|
475
|
+
position_type="all", id=id, comment=comment
|
|
476
|
+
)
|
|
477
|
+
logmsgif("Day", symbol)
|
|
478
|
+
trade.statistics(save=True)
|
|
479
|
+
if day_end:
|
|
480
|
+
strategy.perform_period_end_checks()
|
|
481
|
+
sleep_time = trades_instances[symbols[-1]].sleep_time()
|
|
482
|
+
sleepmsg(sleep_time + delay)
|
|
483
|
+
time.sleep(60 * sleep_time + delay)
|
|
484
|
+
logger.info(sessionmsg)
|
|
485
|
+
time_intervals = 0
|
|
486
|
+
|
|
487
|
+
elif period.lower() == "day":
|
|
437
488
|
for symbol in symbols:
|
|
438
489
|
trade = trades_instances[symbol]
|
|
439
490
|
if trade.days_end() and closing:
|
|
@@ -455,6 +506,7 @@ def _mt5_execution(
|
|
|
455
506
|
sleepmsg(sleep_time + delay)
|
|
456
507
|
time.sleep(60 * sleep_time + delay)
|
|
457
508
|
logger.info(sessionmsg)
|
|
509
|
+
time_intervals = 0
|
|
458
510
|
elif period_end_action == "sleep" and today == FRIDAY:
|
|
459
511
|
sleep_time = trades_instances[symbols[-1]].sleep_time(
|
|
460
512
|
weekend=True
|
|
@@ -462,6 +514,7 @@ def _mt5_execution(
|
|
|
462
514
|
sleepmsg(sleep_time + delay)
|
|
463
515
|
time.sleep(60 * sleep_time + delay)
|
|
464
516
|
logger.info(sessionmsg)
|
|
517
|
+
time_intervals = 0
|
|
465
518
|
|
|
466
519
|
elif period.lower() == "week":
|
|
467
520
|
for symbol in symbols:
|
|
@@ -482,6 +535,7 @@ def _mt5_execution(
|
|
|
482
535
|
sleepmsg(sleep_time + delay)
|
|
483
536
|
time.sleep(60 * sleep_time + delay)
|
|
484
537
|
logger.info(sessionmsg)
|
|
538
|
+
time_intervals = 0
|
|
485
539
|
elif day_end and today == FRIDAY:
|
|
486
540
|
strategy.perform_period_end_checks()
|
|
487
541
|
if period_end_action == "break" and closing:
|
|
@@ -493,6 +547,7 @@ def _mt5_execution(
|
|
|
493
547
|
sleepmsg(sleep_time + delay)
|
|
494
548
|
time.sleep(60 * sleep_time + delay)
|
|
495
549
|
logger.info(sessionmsg)
|
|
550
|
+
time_intervals = 0
|
|
496
551
|
|
|
497
552
|
elif period.lower() == "month":
|
|
498
553
|
for symbol in symbols:
|
|
@@ -502,9 +557,7 @@ def _mt5_execution(
|
|
|
502
557
|
elif trade.days_end() and today == FRIDAY:
|
|
503
558
|
logmsgif("Week", symbol)
|
|
504
559
|
elif (
|
|
505
|
-
trade.days_end()
|
|
506
|
-
and today == FRIDAY
|
|
507
|
-
and num_days >= 20
|
|
560
|
+
trade.days_end() and today == FRIDAY and num_days >= 20
|
|
508
561
|
) and closing:
|
|
509
562
|
for id in expert_ids:
|
|
510
563
|
trade.close_positions(
|
|
@@ -517,18 +570,22 @@ def _mt5_execution(
|
|
|
517
570
|
sleepmsg(sleep_time + delay)
|
|
518
571
|
time.sleep(60 * sleep_time + delay)
|
|
519
572
|
logger.info(sessionmsg)
|
|
573
|
+
time_intervals = 0
|
|
520
574
|
num_days += 1
|
|
521
575
|
elif day_end and today == FRIDAY:
|
|
522
576
|
sleep_time = trades_instances[symbols[-1]].sleep_time(weekend=True)
|
|
523
577
|
sleepmsg(sleep_time + delay)
|
|
524
578
|
time.sleep(60 * sleep_time + delay)
|
|
525
579
|
logger.info(sessionmsg)
|
|
580
|
+
time_intervals = 0
|
|
526
581
|
num_days += 1
|
|
527
582
|
elif day_end and today == FRIDAY and num_days >= 20:
|
|
528
583
|
strategy.perform_period_end_checks()
|
|
529
584
|
break
|
|
530
585
|
except Exception:
|
|
531
|
-
msg =
|
|
586
|
+
msg = (
|
|
587
|
+
f"Handling period end actions, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
588
|
+
)
|
|
532
589
|
_print_exc(debug_mode, msg)
|
|
533
590
|
continue
|
|
534
591
|
|
|
@@ -638,7 +695,7 @@ class MT5ExecutionEngine:
|
|
|
638
695
|
symbol_list : List of symbols to trade
|
|
639
696
|
trades_instances : Dictionary of Trade instances
|
|
640
697
|
strategy_cls : Strategy class to use for trading
|
|
641
|
-
mm : Enable Money Management. Defaults to
|
|
698
|
+
mm : Enable Money Management. Defaults to True.
|
|
642
699
|
optimizer : Risk management optimizer. Defaults to 'equal'.
|
|
643
700
|
See `bbstrader.models.optimization` module for more information.
|
|
644
701
|
show_positions_orders : Print open positions and orders. Defaults to False.
|
|
@@ -726,4 +783,60 @@ class MT5ExecutionEngine:
|
|
|
726
783
|
)
|
|
727
784
|
|
|
728
785
|
|
|
786
|
+
def mt5_engine(account_id: str, **kwargs):
|
|
787
|
+
"""Starts an MT5 execution engine for a given account.
|
|
788
|
+
Args:
|
|
789
|
+
account_id: Account ID to run the execution engine on.
|
|
790
|
+
**kwargs: Additional keyword arguments
|
|
791
|
+
_ symbol_list : List of symbols to trade.
|
|
792
|
+
- trades_instances : Dictionary of Trade instances.
|
|
793
|
+
- strategy_cls : Strategy class to use for trading.
|
|
794
|
+
"""
|
|
795
|
+
log.info(f"Starting execution engine for {account_id}")
|
|
796
|
+
|
|
797
|
+
symbol_list = kwargs.pop("symbol_list")
|
|
798
|
+
trades_instances = kwargs.pop("trades_instances")
|
|
799
|
+
strategy_cls = kwargs.pop("strategy_cls")
|
|
800
|
+
|
|
801
|
+
if symbol_list is None or trades_instances is None or strategy_cls is None:
|
|
802
|
+
log.error(f"Missing required arguments for account {account_id}")
|
|
803
|
+
raise ValueError(f"Missing required arguments for account {account_id}")
|
|
804
|
+
|
|
805
|
+
try:
|
|
806
|
+
engine = MT5ExecutionEngine(
|
|
807
|
+
symbol_list, trades_instances, strategy_cls, **kwargs
|
|
808
|
+
)
|
|
809
|
+
engine.run()
|
|
810
|
+
except Exception as e:
|
|
811
|
+
log.exception(f"Error running execution engine for {account_id}: {e}")
|
|
812
|
+
finally:
|
|
813
|
+
log.info(f"Execution engine for {account_id} completed")
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def RunMt5Engines(accounts: Dict[str, Dict], start_delay: float = 1.0):
|
|
817
|
+
"""Runs multiple MT5 execution engines in parallel using multiprocessing.
|
|
818
|
+
|
|
819
|
+
Args:
|
|
820
|
+
accounts: Dictionary of accounts to run the execution engines on.
|
|
821
|
+
Keys are the account names or IDs and values are the parameters for the execution engine.
|
|
822
|
+
The parameters are the same as the ones passed to the `MT5ExecutionEngine` class.
|
|
823
|
+
start_delay: Delay in seconds between starting the processes. Defaults to 1.0.
|
|
824
|
+
"""
|
|
825
|
+
|
|
826
|
+
processes = {}
|
|
827
|
+
|
|
828
|
+
for account_id, params in accounts.items():
|
|
829
|
+
log.info(f"Starting process for {account_id}")
|
|
830
|
+
process = mp.Process(target=mt5_engine, args=(account_id,), kwargs=params)
|
|
831
|
+
process.start()
|
|
832
|
+
processes[process] = account_id
|
|
833
|
+
|
|
834
|
+
if start_delay:
|
|
835
|
+
time.sleep(start_delay)
|
|
836
|
+
|
|
837
|
+
for process, account_id in processes.items():
|
|
838
|
+
process.join()
|
|
839
|
+
log.info(f"Process for {account_id} joined")
|
|
840
|
+
|
|
841
|
+
|
|
729
842
|
class TWSExecutionEngine: ...
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import multiprocessing as mp
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from bbstrader.btengine import MT5Strategy, Strategy
|
|
8
|
+
from bbstrader.core.utils import load_class, load_module
|
|
9
|
+
from bbstrader.metatrader.trade import create_trade_instance
|
|
10
|
+
from bbstrader.trading.execution import mt5_engine
|
|
11
|
+
|
|
12
|
+
EXECUTION_PATH = os.path.expanduser("~/.bbstrader/execution/execution.py")
|
|
13
|
+
CONFIG_PATH = os.path.expanduser("~/.bbstrader/execution/execution.json")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_config(config_path, strategy_name, account=None):
|
|
17
|
+
if not os.path.exists(config_path):
|
|
18
|
+
raise FileNotFoundError(f"Configuration file not found at {config_path}")
|
|
19
|
+
with open(config_path, "r") as f:
|
|
20
|
+
config = json.load(f)
|
|
21
|
+
try:
|
|
22
|
+
config = config[strategy_name]
|
|
23
|
+
except KeyError:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"Strategy {strategy_name} not found in the configuration file."
|
|
26
|
+
)
|
|
27
|
+
if account is not None:
|
|
28
|
+
try:
|
|
29
|
+
config = config[account]
|
|
30
|
+
except KeyError:
|
|
31
|
+
raise ValueError(f"Account {account} not found in the configuration file.")
|
|
32
|
+
if config.get("symbol_list") is None:
|
|
33
|
+
raise ValueError("symbol_list is required in the configuration file.")
|
|
34
|
+
if config.get("trades_kwargs") is None:
|
|
35
|
+
raise ValueError("trades_kwargs is required in the configuration file.")
|
|
36
|
+
return config
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def worker_function(account, args):
|
|
40
|
+
strategy_module = load_module(args.path)
|
|
41
|
+
strategy_class = load_class(strategy_module, args.strategy, (MT5Strategy, Strategy))
|
|
42
|
+
|
|
43
|
+
config = load_config(args.config, args.strategy, account)
|
|
44
|
+
symbol_list = config.pop("symbol_list")
|
|
45
|
+
trades_kwargs = config.pop("trades_kwargs")
|
|
46
|
+
trades = create_trade_instance(symbol_list, trades_kwargs)
|
|
47
|
+
|
|
48
|
+
kwargs = {
|
|
49
|
+
"symbol_list": symbol_list,
|
|
50
|
+
"trades_instances": trades,
|
|
51
|
+
"strategy_cls": strategy_class,
|
|
52
|
+
"account": account,
|
|
53
|
+
**config,
|
|
54
|
+
}
|
|
55
|
+
mt5_engine(account, **kwargs)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def mt5_terminal(args):
|
|
59
|
+
if args.parallel:
|
|
60
|
+
if len(args.account) == 0:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"account or accounts are required when running in parallel"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
processes = []
|
|
66
|
+
try:
|
|
67
|
+
for account in args.account:
|
|
68
|
+
p = mp.Process(target=worker_function, args=(account, args))
|
|
69
|
+
p.start()
|
|
70
|
+
processes.append(p)
|
|
71
|
+
|
|
72
|
+
for p in processes:
|
|
73
|
+
p.join()
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"Error in parallel execution: {e}")
|
|
76
|
+
raise e
|
|
77
|
+
except KeyboardInterrupt:
|
|
78
|
+
print("\nTerminating Execution...")
|
|
79
|
+
for p in processes:
|
|
80
|
+
p.terminate()
|
|
81
|
+
for p in processes:
|
|
82
|
+
p.join()
|
|
83
|
+
print("Execution terminated")
|
|
84
|
+
else:
|
|
85
|
+
worker_function(None, args)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def tws_terminal(args):
|
|
89
|
+
raise NotImplementedError("TWS terminal is not implemented yet")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def execute_strategy(unknown):
|
|
93
|
+
HELP_MSG = """
|
|
94
|
+
Execute a strategy on one or multiple MT5 accounts.
|
|
95
|
+
|
|
96
|
+
Usage:
|
|
97
|
+
python -m bbstrader --run execution [options]
|
|
98
|
+
|
|
99
|
+
Options:
|
|
100
|
+
-s, --strategy: Strategy class name to run
|
|
101
|
+
-a, --account: Account(s) name(s) or ID(s) to run the strategy on (must be the same as in the configuration file)
|
|
102
|
+
-p, --path: Path to the execution file (default: ~/.bbstrader/execution/execution.py)
|
|
103
|
+
-c, --config: Path to the configuration file (default: ~/.bbstrader/execution/execution.json)
|
|
104
|
+
-l, --parallel: Run the strategy in parallel (default: False)
|
|
105
|
+
-t, --terminal: Terminal to use (default: MT5)
|
|
106
|
+
-h, --help: Show this help message and exit
|
|
107
|
+
|
|
108
|
+
Note:
|
|
109
|
+
The configuration file must contain all the required parameters
|
|
110
|
+
to create trade instances for each account and strategy.
|
|
111
|
+
The configuration file must be a dictionary with the following structure:
|
|
112
|
+
If parallel is True:
|
|
113
|
+
{
|
|
114
|
+
"strategy_name": {
|
|
115
|
+
"account_name": {
|
|
116
|
+
"symbol_list": ["symbol1", "symbol2"],
|
|
117
|
+
"trades_kwargs": {"param1": "value1", "param2": "value2"}
|
|
118
|
+
**other_parameters (for the strategy and the execution engine)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
If parallel is False:
|
|
123
|
+
{
|
|
124
|
+
"strategy_name": {
|
|
125
|
+
"symbol_list": ["symbol1", "symbol2"],
|
|
126
|
+
"trades_kwargs": {"param1": "value1", "param2": "value2"}
|
|
127
|
+
**other_parameters (for the strategy and the execution engine)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
See bbstrader.metatrader.trade.create_trade_instance for more details on the trades_kwargs.
|
|
131
|
+
See bbstrader.trading.execution.MT5ExecutionEngine for more details on the other parameters.
|
|
132
|
+
|
|
133
|
+
All other paramaters must be python built-in types.
|
|
134
|
+
If you have custom type you must set them in your strategy class
|
|
135
|
+
or run the MT5ExecutionEngine directly, don't run on CLI.
|
|
136
|
+
"""
|
|
137
|
+
if "-h" in unknown or "--help" in unknown:
|
|
138
|
+
print(HELP_MSG)
|
|
139
|
+
sys.exit(0)
|
|
140
|
+
|
|
141
|
+
parser = argparse.ArgumentParser()
|
|
142
|
+
parser.add_argument("-s", "--strategy", type=str, required=True)
|
|
143
|
+
parser.add_argument("-a", "--account", type=str, nargs="*", default=[])
|
|
144
|
+
parser.add_argument("-p", "--path", type=str, default=EXECUTION_PATH)
|
|
145
|
+
parser.add_argument("-c", "--config", type=str, default=CONFIG_PATH)
|
|
146
|
+
parser.add_argument("-l", "--parallel", type=bool, default=False)
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"-t", "--terminal", type=str, default="MT5", choices=["MT5", "TWS"]
|
|
149
|
+
)
|
|
150
|
+
args = parser.parse_args(unknown)
|
|
151
|
+
|
|
152
|
+
if args.terminal == "MT5":
|
|
153
|
+
mt5_terminal(args)
|
|
154
|
+
elif args.terminal == "TWS":
|
|
155
|
+
tws_terminal(args)
|
bbstrader/trading/scripts.py
CHANGED
|
@@ -4,6 +4,7 @@ from notifypy import Notify
|
|
|
4
4
|
from telegram import Bot
|
|
5
5
|
from telegram.error import TelegramError
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
__all__ = ["send_telegram_message", "send_notification", "send_message"]
|
|
8
9
|
|
|
9
10
|
|
|
@@ -65,3 +66,4 @@ def send_message(
|
|
|
65
66
|
if token is None or chat_id is None:
|
|
66
67
|
raise ValueError("Token and chat_id must be provided")
|
|
67
68
|
asyncio.run(send_telegram_message(token, chat_id, text=message))
|
|
69
|
+
|
bbstrader/tseries.py
CHANGED
|
@@ -598,7 +598,9 @@ def plot_residuals(df: pd.DataFrame):
|
|
|
598
598
|
|
|
599
599
|
|
|
600
600
|
def run_cadf_test(
|
|
601
|
-
pair: Union[List[str], Tuple[str, ...]],
|
|
601
|
+
pair: Union[List[str], Tuple[str, ...]],
|
|
602
|
+
start: str,
|
|
603
|
+
end: str,
|
|
602
604
|
) -> None:
|
|
603
605
|
"""
|
|
604
606
|
Performs the Cointegration Augmented Dickey-Fuller (CADF) test on a pair of stock tickers
|
|
@@ -666,8 +668,22 @@ def run_cadf_test(
|
|
|
666
668
|
"""
|
|
667
669
|
# Download historical data for required stocks
|
|
668
670
|
p0, p1 = pair[0], pair[1]
|
|
669
|
-
_p0 = yf.download(
|
|
670
|
-
|
|
671
|
+
_p0 = yf.download(
|
|
672
|
+
p0,
|
|
673
|
+
start=start,
|
|
674
|
+
end=end,
|
|
675
|
+
progress=False,
|
|
676
|
+
multi_level_index=False,
|
|
677
|
+
auto_adjust=True,
|
|
678
|
+
)
|
|
679
|
+
_p1 = yf.download(
|
|
680
|
+
p1,
|
|
681
|
+
start=start,
|
|
682
|
+
end=end,
|
|
683
|
+
progress=False,
|
|
684
|
+
multi_level_index=False,
|
|
685
|
+
auto_adjust=True,
|
|
686
|
+
)
|
|
671
687
|
df = pd.DataFrame(index=_p0.index)
|
|
672
688
|
df[p0] = _p0["Adj Close"]
|
|
673
689
|
df[p1] = _p1["Adj Close"]
|
|
@@ -751,7 +767,12 @@ def run_hurst_test(symbol: str, start: str, end: str):
|
|
|
751
767
|
>>> run_hurst_test('AAPL', '2023-01-01', '2023-12-31')
|
|
752
768
|
"""
|
|
753
769
|
data = yf.download(
|
|
754
|
-
symbol,
|
|
770
|
+
symbol,
|
|
771
|
+
start=start,
|
|
772
|
+
end=end,
|
|
773
|
+
progress=False,
|
|
774
|
+
multi_level_index=False,
|
|
775
|
+
auto_adjust=True,
|
|
755
776
|
)
|
|
756
777
|
|
|
757
778
|
# Create a Geometric Brownian Motion, Mean-Reverting, and Trending Series
|
|
@@ -774,6 +795,7 @@ def test_cointegration(ticker1, ticker2, start, end):
|
|
|
774
795
|
end=end,
|
|
775
796
|
progress=False,
|
|
776
797
|
multi_level_index=False,
|
|
798
|
+
auto_adjust=True,
|
|
777
799
|
)["Adj Close"].dropna()
|
|
778
800
|
|
|
779
801
|
# Perform Johansen cointegration test
|
|
@@ -917,8 +939,12 @@ def run_kalman_filter(
|
|
|
917
939
|
|
|
918
940
|
>>> run_kalman_filter(['SPY', 'QQQ'], '2023-01-01', '2023-12-31')
|
|
919
941
|
"""
|
|
920
|
-
etf_df1 = yf.download(
|
|
921
|
-
|
|
942
|
+
etf_df1 = yf.download(
|
|
943
|
+
etfs[0], start, end, progress=False, multi_level_index=False, auto_adjust=True
|
|
944
|
+
)
|
|
945
|
+
etf_df2 = yf.download(
|
|
946
|
+
etfs[1], start, end, progress=False, multi_level_index=False, auto_adjust=True
|
|
947
|
+
)
|
|
922
948
|
|
|
923
949
|
prices = pd.DataFrame(index=etf_df1.index)
|
|
924
950
|
prices[etfs[0]] = etf_df1["Adj Close"]
|
|
@@ -1674,7 +1700,7 @@ def analyze_cointegrated_pairs(
|
|
|
1674
1700
|
y = spreads.coint
|
|
1675
1701
|
X = spreads[["drift", "vol", "corr", "corr_ret"]]
|
|
1676
1702
|
decision_tree.fit(X, y)
|
|
1677
|
-
res = f
|
|
1703
|
+
res = f"{decision_tree.best_score_:.2%}, Depth: {decision_tree.best_params_['max_depth']}"
|
|
1678
1704
|
print(res)
|
|
1679
1705
|
|
|
1680
1706
|
if crosstab:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: bbstrader
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.92
|
|
4
4
|
Summary: Simplified Investment & Trading Toolkit
|
|
5
5
|
Home-page: https://github.com/bbalouki/bbstrader
|
|
6
6
|
Download-URL: https://pypi.org/project/bbstrader/
|
|
@@ -55,7 +55,10 @@ Requires-Dist: financetoolkit
|
|
|
55
55
|
Requires-Dist: tables
|
|
56
56
|
Requires-Dist: lightgbm
|
|
57
57
|
Requires-Dist: alphalens-reloaded
|
|
58
|
-
Requires-Dist:
|
|
58
|
+
Requires-Dist: pyfiglet
|
|
59
|
+
Requires-Dist: colorama
|
|
60
|
+
Provides-Extra: mt5
|
|
61
|
+
Requires-Dist: MetaTrader5; extra == "mt5"
|
|
59
62
|
Dynamic: author
|
|
60
63
|
Dynamic: author-email
|
|
61
64
|
Dynamic: classifier
|
|
@@ -67,6 +70,7 @@ Dynamic: keywords
|
|
|
67
70
|
Dynamic: license
|
|
68
71
|
Dynamic: maintainer
|
|
69
72
|
Dynamic: project-url
|
|
73
|
+
Dynamic: provides-extra
|
|
70
74
|
Dynamic: requires-dist
|
|
71
75
|
Dynamic: summary
|
|
72
76
|
|
|
@@ -102,6 +106,7 @@ It leverages statistical models and algorithms to perform tasks such as cointegr
|
|
|
102
106
|
- **Comprehensive Backtesting**: Assess the performance of trading strategies with historical market data to optimize parameters and strategies for live trading environments.
|
|
103
107
|
- **Integrated Risk Management**: Leverage advanced risk management techniques to adapt to changing market conditions and maintain control over risk exposure.
|
|
104
108
|
- **Automated Trading**: Execute trades automatically on the MT5 platform, with support for managing orders, positions, and risk in real-time.
|
|
109
|
+
- **Trade Copier**: Copy trades from one account to another or multiple accounts.
|
|
105
110
|
- **Flexible Framework**: Customize existing strategies or develop new ones with the flexible, modular architecture designed to accommodate traders' evolving needs.
|
|
106
111
|
- **Advanced Time Series Analysis**: Conduct in-depth analysis of financial time series data to identify patterns, trends, and relationships that can inform trading strategies.
|
|
107
112
|
You can read the full documentation [here](https://bbstrader.readthedocs.io/en/latest/index.html)
|
|
@@ -116,7 +121,8 @@ This Module currenlty support three brokers, [Admirals Group AS](https://cabinet
|
|
|
116
121
|
|
|
117
122
|
Then, you can install `bbstrader` using pip:
|
|
118
123
|
```bash
|
|
119
|
-
pip install bbstrader
|
|
124
|
+
pip install bbstrader # Mac and Linux
|
|
125
|
+
pip install bbstrader[MT5] # Windows
|
|
120
126
|
```
|
|
121
127
|
|
|
122
128
|
## Examples
|