TradingView Backtesting

Variance Ratio Test: Horizons, Overlap and Inference

A variance ratio test compares the variance of q-period log returns with q times the one-period variance. Under the random-walk variance restriction, the ratio is one. Overlapping returns require an appropriate estimator and test variance; changing volatility calls for heteroskedasticity-robust inference. A ratio below one is not, by itself, evidence of stationarity or a profitable mean-reversion strategy.

See the decision guide

What variance scaling measures

Let x(t) = log P(t), r(t) = x(t) - x(t-1), and Rq(t) = x(t) - x(t-q). With uncorrelated increments and a common unconditional variance, Var[Rq] = q × Var[r]. The population ratio is VR(q) = Var[Rq] / (q × Var[r]). Under covariance stationarity, VR(q) = 1 + 2 × Σ[j=1..q-1](1 - j/q) × rho(j), where rho(j) is the return autocorrelation at lag j.

  • VR above one corresponds to a positive weighted sum of return autocorrelations; below one corresponds to a negative sum. Individual lags can have opposite signs. The interpretation applies to the population relation, while a sample ratio also contains estimation error.
  • The heteroskedastic null allows suitable time-varying volatility with uncorrelated increments under the required moment and dependence conditions. It does not require identical variances at every date. The estimated long-run average variance must still have a valid limit.
  • Failure to reject at selected horizons does not establish independence or rule out nonlinear predictability. Rejection only challenges the tested variance restrictions under the maintained assumptions.

Count overlapping returns and correct the denominator

Define n as the number of one-period returns, not the number of prices or q-period windows. Estimate drift once from the full sample: mu = (x(n) - x(0)) / n. Set e(t) = r(t) - mu and S = Σ[t=1..n] e(t)². For the drift-allowed, overlapping, debiased convention used here, v1 = S / (n - 1), m(q) = q × (n - q + 1) × (1 - q/n), and vq = Σ[t=q..n](x(t) - x(t-q) - q × mu)² / m(q). Then VR(q) = vq / v1. The q scaling is already in m; do not divide vq by q again.

  • Consecutive q-period returns share q - 1 one-period returns. Twenty-five overlapping windows are not twenty-five independent observations; the disjoint-block count is a descriptive comparison, not a replacement degrees-of-freedom formula.
  • Use the same one-period mu inside every q-period difference. Demeaning the overlapping windows by their own sample mean produces a different estimator.
  • The finite-sample denominators address estimator bias under the reference construction. They do not make the ratio itself exactly unbiased or make its finite-sample distribution normal. A simple sample variance of rolling sums divided by q times the sample variance of returns will generally differ.
  • With non-overlapping blocks, both the estimator and inference change, and software may discard a remainder. Do not combine a non-overlapping point estimate with the overlapping standard error below.
QuantityMeaningAt n = 32, q = 8
n + 1Number of log-price observations33
n - q + 1Number of overlapping q-period returns25
m(q)Finite-sample denominator for the scaled long-horizon variance150
floor(n / q)Count of complete disjoint q-period blocks4

Calculate the heteroskedasticity-robust test variance

For j = 1, ..., q - 1, define delta(j) = Σ[t=j+1..n] e(t)² × e(t-j)² / S². Then theta(q) = Σ[j=1..q-1][2(q-j)/q]² × delta(j), SErobust = sqrt(theta), and Zrobust = (VR - 1) / SErobust. The two-sided asymptotic p-value is 2 × [1 - Phi(|Zrobust|)], where Phi is the standard-normal CDF.

  • This delta definition has no factor n and theta estimates the variance of VR - 1. The arch implementation instead includes n in each delta and computes sqrt(n) × (VR - 1) / sqrt(theta_arch). These conventions are equivalent because theta_arch = n × theta. Mixing them gives an erroneous factor sqrt(n).
  • For comparison, the homoskedastic overlapping variance is theta_homo = 2(2q - 1)(q - 1) / (3qn). Changing the robust option leaves this page's VR estimate unchanged; it changes the variance used to assess departure from one.
  • The squared cross-products let observations with large magnitudes affect uncertainty together. Heteroskedasticity robustness does not repair infinite moments, structural breaks, arbitrary serial dependence under the null, or irregular sampling. It is not a blanket guarantee that an estimated p-value is valid.

Recompute the same 32 returns at three horizons

