JayLacoma commited on
Commit
ad0e75b
·
verified ·
1 Parent(s): 9d0e359

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +419 -345
app.py CHANGED
@@ -1,39 +1,45 @@
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
  from prophet import Prophet
12
- import plotly.express as px
13
  import warnings
14
- from typing import Optional
15
-
16
- # Ignore common warnings
17
- warnings.filterwarnings('ignore')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  # ============================================================================
20
- # ⚙️ CONFIGURATION & SETUP
21
  # ============================================================================
22
  class Config:
23
- FINNHUB_API_KEY = "cuj17q1r01qm7p9n307gcuj17q1r01qm7p9n3080"
24
  DATA_DIR = "data_cache"
25
  CACHE_TTL_HOURS = 12
26
  SENTIMENT_DAYS = 90
27
  TECH_DATA_YEARS = 3
28
 
29
- PLOT_TEMPLATE = "plotly_dark"
30
- PRIMARY_COLOR = "#00BFFF"
31
- SENTIMENT_POSITIVE_COLOR = "rgba(0, 204, 102, 0.7)"
32
- SENTIMENT_NEGATIVE_COLOR = "rgba(255, 51, 51, 0.7)"
33
- SENTIMENT_NEUTRAL_COLOR = "rgba(128, 128, 128, 0.6)"
34
- BOLLINGER_FILL_COLOR = "rgba(255, 255, 255, 0.1)"
35
- BOLLINGER_LINE_STYLE = dict(color="rgba(255, 255, 255, 0.3)", width=1, dash='dot')
36
-
37
  @classmethod
38
  def initialize(cls):
39
  os.makedirs(cls.DATA_DIR, exist_ok=True)
@@ -41,7 +47,7 @@ class Config:
41
  Config.initialize()
42
 
43
  # ============================================================================
44
- # 📦 DATA CACHING
45
  # ============================================================================
46
  class CacheManager:
47
  @staticmethod
@@ -50,384 +56,452 @@ class CacheManager:
50
 
51
  @staticmethod
52
  def save_df(df: pd.DataFrame, filename: str):
53
- df.to_csv(CacheManager.get_path(filename))
54
 
55
  @staticmethod
56
  def load_df(filename: str) -> Optional[pd.DataFrame]:
57
  path = CacheManager.get_path(filename)
58
  if not os.path.exists(path):
59
  return None
60
-
61
  file_mod_time = datetime.fromtimestamp(os.path.getmtime(path))
62
  if datetime.now() - file_mod_time > timedelta(hours=Config.CACHE_TTL_HOURS):
63
  return None
64
-
65
  try:
66
  df = pd.read_csv(path)
67
  for col in df.columns:
68
  if 'date' in col.lower():
69
  df[col] = pd.to_datetime(df[col])
70
- if 'Date' in df.columns and df.columns[0] == 'Date':
71
- df.set_index('Date', inplace=True)
72
  return df
73
  except Exception:
74
  return None
75
 
76
  # ============================================================================
77
- # 🧠 CORE ANALYSIS LOGIC
78
  # ============================================================================
79
- class StockAnalyzer:
80
- _sentiment_analyzer = SentimentIntensityAnalyzer()
81
-
82
- def __init__(self, ticker: str, force_refresh: bool = False):
83
- self.ticker = ticker.upper()
84
- self.force_refresh = force_refresh
85
- self.tech_df = self._get_technical_data()
86
- self.sentiment_daily, _ = self._get_sentiment_data()
87
- self.forecast_pct, self.forecast_price, self.forecast_df = self._get_forecast()
88
- self.scores, self.decision, self.total_score = self._calculate_decision()
89
-
90
- def _get_technical_data(self) -> pd.DataFrame:
91
- cache_file = f"{self.ticker}_technical.csv"
92
- df = CacheManager.load_df(cache_file)
93
- if df is None or self.force_refresh:
94
- end_date = datetime.now()
95
- start_date = end_date - timedelta(days=365 * Config.TECH_DATA_YEARS)
96
- df = yf.download(self.ticker, start=start_date, end=end_date)
97
- if df.empty:
98
- return pd.DataFrame()
99
- df = self._calculate_indicators(df)
100
- CacheManager.save_df(df.reset_index(), cache_file)
101
- return df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
- def _get_sentiment_data(self):
104
- cache_file = f"{self.ticker}_sentiment.csv"
105
- df_daily = CacheManager.load_df(cache_file)
106
- if df_daily is not None and not self.force_refresh:
107
- return df_daily, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  end_date = datetime.now()
110
- start_date = end_date - timedelta(days=Config.SENTIMENT_DAYS)
 
 
 
 
 
 
 
 
111
  try:
112
- # FIXED: Removed trailing spaces in URL!
113
- res = requests.get(
114
- "https://finnhub.io/api/v1/company-news",
115
- params={
116
- "symbol": self.ticker,
117
- "from": start_date.strftime('%Y-%m-%d'),
118
- "to": end_date.strftime('%Y-%m-%d'),
119
- "token": Config.FINNHUB_API_KEY
120
- },
121
- timeout=10
122
- )
123
- res.raise_for_status()
124
- news = res.json()
125
- if not news or not isinstance(news, list):
126
- return None, None
127
- except requests.RequestException:
128
- return None, None
129
 
130
- news_df = pd.DataFrame(news)
131
- news_df['datetime'] = pd.to_datetime(news_df['datetime'], unit='s')
132
- news_df['sentiment'] = news_df['headline'].apply(
133
- lambda text: self._sentiment_analyzer.polarity_scores(text)['compound']
134
- )
135
  news_df['date'] = pd.to_datetime(news_df['datetime'].dt.date)
136
-
137
- daily_sentiment = news_df.groupby('date').agg(
138
  avg_sentiment=('sentiment', 'mean'),
139
  article_count=('sentiment', 'count'),
140
  positive_count=('sentiment', lambda x: (x > 0.05).sum()),
141
  negative_count=('sentiment', lambda x: (x < -0.05).sum()),
142
  neutral_count=('sentiment', lambda x: ((x >= -0.05) & (x <= 0.05)).sum())
143
  ).reset_index()
 
144
 
