6.1 — Foundations: Stationarity, Unit Roots, ACF/PACF, ARIMA as a Baseline

Chapter 6 · Time Series Models for Trading and Risk

Prof. Xuhu Wan

Section 6.1 · Chapter 6 · Learning Statistics with Python

Foundations: Stationarity, Unit Roots, ACF/PACF, ARIMA as a Baseline

Time Series Models for Trading and Risk

Prof. Xuhu Wan

ISOM, HKUST Business School · 2026 Edition

Foundations: Stationarity, Unit Roots, ACF/PACF, ARIMA as a Baseline

You will look at one trading day of Apple twice — as prices and as returns — and predict which one a model is allowed to touch. Then two tests with opposite nulls give you a verdict you can defend, the ACF tells you how much memory a series has, and AIC picks an ARIMA — which you will hold out and score.

One day of Apple, minute by minute

390 one-minute bars for 2 November 2020. Load the close price and look at it.

389 rows after dropping the first return. The price opens near 109.6, climbs above 110.5, then drifts down to 108.8 by the close. Wherever it is at 10:00 tells you a lot about where it will be at 10:01 — the level has memory.

Which of the two is a model allowed to touch?

Which series looks stationary — constant mean and variance over the day?

  • The close price — it stays between 108 and 111 all day
  • The minute return — it oscillates around zero with a stable spread
  • Both — the day is too short for either to drift
  • Neither — intraday data is never stationary

Mean -0.000018, std 0.00108: a flat cloud around zero. The price is the cumulative sum of these; summing a stationary series is what manufactures a wandering level.

Same shocks, two values of a

\(X_t = a\,X_{t-1} + \varepsilon_t\). With \(a = 1\), \(\text{Var}(X_t) = t\sigma^2\) grows forever — the random walk, a unit root. With \(|a|<1\) it settles at \(\sigma^2/(1-a^2)\). Feed identical shocks into \(a = 1\) and \(a = 0.8\) (seed 1, 200 steps). Predict which ends further from zero.

Same 200 shocks: the random walk wanders to -15.8 (std 5.27); the \(a=0.8\) series never leaves [-3.1, 3.1] (std 1.15). The difference between \(a = 0.99\) and \(a = 1\) is the difference between a model and a lottery.

Two flavours of non-stationary, two cures

Trend-stationary \(y_t = g(t) + X_t\): cure by detrending. Difference-stationary \(y_t = y_{t-1}+\varepsilon_t\): cure by differencing.

\(y_t = 0.5\,t + 3\varepsilon_t\). You difference it instead of detrending. What do you get?

  • A random walk — differencing creates a unit root
  • A stationary series, but with a larger variance than the detrended one (over-differenced)
  • Exactly the same residual as detrending
  • A series with a deterministic trend of slope 1

Detrending recovers the shock scale (2.77 ≈ 3); differencing the same series gives 4.06\(3\sqrt2\). Differencing the random walk gives 0.93: exactly the shocks. Match the cure to the disease.

KPSS: the null is “stationary”

kpss(x, regression="c") tests stationarity around a constant; "ct" around a linear trend. Small p ⇒ reject stationarity. Run it on the trend series with both nulls and predict which one rejects.

Around a constant: stat 2.102, p 0.01 — rejected (the trend is not a constant mean). Around a trend: stat 0.058, p 0.1 — not rejected. KPSS reports p only inside [0.01, 0.1]; 0.01 means “≤ 0.01”.

ADF: the null is “unit root”

adfuller estimates \(\Delta y_t = \alpha + \beta t + \gamma\,y_{t-1} + \dots\) and tests \(\gamma = 0\) (i.e. \(a = 1\)). Small p ⇒ reject the unit root.

The trend series fails ADF with a constant (p 0.95) but rejects the unit root once a trend is allowed (p 0.0000). The random walk: ADF p 0.81, KPSS p 0.01 — both agree it is non-stationary. AR(0.8): ADF p 0.0000, KPSS p 0.1 — both agree it is stationary.

The decision table

ADF rejects (p < 0.05) ADF does not reject
KPSS does not reject (p ≥ 0.05) Stationary — model it Not enough data to tell (low power)
KPSS rejects (p < 0.05) Conflict: trend-stationary or structural break — try "ct" Unit root — difference it

Important

Always run both. ADF alone cannot distinguish “stationary” from “too little data to reject”; KPSS alone cannot distinguish “unit root” from “deterministic trend”. The pair, with "c" and "ct", covers the cases.

Apple’s minute prices: a conflict to read

For the close price: KPSS(c) p = 0.01, ADF(c) p = 0.035. What is the honest verdict?

  • Stationary — ADF rejects at 5 %
  • Unit root — KPSS rejects
  • Conflict: near-unit-root; do not model the level, model the returns
  • The tests are broken on intraday data

Return: KPSS p 0.1, ADF stat -25.07, p 0.0000 — unambiguous. The price is the conflict cell; the return is the “model it” cell.

Your turn: is US inflation stationary? Unemployment?