The code supplies all 32 synthetic log returns in chronological order as integers multiplied by 0.001, with x(0) = 4. Larger magnitudes in the second half illustrate why the variance assumption matters; this deterministic teaching sample is not a market observation or a simulated draw from the null. There are 33 log prices, mu = 0.0005625, S = 0.000441875, and v1 = 0.0000142540322580645. Both an independent exact-rational calculation and arch 7.2.0 reproduce the results, rounded below.

  • At q = 2, the exact ratio is 19189 / 17675. At q = 4 it is 126883 / 717605, and at q = 8 it is 31279 / 265125. These fractions provide a check independent of the rounded table.
  • A small q = 8 ratio has a larger standard error than the q = 4 ratio. The point estimate alone cannot identify which horizon supplies stronger evidence against the null.
qWindowsm(q)vq (scaled variance)VR(q)
23158.1250.0000154750001.085658
429101.50.0000025203200.176815
8251500.0000016816670.117978

Compare test uncertainty before interpreting a rejection

The standard errors below are in variance-ratio units. Z values and p-values use the same corrected point estimates. All three p-values are individual two-sided normal approximations; they are not joint-test probabilities.

  • The q = 4 individual p-value is below 0.05, while q = 8 is not. If the inferential family is the three prespecified horizons, a conservative Bonferroni calculation uses min(1, 3 × p): [1.000000, 0.088751, 0.410890]. None rejects at a familywise 5% level under valid individual p-values.
  • The horizons share data and their statistics are dependent. Do not report the smallest individual p-value as the significance of searching several horizons. A joint variance-ratio procedure such as Chow-Denning has its own reference distribution; the Python function below does not implement it.
  • Thirty-two returns, especially with q = 8, provide little information about long-horizon behavior. Bonferroni controls multiplicity only if the underlying p-values are valid; it cannot repair a poor normal approximation. Use a substantially longer justified sample and, where appropriate, a validated null-respecting wild-bootstrap method.
qtheta robustSE robustZ robustp robustZ homoskedastic
20.0353820.1881000.4553830.6488340.484553
40.1431630.378368-2.1756180.029584-2.489079
80.3517410.593077-1.4871950.136963-1.686748

Run the full calculation in Python

This Python 3 function uses the standard library and implements the drift-allowed, debiased, overlapping robust test. It accepts log-price levels and differences them once. Feeding it returns would instead test changes in returns. For raw prices, first verify P > 0 and calculate log(P). The function rejects missing values and constant or numerically unresolved returns; it never deletes observations or fills gaps.

  • For a library comparison, call arch.unitroot.VarianceRatio(log_prices, lags=q, trend="c", overlap=True, debiased=True, robust=True), then inspect vr, stat and pvalue. Explicit options matter when comparing software.
  • The residual scaling cancels from both ratios and squared cross-product fractions. The 1e-12 guard is a numerical resolution policy in log-return units, not a minimum statistical variance requirement. Inspect higher-precision inputs if it triggers.
Complete synthetic inputs and estimator. All printed p-values are asymptotic arithmetic illustrations.python
from itertools import accumulate
from math import erfc, fsum, isfinite, sqrt


def variance_ratio(log_prices, q):
    x = [float(value) for value in log_prices]
    n = len(x) - 1  # n returns, n + 1 log prices
    if isinstance(q, bool) or not isinstance(q, int) or not 2 <= q < n:
        raise ValueError('Require integer 2 <= q < number of returns')
    if not all(isfinite(value) for value in x):
        raise ValueError('Missing or nonfinite log price')
    r = [x[t] - x[t - 1] for t in range(1, n + 1)]
    if not all(isfinite(value) for value in r):
        raise ValueError('Return overflow')
    mu = fsum(r) / n
    e = [value - mu for value in r]
    scale = max(abs(value) for value in e)
    if scale <= 1e-12 * max(1.0, max(abs(value) for value in r)):
        raise ValueError('Constant or numerically unresolved returns')
    # Common scaling cancels in VR and delta; avoids fourth-power overflow.
    u = [value / scale for value in e]
    squares = [value * value for value in u]
    s = fsum(squares)
    v1 = s / (n - 1)
    blocks = [fsum(u[start:start + q]) for start in range(n - q + 1)]
    m = q * (n - q + 1) * (1 - q / n)
    vq = fsum(value * value for value in blocks) / m
    vr = vq / v1
    # delta has NO n factor here; theta estimates Var(VR - 1).
    delta = [fsum(squares[t] * squares[t - j] for t in range(j, n))
             / s**2 for j in range(1, q)]
    theta = fsum((2 * (q - j) / q)**2 * delta[j - 1]
                 for j in range(1, q))
    if not isfinite(theta) or theta <= 0:
        raise ValueError('Undefined test variance')
    se = sqrt(theta)
    z = (vr - 1) / se
    p = erfc(abs(z) / sqrt(2))  # two-sided asymptotic normal p-value
    se_homo = sqrt(2 * (2 * q - 1) * (q - 1) / (3 * q * n))
    return dict(n=n, blocks=len(blocks), m=m, vr=vr, theta=theta,
                se=se, z=z, p=p, z_homo=(vr - 1) / se_homo)