145
- CacheManager.save_df(daily_sentiment, cache_file)
146
- return daily_sentiment, news_df
147
-
148
- def _get_forecast(self):
149
- if self.tech_df.empty:
150
- return 0, 0, None
151
- try:
152
- prophet_df = self.tech_df.reset_index()[['Date', 'Close']].rename(columns={'Date': 'ds', 'Close': 'y'})
153
- model = Prophet(daily_seasonality=True)
154
- model.fit(prophet_df)
155
- future = model.make_future_dataframe(periods=30)
156
- forecast = model.predict(future)
157
- current_price = prophet_df['y'].iloc[-1]
158
- future_price = forecast['yhat'].iloc[-1]
159
- pct_change = ((future_price - current_price) / current_price) * 100
160
- return pct_change, future_price, forecast
161
- except Exception:
162
- return 0, 0, None
163
-
164
- def _calculate_decision(self):
165
- tech_score = 0
166
- if not self.tech_df.empty:
167
- last_signal = self.tech_df['Technical_Score'].iloc[-1]
168
- if last_signal >= 3: tech_score = 2
169
- elif last_signal >= 1: tech_score = 1
170
- elif last_signal <= -1: tech_score = -1
171
- elif last_signal <= -3: tech_score = -2
172
-
173
- sentiment_score = 0
174
- if self.sentiment_daily is not None:
175
- avg_sentiment = self.sentiment_daily['avg_sentiment'].mean()
176
- if avg_sentiment > 0.3: sentiment_score = 2
177
- elif avg_sentiment > 0.1: sentiment_score = 1
178
- elif avg_sentiment < -0.1: sentiment_score = -1
179
- elif avg_sentiment < -0.3: sentiment_score = -2
180
-
181
- forecast_score = 0
182
- if self.forecast_pct > 8: forecast_score = 2
183
- elif self.forecast_pct > 3: forecast_score = 1
184
- elif self.forecast_pct < -3: forecast_score = -1
185
- elif self.forecast_pct < -8: forecast_score = -2
186
-
187
- scores = {'Technical': tech_score, 'Sentiment': sentiment_score, 'Forecast': forecast_score}
188
- total_score = sum(scores.values())
189
-
190
- if total_score >= 4: decision = "STRONG BUY"
191
- elif total_score >= 2: decision = "BUY"
192
- elif total_score <= -2: decision = "SELL"
193
- elif total_score <= -4: decision = "STRONG SELL"
194
- else: decision = "HOLD"
195
-
196
- return scores, decision, total_score
197
-
198
- @staticmethod
199
- def _calculate_indicators(df: pd.DataFrame) -> pd.DataFrame:
200
- df = df.copy()
201
- # RSI
202
- delta = df['Close'].diff()
203
- gain = (delta.where(delta > 0, 0)).rolling(14).mean()
204
- loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
205
- rs = gain / loss
206
- df['RSI'] = 100 - (100 / (1 + rs))
207
-
208
- # Bollinger Bands
209
- ma = df['Close'].rolling(20).mean()
210
- std = df['Close'].rolling(20).std()
211
- df['UpperBB'] = ma + 2 * std
212
- df['LowerBB'] = ma - 2 * std
213
-
214
- # Stochastic
215
- ll = df['Low'].rolling(14).min()
216
- hh = df['High'].rolling(14).max()
217
- df['SlowK'] = ((df['Close'] - ll) / (hh - ll)) * 100
218
- df['SlowD'] = df['SlowK'].rolling(3).mean()
219
-
220
- # CMF
221
- price_range = df['High'] - df['Low']
222
- price_range = price_range.replace(0, np.nan)
223
- mfv = ((df['Close'] - df['Low']) - (df['High'] - df['Close'])) / price_range * df['Volume']
224
- mfv_sum = mfv.rolling(20).sum()
225
- vol_sum = df['Volume'].rolling(20).sum()
226
- cmf_raw = mfv_sum.values / vol_sum.values
227
- cmf_clean = np.where(np.isfinite(cmf_raw), cmf_raw, np.nan)
228
- df['CMF'] = cmf_clean
229
-
230
- # Signals
231
- df['RSI_Signal'] = np.where(df['RSI'] < 20, 1, np.where(df['RSI'] > 80, -1, 0))
232
- df['BB_Signal'] = np.where(df['Close'] < df['LowerBB'], 1, np.where(df['Close'] > df['UpperBB'], -1, 0))
233
- df['Stochastic_Signal'] = np.where((df['SlowK'] < 15) & (df['SlowD'] < 15), 1, np.where((df['SlowK'] > 85) & (df['SlowD'] > 85), -1, 0))
234
- df['CMF_Signal'] = np.where(df['CMF'] < -0.25, 1, np.where(df['CMF'] > 0.25, -1, 0))
235
- df['Technical_Score'] = df[['RSI_Signal', 'BB_Signal', 'Stochastic_Signal', 'CMF_Signal']].sum(axis=1)
236
-
237
- return df
238
 
239
  # ============================================================================
240
- # 📈 PLOTTING FUNCTIONS
241
  # ============================================================================
242
- class Plotter:
243
- @staticmethod
244
- def create_multi_ticker_plot(data_dict, show_bollinger, time_range):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  fig = go.Figure()
246
- colors = px.colors.qualitative.Plotly
247
-
248
- all_dates = pd.concat([df.index.to_series() for df in data_dict.values()]).unique()
249
- if len(all_dates) == 0:
250
- return fig
251
- max_date = all_dates.max()
252
- range_map = {
253
- "1M": max_date - pd.DateOffset(months=1),
254
- "3M": max_date - pd.DateOffset(months=3),
255
- "6M": max_date - pd.DateOffset(months=6),
256
- "1Y": max_date - pd.DateOffset(years=1),
257
- "YTD": pd.to_datetime(f"{max_date.year}-01-01"),
258
- "All": all_dates.min()
259
- }
260
- start_date = range_map.get(time_range, all_dates.min())
261
-
262
- for i, (ticker, df) in enumerate(data_dict.items()):
263
- df_plot = df[df.index >= start_date]
264
- color = colors[i % len(colors)]
265
- fig.add_trace(go.Scatter(x=df_plot.index, y=df_plot['Close'], mode='lines', name=ticker, line=dict(color=color, width=2)))
266
-
267
- if show_bollinger:
268
- fig.add_trace(go.Scatter(x=df_plot.index, y=df_plot['UpperBB'], mode='lines', line=Config.BOLLINGER_LINE_STYLE, showlegend=False, hoverinfo='skip'))
269
- fig.add_trace(go.Scatter(x=df_plot.index, y=df_plot['LowerBB'], mode='lines', line=Config.BOLLINGER_LINE_STYLE, fill='tonexty', fillcolor=Config.BOLLINGER_FILL_COLOR, showlegend=False, hoverinfo='skip'))
270
-
271
- buy_signals = df_plot[df_plot['Technical_Score'] > 0]
272
- sell_signals = df_plot[df_plot['Technical_Score'] < 0]
273
- fig.add_trace(go.Scatter(x=buy_signals.index, y=buy_signals['Close'], mode='markers', name=f'{ticker} Buy', marker=dict(symbol='triangle-up', color=color, size=8), hoverinfo='skip'))
274
- fig.add_trace(go.Scatter(x=sell_signals.index, y=sell_signals['Close'], mode='markers', name=f'{ticker} Sell', marker=dict(symbol='triangle-down', color='white', size=6, line=dict(color=color, width=1)), hoverinfo='skip'))
275
-
276
- fig.update_layout(
277
- title="Comparative Technical Analysis",
278
- template=Config.PLOT_TEMPLATE,
279
- height=600,
280
- legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
281
- yaxis_title="Stock Price (USD)",
282
- hovermode="x unified"
283
- )
284
  return fig
