bbstrader 0.3.0__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 +17 -13
- bbstrader/core/data.py +92 -29
- bbstrader/metatrader/account.py +4 -10
- bbstrader/metatrader/copier.py +112 -36
- bbstrader/metatrader/scripts.py +26 -12
- bbstrader/metatrader/trade.py +27 -32
- bbstrader/metatrader/utils.py +1 -0
- bbstrader/models/nlp.py +117 -74
- bbstrader/trading/execution.py +58 -37
- {bbstrader-0.3.0.dist-info → bbstrader-0.3.1.dist-info}/METADATA +2 -5
- {bbstrader-0.3.0.dist-info → bbstrader-0.3.1.dist-info}/RECORD +16 -16
- {bbstrader-0.3.0.dist-info → bbstrader-0.3.1.dist-info}/WHEEL +0 -0
- {bbstrader-0.3.0.dist-info → bbstrader-0.3.1.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.3.0.dist-info → bbstrader-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {bbstrader-0.3.0.dist-info → bbstrader-0.3.1.dist-info}/top_level.txt +0 -0
bbstrader/metatrader/scripts.py
CHANGED
|
@@ -2,9 +2,18 @@ import argparse
|
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
4
|
from bbstrader.metatrader.copier import RunCopier, config_copier
|
|
5
|
+
from bbstrader.apps._copier import main as RunCopyAPP
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def copier_args(parser: argparse.ArgumentParser):
|
|
9
|
+
parser.add_argument(
|
|
10
|
+
"-m",
|
|
11
|
+
"--mode",
|
|
12
|
+
type=str,
|
|
13
|
+
default="CLI",
|
|
14
|
+
choices=("CLI", "GUI"),
|
|
15
|
+
help="Run the copier in the terminal or using the GUI",
|
|
16
|
+
)
|
|
8
17
|
parser.add_argument(
|
|
9
18
|
"-s", "--source", type=str, nargs="?", default=None, help="Source section name"
|
|
10
19
|
)
|
|
@@ -52,6 +61,7 @@ def copy_trades(unknown):
|
|
|
52
61
|
python -m bbstrader --run copier [options]
|
|
53
62
|
|
|
54
63
|
Options:
|
|
64
|
+
-m, --mode: CLI for terminal app and GUI for Desktop app
|
|
55
65
|
-s, --source: Source Account section name
|
|
56
66
|
-d, --destinations: Destination Account section names (multiple allowed)
|
|
57
67
|
-i, --interval: Update interval in seconds
|
|
@@ -67,15 +77,19 @@ def copy_trades(unknown):
|
|
|
67
77
|
copy_parser = copier_args(copy_parser)
|
|
68
78
|
copy_args = copy_parser.parse_args(unknown)
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
if copy_args.mode == "GUI":
|
|
81
|
+
RunCopyAPP()
|
|
82
|
+
|
|
83
|
+
elif copy_args.mode == "CLI":
|
|
84
|
+
source, destinations = config_copier(
|
|
85
|
+
source_section=copy_args.source,
|
|
86
|
+
dest_sections=copy_args.destinations,
|
|
87
|
+
inifile=copy_args.config,
|
|
88
|
+
)
|
|
89
|
+
RunCopier(
|
|
90
|
+
source,
|
|
91
|
+
destinations,
|
|
92
|
+
copy_args.interval,
|
|
93
|
+
copy_args.start,
|
|
94
|
+
copy_args.end,
|
|
95
|
+
)
|
bbstrader/metatrader/trade.py
CHANGED
|
@@ -134,15 +134,17 @@ class TradeSignal:
|
|
|
134
134
|
def __repr__(self):
|
|
135
135
|
return (
|
|
136
136
|
f"TradeSignal(id={self.id}, symbol='{self.symbol}', action='{self.action.value}', "
|
|
137
|
-
f"price={self.price}, stoplimit={self.stoplimit}
|
|
137
|
+
f"price={self.price}, stoplimit={self.stoplimit}, comment='{self.comment or ''}')"
|
|
138
138
|
)
|
|
139
139
|
|
|
140
|
+
|
|
140
141
|
class TradingMode(Enum):
|
|
141
142
|
BACKTEST = "BACKTEST"
|
|
142
143
|
LIVE = "LIVE"
|
|
143
144
|
|
|
144
145
|
def isbacktest(self) -> bool:
|
|
145
146
|
return self == TradingMode.BACKTEST
|
|
147
|
+
|
|
146
148
|
def islive(self) -> bool:
|
|
147
149
|
return self == TradingMode.LIVE
|
|
148
150
|
|
|
@@ -740,10 +742,8 @@ class Trade(RiskManagement):
|
|
|
740
742
|
self.check_order(request)
|
|
741
743
|
result = self.send_order(request)
|
|
742
744
|
except Exception as e:
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
746
|
-
)
|
|
745
|
+
msg = trade_retcode_message(result.retcode)
|
|
746
|
+
LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
|
|
747
747
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
748
748
|
if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
|
|
749
749
|
for fill in FILLING_TYPE:
|
|
@@ -773,10 +773,8 @@ class Trade(RiskManagement):
|
|
|
773
773
|
self.check_order(request)
|
|
774
774
|
result = self.send_order(request)
|
|
775
775
|
except Exception as e:
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
779
|
-
)
|
|
776
|
+
msg = trade_retcode_message(result.retcode)
|
|
777
|
+
LOGGER.error(f"Trade Order Request, {msg}{addtionnal}, {e}")
|
|
780
778
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
781
779
|
break
|
|
782
780
|
tries += 1
|
|
@@ -787,7 +785,7 @@ class Trade(RiskManagement):
|
|
|
787
785
|
if type != "BMKT" or type != "SMKT":
|
|
788
786
|
self.opened_orders.append(result.order)
|
|
789
787
|
long_msg = (
|
|
790
|
-
f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{price}, "
|
|
788
|
+
f"1. {pos} Order #{result.order} Sent, Symbol: {self.symbol}, Price: @{round(price, 5)}, "
|
|
791
789
|
f"Lot(s): {result.volume}, Sl: {self.get_stop_loss()}, "
|
|
792
790
|
f"Tp: {self.get_take_profit()}"
|
|
793
791
|
)
|
|
@@ -808,7 +806,7 @@ class Trade(RiskManagement):
|
|
|
808
806
|
profit = round(self.get_account_info().profit, 5)
|
|
809
807
|
order_info = (
|
|
810
808
|
f"2. {order_type} Position Opened, Symbol: {self.symbol}, Price: @{round(position.price_open, 5)}, "
|
|
811
|
-
f"Sl: @{position.sl} Tp: @{position.tp}"
|
|
809
|
+
f"Sl: @{round(position.sl, 5)} Tp: @{round(position.tp, 5)}"
|
|
812
810
|
)
|
|
813
811
|
LOGGER.info(order_info)
|
|
814
812
|
pos_info = (
|
|
@@ -1294,10 +1292,8 @@ class Trade(RiskManagement):
|
|
|
1294
1292
|
self.check_order(request)
|
|
1295
1293
|
result = self.send_order(request)
|
|
1296
1294
|
except Exception as e:
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
1300
|
-
)
|
|
1295
|
+
msg = trade_retcode_message(result.retcode)
|
|
1296
|
+
LOGGER.error(f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}")
|
|
1301
1297
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1302
1298
|
msg = trade_retcode_message(result.retcode)
|
|
1303
1299
|
if result.retcode != Mt5.TRADE_RETCODE_NO_CHANGES:
|
|
@@ -1314,17 +1310,15 @@ class Trade(RiskManagement):
|
|
|
1314
1310
|
self.check_order(request)
|
|
1315
1311
|
result = self.send_order(request)
|
|
1316
1312
|
except Exception as e:
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
1320
|
-
)
|
|
1313
|
+
msg = trade_retcode_message(result.retcode)
|
|
1314
|
+
LOGGER.error(f"Break-Even Order Request, {msg}{addtionnal}, Error: {e}")
|
|
1321
1315
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1322
1316
|
break
|
|
1323
1317
|
tries += 1
|
|
1324
1318
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1325
1319
|
msg = trade_retcode_message(result.retcode)
|
|
1326
1320
|
LOGGER.info(f"Break-Even Order {msg}{addtionnal}")
|
|
1327
|
-
info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{price}"
|
|
1321
|
+
info = f"Stop loss set to Break-even, Position: #{tiket}, Symbol: {self.symbol}, Price: @{round(price, 5)}"
|
|
1328
1322
|
LOGGER.info(info)
|
|
1329
1323
|
self.break_even_status.append(tiket)
|
|
1330
1324
|
|
|
@@ -1402,10 +1396,8 @@ class Trade(RiskManagement):
|
|
|
1402
1396
|
self.check_order(request)
|
|
1403
1397
|
result = self.send_order(request)
|
|
1404
1398
|
except Exception as e:
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
1408
|
-
)
|
|
1399
|
+
msg = trade_retcode_message(result.retcode)
|
|
1400
|
+
LOGGER.error(f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}")
|
|
1409
1401
|
if result.retcode != Mt5.TRADE_RETCODE_DONE:
|
|
1410
1402
|
if result.retcode == Mt5.TRADE_RETCODE_INVALID_FILL: # 10030
|
|
1411
1403
|
for fill in FILLING_TYPE:
|
|
@@ -1417,7 +1409,8 @@ class Trade(RiskManagement):
|
|
|
1417
1409
|
self._retcodes.append(result.retcode)
|
|
1418
1410
|
msg = trade_retcode_message(result.retcode)
|
|
1419
1411
|
LOGGER.error(
|
|
1420
|
-
f"Closing Order Request, {type.capitalize()}: #{ticket},
|
|
1412
|
+
f"Closing Order Request, {type.capitalize()}: #{ticket}, "
|
|
1413
|
+
f"RETCODE={result.retcode}: {msg}{addtionnal}"
|
|
1421
1414
|
)
|
|
1422
1415
|
else:
|
|
1423
1416
|
tries = 0
|
|
@@ -1427,17 +1420,18 @@ class Trade(RiskManagement):
|
|
|
1427
1420
|
self.check_order(request)
|
|
1428
1421
|
result = self.send_order(request)
|
|
1429
1422
|
except Exception as e:
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
result.retcode, display=True, add_msg=f"{e}{addtionnal}"
|
|
1433
|
-
)
|
|
1423
|
+
msg = trade_retcode_message(result.retcode)
|
|
1424
|
+
LOGGER.error(f"Closing {type.capitalize()} Request, {msg}{addtionnal}, Error: {e}")
|
|
1434
1425
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1435
1426
|
break
|
|
1436
1427
|
tries += 1
|
|
1437
1428
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1438
1429
|
msg = trade_retcode_message(result.retcode)
|
|
1439
1430
|
LOGGER.info(f"Closing Order {msg}{addtionnal}")
|
|
1440
|
-
info =
|
|
1431
|
+
info = (
|
|
1432
|
+
f"{type.capitalize()} #{ticket} closed, Symbol: {self.symbol},"
|
|
1433
|
+
f"Price: @{round(request.get('price', 0.0), 5)}"
|
|
1434
|
+
)
|
|
1441
1435
|
LOGGER.info(info)
|
|
1442
1436
|
return True
|
|
1443
1437
|
else:
|
|
@@ -1466,7 +1460,7 @@ class Trade(RiskManagement):
|
|
|
1466
1460
|
orders = self.get_orders(ticket=ticket) or []
|
|
1467
1461
|
if len(orders) == 0:
|
|
1468
1462
|
LOGGER.error(
|
|
1469
|
-
f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={price}"
|
|
1463
|
+
f"Order #{ticket} not found, SYMBOL={self.symbol}, PRICE={round(price, 5)}"
|
|
1470
1464
|
)
|
|
1471
1465
|
return
|
|
1472
1466
|
order = orders[0]
|
|
@@ -1482,7 +1476,8 @@ class Trade(RiskManagement):
|
|
|
1482
1476
|
result = self.send_order(request)
|
|
1483
1477
|
if result.retcode == Mt5.TRADE_RETCODE_DONE:
|
|
1484
1478
|
LOGGER.info(
|
|
1485
|
-
f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={price
|
|
1479
|
+
f"Order #{ticket} modified, SYMBOL={self.symbol}, PRICE={round(price, 5)},"
|
|
1480
|
+
f"SL={round(sl, 5)}, TP={round(tp, 5)}, STOP_LIMIT={round(stoplimit, 5)}"
|
|
1486
1481
|
)
|
|
1487
1482
|
else:
|
|
1488
1483
|
msg = trade_retcode_message(result.retcode)
|
bbstrader/metatrader/utils.py
CHANGED
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",
|
|
@@ -335,15 +338,14 @@ class TopicModeler(object):
|
|
|
335
338
|
nltk.download("stopwords", quiet=True)
|
|
336
339
|
|
|
337
340
|
try:
|
|
338
|
-
self.nlp =
|
|
341
|
+
self.nlp = en_core_web_sm.load()
|
|
339
342
|
self.nlp.disable_pipes("ner")
|
|
340
343
|
except OSError:
|
|
341
|
-
raise
|
|
342
|
-
"
|
|
343
|
-
"Please install it
|
|
344
|
-
" python -m spacy download en_core_web_sm"
|
|
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'."
|
|
345
347
|
)
|
|
346
|
-
|
|
348
|
+
|
|
347
349
|
def preprocess_texts(self, texts: list[str]):
|
|
348
350
|
def clean_doc(Doc):
|
|
349
351
|
doc = []
|
|
@@ -392,22 +394,25 @@ class SentimentAnalyzer(object):
|
|
|
392
394
|
- Downloads NLTK tokenization (`punkt`) and stopwords.
|
|
393
395
|
- Loads the `en_core_web_sm` SpaCy model with Named Entity Recognition (NER) disabled.
|
|
394
396
|
- Initializes VADER's SentimentIntensityAnalyzer for sentiment scoring.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
use_spacy (bool): If True, uses SpaCy for lemmatization. Defaults to False.
|
|
395
400
|
"""
|
|
396
401
|
nltk.download("punkt", quiet=True)
|
|
397
402
|
nltk.download("stopwords", quiet=True)
|
|
398
403
|
|
|
404
|
+
self.analyzer = SentimentIntensityAnalyzer()
|
|
405
|
+
self._stopwords = set(stopwords.words("english"))
|
|
406
|
+
|
|
399
407
|
try:
|
|
400
|
-
self.nlp =
|
|
408
|
+
self.nlp = en_core_web_sm.load()
|
|
401
409
|
self.nlp.disable_pipes("ner")
|
|
402
410
|
except OSError:
|
|
403
|
-
raise
|
|
404
|
-
"
|
|
405
|
-
"Please install it
|
|
406
|
-
" 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'."
|
|
407
414
|
)
|
|
408
|
-
|
|
409
|
-
self.analyzer = SentimentIntensityAnalyzer()
|
|
410
|
-
self._stopwords = set(stopwords.words("english"))
|
|
415
|
+
self.news = FinancialNews()
|
|
411
416
|
|
|
412
417
|
def preprocess_text(self, text: str):
|
|
413
418
|
"""
|
|
@@ -425,13 +430,18 @@ class SentimentAnalyzer(object):
|
|
|
425
430
|
Returns:
|
|
426
431
|
str: The cleaned and lemmatized text.
|
|
427
432
|
"""
|
|
433
|
+
if not isinstance(text, str):
|
|
434
|
+
raise ValueError(f"{self.__class__.__name__}: preprocess_text expects a string, got {type(text)}")
|
|
428
435
|
text = text.lower()
|
|
429
436
|
text = re.sub(r"http\S+", "", text)
|
|
430
437
|
text = re.sub(r"[^a-zA-Z\s]", "", text)
|
|
438
|
+
|
|
431
439
|
words = word_tokenize(text)
|
|
432
440
|
words = [word for word in words if word not in self._stopwords]
|
|
441
|
+
|
|
433
442
|
doc = self.nlp(" ".join(words))
|
|
434
443
|
words = [t.lemma_ for t in doc if t.lemma_ != "-PRON-"]
|
|
444
|
+
|
|
435
445
|
return " ".join(words)
|
|
436
446
|
|
|
437
447
|
def analyze_sentiment(self, texts, lexicon=None, textblob=False) -> float:
|
|
@@ -470,7 +480,7 @@ class SentimentAnalyzer(object):
|
|
|
470
480
|
return avg_sentiment
|
|
471
481
|
|
|
472
482
|
def get_sentiment_for_tickers(
|
|
473
|
-
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
|
|
474
484
|
) -> Dict[str, float]:
|
|
475
485
|
"""
|
|
476
486
|
Computes sentiment scores for a list of financial tickers based on news and social media data.
|
|
@@ -487,9 +497,18 @@ class SentimentAnalyzer(object):
|
|
|
487
497
|
3. Computes an overall sentiment score using a weighted average approach.
|
|
488
498
|
|
|
489
499
|
Args:
|
|
490
|
-
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".
|
|
491
503
|
lexicon (dict, optional): A custom sentiment lexicon to update VADER's default lexicon.
|
|
492
|
-
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)
|
|
493
512
|
top_news (int, optional): Number of news articles/posts to fetch per source. Defaults to 10.
|
|
494
513
|
**kwargs: Additional parameters for API authentication and data retrieval, including:
|
|
495
514
|
- fmp_api (str): API key for Financial Modeling Prep.
|
|
@@ -500,63 +519,85 @@ class SentimentAnalyzer(object):
|
|
|
500
519
|
- Positive values indicate positive sentiment.
|
|
501
520
|
- Negative values indicate negative sentiment.
|
|
502
521
|
- Zero indicates neutral sentiment.
|
|
522
|
+
Notes:
|
|
523
|
+
The tickers names must follow yahoo finance conventions.
|
|
503
524
|
"""
|
|
504
525
|
sentiment_results = {}
|
|
505
|
-
rd_params =
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
|
526
587
|
)
|
|
527
|
-
fmp_source_news += source_news
|
|
528
|
-
except Exception:
|
|
529
|
-
source_news = []
|
|
530
|
-
if any([len(s) > 0 for s in [yahoo_news, google_news]]):
|
|
531
|
-
sources += 1
|
|
532
|
-
for source in [reddit_posts, fmp_source_news, coindesk_news]:
|
|
533
|
-
if len(source) > 0:
|
|
534
|
-
sources += 1
|
|
535
|
-
# Compute sentiment
|
|
536
|
-
news_sentiment = self.analyze_sentiment(
|
|
537
|
-
yahoo_news + google_news, lexicon=lexicon
|
|
538
|
-
)
|
|
539
|
-
reddit_sentiment = self.analyze_sentiment(
|
|
540
|
-
reddit_posts, lexicon=lexicon, textblob=True
|
|
541
|
-
)
|
|
542
|
-
fmp_sentiment = self.analyze_sentiment(
|
|
543
|
-
fmp_source_news, lexicon=lexicon, textblob=True
|
|
544
|
-
)
|
|
545
|
-
coindesk_sentiment = self.analyze_sentiment(
|
|
546
|
-
coindesk_news, lexicon=lexicon, textblob=True
|
|
547
|
-
)
|
|
548
588
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
|
560
601
|
|
|
561
602
|
return sentiment_results
|
|
562
603
|
|
|
@@ -651,8 +692,10 @@ class SentimentAnalyzer(object):
|
|
|
651
692
|
bar and scatter plots. It fetches new sentiment data at specified intervals.
|
|
652
693
|
|
|
653
694
|
Args:
|
|
654
|
-
tickers (
|
|
655
|
-
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".
|
|
656
699
|
asset_type (str, optional):
|
|
657
700
|
The type of financial asset ("stock", "forex", "crypto"). Defaults to "stock".
|
|
658
701
|
lexicon (dict, optional):
|