KYTHY commited on
Commit
3752b51
·
verified ·
1 Parent(s): e80eb4f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +328 -169
app.py CHANGED
@@ -1,105 +1,276 @@
1
- # 📊 Stock Sentiment Analyzer (with Correlation Insight)
2
- # เวอร์ชัน: headline + summary / แสดง summary ในหน้าเว็บ / ป้องกัน error / มีคำอธิบาย correlation
3
-
4
- import gradio as gr
5
  import pandas as pd
 
6
  import numpy as np
 
 
 
 
 
7
  import yfinance as yf
8
- import requests
9
- import datetime
10
- import plotly.graph_objs as go
11
- from transformers import pipeline
12
-
13
 
14
- # ====== CONFIG ======
15
- FINNHUB_API_KEY = "YOUR_FINNHUB_API_KEY" # 🔑 ใส่คีย์ของคุณตรงนี้
 
 
 
 
 
 
 
16
 
 
17
 
18
- # ====== CLASS ANALYZER ======
19
- class StockSentimentAnalyzer:
20
  def __init__(self):
21
- self.sentiment_analyzer = pipeline("sentiment-analysis")
22
-
23
- def get_news(self, symbol, days=7):
24
- """
25
- ดึงข่าวจาก Finnhub API
26
- """
27
- end_date = datetime.datetime.now()
28
- start_date = end_date - datetime.timedelta(days=days)
29
-
30
- url = f"https://finnhub.io/api/v1/company-news?symbol={symbol}&from={start_date.date()}&to={end_date.date()}&token={FINNHUB_API_KEY}"
31
- response = requests.get(url)
32
-
33
- if response.status_code != 200:
34
- raise Exception(f"Finnhub API error: {response.text}")
35
-
36
- news = response.json()
37
- return pd.DataFrame(news)
38
-
39
- def analyze_news_sentiment(self, symbol, days=7, refresh_data=False):
40
- """
41
- วิเคราะห์ sentiment ของข่าว และความสัมพันธ์กับราคาหุ้น
42
- """
43
- # ดึงข่าว
44
- news_df = self.get_news(symbol, days)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  if news_df.empty:
46
- raise Exception("No news data available.")
47
-
48
- # รวม headline + summary (กัน None)
49
- def combine_text(row):
50
- headline = str(row.get("headline", "") or "")
51
- summary = str(row.get("summary", "") or "")
52
- return f"{headline}. {summary}".strip()
53
-
54
- news_df["combined_text"] = news_df.apply(combine_text, axis=1)
55
-
56
- # วิเคราะห์ sentiment แบบปลอดภัย
57
- def safe_analyze(text):
58
  try:
59
- result = self.sentiment_analyzer(text[:512])[0] # จำกัดความยาว
60
- score = result["score"]
61
- return score if result["label"] == "POSITIVE" else -score
62
- except Exception as e:
63
- print("⚠️ Error analyzing text:", e)
64
- return 0.0
65
-
66
- news_df["sentiment_score"] = news_df["combined_text"].apply(safe_analyze)
67
-
68
- # เฉลี่ย sentiment รายวัน
69
- news_df["datetime"] = pd.to_datetime(news_df["datetime"], unit="s")
70
- news_df["date"] = news_df["datetime"].dt.date
71
- daily_sentiment = news_df.groupby("date")["sentiment_score"].mean().reset_index()
72
- daily_sentiment.rename(columns={"sentiment_score": "avg_sentiment"}, inplace=True)
73
-
74
- # ดึงราคาหุ้น
75
- stock_data = yf.download(symbol, start=daily_sentiment["date"].min(), end=daily_sentiment["date"].max() + datetime.timedelta(days=1))
76
- if stock_data.empty:
77
- raise Exception("No stock price data available.")
78
- stock_data["date"] = stock_data.index.date
79
-
80
- merged = pd.merge(daily_sentiment, stock_data, on="date", how="inner")
81
- correlation = merged["avg_sentiment"].corr(merged["Close"]) if len(merged) > 1 else None
82
-
83
- return daily_sentiment, stock_data, news_df.head(10), correlation
84
-
85
-
86
- # ====== SUMMARY FUNCTION ======
87
- def create_summary(daily_sentiment, symbol, correlation):
88
- """
89
- สรุปผลการวิเคราะห์ พร้อมคำอธิบายความสัมพันธ์
90
- """
91
- summary = f"## 📊 Sentiment Summary for {symbol}\n\n"
92
- avg_sentiment = daily_sentiment["avg_sentiment"].mean()
93
-
94
- # สรุปค่า sentiment โดยรวม
95
- if avg_sentiment > 0.2:
96
- summary += f"- Overall sentiment: **Positive** ({avg_sentiment:.2f})\n"
97
- elif avg_sentiment < -0.2:
98
- summary += f"- Overall sentiment: **Negative** ({avg_sentiment:.2f})\n"
99
- else:
100
- summary += f"- Overall sentiment: **Neutral** ({avg_sentiment:.2f})\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- # คำอธิบาย correlation
103
  if correlation is not None:
104
  if correlation >= 0.7:
105
  relation = "strong positive relationship — sentiment moves closely with stock price 📈"
@@ -111,84 +282,72 @@ def create_summary(daily_sentiment, symbol, correlation):
111
  relation = "moderate negative relationship — sentiment often moves opposite to price"
112
  else:
113
  relation = "strong negative relationship — sentiment and price move in opposite directions 📉"
 
114
  summary += f"- **Correlation (Sentiment vs Stock Price)**: {correlation:.2f} → {relation}\n"
115
- else:
116
- summary += "- Not enough data to calculate correlation.\n"
117
 
 
 
 
 
 
 
 
118
  return summary
119
 
120
-
121
- # ====== HEADLINES FORMAT ======
122
- def format_headlines(news_df):
123
- """
124
- แสดงข่าว Top 10 พร้อม summary และ sentiment
125
- """
126
- text = "## 📰 Top Headlines\n\n"
127
- for _, row in news_df.iterrows():
128
- headline = row.get("headline", "No headline")
129
- url = row.get("url", "#")
130
- sentiment = row.get("sentiment_score", 0.0)
131
- summary = row.get("summary", "")
132
- text += f"- **{row['datetime'].date()} | [{headline}]({url})**\n"
133
- if summary:
134
- text += f" \n _Summary_: {summary}\n"
135
- text += f" \n _Sentiment Score_: {sentiment:.2f}\n\n"
136
- return text
137
-
138
-
139
- # ====== MAIN GRADIO FUNCTION ======
140
- def analyze_stock_sentiment(symbol, days, refresh_data=False):
141
- try:
142
- analyzer = StockSentimentAnalyzer()
143
- daily_sentiment, stock_data, top_headlines, correlation = analyzer.analyze_news_sentiment(symbol, days, refresh_data)
144
-
145
- if daily_sentiment.empty:
146
- return f"No sentiment data found for {symbol}.", None, None, "No headlines available."
147
-
148
- summary = create_summary(daily_sentiment, symbol, correlation)
149
- headlines = format_headlines(top_headlines)
150
-
151
- # กราฟ Sentiment
152
- sentiment_chart = go.Figure()
153
- sentiment_chart.add_trace(go.Bar(x=daily_sentiment["date"], y=daily_sentiment["avg_sentiment"], name="Average Sentiment"))
154
- sentiment_chart.update_layout(title=f"{symbol} Daily Sentiment", xaxis_title="Date", yaxis_title="Sentiment Score")
155
-
156
- # กราฟ ราคาหุ้น
157
- price_chart = go.Figure()
158
- if not stock_data.empty:
159
- price_chart.add_trace(go.Scatter(x=stock_data.index, y=stock_data["Close"], mode="lines", name="Close Price"))
160
- price_chart.update_layout(title=f"{symbol} Stock Price", xaxis_title="Date", yaxis_title="Price (USD)")
161
-
162
- return summary, sentiment_chart, price_chart, headlines
163
-
164
- except Exception as e:
165
- import traceback
166
- print("❌ ERROR in analyze_stock_sentiment:", e)
167
- print(traceback.format_exc())
168
- return f"⚠️ Error: {e}", None, None, None
169
-
170
-
171
- # ====== BUILD GRADIO APP ======
172
- with gr.Blocks(title="Stock Sentiment Analyzer") as app:
173
- gr.Markdown("## 🧠 Stock Sentiment Analyzer (with Correlation Insight)")
174
- gr.Markdown("Analyze how news sentiment correlates with stock price movements 📈")
175
-
176
- with gr.Row():
177
- symbol = gr.Textbox(label="Stock Symbol (e.g. AAPL, TSLA)", value="AAPL")
178
- days = gr.Slider(3, 30, value=7, step=1, label="Days of News")
179
- refresh = gr.Checkbox(label="Refresh data", value=False)
180
-
181
- analyze_button = gr.Button("🔍 Analyze Sentiment")
182
-
183
- summary_output = gr.Markdown()
184
- sentiment_plot = gr.Plot()
185
- price_plot = gr.Plot()
186
- headlines_output = gr.Markdown()
187
-
188
- analyze_button.click(
189
- fn=analyze_stock_sentiment,
190
- inputs=[symbol, days, refresh],
191
- outputs=[summary_output, sentiment_plot, price_plot, headlines_output],
192
- )
193
-
194
- app.launch()
 
