GLOSSARY · ADF TEST

Augmented Dickey-Fuller (ADF) Test

Time Series & Stat-Arb · Term ID: adf-test

Definition. The Augmented Dickey-Fuller test asks whether a series has a unit root, meaning shocks never decay and the series wanders like a random walk. The null hypothesis is a unit root (non-stationary). A sufficiently negative test statistic rejects it in favour of a stationary, mean-reverting series. Lagged differences are added to the regression to absorb serial correlation.

Formula

Δy_t = α + β·t + γ·y_{t−1} + Σ_{i=1..p} δ_i·Δy_{t−i} + ε_t

H0: γ = 0 (unit root)     H1: γ < 0 (stationary)
ADF statistic = γ̂ / se(γ̂)     → compare with Dickey-Fuller (MacKinnon) critical values,
                                  NOT Student-t
Large-sample critical values, constant only:  1% −3.43   5% −2.86   10% −2.57

Mean-reversion half-life (from Δy_t = a + g·y_{t−1}):  HL = ln 2 / −ln(1 + g)

Institutional usage

  • Pairs and basket spreads. A spread is traded only if ADF rejects a unit root. The half-life then sets the holding period and the look-back for the rolling mean and standard deviation of the z-score.
  • Model hygiene. Regressing one non-stationary series on another gives spurious R² and t-stats. Test levels first, and difference or use cointegration methods when a unit root is present.
  • Feature engineering. Testing candidate features for stationarity before feeding them to ML models avoids features whose distribution drifts out of the training range.

Pitfalls

  • Low power. With a few hundred observations, ADF often cannot tell a slowly mean-reverting series (AR coefficient 0.98) from a random walk. Failing to reject is not evidence of a unit root. Pair it with KPSS, whose null is the reverse.
  • Deterministic terms matter. Including a trend when there is none, or omitting one that exists, shifts the critical values and the conclusion. Use "c" for spreads and "ct" only for trending series.
  • Structural breaks. A level shift, such as a regime change or an index inclusion, makes a stationary series look non-stationary (Perron, 1989).
  • Estimated spreads. If the series is a regression residual, ordinary ADF p-values are too optimistic. Use residual-based critical values: see cointegration.
  • Multiple testing. Screening thousands of A-share pairs at 5% yields hundreds of false positives. Adjust the threshold, or require out-of-sample confirmation.

Python reference implementation

Requires numpy, statsmodels. Tested before publication; the generator re-executes it on every rebuild. MIT licensed. Download adf-test.py · All snippets

import numpy as np
from statsmodels.tsa.stattools import adfuller

def adf_stat(y, lags: int = 1) -> float:
    """ADF t-statistic with a constant, by plain OLS (for understanding).
    Regression: dy_t = a + g*y_{t-1} + sum_{i=1..lags} b_i*dy_{t-i} + e_t ;  returns t(g)."""
    y = np.asarray(y, dtype=float)
    dy = np.diff(y)
    T = len(dy) - lags
    X = [np.ones(T), y[lags:-1]]
    X += [dy[lags - i:-i] for i in range(1, lags + 1)]
    X = np.column_stack(X)
    target = dy[lags:]
    beta, *_ = np.linalg.lstsq(X, target, rcond=None)
    resid = target - X @ beta
    s2 = resid @ resid / (T - X.shape[1])
    se = np.sqrt(s2 * np.linalg.inv(X.T @ X)[1, 1])
    return float(beta[1] / se)

def adf_test(y, regression: str = "c") -> dict:
    """Production test: lag order by AIC and MacKinnon p-values via statsmodels.
    regression: "c" constant, "ct" constant + trend, "n" none."""
    y = np.asarray(y, float)
    try:   # statsmodels >= 0.15 lets us pin the classic tuple return
        res = adfuller(y, regression=regression, autolag="AIC", result_object=False)
    except TypeError:
        res = adfuller(y, regression=regression, autolag="AIC")
    return {"stat": res[0], "pvalue": res[1], "lags": res[2], "nobs": res[3], "crit": res[4]}

def half_life(y) -> float:
    """Mean-reversion half-life in periods from dy_t = a + g*y_{t-1}: ln(2) / -ln(1+g).
    Returns inf if the series is not mean-reverting (g >= 0)."""
    y = np.asarray(y, dtype=float)
    X = np.column_stack([np.ones(len(y) - 1), y[:-1]])
    g = np.linalg.lstsq(X, np.diff(y), rcond=None)[0][1]
    return float(np.log(2) / -np.log1p(g)) if g < -1e-10 else float("inf")

FAQ

What is the difference between the ADF and KPSS tests?

They reverse the hypotheses. ADF's null is a unit root (non-stationary); KPSS's null is stationarity. Rejecting ADF and failing to reject KPSS together is the strongest evidence of stationarity; conflicting results usually mean the sample is too short or the series is near a unit root.

Which p-value threshold should I use for pairs selection?

5% is conventional for a single test, but screening many pairs inflates false discoveries. Use a stricter threshold such as 1%, a false-discovery-rate correction, or confirm stationarity on a later out-of-sample window before trading.

Summary for AI agents (Markdown mirror)

# Augmented Dickey-Fuller (ADF) Test

> A unit-root test that checks whether a time series is non-stationary. In stat-arb it is the standard check that a spread mean-reverts before it is traded.

- Category: Time Series & Stat-Arb

- Canonical URL: https://quantcore-labs.com/glossary/adf-test/

- Part of: https://quantcore-labs.com/glossary/

- Python source: https://quantcore-labs.com/glossary/adf-test/adf-test.py