FRED-MD, monthly 1959–2022. The first row holds transformation codes — drop it. Test the CPI level and the unemployment rate.

p_cpi is done. Compute p_unrate, the ADF p-value (constant only) for UNRATE, and compare. Which one is stationary in levels?

CPI p 0.9986 — a unit root (with a trend too: "ct" gives 0.77). UNRATE p 0.011 — stationary: unemployment goes up and comes back down. A feature with a unit root goes into a model only after differencing — remember this in §6.2.

Autocorrelation: a series against its own past

\(\rho_k = \text{corr}(y_t, y_{t-k})\). The ACF plots \(\rho_k\) against \(k\); the PACF plots the correlation at lag \(k\) after removing lags \(1,\dots,k-1\).

ACF and PACF of the airline passengers

For monthly airline passengers (144 months, 1949–1960), which is larger: the ACF at lag 6 or at lag 12?

  • Lag 6 — closer in time is always more correlated
  • Lag 12 — same month last year beats opposite season
  • They are equal — the series is a random walk
  • Both are ≈ 0 — passengers are unpredictable

ACF: 0.95, 0.68, 0.76 — slow decay (trend) with a bump at 12 (season). PACF: 0.95, -0.23, 0.04 — a single spike then nothing: once you know last month, the month before adds little. AR-like level, seasonal echo.

How do you recognise AR versus MA — and the ACF of returns

Pattern ACF PACF Model
AR(\(p\)) decays geometrically cuts off after lag \(p\) past values
MA(\(q\)) cuts off after lag \(q\) decays past errors
White noise all inside \(\pm 2/\sqrt n\) same nothing to model

Dow log returns 2015–2019: all ten autocorrelations inside ±0.056 — the mean has no memory. But \(|r|\) has ACF 0.28, 0.26, 0.27, … — volatility remembers. That second panel is §6.5. It is the whole chapter in one figure.

ARIMA(p, d, q): a 9-fit grid on Dow returns

Difference \(d\) times, then fit ARMA(\(p,q\)): \(\Delta^d y_t = c + \sum\phi_i\,\Delta^d y_{t-i} + \varepsilon_t + \sum\theta_j\,\varepsilon_{t-j}\). Log prices need \(d=1\); returns are already stationary, \(d=0\).

\(p, q \in \{0,1,2\}\), \(d=0\), chosen by AIC on the Dow returns above. Which order wins?

  • (0, 0, 0) — a constant; the market has no linear memory worth two parameters
  • (1, 0, 0) — yesterday’s return matters
  • (2, 0, 2) — the richest model always fits best
  • (0, 0, 1) — bid–ask bounce shows up as an MA(1)

Winner: (0, 0, 0), AIC -8409.8. That is not a failure of ARIMA; it is a fact about the market — and the reason §6.2 exists.

Where ARIMA does work: forecast the passengers

Train on log passengers 1949–1959 (132 months), hold out 1960. Grid \(p, q \in \{0,1,2\}\) with \(d = 1\).

Best by AIC: (2, 1, 1), AIC −228.9. RMSE over 1960: ARIMA(2,1,1) versus the seasonal-naive forecast (1959’s value for the same month). Who wins?

  • ARIMA, easily — it has a likelihood
  • Roughly a tie
  • Seasonal-naive, by a wide margin — the ARIMA has no lag-12 term
  • Neither can forecast 12 months ahead

See the flat forecast

ARIMA RMSE 89.4, seasonal-naive 50.7. The forecast settles between 427 and 440 with bands widening from ±84 to ±188. The bands are honest; the model is blind to the season. Order selection is not validation.

Give it eyes: one seasonal term

SARIMA adds a seasonal difference and a seasonal MA at lag 12: order=(0,1,1), seasonal_order=(0,1,1,12) — the classic “airline model”.

RMSE 18.6 — almost five times better than the non-seasonal fit (89.4) and nearly three times better than the seasonal-naive. Same two-parameter budget, one extra idea. AIC agrees: -441.3 versus -228.9. Where a series has structure, the classical model is unbeatable per parameter.

What you discovered

  • Prices remember; returns forget. A stationary series has time-invariant mean, variance and autocorrelation — the random walk (\(a = 1\)) has none of those.
  • Two diseases, two cures: detrend a trend-stationary series, difference a unit-root series (std 4.06 vs 2.77 when you pick wrong).
  • KPSS (null: stationary) and ADF (null: unit root) must be read together; Apple’s minute price landed in the conflict cell, its returns did not. CPI has a unit root (p 0.999); unemployment does not (p 0.011).
  • Dow returns: every ACF inside ±0.056; AIC picks ARIMA(0,0,0). \(|r|\) remembers — that is §6.5.
  • On passengers, ARIMA(2,1,1) won by AIC but lost the 1960 hold-out to seasonal-naive (89.4 vs 50.7); the airline SARIMA scored 18.6.

Next: §6.2 — score a mean forecast honestly, and see what a gradient-boosting model does with the same series.