Time-series forecasting models
Almost every impressive forecasting result on a price series is a lag detector. Predict
tomorrow's price with today's and you get an R^2 of 0.99; the model has learned nothing, and its
implied return forecast is exactly zero. This skill supplies the four baselines a forecast has to
beat, the scale-free score that makes them comparable, the walk-forward that stops the future
leaking in, and the test that says whether a difference is real.
Every number below is printed by scripts/forecast_baselines.py (numpy / scipy, seed 0,
statsmodels optional). Two seeded DGPs run through the same machinery: A, a daily log-price
random walk with drift (1,300 days, drift 3 bp/day, vol 1 %/day), and B, a monthly series
with a linear trend and a 12-period seasonal (240 points).
1. 🚨 The random-walk illusion, measured on one series
| quantity, same 1,300-day price series |
value |
R^2 of price[t] on price[t-1] |
0.987032 |
R^2 of return[t] on return[t-1] |
0.000133 |
| R^2 of the naive price forecast against a constant-mean benchmark |
0.996700 |
| R^2 of the same forecast against the naive benchmark |
0.000000 |
The first three numbers describe a forecast that contains no information; the fourth is its
honest score. An out-of-sample R^2 only means something relative to a benchmark you name, and
for a price series that benchmark is the naive forecast, not the sample mean.
🚨 The lag signature. For the naive price forecast, corr(forecast[t], actual[t-k]) is
| k |
0 |
1 |
2 |
3 |
4 |
5 |
| corr |
0.9891 |
1.0000 |
0.9892 |
0.9780 |
0.9675 |
0.9530 |
The maximum is at k = 1, not k = 0. Any "price forecast" whose cross-correlation with the
truth peaks one step behind is reproducing the last observation. Plot that correlogram before you
believe a price-prediction chart; it is one line of numpy and it settles the question.
And the same model as a return forecast predicts exactly zero every day: out-of-sample R^2
against the training mean return 0.002195, and no direction at all. On this path 51.7 % of
days were up, so "long every day" is the accuracy any directional claim has to beat — not 50 %.
Forecast returns, not levels. Report the benchmark next to every R^2. If the benchmark is
not stated, the R^2 is not a result.
2. Four baselines and one scale-free score
| baseline |
forecast for step i ahead from y[1..T] |
| naive |
y[T] |
| seasonal naive |
y[T + i - m*ceil(i/m)] — the same phase of the last complete cycle |
| drift |
y[T] + i * (y[T] - y[1]) / (T - 1) — the line through the first and last points |
| mean |
mean(y[1..T]) |
MASE (Hyndman-Koehler 2006) divides the forecast's MAE by the in-sample naive MAE,
mean(|y[t] - y[t-m]|) over the training set. That denominator is what makes it comparable
across series and readable without context: MASE > 1 is worse than the training-set naive.
✅ The definition's own identity, checked rather than asserted: the MASE of the in-sample naive
forecast is 1.0000000000, at both m = 1 and m = 12. On the held-out days the naive
forecast scores 0.9054 — not 1, because the scale of the moves changed, which is the one
thing MASE does not normalise away.
⚠️ MAPE and sMAPE are the alternatives you will meet: MAPE is undefined at zero and asymmetric
(it punishes over-forecasts more), sMAPE is bounded but still unstable near zero. Neither was
measured here. Use MASE for anything that crosses zero, which includes every return series.
3. Rolling-origin walk-forward: what beats the naive forecast, and where
One origin per step; at origin T the model sees y[:T] and nothing else, and is scored on
y[T]. ✅ Measured (MASE, one-step-ahead):
| forecast |
A: daily price, 300 origins |
B: monthly seasonal, 72 origins |
| naive |
0.9054 |
1.0995 |
| seasonal naive |
2.1403 |
0.8534 |
| drift |
0.9067 |
1.1034 |
| mean |
15.2999 |
5.4063 |
| ARIMA |
0.9051 (1,1,1) |
0.8385 (1,0,0)x(1,0,0,12) |
| ETS (Holt-Winters) |
0.9067 |
0.4846 |
Diebold-Mariano against the naive forecast, squared loss, h = 1, HLN-corrected (negative = the
model wins):
| forecast |
A: DM / p |
B: DM / p |
| seasonal naive |
+10.942 / 0.0000 |
-2.283 / 0.0255 |
| drift |
+1.460 / 0.1453 |
+0.345 / 0.7313 |
| mean |
+17.819 / 0.0000 |
+11.727 / 0.0000 |
| ARIMA |
-1.408 / 0.1601 |
-2.679 / 0.0092 |
| ETS |
+1.478 / 0.1405 |
-5.936 / 0.0000 |
The contrast is the point. On the random walk, ARIMA's MASE (0.9051) is below the naive's
(0.9054) and its DM statistic is negative — and the p-value is 0.16, so the difference is
noise. Nothing beats the naive forecast on a series with nothing in it, and a table of MASE
values alone would have let you claim otherwise. On the seasonal series three methods beat it and
the DM test agrees at 5 %, with ETS at less than half the naive's error.
⚠️ Refit cadence is a modelling choice, not an implementation detail — though it happened to
be small here: refitting the ARIMA 6 times instead of 2 on DGP A moved MASE from 0.9051 to
0.9050, and 18 times instead of 3 on DGP B moved 0.8385 to 0.8380. State the cadence; a result
that only survives at one cadence is a tuned result. ✅ Between refits the demo extends the model
with res.append(new_obs, refit=False), statsmodels' documented way to add observations without
re-estimating.
🚨 ExponentialSmoothing has no append, so there is no cheap honest way to carry a fit
forward. Freezing the estimated smoothing parameters between refits and re-fitting with
optimized=False is a different estimator — the initial states come from the heuristic rather
than the likelihood — and it is not a small difference. ✅ Measured on DGP B: MASE 4.0784
with the parameters frozen between 24-origin refits against 0.4846 refit at every origin, an
8.4x gap produced entirely by how the walk-forward was wired (on DGP A, 0.9294 against 0.9067).
Refit ETS at every origin and pay the time.
4. Diebold-Mariano: the difference between "smaller MSE" and "better"
d[t] = L(e1[t]) - L(e2[t]); the statistic is mean(d) / sqrt(V(mean(d))) with V a
Newey-West long-run variance truncated at h - 1 lags. harvey=True applies the
Harvey-Leybourne-Newbold (1997) small-sample factor sqrt((T + 1 - 2h + h(h-1)/T)/T) and refers
the result to t(T-1).
✅ Three checks on the implementation, all printed by the script:
- At h = 1 without the Harvey factor it must be a one-sample t-test on
d using the
population variance, so DM / scipy.stats.ttest_1samp must equal sqrt(T/(T-1)). Measured:
DM -1.925657, scipy -1.923248, ratio 1.0012523486 against an expected 1.0012523486 —
agreement to 0.0e+00.
- Antisymmetry: DM(e1, e2) = -2.759902 and DM(e2, e1) = +2.759902, same p-value 0.006139.
- Size under the null: two forecasts with equal expected loss, 1,000 runs of T = 250 — it
rejects 5.8 % at the 5 % level, against a Monte Carlo standard error of 0.7 %.
⚠️ What it does not do. The null is equal expected loss, not "the models are the same". It
assumes the loss differential is covariance-stationary, which nested models estimated on the same
data can violate. And it compares two forecasts. A table of pairwise DM tests over ten models
is ten trials: use arch.bootstrap's SPA, StepM or MCS instead — those take losses, and
../../../fin-libraries/skills/lib-arch/SKILL.md leads with the sign trap that inverts them.
../../../fin-core/skills/backtest-validation/SKILL.md owns the correction.
5. statsmodels: two defaults that change the forecast
✅ Source-verified against the installed statsmodels 0.15.0.
- 🚨 An integrated ARIMA has no drift term by default.
statsmodels/tsa/arima/model.py sets trend = "c" when trend is None and not integrated,
and trend = "n" otherwise — so ARIMA(y, order=(1,1,0)) estimates only ['ar.L1', 'sigma2']. ✅ Measured on a series drifting +0.04634 per step in sample, the 50-step forecast
slope is +0.00007 — a flat line; passing trend="t" recovers +0.04629. On a price
series with a real drift, the default silently forecasts no drift at all.
- ✅
ARIMA(y, order=(0,1,0)) IS the naive forecast. With trend="n" it has no mean
parameters at all, and its one-step forecast equals the last observation to 0.0e+00 across
four origins. Useful as a sanity anchor: if your ARIMA pipeline does not reproduce the naive
forecast at order (0,1,0), the pipeline is wrong, not the data.
- ✅
ARIMA(endog, exog=None, order=(0,0,0), seasonal_order=(0,0,0,0), trend=None, enforce_stationarity=True, enforce_invertibility=True, concentrate_scale=False, trend_offset=1, ...), and MLEResults.append(endog, exog=None, refit=False, fit_kwargs=None) is the walk-forward extension used above.
- ✅
ExponentialSmoothing(endog, trend=None, damped_trend=False, seasonal=None, *, seasonal_periods=None, initialization_method="estimated", ...) — trend and seasonal are
None by default, so a bare call is simple exponential smoothing with neither.
statsmodels.tsa.exponential_smoothing.ets.ETSModel is the newer state-space ETS
(error="add" by default); the two are different implementations of overlapping models.
- ⚠️
sktime, statsforecast, pmdarima, darts, prophet and neuralforecast are not
installed here and nothing about them was checked.
../../../fin-core/skills/factor-and-timeseries-research/SKILL.md chooses between them and
carries the verified leakage findings in their CV splitters.
6. What the M4 and M5 competitions actually say
These are the largest public forecasting evaluations, and they are routinely cited for claims
they do not support. ⚠️ Read at the sources below on 2026-09-09; this repo did not re-run them.
- ⚠️ M4 (2018, 100,000 series) was won by a hybrid, not by deep learning. Slawek Smyl's
ES-RNN mixes exponential smoothing with LSTM layers (Smyl, IJF 36(1), 2020, 75-85). The
second-place method used XGBoost to weight forecasts from standard statistical methods.
- ⚠️ M4's own Finding 5: the six pure ML methods performed poorly — none was more accurate
than the
Comb benchmark, and only one beat Naive2 (Makridakis, Spiliotis &
Assimakopoulos, IJF 34(4), 2018, 802-808; abstract read at
https://ideas.repec.org/a/eee/intfor/v34y2018i4p802-808.html). ✅ From the organizers' own
evaluation code, Comb is the equal average of SES, Holt and Damped exponential smoothing,
and Naive2 is the naive forecast on a seasonally adjusted series
(https://github.com/Mcompetitions/M4-methods, Benchmarks and Evaluation.R).
- ⚠️ OWA is normalised by Naive2, not by Comb:
OWA = 0.5 * [sMAPE/sMAPE(Naive2) + MASE/MASE(Naive2)], straight from that same evaluation code. The two benchmarks get conflated
constantly — Comb is what the ML methods failed to beat; Naive2 is the OWA denominator.
- ⚠️ M5 (2020) is Walmart unit sales, and gradient boosting won it. The Accuracy track was
won by an equal-weighted ensemble of 220 LightGBM models, 22.4 % better than the
ES_bu
benchmark; only 7.5 % of teams' final submissions beat that benchmark at all. Deep learning
did not lose M5: third place was a pure ensemble of 43 LSTM networks and second place used
N-BEATS forecasts to adjust LightGBM (Makridakis, Spiliotis & Assimakopoulos, M5 Accuracy
paper, IJF 38(4), 2022, 1346-1364; authors' preprint at
https://statmodeling.stat.columbia.edu/wp-content/uploads/2021/10/M5_accuracy_competition.pdf).
⚠️ The same paper reports the gains concentrating at the top of the hierarchy: ~40 % over the
benchmark at the most aggregated level and ~3 % at the product-store level, which is where
most of the series are.
- 🚨 Neither competition tells you anything about daily equity returns. M5 is retail unit
sales. M4 contains a "Finance" domain, but it is scored as a level extrapolation with
sMAPE/MASE/OWA, not as return prediction, and there is no cross-section and no trading.
⚠️ Rob Hyndman, writing in the M4 special issue itself: "I know of no large-scale forecasting
competition for finance data" (A brief history of forecasting competitions, IJF 36(1),
2020, 7-14; author's copy at
https://robjhyndman.com/papers/forecasting-competitions.pdf). ⚠️ The M4 Finance domain is
reported as 24,534 of the 100,000 series, of which 1,559 are daily — secondhand, from Table 8
of the sktime M4 replication (arXiv 2005.08067); the primary table was behind a paywall, and
what those series contain could not be established.
On N-BEATS, TFT and LSTM for financial returns, this skill carries no verified claim that any
of them beats the naive forecast on daily equity returns. ⚠️ The nearest thing to a positive
claim found (arXiv 2408.12408, an evaluation of TCN / N-BEATS / TFT / N-HiTS / TiDE / xLSTM on
the S&P 500 and EWZ) reports that on the original undenoised data the models sit near random
guessing, and that only three of the six beat the naive forecast even after wavelet denoising —
N-BEATS and TFT were not among them, and the paper does not say whether the denoising was fitted
before or after the split. ⚠️ Goyal and Welch's out-of-sample study of the equity premium (NBER
w10483; RFS 21(4), 2008) found no standard predictor helped an investor outpredict the
prevailing historical mean — monthly and annual data with economic predictors, so it is evidence
about the difficulty of the target rather than about any architecture.
../../../fin-llm/skills/rl-and-ml-trading/SKILL.md owns deep-learning trading claims.
7. Traps
- 🚨 R^2 on a level, or any metric on a level. Section 1. Forecast the difference.
- 🚨 No baseline in the table. If naive, seasonal-naive, drift and mean are not in the
comparison, the result is unscored. MASE puts them there by construction.
- 🚨 A single train/test split. One split is one draw. Roll the origin, and report the number
of origins next to the score.
- 🚨 Fitting anything on the full series before splitting — scaling, denoising, differencing
parameters, seasonal decomposition, a stationarity test used to choose
d. Every one of those
belongs inside the fold.
../../../fin-core/skills/signal-construction/scripts/assert_causal.py checks the result.
- 🚨
trend=None on an integrated ARIMA. Section 5: a flat forecast on a drifting series.
- 🚨 A p-value from a table of models. DM compares two. More than two needs SPA / StepM / MCS
and a trial ledger (
../../../fin-core/skills/backtest-validation/SKILL.md).
- ⚠️ Accuracy is not profit. A forecast that beats the naive one on MASE can still lose money
after costs, and a directional accuracy of 51.7 % on this seed is exactly the base rate of up
days.
../../../fin-core/skills/signal-construction/SKILL.md turns a forecast into a position;
../../../fin-core/skills/execution-cost-analysis/SKILL.md prices the trading.
8. Scripts
scripts/forecast_baselines.py — the two framings of one series and the lag signature, MASE
and its identity, the rolling-origin tables for both DGPs, the ARIMA and ETS walk-forwards,
the Diebold-Mariano implementation with its three verification checks, and the statsmodels
trend defaults. statsmodels is imported inside four functions; without it the baselines, MASE
and the DM test all still run. Seed 0, 41 s of compute (36-48 s wall clock across runs; the slowest script in
this plugin).
9. Where this sits
../volatility-models/SKILL.md owns forecasting variance (and HAR-RV, which is the baseline
there). ../stat-arb-cointegration/SKILL.md owns unit roots and stationarity testing between
two series. ../state-space-and-kalman/SKILL.md owns latent-state models and their
filtered-vs-smoothed rule. ../../../fin-core/skills/factor-and-timeseries-research/SKILL.md
chooses the library and carries the foundation-model evidence.
../../../fin-core/skills/backtest-validation/SKILL.md corrects across many models.
../../../fin-libraries/skills/lib-arch/SKILL.md owns arch.bootstrap's SPA / StepM / MCS.
../../../fin-llm/skills/rl-and-ml-trading/SKILL.md owns deep-learning trading claims.
1---2name: time-series-forecasting-models3description: Score a forecast against the baseline it has to beat - naive, seasonal-naive, drift, mean - with MASE, rolling-origin evaluation and a Diebold-Mariano test, instead of an R^2 on a price level. TRIGGER - forecasting, ARIMA, SARIMA, SARIMAX, auto_arima, ETS, Holt-Winters, exponential smoothing, statsmodels ARIMA trend, walk-forward, rolling origin, expanding window; naive forecast, seasonal naive, drift method, MASE, sMAPE, MAPE, OWA, Diebold-Mariano, DM test; LSTM stock price prediction, N-BEATS, TFT, "my model predicts prices with 99% R^2", "is my forecast better than the naive one", M4 competition, M5 competition. SKIP for volatility forecasting and HAR-RV (volatility-models), for choosing a forecasting library (factor-and-timeseries-research), for cointegration and unit roots (stat-arb-cointegration), for correcting across many models (backtest-validation), and for turning a forecast into positions (signal-construction).4license: MIT5---67# Time-series forecasting models89**Almost every impressive forecasting result on a price series is a lag detector.** Predict10tomorrow's price with today's and you get an R^2 of 0.99; the model has learned nothing, and its11implied return forecast is exactly zero. This skill supplies the four baselines a forecast has to12beat, the scale-free score that makes them comparable, the walk-forward that stops the future13leaking in, and the test that says whether a difference is real.1415Every number below is printed by `scripts/forecast_baselines.py` (numpy / scipy, seed 0,16`statsmodels` optional). Two seeded DGPs run through the same machinery: **A**, a daily log-price17random walk with drift (1,300 days, drift 3 bp/day, vol 1 %/day), and **B**, a monthly series18with a linear trend and a 12-period seasonal (240 points).1920## 1. 🚨 The random-walk illusion, measured on one series2122| quantity, same 1,300-day price series | value |23|---|---|24| R^2 of `price[t]` on `price[t-1]` | **0.987032** |25| R^2 of `return[t]` on `return[t-1]` | **0.000133** |26| R^2 of the naive price forecast against a constant-mean benchmark | **0.996700** |27| R^2 of the *same* forecast against the naive benchmark | **0.000000** |2829The first three numbers describe a forecast that contains no information; the fourth is its30honest score. **An out-of-sample R^2 only means something relative to a benchmark you name**, and31for a price series that benchmark is the naive forecast, not the sample mean.3233🚨 **The lag signature.** For the naive price forecast, `corr(forecast[t], actual[t-k])` is3435| k | 0 | 1 | 2 | 3 | 4 | 5 |36|---|---|---|---|---|---|---|37| corr | 0.9891 | **1.0000** | 0.9892 | 0.9780 | 0.9675 | 0.9530 |3839The maximum is at **k = 1**, not k = 0. Any "price forecast" whose cross-correlation with the40truth peaks one step behind is reproducing the last observation. Plot that correlogram before you41believe a price-prediction chart; it is one line of numpy and it settles the question.4243And the same model as a **return** forecast predicts exactly zero every day: out-of-sample R^244against the training mean return **0.002195**, and no direction at all. On this path 51.7 % of45days were up, so "long every day" is the accuracy any directional claim has to beat — not 50 %.4647> **Forecast returns, not levels. Report the benchmark next to every R^2. If the benchmark is48> not stated, the R^2 is not a result.**4950## 2. Four baselines and one scale-free score5152| baseline | forecast for step `i` ahead from `y[1..T]` |53|---|---|54| naive | `y[T]` |55| seasonal naive | `y[T + i - m*ceil(i/m)]` — the same phase of the last complete cycle |56| drift | `y[T] + i * (y[T] - y[1]) / (T - 1)` — the line through the first and last points |57| mean | `mean(y[1..T])` |5859**MASE** (Hyndman-Koehler 2006) divides the forecast's MAE by the **in-sample** naive MAE,60`mean(|y[t] - y[t-m]|)` over the *training* set. That denominator is what makes it comparable61across series and readable without context: MASE > 1 is worse than the training-set naive.6263✅ The definition's own identity, checked rather than asserted: the MASE of the in-sample naive64forecast is **1.0000000000**, at both `m = 1` and `m = 12`. On the held-out days the naive65forecast scores **0.9054** — not 1, because the scale of the moves changed, which is the one66thing MASE does not normalise away.6768⚠️ MAPE and sMAPE are the alternatives you will meet: MAPE is undefined at zero and asymmetric69(it punishes over-forecasts more), sMAPE is bounded but still unstable near zero. Neither was70measured here. Use MASE for anything that crosses zero, which includes every return series.7172## 3. Rolling-origin walk-forward: what beats the naive forecast, and where7374One origin per step; at origin `T` the model sees `y[:T]` and nothing else, and is scored on75`y[T]`. ✅ Measured (MASE, one-step-ahead):7677| forecast | **A: daily price**, 300 origins | **B: monthly seasonal**, 72 origins |78|---|---|---|79| **naive** | **0.9054** | 1.0995 |80| seasonal naive | 2.1403 | **0.8534** |81| drift | 0.9067 | 1.1034 |82| mean | 15.2999 | 5.4063 |83| ARIMA | 0.9051 *(1,1,1)* | **0.8385** *(1,0,0)x(1,0,0,12)* |84| ETS (Holt-Winters) | 0.9067 | **0.4846** |8586Diebold-Mariano against the naive forecast, squared loss, h = 1, HLN-corrected (negative = the87model wins):8889| forecast | A: DM / p | B: DM / p |90|---|---|---|91| seasonal naive | +10.942 / 0.0000 | **-2.283 / 0.0255** |92| drift | +1.460 / 0.1453 | +0.345 / 0.7313 |93| mean | +17.819 / 0.0000 | +11.727 / 0.0000 |94| ARIMA | -1.408 / **0.1601** | **-2.679 / 0.0092** |95| ETS | +1.478 / 0.1405 | **-5.936 / 0.0000** |9697**The contrast is the point.** On the random walk, ARIMA's MASE (0.9051) is *below* the naive's98(0.9054) and its DM statistic is negative — and the p-value is **0.16**, so the difference is99noise. Nothing beats the naive forecast on a series with nothing in it, and a table of MASE100values alone would have let you claim otherwise. On the seasonal series three methods beat it and101the DM test agrees at 5 %, with ETS at less than half the naive's error.102103⚠️ **Refit cadence is a modelling choice, not an implementation detail** — though it happened to104be small here: refitting the ARIMA 6 times instead of 2 on DGP A moved MASE from 0.9051 to1050.9050, and 18 times instead of 3 on DGP B moved 0.8385 to 0.8380. State the cadence; a result106that only survives at one cadence is a tuned result. ✅ Between refits the demo extends the model107with `res.append(new_obs, refit=False)`, statsmodels' documented way to add observations without108re-estimating.109110🚨 **`ExponentialSmoothing` has no `append`,** so there is no cheap honest way to carry a fit111forward. Freezing the estimated smoothing parameters between refits and re-fitting with112`optimized=False` is a *different* estimator — the initial states come from the heuristic rather113than the likelihood — and it is not a small difference. ✅ Measured on DGP B: **MASE 4.0784**114with the parameters frozen between 24-origin refits against **0.4846** refit at every origin, an1158.4x gap produced entirely by how the walk-forward was wired (on DGP A, 0.9294 against 0.9067).116Refit ETS at every origin and pay the time.117118## 4. Diebold-Mariano: the difference between "smaller MSE" and "better"119120`d[t] = L(e1[t]) - L(e2[t])`; the statistic is `mean(d) / sqrt(V(mean(d)))` with `V` a121Newey-West long-run variance truncated at `h - 1` lags. `harvey=True` applies the122Harvey-Leybourne-Newbold (1997) small-sample factor `sqrt((T + 1 - 2h + h(h-1)/T)/T)` and refers123the result to `t(T-1)`.124125✅ Three checks on the implementation, all printed by the script:126127- **At h = 1 without the Harvey factor it must be a one-sample t-test on `d`** using the128 population variance, so DM / `scipy.stats.ttest_1samp` must equal `sqrt(T/(T-1))`. Measured:129 DM -1.925657, scipy -1.923248, ratio **1.0012523486** against an expected 1.0012523486 —130 agreement to **0.0e+00**.131- **Antisymmetry:** DM(e1, e2) = -2.759902 and DM(e2, e1) = +2.759902, same p-value 0.006139.132- **Size under the null:** two forecasts with equal expected loss, 1,000 runs of T = 250 — it133 rejects **5.8 %** at the 5 % level, against a Monte Carlo standard error of 0.7 %.134135⚠️ **What it does not do.** The null is *equal expected loss*, not "the models are the same". It136assumes the loss differential is covariance-stationary, which nested models estimated on the same137data can violate. And **it compares two forecasts.** A table of pairwise DM tests over ten models138is ten trials: use `arch.bootstrap`'s SPA, StepM or MCS instead — those take **losses**, and139`../../../fin-libraries/skills/lib-arch/SKILL.md` leads with the sign trap that inverts them.140`../../../fin-core/skills/backtest-validation/SKILL.md` owns the correction.141142## 5. statsmodels: two defaults that change the forecast143144✅ Source-verified against the installed statsmodels 0.15.0.145146- 🚨 **An integrated ARIMA has no drift term by default.**147 `statsmodels/tsa/arima/model.py` sets `trend = "c"` when `trend is None and not integrated`,148 and `trend = "n"` otherwise — so `ARIMA(y, order=(1,1,0))` estimates only `['ar.L1',149 'sigma2']`. ✅ Measured on a series drifting +0.04634 per step in sample, the 50-step forecast150 slope is **+0.00007 — a flat line**; passing `trend="t"` recovers **+0.04629**. On a price151 series with a real drift, the default silently forecasts no drift at all.152- ✅ **`ARIMA(y, order=(0,1,0))` IS the naive forecast.** With `trend="n"` it has no mean153 parameters at all, and its one-step forecast equals the last observation to **0.0e+00** across154 four origins. Useful as a sanity anchor: if your ARIMA pipeline does not reproduce the naive155 forecast at order (0,1,0), the pipeline is wrong, not the data.156- ✅ `ARIMA(endog, exog=None, order=(0,0,0), seasonal_order=(0,0,0,0), trend=None,157 enforce_stationarity=True, enforce_invertibility=True, concentrate_scale=False,158 trend_offset=1, ...)`, and `MLEResults.append(endog, exog=None, refit=False,159 fit_kwargs=None)` is the walk-forward extension used above.160- ✅ `ExponentialSmoothing(endog, trend=None, damped_trend=False, seasonal=None, *,161 seasonal_periods=None, initialization_method="estimated", ...)` — `trend` and `seasonal` are162 `None` by default, so a bare call is simple exponential smoothing with neither.163 `statsmodels.tsa.exponential_smoothing.ets.ETSModel` is the newer state-space ETS164 (`error="add"` by default); the two are different implementations of overlapping models.165- ⚠️ `sktime`, `statsforecast`, `pmdarima`, `darts`, `prophet` and `neuralforecast` are **not166 installed here** and nothing about them was checked.167 `../../../fin-core/skills/factor-and-timeseries-research/SKILL.md` chooses between them and168 carries the verified leakage findings in their CV splitters.169170## 6. What the M4 and M5 competitions actually say171172These are the largest public forecasting evaluations, and they are routinely cited for claims173they do not support. ⚠️ Read at the sources below on 2026-09-09; this repo did not re-run them.174175- ⚠️ **M4 (2018, 100,000 series) was won by a hybrid, not by deep learning.** Slawek Smyl's176 ES-RNN mixes exponential smoothing with LSTM layers (Smyl, *IJF* 36(1), 2020, 75-85). The177 second-place method used XGBoost to *weight* forecasts from standard statistical methods.178- ⚠️ **M4's own Finding 5: the six pure ML methods performed poorly** — none was more accurate179 than the `Comb` benchmark, and only one beat `Naive2` (Makridakis, Spiliotis &180 Assimakopoulos, *IJF* 34(4), 2018, 802-808; abstract read at181 https://ideas.repec.org/a/eee/intfor/v34y2018i4p802-808.html). ✅ From the organizers' own182 evaluation code, `Comb` is the equal average of SES, Holt and Damped exponential smoothing,183 and `Naive2` is the naive forecast on a seasonally adjusted series184 (https://github.com/Mcompetitions/M4-methods, `Benchmarks and Evaluation.R`).185- ⚠️ **OWA is normalised by Naive2, not by Comb**: `OWA = 0.5 * [sMAPE/sMAPE(Naive2) +186 MASE/MASE(Naive2)]`, straight from that same evaluation code. The two benchmarks get conflated187 constantly — Comb is what the ML methods failed to beat; Naive2 is the OWA denominator.188- ⚠️ **M5 (2020) is Walmart unit sales, and gradient boosting won it.** The Accuracy track was189 won by an equal-weighted ensemble of 220 LightGBM models, 22.4 % better than the `ES_bu`190 benchmark; only 7.5 % of teams' final submissions beat that benchmark at all. **Deep learning191 did not lose M5**: third place was a pure ensemble of 43 LSTM networks and second place used192 N-BEATS forecasts to adjust LightGBM (Makridakis, Spiliotis & Assimakopoulos, M5 Accuracy193 paper, *IJF* 38(4), 2022, 1346-1364; authors' preprint at194 https://statmodeling.stat.columbia.edu/wp-content/uploads/2021/10/M5_accuracy_competition.pdf).195 ⚠️ The same paper reports the gains concentrating at the top of the hierarchy: ~40 % over the196 benchmark at the most aggregated level and ~3 % at the product-store level, which is where197 most of the series are.198- 🚨 **Neither competition tells you anything about daily equity returns.** M5 is retail unit199 sales. M4 contains a "Finance" domain, but it is scored as a *level* extrapolation with200 sMAPE/MASE/OWA, not as return prediction, and there is no cross-section and no trading.201 ⚠️ Rob Hyndman, writing in the M4 special issue itself: "I know of no large-scale forecasting202 competition for finance data" (*A brief history of forecasting competitions*, *IJF* 36(1),203 2020, 7-14; author's copy at204 https://robjhyndman.com/papers/forecasting-competitions.pdf). ⚠️ The M4 Finance domain is205 reported as 24,534 of the 100,000 series, of which 1,559 are daily — secondhand, from Table 8206 of the sktime M4 replication (arXiv 2005.08067); the primary table was behind a paywall, and207 **what those series contain could not be established.**208209**On N-BEATS, TFT and LSTM for financial returns, this skill carries no verified claim that any210of them beats the naive forecast on daily equity returns.** ⚠️ The nearest thing to a positive211claim found (arXiv 2408.12408, an evaluation of TCN / N-BEATS / TFT / N-HiTS / TiDE / xLSTM on212the S&P 500 and EWZ) reports that on the original undenoised data the models sit near random213guessing, and that only three of the six beat the naive forecast even after wavelet denoising —214N-BEATS and TFT were not among them, and the paper does not say whether the denoising was fitted215before or after the split. ⚠️ Goyal and Welch's out-of-sample study of the equity premium (NBER216w10483; *RFS* 21(4), 2008) found no standard predictor helped an investor outpredict the217prevailing historical mean — monthly and annual data with economic predictors, so it is evidence218about the difficulty of the target rather than about any architecture.219`../../../fin-llm/skills/rl-and-ml-trading/SKILL.md` owns deep-learning trading claims.220221## 7. Traps222223- 🚨 **R^2 on a level, or any metric on a level.** Section 1. Forecast the difference.224- 🚨 **No baseline in the table.** If naive, seasonal-naive, drift and mean are not in the225 comparison, the result is unscored. MASE puts them there by construction.226- 🚨 **A single train/test split.** One split is one draw. Roll the origin, and report the number227 of origins next to the score.228- 🚨 **Fitting anything on the full series before splitting** — scaling, denoising, differencing229 parameters, seasonal decomposition, a stationarity test used to choose `d`. Every one of those230 belongs inside the fold.231 `../../../fin-core/skills/signal-construction/scripts/assert_causal.py` checks the result.232- 🚨 **`trend=None` on an integrated ARIMA.** Section 5: a flat forecast on a drifting series.233- 🚨 **A p-value from a table of models.** DM compares two. More than two needs SPA / StepM / MCS234 and a trial ledger (`../../../fin-core/skills/backtest-validation/SKILL.md`).235- ⚠️ **Accuracy is not profit.** A forecast that beats the naive one on MASE can still lose money236 after costs, and a directional accuracy of 51.7 % on this seed is exactly the base rate of up237 days. `../../../fin-core/skills/signal-construction/SKILL.md` turns a forecast into a position;238 `../../../fin-core/skills/execution-cost-analysis/SKILL.md` prices the trading.239240## 8. Scripts241242- `scripts/forecast_baselines.py` — the two framings of one series and the lag signature, MASE243 and its identity, the rolling-origin tables for both DGPs, the ARIMA and ETS walk-forwards,244 the Diebold-Mariano implementation with its three verification checks, and the statsmodels245 trend defaults. `statsmodels` is imported inside four functions; without it the baselines, MASE246 and the DM test all still run. Seed 0, **41 s** of compute (36-48 s wall clock across runs; the slowest script in247 this plugin).248249## 9. Where this sits250251`../volatility-models/SKILL.md` owns forecasting *variance* (and HAR-RV, which is the baseline252there). `../stat-arb-cointegration/SKILL.md` owns unit roots and stationarity testing between253two series. `../state-space-and-kalman/SKILL.md` owns latent-state models and their254filtered-vs-smoothed rule. `../../../fin-core/skills/factor-and-timeseries-research/SKILL.md`255chooses the library and carries the foundation-model evidence.256`../../../fin-core/skills/backtest-validation/SKILL.md` corrects across many models.257`../../../fin-libraries/skills/lib-arch/SKILL.md` owns `arch.bootstrap`'s SPA / StepM / MCS.258`../../../fin-llm/skills/rl-and-ml-trading/SKILL.md` owns deep-learning trading claims.