285
-
286
- @staticmethod
287
- def create_decision_gauge(decision: str, total_score: int) -> go.Figure:
288
- colors = {'STRONG BUY': '#00FF00', 'BUY': '#90EE90', 'HOLD': '#FFD700', 'SELL': '#FFA500', 'STRONG SELL': '#FF0000'}
289
- fig = go.Figure(go.Indicator(
290
- mode="gauge+number", value=total_score,
291
- title={'text': decision, 'font': {'size': 24, 'color': 'white'}},
292
- gauge={
293
- 'axis': {'range': [-6, 6]},
294
- 'bar': {'color': colors.get(decision, '#FFD700')},
295
- 'steps': [
296
- {'range': [-6, -4], 'color': 'rgba(255, 0, 0, 0.8)'},
297
- {'range': [-4, -2], 'color': 'rgba(255, 165, 0, 0.7)'},
298
- {'range': [-2, 2], 'color': 'rgba(255, 215, 0, 0.6)'},
299
- {'range': [2, 4], 'color': 'rgba(144, 238, 144, 0.7)'},
300
- {'range': [4, 6], 'color': 'rgba(0, 255, 0, 0.8)'},
301
- ],
302
- }
303
- ))
304
- fig.update_layout(template=Config.PLOT_TEMPLATE, height=250, margin=dict(t=40, b=40))
305
- return fig
306
-
307
- @staticmethod
308
- def create_sentiment_plot(daily_sentiment: pd.DataFrame, ticker: str) -> go.Figure:
309
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.7, 0.3], vertical_spacing=0.1)
310
- fig.add_trace(go.Scatter(x=daily_sentiment['date'], y=daily_sentiment['avg_sentiment'], name='Avg. Sentiment', line=dict(color=Config.PRIMARY_COLOR, width=2)), row=1, col=1)
311
- fig.add_trace(go.Bar(x=daily_sentiment['date'], y=daily_sentiment['positive_count'], name='Positive', marker_color=Config.SENTIMENT_POSITIVE_COLOR), row=2, col=1)
312
- fig.add_trace(go.Bar(x=daily_sentiment['date'], y=daily_sentiment['negative_count'], name='Negative', marker_color=Config.SENTIMENT_NEGATIVE_COLOR), row=2, col=1)
313
- fig.add_trace(go.Bar(x=daily_sentiment['date'], y=daily_sentiment['neutral_count'], name='Neutral', marker_color=Config.SENTIMENT_NEUTRAL_COLOR), row=2, col=1)
314
- fig.update_layout(
315
- title=f"News Sentiment & Article Volume (Last {Config.SENTIMENT_DAYS} Days)",
316
- template=Config.PLOT_TEMPLATE, barmode='stack', height=450, showlegend=False,
317
- yaxis1_title="Sentiment Score", yaxis2_title="Article Count"
318
  )
319
- return fig
 
320
 
321
- @staticmethod
322
- def create_forecast_plot(forecast_df: pd.DataFrame, ticker: str) -> go.Figure:
323
  fig = go.Figure()
324
- fig.add_trace(go.Scatter(x=forecast_df['ds'], y=forecast_df['yhat'], name='Forecast', line=dict(color=Config.PRIMARY_COLOR, width=2)))
325
- fig.add_trace(go.Scatter(x=forecast_df['ds'], y=forecast_df['yhat_upper'], fill=None, mode='lines', line=dict(color='rgba(0, 191, 255, 0.3)'), showlegend=False))
326
- fig.add_trace(go.Scatter(x=forecast_df['ds'], y=forecast_df['yhat_lower'], fill='tonexty', mode='lines', line=dict(color='rgba(0, 191, 255, 0.3)'), showlegend=False))
327
- fig.update_layout(title="30-Day Price Forecast", template=Config.PLOT_TEMPLATE, height=450, yaxis_title="Predicted Price (USD)")
 
 
 
 
328
  return fig
329
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  # ============================================================================
331
- # 🖥️ GRADIO INTERFACE
332
  # ============================================================================
333
- def run_full_analysis(tickers_str: str, time_range: str, show_bollinger: bool, force_refresh: bool, progress=gr.Progress()):
334
- tickers = [t.strip().upper() for t in tickers_str.split(',') if t.strip()][:5]
 
 
 
 
 
 
 
 
 
335
  if not tickers:
336
- return (
337
- "Please enter at least one ticker.",
338
- None, None, None, None
339
- )
340
-
341
- progress(0, desc="Starting analysis...")
342
- all_results = {}
343
- for i, ticker in enumerate(tickers):
344
- progress((i + 1) / len(tickers), desc=f"Analyzing {ticker}...")
345
- try:
346
- analyzer = StockAnalyzer(ticker, force_refresh)
347
- if not analyzer.tech_df.empty:
348
- all_results[ticker] = analyzer
349
- except Exception as e:
350
- print(f"Error analyzing {ticker}: {e}")
351
- continue
352
-
353
- if not all_results:
354
- return (
355
- "Could not retrieve data for any ticker.",
356
- None, None, None, None
357
- )
358
-
359
- # Multi-ticker price plot
360
- multi_plot = Plotter.create_multi_ticker_plot(
361
- {t: r.tech_df for t, r in all_results.items()},
362
- show_bollinger, time_range
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  )
364
 
