Five models — naive, moving average, Ridge, XGBoost, LSTM — benchmarked on 79,249 rows of real daily equity data across 6 expanding walk-forward folds. None of them lower next-day RMSE below tomorrow = today. That is the expected, correct result for near-efficient prices, and reporting it is the point.
20 equities, 2009–2025, from yfinance and FRED.
Training data is always strictly before the scored days — enforced by a test.
Two honest baselines before any learned model is allowed to claim a win.
Above chance — where the predictive value actually shows up.
Every model, scored on data it never saw during training. RMSE vs naive is the only column that matters: positive means the model beat the random walk.
| Model | RMSE | MAE | SMAPE% | Dir. acc | RMSE vs naive |
|---|---|---|---|---|---|
| naive | 2.410 | 1.355 | 1.20 | 0.0% | +0.00% |
| linear | 2.414 | 1.355 | 1.20 | 51.9% | -0.17% |
| lstm | 2.493 | 1.404 | 1.24 | 48.7% | -3.46% |
| xgboost | 2.693 | 1.582 | 1.38 | 51.3% | -11.75% |
| moving_avg | 3.509 | 2.047 | 1.80 | 50.6% | -45.63% |
The same comparison split by market regime. Learned models degrade faster than the benchmark when volatility rises.
| Model | Low-vol | High-vol |
|---|---|---|
| naive | +0.00% | +0.00% |
| linear | -0.13% | -0.30% |
| lstm | -4.36% | -6.15% |
| xgboost | -0.66% | -32.25% |
| moving_avg | -33.73% | -54.96% |
Each feature group removed in turn. The deltas are within noise — no group is carrying the model.
| Variant | RMSE | Δ vs full |
|---|---|---|
| full | 2.414 | +0.00% |
| no_macro | 2.413 | -0.04% |
| no_volatility | 2.412 | -0.08% |
| no_momentum | 2.412 | -0.06% |
reports/summary.md, which is written by the benchmark itself.Two commands. The synthetic mode needs no network.
pip install -r requirements.txt
python -m src.forecast.run --synthetic
pytest -q
Drop --synthetic to refetch the real panel from yfinance and FRED.