JayLacoma commited on
Commit
604c810
·
verified ·
1 Parent(s): 872fd54

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +215 -431
app.py CHANGED
@@ -14,10 +14,16 @@ import plotly.express as px
14
 
15
  warnings.filterwarnings('ignore')
16
 
17
- # Configuration
 
 
 
 
 
 
 
18
  class Config:
19
- FINNHUB_API_KEY = "cuj17q1r01qm7p9n307gcuj17q1r01qm7p9n3080"
20
- DEFAULT_DAYS = 30
21
  DATA_DIR = "data"
22
 
23
  @classmethod
@@ -27,7 +33,7 @@ class Config:
27
  Config.initialize()
28
 
29
  # ============================================================================
30
- # SENTIMENT ANALYSIS COMPONENTS
31
  # ============================================================================
32
 
33
  class SentimentAnalyzer:
@@ -47,9 +53,8 @@ class StockNewsAnalyzer:
47
  def get_file_path(self, file_type):
48
  return os.path.join(Config.DATA_DIR, f"{self.symbol}_{file_type}.csv")
49
 
50
- def get_news(self, days=Config.DEFAULT_DAYS, force_refresh=False):
51
  file_path = self.get_file_path("news")
52
-
53
  if os.path.exists(file_path) and not force_refresh:
54
  try:
55
  return pd.read_csv(file_path, parse_dates=['datetime'])
@@ -57,9 +62,9 @@ class StockNewsAnalyzer:
57
  pass
58
 
59
  end_date = datetime.now()
60
- start_date = end_date - timedelta(days=days)
61
 