365
- # Detailed analysis for FIRST ticker only
366
- primary_ticker = tickers[0]
367
- primary_analyzer = all_results.get(primary_ticker)
368
- if not primary_analyzer:
369
- primary_ticker = list(all_results.keys())[0]
370
- primary_analyzer = all_results[primary_ticker]
371
-
372
- # Summary
373
- current_price = primary_analyzer.tech_df['Close'].iloc[-1]
374
- avg_sent = primary_analyzer.sentiment_daily['avg_sentiment'].mean() if primary_analyzer.sentiment_daily is not None else 0.0
375
- summary_md = f"""
376
- ### 🎯 Decision: **{primary_analyzer.decision}** (Score: {primary_analyzer.total_score}/6)
377
- - **Ticker**: {primary_ticker}
378
- - **Current Price**: ${current_price:.2f}
379
- - **Technical Score**: `{primary_analyzer.scores['Technical']}`
380
- - **Sentiment Score**: `{primary_analyzer.scores['Sentiment']}` (Avg: {avg_sent:.2f})
381
- - **Forecast Score**: `{primary_analyzer.scores['Forecast']}` ({primary_analyzer.forecast_pct:.1f}% → ${primary_analyzer.forecast_price:.2f})
382
- """
383
-
384
- # Plots
385
- gauge_plot = Plotter.create_decision_gauge(primary_analyzer.decision, primary_analyzer.total_score)
386
- sentiment_plot = Plotter.create_sentiment_plot(primary_analyzer.sentiment_daily, primary_ticker) if primary_analyzer.sentiment_daily is not None else None
387
- forecast_plot = Plotter.create_forecast_plot(primary_analyzer.forecast_df, primary_ticker) if primary_analyzer.forecast_df is not None else None
388
-
389
- progress(1.0, "Done!")
390
- return summary_md, multi_plot, gauge_plot, sentiment_plot, forecast_plot
391
-
392
- # Custom CSS
393
- custom_css = """
394
- .gradio-container { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
395
- """
396
-
397
- # Build App
398
- with gr.Blocks(theme=gr.themes.Monochrome(), css=custom_css) as demo:
399
- gr.Markdown("# 📊 Unified Stock Intelligence Dashboard")
400
- gr.Markdown("Technical, sentiment, and predictive analysis for up to 5 stocks.")
401
 
402
  with gr.Row():
403
- with gr.Column(scale=1):
404
- gr.Markdown("### Controls")
405
- tickers_input = gr.Textbox(label="Tickers (comma-separated, max 5)", value="NVDA,TSLA,MSFT")
406
- time_range = gr.Radio(choices=["1M", "3M", "6M", "1Y", "YTD", "All"], value="1Y", label="Chart Time Range")
407
- show_bb = gr.Checkbox(label="Show Bollinger Bands", value=True)
408
- force_refresh = gr.Checkbox(label="Force Refresh Data", value=False)
409
- analyze_btn = gr.Button("Analyze Stocks", variant="primary")
410
- status_output = gr.Textbox(label="Status", interactive=False)
411
-
412
- with gr.Column(scale=4):
413
- gr.Markdown("### Comparative Price Chart")
414
- technical_plot_output = gr.Plot()
415
-
416
- # Detailed Analysis for Primary Ticker
417
- gr.Markdown("### 🔍 Detailed Analysis (First Ticker)")
418
- with gr.Row():
419
- summary_output = gr.Markdown()
420
- decision_gauge_output = gr.Plot()
421
 
422
  with gr.Row():
423
- sentiment_output = gr.Plot()
424
- forecast_output = gr.Plot()
425
-
426
- # NO gr.Progress() in layout!
427
- analyze_btn.click(
428
- fn=run_full_analysis,
429
- inputs=[tickers_input, time_range, show_bb, force_refresh],
430
- outputs=[status_output, technical_plot_output, decision_gauge_output, sentiment_output, forecast_output]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  )
432
 
433
  if __name__ == "__main__":
 
1
  import os
2
  import pandas as pd
 
3
  import numpy as np
4
  import gradio as gr
 
 
5
  import plotly.graph_objects as go
6
  from plotly.subplots import make_subplots
7
+ import plotly.express as px
8
  import yfinance as yf
9
+ import requests
10
+ from datetime import datetime, timedelta
11
+ from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
12
  from prophet import Prophet
 
13
  import warnings
14
+ import logging
15
+ import asyncio
16
+ import concurrent.futures
17
+ import tempfile
18
+ from functools import lru_cache
19
+ from typing import Dict, List, Optional, Any, Tuple
20
+
21
+ warnings.filterwarnings("ignore")
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger("unified_stock_app")
24
+
25
+ # Optional: Try importing TimesFM
26
+ TIMESFM_AVAILABLE = False
27
+ try:
28
+ import timesfm
29
+ TIMESFM_AVAILABLE = True
30
+ except ImportError:
31
+ pass
32
 
33
  # ============================================================================
34
+ # CONFIGURATION
35
  # ============================================================================
36
  class Config:
37
+ FINNHUB_API_KEY = os.getenv("FINNHUB_API_KEY", "cuj17q1r01qm7p9n307gcuj17q1r01qm7p9n3080")
38
  DATA_DIR = "data_cache"
39
  CACHE_TTL_HOURS = 12
40
  SENTIMENT_DAYS = 90
41
  TECH_DATA_YEARS = 3
42
 
 
 
 
 
 
 
 
 
43
  @classmethod
44
  def initialize(cls):
45
  os.makedirs(cls.DATA_DIR, exist_ok=True)
 
47
  Config.initialize()
48
 
49
  # ============================================================================
50
+ # CACHING UTILS
51
  # ============================================================================
52
  class CacheManager:
53
  @staticmethod
 
56
 
57
  @staticmethod
58
  def save_df(df: pd.DataFrame, filename: str):
59
+ df.to_csv(CacheManager.get_path(filename), index=False)
60
 
61
  @staticmethod
62
  def load_df(filename: str) -> Optional[pd.DataFrame]:
63
  path = CacheManager.get_path(filename)
64
  if not os.path.exists(path):
65
  return None
 
66
  file_mod_time = datetime.fromtimestamp(os.path.getmtime(path))
