Spaces:
Build error
Build error
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yfinance as yf
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import plotly.graph_objs as go
|
| 5 |
+
from sklearn.preprocessing import MinMaxScaler
|
| 6 |
+
from keras.src.models.sequential import Sequential
|
| 7 |
+
from keras.src.layers import LSTM, Dense
|
| 8 |
+
import streamlit as st
|
| 9 |
+
from keras.src.callbacks import EarlyStopping
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Fetch stock data
|
| 13 |
+
def fetch_data(ticker):
|
| 14 |
+
return yf.download(ticker, period='6mo')
|
| 15 |
+
|
| 16 |
+
# Add indicators
|
| 17 |
+
def add_indicators(df):
|
| 18 |
+
df['SMA_20'] = df['Close'].rolling(window=20).mean()
|
| 19 |
+
df['SMA_50'] = df['Close'].rolling(window=50).mean()
|
| 20 |
+
|
| 21 |
+
std_20 = df['Close'].rolling(window=20).std()
|
| 22 |
+
std_20 = std_20.squeeze() # ensure it's a Series
|
| 23 |
+
|
| 24 |
+
df['BB_Upper'] = df['SMA_20'] + 2 * std_20
|
| 25 |
+
df['BB_Lower'] = df['SMA_20'] - 2 * std_20
|
| 26 |
+
|
| 27 |
+
df['RSI'] = compute_rsi(df['Close'])
|
| 28 |
+
df['MACD'], df['MACD_Signal'], df['MACD_Hist'] = compute_macd(df['Close'])
|
| 29 |
+
|
| 30 |
+
return df
|
| 31 |
+
|
| 32 |
+
# RSI calculation
|
| 33 |
+
def compute_rsi(series, period=14):
|
| 34 |
+
delta = series.diff()
|
| 35 |
+
gain = delta.clip(lower=0)
|
| 36 |
+
loss = -delta.clip(upper=0)
|
| 37 |
+
avg_gain = gain.rolling(period).mean()
|
| 38 |
+
avg_loss = loss.rolling(period).mean()
|
| 39 |
+
rs = avg_gain / avg_loss
|
| 40 |
+
return 100 - (100 / (1 + rs))
|
| 41 |
+
|
| 42 |
+
# MACD calculation
|
| 43 |
+
def compute_macd(series):
|
| 44 |
+
ema12 = series.ewm(span=12, adjust=False).mean()
|
| 45 |
+
ema26 = series.ewm(span=26, adjust=False).mean()
|
| 46 |
+
macd = ema12 - ema26
|
| 47 |
+
signal = macd.ewm(span=9, adjust=False).mean()
|
| 48 |
+
hist = macd - signal
|
| 49 |
+
return macd, signal, hist
|
| 50 |
+
|
| 51 |
+
# --- Detect candlestick patterns manually ---
|
| 52 |
+
def detect_patterns(df, sma_period=5):
|
| 53 |
+
patterns = []
|
| 54 |
+
hover_texts = []
|
| 55 |
+
|
| 56 |
+
# Calculate Simple Moving Average for trend detection
|
| 57 |
+
df['SMA'] = df['Close'].rolling(window=sma_period).mean()
|
| 58 |
+
sma = df.Close.rolling(window=sma_period).mean()
|
| 59 |
+
# Helper functions for trend direction, with NaN handling
|
| 60 |
+
def is_downtrend(i):
|
| 61 |
+
sma = df.iloc[i].SMA
|
| 62 |
+
if sma is None:
|
| 63 |
+
return False
|
| 64 |
+
return (df['Close'].iloc[i] < df['SMA'].iloc[i]).bool()
|
| 65 |
+
|
| 66 |
+
def is_uptrend(i):
|
| 67 |
+
sma = df.iloc[i].SMA
|
| 68 |
+
if sma is None:
|
| 69 |
+
return False
|
| 70 |
+
return (df['Close'].iloc[i] > df['SMA'].iloc[i]).bool()
|
| 71 |
+
|
| 72 |
+
for i in range(2, len(df)):
|
| 73 |
+
# Skip if not enough data for trend analysis
|
| 74 |
+
if pd.isna(df['SMA'].iloc[i]):
|
| 75 |
+
patterns.append("None")
|
| 76 |
+
hover_texts.append("")
|
| 77 |
+
continue
|
| 78 |
+
|
| 79 |
+
candle = df.iloc[i]
|
| 80 |
+
o = candle.Open.values
|
| 81 |
+
h = candle.High.values
|
| 82 |
+
l = candle.Low.values
|
| 83 |
+
c = candle.Close.values
|
| 84 |
+
|
| 85 |
+
# Previous candle
|
| 86 |
+
prev_candle = df.iloc[i-1]
|
| 87 |
+
prev_o = prev_candle.Open.values
|
| 88 |
+
prev_h = prev_candle.High.values
|
| 89 |
+
prev_l = prev_candle.Low.values
|
| 90 |
+
prev_c = prev_candle.Close.values
|
| 91 |
+
|
| 92 |
+
# Two candles back
|
| 93 |
+
prev_candle2 = df.iloc[i-2]
|
| 94 |
+
prev2_o = prev_candle2.Open.values
|
| 95 |
+
prev2_c = prev_candle2.Close.values
|
| 96 |
+
prev2_h = prev_candle2.High.values
|
| 97 |
+
prev2_l = prev_candle2.Low.values
|
| 98 |
+
|
| 99 |
+
# Calculations
|
| 100 |
+
body = abs(c - o)
|
| 101 |
+
upper_shadow = h - max(o, c)
|
| 102 |
+
lower_shadow = min(o, c) - l
|
| 103 |
+
total_range = h - l if h != l else 0.0001
|
| 104 |
+
prev_body = abs(prev_c - prev_o)
|
| 105 |
+
|
| 106 |
+
pattern = ""
|
| 107 |
+
|
| 108 |
+
# === Pattern Detection ===
|
| 109 |
+
lowShadBiggerBody = lower_shadow >= 2 * body
|
| 110 |
+
upShadSmallerBody = upper_shadow <= body
|
| 111 |
+
closeBiggerOpen = c > o
|
| 112 |
+
openBiggerClose = c < o
|
| 113 |
+
downTrend = is_downtrend(i - 1)
|
| 114 |
+
upTrend = is_uptrend(i - 1)
|
| 115 |
+
upperShadBiggerBody = upper_shadow >= 2 * body
|
| 116 |
+
lowShadSmallerBody = lower_shadow <= body
|
| 117 |
+
prevCloseSmallerPrevOpen = prev_c < prev_o
|
| 118 |
+
openBiggerPrevClose = o >= prev_c
|
| 119 |
+
openSmallerPrevClose = o <= prev_c
|
| 120 |
+
closeBiggerPrevOpen = c >= prev_o
|
| 121 |
+
closeSmallerPrevOpen = c <= prev_o
|
| 122 |
+
|
| 123 |
+
if body <= 0.1 * total_range:
|
| 124 |
+
pattern = "Doji"
|
| 125 |
+
|
| 126 |
+
elif (lowShadBiggerBody & upShadSmallerBody & closeBiggerOpen & downTrend):
|
| 127 |
+
pattern = "Hammer"
|
| 128 |
+
|
| 129 |
+
elif (upperShadBiggerBody & lowShadSmallerBody & closeBiggerOpen & downTrend):
|
| 130 |
+
pattern = "Inverted Hammer"
|
| 131 |
+
|
| 132 |
+
elif (prevCloseSmallerPrevOpen & closeBiggerOpen & openSmallerPrevClose &
|
| 133 |
+
closeBiggerPrevOpen & (body > prev_body) & downTrend):
|
| 134 |
+
pattern = "Bullish Engulfing"
|
| 135 |
+
|
| 136 |
+
elif ((prev_c > prev_o) & openBiggerClose & openBiggerPrevClose &
|
| 137 |
+
closeSmallerPrevOpen & (body > prev_body) & upTrend):
|
| 138 |
+
pattern = "Bearish Engulfing"
|
| 139 |
+
|
| 140 |
+
elif ((upper_shadow >= 1.5 * body) & (lower_shadow <= 0.2 * body) &
|
| 141 |
+
openBiggerClose & upTrend):
|
| 142 |
+
pattern = "Shooting Star"
|
| 143 |
+
|
| 144 |
+
elif ((lower_shadow >= 2 * body) & (upper_shadow <= 0.2 * body) &
|
| 145 |
+
openBiggerClose & upTrend):
|
| 146 |
+
pattern = "Hanging Man"
|
| 147 |
+
|
| 148 |
+
elif (openBiggerClose & (body >= 0.6 * total_range) &
|
| 149 |
+
(upper_shadow <= 0.15 * total_range) & (lower_shadow <= 0.15 * total_range)):
|
| 150 |
+
pattern = "Dark Pool"
|
| 151 |
+
|
| 152 |
+
elif ((prev2_c < prev2_o) &
|
| 153 |
+
(abs(prev_c - prev_o) <= 0.3 * (prev_h - prev_l)) &
|
| 154 |
+
closeBiggerOpen & (c > (prev_o + prev_c) / 2) & downTrend):
|
| 155 |
+
pattern = "Morning Star"
|
| 156 |
+
|
| 157 |
+
elif ((prev2_c > prev2_o) &
|
| 158 |
+
(abs(prev_c - prev_o) <= 0.3 * (prev_h - prev_l)) &
|
| 159 |
+
openBiggerClose & (c < (prev_o + prev_c) / 2) & upTrend):
|
| 160 |
+
pattern = "Evening Star"
|
| 161 |
+
|
| 162 |
+
else:
|
| 163 |
+
pattern = "None"
|
| 164 |
+
|
| 165 |
+
hover_text = PATTERN_DESCRIPTIONS.get(pattern, "") if pattern != "None" else ""
|
| 166 |
+
patterns.append(pattern)
|
| 167 |
+
hover_texts.append(hover_text)
|
| 168 |
+
|
| 169 |
+
# Align output with sliced data
|
| 170 |
+
df = df.iloc[2:].copy()
|
| 171 |
+
df['Pattern'] = patterns
|
| 172 |
+
df['HoverText'] = hover_texts
|
| 173 |
+
df.drop(columns=['SMA'], inplace=True)
|
| 174 |
+
return df
|
| 175 |
+
|
| 176 |
+
PATTERN_DESCRIPTIONS = {
|
| 177 |
+
"Hammer": "Bullish reversal pattern after a downtrend.",
|
| 178 |
+
"Inverted Hammer": "Potential bullish reversal with a long upper wick.",
|
| 179 |
+
"Bullish Engulfing": "Strong bullish signal after a bearish candle.",
|
| 180 |
+
"Bearish Engulfing": "Strong bearish signal after a bullish candle.",
|
| 181 |
+
"Doji": "Market indecision; open and close are very close.",
|
| 182 |
+
"Shooting Star": "Bearish reversal after uptrend with long upper wick.",
|
| 183 |
+
"Hanging Man": "Bearish signal with long lower shadow after uptrend.",
|
| 184 |
+
"Dark Pool": "Heavy bearish candle with short shadows.",
|
| 185 |
+
"Morning Star": "Bullish 3-candle reversal pattern.",
|
| 186 |
+
"Evening Star": "Bearish 3-candle reversal pattern."
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
# Predict next close price using LSTM
|
| 190 |
+
def predict_next_price(df):
|
| 191 |
+
# Select features
|
| 192 |
+
features = ['Close', 'SMA_20', 'SMA_50', 'RSI', 'MACD', 'MACD_Signal']
|
| 193 |
+
|
| 194 |
+
# Drop NaNs caused by rolling indicators
|
| 195 |
+
df = df[features].dropna()
|
| 196 |
+
|
| 197 |
+
if len(df) < 25:
|
| 198 |
+
return None # not enough data
|
| 199 |
+
|
| 200 |
+
# Scale all features
|
| 201 |
+
scaler = MinMaxScaler()
|
| 202 |
+
scaled_data = scaler.fit_transform(df)
|
| 203 |
+
|
| 204 |
+
lookback = 20
|
| 205 |
+
X = []
|
| 206 |
+
y = []
|
| 207 |
+
|
| 208 |
+
for i in range(lookback, len(scaled_data)):
|
| 209 |
+
X.append(scaled_data[i-lookback:i]) # shape: (lookback, num_features)
|
| 210 |
+
y.append(scaled_data[i, 0]) # predict 'Close'
|
| 211 |
+
|
| 212 |
+
X = np.array(X)
|
| 213 |
+
y = np.array(y)
|
| 214 |
+
|
| 215 |
+
# ๐ง Build model that takes multiple features
|
| 216 |
+
model = Sequential()
|
| 217 |
+
model.add(LSTM(64, return_sequences=True, input_shape=(lookback, len(features))))
|
| 218 |
+
model.add(LSTM(32))
|
| 219 |
+
model.add(Dense(1))
|
| 220 |
+
model.compile(optimizer='adam', loss='mean_squared_error')
|
| 221 |
+
model.fit(X, y, epochs=20, batch_size=8, verbose=0)
|
| 222 |
+
|
| 223 |
+
# Predict next day
|
| 224 |
+
last_sequence = scaled_data[-lookback:]
|
| 225 |
+
last_sequence = np.reshape(last_sequence, (1, lookback, len(features)))
|
| 226 |
+
predicted_scaled = model.predict(last_sequence, verbose=0)
|
| 227 |
+
|
| 228 |
+
# ๐ Inverse-transform: only the 'Close' feature
|
| 229 |
+
dummy = np.zeros((1, len(features)))
|
| 230 |
+
dummy[0, 0] = predicted_scaled[0, 0]
|
| 231 |
+
predicted_price = scaler.inverse_transform(dummy)[0, 0]
|
| 232 |
+
|
| 233 |
+
return predicted_price
|
| 234 |
+
|
| 235 |
+
# Chart generator
|
| 236 |
+
def plot_chart(df, ticker):
|
| 237 |
+
fig = go.Figure()
|
| 238 |
+
fig.add_trace(go.Scatter(x=df.index, y=df['Close'], name='Close', line=dict(color='blue')))
|
| 239 |
+
fig.add_trace(go.Scatter(x=df.index, y=df['SMA_20'], name='SMA 20', line=dict(color='orange')))
|
| 240 |
+
fig.add_trace(go.Scatter(x=df.index, y=df['SMA_50'], name='SMA 50', line=dict(color='purple')))
|
| 241 |
+
fig.add_trace(go.Scatter(x=df.index, y=df['BB_Upper'], name='BB Upper', line=dict(color='green', dash='dot')))
|
| 242 |
+
fig.add_trace(go.Scatter(x=df.index, y=df['BB_Lower'], name='BB Lower', line=dict(color='red', dash='dot')))
|
| 243 |
+
fig.update_layout(title=f"{ticker} Price with Indicators", xaxis_title="Date", yaxis_title="Price")
|
| 244 |
+
return fig
|
| 245 |
+
|
| 246 |
+
def plot_macd(df):
|
| 247 |
+
fig = go.Figure()
|
| 248 |
+
fig.add_trace(go.Scatter(x=df.index, y=df['MACD'], name='MACD', line=dict(color='blue')))
|
| 249 |
+
fig.add_trace(go.Scatter(x=df.index, y=df['MACD_Signal'], name='Signal', line=dict(color='orange')))
|
| 250 |
+
fig.add_trace(go.Bar(x=df.index, y=df['MACD_Hist'], name='Histogram', marker_color='gray'))
|
| 251 |
+
fig.update_layout(title='MACD line above the signal line -> (buy) signal โฌ๏ธ, MACD line below the signal line -> (sell) signal โฌ๏ธ', xaxis_title='Date', yaxis_title='MACD')
|
| 252 |
+
return fig
|
| 253 |
+
|
| 254 |
+
# Streamlit dashboard
|
| 255 |
+
st.set_page_config("๐ Smart Stock Analyzer", layout="wide")
|
| 256 |
+
st.title("๐ Smart Stock Analyzer with AI & Indicators")
|
| 257 |
+
|
| 258 |
+
tickers_input = st.text_input("Enter tickers (comma-separated):", value="NVDA, MSFT, AAPL, META, GOOGL, QNTM, ITOCY, MUFG, MARUY, SHL.DE, BCS, ING")
|
| 259 |
+
|
| 260 |
+
tickers = [t.strip().upper() for t in tickers_input.split(",") if t.strip()]
|
| 261 |
+
|
| 262 |
+
for ticker in tickers:
|
| 263 |
+
st.header(f"๐ Analyzing {ticker}")
|
| 264 |
+
try:
|
| 265 |
+
df = fetch_data(ticker)
|
| 266 |
+
df = df.copy()
|
| 267 |
+
patterns = detect_patterns(df, sma_period=5)
|
| 268 |
+
df = add_indicators(df)
|
| 269 |
+
current_price = df['Close'].iloc[-1].values[0]
|
| 270 |
+
predicted_price = predict_next_price(df)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
col1, col2 = st.columns(2)
|
| 274 |
+
|
| 275 |
+
with col1:
|
| 276 |
+
st.write(patterns[patterns['Pattern'] != ''].tail(10))
|
| 277 |
+
#st.plotly_chart(plot_chart(df, ticker), use_container_width=True)
|
| 278 |
+
with col2:
|
| 279 |
+
st.plotly_chart(plot_macd(df), use_container_width=True)
|
| 280 |
+
|
| 281 |
+
st.subheader("๐ Summary")
|
| 282 |
+
rsi = df['RSI'].iloc[-1]
|
| 283 |
+
st.markdown(f"- **RSI (above 70 = overbought conditions, below 30 = oversold conditions)**: {rsi:.2f} {'๐ข BUY' if rsi < 30 else '๐ด SELL' if rsi > 70 else '๐ก HOLD'}")
|
| 284 |
+
st.markdown(f"- **SMA Crossover (potentially signaling a shift in trend direction)**: {'โฌ๏ธ Bullish' if df['SMA_20'].iloc[-1] > df['SMA_50'].iloc[-1] else 'โฌ๏ธ Bearish'}")
|
| 285 |
+
|
| 286 |
+
st.subheader("๐ฎ AI Price Prediction")
|
| 287 |
+
st.markdown(f"- **Current Price**: ${current_price:.2f}")
|
| 288 |
+
st.markdown(f"- **Predicted Next Close**: ${predicted_price:.2f}")
|
| 289 |
+
change = predicted_price - current_price
|
| 290 |
+
percent = (change / current_price) * 100
|
| 291 |
+
if percent > 3:
|
| 292 |
+
st.success("๐ AI says: Likely UP (>3%) โ Consider BUYING")
|
| 293 |
+
elif percent < -3:
|
| 294 |
+
st.error("โ ๏ธ AI says: Likely DOWN (>3%) โ Consider SELLING")
|
| 295 |
+
else:
|
| 296 |
+
st.info("๐ค AI says: Not much movement expected โ HOLD")
|
| 297 |
+
|
| 298 |
+
except Exception as e:
|
| 299 |
+
st.error(f"Failed to analyze {ticker}: {e}")
|