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/__init__.py +1 -1
- bbstrader/__main__.py +35 -14
- bbstrader/btengine/data.py +4 -2
- bbstrader/btengine/execution.py +24 -14
- bbstrader/btengine/strategy.py +33 -15
- bbstrader/core/data.py +93 -30
- bbstrader/core/scripts.py +130 -0
- bbstrader/metatrader/account.py +115 -126
- bbstrader/metatrader/copier.py +114 -39
- bbstrader/metatrader/rates.py +14 -13
- bbstrader/metatrader/risk.py +13 -11
- bbstrader/metatrader/scripts.py +26 -12
- bbstrader/metatrader/trade.py +60 -54
- bbstrader/metatrader/utils.py +80 -26
- bbstrader/models/factors.py +3 -1
- bbstrader/models/ml.py +2 -1
- bbstrader/models/nlp.py +123 -70
- bbstrader/trading/execution.py +74 -36
- bbstrader/trading/strategies.py +15 -14
- bbstrader/tseries.py +8 -9
- bbstrader-0.3.1.dist-info/METADATA +466 -0
- bbstrader-0.3.1.dist-info/RECORD +47 -0
- {bbstrader-0.2.991.dist-info → bbstrader-0.3.1.dist-info}/WHEEL +1 -1
- bbstrader/__ini__.py +0 -20
- bbstrader-0.2.991.dist-info/METADATA +0 -191
- bbstrader-0.2.991.dist-info/RECORD +0 -47
- {bbstrader-0.2.991.dist-info → bbstrader-0.3.1.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.991.dist-info → bbstrader-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.2.991.dist-info → bbstrader-0.3.1.dist-info}/top_level.txt +0 -0
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
|
|
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
|
-
|
|
335
|
-
|
|
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 =
|
|
408
|
+
self.nlp = en_core_web_sm.load()
|
|
391
409
|
self.nlp.disable_pipes("ner")
|
|
392
410
|
except OSError:
|
|
393
|
-
raise
|
|
394
|
-
"
|
|
395
|
-
"Please install it
|
|
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 (
|
|
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
|
|
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 =
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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 (
|
|
645
|
-
A list of asset tickers
|
|
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):
|
bbstrader/trading/execution.py
CHANGED
|
@@ -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="
|
|
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"
|
|
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
|
-
|
|
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
|
|
334
|
-
symbol_list=self.symbols, mode=
|
|
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
|
|
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
|
|
366
|
-
"SYMBOL
|
|
367
|
-
"TYPE
|
|
368
|
-
"DESCRIPTION
|
|
369
|
-
"PRICE
|
|
370
|
-
"STOPLIMIT
|
|
371
|
-
"STRATEGY
|
|
372
|
-
"TIMEFRAME
|
|
373
|
-
"BROKER
|
|
374
|
-
"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
|
|
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.
|
|
623
|
-
|
|
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.
|
|
640
|
-
|
|
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
|
|
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(
|
|
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.
|
bbstrader/trading/strategies.py
CHANGED
|
@@ -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:
|
|
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
|
|
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 ==
|
|
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 ==
|
|
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:
|
|
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 ==
|
|
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 ==
|
|
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:
|
|
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 ==
|
|
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 ==
|
|
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:
|
|
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 ==
|
|
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 ==
|
|
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 ==
|
|
758
|
+
elif self.mode == TradingMode.LIVE:
|
|
758
759
|
return self.calculate_live_signals()
|
|
759
760
|
|
|
760
761
|
|