1
+ import os
 
 
 
2
  import pandas as pd
3
+ import requests
4
  import numpy as np
5
+ import gradio as gr
6
+ from datetime import datetime, timedelta
7
+ from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
8
+ import plotly.graph_objects as go
9
+ from plotly.subplots import make_subplots
10
  import yfinance as yf
 
 
 
 
 
11
 
12
+ # Configuration
13
+ class Config:
14
+ FINNHUB_API_KEY = "cuj17q1r01qm7p9n307gcuj17q1r01qm7p9n3080"
15
+ DEFAULT_DAYS = 30 # Reduced from 365 to make it faster
16
+ DATA_DIR = "data"
17
+
18
+ @classmethod
19
+ def initialize(cls):
20
+ os.makedirs(cls.DATA_DIR, exist_ok=True)
21
 
22
+ Config.initialize()
23
 
24
+ # Simple sentiment analyzer
25
+ class SentimentAnalyzer:
26
  def __init__(self):
27
+ self.analyzer = SentimentIntensityAnalyzer()
28
+
29
+ def analyze(self, text):
30
+ if not isinstance(text, str) or not text.strip():
31
+ return 0
32
+ return self.analyzer.polarity_scores(text)['compound']
33
+
34
+ # News fetcher and sentiment analyzer
35
+ class StockNewsAnalyzer:
36
+ def __init__(self, symbol):
37
+ self.symbol = symbol
38
+ self.sentiment_analyzer = SentimentAnalyzer()
39
+
40
+ def get_file_path(self, file_type):
41
+ return os.path.join(Config.DATA_DIR, f"{self.symbol}_{file_type}.csv")
42
+
43
+ def get_news(self, days=Config.DEFAULT_DAYS, force_refresh=False):
44
+ """Fetch news articles from Finnhub API"""
45
+ file_path = self.get_file_path("news")
46
+
47
+ # Return cached data if it exists and no refresh is forced
48
+ if os.path.exists(file_path) and not force_refresh:
49
+ try:
50
+ return pd.read_csv(file_path, parse_dates=['datetime'])
51
+ except Exception:
52
+ pass
53
+
54
+ # Calculate date range
55
+ end_date = datetime.now()
56
+ start_date = end_date - timedelta(days=days)
57
+
58
+ # Fetch from API
59
+ url = "https://finnhub.io/api/v1/company-news"
60
+ params = {
61
+ "symbol": self.symbol,
62
+ "from": start_date.strftime('%Y-%m-%d'),
63
+ "to": end_date.strftime('%Y-%m-%d'),
64
+ "token": Config.FINNHUB_API_KEY,
65
+ }
66
+
67
+ try:
68
+ response = requests.get(url, params=params, timeout=10)
69
+ data = response.json()
70
+
71
+ if not data or not isinstance(data, list):
72
+ return pd.DataFrame()
73
+
74
+ # Create DataFrame
75
+ df = pd.DataFrame(data)
76
+ if 'datetime' in df.columns:
77
+ df['datetime'] = pd.to_datetime(df['datetime'], unit='s')
78
+ df.to_csv(file_path, index=False)
79
+ return df
80
+ return pd.DataFrame()
81
+ except Exception as e:
82
+ print(f"Error fetching news: {e}")
83
+ return pd.DataFrame()
84
+
85
+ def analyze_news_sentiment(self, days=Config.DEFAULT_DAYS, force_refresh=False):
86
+ """Analyze sentiment from news articles"""
87
+ news_df = self.get_news(days, force_refresh)
88
+
89
  if news_df.empty:
90
+ return None, None, None, None
91
+
92
+ if 'headline' in news_df.columns:
93
+ news_df['sentiment_score'] = news_df['headline'].apply(self.sentiment_analyzer.analyze)
94
+ news_df['date'] = news_df['datetime'].dt.date
95
+ news_df['date'] = pd.to_datetime(news_df['date'])
96
+
97
+ # Get stock price
 
 
 
 
98
  try:
99
+ start_date = news_df['date'].min() - timedelta(days=5)
100
+ end_date = news_df['date'].max() + timedelta(days=1)
101
+ stock_data = yf.download(self.symbol, start=start_date, end=end_date, progress=False)
102
+ if not stock_data.empty and 'Close' in stock_data.columns:
103
+ stock_data = stock_data[['Close']]
104
+ stock_data.columns = ['close']
105
+ stock_data = stock_data.reset_index()
106
+ stock_data.rename(columns={'Date': 'date'}, inplace=True)
107
+ stock_data['date'] = pd.to_datetime(stock_data['date'].dt.date)
108
+ stock_data.set_index('date', inplace=True)
109
+ else:
110
+ stock_data = pd.DataFrame()
111
+ except Exception:
112
+ stock_data = pd.DataFrame()
113
+
114
+ # Daily sentiment
115
+ daily_sentiment = news_df.groupby('date').agg(
116
+ avg_sentiment=('sentiment_score', 'mean'),
117
+ article_count=('sentiment_score', 'count'),
118
+ positive_count=('sentiment_score', lambda x: sum(x > 0.05)),
119
+ negative_count=('sentiment_score', lambda x: sum(x < -0.05)),
120
+ neutral_count=('sentiment_score', lambda x: sum((x >= -0.05) & (x <= 0.05)))
121
+ ).reset_index()
122
+
123
+ # Top headlines
124
+ news_df = news_df.sort_values('sentiment_score', ascending=False)
125
+ top_positive = news_df[news_df['sentiment_score'] > 0].head(5)
126
+ top_negative = news_df[news_df['sentiment_score'] < 0].tail(5)
127
+
128
+ # Compute correlation
129
+ if not stock_data.empty:
130
+ merged = pd.merge(daily_sentiment, stock_data, left_on='date', right_index=True, how='inner')
131
+ if not merged.empty:
132
+ correlation = merged['avg_sentiment'].corr(merged['close'])
133
+ else:
134
+ correlation = None
135
+ else:
136
+ correlation = None
137
+
138
+ return daily_sentiment, stock_data, pd.concat([top_positive, top_negative]), correlation
139
+
140
+ return None, None, None, None
141
+
142
+ # Visualization Functions
143
+ def create_sentiment_overview(daily_sentiment, stock_data, top_headlines, symbol):
144
+ if daily_sentiment is None or daily_sentiment.empty:
145
+ return None
146
+
147
+ fig = make_subplots(rows=2, cols=1, specs=[[{"secondary_y": True}], [{}]],
148
+ row_heights=[0.7, 0.3], vertical_spacing=0.1)
149
+
150
+ if not stock_data.empty:
151
+ fig.add_trace(
152
+ go.Scatter(
153
+ x=stock_data.index,
154
+ y=stock_data['close'],
155
+ name='Stock Price',
156
+ line=dict(color='#1f77b4', width=2)
157
+ ),
158
+ row=1, col=1, secondary_y=False
159
+ )
160
+
161
+ fig.add_trace(
162
+ go.Scatter(
163
+ x=daily_sentiment['date'],
164
+ y=daily_sentiment['avg_sentiment'],
165
+ name='Sentiment Score',
166
+ line=dict(color='#ff7f0e', width=2)
167
+ ),
168
+ row=1, col=1, secondary_y=True
169
+ )
170
+
171
+ fig.add_trace(
172
+ go.Bar(
173
+ x=daily_sentiment['date'],
174
+ y=daily_sentiment['article_count'],
175
+ name='Article Count',
176
+ marker_color='rgba(135, 206, 235, 0.5)',
177
+ opacity=0.7
178
+ ),
179
+ row=2, col=1
180
+ )
181
+
182
+ fig.add_trace(
183
+ go.Bar(
184
+ x=daily_sentiment['date'],
185
+ y=daily_sentiment['positive_count'],
186
+ name='Positive',
187
+ marker_color='rgba(0, 128, 0, 0.7)'
188
+ ),
189
+ row=2, col=1
190
+ )
191
+
192
+ fig.add_trace(
193
+ go.Bar(
194
+ x=daily_sentiment['date'],
195
+ y=daily_sentiment['negative_count'],
196
+ name='Negative',
197
+ marker_color='rgba(255, 0, 0, 0.7)'
198
+ ),
199
+ row=2, col=1
200
+ )
201
+
202
+ fig.add_trace(
203
+ go.Bar(
204
+ x=daily_sentiment['date'],
205
+ y=daily_sentiment['neutral_count'],
206
+ name='Neutral',
207
+ marker_color='rgba(128, 128, 128, 0.7)'
208
+ ),
209
+ row=2, col=1
210
+ )
211
+
212
+ fig.update_layout(
213
+ title=f"{symbol} News Sentiment Analysis",
214
+ template='plotly_white',
215
+ hovermode='x unified',
216
+ barmode='stack',
217
+ legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1),
218
+ height=700,
219
+ margin=dict(l=20, r=20, t=80, b=20)
220
+ )
221
+
222
+ fig.update_yaxes(title_text="Stock Price", row=1, col=1, secondary_y=False)
223
+ fig.update_yaxes(title_text="Sentiment Score", row=1, col=1, secondary_y=True)
224
+ fig.update_yaxes(title_text="Article Count", row=2, col=1)
225
+
226
+ return fig
227
+
228
+ def format_headlines(headlines_df):
229
+ if headlines_df is None or headlines_df.empty:
230
+ return "No headlines available."
231
+
232
+ headlines_df = headlines_df.sort_values('sentiment_score', ascending=False)
233
+
234
+ result = "## Top Positive Headlines\n\n"
235
+ for _, row in headlines_df[headlines_df['sentiment_score'] > 0].head(5).iterrows():
236
+ date = row['datetime'].strftime('%Y-%m-%d')
237
+ sentiment = row['sentiment_score']
238
+ color = "green"
239
+ result += f"- **{date}** | [{row['headline']}]({row['url']}) | <span style='color:{color};'>*{sentiment:.2f}*</span>\n\n"
240
+
241
+ result += "## Top Negative Headlines\n\n"
242
+ for _, row in headlines_df[headlines_df['sentiment_score'] < 0].sort_values('sentiment_score').head(5).iterrows():
243
+ date = row['datetime'].strftime('%Y-%m-%d')
244
+ sentiment = row['sentiment_score']
245
+ color = "red"
246
+ result += f"- **{date}** | [{row['headline']}]({row['url']}) | <span style='color:{color};'>*{sentiment:.2f}*</span>\n\n"
247
+
248
+ return result
249
+
250
+ def create_summary(daily_sentiment, symbol, correlation=None):
251
+ if daily_sentiment is None or daily_sentiment.empty:
252
+ return f"No sentiment data available for {symbol}."
253
+
254
+ avg_sentiment = daily_sentiment['avg_sentiment'].mean()
255
+ total_articles = daily_sentiment['article_count'].sum()
256
+ total_positive = daily_sentiment['positive_count'].sum()
257
+ total_negative = daily_sentiment['negative_count'].sum()
258
+ total_neutral = daily_sentiment['neutral_count'].sum()
259
+
260
+ sentiment_trend = "neutral"
261
+ if avg_sentiment > 0.05:
262
+ sentiment_trend = "positive"
263
+ elif avg_sentiment < -0.05:
264
+ sentiment_trend = "negative"
265
+
266
+ summary = f"""
267
+ ## {symbol} Sentiment Summary
268
+ ### Overview
269
+ - **Overall Sentiment**: {sentiment_trend.title()} (Score: {avg_sentiment:.2f})
270
+ - **Total Articles**: {total_articles}
271
+ - **Date Range**: {daily_sentiment['date'].min().strftime('%Y-%m-%d')} to {daily_sentiment['date'].max().strftime('%Y-%m-%d')}
272
+ """
273
 
 
274
  if correlation is not None:
275
  if correlation >= 0.7:
276
  relation = "strong positive relationship — sentiment moves closely with stock price 📈"
 
282
  relation = "moderate negative relationship — sentiment often moves opposite to price"
283
  else:
284
  relation = "strong negative relationship — sentiment and price move in opposite directions 📉"
285
+
286
  summary += f"- **Correlation (Sentiment vs Stock Price)**: {correlation:.2f} → {relation}\n"
 
 
287
 
288
+ summary += f"""
289
+ ### Sentiment Breakdown
290
+ - **Positive Articles**: {total_positive} ({total_positive/total_articles*100:.1f}%)
291
+ - **Negative Articles**: {total_negative} ({total_negative/total_articles*100:.1f}%)
292
+ - **Neutral Articles**: {total_neutral} ({total_neutral/total_articles*100:.1f}%)
293
+ """
294
+
295
  return summary
296
 
297
+ # Gradio Interface
298
+ def analyze_stock_sentiment(symbol, days, refresh_data):
299
+ if not symbol:
300
+ return "Please enter a valid stock symbol.", None, "No headlines available."
301
+
302
+ symbol = symbol.upper().strip()
303
+ analyzer = StockNewsAnalyzer(symbol)
304
+
305
+ daily_sentiment, stock_data, top_headlines, correlation = analyzer.analyze_news_sentiment(days, refresh_data)
306
+
307
+ if daily_sentiment is None or daily_sentiment.empty:
308
+ return f"No news data available for {symbol}. Try another symbol or increase the time range.", None, "No headlines available."
309
+
310
+ sentiment_plot = create_sentiment_overview(daily_sentiment, stock_data, top_headlines, symbol)
311
+ summary = create_summary(daily_sentiment, symbol, correlation)
312
+ headlines = format_headlines(top_headlines)
313
+
314
+ return summary, sentiment_plot, headlines
315
+
316
+ def build_interface():
317
+ with gr.Blocks(title="Stock Sentiment Analysis", theme=gr.themes.Soft()) as app:
318
+ gr.Markdown("# 📊 Stock News Sentiment Analysis")
319
+ gr.Markdown("Analyze how news sentiment correlates with stock price movement using real data.")
320
+
321
+ with gr.Row():
322
+ with gr.Column(scale=1):
323
+ symbol_input = gr.Textbox(label="Stock Symbol", value="BABA", placeholder="e.g., AAPL, MSFT, GOOGL")
324
+ days_input = gr.Slider(label="Days of History", minimum=7, maximum=90, value=90, step=1)
325
+ refresh_data = gr.Checkbox(label="Refresh Data", value=False)
326
+ analyze_button = gr.Button("Analyze Sentiment", variant="primary")
327
+
328
+ summary_text = gr.Markdown()
329
+ sentiment_plot = gr.Plot()
330
+ headlines_text = gr.Markdown()
331
+
332
+ analyze_button.click(
333
+ fn=analyze_stock_sentiment,
334
+ inputs=[symbol_input, days_input, refresh_data],
335
+ outputs=[summary_text, sentiment_plot, headlines_text]
336
+ )
337
+
338
+ gr.Markdown("""
339
+ ---
340
+ ### ℹ️ About Correlation
341
+ - **+1.0** Sentiment and price move together perfectly (strong positive)
342
+ - **0.0** → No relationship between sentiment and price
343
+ - **-1.0** → Sentiment and price move in opposite directions (strong negative)
344
+ """)
345
+
346
+ return app
347
+
348
+ def main():
349
+ app = build_interface()
350
+ app.launch()
351
+
352
+ if __name__ == "__main__":
353
+ main()