62
- url = "https://finnhub.io/api/v1/company-news"
63
  params = {
64
  "symbol": self.symbol,
65
  "from": start_date.strftime('%Y-%m-%d'),
@@ -70,10 +75,8 @@ class StockNewsAnalyzer:
70
  try:
71
  response = requests.get(url, params=params, timeout=10)
72
  data = response.json()
73
-
74
  if not data or not isinstance(data, list):
75
  return pd.DataFrame()
76
-
77
  df = pd.DataFrame(data)
78
  if 'datetime' in df.columns:
79
  df['datetime'] = pd.to_datetime(df['datetime'], unit='s')
@@ -84,488 +87,269 @@ class StockNewsAnalyzer:
84
  print(f"Error fetching news: {e}")
85
  return pd.DataFrame()
86
 
87
- def get_sentiment_score(self, days=30):
88
- news_df = self.get_news(days)
89
- if news_df.empty:
90
- return 0, 0
 
 
 
91
 
92
- if 'headline' in news_df.columns:
93
- news_df['sentiment_score'] = news_df['headline'].apply(self.sentiment_analyzer.analyze)
94
- avg_sentiment = news_df['sentiment_score'].mean()
95
- article_count = len(news_df)
96
- return avg_sentiment, article_count
97
- return 0, 0
 
 
 
98
 
99
  # ============================================================================
100
- # TECHNICAL ANALYSIS COMPONENTS
101
  # ============================================================================
102
 
103
- def calculate_rsi(df, window=14):
104
  delta = df['Close'].diff()
105
- gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
106
- loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
107
  rs = gain / loss
108
- rsi = 100 - (100 / (1 + rs))
109
- return rsi
110
-
111
- def calculate_macd(df):
112
- short_ema = df['Close'].ewm(span=12, adjust=False).mean()
113
- long_ema = df['Close'].ewm(span=26, adjust=False).mean()
114
- macd = short_ema - long_ema
115
- signal = macd.ewm(span=9, adjust=False).mean()
116
- return macd, signal
117
 
118
- def calculate_bollinger_bands(df, window=20):
119
- middle_bb = df['Close'].rolling(window=window).mean()
120
- upper_bb = middle_bb + 2 * df['Close'].rolling(window=window).std()
121
- lower_bb = middle_bb - 2 * df['Close'].rolling(window=window).std()
122
- return middle_bb, upper_bb, lower_bb
123
 
124
  def calculate_stochastic_oscillator(df):
125
- lowest_low = df['Low'].rolling(window=14).min()
126
- highest_high = df['High'].rolling(window=14).max()
127
- slowk = ((df['Close'] - lowest_low) / (highest_high - lowest_low)) * 100
128
- slowd = slowk.rolling(window=3).mean()
129
- return slowk, slowd
130
 
131
  def calculate_cmf(df, window=20):
132
  mfv = ((df['Close'] - df['Low']) - (df['High'] - df['Close'])) / (df['High'] - df['Low']) * df['Volume']
133
- cmf = mfv.rolling(window=window).sum() / df['Volume'].rolling(window=window).sum()
134
- return cmf
135
 
136
- def calculate_technical_signals(df):
137
- """Calculate all technical indicators and generate signals"""
138
  df['RSI'] = calculate_rsi(df)
139
  df['MiddleBB'], df['UpperBB'], df['LowerBB'] = calculate_bollinger_bands(df)
140
  df['SlowK'], df['SlowD'] = calculate_stochastic_oscillator(df)
141
  df['CMF'] = calculate_cmf(df)
142
 
143
- macd, signal = calculate_macd(df)
144
-
145
- # Generate signals (1 = buy, -1 = sell, 0 = neutral)
146
- df['RSI_Signal'] = np.where(df['RSI'] < 30, 1, np.where(df['RSI'] > 70, -1, 0))
147
- df['MACD_Signal'] = np.where((macd > signal) & (macd.shift(1) <= signal.shift(1)), 1,
148
- np.where((macd < signal) & (macd.shift(1) >= signal.shift(1)), -1, 0))
149
- df['BB_Signal'] = np.where(df['Close'] < df['LowerBB'], 1,
150
- np.where(df['Close'] > df['UpperBB'], -1, 0))
151
- df['Stochastic_Signal'] = np.where((df['SlowK'] < 20) & (df['SlowD'] < 20), 1,
152
- np.where((df['SlowK'] > 80) & (df['SlowD'] > 80), -1, 0))
153
- df['CMF_Signal'] = np.where(df['CMF'] > 0.2, 1, np.where(df['CMF'] < -0.2, -1, 0))
154
-
155
- # Combined technical signal
156
- technical_signals = ['RSI_Signal', 'MACD_Signal', 'BB_Signal', 'Stochastic_Signal', 'CMF_Signal']
157
- df['Technical_Score'] = df[technical_signals].sum(axis=1)
158
 
 
159
  return df
160
 
161
  # ============================================================================
162
- # FORECASTING COMPONENTS
163
  # ============================================================================
164
 
165
- def prophet_forecast_simple(df, days=30):
166
- """Simple Prophet forecast returning just the trend direction"""
167
  try:
168
- # Prepare data for Prophet
169
  prophet_df = df.reset_index()[['Date', 'Close']].rename(columns={'Date': 'ds', 'Close': 'y'})
170
  prophet_df['ds'] = pd.to_datetime(prophet_df['ds'])
171
-
172
- # Fit model
173
- model = Prophet(daily_seasonality=False, yearly_seasonality=False, weekly_seasonality=False)
174
  model.fit(prophet_df)
175
-
176
- # Make future dataframe
177
- future = model.make_future_dataframe(periods=days)
178
  forecast = model.predict(future)
179
-
180
- # Get current price and forecasted price
181
- current_price = prophet_df['y'].iloc[-1]
182
  future_price = forecast['yhat'].iloc[-1]
183
-
184
- # Calculate percentage change
185
- pct_change = ((future_price - current_price) / current_price) * 100
186
-
187
- return pct_change, future_price
188
-
189
  except Exception as e:
190
  print(f"Forecast error: {e}")
191
- return 0, 0
192
 
193
  # ============================================================================
194
- # INTEGRATED DECISION ENGINE
195
  # ============================================================================
196
 
197
- class TradingDecisionEngine:
198
- def __init__(self, symbol):
199
- self.symbol = symbol.upper()
200
- self.news_analyzer = StockNewsAnalyzer(symbol)
201
-
202
- def fetch_stock_data(self, start_date, end_date):
203
- """Fetch stock data"""
204
- try:
205
- stock_data = yf.download(self.symbol, start=start_date, end=end_date)
206
- if isinstance(stock_data.columns, pd.MultiIndex):
207
- stock_data.columns = stock_data.columns.droplevel(1)
208
- return stock_data
209
- except Exception as e:
210
- print(f"Error fetching stock data: {e}")
211
- return pd.DataFrame()
212
-
213
- def analyze_comprehensive(self, days_back=90):
214
- """Comprehensive analysis combining all factors"""
215
- try:
216
- # Fetch stock data
217
- end_date = datetime.now()
218
- start_date = end_date - timedelta(days=days_back)
219
-
220
- df = self.fetch_stock_data(start_date, end_date)
221
- if df.empty:
222
- return None
223
-
224
- # Technical analysis
225
- df = calculate_technical_signals(df)
226
-
227
- # Sentiment analysis
228
- sentiment_score, article_count = self.news_analyzer.get_sentiment_score(days=30)
229
-
230
- # Forecast
231
- forecast_change, forecast_price = prophet_forecast_simple(df, days=30)
232
-
233
- # Current metrics
234
- current_price = df['Close'].iloc[-1]
235
- current_technical = df['Technical_Score'].iloc[-1]
236
- current_rsi = df['RSI'].iloc[-1]
237
- current_macd, current_signal = calculate_macd(df)
238
-
239
- # Decision scoring
240
- scores = {
241
- 'sentiment': self._score_sentiment(sentiment_score),
242
- 'technical': self._score_technical(current_technical),
243
- 'forecast': self._score_forecast(forecast_change),
244
- 'rsi': self._score_rsi(current_rsi),
245
- 'momentum': self._score_momentum(df)
246
- }
247
-
248
- # Calculate final decision
249
- total_score = sum(scores.values())
250
- decision = self._make_decision(total_score)
251
-
252
- return {
253
- 'symbol': self.symbol,
254
- 'current_price': current_price,
255
- 'decision': decision,
256
- 'total_score': total_score,
257
- 'scores': scores,
258
- 'sentiment_score': sentiment_score,
259
- 'article_count': article_count,
260
- 'technical_score': current_technical,
261
- 'forecast_change': forecast_change,
262
- 'forecast_price': forecast_price,
263
- 'rsi': current_rsi,
264
- 'dataframe': df
265
- }
266
-
267
- except Exception as e:
268
- print(f"Analysis error: {e}")
269
- return None
270
-
271
- def _score_sentiment(self, sentiment):
272
- """Score sentiment from -2 to +2"""
273
- if sentiment > 0.3: return 2
274
- elif sentiment > 0.1: return 1
275
- elif sentiment > -0.1: return 0
276
- elif sentiment > -0.3: return -1
277
- else: return -2
278
-
279
- def _score_technical(self, technical_score):
280
- """Score technical indicators from -2 to +2"""
281
- if technical_score >= 3: return 2
282
- elif technical_score >= 1: return 1
283
- elif technical_score <= -3: return -2
284
- elif technical_score <= -1: return -1
285
- else: return 0
286
-
287
- def _score_forecast(self, forecast_change):
288
- """Score forecast from -2 to +2"""
289
- if forecast_change > 10: return 2
290
- elif forecast_change > 5: return 1
291
- elif forecast_change < -10: return -2
292
- elif forecast_change < -5: return -1
293
- else: return 0
294
-
295
- def _score_rsi(self, rsi):
296
- """Score RSI from -2 to +2"""
297
- if rsi < 20: return 2 # Very oversold - strong buy
298
- elif rsi < 30: return 1 # Oversold - buy
299
- elif rsi > 80: return -2 # Very overbought - strong sell
300
- elif rsi > 70: return -1 # Overbought - sell
301
- else: return 0
302
-
303
- def _score_momentum(self, df):
304
- """Score price momentum"""
305
- if len(df) < 10:
306
- return 0
307
-
308
- current = df['Close'].iloc[-1]
309
- week_ago = df['Close'].iloc[-5] if len(df) >= 5 else current
310
-
311
- change = ((current - week_ago) / week_ago) * 100
312
-
313
- if change > 5: return 1
314
- elif change < -5: return -1
315
- else: return 0
316
-
317
- def _make_decision(self, total_score):
318
- """Make final trading decision"""
319
- if total_score >= 5:
320
- return "STRONG BUY"
321
- elif total_score >= 2:
322
- return "BUY"
323
- elif total_score <= -5:
324
- return "STRONG SELL"
325
- elif total_score <= -2:
326
- return "SELL"
327
- else:
328
- return "HOLD"
329
-
330
- # ============================================================================
331
- # VISUALIZATION FUNCTIONS
332
- # ============================================================================
333
-
334
- def create_decision_dashboard(analysis_result):
335
- """Create comprehensive dashboard"""
336
- if not analysis_result:
337
- return None, None, None
338
-
339
- # Extract data
340
- symbol = analysis_result['symbol']
341
- decision = analysis_result['decision']
342
- scores = analysis_result['scores']
343
- df = analysis_result['dataframe']
344
-
345
- # 1. Decision Summary Chart
346
- fig_summary = create_summary_chart(analysis_result)
347
-
348
- # 2. Technical Analysis Chart
349
- fig_technical = create_technical_chart(df, symbol)
350
-
351
- # 3. Score Breakdown Chart
352
- fig_scores = create_scores_chart(scores, analysis_result['total_score'])
353
-
354
- return fig_summary, fig_technical, fig_scores
355
-
356
- def create_summary_chart(analysis):
357
- """Create summary dashboard"""
358
  fig = go.Figure()
359
-
360
- # Decision color mapping
361
- decision_colors = {
362
- 'STRONG BUY': '#00FF00',
363
- 'BUY': '#90EE90',
364
- 'HOLD': '#FFD700',
365
- 'SELL': '#FFA500',
366
- 'STRONG SELL': '#FF0000'
367
- }
368
-
369
- decision = analysis['decision']
370
- color = decision_colors.get(decision, '#FFD700')
371
-
372
- # Create gauge chart for decision strength
373
- fig.add_trace(go.Indicator(
374
- mode="gauge+number+delta",
375
- value=analysis['total_score'],
376
- domain={'x': [0, 1], 'y': [0, 1]},
377
- title={'text': f"{analysis['symbol']} - {decision}"},
378
- delta={'reference': 0},
379
- gauge={
380
- 'axis': {'range': [None, 10]},
381
- 'bar': {'color': color},
382
- 'steps': [
383
- {'range': [-10, -2], 'color': "lightgray"},
384
- {'range': [-2, 2], 'color': "gray"},
385
- {'range': [2, 10], 'color': "lightgreen"}
386
- ],
387
- 'threshold': {
388
- 'line': {'color': "red", 'width': 4},
389
- 'thickness': 0.75,
390
- 'value': 90
391
- }
392
- }
393
- ))
 
 
 
 
 
 
 
 
394
 
395
  fig.update_layout(
396
- paper_bgcolor='#111111',
397
- plot_bgcolor='#111111',
398
- font={'color': "white"},
399
- height=400
 
400
  )
401
-
402
  return fig
403
 
404
- def create_technical_chart(df, symbol):
405
- """Create technical analysis chart"""
406
- fig = make_subplots(rows=3, cols=1,
407
- subplot_titles=('Price & Bollinger Bands', 'RSI', 'MACD'),
408
- vertical_spacing=0.05,
409
- row_heights=[0.6, 0.2, 0.2])
410
-
411
- # Price and Bollinger Bands
412
- fig.add_trace(go.Scatter(x=df.index, y=df['Close'], name='Close Price',
413
- line=dict(color='#00FFFF', width=2)), row=1, col=1)
414
- fig.add_trace(go.Scatter(x=df.index, y=df['UpperBB'], name='Upper BB',
415
- line=dict(color='red', dash='dash')), row=1, col=1)
416
- fig.add_trace(go.Scatter(x=df.index, y=df['LowerBB'], name='Lower BB',
417
- line=dict(color='red', dash='dash')), row=1, col=1)
418
-
419
- # RSI
420
- fig.add_trace(go.Scatter(x=df.index, y=df['RSI'], name='RSI',
421
- line=dict(color='#FF6B6B')), row=2, col=1)
422
- fig.add_hline(y=70, line_dash="dash", line_color="red", row=2, col=1)
423
- fig.add_hline(y=30, line_dash="dash", line_color="green", row=2, col=1)
424
-
425
- # MACD
426
- macd, signal = calculate_macd(df)
427
- fig.add_trace(go.Scatter(x=df.index, y=macd, name='MACD',
428
- line=dict(color='#4ECDC4')), row=3, col=1)
429
- fig.add_trace(go.Scatter(x=df.index, y=signal, name='Signal',
430
- line=dict(color='#FFE66D')), row=3, col=1)
431
-
432
- fig.update_layout(
433
- title=f'{symbol} Technical Analysis',
434
- plot_bgcolor='#111111',
435
- paper_bgcolor='#111111',
436
- font=dict(color='white'),
437
- height=800,
438
- showlegend=False
439
- )
440
-
441
  return fig
442
 
443
- def create_scores_chart(scores, total_score):
444
- """Create score breakdown chart"""
445
- categories = list(scores.keys())
446
- values = list(scores.values())
447
- colors = ['#00FF00' if v > 0 else '#FF0000' if v < 0 else '#FFD700' for v in values]
448
-
449
- fig = go.Figure(data=[
450
- go.Bar(x=categories, y=values, marker_color=colors, text=values, textposition='auto')
451
- ])
452
-
453
- fig.update_layout(
454
- title=f'Score Breakdown (Total: {total_score})',
455
- plot_bgcolor='#111111',
456
- paper_bgcolor='#111111',
457
- font=dict(color='white'),
458
- yaxis=dict(range=[-3, 3]),
459
- height=400
460
- )
461
-
462
  return fig
463
 
464
  # ============================================================================
465
- # GRADIO INTERFACE
466
  # ============================================================================
467
 
468
- def analyze_stock_comprehensive(symbol, days_back):
469
- """Main analysis function for Gradio"""
470
  try:
471
- if not symbol:
472
- return "Please enter a valid stock symbol.", None, None, None, "No data available."
 
473
 
474
- # Create decision engine
475
- engine = TradingDecisionEngine(symbol)
 
 
 
 
 
 
476
 
477
- # Run comprehensive analysis
478
- analysis = engine.analyze_comprehensive(days_back)
479
 
480
- if not analysis:
481
- return "Unable to analyze this stock. Please check the symbol and try again.", None, None, None, "No data available."
 
 
482
 
483
- # Create visualizations
484
- fig_summary, fig_technical, fig_scores = create_decision_dashboard(analysis)
 
 
 
 
485
 
486
- # Create summary text
487
- summary = f"""
488
- # {analysis['symbol']} Trading Analysis
489
-
490
- ## 🎯 RECOMMENDATION: {analysis['decision']}
491
- **Current Price:** ${analysis['current_price']:.2f}
492
- **Overall Score:** {analysis['total_score']}/10
493
-
494
- ### 📊 Analysis Breakdown:
495
- - **Sentiment Score:** {analysis['sentiment_score']:.3f} ({analysis['article_count']} articles)
496
- - **Technical Score:** {analysis['technical_score']}
497
- - **Forecast:** {analysis['forecast_change']:.1f}% (Target: ${analysis['forecast_price']:.2f})
498
- - **RSI:** {analysis['rsi']:.1f}
499
-
500
- ### 🔍 Individual Scores:
501
- - **Sentiment:** {analysis['scores']['sentiment']}/2
502
- - **Technical:** {analysis['scores']['technical']}/2
503
- - **Forecast:** {analysis['scores']['forecast']}/2
504
- - **RSI:** {analysis['scores']['rsi']}/2
505
- - **Momentum:** {analysis['scores']['momentum']}/2
506
-
507
- ### 📈 Decision Logic:
508
- - **Strong Buy (≥5):** Multiple positive signals align
509
- - **Buy (≥2):** More positive than negative signals
510
- - **Hold (-1 to 1):** Mixed or neutral signals
511
- - **Sell (≤-2):** More negative than positive signals
512
- - **Strong Sell (≤-5):** Multiple negative signals align
513
- """
514
-
515
- return summary, fig_summary, fig_technical, fig_scores, "Analysis completed successfully!"
516
-
517
- except Exception as e:
518
- return f"Error during analysis: {str(e)}", None, None, None, "Analysis failed."
519
-
520
- # Build Gradio interface
521
- def build_interface():
522
- """Create the integrated Gradio interface"""
523
- with gr.Blocks(title="Integrated Trading Decision App", theme=gr.themes.Soft()) as app:
524
- gr.Markdown("# 📈 Integrated Stock Trading Decision System")
525
- gr.Markdown("**Combines sentiment analysis, technical indicators, and AI forecasting for comprehensive trading decisions**")
526
-
527
- with gr.Row():
528
- with gr.Column(scale=1):
529
- symbol_input = gr.Textbox(
530
- label="Stock Symbol",
531
- value="AAPL",
532
- placeholder="e.g., AAPL, MSFT, GOOGL, TSLA"
533
- )
534
- days_input = gr.Slider(
535
- label="Analysis Period (Days)",
536
- minimum=30,
537
- maximum=365,
538
- value=90,
539
- step=1
540
- )
541
- analyze_button = gr.Button("🔍 Analyze Stock", variant="primary", size="lg")
542
 
543
- # Status
544
- status = gr.Textbox(label="Status", interactive=False)
545
-
546
- # Summary
547
- summary_text = gr.Markdown()
 
 
 
 
 
548
 
549
- # Charts
550
- with gr.Row():
551
- decision_chart = gr.Plot(label="📊 Decision Summary")
552
- scores_chart = gr.Plot(label="📈 Score Breakdown")
553
 
554
- technical_chart = gr.Plot(label="📉 Technical Analysis")
 
 
 
 
 
 
 
555
 
556
- # Set up event handler
557
- analyze_button.click(
558
- fn=analyze_stock_comprehensive,
559
- inputs=[symbol_input, days_input],
560
- outputs=[summary_text, decision_chart, technical_chart, scores_chart, status]
561
- )
562
 
563
- return app
 
564
 
565
- # Main function
566
- def main():
567
- app = build_interface()
568
- app.launch(share=True, debug=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
  if __name__ == "__main__":
571
- main()
 
14
 
15
  warnings.filterwarnings('ignore')
16
 
17
+ # ============================================================================
18
+ # CONFIGURATION (FIXED DATES)
19
+ # ============================================================================
20
+
21
+ TECH_START = "2022-01-01"
22
+ TECH_END = "2026-01-01"
23
+ SENTIMENT_DAYS = 90 # Always last 90 days
24
+
25
  class Config:
26
+ FINNHUB_API_KEY = "cuj17q1r01qm7p9n307gcuj17q1r01qm7p9n3080" # Replace with your key
 
27
  DATA_DIR = "data"
28
 
29
  @classmethod
 
33
  Config.initialize()
34
 
35
  # ============================================================================
36
+ # SENTIMENT ANALYSIS (90 DAYS ONLY)
37
  # ============================================================================
38
 
39
  class SentimentAnalyzer:
 
53
  def get_file_path(self, file_type):
54
  return os.path.join(Config.DATA_DIR, f"{self.symbol}_{file_type}.csv")
55
 
56
+ def get_news(self, force_refresh=False):
57
  file_path = self.get_file_path("news")
 
58
  if os.path.exists(file_path) and not force_refresh:
59
  try:
60
  return pd.read_csv(file_path, parse_dates=['datetime'])
 
62
  pass
63
 
64
  end_date = datetime.now()
65
+ start_date = end_date - timedelta(days=SENTIMENT_DAYS)
66
 
67
+ url = "https://finnhub.io/api/v1/company-news" # FIXED: no trailing spaces
68
  params = {
69
  "symbol": self.symbol,
70
  "from": start_date.strftime('%Y-%m-%d'),
 
75
  try:
76
  response = requests.get(url, params=params, timeout=10)
77
  data = response.json()
 
78
  if not data or not isinstance(data, list):
79
  return pd.DataFrame()
 
80
  df = pd.DataFrame(data)
81
  if 'datetime' in df.columns:
82
  df['datetime'] = pd.to_datetime(df['datetime'], unit='s')
 
87
  print(f"Error fetching news: {e}")
88
  return pd.DataFrame()
89
 
90
+ def get_sentiment_data(self):
91
+ news_df = self.get_news()
92
+ if news_df.empty or 'headline' not in news_df.columns:
93
+ return None, None
94
+
95
+ news_df['sentiment_score'] = news_df['headline'].apply(self.sentiment_analyzer.analyze)
96
+ news_df['date'] = pd.to_datetime(news_df['datetime'].dt.date)
97
 
98
+ daily = news_df.groupby('date').agg(
99
+ avg_sentiment=('sentiment_score', 'mean'),
100
+ article_count=('sentiment_score', 'count'),
101
+ positive_count=('sentiment_score', lambda x: sum(x > 0.05)),
102
+ negative_count=('sentiment_score', lambda x: sum(x < -0.05)),
103
+ neutral_count=('sentiment_score', lambda x: sum((x >= -0.05) & (x <= 0.05)))
104
+ ).reset_index()
105
+
106
+ return daily, news_df
107
 
108
  # ============================================================================
109
+ # TECHNICAL INDICATORS (ULTRA-STRICT, FIXED DATES)
110
  # ============================================================================
111
 
112
+ def calculate_rsi(df):
113
  delta = df['Close'].diff()
114
+ gain = (delta.where(delta > 0, 0)).rolling(14).mean()
115
+ loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
116
  rs = gain / loss
117
+ return 100 - (100 / (1 + rs))
 
 
 
 
 
 
 
 
118
 
119
+ def calculate_bollinger_bands(df):
120
+ ma = df['Close'].rolling(20).mean()
121
+ std = df['Close'].rolling(20).std()
122
+ return ma, ma + 2*std, ma - 2*std
 
123
 
124
  def calculate_stochastic_oscillator(df):
125
+ ll = df['Low'].rolling(14).min()
126
+ hh = df['High'].rolling(14).max()
127
+ k = ((df['Close'] - ll) / (hh - ll)) * 100
128
+ d = k.rolling(3).mean()
129
+ return k, d
130
 
131
  def calculate_cmf(df, window=20):
132
  mfv = ((df['Close'] - df['Low']) - (df['High'] - df['Close'])) / (df['High'] - df['Low']) * df['Volume']
133
+ return mfv.rolling(window).sum() / df['Volume'].rolling(window).sum()
 
134
 
135
+ def generate_signals(df):
 
136
  df['RSI'] = calculate_rsi(df)
137
  df['MiddleBB'], df['UpperBB'], df['LowerBB'] = calculate_bollinger_bands(df)
138
  df['SlowK'], df['SlowD'] = calculate_stochastic_oscillator(df)
139
  df['CMF'] = calculate_cmf(df)
140
 
141
+ # Ultra-strict thresholds
142
+ df['RSI_Signal'] = np.where(df['RSI'] < 15, 1, np.where(df['RSI'] > 90, -1, 0))
143
+ df['BB_Signal'] = np.where(df['Close'] < df['LowerBB'] * 0.97, 1, np.where(df['Close'] > df['UpperBB'] * 1.03, -1, 0))
144
+ df['Stochastic_Signal'] = np.where((df['SlowK'] < 10) & (df['SlowD'] < 10), 1, np.where((df['SlowK'] > 95) & (df['SlowD'] > 95), -1, 0))
145
+ df['CMF_Signal'] = np.where(df['CMF'] < -0.4, 1, np.where(df['CMF'] > 0.4, -1, 0))
 
 
 
 
 
 
 
 
 
 
146
 
147
+ df['Technical_Score'] = df[['RSI_Signal', 'BB_Signal', 'Stochastic_Signal', 'CMF_Signal']].sum(axis=1)
148
  return df
149
 
150
  # ============================================================================
151
+ # FORECASTING (PROPHET, FIXED DATES)
152
  # ============================================================================
153
 
154
+ def prophet_forecast(df):
 
155
  try:
 
156
  prophet_df = df.reset_index()[['Date', 'Close']].rename(columns={'Date': 'ds', 'Close': 'y'})
157
  prophet_df['ds'] = pd.to_datetime(prophet_df['ds'])
158
+ model = Prophet()
 
 
159
  model.fit(prophet_df)
160
+ future = model.make_future_dataframe(periods=30)
 
 
161
  forecast = model.predict(future)
162
+ current = prophet_df['y'].iloc[-1]
 
 
163
  future_price = forecast['yhat'].iloc[-1]
164
+ pct_change = ((future_price - current) / current) * 100
165
+ return pct_change, future_price, forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']]
 
 
 
 
166
  except Exception as e:
167
  print(f"Forecast error: {e}")
168
+ return 0, 0, None
169
 
170
  # ============================================================================
171
+ # VISUALIZATIONS
172
  # ============================================================================
173
 
174
+ def create_multi_ticker_plot(data_dict, show_bollinger, time_range):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  fig = go.Figure()
176
+ COLORS = px.colors.qualitative.Plotly
177
+ all_dates = pd.concat([df.index.to_series() for df in data_dict.values()], ignore_index=True)
178
+ if all_dates.empty:
179
+ return fig
180
+ end = all_dates.max()
181
+ start = {
182
+ "1M": end - pd.DateOffset(months=1),
183
+ "3M": end - pd.DateOffset(months=3),
184
+ "6M": end - pd.DateOffset(months=6),
185
+ "1Y": end - pd.DateOffset(years=1),
186
+ "YTD": pd.to_datetime(f"{end.year}-01-01"),
187
+ "All": all_dates.min()
188
+ }[time_range]
189
+
190
+ buy_points, sell_points = [], []
191
+ for i, (ticker, df) in enumerate(data_dict.items()):
192
+ df_plot = df[df.index >= start]
193
+ if df_plot.empty:
194
+ continue
195
+ color = COLORS[i % len(COLORS)]
196
+ fig.add_trace(go.Scatter(x=df_plot.index, y=df_plot['Close'], mode='lines', line=dict(color=color, width=1.8), name=ticker))
197
+ if show_bollinger:
198
+ fig.add_trace(go.Scatter(x=df_plot.index, y=df_plot['UpperBB'], mode='lines', line=dict(color='rgba(150,150,150,0.4)', width=1, dash='dot'), showlegend=False, hoverinfo='skip'))
199
+ fig.add_trace(go.Scatter(x=df_plot.index, y=df_plot['LowerBB'], mode='lines', line=dict(color='rgba(150,150,150,0.4)', width=1, dash='dot'), fill='tonexty', fillcolor='rgba(150,150,150,0.05)', showlegend=False, hoverinfo='skip'))
200
+ for date in df_plot.index:
201
+ signals = [('RSI', df_plot.loc[date, 'RSI_Signal']), ('BB', df_plot.loc[date, 'BB_Signal']), ('Stochastic', df_plot.loc[date, 'Stochastic_Signal']), ('CMF', df_plot.loc[date, 'CMF_Signal'])]
202
+ total = sum(sig for _, sig in signals)
203
+ price = df_plot.loc[date, 'Close']
204
+ if total > 0:
205
+ active = [name for name, sig in signals if sig == 1]
206
+ hover = f"<b>{ticker}</b><br>Buy: {', '.join(active)}<br>{date.strftime('%Y-%m-%d')}<br>${price:.2f}"
207
+ buy_points.append((date, price * 0.997, hover))
208
+ elif total < 0:
209
+ active = [name for name, sig in signals if sig == -1]
210
+ hover = f"<b>{ticker}</b><br>Sell: {', '.join(active)}<br>{date.strftime('%Y-%m-%d')}<br>${price:.2f}"
211
+ sell_points.append((date, price * 1.003, hover))
212
+
213
+ if buy_points:
214
+ x, y, text = zip(*buy_points)
215
+ fig.add_trace(go.Scatter(x=x, y=y, mode='markers', marker=dict(symbol='triangle-up', size=9, color='white', line=dict(color='black', width=0.8)), hovertext=text, hoverinfo='text', showlegend=False))
216
+ if sell_points:
217
+ x, y, text = zip(*sell_points)
218
+ fig.add_trace(go.Scatter(x=x, y=y, mode='markers', marker=dict(symbol='triangle-down', size=9, color='black', line=dict(color='white', width=0.8)), hovertext=text, hoverinfo='text', showlegend=False))
219
 
220
  fig.update_layout(
221
+ plot_bgcolor='black', paper_bgcolor='black', font=dict(color='white'),
222
+ xaxis=dict(showgrid=False, zeroline=False, showline=False, ticks=''),
223
+ yaxis=dict(showgrid=False, zeroline=False, showline=False, ticks='', tickprefix='$'),
224
+ legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='center', x=0.5, bgcolor='rgba(0,0,0,0.6)'),
225
+ margin=dict(l=20, r=20, t=30, b=30), height=700, width=1100, hovermode='x unified'
226
  )
 
227
  return fig
228
 
229
+ def create_sentiment_plot(sentiment_daily, stock_data, symbol):
230
+ if sentiment_daily is None:
231
+ return None
232
+ fig = make_subplots(rows=2, cols=1, specs=[[{"secondary_y": True}], [{}]], row_heights=[0.7, 0.3])
233
+ if not stock_data.empty:
234
+ fig.add_trace(go.Scatter(x=stock_data.index, y=stock_data['Close'], name='Price', line=dict(color='#1f77b4')), row=1, col=1, secondary_y=False)
235
+ fig.add_trace(go.Scatter(x=sentiment_daily['date'], y=sentiment_daily['avg_sentiment'], name='Sentiment', line=dict(color='#ff7f0e')), row=1, col=1, secondary_y=True)
236
+ fig.add_trace(go.Bar(x=sentiment_daily['date'], y=sentiment_daily['article_count'], name='Articles', marker_color='rgba(135,206,235,0.5)'), row=2, col=1)
237
+ fig.add_trace(go.Bar(x=sentiment_daily['date'], y=sentiment_daily['positive_count'], name='Positive', marker_color='rgba(0,128,0,0.7)'), row=2, col=1)
238
+ fig.add_trace(go.Bar(x=sentiment_daily['date'], y=sentiment_daily['negative_count'], name='Negative', marker_color='rgba(255,0,0,0.7)'), row=2, col=1)
239
+ fig.add_trace(go.Bar(x=sentiment_daily['date'], y=sentiment_daily['neutral_count'], name='Neutral', marker_color='rgba(128,128,128,0.7)'), row=2, col=1)
240
+ fig.update_layout(title=f"{symbol} News Sentiment (Last {SENTIMENT_DAYS} Days)", template='plotly_white', barmode='stack', height=600)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  return fig
242
 
243
+ def create_decision_gauge(decision, total_score):
244
+ colors = {'STRONG BUY': '#00FF00', 'BUY': '#90EE90', 'HOLD': '#FFD700', 'SELL': '#FFA500', 'STRONG SELL': '#FF0000'}
245
+ fig = go.Figure(go.Indicator(
246
+ mode="gauge+number", value=total_score,
247
+ title={'text': f"{decision}", 'font': {'size': 24}},
248
+ gauge={'axis': {'range': [-6, 6]}, 'bar': {'color': colors.get(decision, '#FFD700')},
249
+ 'steps': [{'range': [-6, -2], 'color': 'red'}, {'range': [-2, 2], 'color': 'gray'}, {'range': [2, 6], 'color': 'green'}]}
250
+ ))
251
+ fig.update_layout(paper_bgcolor='black', plot_bgcolor='black', font=dict(color='white', size=16), height=300)
 
 
 
 
 
 
 
 
 
 
252
  return fig
253
 
254
  # ============================================================================
255
+ # MAIN ANALYSIS FUNCTION
256
  # ============================================================================
257
 
258
+ def run_analysis(tickers_str, show_bollinger, time_range, refresh_sentiment):
 
259
  try:
260
+ tickers = [t.strip().upper() for t in tickers_str.split(',') if t.strip()][:8]
261
+ if not tickers:
262
+ return "Enter at least one ticker", None, None, None, None, "No ticker provided"
263
 
264
+ # === TECHNICAL ANALYSIS (FIXED DATES) ===
265
+ data_dict = {}
266
+ for t in tickers:
267
+ df = yf.download(t, start=TECH_START, end=TECH_END)
268
+ if not df.empty:
269
+ if isinstance(df.columns, pd.MultiIndex):
270
+ df.columns = df.columns.droplevel(1)
271
+ data_dict[t] = generate_signals(df)
272
 
273
+ if not data_dict:
274
+ return "No technical data found", None, None, None, None, "No data"
275
 
276
+ # === SENTIMENT & FORECAST (FIRST TICKER ONLY) ===
277
+ first_ticker = tickers[0]
278
+ news_analyzer = StockNewsAnalyzer(first_ticker)
279
+ sentiment_daily, _ = news_analyzer.get_sentiment_data()
280
 
281
+ # Get stock data for sentiment plot (last 90 days)
282
+ end_now = datetime.now()
283
+ start_90 = end_now - timedelta(days=90)
284
+ stock_90d = yf.download(first_ticker, start=start_90, end=end_now)
285
+ if isinstance(stock_90d.columns, pd.MultiIndex):
286
+ stock_90d.columns = stock_90d.columns.droplevel(1)
287
 
288
+ # Forecast
289
+ tech_df = data_dict[first_ticker]
290
+ forecast_change, forecast_price, _ = prophet_forecast(tech_df)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
+ # Scoring
293
+ current_technical = tech_df['Technical_Score'].iloc[-1]
294
+ avg_sentiment = sentiment_daily['avg_sentiment'].mean() if sentiment_daily is not None else 0
295
+ scores = {
296
+ 'technical': 2 if current_technical >= 3 else 1 if current_technical >= 1 else -1 if current_technical <= -1 else -2 if current_technical <= -3 else 0,
297
+ 'sentiment': 2 if avg_sentiment > 0.3 else 1 if avg_sentiment > 0.1 else -1 if avg_sentiment < -0.1 else -2 if avg_sentiment < -0.3 else 0,
298
+ 'forecast': 2 if forecast_change > 8 else 1 if forecast_change > 3 else -1 if forecast_change < -3 else -2 if forecast_change < -8 else 0
299
+ }
300
+ total_score = sum(scores.values())
301
+ decision = "STRONG BUY" if total_score >= 4 else "BUY" if total_score >= 2 else "SELL" if total_score <= -2 else "STRONG SELL" if total_score <= -4 else "HOLD"
302
 
303
+ # === PLOTS ===
304
+ technical_plot = create_multi_ticker_plot(data_dict, show_bollinger, time_range)
305
+ sentiment_plot = create_sentiment_plot(sentiment_daily, stock_90d, first_ticker) if sentiment_daily is not None else None
306
+ decision_gauge = create_decision_gauge(decision, total_score)
307
 
308
+ summary = f"""
309
+ ## 🎯 Decision: **{decision}**
310
+ - **Ticker**: {first_ticker}
311
+ - **Current Price**: ${tech_df['Close'].iloc[-1]:.2f}
312
+ - **Total Score**: {total_score}/6
313
+ - **Sentiment**: {avg_sentiment:.2f} ({sentiment_daily['article_count'].sum() if sentiment_daily is not None else 0} articles)
314
+ - **Forecast**: {forecast_change:.1f}% → ${forecast_price:.2f}
315
+ """
316
 
317
+ return summary, technical_plot, sentiment_plot, decision_gauge, None, "Success"
 
 
 
 
 
318
 
319
+ except Exception as e:
320
+ return f"Error: {str(e)}", None, None, None, None, "Failed"
321
 
322
+ # ============================================================================
323
+ # GRADIO INTERFACE
324
+ # ============================================================================
325
+
326
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
327
+ gr.Markdown("# 📊 Unified Stock Intelligence Dashboard")
328
+ gr.Markdown(f"**Technical Data**: {TECH_START} to {TECH_END} | **Sentiment**: Last {SENTIMENT_DAYS} days")
329
+
330
+ with gr.Row():
331
+ with gr.Column():
332
+ tickers_input = gr.Textbox(label="Tickers (comma-separated, max 8)", value="NVDA, AAPL, MSFT")
333
+ with gr.Row():
334
+ show_bb = gr.Checkbox(label="Show Bollinger Bands", value=False)
335
+ time_range = gr.Radio(choices=["1M", "3M", "6M", "1Y", "YTD", "All"], value="1Y", label="Time Range")
336
+ refresh_sentiment = gr.Checkbox(label="Refresh News Data", value=False)
337
+ analyze_btn = gr.Button("Analyze", variant="primary")
338
+
339
+ status = gr.Textbox(label="Status", interactive=False)
340
+ summary = gr.Markdown()
341
+
342
+ with gr.Row():
343
+ decision_gauge = gr.Plot(label="Decision")
344
+ sentiment_plot = gr.Plot(label="Sentiment Analysis")
345
+
346
+ technical_plot = gr.Plot(label="Multi-Ticker Technical Signals")
347
+
348
+ analyze_btn.click(
349
+ run_analysis,
350
+ inputs=[tickers_input, show_bb, time_range, refresh_sentiment],
351
+ outputs=[summary, technical_plot, sentiment_plot, decision_gauge, gr.Plot(), status]
352
+ )
353
 
354
  if __name__ == "__main__":
355
+ demo.launch()