bhavani s. ← all writing

Time series · Honest baselines

I benchmarked five forecasters against “tomorrow = today”. It won.

Five models, twenty stocks, sixteen years of real market data — and the one-line baseline came first. Why that is the right answer, and what it takes to trust it.

Bhavani Shankar Ajith

Every forecasting project has a moment where you have to decide what counts as success. I picked mine before I ran anything: beat “tomorrow’s price is today’s price” on RMSE. That is the naive random walk, it takes one line to implement, and it is the thing a next-day price forecaster has to be better than to be worth anything at all.

Five models, twenty stocks, sixteen years of real market data. None of them beat it.

That is the result, and it is the honest one. Here is why I think it is more useful than a tuned number would have been.

The setup

Twenty equities from 2009 to 2025 via yfinance, plus the ten-year Treasury yield from FRED. Five forecasters sharing one interface: naive, moving average, linear (Ridge), XGBoost, and an LSTM. Evaluation is expanding-window walk-forward across six folds — train on everything before a cut-off, score the days after it, move the cut-off forward, repeat.

Two design choices did most of the work. The models forecast next-day return and reconstruct the price level from it, so their RMSE is measured on the same scale as the random walk rather than on a differently-shaped target. And the no-leakage property is enforced by a test, not by my own care: training data is always strictly earlier than the scored days, and the suite fails if that ever stops being true.

The results

ModelRMSEMAEDir. accvs naive
naive (random walk)2.4101.355—±0.00%
linear (Ridge)2.4141.35551.9%−0.17%
LSTM2.4931.40448.7%−3.46%
XGBoost2.6931.58251.3%−11.75%
moving average3.5092.04750.6%−45.63%

Six walk-forward folds, averaged. Lower RMSE is better.

Read the ordering, because it is not random. The simplest model loses by the least. Ridge is within a fifth of a percent of the random walk. The LSTM — the most expressive thing in the table — is three and a half percent worse. XGBoost is twelve percent worse. The moving average, which is the only model here that is structurally wrong about what prices do, is off by forty-five.

Model capacity did not buy accuracy. It bought variance, and the walk-forward split made me pay for it.

Why this is the expected answer

Daily equity prices are close to a martingale. If a simple model could reliably predict tomorrow’s close from today’s, the prediction would be traded away until it stopped working. A benchmark that says otherwise is usually measuring its own leakage.

Which is exactly why the result is worth publishing. The failure mode in this corner of ML is not that people cannot fit a model — it is that a small lookahead bug produces a beautiful number, and nobody checks it because the number is what everyone wanted. Reporting the null result is how you show the pipeline is not lying to you.

Where the signal actually was

RMSE is not the only question. Ridge matches the random walk on error while getting 51.9% directional accuracy — above chance. That gap matters: knowing the sign of tomorrow’s move is a different, easier, and more economically interesting problem than knowing its magnitude.

51.9%
directional accuracy, Ridge
6
expanding-window folds
20
equities, 2009–2025
15
tests, incl. leakage guards

The volatility-regime breakdown says something similar. Relative model behaviour changes with the regime, so a single averaged leaderboard row hides more than it shows. A model that is mediocre on average can be the right one in a specific regime, and you only find that out if you cut the results that way.

Where the work actually went. Not the LSTM. The walk-forward harness, the return-space target, and the test that fails the build if training data ever overlaps a scored day. Those are what make a null result trustworthy enough to publish.

What I took away

Pick the baseline before you pick the model. If you cannot say in one sentence what you must beat, you will end up grading your own homework.

And when the baseline wins, say so in the headline. The results page for this project opens with “the random walk is not beaten” — not because negativity is a virtue, but because a benchmark that can only produce good news is not a benchmark.

The benchmark rebuilds itself from the code on every push, so the results page always reflects the current run rather than a screenshot of a good day.

Live results → Source