Skip to main content

LSTM-CNN Trading Backtest

A research exercise in time-series features, validation design, and the limits of a short backtest.


Can a recurrent-convolutional model beat buy and hold?

I built this experiment to test whether an LSTM-CNN could turn price and indicator histories into useful long and short signals. The architecture takes an idea from wavelet analysis. Recurrent layers process changes across time, while convolutional layers look for local patterns at different scales.

Backtest Result

On a random sample of S&P 1500 tickers, using the most recent 150 days as validation data, the model's trading rule returned about 0.05% per day more than the underlying asset's buy-and-hold return. Compounded, that daily difference would come to roughly 20% over a year. With a short validation window, the sampling design, and no transaction costs, the figure illustrates a backtest and says little about future performance.

Source Code

The source code is on GitHub.

Features for Each Stock Ticker

  • Open, Close, High, Low, EMA(12), EMA(26), MA(6), DI, MACD, and PSY(13)

These features are calculated on first-differenced data: X~t=ΔXt=Xt−Xt−1\widetilde{X}_t = \Delta X_t = X_t - X_{t-1}. I difference the data because financial instruments often trend over short periods. An augmented Dickey-Fuller test can check whether differencing produces a stationary series. The model predicts price direction, not price level, so changes are also a more relevant input than raw levels.

The feature set combines price, trend, momentum, and range measures so the model does not have to infer each of those summaries from raw closing prices alone.


Theoretical Motivation

Time-series analysis often stays in the time domain, but the frequency domain offers another way to describe recurring behavior. For a zero-mean stationary time series XtX_t, the spectral representation is

Xt=∫(−π,π]eitλdZ(λ),−π<λ≤πX_t = \int_{(-\pi, \pi]} e^{it\lambda}dZ(\lambda), \enspace -\pi < \lambda \leq \pi

Here Z(λ)Z(\lambda) is a complex-valued process with uncorrelated increments. This representation moves a stationary series into the frequency domain without losing information.

This is useful when a process contains periodic behavior. Suppose XtX_t is a stochastic periodic series:

Xt=Acos(ωt)+Bsin(ωt);A,B∼N(μA,B,σA,B)X_t = A cos(\omega t) + B sin(\omega t);\enspace A, B \sim N(\mu_{A, B}, \sigma_{A, B})

The angular frequency ω\omega determines how often the process repeats. A frequency-domain representation exposes that component as a peak rather than requiring us to infer the cycle from the raw timeline. Now add a second component with twice the frequency:

Yt=Ccos(2ωt)+Dsin(2ωt);C,D∼N(μC,D,σC,D)Y_t = C cos(2\omega t) + D sin(2\omega t);\enspace C, D \sim N(\mu_{C, D}, \sigma_{C, D}) Wt=Xt+YtW_t = X_t + Y_t

The frequency representation for WtW_t contains peaks at ω\omega and 2ω2\omega. More generally, it separates periodic components that overlap in the time domain. We can add a linear trend with the term ctct:

W~t=Xt+Yt+ct,c∈R+\widetilde{W}_t = X_t + Y_t + ct,\enspace c \in \R^+

Under the difference operator Δ\Delta

ΔW~t=W~t−W~t−1=Xt+Yt+ct−Xt−1−Yt−1−c(t−1)=ΔXt+ΔYt+c\begin{aligned} \Delta \widetilde{W}_t &= \widetilde{W}_t - \widetilde{W}_{t-1} \\ &= X_t + Y_t + ct - X_{t-1} - Y_{t-1} - c(t - 1) \\ &= \Delta X_t + \Delta Y_t + c \end{aligned}

Without loss of generality we have

ΔXt=Xt−Xt−1=Atcos(πt)+Btsin(πt)−At−1cos(π(t−1))−Bt−1sin(π(t−1))\begin{aligned} \Delta X_t &= X_{t} - X_{t-1} \\ &= A_t cos(\pi t) + B_t sin(\pi t) - A_{t-1} cos(\pi (t - 1)) - B_{t-1} sin(\pi (t - 1)) \\ \end{aligned}

The trend disappears after differencing, leaving changes in the periodic components and the constant drift cc.

When frequencies remain fixed, a Fourier transform can expose them. Financial series rarely maintain a stable period across the full sample, which motivates a localized method such as a wavelet transform.

Wavelets

A wavelet lets us analyze localized frequency behavior. We start with two functions: mother wavelet ψ(t)\psi(t) (the wavelet function) and a father wavelet ϕ(t)\phi(t) (the scaling function).

The scaling function is associated with low-pass filtering, while the wavelet function captures higher-frequency changes. A moving average is a familiar low-pass operation. First differencing, Xt−Xt−1X_t - X_{t-1}, emphasizes faster changes. Used together, these filters separate behavior at different scales without assuming a fixed period across the full series.

A CNN learns local filters over short windows, while an LSTM carries information across longer sequences. Those learned filters are not guaranteed to be low-pass or high-pass filters, and the network is not a formal wavelet transform. The wavelet connection is a design intuition: the two paths can learn complementary summaries at different time scales.

Theoretical Implications

For a simplified upward-trending price series, denote profit at time tt by PtP_t, with P0=0P_0 = 0. A buy-and-hold return can be written as

Pt=ct,c∈R+P_t = ct, \enspace c \in \R^+

Suppose a strategy sells and rebuys during a cycle. Let γ\gamma be the extra gross return captured between the sale and repurchase:

Pt=ct+γ.P_t = ct + \gamma.

In this simplified expression, a positive γ\gamma beats buy and hold before costs. Real performance also depends on whether the model identifies the turning points out of sample, how often it trades, bid-ask spreads, slippage, taxes, and shorting costs. So the backtest is a model experiment, not a trading claim.