67
  if datetime.now() - file_mod_time > timedelta(hours=Config.CACHE_TTL_HOURS):
68
  return None
 
69
  try:
70
  df = pd.read_csv(path)
71
  for col in df.columns:
72
  if 'date' in col.lower():
73
  df[col] = pd.to_datetime(df[col])
 
 
74
  return df
75
  except Exception:
76
  return None
77
 
78
  # ============================================================================
79
+ # FUNDAMENTALS MODULE
80
  # ============================================================================
81
+ @lru_cache(maxsize=100)
82
+ def get_financial_data(ticker: str) -> Optional[Dict[str, Any]]:
83
+ try:
84
+ stock = yf.Ticker(ticker)
85
+ info = stock.info
86
+ return {
87
+ 'Ticker': ticker,
88
+ 'PE_Ratio': info.get('forwardPE'),
89
+ 'Debt_to_Equity': info.get('debtToEquity'),
90
+ 'Revenue_Growth': info.get('revenueGrowth'),
91
+ 'ROE': info.get('returnOnEquity'),
92
+ 'ROA': info.get('returnOnAssets'),
93
+ 'Gross_Margin': info.get('grossMargins'),
94
+ 'EBITDA': info.get('ebitda'),
95
+ 'Market_Cap': info.get('marketCap'),
96
+ 'Dividend_Yield': info.get('dividendYield'),
97
+ 'Profit_Margin': info.get('profitMargins'),
98
+ 'EPS_Growth': info.get('earningsGrowth'),
99
+ 'Price_to_Book': info.get('priceToBook'),
100
+ 'Current_Price': info.get('currentPrice')
101
+ }
102
+ except Exception as e:
103
+ logger.error(f"Error fetching data for {ticker}: {e}")
104
+ return None
105
+
106
+ async def fetch_data_concurrently(tickers: List[str]) -> List[Dict[str, Any]]:
107
+ loop = asyncio.get_event_loop()
108
+ with concurrent.futures.ThreadPoolExecutor() as executor:
109
+ tasks = [loop.run_in_executor(executor, get_financial_data, ticker) for ticker in tickers]
110
+ results = await asyncio.gather(*tasks)
111
+ return [r for r in results if r is not None]
112
+
113
+ def sanitize_financial_data(df: pd.DataFrame) -> pd.DataFrame:
114
+ df = df.copy()
115
+ for col in ['ROE', 'ROA', 'Profit_Margin', 'Gross_Margin']:
116
+ if col in df.columns:
117
+ df[col] = df[col].where((df[col] >= -2) & (df[col] <= 2), np.nan)
118
+ for col in ['Revenue_Growth', 'EPS_Growth']:
119
+ if col in df.columns:
120
+ df[col] = df[col].where((df[col] >= -1) & (df[col] <= 5), np.nan)
121
+ for col in ['Debt_to_Equity', 'Dividend_Yield']:
122
+ if col in df.columns:
123
+ df[col] = df[col].where(df[col] >= 0, np.nan)
124
+ for col in ['PE_Ratio', 'Price_to_Book']:
125
+ if col in df.columns:
126
+ df[col] = df[col].where((df[col] > 0) & (df[col] < 1000), np.nan)
127
+ for col in ['Market_Cap', 'EBITDA']:
128
+ if col in df.columns:
129
+ df[col] = df[col].where(df[col] > 0, np.nan)
130
+ if 'Current_Price' in df.columns:
131
+ df['Current_Price'] = df['Current_Price'].where(df['Current_Price'] > 0, np.nan)
132
+ return df
133
+
134
+ def normalize(series: pd.Series, reverse: bool = False, lower_percentile: float = 0.10, upper_percentile: float = 0.90) -> pd.Series:
135
+ valid_series = series.dropna()
136
+ if len(valid_series) == 0 or len(valid_series.unique()) <= 1:
137
+ return pd.Series(5.0, index=series.index, dtype=float)
138
+ q_low = valid_series.quantile(lower_percentile)
139
+ q_high = valid_series.quantile(upper_percentile)
140
+ if q_high <= q_low:
141
+ return pd.Series(5.0, index=series.index, dtype=float)
142
+ clipped = series.clip(q_low, q_high)
143
+ normalized = (clipped - q_low) / (q_high - q_low)
144
+ normalized = normalized.clip(0, 1)
145
+ result = 10 * (1 - normalized) if reverse else 10 * normalized
146
+ return result
147
+
148
+ def calculate_scores(df: pd.DataFrame, growth_weight: float, value_weight: float, risk_weight: float) -> pd.DataFrame:
149
+ scored_df = df.copy()
150
+ scored_df['Revenue_Growth_Score'] = normalize(df['Revenue_Growth'])
151
+ scored_df['EPS_Growth_Score'] = normalize(df['EPS_Growth'])
152
+ scored_df['ROE_Score'] = normalize(df['ROE'])
153
+ scored_df['ROA_Score'] = normalize(df['ROA'])
154
+ scored_df['Growth_Score'] = scored_df[['Revenue_Growth_Score', 'EPS_Growth_Score', 'ROE_Score', 'ROA_Score']].mean(axis=1)
155
+
156
+ scored_df['PE_Ratio_Score'] = normalize(df['PE_Ratio'], reverse=True)
157
+ scored_df['Price_to_Book_Score'] = normalize(df['Price_to_Book'], reverse=True)
158
+ scored_df['Dividend_Yield_Score'] = normalize(df['Dividend_Yield'])
159
+ scored_df['Value_Score'] = scored_df[['PE_Ratio_Score', 'Price_to_Book_Score', 'Dividend_Yield_Score']].mean(axis=1)
160
+
161
+ scored_df['Debt_to_Equity_No_Risk_Score'] = normalize(df['Debt_to_Equity'], reverse=True)
162
+ scored_df['Profit_Margin_No_Risk_Score'] = normalize(df['Profit_Margin'])
163
+ scored_df['Market_Cap_No_Risk_Score'] = normalize(df['Market_Cap'])
164
+ scored_df['No_Risk_Score'] = scored_df[['Debt_to_Equity_No_Risk_Score', 'Profit_Margin_No_Risk_Score', 'Market_Cap_No_Risk_Score']].mean(axis=1)
165
+
166
+ total = growth_weight + value_weight + risk_weight
167
+ if total == 0:
168
+ gw = vw = rw = 1/3
169
+ else:
170
+ gw, vw, rw = growth_weight/total, value_weight/total, risk_weight/total
171
+ scored_df['Total_Score'] = gw * scored_df['Growth_Score'] + vw * scored_df['Value_Score'] + rw * scored_df['No_Risk_Score']
172
+ return scored_df
173
+
174
+ def create_metrics_table(df: pd.DataFrame) -> pd.DataFrame:
175
+ metrics_df = df[['Ticker', 'Current_Price', 'PE_Ratio', 'Price_to_Book',
176
+ 'Debt_to_Equity', 'ROE', 'ROA', 'Revenue_Growth',
177
+ 'EPS_Growth', 'Profit_Margin', 'Dividend_Yield']].copy()
178
+ for col in ['ROE', 'ROA', 'Revenue_Growth', 'EPS_Growth', 'Profit_Margin', 'Dividend_Yield']:
179
+ metrics_df[col] = metrics_df[col].apply(lambda x: f"{x*100:.2f}%" if pd.notnull(x) else "N/A")
180
+ for col in ['PE_Ratio', 'Price_to_Book', 'Debt_to_Equity']:
181
+ metrics_df[col] = metrics_df[col].apply(lambda x: f"{x:.2f}" if pd.notnull(x) else "N/A")
182
+ metrics_df['Current_Price'] = metrics_df['Current_Price'].apply(lambda x: f"${x:.2f}" if pd.notnull(x) else "N/A")
183
+ return metrics_df
184
 
