Stock Price Prediction with LSTM

Build an LSTM neural network to forecast stock prices in a Streamlit dashboard.

Introduction

30 Second Summary

Everyone wants to know where stock prices are heading next. Whether you check your portfolio daily or just follow the market headlines, the idea of predicting future prices feels like a superpower.

In this project, you will build an interactive dashboard that uses a neural network to forecast stock prices for any company you type in. You will go from raw market data all the way to a live web app that displays predicted prices on an interactive chart.

What You'll Build

An LSTM-powered Streamlit web app that fetches real stock data, trains a model on the fly, and displays historical prices alongside a 30-day predicted forecast on an interactive Plotly chart.

By the end of this project, you'll have:

  • A trained LSTM model that predicts stock prices for any publicly traded company ticker.
  • An interactive Streamlit dashboard where you type a ticker symbol and see historical prices alongside a color-coded predicted forecast.
  • Hands-on experience with a complete ML pipeline: data collection, feature engineering, model training, evaluation, and deployment in a web app.
  • Secret Mission: Add RSI (Relative Strength Index) as an additional feature to improve model accuracy and display it in a secondary chart panel.

Are there any prerequisites?

You should have basic Python knowledge (variables, functions, loops). Some familiarity with ML concepts like training, testing, and loss functions is helpful but not required.

Before We Start

To build a stock prediction dashboard, you need a working Python environment with specific machine learning libraries installed. TensorFlow is the backbone of this project, and it requires Python 3.13 specifically.

Your machine currently has Python 3.14, which TensorFlow does not yet support on Windows. In this step, you will install Python 3.13 side-by-side, create an isolated virtual environment, and install all the packages your dashboard needs.

In this step, get ready to:
  • Install Python 3.13 from python.org.
  • Create a virtual environment and install TensorFlow, yfinance, Streamlit, Plotly, and scikit-learn.
  • Verify that TensorFlow 2.21.0 imports successfully.
Install Python 3.13

TensorFlow 2.21.0 only supports Python 3.10 through 3.13 on Windows. Since your system has Python 3.14, you need to install 3.13 alongside it.

  • Open VS Code by pressing the Windows key, typing VS Code, and pressing Enter.
  • Open the integrated terminal by pressing Ctrl+` (backtick key, usually above Tab).
  • Check whether Python 3.13 is already installed by running this command:
py -3.13 --version

✔️ I see Python 3.13

Python 3.13 is already on your machine. Skip ahead to the next substep where you create a virtual environment.

ⓧ I see an error about the version not being installed

Python 3.13 is not on your machine yet. Follow these steps to install it.

  • Open the Python 3.13.14 download page in your browser:
  • https://www.python.org/downloads/release/python-31314/
  • Scroll down to the files table and click Windows installer (64-bit) to download it.
  • Run the downloaded installer.
  • On the first screen of the installer, check the box labelled Add python.exe to PATH.
  • Click Install Now and wait for the installation to complete.
  • Close and reopen your VS Code terminal (Ctrl+` twice) so it picks up the new PATH.
  • Verify the installation by running:
py -3.13 --version

You should see Python 3.13.14 printed in the terminal.

ⓧ 'py' is not recognized

