bbstrader 0.2.92__py3-none-any.whl → 0.2.94__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.

Files changed (36) hide show
  1. bbstrader/__ini__.py +20 -20
  2. bbstrader/__main__.py +50 -50
  3. bbstrader/btengine/__init__.py +54 -54
  4. bbstrader/btengine/data.py +11 -9
  5. bbstrader/btengine/scripts.py +157 -157
  6. bbstrader/compat.py +19 -19
  7. bbstrader/config.py +137 -137
  8. bbstrader/core/data.py +22 -22
  9. bbstrader/core/utils.py +146 -146
  10. bbstrader/metatrader/__init__.py +6 -6
  11. bbstrader/metatrader/account.py +1516 -1516
  12. bbstrader/metatrader/copier.py +750 -735
  13. bbstrader/metatrader/rates.py +584 -584
  14. bbstrader/metatrader/risk.py +749 -748
  15. bbstrader/metatrader/scripts.py +81 -81
  16. bbstrader/metatrader/trade.py +1836 -1826
  17. bbstrader/metatrader/utils.py +645 -645
  18. bbstrader/models/__init__.py +10 -10
  19. bbstrader/models/factors.py +312 -312
  20. bbstrader/models/ml.py +1272 -1265
  21. bbstrader/models/optimization.py +182 -182
  22. bbstrader/models/portfolio.py +223 -223
  23. bbstrader/models/risk.py +398 -398
  24. bbstrader/trading/__init__.py +11 -11
  25. bbstrader/trading/execution.py +846 -842
  26. bbstrader/trading/script.py +155 -155
  27. bbstrader/trading/scripts.py +69 -69
  28. bbstrader/trading/strategies.py +860 -860
  29. bbstrader/tseries.py +1842 -1842
  30. {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/LICENSE +21 -21
  31. {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/METADATA +188 -187
  32. bbstrader-0.2.94.dist-info/RECORD +44 -0
  33. {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/WHEEL +1 -1
  34. bbstrader-0.2.92.dist-info/RECORD +0 -44
  35. {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/entry_points.txt +0 -0
  36. {bbstrader-0.2.92.dist-info → bbstrader-0.2.94.dist-info}/top_level.txt +0 -0
@@ -1,842 +1,846 @@
1
- import multiprocessing as mp
2
- import time
3
- import traceback
4
- from datetime import datetime
5
- from typing import Dict, List, Literal, Optional
6
-
7
- from loguru import logger as log
8
-
9
- from bbstrader.btengine.strategy import MT5Strategy, Strategy
10
- from bbstrader.config import BBSTRADER_DIR
11
- from bbstrader.core.utils import TradeAction
12
- from bbstrader.metatrader.account import Account, check_mt5_connection
13
- from bbstrader.metatrader.trade import Trade
14
- from bbstrader.trading.scripts import send_message
15
-
16
- try:
17
- import MetaTrader5 as MT5
18
- except ImportError:
19
- import bbstrader.compat # noqa: F401
20
-
21
-
22
- __all__ = ["MT5ExecutionEngine", "TWSExecutionEngine"]
23
-
24
- _TF_MAPPING = {
25
- "1m": 1,
26
- "3m": 3,
27
- "5m": 5,
28
- "10m": 10,
29
- "15m": 15,
30
- "30m": 30,
31
- "1h": 60,
32
- "2h": 120,
33
- "4h": 240,
34
- "6h": 360,
35
- "8h": 480,
36
- "12h": 720,
37
- "D1": 1440,
38
- }
39
-
40
- TradingDays = ["monday", "tuesday", "wednesday", "thursday", "friday"]
41
- WEEK_DAYS = TradingDays + ["saturday", "sunday"]
42
-
43
- BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
44
- SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
45
-
46
- ORDERS_TYPES = [
47
- "orders",
48
- "buy_stops",
49
- "sell_stops",
50
- "buy_limits",
51
- "sell_limits",
52
- "buy_stop_limits",
53
- "sell_stop_limits",
54
- ]
55
- POSITIONS_TYPES = ["positions", "buys", "sells", "profitables", "losings"]
56
-
57
- ACTIONS = ["buys", "sells"]
58
- STOPS = ["buy_stops", "sell_stops"]
59
- LIMITS = ["buy_limits", "sell_limits"]
60
- STOP_LIMITS = ["buy_stop_limits", "sell_stop_limits"]
61
-
62
- EXIT_SIGNAL_ACTIONS = {
63
- "EXIT": {a: a[:-1] for a in ACTIONS},
64
- "EXIT_LONG": {"buys": "buy"},
65
- "EXIT_SHORT": {"sells": "sell"},
66
- "EXIT_STOP": {stop: stop for stop in STOPS},
67
- "EXIT_LONG_STOP": {"buy_stops": "buy_stops"},
68
- "EXIT_SHORT_STOP": {"sell_stops": "sell_stops"},
69
- "EXIT_LIMIT": {limit: limit for limit in LIMITS},
70
- "EXIT_LONG_LIMIT": {"buy_limits": "buy_limits"},
71
- "EXIT_SHORT_LIMIT": {"sell_limits": "sell_limits"},
72
- "EXIT_STOP_LIMIT": {sl: sl for sl in STOP_LIMITS},
73
- "EXIT_LONG_STOP_LIMIT": {STOP_LIMITS[0]: STOP_LIMITS[0]},
74
- "EXIT_SHORT_STOP_LIMIT": {STOP_LIMITS[1]: STOP_LIMITS[1]},
75
- "EXIT_PROFITABLES": {"profitables": "profitable"},
76
- "EXIT_LOSINGS": {"losings": "losing"},
77
- "EXIT_ALL_POSITIONS": {"positions": "all"},
78
- "EXIT_ALL_ORDERS": {"orders": "all"},
79
- }
80
-
81
- COMMON_RETCODES = [MT5.TRADE_RETCODE_MARKET_CLOSED, MT5.TRADE_RETCODE_CLOSE_ONLY]
82
-
83
- NON_EXEC_RETCODES = {
84
- "BMKT": [MT5.TRADE_RETCODE_SHORT_ONLY] + COMMON_RETCODES,
85
- "SMKT": [MT5.TRADE_RETCODE_LONG_ONLY] + COMMON_RETCODES,
86
- }
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
-
95
-
96
- def _mt5_execution(
97
- symbol_list,
98
- trades_instances,
99
- strategy_cls,
100
- /,
101
- mm,
102
- optimizer,
103
- trail,
104
- stop_trail,
105
- trail_after_points,
106
- be_plus_points,
107
- show_positions_orders,
108
- iter_time,
109
- use_trade_time,
110
- period,
111
- period_end_action,
112
- closing_pnl,
113
- trading_days,
114
- comment,
115
- **kwargs,
116
- ):
117
- logger = kwargs.get("logger", log)
118
-
119
- def _print_exc(dm, msg):
120
- traceback.print_exc() if dm else logger.error(msg)
121
-
122
- try:
123
- symbols = symbol_list.copy()
124
- time_frame = kwargs.get("time_frame", "15m")
125
- daily_risk = kwargs.get("daily_risk")
126
- STRATEGY = kwargs.get("strategy_name")
127
- mtrades = kwargs.get("max_trades")
128
- notify = kwargs.get("notify", False)
129
- signal_tickers = kwargs.get("signal_tickers", symbols)
130
- debug_mode = kwargs.get("debug_mode", False)
131
- delay = kwargs.get("delay", 0)
132
- ACCOUNT = kwargs.get("account", "MT5 Account")
133
- if notify:
134
- telegram = kwargs.get("telegram", False)
135
- bot_token = kwargs.get("bot_token")
136
- chat_id = kwargs.get("chat_id")
137
-
138
- expert_ids = kwargs.get("expert_ids")
139
- if expert_ids is None:
140
- expert_ids = list(
141
- set([trade.expert_id for trade in trades_instances.values()])
142
- )
143
- elif isinstance(expert_ids, int):
144
- expert_ids = [expert_ids]
145
-
146
- max_trades = {
147
- symbol: mtrades[symbol]
148
- if mtrades is not None and symbol in mtrades
149
- else trades_instances[symbol].max_trade()
150
- for symbol in symbols
151
- }
152
- if comment is None:
153
- trade = trades_instances[symbols[0]]
154
- comment = f"{trade.expert_name}@{trade.version}"
155
-
156
- if period.lower() == "24/7":
157
- trading_days = WEEK_DAYS
158
- except Exception:
159
- _print_exc(
160
- debug_mode,
161
- f"Initializing Execution Engine, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}",
162
- )
163
- return
164
-
165
- def update_risk(weights):
166
- if weights is not None:
167
- for symbol in symbols:
168
- if symbol not in weights:
169
- continue
170
- trade = trades_instances[symbol]
171
- dailydd = round(weights[symbol] * daily_risk, 5)
172
- trade.dailydd = dailydd
173
-
174
- def check_retcode(trade: Trade, position):
175
- if len(trade.retcodes) > 0:
176
- for retcode in trade.retcodes:
177
- if retcode in NON_EXEC_RETCODES[position]:
178
- return True
179
- return False
180
-
181
- def _send_notification(signal, symbol):
182
- if symbol in signal_tickers:
183
- send_message(
184
- message=signal,
185
- notify_me=notify,
186
- telegram=telegram,
187
- token=bot_token,
188
- chat_id=chat_id,
189
- )
190
-
191
- def check(buys, sells, symbol):
192
- if not mm:
193
- return
194
- if buys is not None or sells is not None:
195
- trades_instances[symbol].break_even(
196
- mm=mm,
197
- trail=trail,
198
- stop_trail=stop_trail,
199
- trail_after_points=trail_after_points,
200
- be_plus_points=be_plus_points,
201
- )
202
-
203
- try:
204
- check_mt5_connection(**kwargs)
205
- strategy: MT5Strategy = strategy_cls(symbol_list=symbols, mode="live", **kwargs)
206
- except Exception:
207
- _print_exc(
208
- debug_mode, f"Initializing strategy, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}"
209
- )
210
- return
211
- logger.info(
212
- f"Running {STRATEGY} Strategy in {time_frame} Interval ..., ACCOUNT={ACCOUNT}"
213
- )
214
-
215
- def run_trade_algorithm(signal, symbol, id, trade: Trade, price, stoplimit):
216
- signal = "BMKT" if signal == "LONG" or signal == "BUY" else signal
217
- signal = "SMKT" if signal == "SHORT" or signal == "SELL" else signal
218
- info = f"SIGNAL = {signal}, SYMBOL={symbol}, STRATEGY={STRATEGY}, TIMEFRAME={time_frame} , ACCOUNT={ACCOUNT}"
219
- account = Account(**kwargs)
220
- symbol_type = account.get_symbol_type(symbol)
221
- desc = account.get_symbol_info(symbol).description
222
- sigmsg = (
223
- f"SIGNAL = {signal}, \nSYMBOL={symbol}, \nTYPE={symbol_type}, \nDESCRIPTION={desc}, "
224
- f"\nPRICE={price}, \nSTOPLIMIT={stoplimit}, \nSTRATEGY={STRATEGY}, \nTIMEFRAME={time_frame}"
225
- f"\nBROKER={account.broker.name}, \nTIMESTAMP={datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
226
- )
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}"
230
- if signal not in EXIT_SIGNAL_ACTIONS:
231
- if signal in NON_EXEC_RETCODES and not check_retcode(trade, signal):
232
- logger.info(info)
233
- elif signal not in NON_EXEC_RETCODES:
234
- logger.info(info)
235
- if signal in EXIT_SIGNAL_ACTIONS:
236
- for exit_signal, actions in EXIT_SIGNAL_ACTIONS.items():
237
- for position_type, order_type in actions.items():
238
- clos_func = getattr(
239
- trades_instances[symbol], f"get_current_{position_type}"
240
- )
241
- if clos_func(id=id) is not None:
242
- if notify:
243
- _send_notification(sigmsg, symbol)
244
- if position_type in POSITIONS_TYPES:
245
- trade.close_positions(position_type=order_type, id=id)
246
- else:
247
- trade.close_orders(order_type=order_type, id=id)
248
- elif signal in BUYS and not long_market[symbol]:
249
- if use_trade_time:
250
- if time_intervals % trade_time == 0 or buys[symbol] is None:
251
- if notify:
252
- _send_notification(sigmsg, symbol)
253
- if not check_retcode(trade, "BMKT"):
254
- logger.info(msg)
255
- trade.open_buy_position(
256
- action=signal,
257
- price=price,
258
- stoplimit=stoplimit,
259
- id=id,
260
- mm=mm,
261
- comment=comment,
262
- )
263
- else:
264
- logger.info(tfmsg)
265
- check(buys[symbol], sells[symbol], symbol)
266
- else:
267
- if notify:
268
- _send_notification(sigmsg, symbol)
269
- if not check_retcode(trade, "BMKT"):
270
- logger.info(msg)
271
- trade.open_buy_position(
272
- action=signal,
273
- price=price,
274
- stoplimit=stoplimit,
275
- id=id,
276
- mm=mm,
277
- comment=comment,
278
- )
279
-
280
- elif signal in BUYS and long_market[symbol]:
281
- logger.info(riskmsg)
282
-
283
- elif signal in SELLS and not short_market[symbol]:
284
- if use_trade_time:
285
- if time_intervals % trade_time == 0 or sells[symbol] is None:
286
- if notify:
287
- _send_notification(sigmsg, symbol)
288
- if not check_retcode(trade, "SMKT"):
289
- logger.info(msg)
290
- trade.open_sell_position(
291
- action=signal,
292
- price=price,
293
- stoplimit=stoplimit,
294
- id=id,
295
- mm=mm,
296
- comment=comment,
297
- )
298
- else:
299
- logger.info(tfmsg)
300
- check(buys[symbol], sells[symbol], symbol)
301
- else:
302
- if notify:
303
- _send_notification(sigmsg, symbol)
304
- if not check_retcode(trade, "SMKT"):
305
- logger.info(msg)
306
- trade.open_sell_position(
307
- action=signal,
308
- price=price,
309
- stoplimit=stoplimit,
310
- id=id,
311
- mm=mm,
312
- comment=comment,
313
- )
314
-
315
- elif signal in SELLS and short_market[symbol]:
316
- logger.info(riskmsg)
317
- else:
318
- check(buys[symbol], sells[symbol], symbol)
319
-
320
- num_days = 0
321
- time_intervals = 0
322
- trade_time = _TF_MAPPING[time_frame]
323
-
324
- long_market = {symbol: False for symbol in symbols}
325
- short_market = {symbol: False for symbol in symbols}
326
-
327
- while True:
328
- try:
329
- check_mt5_connection(**kwargs)
330
- current_date = datetime.now()
331
- today = current_date.strftime("%A").lower()
332
- time.sleep(0.5)
333
- positions_orders = {}
334
- for type in POSITIONS_TYPES + ORDERS_TYPES:
335
- positions_orders[type] = {}
336
- for symbol in symbols:
337
- positions_orders[type][symbol] = None
338
- for id in expert_ids:
339
- func = getattr(trades_instances[symbol], f"get_current_{type}")
340
- func_value = func(id=id)
341
- if func_value is not None:
342
- if positions_orders[type][symbol] is None:
343
- positions_orders[type][symbol] = func(id=id)
344
- else:
345
- positions_orders[type][symbol] += func(id=id)
346
- buys = positions_orders["buys"]
347
- sells = positions_orders["sells"]
348
- for symbol in symbols:
349
- for type in POSITIONS_TYPES + ORDERS_TYPES:
350
- if positions_orders[type][symbol] is not None:
351
- if show_positions_orders:
352
- logger.info(
353
- f"Current {type.upper()} SYMBOL={symbol}: \
354
- {positions_orders[type][symbol]}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
355
- )
356
- long_market = {
357
- symbol: buys[symbol] is not None
358
- and len(buys[symbol]) >= max_trades[symbol]
359
- for symbol in symbols
360
- }
361
- short_market = {
362
- symbol: sells[symbol] is not None
363
- and len(sells[symbol]) >= max_trades[symbol]
364
- for symbol in symbols
365
- }
366
- except Exception:
367
- _print_exc(
368
- debug_mode,
369
- f"Checking positions and orders, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}",
370
- )
371
- continue
372
- time.sleep(0.5)
373
- try:
374
- check_mt5_connection(**kwargs)
375
- signals = strategy.calculate_signals()
376
- weights = (
377
- strategy.apply_risk_management(optimizer)
378
- if hasattr(strategy, "apply_risk_management")
379
- else None
380
- )
381
- update_risk(weights)
382
- except Exception:
383
- _print_exc(
384
- debug_mode,
385
- f"Calculating Signals, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}",
386
- )
387
- continue
388
- if len(signals) == 0:
389
- for symbol in symbols:
390
- check(buys[symbol], sells[symbol], symbol)
391
- else:
392
- try:
393
- check_mt5_connection(**kwargs)
394
- for signal in signals:
395
- symbol = signal.symbol
396
- trade: Trade = trades_instances[symbol]
397
- if trade.trading_time() and today in trading_days:
398
- if signal.action is not None:
399
- action = (
400
- signal.action.value
401
- if isinstance(signal.action, TradeAction)
402
- else signal.action
403
- )
404
- run_trade_algorithm(
405
- action,
406
- symbol,
407
- signal.id,
408
- trade,
409
- signal.price,
410
- signal.stoplimit,
411
- )
412
- else:
413
- if len(symbols) >= 10:
414
- if symbol == symbols[-1]:
415
- logger.info(
416
- f"Not trading Time !!!, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
417
- )
418
- else:
419
- logger.info(
420
- f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
421
- )
422
- check(buys[symbol], sells[symbol], symbol)
423
-
424
- except Exception:
425
- msg = f"Handling Signals, SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
426
- _print_exc(debug_mode, msg)
427
- continue
428
-
429
- time.sleep((60 * iter_time) - 1.0)
430
- if iter_time == 1:
431
- time_intervals += 1
432
- elif trade_time % iter_time == 0:
433
- time_intervals += iter_time
434
- else:
435
- if use_trade_time:
436
- raise ValueError(
437
- f"iter_time must be a multiple of the {time_frame} !!!"
438
- f"(e.g., if time_frame is 15m, iter_time must be 1.5, 3, 5, 15 etc)"
439
- )
440
- try:
441
- FRIDAY = "friday"
442
- check_mt5_connection(**kwargs)
443
- day_end = all(trade.days_end() for trade in trades_instances.values())
444
- if closing_pnl is not None:
445
- closing = all(
446
- trade.positive_profit(id=trade.expert_id, th=closing_pnl)
447
- for trade in trades_instances.values()
448
- )
449
- else:
450
- closing = True
451
-
452
- def logmsg(period, symbol):
453
- logger.info(
454
- f"End of the {period} !!! SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
455
- )
456
-
457
- def logmsgif(period, symbol):
458
- if len(symbols) <= 10:
459
- logmsg(period, symbol)
460
- elif len(symbols) > 10 and symbol == symbols[-1]:
461
- logger.info(
462
- f"End of the {period} !!! STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
463
- )
464
-
465
- def sleepmsg(sleep_time):
466
- logger.info(f"{ACCOUNT} Sleeping for {sleep_time} minutes ...\n")
467
-
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":
488
- for symbol in symbols:
489
- trade = trades_instances[symbol]
490
- if trade.days_end() and closing:
491
- for id in expert_ids:
492
- trade.close_positions(
493
- position_type="all", id=id, comment=comment
494
- )
495
- logmsgif("Day", symbol)
496
- trade.statistics(save=True)
497
-
498
- if day_end:
499
- strategy.perform_period_end_checks()
500
- if period_end_action == "break" and closing:
501
- break
502
- elif (
503
- period_end_action == "sleep" and today != FRIDAY or not closing
504
- ):
505
- sleep_time = trades_instances[symbols[-1]].sleep_time()
506
- sleepmsg(sleep_time + delay)
507
- time.sleep(60 * sleep_time + delay)
508
- logger.info(sessionmsg)
509
- time_intervals = 0
510
- elif period_end_action == "sleep" and today == FRIDAY:
511
- sleep_time = trades_instances[symbols[-1]].sleep_time(
512
- weekend=True
513
- )
514
- sleepmsg(sleep_time + delay)
515
- time.sleep(60 * sleep_time + delay)
516
- logger.info(sessionmsg)
517
- time_intervals = 0
518
-
519
- elif period.lower() == "week":
520
- for symbol in symbols:
521
- trade = trades_instances[symbol]
522
- if trade.days_end() and today != FRIDAY:
523
- logmsgif("Day", symbol)
524
-
525
- elif trade.days_end() and today == FRIDAY and closing:
526
- for id in expert_ids:
527
- trade.close_positions(
528
- position_type="all", id=id, comment=comment
529
- )
530
- logmsgif("Week", symbol)
531
- trade.statistics(save=True)
532
-
533
- if day_end and today != FRIDAY:
534
- sleep_time = trades_instances[symbols[-1]].sleep_time()
535
- sleepmsg(sleep_time + delay)
536
- time.sleep(60 * sleep_time + delay)
537
- logger.info(sessionmsg)
538
- time_intervals = 0
539
- elif day_end and today == FRIDAY:
540
- strategy.perform_period_end_checks()
541
- if period_end_action == "break" and closing:
542
- break
543
- elif period_end_action == "sleep" or not closing:
544
- sleep_time = trades_instances[symbols[-1]].sleep_time(
545
- weekend=True
546
- )
547
- sleepmsg(sleep_time + delay)
548
- time.sleep(60 * sleep_time + delay)
549
- logger.info(sessionmsg)
550
- time_intervals = 0
551
-
552
- elif period.lower() == "month":
553
- for symbol in symbols:
554
- trade = trades_instances[symbol]
555
- if trade.days_end() and today != FRIDAY:
556
- logmsgif("Day", symbol)
557
- elif trade.days_end() and today == FRIDAY:
558
- logmsgif("Week", symbol)
559
- elif (
560
- trade.days_end() and today == FRIDAY and num_days >= 20
561
- ) and closing:
562
- for id in expert_ids:
563
- trade.close_positions(
564
- position_type="all", id=id, comment=comment
565
- )
566
- logmsgif("Month", symbol)
567
- trade.statistics(save=True)
568
- if day_end and today != FRIDAY:
569
- sleep_time = trades_instances[symbols[-1]].sleep_time()
570
- sleepmsg(sleep_time + delay)
571
- time.sleep(60 * sleep_time + delay)
572
- logger.info(sessionmsg)
573
- time_intervals = 0
574
- num_days += 1
575
- elif day_end and today == FRIDAY:
576
- sleep_time = trades_instances[symbols[-1]].sleep_time(weekend=True)
577
- sleepmsg(sleep_time + delay)
578
- time.sleep(60 * sleep_time + delay)
579
- logger.info(sessionmsg)
580
- time_intervals = 0
581
- num_days += 1
582
- elif day_end and today == FRIDAY and num_days >= 20:
583
- strategy.perform_period_end_checks()
584
- break
585
- except Exception:
586
- msg = (
587
- f"Handling period end actions, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
588
- )
589
- _print_exc(debug_mode, msg)
590
- continue
591
-
592
-
593
- def _tws_execution(*args, **kwargs):
594
- raise NotImplementedError("TWS Execution is not yet implemented !!!")
595
-
596
-
597
- class MT5ExecutionEngine:
598
- """
599
- The `MT5ExecutionEngine` class serves as the central hub for executing your trading strategies within the `bbstrader` framework.
600
- It orchestrates the entire trading process, ensuring seamless interaction between your strategies, market data, and your chosen
601
- trading platform.
602
-
603
- Key Features
604
- ------------
605
-
606
- - **Strategy Execution:** The `MT5ExecutionEngine` is responsible for running your strategy, retrieving signals, and executing trades based on those signals.
607
- - **Time Management:** You can define a specific time frame for your trades and set the frequency with which the engine checks for signals and manages trades.
608
- - **Trade Period Control:** Define whether your strategy runs for a day, a week, or a month, allowing for flexible trading durations.
609
- - **Money Management:** The engine supports optional money management features, allowing you to control risk and optimize your trading performance.
610
- - **Trading Day Configuration:** You can customize the days of the week your strategy will execute, providing granular control over your trading schedule.
611
- - **Platform Integration:** The `MT5ExecutionEngine` is currently designed to work with MT5.
612
-
613
- Examples
614
- --------
615
-
616
- >>> from bbstrader.metatrader import create_trade_instance
617
- >>> from bbstrader.trading.execution import MT5ExecutionEngine
618
- >>> from bbstrader.trading.strategies import StockIndexCFDTrading
619
- >>> from bbstrader.config import config_logger
620
- >>>
621
- >>> if __name__ == '__main__':
622
- >>> logger = config_logger(index_trade.log, console_log=True)
623
- >>> # Define symbols
624
- >>> ndx = '[NQ100]'
625
- >>> spx = '[SP500]'
626
- >>> dji = '[DJI30]'
627
- >>> dax = 'GERMANY40'
628
- >>>
629
- >>> symbol_list = [spx, dax, dji, ndx]
630
- >>>
631
- >>> trade_kwargs = {
632
- ... 'expert_id': 5134,
633
- ... 'version': 2.0,
634
- ... 'time_frame': '15m',
635
- ... 'var_level': 0.99,
636
- ... 'start_time': '8:30',
637
- ... 'finishing_time': '19:30',
638
- ... 'ending_time': '21:30',
639
- ... 'max_risk': 5.0,
640
- ... 'daily_risk': 0.10,
641
- ... 'pchange_sl': 1.5,
642
- ... 'rr': 3.0,
643
- ... 'logger': logger
644
- ... }
645
- >>> strategy_kwargs = {
646
- ... 'max_trades': {ndx: 3, spx: 3, dji: 3, dax: 3},
647
- ... 'expected_returns': {ndx: 1.5, spx: 1.5, dji: 1.0, dax: 1.0},
648
- ... 'strategy_name': 'SISTBO',
649
- ... 'logger': logger,
650
- ... 'expert_id': 5134
651
- ... }
652
- >>> trades_instances = create_trade_instance(
653
- ... symbol_list, trade_kwargs,
654
- ... logger=logger,
655
- ... )
656
- >>>
657
- >>> engine = MT5ExecutionEngine(
658
- ... symbol_list,
659
- ... trades_instances,
660
- ... StockIndexCFDTrading,
661
- ... time_frame='15m',
662
- ... iter_time=5,
663
- ... mm=True,
664
- ... period='week',
665
- ... comment='bbs_SISTBO_@2.0',
666
- ... **strategy_kwargs
667
- ... )
668
- >>> engine.run()
669
- """
670
-
671
- def __init__(
672
- self,
673
- symbol_list: List[str],
674
- trades_instances: Dict[str, Trade],
675
- strategy_cls: Strategy,
676
- /,
677
- mm: bool = True,
678
- optimizer: str = "equal",
679
- trail: bool = True,
680
- stop_trail: Optional[int] = None,
681
- trail_after_points: int | str = None,
682
- be_plus_points: Optional[int] = None,
683
- show_positions_orders: bool = False,
684
- iter_time: int | float = 5,
685
- use_trade_time: bool = True,
686
- period: Literal["day", "week", "month"] = "week",
687
- period_end_action: Literal["break", "sleep"] = "break",
688
- closing_pnl: Optional[float] = None,
689
- trading_days: Optional[List[str]] = TradingDays,
690
- comment: Optional[str] = None,
691
- **kwargs,
692
- ):
693
- """
694
- Args:
695
- symbol_list : List of symbols to trade
696
- trades_instances : Dictionary of Trade instances
697
- strategy_cls : Strategy class to use for trading
698
- mm : Enable Money Management. Defaults to True.
699
- optimizer : Risk management optimizer. Defaults to 'equal'.
700
- See `bbstrader.models.optimization` module for more information.
701
- show_positions_orders : Print open positions and orders. Defaults to False.
702
- iter_time : Interval to check for signals and `mm`. Defaults to 5.
703
- use_trade_time : Open trades after the time is completed. Defaults to True.
704
- period : Period to trade. Defaults to 'week'.
705
- period_end_action : Action to take at the end of the period. Defaults to 'break',
706
- this only applies when period is 'day', 'week'.
707
- closing_pnl : Minimum profit in percentage of target profit to close positions. Defaults to -0.001.
708
- trading_days : Trading days in a week. Defaults to monday to friday.
709
- comment: Comment for trades. Defaults to None.
710
- **kwargs: Additional keyword arguments
711
- _ time_frame : Time frame to trade. Defaults to '15m'.
712
- - strategy_name (Optional[str]): Strategy name. Defaults to None.
713
- - max_trades (Dict[str, int]): Maximum trades per symbol. Defaults to None.
714
- - notify (bool): Enable notifications. Defaults to False.
715
- - telegram (bool): Enable telegram notifications. Defaults to False.
716
- - bot_token (str): Telegram bot token. Defaults to None.
717
- - chat_id (Union[int, str, List] ): Telegram chat id. Defaults to None.
718
-
719
- Note:
720
- 1. For `trail` , `stop_trail` , `trail_after_points` , `be_plus_points` see `bbstrader.metatrader.trade.Trade.break_even()` .
721
- 2. All Strategies must inherit from `bbstrader.btengine.strategy.Strategy` or `bbstrader.btengine.strategy.MT5Strategy` class
722
- and have a `calculate_signals` method that returns a dictionary of signals for each symbol in symbol_list.
723
-
724
- 3. All strategies must have the following arguments in their `__init__` method:
725
- - bars (DataHandler): DataHandler instance default to None
726
- - events (Queue): Queue instance default to None
727
- - symbol_list (List[str]): List of symbols to trade can be none for backtesting
728
- - mode (str): Mode of the strategy. Must be either 'live' or 'backtest'
729
- - **kwargs: Additional keyword arguments
730
- The keyword arguments are all the additional arguments passed to the `MT5ExecutionEngine` class,
731
- the `Strategy` class, the `DataHandler` class, the `Portfolio` class and the `ExecutionHandler` class.
732
- - The `bars` and `events` arguments are used for backtesting only.
733
-
734
- 4. All strategies must generate signals for backtesting and live trading.
735
- See the `bbstrader.trading.strategies` module for more information on how to create custom strategies.
736
- See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
737
- """
738
- self.symbol_list = symbol_list
739
- self.trades_instances = trades_instances
740
- self.strategy_cls = strategy_cls
741
- self.mm = mm
742
- self.optimizer = optimizer
743
- self.trail = trail
744
- self.stop_trail = stop_trail
745
- self.trail_after_points = trail_after_points
746
- self.be_plus_points = be_plus_points
747
- self.show_positions_orders = show_positions_orders
748
- self.iter_time = iter_time
749
- self.use_trade_time = use_trade_time
750
- self.period = period
751
- self.period_end_action = period_end_action
752
- self.closing_pnl = closing_pnl
753
- self.trading_days = trading_days
754
- self.comment = comment
755
- self.kwargs = kwargs
756
-
757
- def __repr__(self):
758
- trades = self.trades_instances.keys()
759
- s = self.strategy_cls.__name__
760
- return f"MT5ExecutionEngine(Symbols={list(trades)}, Strategy={s})"
761
-
762
- def run(self):
763
- check_mt5_connection(**self.kwargs)
764
- _mt5_execution(
765
- self.symbol_list,
766
- self.trades_instances,
767
- self.strategy_cls,
768
- mm=self.mm,
769
- optimizer=self.optimizer,
770
- trail=self.trail,
771
- stop_trail=self.stop_trail,
772
- trail_after_points=self.trail_after_points,
773
- be_plus_points=self.be_plus_points,
774
- show_positions_orders=self.show_positions_orders,
775
- iter_time=self.iter_time,
776
- use_trade_time=self.use_trade_time,
777
- period=self.period,
778
- period_end_action=self.period_end_action,
779
- closing_pnl=self.closing_pnl,
780
- trading_days=self.trading_days,
781
- comment=self.comment,
782
- **self.kwargs,
783
- )
784
-
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
-
842
- class TWSExecutionEngine: ...
1
+ import multiprocessing as mp
2
+ import time
3
+ import traceback
4
+ from datetime import datetime
5
+ from typing import Dict, List, Literal, Optional
6
+
7
+ from loguru import logger as log
8
+
9
+ from bbstrader.btengine.strategy import MT5Strategy, Strategy
10
+ from bbstrader.config import BBSTRADER_DIR
11
+ from bbstrader.core.utils import TradeAction
12
+ from bbstrader.metatrader.account import Account, check_mt5_connection
13
+ from bbstrader.metatrader.trade import Trade
14
+ from bbstrader.trading.scripts import send_message
15
+
16
+ try:
17
+ import MetaTrader5 as MT5
18
+ except ImportError:
19
+ import bbstrader.compat # noqa: F401
20
+
21
+
22
+ __all__ = ["MT5ExecutionEngine", "TWSExecutionEngine"]
23
+
24
+ _TF_MAPPING = {
25
+ "1m": 1,
26
+ "3m": 3,
27
+ "5m": 5,
28
+ "10m": 10,
29
+ "15m": 15,
30
+ "30m": 30,
31
+ "1h": 60,
32
+ "2h": 120,
33
+ "4h": 240,
34
+ "6h": 360,
35
+ "8h": 480,
36
+ "12h": 720,
37
+ "D1": 1440,
38
+ }
39
+
40
+ TradingDays = ["monday", "tuesday", "wednesday", "thursday", "friday"]
41
+ WEEK_DAYS = TradingDays + ["saturday", "sunday"]
42
+
43
+ BUYS = ["BMKT", "BLMT", "BSTP", "BSTPLMT"]
44
+ SELLS = ["SMKT", "SLMT", "SSTP", "SSTPLMT"]
45
+
46
+ ORDERS_TYPES = [
47
+ "orders",
48
+ "buy_stops",
49
+ "sell_stops",
50
+ "buy_limits",
51
+ "sell_limits",
52
+ "buy_stop_limits",
53
+ "sell_stop_limits",
54
+ ]
55
+ POSITIONS_TYPES = ["positions", "buys", "sells", "profitables", "losings"]
56
+
57
+ ACTIONS = ["buys", "sells"]
58
+ STOPS = ["buy_stops", "sell_stops"]
59
+ LIMITS = ["buy_limits", "sell_limits"]
60
+ STOP_LIMITS = ["buy_stop_limits", "sell_stop_limits"]
61
+
62
+ EXIT_SIGNAL_ACTIONS = {
63
+ "EXIT": {a: a[:-1] for a in ACTIONS},
64
+ "EXIT_LONG": {"buys": "buy"},
65
+ "EXIT_SHORT": {"sells": "sell"},
66
+ "EXIT_STOP": {stop: stop for stop in STOPS},
67
+ "EXIT_LONG_STOP": {"buy_stops": "buy_stops"},
68
+ "EXIT_SHORT_STOP": {"sell_stops": "sell_stops"},
69
+ "EXIT_LIMIT": {limit: limit for limit in LIMITS},
70
+ "EXIT_LONG_LIMIT": {"buy_limits": "buy_limits"},
71
+ "EXIT_SHORT_LIMIT": {"sell_limits": "sell_limits"},
72
+ "EXIT_STOP_LIMIT": {sl: sl for sl in STOP_LIMITS},
73
+ "EXIT_LONG_STOP_LIMIT": {STOP_LIMITS[0]: STOP_LIMITS[0]},
74
+ "EXIT_SHORT_STOP_LIMIT": {STOP_LIMITS[1]: STOP_LIMITS[1]},
75
+ "EXIT_PROFITABLES": {"profitables": "profitable"},
76
+ "EXIT_LOSINGS": {"losings": "losing"},
77
+ "EXIT_ALL_POSITIONS": {"positions": "all"},
78
+ "EXIT_ALL_ORDERS": {"orders": "all"},
79
+ }
80
+
81
+ COMMON_RETCODES = [MT5.TRADE_RETCODE_MARKET_CLOSED, MT5.TRADE_RETCODE_CLOSE_ONLY]
82
+
83
+ NON_EXEC_RETCODES = {
84
+ "BMKT": [MT5.TRADE_RETCODE_SHORT_ONLY] + COMMON_RETCODES,
85
+ "SMKT": [MT5.TRADE_RETCODE_LONG_ONLY] + COMMON_RETCODES,
86
+ }
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
+
95
+
96
+ def _mt5_execution(
97
+ symbol_list,
98
+ trades_instances,
99
+ strategy_cls,
100
+ /,
101
+ mm,
102
+ optimizer,
103
+ trail,
104
+ stop_trail,
105
+ trail_after_points,
106
+ be_plus_points,
107
+ show_positions_orders,
108
+ iter_time,
109
+ use_trade_time,
110
+ period,
111
+ period_end_action,
112
+ closing_pnl,
113
+ trading_days,
114
+ comment,
115
+ **kwargs,
116
+ ):
117
+ logger = kwargs.get("logger", log)
118
+
119
+ def _print_exc(dm, msg):
120
+ traceback.print_exc() if dm else logger.error(msg)
121
+
122
+ try:
123
+ symbols = symbol_list.copy()
124
+ time_frame = kwargs.get("time_frame", "15m")
125
+ daily_risk = kwargs.get("daily_risk")
126
+ STRATEGY = kwargs.get("strategy_name")
127
+ mtrades = kwargs.get("max_trades")
128
+ notify = kwargs.get("notify", False)
129
+ signal_tickers = kwargs.get("signal_tickers", symbols)
130
+ debug_mode = kwargs.get("debug_mode", False)
131
+ delay = kwargs.get("delay", 0)
132
+ ACCOUNT = kwargs.get("account", "MT5 Account")
133
+ if notify:
134
+ telegram = kwargs.get("telegram", False)
135
+ bot_token = kwargs.get("bot_token")
136
+ chat_id = kwargs.get("chat_id")
137
+
138
+ expert_ids = kwargs.get("expert_ids")
139
+ if expert_ids is None:
140
+ expert_ids = list(
141
+ set([trade.expert_id for trade in trades_instances.values()])
142
+ )
143
+ elif isinstance(expert_ids, int):
144
+ expert_ids = [expert_ids]
145
+
146
+ max_trades = {
147
+ symbol: mtrades[symbol]
148
+ if mtrades is not None and symbol in mtrades
149
+ else trades_instances[symbol].max_trade()
150
+ for symbol in symbols
151
+ }
152
+ if comment is None:
153
+ trade = trades_instances[symbols[0]]
154
+ comment = f"{trade.expert_name}@{trade.version}"
155
+
156
+ if period.lower() == "24/7":
157
+ trading_days = WEEK_DAYS
158
+ except Exception:
159
+ _print_exc(
160
+ debug_mode,
161
+ f"Initializing Execution Engine, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}",
162
+ )
163
+ return
164
+
165
+ def update_risk(weights):
166
+ if weights is not None:
167
+ for symbol in symbols:
168
+ if symbol not in weights:
169
+ continue
170
+ trade = trades_instances[symbol]
171
+ dailydd = round(weights[symbol] * daily_risk, 5)
172
+ trade.dailydd = dailydd
173
+
174
+ def check_retcode(trade: Trade, position):
175
+ if len(trade.retcodes) > 0:
176
+ for retcode in trade.retcodes:
177
+ if retcode in NON_EXEC_RETCODES[position]:
178
+ return True
179
+ return False
180
+
181
+ def _send_notification(signal, symbol):
182
+ if symbol in signal_tickers:
183
+ send_message(
184
+ message=signal,
185
+ notify_me=notify,
186
+ telegram=telegram,
187
+ token=bot_token,
188
+ chat_id=chat_id,
189
+ )
190
+
191
+ def check(buys, sells, symbol):
192
+ if not mm:
193
+ return
194
+ if buys is not None or sells is not None:
195
+ trades_instances[symbol].break_even(
196
+ mm=mm,
197
+ trail=trail,
198
+ stop_trail=stop_trail,
199
+ trail_after_points=trail_after_points,
200
+ be_plus_points=be_plus_points,
201
+ )
202
+
203
+ try:
204
+ check_mt5_connection(**kwargs)
205
+ strategy: MT5Strategy = strategy_cls(symbol_list=symbols, mode="live", **kwargs)
206
+ except Exception:
207
+ _print_exc(
208
+ debug_mode, f"Initializing strategy, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}"
209
+ )
210
+ return
211
+ logger.info(
212
+ f"Running {STRATEGY} Strategy in {time_frame} Interval ..., ACCOUNT={ACCOUNT}"
213
+ )
214
+
215
+ def run_trade_algorithm(signal, symbol, id, trade: Trade, price, stoplimit):
216
+ signal = "BMKT" if signal == "LONG" or signal == "BUY" else signal
217
+ signal = "SMKT" if signal == "SHORT" or signal == "SELL" else signal
218
+ info = f"SIGNAL = {signal}, SYMBOL={symbol}, STRATEGY={STRATEGY}, TIMEFRAME={time_frame} , ACCOUNT={ACCOUNT}"
219
+ account = Account(**kwargs)
220
+ symbol_type = account.get_symbol_type(symbol)
221
+ desc = account.get_symbol_info(symbol).description
222
+ sigmsg = (
223
+ f"SIGNAL = {signal}, \nSYMBOL={symbol}, \nTYPE={symbol_type}, \nDESCRIPTION={desc}, "
224
+ f"\nPRICE={price}, \nSTOPLIMIT={stoplimit}, \nSTRATEGY={STRATEGY}, \nTIMEFRAME={time_frame},"
225
+ f"\nBROKER={account.broker.name}, \nTIMESTAMP={datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
226
+ )
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}"
230
+ if signal not in EXIT_SIGNAL_ACTIONS:
231
+ if signal in NON_EXEC_RETCODES and not check_retcode(trade, signal):
232
+ logger.info(info)
233
+ elif signal not in NON_EXEC_RETCODES:
234
+ logger.info(info)
235
+ if signal in EXIT_SIGNAL_ACTIONS:
236
+ for exit_signal, actions in EXIT_SIGNAL_ACTIONS.items():
237
+ for position_type, order_type in actions.items():
238
+ clos_func = getattr(
239
+ trades_instances[symbol], f"get_current_{position_type}"
240
+ )
241
+ if clos_func(id=id) is not None:
242
+ if notify:
243
+ _send_notification(sigmsg, symbol)
244
+ if position_type in POSITIONS_TYPES:
245
+ trade.close_positions(position_type=order_type, id=id)
246
+ else:
247
+ trade.close_orders(order_type=order_type, id=id)
248
+ elif signal in BUYS and not long_market[symbol]:
249
+ if use_trade_time:
250
+ if time_intervals % trade_time == 0 or buys[symbol] is None:
251
+ if notify:
252
+ _send_notification(sigmsg, symbol)
253
+ if not check_retcode(trade, "BMKT"):
254
+ logger.info(msg)
255
+ trade.open_buy_position(
256
+ action=signal,
257
+ price=price,
258
+ stoplimit=stoplimit,
259
+ id=id,
260
+ mm=mm,
261
+ trail=trail,
262
+ comment=comment,
263
+ )
264
+ else:
265
+ logger.info(tfmsg)
266
+ check(buys[symbol], sells[symbol], symbol)
267
+ else:
268
+ if notify:
269
+ _send_notification(sigmsg, symbol)
270
+ if not check_retcode(trade, "BMKT"):
271
+ logger.info(msg)
272
+ trade.open_buy_position(
273
+ action=signal,
274
+ price=price,
275
+ stoplimit=stoplimit,
276
+ id=id,
277
+ mm=mm,
278
+ trail=trail,
279
+ comment=comment,
280
+ )
281
+
282
+ elif signal in BUYS and long_market[symbol]:
283
+ logger.info(riskmsg)
284
+
285
+ elif signal in SELLS and not short_market[symbol]:
286
+ if use_trade_time:
287
+ if time_intervals % trade_time == 0 or sells[symbol] is None:
288
+ if notify:
289
+ _send_notification(sigmsg, symbol)
290
+ if not check_retcode(trade, "SMKT"):
291
+ logger.info(msg)
292
+ trade.open_sell_position(
293
+ action=signal,
294
+ price=price,
295
+ stoplimit=stoplimit,
296
+ id=id,
297
+ mm=mm,
298
+ trail=trail,
299
+ comment=comment,
300
+ )
301
+ else:
302
+ logger.info(tfmsg)
303
+ check(buys[symbol], sells[symbol], symbol)
304
+ else:
305
+ if notify:
306
+ _send_notification(sigmsg, symbol)
307
+ if not check_retcode(trade, "SMKT"):
308
+ logger.info(msg)
309
+ trade.open_sell_position(
310
+ action=signal,
311
+ price=price,
312
+ stoplimit=stoplimit,
313
+ id=id,
314
+ mm=mm,
315
+ trail=trail,
316
+ comment=comment,
317
+ )
318
+
319
+ elif signal in SELLS and short_market[symbol]:
320
+ logger.info(riskmsg)
321
+ else:
322
+ check(buys[symbol], sells[symbol], symbol)
323
+
324
+ num_days = 0
325
+ time_intervals = 0
326
+ trade_time = _TF_MAPPING[time_frame]
327
+
328
+ long_market = {symbol: False for symbol in symbols}
329
+ short_market = {symbol: False for symbol in symbols}
330
+
331
+ while True:
332
+ try:
333
+ check_mt5_connection(**kwargs)
334
+ current_date = datetime.now()
335
+ today = current_date.strftime("%A").lower()
336
+ time.sleep(0.5)
337
+ positions_orders = {}
338
+ for type in POSITIONS_TYPES + ORDERS_TYPES:
339
+ positions_orders[type] = {}
340
+ for symbol in symbols:
341
+ positions_orders[type][symbol] = None
342
+ for id in expert_ids:
343
+ func = getattr(trades_instances[symbol], f"get_current_{type}")
344
+ func_value = func(id=id)
345
+ if func_value is not None:
346
+ if positions_orders[type][symbol] is None:
347
+ positions_orders[type][symbol] = func(id=id)
348
+ else:
349
+ positions_orders[type][symbol] += func(id=id)
350
+ buys = positions_orders["buys"]
351
+ sells = positions_orders["sells"]
352
+ for symbol in symbols:
353
+ for type in POSITIONS_TYPES + ORDERS_TYPES:
354
+ if positions_orders[type][symbol] is not None:
355
+ if show_positions_orders:
356
+ logger.info(
357
+ f"Current {type.upper()} SYMBOL={symbol}: \
358
+ {positions_orders[type][symbol]}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
359
+ )
360
+ long_market = {
361
+ symbol: buys[symbol] is not None
362
+ and len(buys[symbol]) >= max_trades[symbol]
363
+ for symbol in symbols
364
+ }
365
+ short_market = {
366
+ symbol: sells[symbol] is not None
367
+ and len(sells[symbol]) >= max_trades[symbol]
368
+ for symbol in symbols
369
+ }
370
+ except Exception:
371
+ _print_exc(
372
+ debug_mode,
373
+ f"Checking positions and orders, STRATEGY={STRATEGY}, ACCOUNT={ACCOUNT}",
374
+ )
375
+ continue
376
+ time.sleep(0.5)
377
+ try:
378
+ check_mt5_connection(**kwargs)
379
+ signals = strategy.calculate_signals()
380
+ weights = (
381
+ strategy.apply_risk_management(optimizer)
382
+ if hasattr(strategy, "apply_risk_management")
383
+ else None
384
+ )
385
+ update_risk(weights)
386
+ except Exception:
387
+ _print_exc(
388
+ debug_mode,
389
+ f"Calculating Signals, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}",
390
+ )
391
+ continue
392
+ if len(signals) == 0:
393
+ for symbol in symbols:
394
+ check(buys[symbol], sells[symbol], symbol)
395
+ else:
396
+ try:
397
+ check_mt5_connection(**kwargs)
398
+ for signal in signals:
399
+ symbol = signal.symbol
400
+ trade: Trade = trades_instances[symbol]
401
+ if trade.trading_time() and today in trading_days:
402
+ if signal.action is not None:
403
+ action = (
404
+ signal.action.value
405
+ if isinstance(signal.action, TradeAction)
406
+ else signal.action
407
+ )
408
+ run_trade_algorithm(
409
+ action,
410
+ symbol,
411
+ signal.id,
412
+ trade,
413
+ signal.price,
414
+ signal.stoplimit,
415
+ )
416
+ else:
417
+ if len(symbols) >= 10:
418
+ if symbol == symbols[-1]:
419
+ logger.info(
420
+ f"Not trading Time !!!, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
421
+ )
422
+ else:
423
+ logger.info(
424
+ f"Not trading Time !!! SYMBOL={trade.symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
425
+ )
426
+ check(buys[symbol], sells[symbol], symbol)
427
+
428
+ except Exception:
429
+ msg = f"Handling Signals, SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
430
+ _print_exc(debug_mode, msg)
431
+ continue
432
+
433
+ time.sleep((60 * iter_time) - 1.0)
434
+ if iter_time == 1:
435
+ time_intervals += 1
436
+ elif trade_time % iter_time == 0:
437
+ time_intervals += iter_time
438
+ else:
439
+ if use_trade_time:
440
+ raise ValueError(
441
+ f"iter_time must be a multiple of the {time_frame} !!!"
442
+ f"(e.g., if time_frame is 15m, iter_time must be 1.5, 3, 5, 15 etc)"
443
+ )
444
+ try:
445
+ FRIDAY = "friday"
446
+ check_mt5_connection(**kwargs)
447
+ day_end = all(trade.days_end() for trade in trades_instances.values())
448
+ if closing_pnl is not None:
449
+ closing = all(
450
+ trade.positive_profit(id=trade.expert_id, th=closing_pnl)
451
+ for trade in trades_instances.values()
452
+ )
453
+ else:
454
+ closing = True
455
+
456
+ def logmsg(period, symbol):
457
+ logger.info(
458
+ f"End of the {period} !!! SYMBOL={symbol}, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
459
+ )
460
+
461
+ def logmsgif(period, symbol):
462
+ if len(symbols) <= 10:
463
+ logmsg(period, symbol)
464
+ elif len(symbols) > 10 and symbol == symbols[-1]:
465
+ logger.info(
466
+ f"End of the {period} !!! STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
467
+ )
468
+
469
+ def sleepmsg(sleep_time):
470
+ logger.info(f"{ACCOUNT} Sleeping for {sleep_time} minutes ...\n")
471
+
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":
492
+ for symbol in symbols:
493
+ trade = trades_instances[symbol]
494
+ if trade.days_end() and closing:
495
+ for id in expert_ids:
496
+ trade.close_positions(
497
+ position_type="all", id=id, comment=comment
498
+ )
499
+ logmsgif("Day", symbol)
500
+ trade.statistics(save=True)
501
+
502
+ if day_end:
503
+ strategy.perform_period_end_checks()
504
+ if period_end_action == "break" and closing:
505
+ break
506
+ elif (
507
+ period_end_action == "sleep" and today != FRIDAY or not closing
508
+ ):
509
+ sleep_time = trades_instances[symbols[-1]].sleep_time()
510
+ sleepmsg(sleep_time + delay)
511
+ time.sleep(60 * sleep_time + delay)
512
+ logger.info(sessionmsg)
513
+ time_intervals = 0
514
+ elif period_end_action == "sleep" and today == FRIDAY:
515
+ sleep_time = trades_instances[symbols[-1]].sleep_time(
516
+ weekend=True
517
+ )
518
+ sleepmsg(sleep_time + delay)
519
+ time.sleep(60 * sleep_time + delay)
520
+ logger.info(sessionmsg)
521
+ time_intervals = 0
522
+
523
+ elif period.lower() == "week":
524
+ for symbol in symbols:
525
+ trade = trades_instances[symbol]
526
+ if trade.days_end() and today != FRIDAY:
527
+ logmsgif("Day", symbol)
528
+
529
+ elif trade.days_end() and today == FRIDAY and closing:
530
+ for id in expert_ids:
531
+ trade.close_positions(
532
+ position_type="all", id=id, comment=comment
533
+ )
534
+ logmsgif("Week", symbol)
535
+ trade.statistics(save=True)
536
+
537
+ if day_end and today != FRIDAY:
538
+ sleep_time = trades_instances[symbols[-1]].sleep_time()
539
+ sleepmsg(sleep_time + delay)
540
+ time.sleep(60 * sleep_time + delay)
541
+ logger.info(sessionmsg)
542
+ time_intervals = 0
543
+ elif day_end and today == FRIDAY:
544
+ strategy.perform_period_end_checks()
545
+ if period_end_action == "break" and closing:
546
+ break
547
+ elif period_end_action == "sleep" or not closing:
548
+ sleep_time = trades_instances[symbols[-1]].sleep_time(
549
+ weekend=True
550
+ )
551
+ sleepmsg(sleep_time + delay)
552
+ time.sleep(60 * sleep_time + delay)
553
+ logger.info(sessionmsg)
554
+ time_intervals = 0
555
+
556
+ elif period.lower() == "month":
557
+ for symbol in symbols:
558
+ trade = trades_instances[symbol]
559
+ if trade.days_end() and today != FRIDAY:
560
+ logmsgif("Day", symbol)
561
+ elif trade.days_end() and today == FRIDAY:
562
+ logmsgif("Week", symbol)
563
+ elif (
564
+ trade.days_end() and today == FRIDAY and num_days >= 20
565
+ ) and closing:
566
+ for id in expert_ids:
567
+ trade.close_positions(
568
+ position_type="all", id=id, comment=comment
569
+ )
570
+ logmsgif("Month", symbol)
571
+ trade.statistics(save=True)
572
+ if day_end and today != FRIDAY:
573
+ sleep_time = trades_instances[symbols[-1]].sleep_time()
574
+ sleepmsg(sleep_time + delay)
575
+ time.sleep(60 * sleep_time + delay)
576
+ logger.info(sessionmsg)
577
+ time_intervals = 0
578
+ num_days += 1
579
+ elif day_end and today == FRIDAY:
580
+ sleep_time = trades_instances[symbols[-1]].sleep_time(weekend=True)
581
+ sleepmsg(sleep_time + delay)
582
+ time.sleep(60 * sleep_time + delay)
583
+ logger.info(sessionmsg)
584
+ time_intervals = 0
585
+ num_days += 1
586
+ elif day_end and today == FRIDAY and num_days >= 20:
587
+ strategy.perform_period_end_checks()
588
+ break
589
+ except Exception:
590
+ msg = (
591
+ f"Handling period end actions, STRATEGY={STRATEGY} , ACCOUNT={ACCOUNT}"
592
+ )
593
+ _print_exc(debug_mode, msg)
594
+ continue
595
+
596
+
597
+ def _tws_execution(*args, **kwargs):
598
+ raise NotImplementedError("TWS Execution is not yet implemented !!!")
599
+
600
+
601
+ class MT5ExecutionEngine:
602
+ """
603
+ The `MT5ExecutionEngine` class serves as the central hub for executing your trading strategies within the `bbstrader` framework.
604
+ It orchestrates the entire trading process, ensuring seamless interaction between your strategies, market data, and your chosen
605
+ trading platform.
606
+
607
+ Key Features
608
+ ------------
609
+
610
+ - **Strategy Execution:** The `MT5ExecutionEngine` is responsible for running your strategy, retrieving signals, and executing trades based on those signals.
611
+ - **Time Management:** You can define a specific time frame for your trades and set the frequency with which the engine checks for signals and manages trades.
612
+ - **Trade Period Control:** Define whether your strategy runs for a day, a week, or a month, allowing for flexible trading durations.
613
+ - **Money Management:** The engine supports optional money management features, allowing you to control risk and optimize your trading performance.
614
+ - **Trading Day Configuration:** You can customize the days of the week your strategy will execute, providing granular control over your trading schedule.
615
+ - **Platform Integration:** The `MT5ExecutionEngine` is currently designed to work with MT5.
616
+
617
+ Examples
618
+ --------
619
+
620
+ >>> from bbstrader.metatrader import create_trade_instance
621
+ >>> from bbstrader.trading.execution import MT5ExecutionEngine
622
+ >>> from bbstrader.trading.strategies import StockIndexCFDTrading
623
+ >>> from bbstrader.config import config_logger
624
+ >>>
625
+ >>> if __name__ == '__main__':
626
+ >>> logger = config_logger(index_trade.log, console_log=True)
627
+ >>> # Define symbols
628
+ >>> ndx = '[NQ100]'
629
+ >>> spx = '[SP500]'
630
+ >>> dji = '[DJI30]'
631
+ >>> dax = 'GERMANY40'
632
+ >>>
633
+ >>> symbol_list = [spx, dax, dji, ndx]
634
+ >>>
635
+ >>> trade_kwargs = {
636
+ ... 'expert_id': 5134,
637
+ ... 'version': 2.0,
638
+ ... 'time_frame': '15m',
639
+ ... 'var_level': 0.99,
640
+ ... 'start_time': '8:30',
641
+ ... 'finishing_time': '19:30',
642
+ ... 'ending_time': '21:30',
643
+ ... 'max_risk': 5.0,
644
+ ... 'daily_risk': 0.10,
645
+ ... 'pchange_sl': 1.5,
646
+ ... 'rr': 3.0,
647
+ ... 'logger': logger
648
+ ... }
649
+ >>> strategy_kwargs = {
650
+ ... 'max_trades': {ndx: 3, spx: 3, dji: 3, dax: 3},
651
+ ... 'expected_returns': {ndx: 1.5, spx: 1.5, dji: 1.0, dax: 1.0},
652
+ ... 'strategy_name': 'SISTBO',
653
+ ... 'logger': logger,
654
+ ... 'expert_id': 5134
655
+ ... }
656
+ >>> trades_instances = create_trade_instance(
657
+ ... symbol_list, trade_kwargs,
658
+ ... logger=logger,
659
+ ... )
660
+ >>>
661
+ >>> engine = MT5ExecutionEngine(
662
+ ... symbol_list,
663
+ ... trades_instances,
664
+ ... StockIndexCFDTrading,
665
+ ... time_frame='15m',
666
+ ... iter_time=5,
667
+ ... mm=True,
668
+ ... period='week',
669
+ ... comment='bbs_SISTBO_@2.0',
670
+ ... **strategy_kwargs
671
+ ... )
672
+ >>> engine.run()
673
+ """
674
+
675
+ def __init__(
676
+ self,
677
+ symbol_list: List[str],
678
+ trades_instances: Dict[str, Trade],
679
+ strategy_cls: Strategy,
680
+ /,
681
+ mm: bool = True,
682
+ optimizer: str = "equal",
683
+ trail: bool = True,
684
+ stop_trail: Optional[int] = None,
685
+ trail_after_points: int | str = None,
686
+ be_plus_points: Optional[int] = None,
687
+ show_positions_orders: bool = False,
688
+ iter_time: int | float = 5,
689
+ use_trade_time: bool = True,
690
+ period: Literal["day", "week", "month"] = "week",
691
+ period_end_action: Literal["break", "sleep"] = "break",
692
+ closing_pnl: Optional[float] = None,
693
+ trading_days: Optional[List[str]] = TradingDays,
694
+ comment: Optional[str] = None,
695
+ **kwargs,
696
+ ):
697
+ """
698
+ Args:
699
+ symbol_list : List of symbols to trade
700
+ trades_instances : Dictionary of Trade instances
701
+ strategy_cls : Strategy class to use for trading
702
+ mm : Enable Money Management. Defaults to True.
703
+ optimizer : Risk management optimizer. Defaults to 'equal'.
704
+ See `bbstrader.models.optimization` module for more information.
705
+ show_positions_orders : Print open positions and orders. Defaults to False.
706
+ iter_time : Interval to check for signals and `mm`. Defaults to 5.
707
+ use_trade_time : Open trades after the time is completed. Defaults to True.
708
+ period : Period to trade. Defaults to 'week'.
709
+ period_end_action : Action to take at the end of the period. Defaults to 'break',
710
+ this only applies when period is 'day', 'week'.
711
+ closing_pnl : Minimum profit in percentage of target profit to close positions. Defaults to -0.001.
712
+ trading_days : Trading days in a week. Defaults to monday to friday.
713
+ comment: Comment for trades. Defaults to None.
714
+ **kwargs: Additional keyword arguments
715
+ _ time_frame : Time frame to trade. Defaults to '15m'.
716
+ - strategy_name (Optional[str]): Strategy name. Defaults to None.
717
+ - max_trades (Dict[str, int]): Maximum trades per symbol. Defaults to None.
718
+ - notify (bool): Enable notifications. Defaults to False.
719
+ - telegram (bool): Enable telegram notifications. Defaults to False.
720
+ - bot_token (str): Telegram bot token. Defaults to None.
721
+ - chat_id (Union[int, str, List] ): Telegram chat id. Defaults to None.
722
+
723
+ Note:
724
+ 1. For `trail` , `stop_trail` , `trail_after_points` , `be_plus_points` see `bbstrader.metatrader.trade.Trade.break_even()` .
725
+ 2. All Strategies must inherit from `bbstrader.btengine.strategy.Strategy` or `bbstrader.btengine.strategy.MT5Strategy` class
726
+ and have a `calculate_signals` method that returns a dictionary of signals for each symbol in symbol_list.
727
+
728
+ 3. All strategies must have the following arguments in their `__init__` method:
729
+ - bars (DataHandler): DataHandler instance default to None
730
+ - events (Queue): Queue instance default to None
731
+ - symbol_list (List[str]): List of symbols to trade can be none for backtesting
732
+ - mode (str): Mode of the strategy. Must be either 'live' or 'backtest'
733
+ - **kwargs: Additional keyword arguments
734
+ The keyword arguments are all the additional arguments passed to the `MT5ExecutionEngine` class,
735
+ the `Strategy` class, the `DataHandler` class, the `Portfolio` class and the `ExecutionHandler` class.
736
+ - The `bars` and `events` arguments are used for backtesting only.
737
+
738
+ 4. All strategies must generate signals for backtesting and live trading.
739
+ See the `bbstrader.trading.strategies` module for more information on how to create custom strategies.
740
+ See `bbstrader.metatrader.account.check_mt5_connection()` for more details on how to connect to MT5 terminal.
741
+ """
742
+ self.symbol_list = symbol_list
743
+ self.trades_instances = trades_instances
744
+ self.strategy_cls = strategy_cls
745
+ self.mm = mm
746
+ self.optimizer = optimizer
747
+ self.trail = trail
748
+ self.stop_trail = stop_trail
749
+ self.trail_after_points = trail_after_points
750
+ self.be_plus_points = be_plus_points
751
+ self.show_positions_orders = show_positions_orders
752
+ self.iter_time = iter_time
753
+ self.use_trade_time = use_trade_time
754
+ self.period = period
755
+ self.period_end_action = period_end_action
756
+ self.closing_pnl = closing_pnl
757
+ self.trading_days = trading_days
758
+ self.comment = comment
759
+ self.kwargs = kwargs
760
+
761
+ def __repr__(self):
762
+ trades = self.trades_instances.keys()
763
+ s = self.strategy_cls.__name__
764
+ return f"MT5ExecutionEngine(Symbols={list(trades)}, Strategy={s})"
765
+
766
+ def run(self):
767
+ check_mt5_connection(**self.kwargs)
768
+ _mt5_execution(
769
+ self.symbol_list,
770
+ self.trades_instances,
771
+ self.strategy_cls,
772
+ mm=self.mm,
773
+ optimizer=self.optimizer,
774
+ trail=self.trail,
775
+ stop_trail=self.stop_trail,
776
+ trail_after_points=self.trail_after_points,
777
+ be_plus_points=self.be_plus_points,
778
+ show_positions_orders=self.show_positions_orders,
779
+ iter_time=self.iter_time,
780
+ use_trade_time=self.use_trade_time,
781
+ period=self.period,
782
+ period_end_action=self.period_end_action,
783
+ closing_pnl=self.closing_pnl,
784
+ trading_days=self.trading_days,
785
+ comment=self.comment,
786
+ **self.kwargs,
787
+ )
788
+
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
+
846
+ class TWSExecutionEngine: ...