Stock Price LSTM

Forecasts Apple's (AAPL) daily closing price after a chosen cutoff date. A small LSTM reads the last 60 daily log-returns and predicts the next one. Each prediction is fed back in to forecast up to 60 business days. The repo ships a frozen copy of the price history (prices.csv), so forecasts need no market-data API.

The honest result: on the held-out test period (2024-11-04 to 2026-07-31), the model's next-day RMSE is $4.48. The naive forecast "tomorrow = today" scores $4.47, so the model does not beat it. Daily prices are close to a random walk, and this model has learned little more than the average daily drift (numbers from the run of 2026-09-25, metrics.json).

Model

  • Architecture (model.build_lstm): Input(60, 1) -> LSTM(64) -> Dropout(0.2) -> Dense(1), 16,961 parameters, float32. It is saved as model.keras (Keras 3).
  • Input: the last 60 daily log-returns r_t = ln(C_t / C_(t-1)) up to the cutoff, standardized with the training split's mean (0.000827) and standard deviation (0.018858). Both are stored in config.json.
  • Output: the next standardized log-return. Prices are rebuilt as C_cutoff × exp(cumulative returns). For a horizon of h days, the model runs h times, and each prediction becomes the newest input.
  • prices.csv: 2,911 daily closes from 2015-01-02 to 2026-07-31 (yfinance Close, split-adjusted, not dividend-adjusted). Valid cutoffs run from 2015-03-31 (the first day with a full 60-return window) to 2026-07-31. Other dates are clamped to that range and snapped to the previous trading day.

predict(cutoff, horizon) returns the portfolio's timeseries format:

{"model": "LSTM(64) on log-returns",
 "history": [{"date": "2025-01-06", "price": 245.0}, ..., {"date": "2025-06-30", "price": 205.17}],
 "forecast": [...], "actual": [...], "naive": [...],
 "mae": 5.7788, "naiveMae": 7.8303}
  • history: up to 120 trading days, ending at the effective cutoff.
  • forecast / naive: one point per forecast day. naive repeats the cutoff close.
  • actual: the real closes after the cutoff that exist in prices.csv. It is empty for the last date.
  • mae / naiveMae: mean absolute error in $ over the days that have an actual price, or null.

Usage

from huggingface_hub import snapshot_download
import sys
path = snapshot_download("shalev396/stock-lstm-forecast")
sys.path.insert(0, path)
import model
predictor = model.load(path, device="cpu")
result = predictor.predict("2025-06-30", 30)   # cutoff "YYYY-MM-DD", horizon in business days (1-60)
print(result["forecast"][-1], result["mae"], result["naiveMae"])
  • Space API: POST /gradio_api/call/predict with {"data": ["2025-06-30", 30]}. See the Space.
  • Inference Endpoint: handler.py accepts {"inputs": {"cutoff": "2025-06-30", "horizon": 30}}, or {"inputs": "2025-06-30", "parameters": {"horizon": 30}}.

Training

  • Data: AAPL daily OHLCV from yfinance, downloaded on 2026-08-01 (2015-01-02 to 2026-07-31). The first 14 days are dropped as RSI warm-up, which leaves 2,897 days.
  • Split: chronological 70 / 15 / 15. Each 60-day window belongs to the split of the day it predicts, so no window crosses a boundary. Train has 1,966 windows (2015-04-22 to 2023-02-09), validation 435 (2023-02-10 to 2024-11-01) and test 435 (2024-11-04 to 2026-07-31). Every scaler is fit on the train rows only.
  • Recipe: Adam 1e-3, MSE on the scaled target, batch 32, up to 40 epochs. Early stopping (patience 6) and learning-rate halving watch validation loss, and the best epoch's weights are restored.
  • Selection: among the univariate models the Space can run recursively, the one with the lowest validation RMSE is deployed. Test metrics are computed once.
  • Hardware / time: CPU. Training all four networks took 575.3 s in this run, but the CPU was shared with several other training jobs. An earlier uncontended run of the same recipe took 71.8 s (the legacy results.json).
  • Full code: training/ · Colab

Experiments

Next-day (one-step) predictions on the original $ scale, for the same 435 test days across all variants. Rows are ranked by validation RMSE, and the deployed model is in bold:

variant params val RMSE ($) test RMSE ($) test MAE ($) test MAPE (%)
naive_persistence - 2.583 4.469 3.020 1.233
lstm_returns (deployed) 16,961 2.588 4.483 3.026 1.237
simple_rnn_levels 4,289 4.562 12.875 10.803 4.159
lstm_levels 16,961 5.896 14.482 10.904 4.173
stacked_lstm_multi 51,009 6.544 21.975 18.107 6.854
  • Price levels break under a train-fit scaler. Train closes range from $22.58 to $182.01, and test closes from $172.42 to $340.08. Models that predict MinMax-scaled levels must extrapolate beyond the range they were trained on, so they lag badly: their test RMSE is $12.88 to $21.98. Adding volume, RSI and EMA features (the stacked multivariate LSTM) makes this worse.
  • Log-returns fix the scaling problem, but the model has no signal to find. The returns target is roughly stationary, so the LSTM stays in its training range and matches naive persistence: test RMSE $4.483 vs $4.469, MAPE 1.237 % vs 1.233 %. Its validation loss was lowest after the first epoch, which means it mostly predicts the average daily return.
  • Multi-day forecasts (what the Space shows): the recursive forecast was run from 84 test cutoffs (every 5th test day, 2024-11-01 to 2026-07-02) for 20 business days each. Its mean MAE is $10.67, against $10.87 for repeating the cutoff close. The small edge comes from the positive drift built into the forecast during a period when AAPL mostly rose. It is not evidence of predictive skill.
  • Changes from the earlier version of this project: the model is now chosen on validation instead of test RMSE, and everything runs in float32 (the old GPU path enabled mixed precision without float32 heads). The Space now serves a frozen copy of the data instead of calling yfinance, and it compares every forecast with the naive baseline. With the same data and seed, the retrained test metrics match the earlier run.

Model comparison Test forecast Multi-step MAE Training curves

Evaluation

Deployed model (lstm_returns), next-day predictions on the test split (2024-11-04 to 2026-07-31):

metric (test) value
rmse 4.4831
mae 3.0255
mape 1.2368

Naive persistence on the same days: RMSE 4.469, MAE 3.020, MAPE 1.233 %.

Limitations

  • It does not beat naive persistence on next-day prices. Do not use it for trading. Not financial advice.
  • One stock, one period. It was trained on AAPL from 2015 to 2023 only. The return statistics (mean and std) are Apple's, and the model was not validated on any other ticker.
  • Frozen data. Forecasts exist only for cutoffs up to 2026-07-31. Refreshing needs a retrain and a new prices.csv.
  • Recursive forecasts converge to the drift. After a few steps the forecast is close to a smooth exponential trend at about 0.08 % a day. It has no notion of earnings, news or volatility regimes.
  • Close only. Prices are split-adjusted but not dividend-adjusted, and there are no uncertainty bands.
  • Educational portfolio project.
Downloads last month
11
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Space using shalev396/stock-lstm-forecast 1

Evaluation results