Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import yfinance as yf | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| import mplfinance as mpf | |
| import numpy as np | |
| import os | |
| from datetime import date, timedelta | |
| from langchain_openai import ChatOpenAI | |
| isPswdValid = False # Set to True to temporarily disable password checking. For production set to False. | |
| OPEN_ROUTER_KEY = st.secrets["OPEN_ROUTER_KEY"] | |
| OPEN_ROUTER_MODEL = "meta-llama/llama-3.3-70b-instruct:free" | |
| query_params = st.experimental_get_query_params() | |
| try: | |
| pswdVal = query_params['pwd'][0] # No need to worry about warning here as this will not be displayed in hg | |
| if pswdVal==st.secrets["PSWD"]: | |
| isPswdValid = True | |
| except: | |
| pass | |
| if not isPswdValid: | |
| st.write("Invalid Password") | |
| else: | |
| # Initialize language model | |
| llm = ChatOpenAI(model=OPEN_ROUTER_MODEL, temperature=0.1, openai_api_key=OPEN_ROUTER_KEY, openai_api_base="https://openrouter.ai/api/v1") | |
| # Set the Streamlit app title and icon | |
| st.set_page_config(page_title="Stock Analysis", page_icon="📈", layout="wide", initial_sidebar_state="expanded") | |
| # Global styling for a cleaner, modern look | |
| st.markdown( | |
| """ | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap'); | |
| :root { | |
| --bg: #0b1220; | |
| --card: rgba(255,255,255,0.03); | |
| --border: rgba(255,255,255,0.08); | |
| --text: #e8edf7; | |
| --muted: #a5b4d4; | |
| --accent: #6dd6ff; | |
| --accent-2: #7cf0c6; | |
| } | |
| .stApp { | |
| background: | |
| radial-gradient(circle at 10% 20%, rgba(80, 160, 255, 0.18), transparent 25%), | |
| radial-gradient(circle at 85% 10%, rgba(90, 223, 197, 0.15), transparent 22%), | |
| radial-gradient(circle at 50% 90%, rgba(255, 255, 255, 0.05), transparent 30%), | |
| var(--bg); | |
| color: var(--text); | |
| font-family: 'Space Grotesk', sans-serif; | |
| } | |
| div.block-container { | |
| padding-top: 2rem; | |
| padding-bottom: 2rem; | |
| max-width: 1200px; | |
| } | |
| div[data-testid="stSidebar"] { | |
| background: #0f172a; | |
| border-right: 1px solid rgba(255,255,255,0.05); | |
| } | |
| .hero-card { | |
| background: linear-gradient(135deg, rgba(71, 120, 210, 0.75), rgba(17, 39, 83, 0.9)); | |
| border: 1px solid rgba(255,255,255,0.06); | |
| border-radius: 16px; | |
| padding: 18px 20px; | |
| box-shadow: 0 10px 40px rgba(0,0,0,0.35); | |
| color: var(--text); | |
| margin-bottom: 1rem; | |
| } | |
| .hero-card h1 { margin-bottom: 0.35rem; font-size: 1.8rem; } | |
| .hero-pill { | |
| display: inline-block; | |
| background: rgba(255,255,255,0.12); | |
| padding: 6px 12px; | |
| border-radius: 999px; | |
| font-size: 0.85rem; | |
| letter-spacing: .05em; | |
| text-transform: uppercase; | |
| } | |
| .subdued { color: var(--muted); font-size: 0.95rem; } | |
| .metric-card { | |
| background: var(--card); | |
| border: 1px solid var(--border); | |
| padding: 14px 16px; | |
| border-radius: 12px; | |
| box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); | |
| } | |
| .metric-card h3 { | |
| margin: 0; | |
| font-size: .95rem; | |
| color: var(--muted); | |
| text-transform: uppercase; | |
| letter-spacing: .08em; | |
| } | |
| .metric-value { font-size: 1.4rem; font-weight: 700; color: var(--text); margin-top: 6px; } | |
| .delta { color: #7cf0c6; font-weight: 600; font-size: 0.95rem; } | |
| .delta.negative { color: #ff9b9b; } | |
| .section-caption { color: var(--muted); margin-top: -6px; margin-bottom: 10px; } | |
| </style> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| # Create a Streamlit sidebar for user input | |
| st.sidebar.title("Stock Analysis") | |
| ticker_symbol = st.sidebar.text_input("Enter Stock Ticker Symbol:", value='AAPL').strip().upper() | |
| default_end = date.today() - timedelta(days=1) | |
| default_start = default_end - timedelta(days=365 * 3) | |
| start_date = st.sidebar.date_input("Start Date", default_start) | |
| end_date = st.sidebar.date_input("End Date", default_end) | |
| st.sidebar.markdown("---") | |
| st.sidebar.caption("Tip: Choose a wide date range for smoother moving averages and richer AI insights.") | |
| def load_stock_data(symbol: str, start_dt, end_dt): | |
| """Download data with a flat schema to avoid Plotly/MultiIndex issues on some runtimes.""" | |
| try: | |
| ticker = yf.Ticker(symbol) | |
| data = ticker.history(start=start_dt, end=end_dt, actions=False, auto_adjust=False) | |
| except Exception: | |
| st.error("Error fetching stock data. Please check the ticker symbol and date range.") | |
| st.stop() | |
| if data.empty: | |
| st.error("No data returned. Try widening the date range or verifying the ticker.") | |
| st.stop() | |
| if isinstance(data.columns, pd.MultiIndex): | |
| data = data.copy() | |
| data.columns = data.columns.get_level_values(-1) | |
| data = data.reset_index() # Ensure Date is a column | |
| data["Date"] = pd.to_datetime(data["Date"]).dt.tz_localize(None) | |
| core_cols = ["Open", "High", "Low", "Close", "Volume"] | |
| if data[core_cols].dropna(how="all").empty: | |
| st.error("Downloaded data is empty or invalid. Try another ticker or date range.") | |
| st.stop() | |
| return data | |
| # Fetch stock data from Yahoo Finance | |
| df = load_stock_data(ticker_symbol, start_date, end_date) | |
| close_series = df['Close'] | |
| high_series = df['High'] | |
| low_series = df['Low'] | |
| volume_series = df['Volume'] | |
| debug_mode = query_params.get("debug", ["0"])[0] == "1" | |
| latest_close = close_series.iloc[-1] if not close_series.empty else None | |
| prev_close = close_series.iloc[-2] if len(close_series) > 1 else None | |
| change = (latest_close - prev_close) if latest_close is not None and prev_close is not None else None | |
| change_pct = (change / prev_close * 100) if change not in [None, 0] and prev_close not in [None, 0] else None | |
| period_high = high_series.max() if not high_series.empty else None | |
| period_low = low_series.min() if not low_series.empty else None | |
| avg_volume = volume_series.mean() if not volume_series.empty else None | |
| def fmt_currency(val): | |
| return "-" if val is None or pd.isna(val) else f"${val:,.2f}" | |
| def fmt_delta(delta_val, pct_val): | |
| if delta_val is None or pd.isna(delta_val): | |
| return "—", "" | |
| symbol = "negative" if delta_val < 0 else "" | |
| pct_text = f" ({pct_val:+.2f}%)" if pct_val not in [None, np.nan] else "" | |
| return f"{delta_val:+.2f}{pct_text}", symbol | |
| delta_text, delta_class = fmt_delta(change, change_pct) | |
| st.markdown( | |
| f""" | |
| <div class="hero-card"> | |
| <div class="hero-pill">Market Pulse</div> | |
| <h1>{ticker_symbol.upper()} | Stock Intelligence</h1> | |
| <p class="subdued">Sharper visuals for price action, technicals, and AI commentary across your chosen dates.</p> | |
| <div class="subdued">Range: {start_date.strftime('%b %d, %Y')} → {end_date.strftime('%b %d, %Y')}</div> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| mc1, mc2, mc3 = st.columns(3) | |
| mc1.markdown( | |
| f""" | |
| <div class="metric-card"> | |
| <h3>Last Close</h3> | |
| <div class="metric-value">{fmt_currency(latest_close)}</div> | |
| <div class="delta {delta_class}">{delta_text}</div> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| mc2.markdown( | |
| f""" | |
| <div class="metric-card"> | |
| <h3>Period Range</h3> | |
| <div class="metric-value">{fmt_currency(period_low)} – {fmt_currency(period_high)}</div> | |
| <div class="delta">Session High / Low</div> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| mc3.markdown( | |
| f""" | |
| <div class="metric-card"> | |
| <h3>Avg Volume</h3> | |
| <div class="metric-value">{'-' if avg_volume is None or pd.isna(avg_volume) else f"{avg_volume:,.0f}"}</div> | |
| <div class="delta">Across selected window</div> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| price_tab, indicators_tab, ai_tab = st.tabs(["Price Action", "Technical Indicators", "AI Deep Dive"]) | |
| with price_tab: | |
| st.subheader("Stock Price Chart") | |
| st.caption("Candlestick price action with volume on a shared timeline for quick at-a-glance context.") | |
| if debug_mode: | |
| st.write("Data sample:", df.head(3)) | |
| st.write("dtypes:", df.dtypes) | |
| df_plot = df.set_index("Date")[["Open", "High", "Low", "Close", "Volume"]] | |
| market_colors = mpf.make_marketcolors(up="#7cf0c6", down="#ff9b9b", edge="inherit", wick="inherit", volume="in") | |
| style = mpf.make_mpf_style(base_mpf_style="nightclouds", marketcolors=market_colors, facecolor="#0c1320", edgecolor="#0c1320", gridcolor="#1b2a45") | |
| fig, _ = mpf.plot( | |
| df_plot, | |
| type="candle", | |
| volume=True, | |
| style=style, | |
| returnfig=True, | |
| figsize=(10, 6), | |
| tight_layout=True, | |
| update_width_config=dict(candle_linewidth=0.8, candle_width=0.6), | |
| ) | |
| st.pyplot(fig, clear_figure=True) | |
| plt.close(fig) | |
| with indicators_tab: | |
| st.subheader("Moving Averages") | |
| st.caption("Compare recent closes against short and intermediate trend lines.") | |
| df['SMA_20'] = close_series.rolling(window=20).mean() | |
| df['SMA_50'] = close_series.rolling(window=50).mean() | |
| fig, ax = plt.subplots(figsize=(10, 4)) | |
| ax.plot(df['Date'], close_series, label='Close Price', color="#7cf0c6", linewidth=1.4) | |
| ax.plot(df['Date'], df['SMA_20'], label='20-Day SMA', color="#6dd6ff", linewidth=1.2) | |
| ax.plot(df['Date'], df['SMA_50'], label='50-Day SMA', color="#b0b8ff", linewidth=1.2) | |
| ax.set_ylabel("Price (USD)") | |
| ax.grid(alpha=0.2) | |
| ax.legend() | |
| fig.tight_layout() | |
| st.pyplot(fig, clear_figure=True) | |
| plt.close(fig) | |
| st.subheader("Relative Strength Index (RSI)") | |
| st.caption("Momentum oscillator highlighting overbought/oversold zones.") | |
| window_length = 14 | |
| delta = close_series.diff() | |
| gain = delta.where(delta > 0, 0) | |
| loss = -delta.where(delta < 0, 0) | |
| avg_gain = gain.rolling(window=window_length, min_periods=1).mean() | |
| avg_loss = loss.rolling(window=window_length, min_periods=1).mean() | |
| rs = avg_gain / avg_loss | |
| df['RSI'] = 100 - (100 / (1 + rs)) | |
| fig, ax = plt.subplots(figsize=(10, 3.5)) | |
| ax.plot(df['Date'], df['RSI'], label='RSI', color="#6dd6ff", linewidth=1.4) | |
| ax.axhline(70, color="#ff9b9b", linestyle="--", linewidth=1, label="Overbought") | |
| ax.axhline(30, color="#7cf0c6", linestyle="--", linewidth=1, label="Oversold") | |
| ax.set_ylabel("RSI") | |
| ax.grid(alpha=0.2) | |
| ax.legend() | |
| fig.tight_layout() | |
| st.pyplot(fig, clear_figure=True) | |
| plt.close(fig) | |
| st.subheader("Volume Analysis") | |
| st.caption("Volume bars styled to match the rest of the dashboard.") | |
| fig, ax = plt.subplots(figsize=(10, 3.5)) | |
| ax.bar(df['Date'], volume_series, color=(109/255, 214/255, 255/255, 0.55)) | |
| ax.set_ylabel("Volume") | |
| ax.grid(alpha=0.15) | |
| fig.tight_layout() | |
| st.pyplot(fig, clear_figure=True) | |
| plt.close(fig) | |
| with ai_tab: | |
| st.subheader("In-depth Analysis") | |
| st.caption("AI-generated commentary stays on a dedicated tab so charts remain uncluttered.") | |
| chatTextStr = f""" | |
| Analyze the following stock market data to identify notable patterns, trends, and anomalies. | |
| Summarize key price movements, volume behavior, and any significant shifts in market sentiment. | |
| Provide insights in clear, plain language and do not include any programming code. | |
| """ | |
| with st.spinner("Running in-depth AI analysis..."): | |
| try: | |
| answer = llm.predict(f''' | |
| I have yfinance data below on {ticker_symbol} symbol: | |
| {str(df[['Date', 'Open', 'High', 'Low', 'Close']].tail(30))} | |
| {chatTextStr} | |
| ''') | |
| st.write(answer) | |
| except Exception as exc: | |
| st.error(f"AI analysis failed: {exc}") | |