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