Spaces:
Sleeping
Sleeping
File size: 12,816 Bytes
7a000af 6d4e8d7 7a000af 297af86 7a000af 742aa68 7a000af 297af86 7a000af 742aa68 7a000af 082f7e8 742aa68 082f7e8 742aa68 082f7e8 8405d25 7a000af 742aa68 297af86 6d4e8d7 742aa68 6d4e8d7 297af86 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 |
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}")
|