# Complete synthetic log-return sample: each integer is 0.001 log units.
units = [2, 1, -1, 3, 2, -2, 1, 0, 4, 3, -3, -1, 2, 1, -2, 0,
         8, 5, -6, -4, 7, 3, -5, -2, 6, 4, -7, -3, 5, 2, -4, -1]
log_prices = [4.0] + [4.0 + value / 1000 for value in accumulate(units)]

if __name__ == '__main__':
    print('log_prices:', log_prices)
    for horizon in (2, 4, 8):
        print(horizon, variance_ratio(log_prices, horizon))

Define a valid sample before choosing q

Choose the observation clock, date range and horizon grid before inspecting significance. On daily trading-session closes, q = 4 means four observed sessions, not four calendar days. Keep one price definition, corporate-action treatment and session policy throughout the sample.

  • Require integer 2 <= q < n for this debiased estimator. At q = n, m is zero; near n, only a few distinct long-horizon comparisons remain. Mathematical computability is a weaker condition than useful inference.
  • Missing or nonfinite prices must stop this calculation. Investigate the gap, then select and document a suitable contiguous sample or explicitly redefine the sampling clock. Silently deleting missing rows changes what q means; forward-filling can introduce artificial zeros and dependence.
  • Constant prices and constant returns both have zero demeaned variance, so VR is undefined. A numerically tiny variance can also make the result unstable. Report the data issue instead of substituting VR = 1 or p = 1.
  • Non-synchronous observations, bid-ask bounce, stale quotes and session boundaries can create apparent dependence. Compare economically justified sampling frequencies rather than selecting the one with the smallest p-value.
  • Record n returns, n + 1 prices and n - q + 1 overlapping windows for each horizon. Fixed-q asymptotic reasoning uses a growing number of returns, not the fiction of independent overlapping windows; increasing q along with a short sample changes the approximation.

A variance-ratio rejection is different from an ADF rejection

ADF tests a unit-root restriction in an autoregressive regression with specified deterministic terms and lagged differences. A variance-ratio test examines variance scaling across horizons. An integrated process can have correlated increments and fail the random-walk variance restriction while retaining a unit root. Lo and MacKinlay explicitly distinguish their rejection from evidence for stationary mean-reverting price levels.

  • VR below one does not establish that log prices revert to a fixed mean. ADF and VR can disagree without either result being a software error; they ask different questions and rely on different assumptions.
  • Neither test supplies an entry rule, executable fill, position size or cost estimate. Freeze a strategy hypothesis and assess it on later data with fees, slippage and financing. Statistical departure from a random walk does not imply positive tradable returns.

Reconcile TradingView exports before the statistical study

TradingView exports Strategy Report tabs separately. The List of Trades export contains transactions, while Performance Summary contains performance metrics. Pineify Backtest Deep Report reads trade timestamps, prices, quantities and net P&L; its supported outputs include net profit, profit factor and closed-trade equity drawdown. An irregular trade list is not the equally spaced log-price series required here.

  • Keep the original trade CSV, strategy settings and price-history source. Check that symbol, sample dates, session rules and costs match the strategy under review; calculate variance ratios independently on the appropriate price history.
  • The variance-ratio statistic, robust Z and multi-horizon adjustment on this page are external calculations, not claimed built-in Backtest Deep Report fields.
  • Pine Script AI Coding Agent can help draft chart-side research outputs from an explicit formula and confirmed-bar timing. Compare generated series against the external dataset before using them; code generation does not validate a statistical null or establish profitability.
Where Pineify fits

Backtest Deep Report

Review supported trade and performance fields from your TradingView export. Keep the variance-ratio inference with its separate price dataset and assumptions.

Also useful: Pine Script AI Coding Agent. Draft chart research outputs from stated formulas and sampling rules, then verify their values against independent calculations.

Review exported trades

Frequently asked questions

Educational information, not investment advice. All numerical inputs are synthetic. Statistical tests depend on sampling and model assumptions and do not guarantee profitable trading or future price behavior.

Sources and verification

Published by Pineify. Pineify is an independent product and is not affiliated with TradingView. Sources support the technical explanations; examples do not establish trading performance.