The *Variance Ratio Test* is used to test whether a time series follows a [[Random Walk Model|Random Walk]]. It leverages the property that, under a pure random walk, returns are [[Independence and Identical Distribution|i.i.d.]] and thus the [[Variance]] of multi-period returns should scale linearly with the holding period $q$.
## Simple Variance Ratio Test
Consider log returns $r$ at some base frequency (e.g. daily). The multi-period return $r_t^{(q)}$ is defined as the sum of log returns over $q$ periods, or equivalently the natural logarithm of the ratio of asset prices $P_t$ over $P_{t-q}$.
$r_t^{(q)} = \sum_{i=1}^qr_{t-i+1}= \log\left(\frac{P_t}{P_{t-q}}\right)$
Under the assumption of a Random Walk, log returns are i.i.d.. Thus the [[Variance of Sum of Random Variables]] can be written as the sum of individual variances. Additionally assuming constant variance of $r_t$ for all $t$, we can state the following:
$ \mathrm{Var}(r_t^{(q)})=q* \mathrm{Var}(r_t)$
The variance ratio $\mathrm{VR}$ is given by:
$ \mathrm{VR}(q) = \frac{\mathrm{Var}(r_t^{(q)})}{q*\mathrm{Var}(r_t)}$
If the null hypothesis $H_0$ of a Random Walk holds, we expect $\mathrm{VR}(q)=1$ for all $q$. To assess if the empirical variance ratio $\widehat{\mathrm{VR}}(q)$ is significantly different from $1$, we compute the test statistic $Z(q)$,
$ Z(q) = \frac{\mathrm{VR}(q)-1}{\sqrt{\frac{2(2q-1)(q-1)}{3q(T-q)}}}$
$Z(q)$ follows a standard [[Gaussian Distribution|Gaussian]] under $H_0$.
## Heteroskedasticity-Robust Variance Ratio Test
**Weak form Random Walk:** While the *strict Random Walk* assumes i.i.d. returns, financial returns often exhibit heteroskedasticity. Therefore a *weaker form* of the Random Walk is considered, where returns are unpredictable, even if the *variance is time-varying* (violating i.i.d. assumption).
Lo & Mackinlay (1988) proposed a robust version of the VR test to account for this. In this approach, the denominator of the test statistic. is modified, to incorporate an estimate of time-varying variance.
$ \hat \sigma^2(q)=\sum_{j=1}^{q-1}\left(2\hat \delta_j \left(1-\frac{j}{q} \right)\right)$
Here $\hat \delta_j$ represents autocovariances of the squared log returns. The adapted test statistic still follows a standard Gaussian distribution. It writes as follows:
$ Z(q) = \frac{\mathrm{Var}(q)-1}{\sqrt{\hat \sigma^2(q)}}$
## Interpretation of Variance Ratio Test
- $\mathrm{VR}(q)=1$: The series follows a random walk.
- $\mathrm{VR}(q)>1$: The return series exhibits positive autocorrelation (momentum).
- $\mathrm{VR}(q)=1$: The return series exhibits negative autocorrelation (mean reversion).
## Example
**Steps:**
1. *Base variance:* Compute variance of time series for base frequency (e.g. daily)
2. *Aggregated returns:* Compute multi-period returns (over $q$ periods) over the same time horizon.
3. *Aggregated variance:* Compute variance of the multi-period returns.
4. *Variance ratio:* Compute $\mathrm{VR}$ as a fraction between the two variance measures.
5. *Test statistic:* Compute test statistic $Z(q)$ to determine statistical significance.
**Implementation:**
```python
import numpy as np
import pandas as pd
import yfinance as yf
from scipy.stats import norm
def compute_vr(returns: np.ndarray, m: int) -> float:
T = len(returns)
agg = np.array([np.sum(returns[i : i + m]) for i in range(T - m + 1)])
return np.var(agg, ddof=1) / (m * np.var(returns, ddof=1))
def vr_test_simple(returns: np.ndarray, m: int) -> tuple[float, float, float]:
T = len(returns)
vr = compute_vr(returns, m)
theta = 2 * (2 * m - 1) * (m - 1) / (3 * m * T)
z = (vr - 1) / np.sqrt(theta)
return vr, z, 2 * (1 - norm.cdf(np.abs(z)))
def vr_test_robust(returns: np.ndarray, m: int) -> tuple[float, float, float]:
T = len(returns)
rbar = np.mean(returns)
vr = compute_vr(returns, m)
sum_sq = np.sum((returns - rbar) ** 2)
theta = 0.0
for j in range(1, m):
delta = np.sum((returns[j:] - rbar) ** 2 * (returns[:-j] - rbar) ** 2)
theta += (2 * (m - j) / m) ** 2 * (delta / (sum_sq ** 2))
z = (vr - 1) / np.sqrt(theta)
return vr, z, 2 * (1 - norm.cdf(np.abs(z)))
def download_returns(start: str, end: str, ticker: str="AAPL") -> pd.Series:
data = yf.download(ticker, start=start, end=end)
prices = data["Adj Close"]
return np.log(prices).diff().dropna()
def main() -> None:
start_date = "2010-01-01"
end_date = "2023-01-01"
returns = download_returns(start_date, end_date).values
print("N =", len(returns))
for m in [2, 5, 10]:
vr_s, z_s, p_s = vr_test_simple(returns, m)
vr_r, z_r, p_r = vr_test_robust(returns, m)
print(f"m = {m}")
print(f" Simple: VR = {vr_s:.4f}, z = {z_s:.4f}, p = {p_s:.4f}")
print(f" Robust: VR = {vr_r:.4f}, z = {z_r:.4f}, p = {p_r:.4f}\n")
```