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") ```