Forecast Stock Prices with LSTM in Python: Complete Project Guide from Data Prep to Deployment

If you’ve ever watched a news anchor brag about “the next big thing” in the market and thought, “I could have seen that coming,” you’re not alone. Predicting stock prices feels like trying to read the future, but with the right tools—like an LSTM network—you can turn a hunch into a data‑driven guess that actually makes sense.

Why LSTM?

Long Short‑Term Memory (LSTM) networks belong to the family of recurrent neural networks (RNNs). In plain English, they are designed to remember patterns over time. Stock prices are a classic time‑series problem: today’s price depends on yesterday’s, last week’s, maybe even last year’s trends. Traditional feed‑forward models forget that order, but LSTMs keep a “memory” of past values, making them a natural fit for financial forecasting.

The Project Roadmap

Below is the step‑by‑step flow we’ll follow:

  1. Gather historical price data
  2. Clean and scale the data
  3. Create sequences for the LSTM
  4. Build and train the model
  5. Evaluate performance
  6. Deploy as a simple Flask API

Let’s dive in.

1. Data Collection

For a quick demo I used Yahoo Finance’s free API via the yfinance Python package. It’s a handy way to pull daily OHLCV (Open, High, Low, Close, Volume) data without signing up for a paid service.

import yfinance as yf
import pandas as pd

ticker = "AAPL"
df = yf.download(ticker, start="2015-01-01", end="2024-01-01")
df = df[['Close']]          # we only need closing price for now
df.head()

The resulting DataFrame has a Date index and a single column called Close. Feel free to add other columns later; they can improve the model, but they also add complexity.

2. Data Preparation

2.1 Handling Missing Values

Financial data sometimes has gaps (holidays, market closures). A simple forward‑fill works well:

df.ffill(inplace=True)

2.2 Scaling

Neural networks love numbers that sit roughly between 0 and 1. We’ll use MinMaxScaler from scikit‑learn.

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
scaled = scaler.fit_transform(df)

2.3 Train‑Test Split

We keep the most recent 20% of data for testing, mimicking a real‑world scenario where the model predicts future values.

train_size = int(len(scaled) * 0.8)
train, test = scaled[:train_size], scaled[train_size:]

3. Creating Sequences

LSTMs need input shaped as samples × timesteps × features. We’ll use a sliding window of 60 days (about three months) to predict the next day’s price.

import numpy as np

def create_sequences(data, window=60):
    X, y = [], []
    for i in range(len(data) - window):
        X.append(data[i:i+window])
        y.append(data[i+window])
    return np.array(X), np.array(y)

X_train, y_train = create_sequences(train)
X_test,  y_test  = create_sequences(test)

Now X_train has shape (num_samples, 60, 1)—the extra dimension is for the single feature (Close price).

4. Building the LSTM Model

I like to keep the architecture simple: one LSTM layer followed by a dense output layer.

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout

model = Sequential([
    LSTM(50, return_sequences=False, input_shape=(X_train.shape[1], 1)),
    Dropout(0.2),
    Dense(1)
])

model.compile(optimizer='adam', loss='mean_squared_error')
model.summary()

The return_sequences=False flag tells the LSTM to output only the last hidden state, which is enough for a single‑step forecast. The dropout layer helps prevent over‑fitting—think of it as a gentle reminder to the model not to memorize the training data.

5. Training the Model

Training for 30 epochs usually gives a decent result for this toy example. Adjust the number based on your hardware and patience.

history = model.fit(
    X_train, y_train,
    epochs=30,
    batch_size=32,
    validation_split=0.1,
    verbose=1
)

You’ll see the loss decreasing on both training and validation sets. If validation loss starts rising while training loss keeps falling, you’re over‑fitting—time to add more dropout or reduce epochs.

6. Evaluation

First, we predict on the test set and then invert the scaling to get actual price values.

pred_scaled = model.predict(X_test)
pred = scaler.inverse_transform(pred_scaled)
actual = scaler.inverse_transform(y_test)

A quick visual check:

import matplotlib.pyplot as plt

plt.figure(figsize=(12,5))
plt.plot(actual, label='Actual')
plt.plot(pred,   label='Predicted')
plt.title('AAPL Close Price: Actual vs Predicted')
plt.legend()
plt.show()

You’ll notice the lines are close but not perfect—stock markets are noisy! A common metric is Root Mean Squared Error (RMSE). Lower is better.

from sklearn.metrics import mean_squared_error
import math

rmse = math.sqrt(mean_squared_error(actual, pred))
print(f'RMSE: {rmse:.2f}')

If the RMSE is within a few dollars for a high‑priced stock, you’ve done a respectable job for a first pass.

7. Deploying the Model

7.1 Saving the Model

model.save('lstm_stock_model.h5')
import joblib
joblib.dump(scaler, 'scaler.save')

7.2 Simple Flask API

Below is a minimal Flask app that loads the model and returns a one‑day‑ahead forecast for a given ticker. In practice you’d add error handling, logging, and maybe a caching layer.

from flask import Flask, request, jsonify
import tensorflow as tf
import numpy as np
import joblib
import yfinance as yf

app = Flask(__name__)

model = tf.keras.models.load_model('lstm_stock_model.h5')
scaler = joblib.load('scaler.save')
WINDOW = 60

def get_recent_prices(ticker):
    df = yf.download(ticker, period='90d')[['Close']]
    df.ffill(inplace=True)
    return scaler.transform(df)

@app.route('/predict', methods=['GET'])
def predict():
    ticker = request.args.get('ticker', default='AAPL')
    data = get_recent_prices(ticker)
    recent_seq = data[-WINDOW:].reshape(1, WINDOW, 1)
    pred_scaled = model.predict(recent_seq)
    pred_price = scaler.inverse_transform(pred_scaled)[0][0]
    return jsonify({'ticker': ticker, 'predicted_close': round(pred_price, 2)})

if __name__ == '__main__':
    app.run(debug=True)

Run the script, hit http://127.0.0.1:5000/predict?ticker=MSFT, and you’ll get a JSON response with tomorrow’s predicted closing price. It’s a fun way to turn a notebook experiment into a tiny web service.

8. Things to Keep in Mind

  • Data leakage: Never let future data sneak into the training set. The sliding window approach helps avoid this.
  • Feature engineering: Adding technical indicators (moving averages, RSI) can boost performance, but they also increase the risk of over‑fitting.
  • Model stability: Stock markets are influenced by news, earnings, and macro events—none of which are captured in price history alone. Treat the model as a guide, not a crystal ball.
  • Regulation: If you plan to use predictions for real trading, check the legal requirements in your jurisdiction.

That’s it—a full walk‑through from raw price data to a deployable LSTM service. I hope this project gives you a solid foundation to experiment further, perhaps by trying bidirectional LSTMs, attention mechanisms, or even transformer models. The world of finance is noisy, but with clear code and a disciplined workflow, you can turn that noise into insight.

Reactions