Note
Go to the end to download the full example code or to run this example in your browser via JupyterLite.
Fama’s runs test#
Correlation coefficients can be dominated by a few large changes. In his 1965 thesis Eugene Fama added a distribution-free check on the 30 Dow Jones stocks: count the runs, the unbroken sequences of price changes with the same sign. Positive dependence produces long runs and too few of them; reversals produce too many. Under independence the number of runs is approximately normal with a known mean and variance, and Fama found the observed counts close to the expected ones.
Runs in three series#
Independent changes, positively autocorrelated changes (trends), and negatively autocorrelated changes (reversals, such as bid-ask bounce).
rng = np.random.default_rng(1965)
e = rng.standard_normal(1000)
cases = {
"independent": e,
"trending (phi = +0.3)": lfilter([1.0], [1.0, -0.3], e),
"reverting (phi = -0.3)": lfilter([1.0], [1.0, 0.3], e),
}
fig, axes = plt.subplots(3, 1, figsize=(8, 5), sharex=True)
for ax, (name, x) in zip(axes, cases.items(), strict=True):
res = runs_test(x)
ax.bar(np.arange(60), np.sign(x[:60]), color=np.where(x[:60] > 0, "C2", "C3"), width=1.0)
ax.set_yticks([])
ax.set_title(f"{name}: z = {res.statistic:+.2f}, p = {res.pvalue:.3f}", fontsize=9)
axes[-1].set_xlabel("day (first 60 signs shown)")
fig.tight_layout()

The test’s size#
Under independence the statistic is close to standard normal, so the test rejects about 5% of the time.
z = np.array([runs_test(rng.standard_normal(250)).statistic for _ in range(1000)])
fig, ax = plt.subplots()
ax.hist(z, bins=30, density=True, color="0.7", label="1000 independent samples")
grid = np.linspace(-4, 4, 200)
ax.plot(grid, np.exp(-(grid**2) / 2) / np.sqrt(2 * np.pi), "k", label="N(0, 1)")
ax.set_xlabel("runs statistic")
ax.legend()
print(f"rejection rate at 5%: {np.mean(np.abs(z) > 1.96):.3f}")
plt.show()

rejection rate at 5%: 0.051
Total running time of the script: (0 minutes 0.125 seconds)