185
+ # ============================================================================
186
+ # SENTIMENT MODULE
187
+ # ============================================================================
188
+ class SentimentAnalyzer:
189
+ def __init__(self):
190
+ self.analyzer = SentimentIntensityAnalyzer()
191
+ def analyze(self, text):
192
+ if not isinstance(text, str) or not text.strip():
193
+ return 0
194
+ return self.analyzer.polarity_scores(text)['compound']
195
+
196
+ class StockNewsAnalyzer:
197
+ def __init__(self, symbol):
198
+ self.symbol = symbol
199
+ self.sentiment_analyzer = SentimentAnalyzer()
200
+
201
+ def get_news(self, days=Config.SENTIMENT_DAYS, force_refresh=False):
202
+ cache_file = f"{self.symbol}_news.csv"
203
+ df = CacheManager.load_df(cache_file)
204
+ if df is not None and not force_refresh:
205
+ return df
206
 
207
  end_date = datetime.now()
208
+ start_date = end_date - timedelta(days=days)
209
+ # FIXED URL: NO TRAILING SPACES!
210
+ url = "https://finnhub.io/api/v1/company-news"
211
+ params = {
212
+ "symbol": self.symbol,
213
+ "from": start_date.strftime('%Y-%m-%d'),
214
+ "to": end_date.strftime('%Y-%m-%d'),
215
+ "token": Config.FINNHUB_API_KEY,
216
+ }
217
  try:
218
+ response = requests.get(url, params=params, timeout=10)
219
+ response.raise_for_status()
220
+ data = response.json()
221
+ if not data or not isinstance(data, list):
222
+ return pd.DataFrame()
223
+ df = pd.DataFrame(data)
224
+ if 'datetime' in df.columns:
225
+ df['datetime'] = pd.to_datetime(df['datetime'], unit='s')
226
+ CacheManager.save_df(df, cache_file)
227
+ return df
228
+ return pd.DataFrame()
229
+ except Exception as e:
230
+ print(f"Error fetching news: {e}")
231
+ return pd.DataFrame()
 
 
 
232
 
233
+ def get_sentiment_data(self, days=Config.SENTIMENT_DAYS, force_refresh=False):
234
+ news_df = self.get_news(days, force_refresh)
235
+ if news_df.empty or 'headline' not in news_df.columns:
236
+ return None, None
237
+ news_df['sentiment'] = news_df['headline'].apply(self.sentiment_analyzer.analyze)
238
  news_df['date'] = pd.to_datetime(news_df['datetime'].dt.date)
239
+ daily = news_df.groupby('date').agg(
 
240
  avg_sentiment=('sentiment', 'mean'),
241
  article_count=('sentiment', 'count'),
242
  positive_count=('sentiment', lambda x: (x > 0.05).sum()),
243
  negative_count=('sentiment', lambda x: (x < -0.05).sum()),
244
  neutral_count=('sentiment', lambda x: ((x >= -0.05) & (x <= 0.05)).sum())
245
  ).reset_index()
246
+ return daily, news_df
247
 
248
+ def create_sentiment_plot(daily_sentiment: pd.DataFrame, symbol: str) -> go.Figure:
249
+ fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.7, 0.3])
250
+ fig.add_trace(go.Scatter(x=daily_sentiment['date'], y=daily_sentiment['avg_sentiment'], name='Avg Sentiment', line=dict(color='#ff7f0e')), row=1, col=1)
251
+ fig.add_trace(go.Bar(x=daily_sentiment['date'], y=daily_sentiment['positive_count'], name='Positive', marker_color='rgba(0,200,0,0.7)'), row=2, col=1)
252
+ fig.add_trace(go.Bar(x=daily_sentiment['date'], y=daily_sentiment['negative_count'], name='Negative', marker_color='rgba(255,0,0,0.7)'), row=2, col=1)
253
+ fig.add_trace(go.Bar(x=daily_sentiment['date'], y=daily_sentiment['neutral_count'], name='Neutral', marker_color='rgba(128,128,128,0.6)'), row=2, col=1)
254
+ fig.update_layout(title=f"{symbol} News Sentiment (Last {Config.SENTIMENT_DAYS} Days)", template="plotly_dark", barmode='stack', height=500)
255
+ return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  # ============================================================================
258
+ # TECHNICAL & FORECASTING MODULES
259
  # ============================================================================
