stkmkt / app.py
markytools's picture
replaced plotly with matplotlib
6d4e8d7
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}")