Spaces:
Build error
Build error
| import yfinance as yf | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.graph_objs as go | |
| from sklearn.preprocessing import MinMaxScaler | |
| from keras.src.models.sequential import Sequential | |
| from keras.src.layers import LSTM, Dense | |
| import streamlit as st | |
| from keras.src.callbacks import EarlyStopping | |
| # Fetch stock data | |
| def fetch_data(ticker): | |
| return yf.download(ticker, period='6mo') | |
| # Add indicators | |
| def add_indicators(df): | |
| df['SMA_20'] = df['Close'].rolling(window=20).mean() | |
| df['SMA_50'] = df['Close'].rolling(window=50).mean() | |
| std_20 = df['Close'].rolling(window=20).std() | |
| std_20 = std_20.squeeze() # ensure it's a Series | |
| df['BB_Upper'] = df['SMA_20'] + 2 * std_20 | |
| df['BB_Lower'] = df['SMA_20'] - 2 * std_20 | |
| df['RSI'] = compute_rsi(df['Close']) | |
| df['MACD'], df['MACD_Signal'], df['MACD_Hist'] = compute_macd(df['Close']) | |
| return df | |
| # RSI calculation | |
| def compute_rsi(series, period=14): | |
| delta = series.diff() | |
| gain = delta.clip(lower=0) | |
| loss = -delta.clip(upper=0) | |
| avg_gain = gain.rolling(period).mean() | |
| avg_loss = loss.rolling(period).mean() | |
| rs = avg_gain / avg_loss | |
| return 100 - (100 / (1 + rs)) | |
| # MACD calculation | |
| def compute_macd(series): | |
| ema12 = series.ewm(span=12, adjust=False).mean() | |
| ema26 = series.ewm(span=26, adjust=False).mean() | |
| macd = ema12 - ema26 | |
| signal = macd.ewm(span=9, adjust=False).mean() | |
| hist = macd - signal | |
| return macd, signal, hist | |
| # --- Detect candlestick patterns manually --- | |
| def detect_patterns(df, sma_period=5): | |
| patterns = [] | |
| hover_texts = [] | |
| # Calculate Simple Moving Average for trend detection | |
| df['SMA'] = df['Close'].rolling(window=sma_period).mean() | |
| sma = df.Close.rolling(window=sma_period).mean() | |
| # Helper functions for trend direction, with NaN handling | |
| def is_downtrend(i): | |
| sma = df.iloc[i].SMA | |
| if sma is None: | |
| return False | |
| return (df['Close'].iloc[i] < df['SMA'].iloc[i]).bool() | |
| def is_uptrend(i): | |
| sma = df.iloc[i].SMA | |
| if sma is None: | |
| return False | |
| return (df['Close'].iloc[i] > df['SMA'].iloc[i]).bool() | |
| for i in range(2, len(df)): | |
| # Skip if not enough data for trend analysis | |
| if pd.isna(df['SMA'].iloc[i]): | |
| patterns.append("None") | |
| hover_texts.append("") | |
| continue | |
| candle = df.iloc[i] | |
| o = candle.Open.values | |
| h = candle.High.values | |
| l = candle.Low.values | |
| c = candle.Close.values | |
| # Previous candle | |
| prev_candle = df.iloc[i-1] | |
| prev_o = prev_candle.Open.values | |
| prev_h = prev_candle.High.values | |
| prev_l = prev_candle.Low.values | |
| prev_c = prev_candle.Close.values | |
| # Two candles back | |
| prev_candle2 = df.iloc[i-2] | |
| prev2_o = prev_candle2.Open.values | |
| prev2_c = prev_candle2.Close.values | |
| prev2_h = prev_candle2.High.values | |
| prev2_l = prev_candle2.Low.values | |
| # Calculations | |
| body = abs(c - o) | |
| upper_shadow = h - max(o, c) | |
| lower_shadow = min(o, c) - l | |
| total_range = h - l if h != l else 0.0001 | |
| prev_body = abs(prev_c - prev_o) | |
| pattern = "" | |
| # === Pattern Detection === | |
| lowShadBiggerBody = lower_shadow >= 2 * body | |
| upShadSmallerBody = upper_shadow <= body | |
| closeBiggerOpen = c > o | |
| openBiggerClose = c < o | |
| downTrend = is_downtrend(i - 1) | |
| upTrend = is_uptrend(i - 1) | |
| upperShadBiggerBody = upper_shadow >= 2 * body | |
| lowShadSmallerBody = lower_shadow <= body | |
| prevCloseSmallerPrevOpen = prev_c < prev_o | |
| openBiggerPrevClose = o >= prev_c | |
| openSmallerPrevClose = o <= prev_c | |
| closeBiggerPrevOpen = c >= prev_o | |
| closeSmallerPrevOpen = c <= prev_o | |
| if body <= 0.1 * total_range: | |
| pattern = "Doji" | |
| elif (lowShadBiggerBody & upShadSmallerBody & closeBiggerOpen & downTrend): | |
| pattern = "Hammer" | |
| elif (upperShadBiggerBody & lowShadSmallerBody & closeBiggerOpen & downTrend): | |
| pattern = "Inverted Hammer" | |
| elif (prevCloseSmallerPrevOpen & closeBiggerOpen & openSmallerPrevClose & | |
| closeBiggerPrevOpen & (body > prev_body) & downTrend): | |
| pattern = "Bullish Engulfing" | |
| elif ((prev_c > prev_o) & openBiggerClose & openBiggerPrevClose & | |
| closeSmallerPrevOpen & (body > prev_body) & upTrend): | |
| pattern = "Bearish Engulfing" | |
| elif ((upper_shadow >= 1.5 * body) & (lower_shadow <= 0.2 * body) & | |
| openBiggerClose & upTrend): | |
| pattern = "Shooting Star" | |
| elif ((lower_shadow >= 2 * body) & (upper_shadow <= 0.2 * body) & | |
| openBiggerClose & upTrend): | |
| pattern = "Hanging Man" | |
| elif (openBiggerClose & (body >= 0.6 * total_range) & | |
| (upper_shadow <= 0.15 * total_range) & (lower_shadow <= 0.15 * total_range)): | |
| pattern = "Dark Pool" | |
| elif ((prev2_c < prev2_o) & | |
| (abs(prev_c - prev_o) <= 0.3 * (prev_h - prev_l)) & | |
| closeBiggerOpen & (c > (prev_o + prev_c) / 2) & downTrend): | |
| pattern = "Morning Star" | |
| elif ((prev2_c > prev2_o) & | |
| (abs(prev_c - prev_o) <= 0.3 * (prev_h - prev_l)) & | |
| openBiggerClose & (c < (prev_o + prev_c) / 2) & upTrend): | |
| pattern = "Evening Star" | |
| else: | |
| pattern = "None" | |
| hover_text = PATTERN_DESCRIPTIONS.get(pattern, "") if pattern != "None" else "" | |
| patterns.append(pattern) | |
| hover_texts.append(hover_text) | |
| # Align output with sliced data | |
| df = df.iloc[2:].copy() | |
| df['Pattern'] = patterns | |
| df['HoverText'] = hover_texts | |
| df.drop(columns=['SMA'], inplace=True) | |
| return df | |
| PATTERN_DESCRIPTIONS = { | |
| "Hammer": "Bullish reversal pattern after a downtrend.", | |
| "Inverted Hammer": "Potential bullish reversal with a long upper wick.", | |
| "Bullish Engulfing": "Strong bullish signal after a bearish candle.", | |
| "Bearish Engulfing": "Strong bearish signal after a bullish candle.", | |
| "Doji": "Market indecision; open and close are very close.", | |
| "Shooting Star": "Bearish reversal after uptrend with long upper wick.", | |
| "Hanging Man": "Bearish signal with long lower shadow after uptrend.", | |
| "Dark Pool": "Heavy bearish candle with short shadows.", | |
| "Morning Star": "Bullish 3-candle reversal pattern.", | |
| "Evening Star": "Bearish 3-candle reversal pattern." | |
| } | |
| # Predict next close price using LSTM | |
| def predict_next_price(df): | |
| # Select features | |
| features = ['Close', 'SMA_20', 'SMA_50', 'RSI', 'MACD', 'MACD_Signal'] | |
| # Drop NaNs caused by rolling indicators | |
| df = df[features].dropna() | |
| if len(df) < 25: | |
| return None # not enough data | |
| # Scale all features | |
| scaler = MinMaxScaler() | |
| scaled_data = scaler.fit_transform(df) | |
| lookback = 20 | |
| X = [] | |
| y = [] | |
| for i in range(lookback, len(scaled_data)): | |
| X.append(scaled_data[i-lookback:i]) # shape: (lookback, num_features) | |
| y.append(scaled_data[i, 0]) # predict 'Close' | |
| X = np.array(X) | |
| y = np.array(y) | |
| # ๐ง Build model that takes multiple features | |
| model = Sequential() | |
| model.add(LSTM(64, return_sequences=True, input_shape=(lookback, len(features)))) | |
| model.add(LSTM(32)) | |
| model.add(Dense(1)) | |
| model.compile(optimizer='adam', loss='mean_squared_error') | |
| model.fit(X, y, epochs=20, batch_size=8, verbose=0) | |
| # Predict next day | |
| last_sequence = scaled_data[-lookback:] | |
| last_sequence = np.reshape(last_sequence, (1, lookback, len(features))) | |
| predicted_scaled = model.predict(last_sequence, verbose=0) | |
| # ๐ Inverse-transform: only the 'Close' feature | |
| dummy = np.zeros((1, len(features))) | |
| dummy[0, 0] = predicted_scaled[0, 0] | |
| predicted_price = scaler.inverse_transform(dummy)[0, 0] | |
| return predicted_price | |
| # Chart generator | |
| def plot_chart(df, ticker): | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter(x=df.index, y=df['Close'], name='Close', line=dict(color='blue'))) | |
| fig.add_trace(go.Scatter(x=df.index, y=df['SMA_20'], name='SMA 20', line=dict(color='orange'))) | |
| fig.add_trace(go.Scatter(x=df.index, y=df['SMA_50'], name='SMA 50', line=dict(color='purple'))) | |
| fig.add_trace(go.Scatter(x=df.index, y=df['BB_Upper'], name='BB Upper', line=dict(color='green', dash='dot'))) | |
| fig.add_trace(go.Scatter(x=df.index, y=df['BB_Lower'], name='BB Lower', line=dict(color='red', dash='dot'))) | |
| fig.update_layout(title=f"{ticker} Price with Indicators", xaxis_title="Date", yaxis_title="Price") | |
| return fig | |
| def plot_macd(df): | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter(x=df.index, y=df['MACD'], name='MACD', line=dict(color='blue'))) | |
| fig.add_trace(go.Scatter(x=df.index, y=df['MACD_Signal'], name='Signal', line=dict(color='orange'))) | |
| fig.add_trace(go.Bar(x=df.index, y=df['MACD_Hist'], name='Histogram', marker_color='gray')) | |
| 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') | |
| return fig | |
| # Streamlit dashboard | |
| st.set_page_config("๐ Smart Stock Analyzer", layout="wide") | |
| st.title("๐ Smart Stock Analyzer with AI & Indicators") | |
| tickers_input = st.text_input("Enter tickers (comma-separated):", value="QNTM") | |
| tickers = [t.strip().upper() for t in tickers_input.split(",") if t.strip()] | |
| for ticker in tickers: | |
| st.header(f"๐ Analyzing {ticker}") | |
| try: | |
| df = fetch_data(ticker) | |
| df = df.copy() | |
| patterns = detect_patterns(df, sma_period=5) | |
| df = add_indicators(df) | |
| current_price = df['Close'].iloc[-1].values[0] | |
| predicted_price = predict_next_price(df) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.write(patterns[patterns['Pattern'] != ''].tail(10)) | |
| #st.plotly_chart(plot_chart(df, ticker), use_container_width=True) | |
| with col2: | |
| st.plotly_chart(plot_macd(df), use_container_width=True) | |
| st.subheader("๐ Summary") | |
| rsi = df['RSI'].iloc[-1] | |
| 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'}") | |
| 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'}") | |
| st.subheader("๐ฎ AI Price Prediction") | |
| st.markdown(f"- **Current Price**: ${current_price:.2f}") | |
| st.markdown(f"- **Predicted Next Close**: ${predicted_price:.2f}") | |
| change = predicted_price - current_price | |
| percent = (change / current_price) * 100 | |
| if percent > 3: | |
| st.success("๐ AI says: Likely UP (>3%) โ Consider BUYING") | |
| elif percent < -3: | |
| st.error("โ ๏ธ AI says: Likely DOWN (>3%) โ Consider SELLING") | |
| else: | |
| st.info("๐ค AI says: Not much movement expected โ HOLD") | |
| except Exception as e: | |
| st.error(f"Failed to analyze {ticker}: {e}") |