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

@@ -0,0 +1,784 @@
1
+ import re
2
+ import time
3
+ from datetime import datetime
4
+ from typing import Dict
5
+
6
+ import dash
7
+ import matplotlib.pyplot as plt
8
+ import nltk
9
+ import pandas as pd
10
+ import plotly.express as px
11
+ import spacy
12
+ from dash import dcc, html
13
+ from dash.dependencies import Input, Output
14
+ from nltk.corpus import stopwords
15
+ from nltk.tokenize import word_tokenize
16
+ from textblob import TextBlob
17
+ from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
18
+
19
+ from bbstrader.core.data import FinancialNews
20
+
21
+ __all__ = [
22
+ "TopicModeler",
23
+ "SentimentAnalyzer",
24
+ "EQUITY_LEXICON",
25
+ "FOREX_LEXICON",
26
+ "COMMODITIES_LEXICON",
27
+ "CRYPTO_LEXICON",
28
+ "BONDS_LEXICON",
29
+ "FINANCIAL_LEXICON",
30
+ ]
31
+
32
+
33
+ EQUITY_LEXICON = {
34
+ # Strongly Positive Sentiment
35
+ "bullish": 3.0,
36
+ "rally": 2.5,
37
+ "breakout": 2.5,
38
+ "upgrade": 2.5,
39
+ "beat estimates": 3.2,
40
+ "strong earnings": 3.5,
41
+ "record revenue": 3.5,
42
+ "profit surge": 3.2,
43
+ "buyback": 2.5,
44
+ "dividend increase": 2.5,
45
+ "guidance raised": 3.0,
46
+ "expanding market share": 2.5,
47
+ "exceeded expectations": 3.2,
48
+ "all-time high": 3.0,
49
+ "strong fundamentals": 3.0,
50
+ "robust growth": 3.2,
51
+ "cash flow positive": 3.0,
52
+ "market leader": 2.8,
53
+ "acquisition": 2.0,
54
+ "cost-cutting": 1.5,
55
+ "strong guidance": 3.0,
56
+ "positive outlook": 2.8,
57
+ "EPS growth": 2.5,
58
+ "undervalued": 2.0,
59
+ # Moderately Positive Sentiment
60
+ "merger talks": 1.8,
61
+ "strategic partnership": 2.0,
62
+ "shareholder value": 2.2,
63
+ "restructuring": 1.5,
64
+ "capital appreciation": 2.0,
65
+ "competitive advantage": 2.5,
66
+ "economic expansion": 2.0,
67
+ "strong balance sheet": 2.8,
68
+ # Neutral Sentiment
69
+ "consolidation": 0.5,
70
+ "technical correction": -0.8,
71
+ "volatility": -1.0,
72
+ "profit-taking": -0.5,
73
+ "neutral rating": 0.0,
74
+ "steady growth": 1.0,
75
+ # Moderately Negative Sentiment
76
+ "short interest rising": -2.0,
77
+ "debt restructuring": -1.5,
78
+ "share dilution": -2.0,
79
+ "regulatory scrutiny": -2.5,
80
+ "missed expectations": -3.0,
81
+ "guidance lowered": -3.0,
82
+ "cost overruns": -2.0,
83
+ "flat revenue": -1.5,
84
+ "underperformance": -2.5,
85
+ "profit margin decline": -2.2,
86
+ "competitive pressure": -1.8,
87
+ "legal issues": -2.0,
88
+ # Strongly Negative Sentiment
89
+ "bearish": -3.0,
90
+ "sell-off": -3.5,
91
+ "downgrade": -2.8,
92
+ "weak earnings": -3.5,
93
+ "profit warning": -3.5,
94
+ "default risk": -4.0,
95
+ "bankruptcy filing": -4.2,
96
+ "liquidity crisis": -3.8,
97
+ "cut dividend": -2.8,
98
+ "earnings decline": -3.2,
99
+ "stock crash": -4.0,
100
+ "economic slowdown": -2.8,
101
+ "recession fears": -3.5,
102
+ "high debt levels": -3.0,
103
+ "market downturn": -3.2,
104
+ "losses widen": -3.8,
105
+ "credit downgrade": -3.5,
106
+ }
107
+ FOREX_LEXICON = {
108
+ # Strongly Positive Sentiment
109
+ "hawkish": 3.0,
110
+ "rate hike": 2.8,
111
+ "tightening policy": 2.8,
112
+ "currency appreciation": 2.8,
113
+ "strong labor market": 2.5,
114
+ "GDP expansion": 2.5,
115
+ "economic boom": 3.2,
116
+ "inflation under control": 2.5,
117
+ "positive trade balance": 2.5,
118
+ "fiscal stimulus": 2.8,
119
+ "interest rate hike": 2.8,
120
+ "capital inflows": 2.5,
121
+ "strong consumer spending": 2.5,
122
+ "foreign investment inflow": 2.5,
123
+ # Moderately Positive Sentiment
124
+ "interest rate decision": 1.5,
125
+ "central bank intervention": 2.2,
126
+ "GDP growth": 2.2,
127
+ "trade surplus": 2.2,
128
+ "moderate inflation": 1.8,
129
+ "foreign capital influx": 2.0,
130
+ "economic stability": 2.0,
131
+ "currency stabilization": 2.0,
132
+ "improving employment": 2.0,
133
+ "positive business confidence": 2.0,
134
+ # Neutral Sentiment
135
+ "monetary policy": 0.0,
136
+ "exchange rate fluctuation": 0.0,
137
+ "interest rate unchanged": 0.0,
138
+ "trade negotiations": 0.5,
139
+ "stable inflation": 0.5,
140
+ # Moderately Negative Sentiment
141
+ "trade deficit": -2.2,
142
+ "currency depreciation": -2.5,
143
+ "inflation risk": -2.0,
144
+ "economic slowdown": -2.5,
145
+ "high fiscal deficit": -2.2,
146
+ "sovereign debt concerns": -2.5,
147
+ "capital outflows": -2.2,
148
+ "weak consumer confidence": -2.0,
149
+ "soft labor market": -2.0,
150
+ "rising unemployment": -2.5,
151
+ # Strongly Negative Sentiment
152
+ "dovish": -3.0,
153
+ "rate cut": -2.8,
154
+ "quantitative easing": -3.2,
155
+ "recession fears": -3.5,
156
+ "market turmoil": -3.5,
157
+ "economic contraction": -3.2,
158
+ "currency crisis": -3.8,
159
+ "sovereign default": -4.0,
160
+ "credit rating downgrade": -3.5,
161
+ "financial instability": -3.5,
162
+ "debt crisis": -3.8,
163
+ "hyperinflation": -4.0,
164
+ }
165
+ COMMODITIES_LEXICON = {
166
+ # Strongly Positive Sentiment
167
+ "supply shortage": 3.0,
168
+ "OPEC production cut": 3.2,
169
+ "energy crisis": 3.5,
170
+ "oil embargo": 3.8,
171
+ "commodity supercycle": 3.2,
172
+ "gold safe-haven demand": 2.8,
173
+ "inflation hedge": 2.5,
174
+ "weak dollar": 2.8,
175
+ "geopolitical risk": 2.5,
176
+ "strong demand": 3.0,
177
+ "rising crude prices": 3.2,
178
+ "bullish commodity outlook": 3.0,
179
+ "supply constraints": 3.0,
180
+ # Moderately Positive Sentiment
181
+ "rising metal prices": 2.5,
182
+ "higher energy demand": 2.5,
183
+ "limited production capacity": 2.2,
184
+ "low inventory levels": 2.5,
185
+ "export restrictions": 2.0,
186
+ "strategic reserves release": 1.5,
187
+ "drought impact on crops": 2.0,
188
+ "agriculture supply risk": 2.2,
189
+ # Neutral Sentiment
190
+ "market rebalancing": 0.0,
191
+ "seasonal demand": 0.5,
192
+ "commodity price stabilization": 0.5,
193
+ "production levels steady": 0.0,
194
+ # Moderately Negative Sentiment
195
+ "inventory build-up": -2.5,
196
+ "OPEC production increase": -3.0,
197
+ "mining output increase": -2.2,
198
+ "price cap": -2.2,
199
+ "demand destruction": -2.5,
200
+ "falling oil demand": -2.2,
201
+ "oversupply concerns": -2.5,
202
+ "slowing industrial activity": -2.0,
203
+ "crop surplus": -2.0,
204
+ "market correction": -1.5,
205
+ # Strongly Negative Sentiment
206
+ "strong dollar": -2.8,
207
+ "commodity price crash": -3.5,
208
+ "recession-driven demand slump": -3.5,
209
+ "economic downturn impact": -3.5,
210
+ "excess oil production": -3.0,
211
+ "weak commodity prices": -3.0,
212
+ "global trade slowdown": -3.5,
213
+ "deflationary pressure": -3.8,
214
+ }
215
+ CRYPTO_LEXICON = {
216
+ # Strongly Positive Sentiment
217
+ "bull run": 3.5,
218
+ "institutional adoption": 3.2,
219
+ "mainnet launch": 3.2,
220
+ "layer 2 adoption": 3.0,
221
+ "token burn": 2.8,
222
+ "hash rate increase": 2.8,
223
+ "exchange outflow rising": 2.8,
224
+ "staking rewards increase": 2.5,
225
+ "whale accumulation": 2.5,
226
+ "strong on-chain activity": 2.5,
227
+ "NFT boom": 2.5,
228
+ "defi yield farming": 2.2,
229
+ "crypto ETF approval": 3.5,
230
+ "blockchain upgrade": 3.2,
231
+ "bullish sentiment": 3.0,
232
+ # Moderately Positive Sentiment
233
+ "FOMO": 2.5,
234
+ "airdrops": 2.2,
235
+ "crypto partnerships": 2.2,
236
+ "cross-chain adoption": 2.2,
237
+ "rising transaction volume": 2.0,
238
+ "mass adoption": 2.5,
239
+ "long liquidations": 2.0,
240
+ "staking demand": 2.0,
241
+ "increasing DeFi TVL": 2.5,
242
+ "uptrend confirmation": 2.5,
243
+ # Neutral Sentiment
244
+ "market correction": 0.0,
245
+ "smart contract execution": 0.0,
246
+ "blockchain fork": 0.0,
247
+ "stablecoin issuance": 0.0,
248
+ "on-chain metrics neutral": 0.0,
249
+ "volatility spike": 0.0,
250
+ # Moderately Negative Sentiment
251
+ "exchange inflow rising": -2.5,
252
+ "network congestion": -2.2,
253
+ "liquidity crisis": -2.5,
254
+ "flash crash": -2.5,
255
+ "stablecoin depeg": -2.8,
256
+ "security breach": -2.8,
257
+ "mining ban": -2.5,
258
+ "bearish divergence": -2.5,
259
+ "liquidation cascade": -2.5,
260
+ "funding rates negative": -2.5,
261
+ # Strongly Negative Sentiment
262
+ "bear market": -3.5,
263
+ "whale dumping": -3.2,
264
+ "FUD": -3.0,
265
+ "rug pull": -3.8,
266
+ "smart contract exploit": -3.5,
267
+ "regulatory crackdown": -3.8,
268
+ "exchange insolvency": -4.0,
269
+ "crypto ban": -4.0,
270
+ "market manipulation": -3.5,
271
+ "scam project": -3.8,
272
+ "protocol failure": -3.5,
273
+ "hacked exchange": -3.8,
274
+ "capitulation": -3.5,
275
+ }
276
+ BONDS_LEXICON = {
277
+ # Strongly Positive Sentiment
278
+ "yields falling": 2.5,
279
+ "credit upgrade": 3.0,
280
+ "investment grade": 3.2,
281
+ "flight to safety": 2.8,
282
+ "bond rally": 2.8,
283
+ "rate cut expectation": 2.8,
284
+ "monetary easing": 2.8,
285
+ "central bank bond purchases": 2.5,
286
+ "bond demand rising": 2.5,
287
+ "strong bond auction": 2.2,
288
+ "stable credit outlook": 2.5,
289
+ # Moderately Positive Sentiment
290
+ "safe-haven demand": 2.2,
291
+ "long-term bond buying": 2.2,
292
+ "falling credit spreads": 2.0,
293
+ "economic slowdown favoring bonds": 2.0,
294
+ "deflationary environment": 2.2,
295
+ "low-rate environment": 2.2,
296
+ # Neutral Sentiment
297
+ "bond market stabilization": 0.0,
298
+ "steady credit ratings": 0.0,
299
+ "balanced bond flows": 0.0,
300
+ "interest rate outlook neutral": 0.0,
301
+ # Moderately Negative Sentiment
302
+ "corporate debt issuance": -2.2,
303
+ "widening credit spreads": -2.2,
304
+ "rate hike expectation": -2.8,
305
+ "rising borrowing costs": -2.5,
306
+ "tightening liquidity": -2.5,
307
+ "bond outflows": -2.2,
308
+ "weaker bond auction": -2.2,
309
+ # Strongly Negative Sentiment
310
+ "yields rising": -3.0,
311
+ "inverted yield curve": -3.2,
312
+ "credit downgrade": -3.5,
313
+ "default risk rising": -3.8,
314
+ "junk bond status": -3.2,
315
+ "inflation concerns": -3.2,
316
+ "liquidity crunch": -3.2,
317
+ "monetary tightening": -3.2,
318
+ "debt ceiling uncertainty": -3.2,
319
+ "sovereign debt crisis": -4.0,
320
+ "bond market crash": -3.8,
321
+ "hyperinflation risk": -3.8,
322
+ }
323
+ FINANCIAL_LEXICON = {
324
+ **EQUITY_LEXICON,
325
+ **FOREX_LEXICON,
326
+ **COMMODITIES_LEXICON,
327
+ **CRYPTO_LEXICON,
328
+ **BONDS_LEXICON,
329
+ }
330
+
331
+
332
+ class TopicModeler(object):
333
+ def __init__(self):
334
+ self.nlp = spacy.load("en_core_web_sm")
335
+ self.nlp.disable_pipes("ner")
336
+
337
+ def preprocess_texts(self, texts: list[str]):
338
+ def clean_doc(Doc):
339
+ doc = []
340
+ for t in Doc:
341
+ if not any(
342
+ [
343
+ t.is_stop,
344
+ t.is_digit,
345
+ not t.is_alpha,
346
+ t.is_punct,
347
+ t.is_space,
348
+ t.lemma_ == "-PRON-",
349
+ ]
350
+ ):
351
+ doc.append(t.lemma_)
352
+ return " ".join(doc)
353
+
354
+ texts = (text for text in texts)
355
+ clean_texts = []
356
+ for i, doc in enumerate(self.nlp.pipe(texts, batch_size=100, n_process=8), 1):
357
+ clean_texts.append(clean_doc(doc))
358
+ return clean_texts
359
+
360
+
361
+ class SentimentAnalyzer(object):
362
+ """
363
+ A financial sentiment analysis tool that processes and analyzes sentiment
364
+ from news articles, social media posts, and financial reports.
365
+
366
+ This class utilizes NLP techniques to preprocess text and apply sentiment
367
+ analysis using VADER (SentimentIntensityAnalyzer) and optional TextBlob
368
+ for enhanced polarity scoring.
369
+
370
+ Attributes:
371
+ nlp (spacy.Language): A SpaCy NLP pipeline for tokenization and lemmatization,
372
+ with Named Entity Recognition (NER) disabled.
373
+ analyzer (SentimentIntensityAnalyzer): An instance of VADER's sentiment analyzer
374
+ for financial sentiment scoring.
375
+ """
376
+
377
+ def __init__(self):
378
+ """
379
+ Initializes the SentimentAnalyzer class by downloading necessary
380
+ NLTK resources and loading the SpaCy NLP model.
381
+
382
+ - Downloads NLTK tokenization (`punkt`) and stopwords.
383
+ - Loads the `en_core_web_sm` SpaCy model with Named Entity Recognition (NER) disabled.
384
+ - Initializes VADER's SentimentIntensityAnalyzer for sentiment scoring.
385
+ """
386
+ nltk.download("punkt", quiet=True)
387
+ nltk.download("stopwords", quiet=True)
388
+
389
+ try:
390
+ self.nlp = spacy.load("en_core_web_sm")
391
+ self.nlp.disable_pipes("ner")
392
+ 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"
397
+ )
398
+
399
+ self.analyzer = SentimentIntensityAnalyzer()
400
+ self._stopwords = set(stopwords.words("english"))
401
+
402
+ def preprocess_text(self, text: str):
403
+ """
404
+ Preprocesses the input text by performing the following steps:
405
+ 1. Converts text to lowercase.
406
+ 2. Removes URLs.
407
+ 3. Removes all non-alphabetic characters (punctuation, numbers, special symbols).
408
+ 4. Tokenizes the text into words.
409
+ 5. Removes stop words.
410
+ 6. Lemmatizes the words using SpaCy, excluding pronouns.
411
+
412
+ Args:
413
+ text (str): The input text to preprocess.
414
+
415
+ Returns:
416
+ str: The cleaned and lemmatized text.
417
+ """
418
+ text = text.lower()
419
+ text = re.sub(r"http\S+", "", text)
420
+ text = re.sub(r"[^a-zA-Z\s]", "", text)
421
+ words = word_tokenize(text)
422
+ words = [word for word in words if word not in self._stopwords]
423
+ doc = self.nlp(" ".join(words))
424
+ words = [t.lemma_ for t in doc if t.lemma_ != "-PRON-"]
425
+ return " ".join(words)
426
+
427
+ def analyze_sentiment(self, texts, lexicon=None, textblob=False) -> float:
428
+ """
429
+ Analyzes the sentiment of a list of texts using VADER or TextBlob.
430
+
431
+ Steps:
432
+ 1. If a custom lexicon is provided, updates the VADER lexicon.
433
+ 2. If `textblob` is set to True, computes sentiment using TextBlob.
434
+ 3. Otherwise, preprocesses the text and computes sentiment using VADER.
435
+ 4. Returns the average sentiment score of all input texts.
436
+
437
+ Args:
438
+ texts (list of str): A list of text inputs to analyze.
439
+ lexicon (dict, optional): A custom sentiment lexicon to update VADER's default lexicon.
440
+ textblob (bool, optional): If True, uses TextBlob for sentiment analysis instead of VADER.
441
+
442
+ Returns:
443
+ float: The average sentiment score across all input texts.
444
+ - Positive values indicate positive sentiment.
445
+ - Negative values indicate negative sentiment.
446
+ - Zero indicates neutral sentiment.
447
+ """
448
+ if lexicon is not None:
449
+ self.analyzer.lexicon.update(lexicon)
450
+ if textblob:
451
+ blob = TextBlob(" ".join(texts))
452
+ return blob.sentiment.polarity
453
+ sentiment_scores = [
454
+ self.analyzer.polarity_scores(self.preprocess_text(text))["compound"]
455
+ for text in texts
456
+ ]
457
+ avg_sentiment = (
458
+ sum(sentiment_scores) / len(sentiment_scores) if sentiment_scores else 0.0
459
+ )
460
+ return avg_sentiment
461
+
462
+ def get_sentiment_for_tickers(
463
+ self, tickers, lexicon=None, asset_type="stock", top_news=10, **kwargs
464
+ ) -> Dict[str, float]:
465
+ """
466
+ Computes sentiment scores for a list of financial tickers based on news and social media data.
467
+
468
+ Process:
469
+ 1. Collects news articles and posts related to each ticker from various sources:
470
+ - Yahoo Finance News
471
+ - Google Finance News
472
+ - Reddit posts
473
+ - Financial Modeling Prep (FMP) news
474
+ 2. Analyzes sentiment from each source:
475
+ - Uses VADER for Yahoo and Google Finance news.
476
+ - Uses TextBlob for Reddit and FMP news.
477
+ 3. Computes an overall sentiment score using a weighted average approach.
478
+
479
+ Args:
480
+ tickers (list of str): A list of stock, forex, crypto, or other asset tickers.
481
+ 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".
483
+ top_news (int, optional): Number of news articles/posts to fetch per source. Defaults to 10.
484
+ **kwargs: Additional parameters for API authentication and data retrieval, including:
485
+ - fmp_api (str): API key for Financial Modeling Prep.
486
+ - client_id, client_secret, user_agent (str): Credentials for accessing Reddit API.
487
+
488
+ Returns:
489
+ Dict[str, float]: A dictionary mapping each ticker to its overall sentiment score.
490
+ - Positive values indicate positive sentiment.
491
+ - Negative values indicate negative sentiment.
492
+ - Zero indicates neutral sentiment.
493
+ """
494
+ 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
+ fmp_source_news = []
510
+ fmp_news = news.get_fmp_news(kwargs.get("fmp_api"))
511
+ for source in ["articles"]: # , "releases", asset_type]:
512
+ try:
513
+ source_news = fmp_news.get_news(
514
+ ticker, source=source, symbol=ticker, **kwargs
515
+ )
516
+ fmp_source_news += source_news
517
+ except Exception:
518
+ source_news = []
519
+ if any([len(s) > 0 for s in [yahoo_news, google_news]]):
520
+ sources += 1
521
+ for source in [reddit_posts, fmp_source_news]:
522
+ if len(source) > 0:
523
+ sources += 1
524
+ # Compute sentiment
525
+ news_sentiment = self.analyze_sentiment(
526
+ yahoo_news + google_news, lexicon=lexicon
527
+ )
528
+ reddit_sentiment = self.analyze_sentiment(
529
+ reddit_posts, lexicon=lexicon, textblob=True
530
+ )
531
+ fmp_sentiment = self.analyze_sentiment(
532
+ fmp_source_news, lexicon=lexicon, textblob=True
533
+ )
534
+
535
+ # Weighted average sentiment score
536
+ if sources != 0:
537
+ overall_sentiment = (
538
+ news_sentiment + reddit_sentiment + fmp_sentiment
539
+ ) / sources
540
+ else:
541
+ overall_sentiment = 0.0
542
+ sentiment_results[ticker] = overall_sentiment
543
+
544
+ return sentiment_results
545
+
546
+ def get_topn_sentiments(self, sentiments, topn=10):
547
+ """
548
+ Retrieves the top and bottom N assets based on sentiment scores.
549
+
550
+ Args:
551
+ sentiments (dict): A dictionary mapping asset tickers to their sentiment scores.
552
+ topn (int, optional): The number of top and bottom assets to return. Defaults to 10.
553
+
554
+ Returns:
555
+ tuple: A tuple containing two lists:
556
+ - bottom (list of tuples): The `topn` assets with the lowest sentiment scores, sorted in ascending order.
557
+ - top (list of tuples): The `topn` assets with the highest sentiment scores, sorted in descending order.
558
+ """
559
+ sorted_sentiments = sorted(sentiments.items(), key=lambda x: x[1])
560
+ bottom = sorted_sentiments[:topn]
561
+ top = sorted_sentiments[-topn:]
562
+ return bottom, top
563
+
564
+ def _sentiment_bar(self, sentiment_dict, top_n=10):
565
+ bottom_stocks, top_stocks = self.get_topn_sentiments(sentiment_dict, topn=top_n)
566
+ top_bottom_stocks = bottom_stocks + top_stocks
567
+
568
+ stocks = [x[0] for x in top_bottom_stocks]
569
+ scores = [x[1] for x in top_bottom_stocks]
570
+ colors = ["red" if s < 0 else "green" for s in scores]
571
+
572
+ plt.figure(figsize=(12, 6))
573
+ plt.barh(stocks, scores, color=colors)
574
+ plt.axvline(0, color="black", linewidth=1)
575
+
576
+ plt.xlabel("Sentiment Score")
577
+ plt.ylabel("Stock Ticker")
578
+ plt.title(f"Top {top_n} Positive & Negative Stock Sentiments")
579
+
580
+ plt.show()
581
+
582
+ def _sentiment_scatter(self, sentiment_dict):
583
+ df = pd.DataFrame(
584
+ list(sentiment_dict.items()), columns=["Ticker", "Sentiment Score"]
585
+ )
586
+ fig = px.scatter(
587
+ df,
588
+ x=df.index,
589
+ y="Sentiment Score",
590
+ hover_data=["Ticker"],
591
+ color="Sentiment Score",
592
+ color_continuous_scale=["red", "yellow", "green"],
593
+ title="Stock Sentiment Analysis - Interactive Scatter Plot",
594
+ )
595
+ fig.update_layout(xaxis=dict(showticklabels=False))
596
+ fig.show()
597
+
598
+ def visualize_sentiments(self, sentiment_dict, mode="bar", top_n=10):
599
+ """
600
+ Visualizes sentiment scores for financial assets using different chart types.
601
+
602
+ Visualization Modes:
603
+ - "bar": Displays a bar chart of the top N assets by sentiment score.
604
+ - "scatter": Displays a scatter plot of sentiment scores.
605
+
606
+ Args:
607
+ sentiment_dict (dict): A dictionary mapping asset tickers to their sentiment scores.
608
+ mode (str, optional): The type of visualization to generate.
609
+ Options: "bar" (default), "scatter".
610
+ top_n (int, optional): The number of top tickers to display in the bar chart.
611
+ Only applicable when mode is "bar".
612
+
613
+ Returns:
614
+ None: Displays the sentiment visualization.
615
+ """
616
+ if mode == "bar":
617
+ self._sentiment_bar(sentiment_dict, top_n=top_n)
618
+ elif mode == "scatter":
619
+ self._sentiment_scatter(sentiment_dict)
620
+
621
+ def display_sentiment_dashboard(
622
+ self,
623
+ tickers,
624
+ asset_type="stock",
625
+ lexicon=None,
626
+ interval=100_000,
627
+ top_n=20,
628
+ **kwargs,
629
+ ):
630
+ """
631
+ Creates and runs a real-time sentiment analysis dashboard for financial assets.
632
+
633
+ The dashboard visualizes sentiment scores for given tickers using interactive
634
+ bar and scatter plots. It fetches new sentiment data at specified intervals.
635
+
636
+ Args:
637
+ tickers (list[str]):
638
+ A list of asset tickers (e.g., ["AAPL", "GOOGL", "TSLA"]).
639
+ asset_type (str, optional):
640
+ The type of financial asset ("stock", "forex", "crypto"). Defaults to "stock".
641
+ lexicon (dict, optional):
642
+ A custom sentiment lexicon. Defaults to None.
643
+ interval (int, optional):
644
+ The refresh interval (in milliseconds) for sentiment data updates. Defaults to 100000.
645
+ top_n (int, optional):
646
+ The number of top and bottom assets to display in the sentiment bar chart. Defaults to 20.
647
+ **kwargs (dict):
648
+ Additional arguments required for fetching sentiment data. Must include:
649
+ - client_id (str): Reddit API client ID.
650
+ - client_secret (str): Reddit API client secret.
651
+ - user_agent (str): User agent for Reddit API.
652
+ - fmp_api (str): Financial Modeling Prep (FMP) API key.
653
+
654
+ Returns:
655
+ None: The function does not return anything but starts a real-time interactive dashboard.
656
+
657
+ Example Usage:
658
+ sa = SentimentAnalyzer()
659
+ sa.display_sentiment_dashboard(
660
+ tickers=["AAPL", "TSLA", "GOOGL"],
661
+ asset_type="stock",
662
+ lexicon=my_lexicon,
663
+ display=True,
664
+ interval=5000,
665
+ top_n=10,
666
+ client_id="your_reddit_id",
667
+ client_secret="your_reddit_secret",
668
+ user_agent="your_user_agent",
669
+ fmp_api="your_fmp_api_key",
670
+ )
671
+
672
+ Notes:
673
+ - Sentiment analysis is performed using financial news and social media discussions.
674
+ - The dashboard updates in real-time at the specified interval.
675
+ - The dashboard will keep running unless manually stopped (Ctrl+C).
676
+ """
677
+
678
+ app = dash.Dash(__name__)
679
+
680
+ sentiment_history = {ticker: [] for ticker in tickers}
681
+
682
+ # Dash Layout
683
+ app.layout = html.Div(
684
+ children=[
685
+ html.H1("📊 Real-Time Sentiment Dashboard"),
686
+ dcc.Graph(id="top-sentiment-bar"),
687
+ dcc.Graph(id="sentiment-interactive"),
688
+ dcc.Interval(id="interval-component", interval=interval, n_intervals=0),
689
+ ]
690
+ )
691
+
692
+ # Update Sentiment Data
693
+ @app.callback(
694
+ [
695
+ Output("top-sentiment-bar", "figure"),
696
+ Output("sentiment-interactive", "figure"),
697
+ ],
698
+ [Input("interval-component", "n_intervals")],
699
+ )
700
+ def update_dashboard(n):
701
+ start_time = time.time()
702
+ sentiment_data = self.get_sentiment_for_tickers(
703
+ tickers,
704
+ lexicon=lexicon,
705
+ asset_type=asset_type,
706
+ top_news=top_n,
707
+ **kwargs,
708
+ )
709
+ elapsed_time = time.time() - start_time
710
+ print(f"Sentiment Fetch Time: {elapsed_time:.2f} seconds")
711
+ timestamp = datetime.now().strftime("%H:%M:%S")
712
+ for stock, score in sentiment_data.items():
713
+ sentiment_history[stock].append(
714
+ {"timestamp": timestamp, "score": score}
715
+ )
716
+ data = []
717
+ for stock, scores in sentiment_history.items():
718
+ for entry in scores:
719
+ data.append(
720
+ {
721
+ "Ticker": stock,
722
+ "Time": entry["timestamp"],
723
+ "Sentiment Score": entry["score"],
724
+ }
725
+ )
726
+ df = pd.DataFrame(data)
727
+
728
+ # Top Sentiment Bar Chart
729
+ latest_timestamp = df["Time"].max()
730
+ latest_sentiments = (
731
+ df[df["Time"] == latest_timestamp]
732
+ if not df.empty
733
+ else pd.DataFrame(columns=["Ticker", "Sentiment Score"])
734
+ )
735
+
736
+ if latest_sentiments.empty:
737
+ bar_chart = px.bar(title="No Sentiment Data Available")
738
+ else:
739
+ # Get top N and bottom N stocks
740
+ bottom_stocks, top_stocks = self.get_topn_sentiments(
741
+ sentiment_data, topn=top_n
742
+ )
743
+ top_bottom_stocks = bottom_stocks + top_stocks
744
+
745
+ stocks = [x[0] for x in top_bottom_stocks]
746
+ scores = [x[1] for x in top_bottom_stocks]
747
+
748
+ df_plot = pd.DataFrame({"Ticker": stocks, "Sentiment Score": scores})
749
+ # Horizontal bar chart
750
+ bar_chart = px.bar(
751
+ df_plot,
752
+ x="Sentiment Score",
753
+ y="Ticker",
754
+ title=f"Top {top_n} Positive & Negative Sentiment Stocks",
755
+ color="Sentiment Score",
756
+ color_continuous_scale=["red", "yellow", "green"],
757
+ orientation="h",
758
+ )
759
+ bar_chart.add_vline(
760
+ x=0, line_width=2, line_dash="dash", line_color="black"
761
+ )
762
+ bar_chart.update_layout(
763
+ xaxis_title="Sentiment Score",
764
+ yaxis_title="Stock Ticker",
765
+ yaxis=dict(autorange="reversed"),
766
+ width=1500,
767
+ height=600,
768
+ )
769
+
770
+ # Sentiment Interactive Scatter Plot
771
+ scatter_chart = px.scatter(
772
+ latest_sentiments,
773
+ x=latest_sentiments.index,
774
+ y="Sentiment Score",
775
+ hover_data=["Ticker"],
776
+ color="Sentiment Score",
777
+ color_continuous_scale=["red", "yellow", "green"],
778
+ title="Stock Sentiment Analysis - Interactive Scatter Plot",
779
+ )
780
+ scatter_chart.update_layout(width=1500, height=600)
781
+
782
+ return bar_chart, scatter_chart
783
+
784
+ app.run()