260
+ def calculate_rsi(df):
261
+ delta = df['Close'].diff()
262
+ gain = (delta.where(delta > 0, 0)).rolling(14).mean()
263
+ loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
264
+ rs = gain / loss
265
+ return 100 - (100 / (1 + rs))
266
+
267
+ def calculate_bollinger_bands(df):
268
+ ma = df['Close'].rolling(20).mean()
269
+ std = df['Close'].rolling(20).std()
270
+ return ma, ma + 2*std, ma - 2*std
271
+
272
+ def calculate_stochastic_oscillator(df):
273
+ ll = df['Low'].rolling(14).min()
274
+ hh = df['High'].rolling(14).max()
275
+ k = ((df['Close'] - ll) / (hh - ll)) * 100
276
+ d = k.rolling(3).mean()
277
+ return k, d
278
+
279
+ def calculate_cmf(df, window=20):
280
+ price_range = df['High'] - df['Low']
281
+ price_range = price_range.replace(0, np.nan)
282
+ mfv = ((df['Close'] - df['Low']) - (df['High'] - df['Close'])) / price_range * df['Volume']
283
+ mfv_sum = mfv.rolling(window).sum()
284
+ vol_sum = df['Volume'].rolling(window).sum()
285
+ return np.where(vol_sum > 0, mfv_sum / vol_sum, np.nan)
286
+
287
+ def generate_signals(df):
288
+ df = df.copy()
289
+ df['RSI'] = calculate_rsi(df)
290
+ df['MiddleBB'], df['UpperBB'], df['LowerBB'] = calculate_bollinger_bands(df)
291
+ df['SlowK'], df['SlowD'] = calculate_stochastic_oscillator(df)
292
+ df['CMF'] = calculate_cmf(df)
293
+
294
+ df['RSI_Signal'] = np.where(df['RSI'] < 20, 1, np.where(df['RSI'] > 80, -1, 0))
295
+ df['BB_Signal'] = np.where(df['Close'] < df['LowerBB'], 1, np.where(df['Close'] > df['UpperBB'], -1, 0))
296
+ df['Stochastic_Signal'] = np.where((df['SlowK'] < 15) & (df['SlowD'] < 15), 1, np.where((df['SlowK'] > 85) & (df['SlowD'] > 85), -1, 0))
297
+ df['CMF_Signal'] = np.where(df['CMF'] < -0.25, 1, np.where(df['CMF'] > 0.25, -1, 0))
298
+ df['Technical_Score'] = df[['RSI_Signal', 'BB_Signal', 'Stochastic_Signal', 'CMF_Signal']].sum(axis=1)
299
+ return df
300
+
301
+ def prophet_forecast(ticker, start_date, end_date):
302
+ try:
303
+ df = yf.download(ticker, start=start_date, end=end_date)
304
+ if isinstance(df.columns, pd.MultiIndex):
305
+ df.columns = df.columns.droplevel(1)
306
+ df_plot = df.reset_index()[['Date', 'Close']].rename(columns={'Date': 'ds', 'Close': 'y'})
307
+ model = Prophet()
308
+ model.fit(df_plot)
309
+ future = model.make_future_dataframe(periods=30)
310
+ forecast = model.predict(future)
311
+ fig1 = go.Figure()
312
+ fig1.add_trace(go.Scatter(x=df_plot['ds'], y=df_plot['y'], mode='lines', name='Actual'))
313
+ fig1.add_trace(go.Scatter(x=forecast['ds'], y=forecast['trend'], mode='lines', name='Trend'))
314
+ fig1.update_layout(title=f"{ticker} Price & Trend", template="plotly_dark")
315
+
316
+ fig2 = go.Figure()
317
+ tail = forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail(40)
318
+ fig2.add_trace(go.Scatter(x=tail['ds'], y=tail['yhat'], mode='lines', name='Forecast'))
319
+ fig2.add_trace(go.Scatter(x=list(tail['ds']) + list(tail['ds'])[::-1],
320
+ y=list(tail['yhat_upper']) + list(tail['yhat_lower'])[::-1],
321
+ fill='toself', name='Confidence'))
322
+ fig2.update_layout(title=f"{ticker} 30-Day Forecast", template="plotly_dark")
323
+ return fig1, fig2
324
+ except Exception as e:
325
+ empty = go.Figure()
326
+ empty.add_annotation(text=f"Error: {e}", x=0.5, y=0.5, xref="paper", yref="paper")
327
+ return empty, empty
328
+
329
+ def timesfm_forecast(ticker, start_date, end_date):
330
+ if not TIMESFM_AVAILABLE:
331
  fig = go.Figure()
332
+ fig.add_annotation(text="TimesFM not installed", x=0.5, y=0.5, xref="paper", yref="paper")
333
+ fig.update_layout(template="plotly_dark")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  return fig
335
+ try:
336
+ df = yf.download(ticker, start=start_date, end=end_date)
337
+ if isinstance(df.columns, pd.MultiIndex):
338
+ df.columns = df.columns.droplevel(1)
339
+ df = df.reset_index()[['Date', 'Close']].rename(columns={'Date': 'ds', 'Close': 'y'})
340
+ df['ds'] = pd.to_datetime(df['ds'])
341
+ df['unique_id'] = ticker
342
+
343
+ tfm = timesfm.TimesFm(
344
+ hparams=timesfm.TimesFmHparams(
345
+ backend="pytorch",
346
+ per_core_batch_size=32,
347
+ horizon_len=30,
348
+ input_patch_len=32,
349
+ output_patch_len=128,
350
+ num_layers=50,
351
+ model_dims=1280,
352
+ ),
353
+ checkpoint=timesfm.TimesFmCheckpoint(huggingface_repo_id="google/timesfm-2.0-500m-pytorch")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  )
355
+ forecast_df = tfm.forecast_on_df(inputs=df, freq="D", value_name="y")
356
+ forecast_df.rename(columns={"timesfm": "forecast"}, inplace=True)
357
 
 
 
358
  fig = go.Figure()
359
+ fig.add_trace(go.Scatter(x=df["ds"], y=df["y"], mode="lines", name="Actual"))
360
+ fig.add_trace(go.Scatter(x=forecast_df["ds"], y=forecast_df["forecast"], mode="lines", name="Forecast"))
361
+ fig.update_layout(title=f"{ticker} TimesFM Forecast", template="plotly_dark")
362
+ return fig
363
+ except Exception as e:
364
+ fig = go.Figure()
365
+ fig.add_annotation(text=f"TimesFM Error: {e}", x=0.5, y=0.5, xref="paper", yref="paper")
366
+ fig.update_layout(template="plotly_dark")
367
  return fig
368
 
