Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -82,7 +82,7 @@ class CacheManager:
|
|
| 82 |
df[col] = pd.to_datetime(df[col])
|
| 83 |
# If the first column is the index, set it
|
| 84 |
if 'Date' in df.columns and df.columns[0] == 'Date':
|
| 85 |
-
|
| 86 |
return df
|
| 87 |
except Exception:
|
| 88 |
return None
|
|
@@ -121,7 +121,7 @@ class StockAnalyzer:
|
|
| 121 |
cache_file = f"{self.ticker}_sentiment.csv"
|
| 122 |
df_daily = CacheManager.load_df(cache_file)
|
| 123 |
if df_daily is not None and not self.force_refresh:
|
| 124 |
-
return df_daily, None
|
| 125 |
|
| 126 |
end_date = datetime.now()
|
| 127 |
start_date = end_date - timedelta(days=Config.SENTIMENT_DAYS)
|
|
@@ -219,6 +219,8 @@ class StockAnalyzer:
|
|
| 219 |
@staticmethod
|
| 220 |
def _calculate_indicators(df: pd.DataFrame) -> pd.DataFrame:
|
| 221 |
"""Calculates a suite of technical indicators."""
|
|
|
|
|
|
|
| 222 |
# RSI
|
| 223 |
delta = df['Close'].diff()
|
| 224 |
gain = (delta.where(delta > 0, 0)).rolling(14).mean()
|
|
@@ -238,9 +240,20 @@ class StockAnalyzer:
|
|
| 238 |
df['SlowK'] = ((df['Close'] - ll) / (hh - ll)) * 100
|
| 239 |
df['SlowD'] = df['SlowK'].rolling(3).mean()
|
| 240 |
|
| 241 |
-
# Chaikin Money Flow (CMF)
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
# Signals (using stricter thresholds)
|
| 246 |
df['RSI_Signal'] = np.where(df['RSI'] < 20, 1, np.where(df['RSI'] > 80, -1, 0))
|
|
@@ -248,6 +261,7 @@ class StockAnalyzer:
|
|
| 248 |
df['Stochastic_Signal'] = np.where((df['SlowK'] < 15) & (df['SlowD'] < 15), 1, np.where((df['SlowK'] > 85) & (df['SlowD'] > 85), -1, 0))
|
| 249 |
df['CMF_Signal'] = np.where(df['CMF'] < -0.25, 1, np.where(df['CMF'] > 0.25, -1, 0))
|
| 250 |
df['Technical_Score'] = df[['RSI_Signal', 'BB_Signal', 'Stochastic_Signal', 'CMF_Signal']].sum(axis=1)
|
|
|
|
| 251 |
return df
|
| 252 |
|
| 253 |
# ============================================================================
|
|
@@ -342,9 +356,9 @@ class Plotter:
|
|
| 342 |
# ============================================================================
|
| 343 |
def run_full_analysis(tickers_str: str, time_range: str, show_bollinger: bool, force_refresh: bool, progress=gr.Progress()):
|
| 344 |
"""Main function triggered by the Gradio button."""
|
| 345 |
-
tickers = [t.strip().upper() for t in tickers_str.split(',') if t.strip()][:5]
|
| 346 |
if not tickers:
|
| 347 |
-
return "Please enter at least one ticker.", None, gr.
|
| 348 |
|
| 349 |
progress(0, desc="Starting analysis...")
|
| 350 |
all_results = {}
|
|
@@ -353,14 +367,14 @@ def run_full_analysis(tickers_str: str, time_range: str, show_bollinger: bool, f
|
|
| 353 |
try:
|
| 354 |
analyzer = StockAnalyzer(ticker, force_refresh)
|
| 355 |
if analyzer.tech_df.empty:
|
| 356 |
-
continue
|
| 357 |
all_results[ticker] = analyzer
|
| 358 |
except Exception as e:
|
| 359 |
print(f"Error analyzing {ticker}: {e}")
|
| 360 |
continue
|
| 361 |
|
| 362 |
if not all_results:
|
| 363 |
-
return "Could not retrieve data for the entered tickers.", None, gr.
|
| 364 |
|
| 365 |
# 1. Create the comparative multi-ticker plot
|
| 366 |
multi_plot = Plotter.create_multi_ticker_plot(
|
|
@@ -372,11 +386,13 @@ def run_full_analysis(tickers_str: str, time_range: str, show_bollinger: bool, f
|
|
| 372 |
accordion_items = []
|
| 373 |
for ticker, analyzer in all_results.items():
|
| 374 |
# Summary Tab Content
|
|
|
|
|
|
|
| 375 |
summary_md = f"""
|
| 376 |
### 🎯 Decision: **{analyzer.decision}** (Score: {analyzer.total_score}/6)
|
| 377 |
-
- **Current Price:** `${
|
| 378 |
- **Technical Score:** `{analyzer.scores['Technical']}`
|
| 379 |
-
- **Sentiment Score:** `{analyzer.scores['Sentiment']}` (Avg: {
|
| 380 |
- **Forecast Score:** `{analyzer.scores['Forecast']}` ({analyzer.forecast_pct:.1f}% change to `${analyzer.forecast_price:.2f}`)
|
| 381 |
"""
|
| 382 |
gauge_plot = Plotter.create_decision_gauge(analyzer.decision, analyzer.total_score)
|
|
@@ -394,16 +410,22 @@ def run_full_analysis(tickers_str: str, time_range: str, show_bollinger: bool, f
|
|
| 394 |
# Assemble the accordion item
|
| 395 |
ticker_accordion = gr.Accordion(
|
| 396 |
label=f"📊 {ticker} Analysis",
|
| 397 |
-
open=ticker == tickers[0]
|
| 398 |
)
|
| 399 |
with ticker_accordion:
|
| 400 |
with gr.Tabs():
|
| 401 |
with gr.TabItem("📈 Summary & Decision"):
|
| 402 |
summary_col.render()
|
| 403 |
with gr.TabItem("😊 Sentiment Analysis"):
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
| 405 |
with gr.TabItem("🔮 Forecast"):
|
| 406 |
-
|
|
|
|
|
|
|
|
|
|
| 407 |
accordion_items.append(ticker_accordion)
|
| 408 |
|
| 409 |
progress(1, "Analysis complete!")
|
|
@@ -411,7 +433,7 @@ def run_full_analysis(tickers_str: str, time_range: str, show_bollinger: bool, f
|
|
| 411 |
|
| 412 |
|
| 413 |
# --- Build the Gradio App ---
|
| 414 |
-
with gr.Blocks(theme=gr.themes.Default(primary_hue="
|
| 415 |
gr.Markdown("# 📊 Unified Stock Intelligence Dashboard")
|
| 416 |
gr.Markdown("An advanced tool for technical, sentiment, and predictive analysis of stocks.")
|
| 417 |
|
|
|
|
| 82 |
df[col] = pd.to_datetime(df[col])
|
| 83 |
# If the first column is the index, set it
|
| 84 |
if 'Date' in df.columns and df.columns[0] == 'Date':
|
| 85 |
+
df.set_index('Date', inplace=True)
|
| 86 |
return df
|
| 87 |
except Exception:
|
| 88 |
return None
|
|
|
|
| 121 |
cache_file = f"{self.ticker}_sentiment.csv"
|
| 122 |
df_daily = CacheManager.load_df(cache_file)
|
| 123 |
if df_daily is not None and not self.force_refresh:
|
| 124 |
+
return df_daily, None # Detailed news_df not needed from cache
|
| 125 |
|
| 126 |
end_date = datetime.now()
|
| 127 |
start_date = end_date - timedelta(days=Config.SENTIMENT_DAYS)
|
|
|
|
| 219 |
@staticmethod
|
| 220 |
def _calculate_indicators(df: pd.DataFrame) -> pd.DataFrame:
|
| 221 |
"""Calculates a suite of technical indicators."""
|
| 222 |
+
df = df.copy() # Avoid modifying original
|
| 223 |
+
|
| 224 |
# RSI
|
| 225 |
delta = df['Close'].diff()
|
| 226 |
gain = (delta.where(delta > 0, 0)).rolling(14).mean()
|
|
|
|
| 240 |
df['SlowK'] = ((df['Close'] - ll) / (hh - ll)) * 100
|
| 241 |
df['SlowD'] = df['SlowK'].rolling(3).mean()
|
| 242 |
|
| 243 |
+
# Chaikin Money Flow (CMF) — FIXED SECTION
|
| 244 |
+
price_range = df['High'] - df['Low']
|
| 245 |
+
# Avoid division by zero
|
| 246 |
+
price_range = price_range.replace(0, np.nan)
|
| 247 |
+
mfv = ((df['Close'] - df['Low']) - (df['High'] - df['Close'])) / price_range * df['Volume']
|
| 248 |
+
|
| 249 |
+
mfv_sum = mfv.rolling(20).sum()
|
| 250 |
+
vol_sum = df['Volume'].rolling(20).sum()
|
| 251 |
+
|
| 252 |
+
# Use .values to prevent pandas alignment errors
|
| 253 |
+
cmf_raw = mfv_sum.values / vol_sum.values
|
| 254 |
+
# Replace inf/-inf with NaN
|
| 255 |
+
cmf_clean = np.where(np.isfinite(cmf_raw), cmf_raw, np.nan)
|
| 256 |
+
df['CMF'] = cmf_clean
|
| 257 |
|
| 258 |
# Signals (using stricter thresholds)
|
| 259 |
df['RSI_Signal'] = np.where(df['RSI'] < 20, 1, np.where(df['RSI'] > 80, -1, 0))
|
|
|
|
| 261 |
df['Stochastic_Signal'] = np.where((df['SlowK'] < 15) & (df['SlowD'] < 15), 1, np.where((df['SlowK'] > 85) & (df['SlowD'] > 85), -1, 0))
|
| 262 |
df['CMF_Signal'] = np.where(df['CMF'] < -0.25, 1, np.where(df['CMF'] > 0.25, -1, 0))
|
| 263 |
df['Technical_Score'] = df[['RSI_Signal', 'BB_Signal', 'Stochastic_Signal', 'CMF_Signal']].sum(axis=1)
|
| 264 |
+
|
| 265 |
return df
|
| 266 |
|
| 267 |
# ============================================================================
|
|
|
|
| 356 |
# ============================================================================
|
| 357 |
def run_full_analysis(tickers_str: str, time_range: str, show_bollinger: bool, force_refresh: bool, progress=gr.Progress()):
|
| 358 |
"""Main function triggered by the Gradio button."""
|
| 359 |
+
tickers = [t.strip().upper() for t in tickers_str.split(',') if t.strip()][:5] # Limit to 5 tickers
|
| 360 |
if not tickers:
|
| 361 |
+
return "Please enter at least one ticker.", None, gr.Column(visible=False)
|
| 362 |
|
| 363 |
progress(0, desc="Starting analysis...")
|
| 364 |
all_results = {}
|
|
|
|
| 367 |
try:
|
| 368 |
analyzer = StockAnalyzer(ticker, force_refresh)
|
| 369 |
if analyzer.tech_df.empty:
|
| 370 |
+
continue # Skip if no data
|
| 371 |
all_results[ticker] = analyzer
|
| 372 |
except Exception as e:
|
| 373 |
print(f"Error analyzing {ticker}: {e}")
|
| 374 |
continue
|
| 375 |
|
| 376 |
if not all_results:
|
| 377 |
+
return "Could not retrieve data for the entered tickers.", None, gr.Column(visible=False)
|
| 378 |
|
| 379 |
# 1. Create the comparative multi-ticker plot
|
| 380 |
multi_plot = Plotter.create_multi_ticker_plot(
|
|
|
|
| 386 |
accordion_items = []
|
| 387 |
for ticker, analyzer in all_results.items():
|
| 388 |
# Summary Tab Content
|
| 389 |
+
current_price = analyzer.tech_df['Close'].iloc[-1] if not analyzer.tech_df.empty else 0.0
|
| 390 |
+
avg_sent = analyzer.sentiment_daily['avg_sentiment'].mean() if analyzer.sentiment_daily is not None else 0.0
|
| 391 |
summary_md = f"""
|
| 392 |
### 🎯 Decision: **{analyzer.decision}** (Score: {analyzer.total_score}/6)
|
| 393 |
+
- **Current Price:** `${current_price:.2f}`
|
| 394 |
- **Technical Score:** `{analyzer.scores['Technical']}`
|
| 395 |
+
- **Sentiment Score:** `{analyzer.scores['Sentiment']}` (Avg: {avg_sent:.2f})
|
| 396 |
- **Forecast Score:** `{analyzer.scores['Forecast']}` ({analyzer.forecast_pct:.1f}% change to `${analyzer.forecast_price:.2f}`)
|
| 397 |
"""
|
| 398 |
gauge_plot = Plotter.create_decision_gauge(analyzer.decision, analyzer.total_score)
|
|
|
|
| 410 |
# Assemble the accordion item
|
| 411 |
ticker_accordion = gr.Accordion(
|
| 412 |
label=f"📊 {ticker} Analysis",
|
| 413 |
+
open=ticker == tickers[0] # Open the first one by default
|
| 414 |
)
|
| 415 |
with ticker_accordion:
|
| 416 |
with gr.Tabs():
|
| 417 |
with gr.TabItem("📈 Summary & Decision"):
|
| 418 |
summary_col.render()
|
| 419 |
with gr.TabItem("😊 Sentiment Analysis"):
|
| 420 |
+
if isinstance(sentiment_plot, go.Figure):
|
| 421 |
+
gr.Plot(sentiment_plot).render()
|
| 422 |
+
else:
|
| 423 |
+
sentiment_plot.render()
|
| 424 |
with gr.TabItem("🔮 Forecast"):
|
| 425 |
+
if isinstance(forecast_plot, go.Figure):
|
| 426 |
+
gr.Plot(forecast_plot).render()
|
| 427 |
+
else:
|
| 428 |
+
forecast_plot.render()
|
| 429 |
accordion_items.append(ticker_accordion)
|
| 430 |
|
| 431 |
progress(1, "Analysis complete!")
|
|
|
|
| 433 |
|
| 434 |
|
| 435 |
# --- Build the Gradio App ---
|
| 436 |
+
with gr.Blocks(theme=gr.themes.Default(primary_hue="black", secondary_hue="purple"), title="Stock Intelligence Dashboard") as demo:
|
| 437 |
gr.Markdown("# 📊 Unified Stock Intelligence Dashboard")
|
| 438 |
gr.Markdown("An advanced tool for technical, sentiment, and predictive analysis of stocks.")
|
| 439 |
|