LSTM Networks for Financial Time Series

LSTM networks are the deep-learning model traders most want to work, and the one most likely to humble them. A long short-term memory network is a recurrent neural network designed to carry information across long sequences — which sounds tailor-made for price history. In practice, applying LSTMs to raw prices is a minefield of leakage, non-stationarity, and self-deception, and the resulting model rarely beats a one-line baseline. This guide explains the intuition, the traps, and how to test honestly. For the bigger picture see neural networks and deep learning for trading.

RNN and LSTM intuition

A plain recurrent network processes a sequence one step at a time, maintaining a hidden state ht updated from the previous state and the current input: `ht = f(Wh h{t-1}

  • Wx xt)`. In principle this lets it "remember" earlier inputs. In practice, vanilla

RNNs suffer from vanishing and exploding gradients and forget anything more than a few steps back.

The LSTM fixes this with a cell state and three gates — input, forget, and output — that control what information is written, retained, and read at each step. The forget gate, in particular, lets the network learn to keep relevant context over long spans without the gradient collapsing. This is genuinely useful for language and speech. The open question for trading is whether price history actually contains the kind of long-range, learnable structure that justifies the machinery.

Windowing price data into sequences

To feed an LSTM you slice a time series into overlapping windows: each training example is a window of lookback steps of features, and the label is something about the next step. A common mistake starts right here — windowing raw price levels.

import numpy as np

def make_sequences(features, target, lookback=30):
    X, y = [], []
    for t in range(lookback, len(features)):
        X.append(features[t - lookback:t])   # past window, exclusive of t
        y.append(target[t])                  # label aligned to time t
    return np.array(X), np.array(y)

# features should be STATIONARY: returns / scaled indicators, NOT raw price levels
returns = np.diff(np.log(price))
X, y = make_sequences(returns_scaled, label, lookback=30)

Note the comment: feed returns or scaled, stationary features, never raw price levels. An LSTM trained on price levels will appear stunningly accurate because it learns to predict "tomorrow's price ≈ today's price." Plot the predictions and you see the classic lagged curve — it is just echoing the last value, contributing zero tradable edge. See stationarity in time series for why this matters.

The leakage traps

LSTM pipelines leak in subtle ways that inflate backtests:

  • Scaling before splitting. If you fit a StandardScaler (or min-max) on the whole

series and then split, the scaler has seen the future. Fit scalers on train only and apply to validation/test.

  • Shuffled windows. Overlapping windows share rows. Shuffle them into random

train/test folds and adjacent windows leak almost identical information across the boundary.

  • Label alignment. Off-by-one indexing that includes time t in the window used to

predict t is direct lookahead.

  • Global normalization stats. Any statistic (mean, vol, quantiles) computed over the

full sample and used as a feature leaks.

TrapSymptomFix
Raw price levels as inputEerily high R², lagged prediction curveUse returns / stationary features
Scaler fit on full seriesBacktest beats live by a wide marginFit scaler on train only
Shuffled overlapping windowsValidation loss too goodTime-ordered split + embargo
Off-by-one labelNear-perfect direction accuracyAudit window/label indexing

Sequence-aware train/validation split

You must split by time, not at random, and leave an embargo gap so overlapping windows do not straddle the boundary.

n = len(X)
train_end = int(n * 0.7)
val_end = int(n * 0.85)
embargo = 30  # >= lookback so windows don't overlap across the cut

X_train, y_train = X[:train_end], y[:train_end]
X_val,   y_val   = X[train_end + embargo:val_end], y[train_end + embargo:val_end]
X_test,  y_test  = X[val_end + embargo:], y[val_end + embargo:]

A minimal Keras LSTM on top of this, kept small and regularized because the data is weak:

from tensorflow import keras
from tensorflow.keras import layers

model = keras.Sequential([
    layers.Input(shape=(30, n_features)),
    layers.LSTM(16, dropout=0.2, recurrent_dropout=0.0),
    layers.Dense(1, activation="sigmoid"),
])
model.compile(optimizer=keras.optimizers.Adam(1e-3), loss="binary_crossentropy")

early = keras.callbacks.EarlyStopping(
    monitor="val_loss", patience=8, restore_best_weights=True)
model.fit(X_train, y_train, validation_data=(X_val, y_val),
          epochs=100, batch_size=128, callbacks=[early], verbose=0)

Always benchmark against a dumb baseline

This is the step that separates honest research from wishful thinking. Before believing your LSTM, compare it to trivial baselines on the same out-of-sample window:

  • Persistence — predict the sign of the last return.
  • Logistic regression on the same flattened features.
  • A gradient-boosted tree on engineered features.
from sklearn.metrics import accuracy_score
import numpy as np

baseline = np.sign(returns_scaled[val_end + embargo - 1:-1])  # last-return sign
print("baseline acc:", accuracy_score(y_test, (baseline > 0).astype(int)))
print("lstm acc:", accuracy_score(y_test, (model.predict(X_test).ravel() > 0.5)))

The uncomfortable, well-replicated finding is that on raw or lightly-processed price data, LSTMs rarely beat these baselines out of sample after costs. The cases where they add value tend to involve rich multivariate inputs (order book, cross-asset panels) and large datasets — not a single instrument's daily closes.

Why LSTMs underperform on prices

  • No long-range structure to learn. Efficient-ish markets leave little exploitable

autocorrelation in returns; the LSTM's memory advantage targets structure that mostly is not there.

  • Non-stationarity. The relationship drifts, so a sequence model fit on old regimes

generalizes poorly.

  • Tiny effective sample. Overlapping windows are highly correlated, so thousands of

windows carry far less independent information than the count implies.

  • High variance. Results swing with random seed and initialization; a single good

run is often luck.

Common mistakes

  • Feeding raw price levels, producing a lagged "predictor" with no edge.
  • Fitting scalers or normalization stats on the full series before splitting.
  • Shuffling overlapping windows into random folds, leaking across the boundary.
  • Skipping the baseline comparison and celebrating accuracy that a one-liner beats.
  • Oversized networks and too many epochs with no early stopping.
  • Reporting a single seed instead of an ensemble or distribution over seeds.
  • Evaluating on accuracy/MSE rather than net, after-cost trading PnL.

Key takeaways

  • LSTMs carry long-range context via gated cell state, useful for language but often

mismatched to weak, non-stationary price data.

  • Window stationary features (returns, scaled indicators), never raw price levels.
  • Split strictly by time with an embargo, and fit all scalers on the training set only.
  • Always benchmark against persistence, logistic regression, and a

tree; LSTMs rarely win on raw prices.

multivariate inputs are where sequence models have a chance.

  • Treat LSTM results with the same skepticism as any model — judge net PnL, ensemble

seeds, and read machine learning for trading for the broader workflow.

#LSTM #recurrent neural network #time series #deep learning #forecasting