369
+ def plot_technical_signals(df, ticker):
370
+ df = generate_signals(df)
371
+ df_120 = df.tail(120)
372
+ fig = go.Figure()
373
+ fig.add_trace(go.Scatter(x=df_120.index, y=df_120['Close'], mode='lines', name='Price'))
374
+ buy = df_120[df_120['Technical_Score'] > 0]
375
+ sell = df_120[df_120['Technical_Score'] < 0]
376
+ fig.add_trace(go.Scatter(x=buy.index, y=buy['Close'], mode='markers', name='Buy', marker=dict(symbol='triangle-up', color='green')))
377
+ fig.add_trace(go.Scatter(x=sell.index, y=sell['Close'], mode='markers', name='Sell', marker=dict(symbol='triangle-down', color='red')))
378
+ fig.update_layout(title=f"{ticker} Technical Signals", template="plotly_dark")
379
+ return fig
380
+
381
  # ============================================================================
382
+ # MAIN ANALYSIS FUNCTION
383
  # ============================================================================
384
+ async def run_unified_analysis(
385
+ tickers_str: str,
386
+ start_date: str,
387
+ end_date: str,
388
+ days_sentiment: int,
389
+ refresh_news: bool,
390
+ growth_w: float,
391
+ value_w: float,
392
+ risk_w: float
393
+ ):
394
+ tickers = [t.strip().upper() for t in tickers_str.split(",") if t.strip()][:5]
395
  if not tickers:
396
+ empty = go.Figure()
397
+ empty.add_annotation(text="Enter tickers", x=0.5, y=0.5, xref="paper", yref="paper")
398
+ return ("No tickers",) + (empty,) * 8 + (pd.DataFrame(), pd.DataFrame())
399
+
400
+ primary = tickers[0]
401
+
402
+ # Fundamentals
403
+ scores_df, metrics_df = pd.DataFrame(), pd.DataFrame()
404
+ try:
405
+ fund_data = await fetch_data_concurrently(tickers)
406
+ if fund_data:
407
+ df = pd.DataFrame(fund_data)
408
+ df = sanitize_financial_data(df)
409
+ numerical_cols = df.select_dtypes(include=[np.number]).columns
410
+ for col in numerical_cols:
411
+ df[col] = df[col].fillna(df[col].median() if not pd.isna(df[col].median()) else 0)
412
+ df = calculate_scores(df, growth_w, value_w, risk_w)
413
+ df = df.sort_values('Total_Score', ascending=False).reset_index(drop=True)
414
+ scores_df = df[['Ticker', 'Total_Score', 'Growth_Score', 'Value_Score', 'No_Risk_Score']].round(2)
415
+ metrics_df = create_metrics_table(df)
416
+ except Exception as e:
417
+ logger.error(f"Fundamentals error: {e}")
418
+
419
+ # Sentiment
420
+ sentiment_plot = go.Figure()
421
+ try:
422
+ analyzer = StockNewsAnalyzer(primary)
423
+ daily_sent, _ = analyzer.get_sentiment_data(days=days_sentiment, force_refresh=refresh_news)
424
+ if daily_sent is not None:
425
+ sentiment_plot = create_sentiment_plot(daily_sent, primary)
426
+ except Exception as e:
427
+ logger.error(f"Sentiment error: {e}")
428
+
429
+ # Technicals
430
+ tech_plot = go.Figure()
431
+ try:
432
+ tech_df = yf.download(primary, start=start_date, end=end_date)
433
+ if not tech_df.empty:
434
+ if isinstance(tech_df.columns, pd.MultiIndex):
435
+ tech_df.columns = tech_df.columns.droplevel(1)
436
+ tech_plot = plot_technical_signals(tech_df, primary)
437
+ except Exception as e:
438
+ logger.error(f"Technical error: {e}")
439
+
440
+ # Forecasting
441
+ prophet_price, prophet_forecast = prophet_forecast(primary, start_date, end_date)
442
+ timesfm_plot = timesfm_forecast(primary, start_date, end_date)
443
+
444
+ return (
445
+ f"Analysis for: {', '.join(tickers)}",
446
+ scores_df,
447
+ metrics_df,
448
+ sentiment_plot,
449
+ tech_plot,
450
+ timesfm_plot,
451
+ prophet_price,
452
+ prophet_forecast
453
  )
454
 
455
+ # ============================================================================
456
+ # GRADIO INTERFACE
457
+ # ============================================================================
458
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
459
+ gr.Markdown("# 🚀 Unified Stock Intelligence Platform")
460
+ gr.Markdown("Fundamentals + Sentiment + Technicals + AI Forecasting")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
 
462
  with gr.Row():
463
+ tickers = gr.Textbox(label="Tickers (comma-separated)", value="NVDA, AAPL, MSFT")
464
+ start_date = gr.Textbox(label="Start Date", value="2022-01-01")
465
+ end_date = gr.Textbox(label="End Date", value="2026-01-01")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
467
  with gr.Row():
468
+ days_sentiment = gr.Slider(7, 90, value=90, label="Sentiment Days")
469
+ refresh_news = gr.Checkbox(label="Refresh News", value=False)
470
+ growth_w = gr.Slider(0, 1, 0.4, label="Growth Weight")
471
+ value_w = gr.Slider(0, 1, 0.4, label="Value Weight")
472
+ risk_w = gr.Slider(0, 1, 0.2, label="Risk Weight")
473
+
474
+ run_btn = gr.Button("Analyze", variant="primary")
475
+
476
+ with gr.Tabs():
477
+ with gr.Tab("📊 Fundamentals"):
478
+ scores_table = gr.Dataframe()
479
+ metrics_table = gr.Dataframe()
480
+
481
+ with gr.Tab("📰 Sentiment"):
482
+ sentiment_plot = gr.Plot()
483
+
484
+ with gr.Tab("📈 Technicals"):
485
+ tech_plot = gr.Plot()
486
+
487
+ with gr.Tab("🔮 Forecasting"):
488
+ with gr.Row():
489
+ timesfm_plot = gr.Plot()
490
+ prophet_price = gr.Plot()
491
+ prophet_forecast = gr.Plot()
492
+
493
+ run_btn.click(
494
+ lambda *args: asyncio.run(run_unified_analysis(*args)),
495
+ inputs=[tickers, start_date, end_date, days_sentiment, refresh_news, growth_w, value_w, risk_w],
496
+ outputs=[
497
+ gr.Textbox(label="Status"),
498
+ scores_table, metrics_table,
499
+ sentiment_plot,
500
+ tech_plot,
501
+ timesfm_plot,
502
+ prophet_price,
503
+ prophet_forecast
504
+ ]
505
  )
506
 
507
  if __name__ == "__main__":