6.2 — Why Mean Forecasting Lost to ML: Forecast Accuracy, Walk-Forward Discipline, Time Series as the Base of the ML Pipeline

Chapter 6 · Time Series Models for Trading and Risk

Prof. Xuhu Wan

Section 6.2 · Chapter 6 · Learning Statistics with Python

Why Mean Forecasting Lost to ML: Forecast Accuracy, Walk-Forward Discipline, Time Series as the Base of the ML Pipeline

Time Series Models for Trading and Risk

Prof. Xuhu Wan

ISOM, HKUST Business School · 2026 Edition

Why Mean Forecasting Lost to ML: Forecast Accuracy, Walk-Forward Discipline, Time Series as the Base of the ML Pipeline

A forecast is only as good as the baseline it beats. You will score the laziest forecast of all and discover that “predict zero” beats it; then watch the AIC-winning ARIMA lose money out of sample. The pivot: what a modern ML pipeline inherits from time series — stationary features, lags without leakage, walk-forward validation — and what a gradient-boosting model honestly delivers on one series.

Four ways to score a forecast

With actual \(y_t\) and forecast \(\hat y_t\) over \(n\) periods:

\[\text{MAE} = \tfrac1n\sum|y_t - \hat y_t|, \quad \text{RMSE} = \sqrt{\tfrac1n\sum (y_t-\hat y_t)^2}, \quad \text{MAPE} = \tfrac{100}{n}\sum\left|\tfrac{y_t-\hat y_t}{y_t}\right|,\]

\[\text{DA} = \tfrac1n\sum \mathbf 1\{\text{sign}(y_t) = \text{sign}(\hat y_t)\}.\]

Which metric is unusable for forecasting minute-by-minute price changes?

  • MAE — absolute errors are not differentiable
  • RMSE — it over-penalises large errors
  • MAPE — it divides by actual values that are near or exactly zero
  • Directional accuracy — the sign of a change is meaningless

The naive benchmark: “tomorrow’s change = today’s”

Target Y is the next minute’s price change; the forecast Yhat is the current change.

388 forecasts: MAE 0.1157 dollars, RMSE 0.1694, directional accuracy 0.466worse than a coin. Minute changes are slightly negatively autocorrelated (bid–ask bounce), so copying the last change points you the wrong way.

Can “predict nothing” beat “predict the last change”?

Forecast zero for every minute. The RMSE of the zero forecast is just the std of Y around zero. Is it above or below the naive RMSE of 0.1694?

print(round(RMSE(d.Y, 0 * d.Y), 4))

0.1057

0.1057 vs 0.1694. Copying the last change adds the variance of two changes; predicting the mean (≈ 0) adds none. Any forecast you build must beat 0.1057, not the naive number. That zero forecast is the first baseline in every ML pipeline too.

Simple moving average: average the last 40 changes

\(\hat y_{t+1} = \tfrac{1}{40}\sum_{i=0}^{39} \Delta P_{t-i}\).rolling(40).mean(). Predict: does averaging out the bounce beat the zero forecast (0.1057)?

On the common 349 rows: SMA RMSE 0.0993, zero 0.0986, naive 0.1428. The SMA crushes naive and ties the zero forecast; its only edge is a direction call (DA 0.521) that a zero forecast cannot make. And compare only on the same rows — the naive RMSE moved from 0.1694 to 0.1428 just by dropping 40 warm-up minutes.

EWMA: recent values count more

\(s_t = \alpha\,x_t + (1-\alpha)\,s_{t-1}\), \(s_1 = x_1\). As \(\alpha \to 1\) it is the naive forecast; as \(\alpha \to 0\) the long-run mean.

x = [10, 20, 30], α = 0.3, adjust=False. \(s_1 = 10\), \(s_2 = 0.3·20 + 0.7·10\), \(s_3 = 0.3·30 + 0.7·s_2\). What prints?

print(pd.Series([10., 20., 30.]).ewm(alpha=0.3, adjust=False).mean().round(2).tolist())

[10.0, 13.0, 18.1]

EWMA(0.3): RMSE 0.1075, DA 0.496 — between naive and SMA, because α = 0.3 remembers only ~2 minutes and inherits the bounce. The window is the model — and you tuned it by eye. Now let a likelihood choose.

The ARIMA trading test: fixed parameters, one step ahead

Fit ARIMA(2,0,1) — the AIC winner — on AAPL log returns 2020–2023, then feed 2024 through res.apply(new_data) for one-step-ahead forecasts without refitting. Trade the sign.

DA 0.480, RMSE 0.01451worse than forecasting the training mean (0.01421); Sharpe -0.20 against 1.16 for holding. The AIC winner in-sample is a loser out of sample. This is the result that moved mean forecasting off the classical desk.

The pivot: what the ML pipeline inherits from time series

ML did not replace time series; it sits on top of it. Four things every return-prediction model in Chapter 4 quietly assumed:

Inherited discipline From Failure if skipped
Stationary features — returns, ratios, differences, never price levels §6.1 tree splits on a level that never recurs; the model memorises 2021
Lag to avoid leakage — every feature at \(t\) uses data to \(t-1\) .shift(1) in-sample accuracy 0.9, live accuracy 0.5
Walk-forward validation — expanding window, refit per period §6.2 random K-fold lets the future train the past
A baseline that must be beaten — zero forecast, ARIMA(0,0,0) §6.1–5.2 a Sharpe with nothing to compare it to

Tip

The ML model is the last step of a time-series pipeline. Most of the value is in the first three rows.

Build the features — and spot the leak first

Target is y = r (today’s return). A colleague builds X[“std21”] = r.rolling(21).std() with no shift. What is wrong?

  • Nothing — volatility is a legitimate feature
  • The window at day \(t\) contains \(r_t\) itself: the target leaks into the feature
  • Rolling std needs at least 60 days to be stable
  • The feature is non-stationary

Eleven features, all stationary (returns, rolling moments, a ratio), all shifted. The first fold trains on 2016–2017 and tests on 2018; each later fold expands the training window by a year. That is the walk-forward split.

Gradient boosting, trained the way a desk would train it

Same spec as Chapter 4: HistGradientBoostingRegressor(max_depth=2, learning_rate=0.05, max_iter=200, min_samples_leaf=50). Refit every January on all data to date; predict the year; trade long/flat on the sign.

Honest numbers: DA 0.494, RMSE 0.01276worse than the zero forecast (0.01247), correlation -0.013; the rule’s Sharpe 0.31 against 0.57 for holding the index, in the market 59 % of days. No year’s DA clears 0.54. The same walk-forward discipline that exposed ARIMA exposes the GBM. ML did not win by forecasting one series from its own past — it won by moving to the cross-section and to richer features (Ch. 3.5). The pipeline is the inheritance; the alpha is elsewhere.

What about LSTM and sequence models?

In Colab

import torch, torch.nn as nn
class LSTMReg(nn.Module):
    def __init__(self, n_feat, hidden=32):
        super().__init__(); self.lstm = nn.LSTM(n_feat, hidden, batch_first=True); self.head = nn.Linear(hidden, 1)
    def forward(self, x): out, _ = self.lstm(x); return self.head(out[:, -1])
model = LSTMReg(n_feat=11); opt = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(20):                       # X_seq: (batch, 21 days, 11 features), y: next-day return
    opt.zero_grad(); loss = ((model(X_seq) - y).pow(2)).mean(); loss.backward(); opt.step()
  • Gu, Kelly & Xiu (RFS 2020): on the US stock cross-section, shallow trees and 2–3-layer nets beat deep ones; the gain over linear models comes from interactions and features, not from depth.
  • Zero-shot foundation models (Chronos, TimesFM): no consistent edge over a GBM baseline on daily returns — there is too little predictable structure in returns for pre-training to transfer.
  • Where sequence models do help: intraday order flow, the volatility surface, and joint modelling of many series at once.

Your LSTM beats the GBM by 4 % accuracy in-sample. What do you check first?

  • Increase the hidden size — the LSTM is under-fitting
  • Walk-forward out-of-sample on the same features and folds, plus a leakage audit of the sequence windows
  • Switch the loss from MSE to directional accuracy
  • Add more epochs

Aside: when you have several series — Granger causality

\(X\) Granger-causes \(Y\) if lags of \(X\) improve the forecast of \(Y\) beyond \(Y\)’s own lags — an F-test on the \(X\) coefficients in \(Y\)’s equation (a VAR is \(k\) such regressions on shared lags). Forecastability, not mechanism.

p_nvda_to_spy is done. Compute p_spy_to_nvda — the p-value that SPY’s past helps forecast NVDA — using granger_p with 2 lags.

NVDA → SPY 0.6835, SPY → NVDA 0.1257. Neither direction at 5 %: in daily returns the lead–lag is priced away within the day. On macro data (Stock–Watson, FRED-MD) Granger tests are informative; on liquid daily prices they mostly confirm efficiency.

What you discovered

  • Score forecasts with MAE, RMSE and DA on the same rows; MAPE only away from zero. The naive forecast (RMSE 0.169) loses to predicting zero (0.106); SMA(40) ties zero (0.099 vs 0.099).
  • The AIC-winning ARIMA on AAPL: 2024 DA 0.48, RMSE worse than the training mean, Sharpe −0.20 vs 1.16 for holding.
  • What ML inherits: stationary features, .shift(1) before anything else, expanding-window walk-forward, and a baseline to beat.
  • The GBM, trained walk-forward on eleven lagged/rolling features of the S&P 500: DA 0.494, Sharpe 0.31 vs 0.57. One series has no exploitable mean memory — the edge in ML comes from the cross-section, not from depth.
  • NVDA/SPY Granger causality: 0.68 and 0.13 — nothing at daily frequency.

Next: §6.3 — stop forecasting a direction; trade a stationary relationship instead.