bbstrader 0.2.991__py3-none-any.whl → 0.3.1__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/models/nlp.py CHANGED
@@ -1,14 +1,16 @@
1
+ import contextlib
2
+ import os
1
3
  import re
2
4
  import time
3
5
  from datetime import datetime
4
- from typing import Dict
6
+ from typing import Dict, List, Tuple
5
7
 
6
8
  import dash
7
9
  import matplotlib.pyplot as plt
8
10
  import nltk
9
11
  import pandas as pd
10
12
  import plotly.express as px
11
- import spacy
13
+ import en_core_web_sm
12
14
  from dash import dcc, html
13
15
  from dash.dependencies import Input, Output
14
16
  from nltk.corpus import stopwords
@@ -18,6 +20,7 @@ from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
18
20
 
19
21
  from bbstrader.core.data import FinancialNews
20
22
 
23
+
21
24
  __all__ = [
22
25
  "TopicModeler",
23
26
  "SentimentAnalyzer",
@@ -331,9 +334,18 @@ FINANCIAL_LEXICON = {
331
334
 
332
335
  class TopicModeler(object):
333
336
  def __init__(self):
334
- self.nlp = spacy.load("en_core_web_sm")
335
- self.nlp.disable_pipes("ner")
337
+ nltk.download("punkt", quiet=True)
338
+ nltk.download("stopwords", quiet=True)
336
339
 
340
+ try:
341
+ self.nlp = en_core_web_sm.load()
342
+ self.nlp.disable_pipes("ner")
343
+ except OSError:
344
+ raise OSError(
345
+ "SpaCy model 'en_core_web_sm' not found. "
346
+ "Please install it using 'python -m spacy download en_core_web_sm'."
347
+ )
348
+
337
349
  def preprocess_texts(self, texts: list[str]):
338
350
  def clean_doc(Doc):
339
351
  doc = []
@@ -382,22 +394,25 @@ class SentimentAnalyzer(object):
382
394
  - Downloads NLTK tokenization (`punkt`) and stopwords.
383
395
  - Loads the `en_core_web_sm` SpaCy model with Named Entity Recognition (NER) disabled.
384
396
  - Initializes VADER's SentimentIntensityAnalyzer for sentiment scoring.
397
+
398
+ Args:
399
+ use_spacy (bool): If True, uses SpaCy for lemmatization. Defaults to False.
385
400
  """
386
401
  nltk.download("punkt", quiet=True)
387
402
  nltk.download("stopwords", quiet=True)
388
403
 
404
+ self.analyzer = SentimentIntensityAnalyzer()
405
+ self._stopwords = set(stopwords.words("english"))
406
+
389
407
  try:
390
- self.nlp = spacy.load("en_core_web_sm")
408
+ self.nlp = en_core_web_sm.load()
391
409
  self.nlp.disable_pipes("ner")
392
410
  except OSError:
393
- raise RuntimeError(
394
- "The SpaCy model 'en_core_web_sm' is not installed.\n"
395
- "Please install it by running:\n"
396
- " python -m spacy download en_core_web_sm"
411
+ raise OSError(
412
+ "SpaCy model 'en_core_web_sm' not found. "
413
+ "Please install it using 'python -m spacy download en_core_web_sm'."
397
414
  )
398
-
399
- self.analyzer = SentimentIntensityAnalyzer()
400
- self._stopwords = set(stopwords.words("english"))
415
+ self.news = FinancialNews()
401
416
 
402
417
  def preprocess_text(self, text: str):
403
418
  """
@@ -415,13 +430,18 @@ class SentimentAnalyzer(object):
415
430
  Returns:
416
431
  str: The cleaned and lemmatized text.
417
432
  """
433
+ if not isinstance(text, str):
434
+ raise ValueError(f"{self.__class__.__name__}: preprocess_text expects a string, got {type(text)}")
418
435
  text = text.lower()
419
436
  text = re.sub(r"http\S+", "", text)
420
437
  text = re.sub(r"[^a-zA-Z\s]", "", text)
438
+
421
439
  words = word_tokenize(text)
422
440
  words = [word for word in words if word not in self._stopwords]
441
+
423
442
  doc = self.nlp(" ".join(words))
424
443
  words = [t.lemma_ for t in doc if t.lemma_ != "-PRON-"]
444
+
425
445
  return " ".join(words)
426
446
 
427
447
  def analyze_sentiment(self, texts, lexicon=None, textblob=False) -> float:
@@ -460,7 +480,7 @@ class SentimentAnalyzer(object):
460
480
  return avg_sentiment
461
481
 
462
482
  def get_sentiment_for_tickers(
463
- self, tickers, lexicon=None, asset_type="stock", top_news=10, **kwargs
483
+ self, tickers: List[str] | List[Tuple[str, str]], lexicon=None, asset_type="stock", top_news=10, **kwargs
464
484
  ) -> Dict[str, float]:
465
485
  """
466
486
  Computes sentiment scores for a list of financial tickers based on news and social media data.
@@ -477,9 +497,18 @@ class SentimentAnalyzer(object):
477
497
  3. Computes an overall sentiment score using a weighted average approach.
478
498
 
479
499
  Args:
480
- tickers (list of str): A list of stock, forex, crypto, or other asset tickers.
500
+ tickers (List[str] | List[Tuple[str, str]]): A list of asset tickers to analyze
501
+ - if using tuples, the first element is the ticker and the second is the asset type.
502
+ - if using a single string, the asset type must be specified or the default is "stock".
481
503
  lexicon (dict, optional): A custom sentiment lexicon to update VADER's default lexicon.
482
- asset_type (str, optional): The type of asset (e.g., "stock", "forex", "crypto"). Defaults to "stock".
504
+ asset_type (str, optional): The type of asset, Defaults to "stock",
505
+ supported types include:
506
+ - "stock": Stock symbols (e.g., AAPL, MSFT)
507
+ - "etf": Exchange-traded funds (e.g., SPY, QQQ)
508
+ - "future": Futures contracts (e.g., CL=F for crude oil)
509
+ - "forex": Forex pairs (e.g., EURUSD=X, USDJPY=X)
510
+ - "crypto": Cryptocurrency pairs (e.g., BTC-USD, ETH-USD)
511
+ - "index": Stock market indices (e.g., ^GSPC for S&P 500)
483
512
  top_news (int, optional): Number of news articles/posts to fetch per source. Defaults to 10.
484
513
  **kwargs: Additional parameters for API authentication and data retrieval, including:
485
514
  - fmp_api (str): API key for Financial Modeling Prep.
@@ -490,63 +519,85 @@ class SentimentAnalyzer(object):
490
519
  - Positive values indicate positive sentiment.
491
520
  - Negative values indicate negative sentiment.
492
521
  - Zero indicates neutral sentiment.
522
+ Notes:
523
+ The tickers names must follow yahoo finance conventions.
493
524
  """
494
525
  sentiment_results = {}
495
- rd_params = ["client_id", "client_secret", "user_agent"]
496
- news = FinancialNews()
497
- for ticker in tickers:
498
- # Collect data
499
- sources = 0
500
- yahoo_news = news.get_yahoo_finance_news(
501
- ticker, asset_type=asset_type, n_news=top_news
502
- )
503
- google_news = news.get_google_finance_news(
504
- ticker, asset_type=asset_type, n_news=top_news
505
- )
506
- reddit_posts = news.get_reddit_posts(
507
- ticker, n_posts=top_news, **{k: kwargs.get(k) for k in rd_params}
508
- )
509
- coindesk_news = news.get_coindesk_news(query=ticker, list_of_str=True)
510
- fmp_source_news = []
511
- fmp_news = news.get_fmp_news(kwargs.get("fmp_api"))
512
- for source in ["articles"]: # , "releases", asset_type]:
513
- try:
514
- source_news = fmp_news.get_news(
515
- ticker, source=source, symbol=ticker, **kwargs
526
+ rd_params = {"client_id", "client_secret", "user_agent"}
527
+ fm_params = {"start", "end", "page", "limit"}
528
+ with open(os.devnull, 'w') as devnull:
529
+ with contextlib.redirect_stdout(devnull), contextlib.redirect_stderr(devnull):
530
+ for asset in tickers:
531
+ if isinstance(asset, tuple):
532
+ ticker, asset_type = asset
533
+ if asset_type not in [
534
+ "stock",
535
+ "etf",
536
+ "future",
537
+ "forex",
538
+ "crypto",
539
+ "index",
540
+ ]:
541
+ raise ValueError(
542
+ f"Unsupported asset type '{asset_type}'. "
543
+ "Supported types: stock, etf, future, forex, crypto, index."
544
+ )
545
+ # Collect data
546
+ sources = 0
547
+ yahoo_news = self.news.get_yahoo_finance_news(
548
+ ticker, asset_type=asset_type, n_news=top_news
549
+ )
550
+ google_news = self.news.get_google_finance_news(
551
+ ticker, asset_type=asset_type, n_news=top_news
552
+ )
553
+ reddit_posts = []
554
+ if all(kwargs.get(rd) for rd in rd_params):
555
+ reddit_posts = self.news.get_reddit_posts(
556
+ ticker, n_posts=top_news, **{k: kwargs.get(k) for k in rd_params}
557
+ )
558
+ coindesk_news = self.news.get_coindesk_news(query=ticker, list_of_str=True)
559
+ fmp_source_news = []
560
+ if kwargs.get("fmp_api"):
561
+ fmp_news = self.news.get_fmp_news(kwargs.get("fmp_api"))
562
+ for src in ["articles"]: # , "releases", asset_type]:
563
+ try:
564
+ source_news = fmp_news.get_news(
565
+ ticker, source=src, symbol=ticker, **{k: kwargs.get(k) for k in fm_params}
566
+ )
567
+ fmp_source_news += source_news
568
+ except Exception:
569
+ continue
570
+ if any([len(s) > 0 for s in [yahoo_news, google_news]]):
571
+ sources += 1
572
+ for source in [reddit_posts, fmp_source_news, coindesk_news]:
573
+ if len(source) > 0:
574
+ sources += 1
575
+ # Compute sentiment
576
+ news_sentiment = self.analyze_sentiment(
577
+ yahoo_news + google_news, lexicon=lexicon
578
+ )
579
+ reddit_sentiment = self.analyze_sentiment(
580
+ reddit_posts, lexicon=lexicon, textblob=True
581
+ )
582
+ fmp_sentiment = self.analyze_sentiment(
583
+ fmp_source_news, lexicon=lexicon, textblob=True
584
+ )
585
+ coindesk_sentiment = self.analyze_sentiment(
586
+ coindesk_news, lexicon=lexicon, textblob=True
516
587
  )
517
- fmp_source_news += source_news
518
- except Exception:
519
- source_news = []
520
- if any([len(s) > 0 for s in [yahoo_news, google_news]]):
521
- sources += 1
522
- for source in [reddit_posts, fmp_source_news, coindesk_news]:
523
- if len(source) > 0:
524
- sources += 1
525
- # Compute sentiment
526
- news_sentiment = self.analyze_sentiment(
527
- yahoo_news + google_news, lexicon=lexicon
528
- )
529
- reddit_sentiment = self.analyze_sentiment(
530
- reddit_posts, lexicon=lexicon, textblob=True
531
- )
532
- fmp_sentiment = self.analyze_sentiment(
533
- fmp_source_news, lexicon=lexicon, textblob=True
534
- )
535
- coindesk_sentiment = self.analyze_sentiment(
536
- coindesk_news, lexicon=lexicon, textblob=True
537
- )
538
588
 
539
- # Weighted average sentiment score
540
- if sources != 0:
541
- overall_sentiment = (
542
- news_sentiment
543
- + reddit_sentiment
544
- + fmp_sentiment
545
- + coindesk_sentiment
546
- ) / sources
547
- else:
548
- overall_sentiment = 0.0
549
- sentiment_results[ticker] = overall_sentiment
589
+ # Weighted average sentiment score
590
+ if sources != 0:
591
+ overall_sentiment = (
592
+ news_sentiment
593
+ + reddit_sentiment
594
+ + fmp_sentiment
595
+ + coindesk_sentiment
596
+ ) / sources
597
+ else:
598
+ overall_sentiment = 0.0
599
+ sentiment_results[ticker] = overall_sentiment
600
+ time.sleep(1) # To avoid hitting API rate limits
550
601
 
551
602
  return sentiment_results
552
603
 
@@ -641,8 +692,10 @@ class SentimentAnalyzer(object):
641
692
  bar and scatter plots. It fetches new sentiment data at specified intervals.
642
693
 
643
694
  Args:
644
- tickers (list[str]):
645
- A list of asset tickers (e.g., ["AAPL", "GOOGL", "TSLA"]).
695
+ tickers (List[str] | List[Tuple[str, str]]):
696
+ A list of financial asset tickers to analyze.
697
+ - If using tuples, the first element is the ticker and the second is the asset type.
698
+ - If using a single string, the asset type must be specified or defaults to "stock".
646
699
  asset_type (str, optional):
647
700
  The type of financial asset ("stock", "forex", "crypto"). Defaults to "stock".
648
701
  lexicon (dict, optional):
@@ -1,7 +1,8 @@
1
1
  import multiprocessing as mp
2
+ import sys
2
3
  import time
3
4
  from datetime import date, datetime
4
- from typing import Dict, List, Literal, Optional
5
+ from typing import Callable, Dict, List, Literal, Optional
5
6
 
6
7
  import pandas as pd
7
8
  from loguru import logger as log
@@ -9,7 +10,7 @@ from loguru import logger as log
9
10
  from bbstrader.btengine.strategy import MT5Strategy, Strategy
10
11
  from bbstrader.config import BBSTRADER_DIR
11
12
  from bbstrader.metatrader.account import Account, check_mt5_connection
12
- from bbstrader.metatrader.trade import Trade, TradeAction
13
+ from bbstrader.metatrader.trade import Trade, TradeAction, TradingMode
13
14
  from bbstrader.trading.utils import send_message
14
15
 
15
16
  try:
@@ -88,7 +89,7 @@ NON_EXEC_RETCODES = {
88
89
  log.add(
89
90
  f"{BBSTRADER_DIR}/logs/execution.log",
90
91
  enqueue=True,
91
- level="INFO",
92
+ level="DEBUG",
92
93
  format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
93
94
  )
94
95
 
@@ -171,9 +172,11 @@ class Mt5ExecutionEngine:
171
172
  self,
172
173
  symbol_list: List[str],
173
174
  trades_instances: Dict[str, Trade],
174
- strategy_cls: Strategy,
175
+ strategy_cls: Strategy | MT5Strategy,
175
176
  /,
176
177
  mm: bool = True,
178
+ auto_trade: bool = True,
179
+ prompt_callback: Callable = None,
177
180
  optimizer: str = "equal",
178
181
  trail: bool = True,
179
182
  stop_trail: Optional[int] = None,
@@ -187,7 +190,7 @@ class Mt5ExecutionEngine:
187
190
  closing_pnl: Optional[float] = None,
188
191
  trading_days: Optional[List[str]] = None,
189
192
  comment: Optional[str] = None,
190
- **kwargs
193
+ **kwargs,
191
194
  ):
192
195
  """
193
196
  Args:
@@ -197,6 +200,11 @@ class Mt5ExecutionEngine:
197
200
  mm : Enable Money Management. Defaults to True.
198
201
  optimizer : Risk management optimizer. Defaults to 'equal'.
199
202
  See `bbstrader.models.optimization` module for more information.
203
+ auto_trade : If set to true, when signal are generated by the strategy class,
204
+ the Execution engine will automaticaly open position in other whise it will prompt
205
+ the user for confimation.
206
+ prompt_callback : Callback function to prompt the user for confirmation.
207
+ This is useful when integrating with GUI applications.
200
208
  show_positions_orders : Print open positions and orders. Defaults to False.
201
209
  iter_time : Interval to check for signals and `mm`. Defaults to 5.
202
210
  use_trade_time : Open trades after the time is completed. Defaults to True.
@@ -239,6 +247,8 @@ class Mt5ExecutionEngine:
239
247
  self.trades_instances = trades_instances
240
248
  self.strategy_cls = strategy_cls
241
249
  self.mm = mm
250
+ self.auto_trade = auto_trade
251
+ self.prompt_callback = prompt_callback
242
252
  self.optimizer = optimizer
243
253
  self.trail = trail
244
254
  self.stop_trail = stop_trail
@@ -262,11 +272,12 @@ class Mt5ExecutionEngine:
262
272
 
263
273
  self._initialize_engine(**kwargs)
264
274
  self.strategy = self._init_strategy(**kwargs)
275
+ self._running = True
265
276
 
266
277
  def __repr__(self):
267
278
  trades = self.trades_instances.keys()
268
279
  strategy = self.strategy_cls.__name__
269
- return f"Mt5ExecutionEngine(Symbols={list(trades)}, Strategy={strategy})"
280
+ return f"{self.__class__.__name__}(Symbols={list(trades)}, Strategy={strategy})"
270
281
 
271
282
  def _initialize_engine(self, **kwargs):
272
283
  global logger
@@ -300,14 +311,14 @@ class Mt5ExecutionEngine:
300
311
  )
301
312
  return
302
313
 
303
- def _print_exc(self, msg, e: Exception):
314
+ def _print_exc(self, msg: str, e: Exception):
304
315
  if isinstance(e, KeyboardInterrupt):
305
316
  logger.info("Stopping the Execution Engine ...")
306
- quit()
317
+ sys.exit(0)
307
318
  if self.debug_mode:
308
319
  raise ValueError(msg).with_traceback(e.__traceback__)
309
320
  else:
310
- logger.error(msg)
321
+ logger.error(f"{msg, repr(e)}")
311
322
 
312
323
  def _max_trades(self, mtrades):
313
324
  max_trades = {
@@ -327,11 +338,11 @@ class Mt5ExecutionEngine:
327
338
  expert_ids = [expert_ids]
328
339
  return expert_ids
329
340
 
330
- def _init_strategy(self, **kwargs):
341
+ def _init_strategy(self, **kwargs) -> Strategy | MT5Strategy:
331
342
  try:
332
343
  check_mt5_connection(**kwargs)
333
- strategy: MT5Strategy = self.strategy_cls(
334
- symbol_list=self.symbols, mode="live", **kwargs
344
+ strategy = self.strategy_cls(
345
+ symbol_list=self.symbols, mode=TradingMode.LIVE, **kwargs
335
346
  )
336
347
  except Exception as e:
337
348
  self._print_exc(
@@ -357,32 +368,32 @@ class Mt5ExecutionEngine:
357
368
  }
358
369
 
359
370
  info = (
360
- "SIGNAL = {signal}, SYMBOL={symbol}, STRATEGY={strategy}, "
371
+ "SIGNAL={signal}, SYMBOL={symbol}, STRATEGY={strategy}, "
361
372
  "TIMEFRAME={timeframe}, ACCOUNT={account}"
362
373
  ).format(**common_data)
363
374
 
364
375
  sigmsg = (
365
- "SIGNAL = {signal},\n"
366
- "SYMBOL = {symbol},\n"
367
- "TYPE = {symbol_type},\n"
368
- "DESCRIPTION = {description},\n"
369
- "PRICE = {price},\n"
370
- "STOPLIMIT = {stoplimit},\n"
371
- "STRATEGY = {strategy},\n"
372
- "TIMEFRAME = {timeframe},\n"
373
- "BROKER = {broker},\n"
374
- "TIMESTAMP = {timestamp}"
376
+ "SIGNAL={signal}\n"
377
+ "SYMBOL={symbol}\n"
378
+ "TYPE={symbol_type}\n"
379
+ "DESCRIPTION={description}\n"
380
+ "PRICE={price}\n"
381
+ "STOPLIMIT={stoplimit}\n"
382
+ "STRATEGY={strategy}\n"
383
+ "TIMEFRAME={timeframe}\n"
384
+ "BROKER={broker}\n"
385
+ "TIMESTAMP={timestamp}"
375
386
  ).format(
376
387
  **common_data,
377
- symbol_type=account.get_symbol_type(symbol),
378
- description=symbol_info.description,
379
- price=price,
388
+ symbol_type=account.get_symbol_type(symbol).value,
389
+ description=symbol_info.description if symbol_info else "N/A",
390
+ price=price if price else "MARKET",
380
391
  stoplimit=stoplimit,
381
392
  broker=account.broker.name,
382
393
  timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
383
394
  )
384
395
 
385
- msg_template = "SYMBOL = {symbol}, STRATEGY = {strategy}, ACCOUNT = {account}"
396
+ msg_template = "SYMBOL={symbol}, STRATEGY={strategy}, ACCOUNT={account}"
386
397
  msg = f"Sending {signal} Order ... " + msg_template.format(**common_data)
387
398
  tfmsg = "Time Frame Not completed !!! " + msg_template.format(**common_data)
388
399
  riskmsg = "Risk not allowed !!! " + msg_template.format(**common_data)
@@ -515,7 +526,7 @@ class Mt5ExecutionEngine:
515
526
  def _daily_end_checks(self, today, closing, sessionmsg):
516
527
  self.strategy.perform_period_end_checks()
517
528
  if self.period_end_action == "break" and closing:
518
- exit(0)
529
+ sys.exit(0)
519
530
  elif self.period_end_action == "sleep" and today != FRIDAY or not closing:
520
531
  self._sleep_over_night(sessionmsg)
521
532
  elif self.period_end_action == "sleep" and today == FRIDAY:
@@ -527,7 +538,7 @@ class Mt5ExecutionEngine:
527
538
  elif today == FRIDAY:
528
539
  self.strategy.perform_period_end_checks()
529
540
  if self.period_end_action == "break" and closing:
530
- exit(0)
541
+ sys.exit(0)
531
542
  elif self.period_end_action == "sleep" or not closing:
532
543
  self._sleep_over_weekend(sessionmsg)
533
544
 
@@ -536,7 +547,7 @@ class Mt5ExecutionEngine:
536
547
  self._sleep_over_night(sessionmsg)
537
548
  elif today == FRIDAY and self._is_month_ends() and closing:
538
549
  self.strategy.perform_period_end_checks()
539
- exit(0)
550
+ sys.exit(0)
540
551
  else:
541
552
  self._sleep_over_weekend(sessionmsg)
542
553
 
@@ -616,11 +627,30 @@ class Mt5ExecutionEngine:
616
627
  )
617
628
  pass
618
629
 
630
+ def _handle_auto_trade(self, sigmsg, symbol) -> bool:
631
+ if self.notify:
632
+ self._send_notification(sigmsg, symbol)
633
+ if self.auto_trade:
634
+ return True
635
+ if not self.auto_trade:
636
+ prompt = f"{sigmsg} \n Enter Y/Yes to accept or N/No to reject this order : "
637
+ if self.prompt_callback is not None:
638
+ auto_trade = self.prompt_callback(prompt)
639
+ else:
640
+ auto_trade = input(prompt)
641
+ if not auto_trade.upper().startswith("Y"):
642
+ info = f"Order Rejected !!! SYMBOL={symbol}, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
643
+ logger.info(info)
644
+ if self.notify:
645
+ self._send_notification(info, symbol)
646
+ return False
647
+ return True
648
+
619
649
  def _open_buy(
620
650
  self, signal, symbol, id, trade: Trade, price, stoplimit, sigmsg, msg, comment
621
651
  ):
622
- if self.notify:
623
- self._send_notification(sigmsg, symbol)
652
+ if not self._handle_auto_trade(sigmsg, symbol):
653
+ return
624
654
  if not self._check_retcode(trade, "BMKT"):
625
655
  logger.info(msg)
626
656
  trade.open_buy_position(
@@ -636,8 +666,8 @@ class Mt5ExecutionEngine:
636
666
  def _open_sell(
637
667
  self, signal, symbol, id, trade: Trade, price, stoplimit, sigmsg, msg, comment
638
668
  ):
639
- if self.notify:
640
- self._send_notification(sigmsg, symbol)
669
+ if not self._handle_auto_trade(sigmsg, symbol):
670
+ return
641
671
  if not self._check_retcode(trade, "SMKT"):
642
672
  logger.info(msg)
643
673
  trade.open_sell_position(
@@ -888,7 +918,7 @@ class Mt5ExecutionEngine:
888
918
  pass
889
919
 
890
920
  def run(self):
891
- while True:
921
+ while self._running:
892
922
  try:
893
923
  check_mt5_connection(**self.kwargs)
894
924
  positions_orders = self._check_positions_orders()
@@ -910,13 +940,21 @@ class Mt5ExecutionEngine:
910
940
  self._sleep()
911
941
  self._handle_period_end_actions(today)
912
942
  except KeyboardInterrupt:
913
- logger.info("Stopping the Execution Engine ...")
943
+ logger.info(
944
+ f"Stopping Execution Engine for {self.STRATEGY} on {self.ACCOUNT}"
945
+ )
914
946
  break
915
947
  except Exception as e:
916
948
  msg = f"Running Execution Engine, STRATEGY={self.STRATEGY} , ACCOUNT={self.ACCOUNT}"
917
949
  self._print_exc(msg, e)
918
950
  continue
919
951
 
952
+ def stop(self):
953
+ """Stops the execution engine."""
954
+ self._running = False
955
+ logger.info(f"Stopping Execution Engine for {self.STRATEGY} on {self.ACCOUNT}")
956
+ logger.info("Execution Engine stopped successfully.")
957
+
920
958
 
921
959
  def RunMt5Engine(account_id: str, **kwargs):
922
960
  """Starts an MT5 execution engine for a given account.
@@ -31,6 +31,7 @@ from bbstrader.btengine.execution import MT5ExecutionHandler, SimExecutionHandle
31
31
  from bbstrader.btengine.strategy import Strategy
32
32
  from bbstrader.metatrader.account import Account
33
33
  from bbstrader.metatrader.rates import Rates
34
+ from bbstrader.metatrader.trade import TradingMode
34
35
  from bbstrader.models.risk import build_hmm_models
35
36
  from bbstrader.tseries import ArimaGarchModel, KalmanFilterModel
36
37
 
@@ -73,7 +74,7 @@ class SMAStrategy(Strategy):
73
74
  bars: DataHandler = None,
74
75
  events: Queue = None,
75
76
  symbol_list: List[str] = None,
76
- mode: Literal["backtest", "live"] = "backtest",
77
+ mode: TradingMode = TradingMode.BACKTEST,
77
78
  **kwargs,
78
79
  ):
79
80
  """
@@ -81,7 +82,7 @@ class SMAStrategy(Strategy):
81
82
  bars (DataHandler): A data handler object that provides market data.
82
83
  events (Queue): An event queue object where generated signals are placed.
83
84
  symbol_list (List[str]): A list of symbols to consider for trading.
84
- mode (Literal['backtest', 'live']): The mode of operation for the strategy.
85
+ mode TradingMode: The mode of operation for the strategy.
85
86
  short_window (int, optional): The period for the short moving average.
86
87
  long_window (int, optional): The period for the long moving average.
87
88
  time_frame (str, optional): The time frame for the data.
@@ -196,13 +197,13 @@ class SMAStrategy(Strategy):
196
197
  return signals
197
198
 
198
199
  def calculate_signals(self, event=None):
199
- if self.mode == "backtest" and event is not None:
200
+ if self.mode == TradingMode.BACKTEST and event is not None:
200
201
  if event.type == Events.MARKET:
201
202
  signals = self.create_backtest_signals()
202
203
  for signal in signals.values():
203
204
  if signal is not None:
204
205
  self.events.put(signal)
205
- elif self.mode == "live":
206
+ elif self.mode == TradingMode.LIVE:
206
207
  signals = self.create_live_signals()
207
208
  return signals
208
209
 
@@ -238,7 +239,7 @@ class ArimaGarchStrategy(Strategy):
238
239
  bars: DataHandler = None,
239
240
  events: Queue = None,
240
241
  symbol_list: List[str] = None,
241
- mode: Literal["backtest", "live"] = "backtest",
242
+ mode: TradingMode = TradingMode.BACKTEST,
242
243
  **kwargs,
243
244
  ):
244
245
  """
@@ -384,13 +385,13 @@ class ArimaGarchStrategy(Strategy):
384
385
  return signals
385
386
 
386
387
  def calculate_signals(self, event=None):
387
- if self.mode == "backtest" and event is not None:
388
+ if self.mode == TradingMode.BACKTEST and event is not None:
388
389
  if event.type == Events.MARKET:
389
390
  signals = self.create_backtest_signal()
390
391
  for signal in signals.values():
391
392
  if signal is not None:
392
393
  self.events.put(signal)
393
- elif self.mode == "live":
394
+ elif self.mode == TradingMode.LIVE:
394
395
  return self.create_live_signals()
395
396
 
396
397
 
@@ -408,7 +409,7 @@ class KalmanFilterStrategy(Strategy):
408
409
  bars: DataHandler = None,
409
410
  events: Queue = None,
410
411
  symbol_list: List[str] = None,
411
- mode: Literal["backtest", "live"] = "backtest",
412
+ mode: TradingMode = TradingMode.BACKTEST,
412
413
  **kwargs,
413
414
  ):
414
415
  """
@@ -567,10 +568,10 @@ class KalmanFilterStrategy(Strategy):
567
568
  """
568
569
  Calculate the Kalman Filter strategy.
569
570
  """
570
- if self.mode == "backtest" and event is not None:
571
+ if self.mode == TradingMode.BACKTEST and event is not None:
571
572
  if event.type == Events.MARKET:
572
573
  self.calculate_backtest_signals()
573
- elif self.mode == "live":
574
+ elif self.mode == TradingMode.LIVE:
574
575
  return self.calculate_live_signals()
575
576
 
576
577
 
@@ -589,7 +590,7 @@ class StockIndexSTBOTrading(Strategy):
589
590
  bars: DataHandler = None,
590
591
  events: Queue = None,
591
592
  symbol_list: List[str] = None,
592
- mode: Literal["backtest", "live"] = "backtest",
593
+ mode: TradingMode = TradingMode.BACKTEST,
593
594
  **kwargs,
594
595
  ):
595
596
  """
@@ -632,7 +633,7 @@ class StockIndexSTBOTrading(Strategy):
632
633
  self.heightest_price = {index: None for index in symbols}
633
634
  self.lowerst_price = {index: None for index in symbols}
634
635
 
635
- if self.mode == "backtest":
636
+ if self.mode == TradingMode.BACKTEST:
636
637
  self.qty = get_quantities(quantities, symbols)
637
638
  self.num_buys = {index: 0 for index in symbols}
638
639
  self.buy_prices = {index: [] for index in symbols}
@@ -751,10 +752,10 @@ class StockIndexSTBOTrading(Strategy):
751
752
  self.buy_prices[index] = []
752
753
 
753
754
  def calculate_signals(self, event=None) -> Dict[str, Union[str, None]]:
754
- if self.mode == "backtest" and event is not None:
755
+ if self.mode == TradingMode.BACKTEST and event is not None:
755
756
  if event.type == Events.MARKET:
756
757
  self.calculate_backtest_signals()
757
- elif self.mode == "live":
758
+ elif self.mode == TradingMode.LIVE:
758
759
  return self.calculate_live_signals()
759
760
 
760
761