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/models/ml.py
CHANGED
|
@@ -243,6 +243,7 @@ class LightGBModel(object):
|
|
|
243
243
|
end=end,
|
|
244
244
|
progress=False,
|
|
245
245
|
multi_level_index=False,
|
|
246
|
+
auto_adjust=True,
|
|
246
247
|
)
|
|
247
248
|
prices["symbol"] = ticker
|
|
248
249
|
data.append(prices)
|
|
@@ -633,14 +634,13 @@ class LightGBModel(object):
|
|
|
633
634
|
|
|
634
635
|
# set up cross-validation
|
|
635
636
|
n_splits = int(2 * YEAR / test_length)
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
)
|
|
637
|
+
print(
|
|
638
|
+
f"Lookahead: {lookahead:2.0f} | "
|
|
639
|
+
f"Train: {train_length:3.0f} | "
|
|
640
|
+
f"Test: {test_length:2.0f} | "
|
|
641
|
+
f"Params: {len(cv_params_):3.0f} | "
|
|
642
|
+
f"Train configs: {len(test_params)}"
|
|
643
|
+
)
|
|
644
644
|
|
|
645
645
|
# time-series cross-validation
|
|
646
646
|
cv = MultipleTimeSeriesCV(
|
|
@@ -1234,7 +1234,10 @@ class LightGBModel(object):
|
|
|
1234
1234
|
.to_frame("prediction")
|
|
1235
1235
|
)
|
|
1236
1236
|
tickers = predictions.index.get_level_values("symbol").unique().tolist()
|
|
1237
|
-
|
|
1237
|
+
try:
|
|
1238
|
+
return (predictions.unstack("symbol").prediction.tz_convert("UTC")), tickers
|
|
1239
|
+
except TypeError:
|
|
1240
|
+
return (predictions.unstack("symbol").prediction.tz_localize("UTC")), tickers
|
|
1238
1241
|
|
|
1239
1242
|
def assert_last_date(self, predictions: pd.DataFrame):
|
|
1240
1243
|
"""
|
|
@@ -1242,9 +1245,14 @@ class LightGBModel(object):
|
|
|
1242
1245
|
is the previous day, so it predicts today's returns.
|
|
1243
1246
|
"""
|
|
1244
1247
|
last_date = predictions.index.get_level_values("date").max()
|
|
1245
|
-
|
|
1246
|
-
last_date
|
|
1247
|
-
|
|
1248
|
+
try:
|
|
1249
|
+
if last_date.tzinfo is None:
|
|
1250
|
+
last_date = last_date.tz_localize("UTC")
|
|
1251
|
+
else:
|
|
1252
|
+
last_date = last_date.tz_convert("UTC")
|
|
1253
|
+
last_date = last_date.normalize()
|
|
1254
|
+
except Exception as e:
|
|
1255
|
+
print(f"Error getting last date: {e}")
|
|
1248
1256
|
try:
|
|
1249
1257
|
days = 3 if datetime.now().strftime("%A") == "Monday" else 1
|
|
1250
1258
|
td = (
|
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,25 +204,29 @@ 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
|
|
195
222
|
sigmsg = (
|
|
196
223
|
f"SIGNAL = {signal}, \nSYMBOL={symbol}, \nTYPE={symbol_type}, \nDESCRIPTION={desc}, "
|
|
197
|
-
f"\nPRICE={price}, \nSTOPLIMIT={stoplimit}, \nSTRATEGY={STRATEGY}, \nTIMEFRAME={time_frame}"
|
|
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)
|
|
@@ -231,6 +258,7 @@ def _mt5_execution(
|
|
|
231
258
|
stoplimit=stoplimit,
|
|
232
259
|
id=id,
|
|
233
260
|
mm=mm,
|
|
261
|
+
trail=trail,
|
|
234
262
|
comment=comment,
|
|
235
263
|
)
|
|
236
264
|
else:
|
|
@@ -247,6 +275,7 @@ def _mt5_execution(
|
|
|
247
275
|
stoplimit=stoplimit,
|
|
248
276
|
id=id,
|
|
249
277
|
mm=mm,
|
|
278
|
+
trail=trail,
|
|
250
279
|
comment=comment,
|
|
251
280
|
)
|
|
252
281
|
|
|
@@ -266,6 +295,7 @@ def _mt5_execution(
|
|
|
266
295
|
stoplimit=stoplimit,
|
|
267
296
|
id=id,
|
|
268
297
|
mm=mm,
|
|
298
|
+
trail=trail,
|
|
269
299
|
comment=comment,
|
|
270
300
|
)
|
|
271
301
|
else:
|
|
@@ -282,6 +312,7 @@ def _mt5_execution(
|
|
|
282
312
|
stoplimit=stoplimit,
|
|
283
313
|
id=id,
|
|
284
314
|
mm=mm,
|
|
315
|
+
trail=trail,
|
|
285
316
|
comment=comment,
|
|
286
317
|
)
|
|
287
318
|
|
|
@@ -324,7 +355,7 @@ def _mt5_execution(
|
|
|
324
355
|
if show_positions_orders:
|
|
325
356
|
logger.info(
|
|
326
357
|
f"Current {type.upper()} SYMBOL={symbol}: \
|
|
327
|
-
{positions_orders[type][symbol]}, STRATEGY={STRATEGY}"
|
|
358
|
+
{positions_orders[type][symbol]}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
328
359
|
)
|
|
329
360
|
long_market = {
|
|
330
361
|
symbol: buys[symbol] is not None
|
|
@@ -338,7 +369,8 @@ def _mt5_execution(
|
|
|
338
369
|
}
|
|
339
370
|
except Exception:
|
|
340
371
|
_print_exc(
|
|
341
|
-
debug_mode,
|
|
372
|
+
debug_mode,
|
|
373
|
+
f"Checking positions and orders, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}",
|
|
342
374
|
)
|
|
343
375
|
continue
|
|
344
376
|
time.sleep(0.5)
|
|
@@ -352,7 +384,10 @@ def _mt5_execution(
|
|
|
352
384
|
)
|
|
353
385
|
update_risk(weights)
|
|
354
386
|
except Exception:
|
|
355
|
-
_print_exc(
|
|
387
|
+
_print_exc(
|
|
388
|
+
debug_mode,
|
|
389
|
+
f"Calculating Signals, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}",
|
|
390
|
+
)
|
|
356
391
|
continue
|
|
357
392
|
if len(signals) == 0:
|
|
358
393
|
for symbol in symbols:
|
|
@@ -382,16 +417,16 @@ def _mt5_execution(
|
|
|
382
417
|
if len(symbols) >= 10:
|
|
383
418
|
if symbol == symbols[-1]:
|
|
384
419
|
logger.info(
|
|
385
|
-
f"Not trading Time !!!, STRATEGY={STRATEGY}"
|
|
420
|
+
f"Not trading Time !!!, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
386
421
|
)
|
|
387
422
|
else:
|
|
388
423
|
logger.info(
|
|
389
|
-
f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY}"
|
|
424
|
+
f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
390
425
|
)
|
|
391
426
|
check(buys[symbol], sells[symbol], symbol)
|
|
392
427
|
|
|
393
428
|
except Exception:
|
|
394
|
-
msg = f"Handling Signals, SYMBOL={symbol}, STRATEGY={STRATEGY}"
|
|
429
|
+
msg = f"Handling Signals, SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
395
430
|
_print_exc(debug_mode, msg)
|
|
396
431
|
continue
|
|
397
432
|
|
|
@@ -420,20 +455,40 @@ def _mt5_execution(
|
|
|
420
455
|
|
|
421
456
|
def logmsg(period, symbol):
|
|
422
457
|
logger.info(
|
|
423
|
-
f"End of the {period} !!! SYMBOL={symbol}, STRATEGY={STRATEGY}"
|
|
458
|
+
f"End of the {period} !!! SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
424
459
|
)
|
|
425
460
|
|
|
426
461
|
def logmsgif(period, symbol):
|
|
427
462
|
if len(symbols) <= 10:
|
|
428
463
|
logmsg(period, symbol)
|
|
429
464
|
elif len(symbols) > 10 and symbol == symbols[-1]:
|
|
430
|
-
logger.info(
|
|
465
|
+
logger.info(
|
|
466
|
+
f"End of the {period} !!! STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
467
|
+
)
|
|
431
468
|
|
|
432
469
|
def sleepmsg(sleep_time):
|
|
433
|
-
logger.info(f"Sleeping for {sleep_time} minutes ...\n")
|
|
470
|
+
logger.info(f"{ACCOUNT} Sleeping for {sleep_time} minutes ...\n")
|
|
434
471
|
|
|
435
|
-
sessionmsg = "STARTING NEW TRADING SESSION ...\n"
|
|
436
|
-
if period.lower() == "
|
|
472
|
+
sessionmsg = f"{ACCOUNT} STARTING NEW TRADING SESSION ...\n"
|
|
473
|
+
if period.lower() == "24/7": # For Cryptos
|
|
474
|
+
for symbol in symbols:
|
|
475
|
+
trade = trades_instances[symbol]
|
|
476
|
+
if trade.days_end() and closing:
|
|
477
|
+
for id in expert_ids:
|
|
478
|
+
trade.close_positions(
|
|
479
|
+
position_type="all", id=id, comment=comment
|
|
480
|
+
)
|
|
481
|
+
logmsgif("Day", symbol)
|
|
482
|
+
trade.statistics(save=True)
|
|
483
|
+
if day_end:
|
|
484
|
+
strategy.perform_period_end_checks()
|
|
485
|
+
sleep_time = trades_instances[symbols[-1]].sleep_time()
|
|
486
|
+
sleepmsg(sleep_time + delay)
|
|
487
|
+
time.sleep(60 * sleep_time + delay)
|
|
488
|
+
logger.info(sessionmsg)
|
|
489
|
+
time_intervals = 0
|
|
490
|
+
|
|
491
|
+
elif period.lower() == "day":
|
|
437
492
|
for symbol in symbols:
|
|
438
493
|
trade = trades_instances[symbol]
|
|
439
494
|
if trade.days_end() and closing:
|
|
@@ -455,6 +510,7 @@ def _mt5_execution(
|
|
|
455
510
|
sleepmsg(sleep_time + delay)
|
|
456
511
|
time.sleep(60 * sleep_time + delay)
|
|
457
512
|
logger.info(sessionmsg)
|
|
513
|
+
time_intervals = 0
|
|
458
514
|
elif period_end_action == "sleep" and today == FRIDAY:
|
|
459
515
|
sleep_time = trades_instances[symbols[-1]].sleep_time(
|
|
460
516
|
weekend=True
|
|
@@ -462,6 +518,7 @@ def _mt5_execution(
|
|
|
462
518
|
sleepmsg(sleep_time + delay)
|
|
463
519
|
time.sleep(60 * sleep_time + delay)
|
|
464
520
|
logger.info(sessionmsg)
|
|
521
|
+
time_intervals = 0
|
|
465
522
|
|
|
466
523
|
elif period.lower() == "week":
|
|
467
524
|
for symbol in symbols:
|
|
@@ -482,6 +539,7 @@ def _mt5_execution(
|
|
|
482
539
|
sleepmsg(sleep_time + delay)
|
|
483
540
|
time.sleep(60 * sleep_time + delay)
|
|
484
541
|
logger.info(sessionmsg)
|
|
542
|
+
time_intervals = 0
|
|
485
543
|
elif day_end and today == FRIDAY:
|
|
486
544
|
strategy.perform_period_end_checks()
|
|
487
545
|
if period_end_action == "break" and closing:
|
|
@@ -493,6 +551,7 @@ def _mt5_execution(
|
|
|
493
551
|
sleepmsg(sleep_time + delay)
|
|
494
552
|
time.sleep(60 * sleep_time + delay)
|
|
495
553
|
logger.info(sessionmsg)
|
|
554
|
+
time_intervals = 0
|
|
496
555
|
|
|
497
556
|
elif period.lower() == "month":
|
|
498
557
|
for symbol in symbols:
|
|
@@ -502,9 +561,7 @@ def _mt5_execution(
|
|
|
502
561
|
elif trade.days_end() and today == FRIDAY:
|
|
503
562
|
logmsgif("Week", symbol)
|
|
504
563
|
elif (
|
|
505
|
-
trade.days_end()
|
|
506
|
-
and today == FRIDAY
|
|
507
|
-
and num_days >= 20
|
|
564
|
+
trade.days_end() and today == FRIDAY and num_days >= 20
|
|
508
565
|
) and closing:
|
|
509
566
|
for id in expert_ids:
|
|
510
567
|
trade.close_positions(
|
|
@@ -517,18 +574,22 @@ def _mt5_execution(
|
|
|
517
574
|
sleepmsg(sleep_time + delay)
|
|
518
575
|
time.sleep(60 * sleep_time + delay)
|
|
519
576
|
logger.info(sessionmsg)
|
|
577
|
+
time_intervals = 0
|
|
520
578
|
num_days += 1
|
|
521
579
|
elif day_end and today == FRIDAY:
|
|
522
580
|
sleep_time = trades_instances[symbols[-1]].sleep_time(weekend=True)
|
|
523
581
|
sleepmsg(sleep_time + delay)
|
|
524
582
|
time.sleep(60 * sleep_time + delay)
|
|
525
583
|
logger.info(sessionmsg)
|
|
584
|
+
time_intervals = 0
|
|
526
585
|
num_days += 1
|
|
527
586
|
elif day_end and today == FRIDAY and num_days >= 20:
|
|
528
587
|
strategy.perform_period_end_checks()
|
|
529
588
|
break
|
|
530
589
|
except Exception:
|
|
531
|
-
msg =
|
|
590
|
+
msg = (
|
|
591
|
+
f"Handling period end actions, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
|
|
592
|
+
)
|
|
532
593
|
_print_exc(debug_mode, msg)
|
|
533
594
|
continue
|
|
534
595
|
|
|
@@ -638,7 +699,7 @@ class MT5ExecutionEngine:
|
|
|
638
699
|
symbol_list : List of symbols to trade
|
|
639
700
|
trades_instances : Dictionary of Trade instances
|
|
640
701
|
strategy_cls : Strategy class to use for trading
|
|
641
|
-
mm : Enable Money Management. Defaults to
|
|
702
|
+
mm : Enable Money Management. Defaults to True.
|
|
642
703
|
optimizer : Risk management optimizer. Defaults to 'equal'.
|
|
643
704
|
See `bbstrader.models.optimization` module for more information.
|
|
644
705
|
show_positions_orders : Print open positions and orders. Defaults to False.
|
|
@@ -726,4 +787,60 @@ class MT5ExecutionEngine:
|
|
|
726
787
|
)
|
|
727
788
|
|
|
728
789
|
|
|
790
|
+
def mt5_engine(account_id: str, **kwargs):
|
|
791
|
+
"""Starts an MT5 execution engine for a given account.
|
|
792
|
+
Args:
|
|
793
|
+
account_id: Account ID to run the execution engine on.
|
|
794
|
+
**kwargs: Additional keyword arguments
|
|
795
|
+
_ symbol_list : List of symbols to trade.
|
|
796
|
+
- trades_instances : Dictionary of Trade instances.
|
|
797
|
+
- strategy_cls : Strategy class to use for trading.
|
|
798
|
+
"""
|
|
799
|
+
log.info(f"Starting execution engine for {account_id}")
|
|
800
|
+
|
|
801
|
+
symbol_list = kwargs.pop("symbol_list")
|
|
802
|
+
trades_instances = kwargs.pop("trades_instances")
|
|
803
|
+
strategy_cls = kwargs.pop("strategy_cls")
|
|
804
|
+
|
|
805
|
+
if symbol_list is None or trades_instances is None or strategy_cls is None:
|
|
806
|
+
log.error(f"Missing required arguments for account {account_id}")
|
|
807
|
+
raise ValueError(f"Missing required arguments for account {account_id}")
|
|
808
|
+
|
|
809
|
+
try:
|
|
810
|
+
engine = MT5ExecutionEngine(
|
|
811
|
+
symbol_list, trades_instances, strategy_cls, **kwargs
|
|
812
|
+
)
|
|
813
|
+
engine.run()
|
|
814
|
+
except Exception as e:
|
|
815
|
+
log.exception(f"Error running execution engine for {account_id}: {e}")
|
|
816
|
+
finally:
|
|
817
|
+
log.info(f"Execution engine for {account_id} completed")
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def RunMt5Engines(accounts: Dict[str, Dict], start_delay: float = 1.0):
|
|
821
|
+
"""Runs multiple MT5 execution engines in parallel using multiprocessing.
|
|
822
|
+
|
|
823
|
+
Args:
|
|
824
|
+
accounts: Dictionary of accounts to run the execution engines on.
|
|
825
|
+
Keys are the account names or IDs and values are the parameters for the execution engine.
|
|
826
|
+
The parameters are the same as the ones passed to the `MT5ExecutionEngine` class.
|
|
827
|
+
start_delay: Delay in seconds between starting the processes. Defaults to 1.0.
|
|
828
|
+
"""
|
|
829
|
+
|
|
830
|
+
processes = {}
|
|
831
|
+
|
|
832
|
+
for account_id, params in accounts.items():
|
|
833
|
+
log.info(f"Starting process for {account_id}")
|
|
834
|
+
process = mp.Process(target=mt5_engine, args=(account_id,), kwargs=params)
|
|
835
|
+
process.start()
|
|
836
|
+
processes[process] = account_id
|
|
837
|
+
|
|
838
|
+
if start_delay:
|
|
839
|
+
time.sleep(start_delay)
|
|
840
|
+
|
|
841
|
+
for process, account_id in processes.items():
|
|
842
|
+
process.join()
|
|
843
|
+
log.info(f"Process for {account_id} joined")
|
|
844
|
+
|
|
845
|
+
|
|
729
846
|
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", action="store_true")
|
|
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
|
+
|