The Windows Python Launcher is not available. This can happen if Python was installed from the Microsoft Store instead of python.org.

  • Open the Python 3.13.14 download page in your browser:
  • https://www.python.org/downloads/release/python-31314/
  • Scroll down to the files table and click Windows installer (64-bit) to download it.
  • Run the downloaded installer.
  • On the first screen of the installer, check the box labelled Add python.exe to PATH.
  • Click Install Now and wait for the installation to complete. This also installs the py launcher.
  • Close and reopen your VS Code terminal (Ctrl+` twice) so it picks up the new PATH.
  • Verify the installation by running:
py -3.13 --version

You should see Python 3.13.14 printed in the terminal.

Why Python 3.13 instead of 3.14?

TensorFlow publishes pre-built packages (called wheels) for specific Python versions. As of June 2026, no TensorFlow wheel exists for Python 3.14 on Windows.

Installing Python 3.13 alongside 3.14 is safe. The Windows py launcher lets you target a specific version with py -3.13, so the two installations never conflict.

Create a virtual environment and install dependencies

A virtual environment keeps this project's packages separate from your system Python. You will create one using Python 3.13 specifically, then install everything the dashboard needs.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create a project folder, move into it, and set up the virtual environment by running these commands:
mkdir stock-predictor
cd stock-predictor
py -3.13 -m venv stock-predictor-env
  • Activate the virtual environment by running:
stock-predictor-env\Scripts\activate

You should see (stock-predictor-env) appear at the start of your terminal prompt. This confirms you are inside the virtual environment.

  • Install all project dependencies by running:
pip install tensorflow==2.21.0 yfinance==1.4.1 streamlit==1.58.0 plotly==6.8.0 scikit-learn pandas numpy

What are these packages?

  • tensorflow provides the LSTM neural network you will train.
  • yfinance downloads historical stock data from Yahoo Finance with no API key required.
  • streamlit turns your Python script into an interactive web dashboard.
  • plotly renders interactive, zoomable charts inside Streamlit.
  • scikit-learn provides the MinMaxScaler for normalizing data before training.
  • pandas and numpy handle data manipulation and numerical operations.

Installation taking a long time or failing?

TensorFlow is a large package (over 500 MB). The download can take several minutes on slower connections.

If you see a permissions error, make sure your virtual environment is activated. You should see (stock-predictor-env) in your prompt.

If pip cannot find a compatible TensorFlow wheel, confirm you are using the Python 3.13 venv by running python --version inside the activated environment.

Help me troubleshoot my pip install failure

Verify the installation
  • Confirm TensorFlow installed correctly by running:
python -c "import tensorflow as tf; print(tf.__version__)"

You should see 2.21.0 printed with no errors.

Seeing a ModuleNotFoundError?

Make sure your virtual environment is still activated. Run stock-predictor-env\Scripts\activate again if the (stock-predictor-env) prefix disappeared from your prompt.

If you accidentally ran python outside the venv, it may use your system Python 3.14 which does not have TensorFlow installed.

Help me fix my TensorFlow import error

Your environment is ready. Next up, you will fetch real stock market data and display it in your first Streamlit chart.

Fetch and Explore Stock Data

In the previous step, you set up your Python environment with all the packages your stock predictor needs. Now it's time to connect to real financial data and see it on screen.

Every ML project starts with data. In this step, you'll write a function that pulls 5 years of historical stock prices from Yahoo Finance, then build a basic Streamlit dashboard that displays that data as an interactive chart.

In this step, get ready to:
  • Create a data-fetching function that downloads stock prices from Yahoo Finance.
  • Build a Streamlit app with a ticker input and a Plotly chart.
  • Run the dashboard and explore real stock data interactively.
Write the data-fetching function

Your LSTM model will need historical stock data to learn from. The yfinance library connects directly to Yahoo Finance and returns data as a pandas DataFrame, so you get clean tabular data with one line of code.

  • In VS Code, create a new file called model.py in your project folder (the same folder where your virtual environment lives).
  • Add the following code to model.py:
import numpy as np
import pandas as pd
import yfinance as yf
from sklearn.preprocessing import MinMaxScaler
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout


def fetch_data(ticker, period="5y"):
    """Download historical stock data from Yahoo Finance."""
    data = yf.download(ticker, period=period, progress=False)
    df = data[["Close", "Volume"]].copy()
    df.reset_index(inplace=True)
    df.columns = ["Date", "Close", "Volume"]
    return df

What does this code do?

The imports at the top bring in all the libraries you'll need throughout this project. You won't use all of them right now, but importing them early keeps the file organized as you add more functions in later steps.

  • yf.download() fetches historical market data from Yahoo Finance for the given ticker symbol.
  • The period="5y" argument requests 5 years of daily price data.
  • You extract just the Close and Volume columns, since those are the features the LSTM will learn from.
  • reset_index() moves the Date from the index into a regular column so it's easier to work with.
  • Save model.py.
  • You won't see a visible change yet. The next substep will use this function in a web app.

Seeing a red underline on the imports?

If VS Code shows import errors, make sure your stock-predictor-env virtual environment is selected as the Python interpreter. Click the Python version in the bottom status bar of VS Code, then select the interpreter path that includes stock-predictor-env.

help me fix import errors in VS Code for my stock predictor project

Build the Streamlit dashboard

Streamlit turns a plain Python script into a web app with just a few function calls. You'll create an app that takes a ticker symbol, fetches its data, and plots the historical closing price using Plotly.

  • Create a new file called app.py in the same folder as model.py.
  • Add the following code to app.py:
import streamlit as st
import plotly.graph_objects as go
import pandas as pd
from datetime import timedelta
from model import fetch_data

st.set_page_config(page_title="Stock Price Predictor", layout="wide")
st.title("Stock Price Prediction Dashboard")
st.write("Enter any stock ticker to see historical data and a 30-day LSTM forecast.")

ticker = st.text_input("Stock Ticker", value="AAPL", max_chars=10)
predict_button = st.button("Predict")

if predict_button and ticker:
    with st.spinner("Fetching data..."):
        df = fetch_data(ticker.upper())

    if df.empty:
        st.error("Could not fetch data for that ticker. Please check the symbol and try again.")
    else:
        st.success(f"Fetched {len(df)} days of data for {ticker.upper()}")
        st.write(f"Data shape: {df.shape}")

        fig = go.Figure()
        fig.add_trace(go.Scatter(
            x=df["Date"],
            y=df["Close"],
            mode="lines",
            name="Historical Price",
            line=dict(color="blue")
        ))
        fig.update_layout(
            title=f"{ticker.upper()} - Historical Closing Price",
            xaxis_title="Date",
            yaxis_title="Price (USD)",
            template="plotly_white"
        )
        st.plotly_chart(fig, use_container_width=True)

What does this code do?

This file is your entire web dashboard in one script. Streamlit reads it top to bottom and renders each element in order.

  • st.text_input() creates a text field where users type a stock ticker (defaulting to AAPL).
  • st.button("Predict") creates a button that triggers the data fetch and chart rendering when clicked.
  • st.spinner() shows a loading message while the Yahoo Finance download runs.
  • go.Figure() and go.Scatter() build an interactive Plotly line chart. Users can hover, zoom, and pan.
  • st.plotly_chart() embeds the Plotly figure directly into the Streamlit page.
  • Save app.py.

Seeing an error on the import from model?

The line from model import fetch_data requires that model.py exists in the same folder as app.py. Make sure both files are saved in the same directory.

help me fix my model import in app.py

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the full model.py at this point:

import numpy as np
import pandas as pd
import yfinance as yf
from sklearn.preprocessing import MinMaxScaler
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout


def fetch_data(ticker, period="5y"):
    """Download historical stock data from Yahoo Finance."""
    data = yf.download(ticker, period=period, progress=False)
    df = data[["Close", "Volume"]].copy()
    df.reset_index(inplace=True)
    df.columns = ["Date", "Close", "Volume"]
    return df

And here is the full app.py:

import streamlit as st
import plotly.graph_objects as go
import pandas as pd
from datetime import timedelta
from model import fetch_data

st.set_page_config(page_title="Stock Price Predictor", layout="wide")
st.title("Stock Price Prediction Dashboard")
st.write("Enter any stock ticker to see historical data and a 30-day LSTM forecast.")

ticker = st.text_input("Stock Ticker", value="AAPL", max_chars=10)
predict_button = st.button("Predict")

if predict_button and ticker:
    with st.spinner("Fetching data..."):
        df = fetch_data(ticker.upper())

    if df.empty:
        st.error("Could not fetch data for that ticker. Please check the symbol and try again.")
    else:
        st.success(f"Fetched {len(df)} days of data for {ticker.upper()}")
        st.write(f"Data shape: {df.shape}")

        fig = go.Figure()
        fig.add_trace(go.Scatter(
            x=df["Date"],
            y=df["Close"],
            mode="lines",
            name="Historical Price",
            line=dict(color="blue")
        ))
        fig.update_layout(
            title=f"{ticker.upper()} - Historical Closing Price",
            xaxis_title="Date",
            yaxis_title="Price (USD)",
            template="plotly_white"
        )
        st.plotly_chart(fig, use_container_width=True)
Run the app and explore stock data

Time to see your dashboard in action. Streamlit's run command starts a local server and automatically opens the app in your browser.

  • Launch the Streamlit app by running this command in your terminal (make sure your virtual environment is still active):
streamlit run app.py

A new browser tab opens automatically with your dashboard. You should see the title "Stock Price Prediction Dashboard", a text input pre-filled with AAPL, and a Predict button.

  • Click the Predict button.
  • Wait a few seconds for the data to download from Yahoo Finance.

You should see a success message showing how many days of data were fetched, and an interactive blue line chart of the closing price over time. Try hovering over the chart to see exact prices, or drag to zoom into a specific time range.

  • Try changing the ticker to TSLA or MSFT and clicking Predict again to see different stocks.

App not loading or showing an error?

Make sure your stock-predictor-env virtual environment is active in the terminal (you should see (stock-predictor-env) at the start of your prompt). If the app shows a ModuleNotFoundError, your terminal is likely using the wrong Python environment.

Also confirm that model.py and app.py are in the same folder, and that you're running the command from that folder.

help me troubleshoot my Streamlit app not loading

You now have real financial data flowing into an interactive web app. Next up, you'll engineer features from this raw price data to give your LSTM model more patterns to learn from.

Engineer Features and Prepare Data for LSTM

Your app can fetch and display raw stock prices. That's a good start, but an LSTM model needs more than a single price column to learn meaningful patterns.

In this step, you'll perform feature engineering to create technical indicators that help the model spot trends. Then you'll transform everything into the sliding-window sequences that LSTMs require as input.

In this step, get ready to:
  • Compute SMA and EMA technical indicators for the stock data.
  • Build sliding window sequences and split data chronologically into train/test sets.
  • Wire the data pipeline into your Streamlit app and confirm the sample counts.
Add technical indicators to your data

Raw closing prices alone don't give the model enough context about market momentum. Technical indicators like moving averages summarize recent price behavior into signals the model can learn from.

  • In model.py, add the following function below your existing fetch_data function:
def prepare_features(df):
    """Add technical indicators as features."""
    df = df.copy()
    # Calculate 20-day Simple Moving Average
    df["SMA_20"] = df["Close"].rolling(window=20).mean()
    # Calculate 20-day Exponential Moving Average
    df["EMA_20"] = df["Close"].ewm(span=20, adjust=False).mean()
    # Drop rows where rolling calculations produced NaN
    df.dropna(inplace=True)
    df.reset_index(drop=True, inplace=True)
    return df

What does this code do?

SMA_20 is the Simple Moving Average over the last 20 days. It smooths out daily noise so the model can see the underlying trend.

EMA_20 is the Exponential Moving Average, which gives more weight to recent prices. This makes it respond faster to sudden price changes than SMA.

The first 19 rows will have NaN values because there aren't enough previous days to compute a 20-day average. dropna() removes those incomplete rows.

  • Save model.py.

You won't see a visible change yet. The next function will use these features to build the LSTM input sequences.

Create sliding window sequences for the LSTM

LSTMs learn by looking at sequences of consecutive days. You need to reshape your data into "windows" where each sample contains 60 days of history, and the label is the next day's closing price. You also need to normalize everything to the 0-1 range so the model trains efficiently.

  • In model.py, add this function below prepare_features:
def prepare_lstm_data(df, lookback=60):
    """Scale features and create sequences for LSTM input."""
    feature_columns = ["Close", "Volume", "SMA_20", "EMA_20"]
    data = df[feature_columns].values

    # Normalize all features to 0-1 range
    scaler = MinMaxScaler(feature_range=(0, 1))
    scaled_data = scaler.fit_transform(data)

    # Create sliding window sequences
    X, y = [], []
    for i in range(lookback, len(scaled_data)):
        X.append(scaled_data[i - lookback:i])
        y.append(scaled_data[i, 0])

    X, y = np.array(X), np.array(y)

    # Split chronologically - no shuffling for time series
    split = int(len(X) * 0.8)
    X_train, X_test = X[:split], X[split:]
    y_train, y_test = y[:split], y[split:]

    return X_train, X_test, y_train, y_test, scaler

What does this code do?

MinMaxScaler normalizes each feature column to a 0-1 range. Neural networks train much faster and more reliably when inputs are on a similar scale.

The sliding window loop creates sequences of 60 consecutive days. Each X sample is a (60, 4) array containing 60 days of all 4 features. Each y label is the scaled closing price on day 61.

The 80/20 split is done chronologically (the first 80% for training, the last 20% for testing). Shuffling would leak future data into training, which is a critical mistake in time-series problems.

Why no shuffling?

In typical machine learning, you shuffle data before splitting. With stock prices, shuffling would let the model "peek" at future prices during training. A chronological split ensures the model only learns from the past and is evaluated on genuinely unseen future data.

  • Save model.py.

You won't see output yet. The next substep connects these functions to your Streamlit app.

✔️ Awesome, I've got everything!

Great. Double check you've saved model.py with both new functions.

ⓧ I'd like to double check the full code

import numpy as np
import pandas as pd
import yfinance as yf
from sklearn.preprocessing import MinMaxScaler
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout


def fetch_data(ticker, period="5y"):
    """Download historical stock data from Yahoo Finance."""
    data = yf.download(ticker, period=period, progress=False)
    df = data[["Close", "Volume"]].copy()
    df.reset_index(inplace=True)
    df.columns = ["Date", "Close", "Volume"]
    return df


def prepare_features(df):
    """Add technical indicators as features."""
    df = df.copy()
    df["SMA_20"] = df["Close"].rolling(window=20).mean()
    df["EMA_20"] = df["Close"].ewm(span=20, adjust=False).mean()
    df.dropna(inplace=True)
    df.reset_index(drop=True, inplace=True)
    return df


def prepare_lstm_data(df, lookback=60):
    """Scale features and create sequences for LSTM input."""
    feature_columns = ["Close", "Volume", "SMA_20", "EMA_20"]
    data = df[feature_columns].values

    scaler = MinMaxScaler(feature_range=(0, 1))
    scaled_data = scaler.fit_transform(data)

    X, y = [], []
    for i in range(lookback, len(scaled_data)):
        X.append(scaled_data[i - lookback:i])
        y.append(scaled_data[i, 0])

    X, y = np.array(X), np.array(y)

    split = int(len(X) * 0.8)
    X_train, X_test = X[:split], X[split:]
    y_train, y_test = y[:split], y[split:]

    return X_train, X_test, y_train, y_test, scaler
Connect the data pipeline in your app

Now you'll update app.py to import and call both functions, showing progress messages as the data is prepared.

  • In app.py, find this import line at the top:
from model import fetch_data
  • Replace it with this updated import:
from model import fetch_data, prepare_features, prepare_lstm_data
  • Now find this section inside the else: block, starting after the st.success(...) line:
        st.write(f"Data shape: {df.shape}")

        fig = go.Figure()
        fig.add_trace(go.Scatter(
            x=df["Date"],
            y=df["Close"],
            mode="lines",
            name="Historical Price",
            line=dict(color="blue")
        ))
        fig.update_layout(
            title=f"{ticker.upper()} - Historical Closing Price",
            xaxis_title="Date",
            yaxis_title="Price (USD)",
            template="plotly_white"
        )
        st.plotly_chart(fig, use_container_width=True)
  • Delete that entire section and replace it with the following code:
        # Compute technical indicators
        with st.spinner("Preparing features..."):
            df_features = prepare_features(df)

        # Create LSTM input sequences and split data
        with st.spinner("Preparing LSTM sequences..."):
            X_train, X_test, y_train, y_test, scaler = prepare_lstm_data(df_features)
            st.write(f"Training samples: {len(X_train)}, Test samples: {len(X_test)}")

What does this code do?

Each st.spinner() block displays a loading message while the enclosed function runs. This gives the user visual feedback that something is happening.

After the data is prepared, st.write() displays the exact number of training and test samples. This confirms the pipeline ran successfully and shows how much data the model will learn from.

  • Save app.py.
  • Return to your terminal and run the app:
streamlit run app.py
  • Enter a ticker like AAPL and click Predict. You should see the sample counts displayed, such as "Training samples: 940, Test samples: 236".

Seeing an error when you click Predict?

  • If you see an ImportError, make sure you updated the import line in app.py to include prepare_features and prepare_lstm_data.
  • If you see a KeyError for SMA_20 or EMA_20, check that prepare_features is called before prepare_lstm_data in your app.
  • Make sure your virtual environment is still active. You should see (stock-predictor-env) in your terminal prompt.

Help me debug my data preparation step

✔️ Awesome, I've got everything!

Great. Double check you've saved app.py with the updated import and new data preparation code.

ⓧ I'd like to double check the full code

import streamlit as st
import plotly.graph_objects as go
import pandas as pd
from datetime import timedelta
from model import fetch_data, prepare_features, prepare_lstm_data

st.set_page_config(page_title="Stock Price Predictor", layout="wide")
st.title("Stock Price Prediction Dashboard")
st.write("Enter any stock ticker to see historical data and a 30-day LSTM forecast.")

ticker = st.text_input("Stock Ticker", value="AAPL", max_chars=10)
predict_button = st.button("Predict")

if predict_button and ticker:
    with st.spinner("Fetching data..."):
        df = fetch_data(ticker.upper())

    if df.empty:
        st.error("Could not fetch data for that ticker. Please check the symbol and try again.")
    else:
        st.success(f"Fetched {len(df)} days of data for {ticker.upper()}")

        with st.spinner("Preparing features..."):
            df_features = prepare_features(df)

        with st.spinner("Preparing LSTM sequences..."):
            X_train, X_test, y_train, y_test, scaler = prepare_lstm_data(df_features)
            st.write(f"Training samples: {len(X_train)}, Test samples: {len(X_test)}")

Your data pipeline is complete. The raw stock prices are now enriched with trend indicators and packaged into sequences the LSTM can learn from. Next up, you'll build and train the actual neural network.

Build, Train, and Evaluate the LSTM Model

Your data pipeline is ready. You have historical prices flowing in, technical indicators computed, and 60-day sliding windows shaped for an LSTM. Now it's time to build the brain of your dashboard.

In this step, you'll define a neural network architecture that learns patterns from your prepared sequences, train it on the historical data, and measure how accurately it predicts stock prices using RMSE.

In this step, get ready to:
  • Build an LSTM neural network with two recurrent layers.
  • Evaluate the trained model's prediction accuracy using RMSE.
  • Display training progress and accuracy metrics in the Streamlit dashboard.
Write the model training function

An LSTM (Long Short-Term Memory) network is designed to learn from sequences of data. It remembers patterns across time steps, making it ideal for stock price history where yesterday's price influences today's.

  • In model.py, scroll to the bottom of the file (below the prepare_lstm_data function) and add the following function:
def build_and_train_model(X_train, y_train, epochs=20, batch_size=32):
    """Build and train the LSTM model."""
    model = Sequential([
        LSTM(50, return_sequences=True, input_shape=(X_train.shape[1], X_train.shape[2])),
        Dropout(0.2),
        LSTM(50, return_sequences=False),
        Dropout(0.2),
        Dense(1)
    ])

    model.compile(optimizer="adam", loss="mean_squared_error")
    model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0)

    return model

What does this code do?

This function defines a Sequential model with two LSTM layers stacked on top of each other. The first LSTM has return_sequences=True so it passes its full output sequence to the second LSTM layer. Without this, the second LSTM would receive a single value instead of a sequence and throw an error.

Each LSTM layer has 50 units (neurons) followed by a Dropout(0.2) layer that randomly disables 20% of connections during training. This prevents overfitting. The final Dense(1) layer outputs a single predicted price value.

The model is compiled with the Adam optimizer and mean squared error loss, then trained for 20 epochs in silent mode (verbose=0).

  • Save model.py.

Seeing a shape-related error?

If you see an error mentioning "expected ndim=3, found ndim=2", double check that the first LSTM layer has return_sequences=True. Without it, the first LSTM outputs a 2D tensor that the second LSTM cannot accept.

Also verify your input_shape matches (X_train.shape[1], X_train.shape[2]) exactly. This tells the model to expect 60 timesteps with 4 features each.

Help me debug my LSTM model shape error

Write the evaluation function

Training a model means nothing without measuring how well it performs. RMSE (Root Mean Squared Error) tells you, in dollars, how far off your predictions are from actual prices on average.

  • Below the build_and_train_model function in model.py, add the following evaluation function:
def evaluate_model(model, X_test, y_test, scaler):
    """Generate predictions and calculate RMSE."""
    predictions = model.predict(X_test, verbose=0)

    num_features = scaler.n_features_in_
    pred_full = np.zeros((len(predictions), num_features))
    pred_full[:, 0] = predictions.flatten()
    pred_prices = scaler.inverse_transform(pred_full)[:, 0]

    actual_full = np.zeros((len(y_test), num_features))
    actual_full[:, 0] = y_test
    actual_prices = scaler.inverse_transform(actual_full)[:, 0]

    rmse = np.sqrt(np.mean((pred_prices - actual_prices) ** 2))

    return pred_prices, actual_prices, rmse

What does this code do?

The model's predictions are in scaled form (values between 0 and 1) because we normalized the data earlier. This function reverses that scaling using scaler.inverse_transform() to convert predictions back to real dollar amounts.

The trick with pred_full and actual_full is that the scaler expects all 4 features to inverse-transform correctly. We pad zeros for the other features and extract only column 0 (the Close price) after transforming.

Finally, RMSE is calculated as the square root of the average squared differences between predicted and actual prices. A lower RMSE means better predictions.

  • Save model.py.

Getting a ValueError about array shapes?

Make sure predictions.flatten() is used when assigning to pred_full[:, 0]. The model outputs a 2D array with shape (samples, 1), and flatten() converts it to 1D so it fits into the column.

Help me fix my evaluate_model function

✔️ Awesome, I've got everything!

Great. Make sure you've saved model.py with both new functions.

ⓧ I'd like to double check the full code

import numpy as np
import pandas as pd
import yfinance as yf
from sklearn.preprocessing import MinMaxScaler
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout


def fetch_data(ticker, period="5y"):
    """Download historical stock data from Yahoo Finance."""
    data = yf.download(ticker, period=period, progress=False)
    df = data[["Close", "Volume"]].copy()
    df.reset_index(inplace=True)
    df.columns = ["Date", "Close", "Volume"]
    return df


def prepare_features(df):
    """Add technical indicators as features."""
    df = df.copy()
    df["SMA_20"] = df["Close"].rolling(window=20).mean()
    df["EMA_20"] = df["Close"].ewm(span=20, adjust=False).mean()
    df.dropna(inplace=True)
    df.reset_index(drop=True, inplace=True)
    return df


def prepare_lstm_data(df, lookback=60):
    """Scale features and create sequences for LSTM input."""
    feature_columns = ["Close", "Volume", "SMA_20", "EMA_20"]
    data = df[feature_columns].values

    scaler = MinMaxScaler(feature_range=(0, 1))
    scaled_data = scaler.fit_transform(data)

    X, y = [], []
    for i in range(lookback, len(scaled_data)):
        X.append(scaled_data[i - lookback:i])
        y.append(scaled_data[i, 0])

    X, y = np.array(X), np.array(y)

    split = int(len(X) * 0.8)
    X_train, X_test = X[:split], X[split:]
    y_train, y_test = y[:split], y[split:]

    return X_train, X_test, y_train, y_test, scaler


def build_and_train_model(X_train, y_train, epochs=20, batch_size=32):
    """Build and train the LSTM model."""
    model = Sequential([
        LSTM(50, return_sequences=True, input_shape=(X_train.shape[1], X_train.shape[2])),
        Dropout(0.2),
        LSTM(50, return_sequences=False),
        Dropout(0.2),
        Dense(1)
    ])

    model.compile(optimizer="adam", loss="mean_squared_error")
    model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0)

    return model


def evaluate_model(model, X_test, y_test, scaler):
    """Generate predictions and calculate RMSE."""
    predictions = model.predict(X_test, verbose=0)

    num_features = scaler.n_features_in_
    pred_full = np.zeros((len(predictions), num_features))
    pred_full[:, 0] = predictions.flatten()
    pred_prices = scaler.inverse_transform(pred_full)[:, 0]

    actual_full = np.zeros((len(y_test), num_features))
    actual_full[:, 0] = y_test
    actual_prices = scaler.inverse_transform(actual_full)[:, 0]

    rmse = np.sqrt(np.mean((pred_prices - actual_prices) ** 2))

    return pred_prices, actual_prices, rmse
Display training progress and metrics in the app

Now you'll connect these new functions to your Streamlit app so learners can watch the model train and see how accurate it is.

  • Switch back to app.py in VS Code.
  • Find this import line at the top of the file:
from model import fetch_data, prepare_features, prepare_lstm_data
  • Replace it with this updated import that includes the two new functions:
from model import fetch_data, prepare_features, prepare_lstm_data, build_and_train_model, evaluate_model
  • Scroll to the bottom of app.py (below the line that displays training/test sample counts) and add the following code:
        with st.spinner("Training LSTM model (this may take a minute)..."):
            model = build_and_train_model(X_train, y_train)

        with st.spinner("Evaluating model..."):
            pred_prices, actual_prices, rmse = evaluate_model(model, X_test, y_test, scaler)

        col1, col2 = st.columns(2)
        col1.metric("RMSE", f"${rmse:.2f}")
        col2.metric("Test Samples", len(X_test))

What does this code do?

The st.spinner() blocks show a loading animation while the model trains and evaluates. Training 20 epochs on ~940 samples typically takes 30-60 seconds depending on your machine.

The st.columns(2) creates a two-column layout. The RMSE metric shows how many dollars off the predictions are on average. The test samples count shows how much data was used for evaluation.

  • Save app.py.
  • Return to your terminal and run the app:
streamlit run app.py
  • Enter a ticker like AAPL and click Predict. After training completes, you should see an RMSE value displayed as a dollar amount (typically between $2 and $15 depending on the stock).

Model taking too long or app crashing?

Training can take 30-60 seconds. If it takes much longer, make sure your virtual environment is active (you should see (stock-predictor-env) in your terminal prompt).

If the app crashes with an import error, confirm that build_and_train_model and evaluate_model are both listed in your import line in app.py. A missing function name will cause an ImportError.

Help me fix my Streamlit stock predictor app

✔️ Awesome, I've got everything!

Great. Make sure you've saved app.py with the updated import and the new training/evaluation code.

ⓧ I'd like to double check the full code

import streamlit as st
import plotly.graph_objects as go
import pandas as pd
from datetime import timedelta
from model import fetch_data, prepare_features, prepare_lstm_data, build_and_train_model, evaluate_model

st.set_page_config(page_title="Stock Price Predictor", layout="wide")
st.title("Stock Price Prediction Dashboard")
st.write("Enter any stock ticker to see historical data and a 30-day LSTM forecast.")

ticker = st.text_input("Stock Ticker", value="AAPL", max_chars=10)
predict_button = st.button("Predict")

if predict_button and ticker:
    with st.spinner("Fetching data..."):
        df = fetch_data(ticker.upper())

    if df.empty:
        st.error("Could not fetch data for that ticker. Please check the symbol and try again.")
    else:
        st.success(f"Fetched {len(df)} days of data for {ticker.upper()}")

        with st.spinner("Preparing features..."):
            df_features = prepare_features(df)

        with st.spinner("Preparing LSTM sequences..."):
            X_train, X_test, y_train, y_test, scaler = prepare_lstm_data(df_features)
            st.write(f"Training samples: {len(X_train)}, Test samples: {len(X_test)}")

        with st.spinner("Training LSTM model (this may take a minute)..."):
            model = build_and_train_model(X_train, y_train)

        with st.spinner("Evaluating model..."):
            pred_prices, actual_prices, rmse = evaluate_model(model, X_test, y_test, scaler)

        col1, col2 = st.columns(2)
        col1.metric("RMSE", f"${rmse:.2f}")
        col2.metric("Test Samples", len(X_test))

Your LSTM model is trained and evaluated. Next up, you'll use it to generate 30-day future price predictions and visualize them in an interactive chart alongside the historical data.

Generate Future Predictions and Visualize

Your model can now evaluate how well it fits historical data. The real goal of a forecasting model, though, is to predict what happens next. In this step, you will generate a 30-day future price forecast and display everything in one interactive chart.

The technique you will use is called autoregressive prediction. The model predicts day 1, feeds that prediction back as input, then predicts day 2, and repeats. This is how real-world sequence models generate multi-step forecasts.

In this step, get ready to:
  • Write an autoregressive prediction function that forecasts 30 days of future prices.
  • Build an interactive Plotly chart with historical prices in blue and predictions in red.
  • Add a disclaimer caption reminding users this is an educational tool.
Write the autoregressive prediction function

Autoregressive prediction works by feeding each prediction back into the model as input for the next step. The model uses a 60-day window, so after predicting day 1, it drops the oldest day, appends the new prediction, and uses this updated window to predict day 2.

  • In model.py, add the following function at the bottom of the file, after evaluate_model():
def predict_future(model, last_sequence, scaler, days=30):
    """Predict future prices using autoregressive generation."""
    predictions = []
    current_sequence = last_sequence.copy()

    for _ in range(days):
        pred = model.predict(current_sequence.reshape(1, current_sequence.shape[0], current_sequence.shape[1]), verbose=0)
        pred_value = pred[0, 0]
        predictions.append(pred_value)

        new_row = current_sequence[-1].copy()
        new_row[0] = pred_value
        current_sequence = np.vstack([current_sequence[1:], new_row])

    num_features = scaler.n_features_in_
    pred_full = np.zeros((len(predictions), num_features))
    pred_full[:, 0] = np.array(predictions)
    future_prices = scaler.inverse_transform(pred_full)[:, 0]

    return future_prices

What does this code do?

  • current_sequence holds the most recent 60 days of scaled feature data. Each iteration predicts one future day.
  • After each prediction, the function drops the oldest row and appends the new prediction as the latest row. This sliding window moves forward one day at a time.
  • Once all 30 predictions are collected, inverse_transform converts the scaled values back to real dollar prices.

Why do errors compound in autoregressive forecasting?

Each prediction becomes input for the next one. If day 1's prediction is slightly off, that error feeds into day 2, which feeds into day 3, and so on. This means predictions further out become less reliable. 30 days is a reasonable horizon to see meaningful trends without the predictions becoming noise.

  • Save model.py.

Seeing a NameError for np or scaler?

Make sure the function is inside model.py where numpy is already imported as np at the top of the file.

Double check that the function signature matches exactly, including the scaler parameter.

Help me debug my predict_future function

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before continuing.

ⓧ I'd like to double check the full code

import numpy as np
import pandas as pd
import yfinance as yf
from sklearn.preprocessing import MinMaxScaler
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout


def fetch_data(ticker, period="5y"):
    """Download historical stock data from Yahoo Finance."""
    data = yf.download(ticker, period=period, progress=False)
    df = data[["Close", "Volume"]].copy()
    df.reset_index(inplace=True)
    df.columns = ["Date", "Close", "Volume"]
    return df


def prepare_features(df):
    """Add technical indicators as features."""
    df = df.copy()
    df["SMA_20"] = df["Close"].rolling(window=20).mean()
    df["EMA_20"] = df["Close"].ewm(span=20, adjust=False).mean()
    df.dropna(inplace=True)
    df.reset_index(drop=True, inplace=True)
    return df


def prepare_lstm_data(df, lookback=60):
    """Scale features and create sequences for LSTM input."""
    feature_columns = ["Close", "Volume", "SMA_20", "EMA_20"]
    data = df[feature_columns].values

    scaler = MinMaxScaler(feature_range=(0, 1))
    scaled_data = scaler.fit_transform(data)

    X, y = [], []
    for i in range(lookback, len(scaled_data)):
        X.append(scaled_data[i - lookback:i])
        y.append(scaled_data[i, 0])

    X, y = np.array(X), np.array(y)

    split = int(len(X) * 0.8)
    X_train, X_test = X[:split], X[split:]
    y_train, y_test = y[:split], y[split:]

    return X_train, X_test, y_train, y_test, scaler


def build_and_train_model(X_train, y_train, epochs=20, batch_size=32):
    """Build and train the LSTM model."""
    model = Sequential([
        LSTM(50, return_sequences=True, input_shape=(X_train.shape[1], X_train.shape[2])),
        Dropout(0.2),
        LSTM(50, return_sequences=False),
        Dropout(0.2),
        Dense(1)
    ])

    model.compile(optimizer="adam", loss="mean_squared_error")
    model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0)

    return model


def evaluate_model(model, X_test, y_test, scaler):
    """Generate predictions and calculate RMSE."""
    predictions = model.predict(X_test, verbose=0)

    num_features = scaler.n_features_in_
    pred_full = np.zeros((len(predictions), num_features))
    pred_full[:, 0] = predictions.flatten()
    pred_prices = scaler.inverse_transform(pred_full)[:, 0]

    actual_full = np.zeros((len(y_test), num_features))
    actual_full[:, 0] = y_test
    actual_prices = scaler.inverse_transform(actual_full)[:, 0]

    rmse = np.sqrt(np.mean((pred_prices - actual_prices) ** 2))

    return pred_prices, actual_prices, rmse


def predict_future(model, last_sequence, scaler, days=30):
    """Predict future prices using autoregressive generation."""
    predictions = []
    current_sequence = last_sequence.copy()

    for _ in range(days):
        pred = model.predict(current_sequence.reshape(1, current_sequence.shape[0], current_sequence.shape[1]), verbose=0)
        pred_value = pred[0, 0]
        predictions.append(pred_value)

        new_row = current_sequence[-1].copy()
        new_row[0] = pred_value
        current_sequence = np.vstack([current_sequence[1:], new_row])

    num_features = scaler.n_features_in_
    pred_full = np.zeros((len(predictions), num_features))
    pred_full[:, 0] = np.array(predictions)
    future_prices = scaler.inverse_transform(pred_full)[:, 0]

    return future_prices
Build the interactive Plotly chart

Now you will wire up the prediction function in your Streamlit app and build an interactive chart that shows historical prices alongside the 30-day forecast.

  • In app.py, find the import line at the top:
from model import fetch_data, prepare_features, prepare_lstm_data, build_and_train_model, evaluate_model
  • Add predict_future to the end of the import list, so the line now looks like this:
from model import fetch_data, prepare_features, prepare_lstm_data, build_and_train_model, evaluate_model, predict_future
  • Next, find the two metric lines at the bottom of your app.py file:
        col1, col2 = st.columns(2)
        col1.metric("RMSE", f"${rmse:.2f}")
        col2.metric("Test Samples", len(X_test))
  • Add the forecast generation code directly below those metric lines by copying this code:
        with st.spinner("Generating 30-day forecast..."):
            feature_columns = ["Close", "Volume", "SMA_20", "EMA_20"]
            scaled_all = scaler.transform(df_features[feature_columns].values)
            last_sequence = scaled_all[-60:]
            future_prices = predict_future(model, last_sequence, scaler, days=30)

What does this code do?

  • feature_columns lists the same four columns used during training. The scaler expects data in the same shape.
  • scaler.transform() scales the full dataset using the same min/max values from training. You use transform here (not fit_transform) because the scaler was already fitted during prepare_lstm_data().
  • last_sequence = scaled_all[-60:] grabs the most recent 60 days of data. This is the starting window the model uses to predict day 1.
  • Directly below the forecast spinner block, add the chart-building code:
        last_date = pd.to_datetime(df_features["Date"].iloc[-1])
        future_dates = [last_date + timedelta(days=i + 1) for i in range(30)]

        fig = go.Figure()
        fig.add_trace(go.Scatter(
            x=df_features["Date"],
            y=df_features["Close"],
            mode="lines",
            name="Historical Price",
            line=dict(color="blue")
        ))
        fig.add_trace(go.Scatter(
            x=future_dates,
            y=future_prices,
            mode="lines",
            name="Predicted Price (30 days)",
            line=dict(color="red", dash="dash")
        ))
        fig.update_layout(
            title=f"{ticker.upper()} - Historical vs Predicted Prices",
            xaxis_title="Date",
            yaxis_title="Price (USD)",
            hovermode="x unified",
            template="plotly_white"
        )
        st.plotly_chart(fig, use_container_width=True)

What does this chart code do?

  • future_dates generates 30 calendar dates starting the day after the last historical date.
  • The first go.Scatter trace plots all historical closing prices in a solid blue line.
  • The second trace plots the 30 predicted prices as a red dashed line, visually distinguishing them from real data.
  • hovermode="x unified" means hovering over any point shows a tooltip with all trace values at that date. Plotly makes the chart zoomable and pannable by default.
  • Save app.py.

Seeing an IndentationError?

All of this new code must be indented to the same level as the col1, col2 = st.columns(2) line above it. That means two levels of indentation (8 spaces) because it lives inside both the if predict_button and ticker: block and the else: block.

Help me fix indentation in my app.py

Add a disclaimer below the chart

Stock prediction models are educational tools, not crystal balls. Adding a clear disclaimer protects users from making financial decisions based on model output.

  • In app.py, add this line directly below st.plotly_chart(fig, use_container_width=True):
        st.caption("This is an educational tool, not financial advice. Model accuracy is limited by market unpredictability.")

What does st.caption do?

st.caption() renders small, muted text below the chart. It is the standard Streamlit widget for footnotes and disclaimers.

  • Save app.py.
  • Return to your terminal and run the app by running:
streamlit run app.py
  • Type a ticker (like AAPL) and click Predict.
  • Wait for the model to train. You should see an interactive chart with historical prices in a solid blue line and predicted future prices in a red dashed line extending 30 days beyond the last historical date.
  • Below the chart, you should see the disclaimer text in small, muted font.

Chart not showing or app crashes?

  • If you see an ImportError for predict_future, make sure you added it to the import line at the top of app.py and saved both files.
  • If the chart shows but the red line is flat or missing, check that your predict_future function in model.py matches the code above exactly. A misplaced bracket or wrong index will silently produce bad predictions.
  • If you see a ValueError about array shapes, make sure feature_columns in app.py lists the same four columns used in prepare_lstm_data().

Help me debug my stock prediction chart

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the complete app.py at this point:

import streamlit as st
import plotly.graph_objects as go
import pandas as pd
from datetime import timedelta
from model import fetch_data, prepare_features, prepare_lstm_data, build_and_train_model, evaluate_model, predict_future

st.set_page_config(page_title="Stock Price Predictor", layout="wide")
st.title("Stock Price Prediction Dashboard")
st.write("Enter any stock ticker to see historical data and a 30-day LSTM forecast.")

ticker = st.text_input("Stock Ticker", value="AAPL", max_chars=10)
predict_button = st.button("Predict")

if predict_button and ticker:
    with st.spinner("Fetching data..."):
        df = fetch_data(ticker.upper())

    if df.empty:
        st.error("Could not fetch data for that ticker. Please check the symbol and try again.")
    else:
        st.success(f"Fetched {len(df)} days of data for {ticker.upper()}")

        with st.spinner("Preparing features..."):
            df_features = prepare_features(df)

        with st.spinner("Preparing LSTM sequences..."):
            X_train, X_test, y_train, y_test, scaler = prepare_lstm_data(df_features)
            st.write(f"Training samples: {len(X_train)}, Test samples: {len(X_test)}")

        with st.spinner("Training LSTM model (this may take a minute)..."):
            model = build_and_train_model(X_train, y_train)

        with st.spinner("Evaluating model..."):
            pred_prices, actual_prices, rmse = evaluate_model(model, X_test, y_test, scaler)

        col1, col2 = st.columns(2)
        col1.metric("RMSE", f"${rmse:.2f}")
        col2.metric("Test Samples", len(X_test))

        with st.spinner("Generating 30-day forecast..."):
            feature_columns = ["Close", "Volume", "SMA_20", "EMA_20"]
            scaled_all = scaler.transform(df_features[feature_columns].values)
            last_sequence = scaled_all[-60:]
            future_prices = predict_future(model, last_sequence, scaler, days=30)

        last_date = pd.to_datetime(df_features["Date"].iloc[-1])
        future_dates = [last_date + timedelta(days=i + 1) for i in range(30)]

        fig = go.Figure()
        fig.add_trace(go.Scatter(
            x=df_features["Date"],
            y=df_features["Close"],
            mode="lines",
            name="Historical Price",
            line=dict(color="blue")
        ))
        fig.add_trace(go.Scatter(
            x=future_dates,
            y=future_prices,
            mode="lines",
            name="Predicted Price (30 days)",
            line=dict(color="red", dash="dash")
        ))
        fig.update_layout(
            title=f"{ticker.upper()} - Historical vs Predicted Prices",
            xaxis_title="Date",
            yaxis_title="Price (USD)",
            hovermode="x unified",
            template="plotly_white"
        )
        st.plotly_chart(fig, use_container_width=True)

        st.caption("This is an educational tool, not financial advice. Model accuracy is limited by market unpredictability.")

Your stock prediction dashboard is fully functional. You have a complete ML pipeline from raw Yahoo Finance data to a trained LSTM model to an interactive visualization. Ready for a challenge? The Secret Mission will have you add RSI as an additional feature and see if it improves your model's accuracy.

Secret mission

Add RSI as a Feature and Display It

Your LSTM model currently uses two technical indicators (SMA and EMA) to predict stock prices. Can you add a third indicator, RSI (Relative Strength Index), to give the model more information about momentum? You'll also build a secondary chart panel to visualize RSI alongside your price predictions.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely locally, so there are no ongoing costs. The only resources are your local files, the virtual environment, and the Python 3.13 installation.

Resources you used:

  • The stock-predictor-env virtual environment (contains all pip packages).
  • Project files: model.py and app.py.
  • Python 3.13 installation.
  • The Streamlit local server process (if still running).

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment with different tickers and model configurations.

  • Your virtual environment stays ready. Reactivate it any time by running stock-predictor-env\Scripts\activate from the project folder.
  • If you closed the Streamlit server, restart it with streamlit run app.py.
  • Python 3.13 remains installed on your system alongside your existing Python 3.14.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files and virtual environment stay on disk for when you return.

  • Stop the Streamlit server by pressing Ctrl+C in the terminal where it is running.
  • Deactivate the virtual environment by running:
deactivate
  • When you come back, reactivate the environment and run the app again.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your trained model code and virtual environment permanently.

  • Stop the Streamlit server by pressing Ctrl+C in the terminal where it is running.
  • Deactivate the virtual environment by running:
deactivate
  • Delete the virtual environment and project files. On Windows, run:
rmdir /s /q stock-predictor-env
del model.py
del app.py
  • On macOS or Linux, run:
rm -rf stock-predictor-env
rm model.py
rm app.py
  • To uninstall Python 3.13 on Windows, open Settings.
  • Select Apps, then Installed apps.
  • Search for Python 3.13, click the three-dot menu, and select Uninstall.

Nice Work!

Nice Work!

You just built a complete machine learning pipeline from scratch and deployed it as an interactive web application. From raw stock market data to a trained LSTM neural network generating 30-day forecasts, this is a production-style workflow you can apply to any time-series problem.

You've learned how to:

  • Build a complete ML pipeline from raw Yahoo Finance data to a trained LSTM neural network that captures temporal dependencies in stock prices.
  • Implement autoregressive multi-step forecasting to predict 30 days of future prices, where each prediction feeds into the next.
  • Deploy your model in an interactive Streamlit dashboard with a Plotly chart showing historical prices in blue and predicted prices in red.
  • Secret Mission: Added RSI as an additional feature to improve model accuracy and visualized it in a secondary chart panel with overbought/oversold threshold lines.

Ready to quiz yourself?