TradingView Backtesting

Augmented Dickey-Fuller Test for Trading Spreads

The augmented Dickey-Fuller test asks whether a time series contains a unit root. A sufficiently negative statistic provides evidence against that null under a specified regression model. The constant, trend and lagged differences affect the result. For a trading spread estimated by regressing one price series on another, use Engle-Granger inference: ordinary ADF p-values on those fitted residuals are not valid cointegration p-values.

See the decision guide

What stationarity means for the series you trade

Weak stationarity means a constant finite mean and variance, with covariance determined by the time gap rather than the calendar date. ADF tests one particular departure, a unit root, within a time-series model. A small p-value does not certify constant volatility, absence of breaks, or a profitable trading rule. A Hurst estimate describes a scaling property and is not a substitute for this hypothesis test.

  • For a fixed, prespecified spread s(t) = A(t) - h × B(t), test the sampled spread with justified deterministic terms. State whether prices are levels or logs and whether h was fixed independently of the tested sample.
  • For fitted residuals e(t) = A(t) - a_hat - h_hat × B(t), the same sample estimated the relationship. That estimation must enter the inference. First assess whether the component series are plausibly I(1), meaning their first differences are stationary, before applying the usual two-step cointegration framework.
  • Keep synchronized observations, session definitions and corporate-action treatment. A closed-trade P&L list is not a regularly sampled spread. Dropping unmatched timestamps or forward-filling stale quotes can change the dependence being tested.

Read the ADF regression and its null

Write the regression as Δs(t) = a + b × t + gamma × s(t-1) + Σ[j=1..k] phi(j) × Δs(t-j) + epsilon(t). Some specifications omit a or b. The null is gamma = 0, corresponding to a unit root; the usual stationary alternative has gamma < 0. The statistic is gamma_hat / SE(gamma_hat), but its reference distribution is not the ordinary regression t table for these standard ADF specifications.

  • Choose deterministic terms from the construction and economic hypothesis before searching for significance. Omitting a real trend changes the question; adding an unnecessary trend spends degrees of freedom and can reduce power.
  • Compare the statistic with critical values for that specification and effective sample size. A statistic below the 5% critical value rejects at that level. A p-value is not the probability that the unit-root hypothesis is true.
  • With N observations and k lagged differences, an uninterrupted regression normally uses N - 1 - k rows. ADF(0) has no lagged differences and is the unaugmented Dickey-Fuller case.
Statsmodels settingIncluded deterministic termsMeaning of the stationary alternative
nNeither constant nor trendFluctuation around zero under the maintained model
cConstantFluctuation around an unknown constant mean
ctConstant and linear time trendStationary deviations from a deterministic linear trend

A reproducible trend-and-lag comparison

Construct 200 equally spaced observations using u(0) = 0, u(t) = 0.8 × u(t-1) + z(t), and s(t) = 5 + 0.03 × t + u(t). The code below fixes NumPy seed 93 and draws standard-normal innovations. This is a synthetic trend-stationary design with a fixed initial condition, not market evidence. The same observed series gives the following results with Statsmodels 0.14.6; values are rounded to six decimals.

  • At 5%, the constant-only result changes from rejection at k = 0 to non-rejection at k = 4. The two ct specifications reject, but support deviations around a rising trend, not a constant-mean spread.
  • This table is a sensitivity exercise, not a rule to pick whichever row rejects. Here the construction supplies the trend in advance. For observed data, document the rationale and report all prespecified comparisons.
  • A hand-checkable statistic example uses s = [1, 0, -1, 0, 1, 0, -1, 0, 1], n terms and k = 0. The eight lagged levels have squared sum 4, and their cross-product with Δs is -4. Thus gamma_hat = -1, residual squared sum = 4, residual degrees of freedom = 7, SE = sqrt((4/7)/4), and ADF = -sqrt(7) = -2.645751. Nine constructed observations illustrate arithmetic, not reliable market inference.
TermskRowsADF statisticp-value5% critical value
n0199-0.6999140.412647-1.942431
n4195-0.2539280.593702-1.942462
c0199-3.4009180.010928-2.876176
c4195-2.2755250.179983-2.876479
ct0199-5.0399270.000174-3.432786
ct4195-3.7022070.022212-3.433249

Fitted spread residuals need Engle-Granger critical values

Continue the seeded simulation with x equal to the cumulative sum of 200 new normal draws and y = 2 + 1.5 × x + u. Fit y on x and a constant. Its residuals have an estimated hedge ratio. At k = 4, running an ordinary ADF on those same residuals with n, c and ct produces statistics -3.791572, -3.770199 and -3.786733. Their ordinary p-values are 0.000164, 0.003227 and 0.017244. None is a valid Engle-Granger p-value for this fitted relationship.

  • For the c, k = 4 case, the residual statistic is identical to the no-constant ADF statistic, but the p-value is 0.013911 instead of 0.000164. Estimating the cointegrating regression changes the null distribution even when the test statistic is unchanged.
  • In this two-step implementation, c or ct specifies the first-stage cointegrating equation; its residual ADF regression has no added constant or trend. Choosing ct refits the first stage and changes the relevant critical values. Adding a trend to an already fitted residual ADF is not an equivalent procedure.
  • The cointegration null is no cointegration among the assumed I(1) series. Record the number of component series and which variable is dependent. Testing many pairs, directions or windows and reporting only the smallest p-value adds selection bias that this single-test p-value does not correct.
First-stage termsResidual lags kEG statisticEG p-valueEG 5% critical value
c0-5.1532360.000085-3.367006
c4-3.7915720.013911-3.367006
ct0-5.2089470.000365-3.828667
ct4-3.8372700.043136-3.828667

Run the example and inspect the regression errors

The Python example uses Statsmodels 0.14.6 and NumPy. It fixes k rather than using automatic lag selection. The final regression residuals are epsilon_hat, distinct from the fitted spread e_hat. Inspect their time plot and ACF; a Breusch-Godfrey test can check residual serial correlation at a declared lag range. A clean diagnostic does not establish independence or validate every model assumption.

  • Too few lagged differences can leave serially correlated errors and distort inference. Too many consume observations and coefficient precision. Choose a maximum lag from sampling frequency and plausible short-run dynamics; AIC or BIC can select within it, followed by diagnostic and sensitivity checks.
  • Compare candidate lag specifications on a common estimation window when comparing information criteria manually. Record selected k, usable observations, the lag-selection rule and diagnostic lags. Do not confuse a low ADF p-value with a satisfactory residual check.
Synthetic data only. Ordinary residual ADF p-values are printed to expose the inference error, not to select a pair.python
import numpy as np
from statsmodels.regression.linear_model import OLS
from statsmodels.tsa.stattools import adfuller, coint
from statsmodels.stats.diagnostic import acorr_breusch_godfrey

rng = np.random.default_rng(93)
u = np.zeros(200)
z = rng.normal(size=200)
for t in range(1, 200):
    u[t] = 0.8 * u[t-1] + z[t]
s = 5 + 0.03 * np.arange(200) + u
for terms in ["n", "c", "ct"]:
    for k in [0, 4]:
        stat, p, used, rows, cv = adfuller(
            s, maxlag=k, regression=terms, autolag=None)
        print("ADF", terms, used, rows, stat, p, cv["5%"])

x = np.cumsum(rng.normal(size=200))
y = 2 + 1.5 * x + u
for terms in ["c", "ct"]:
    for k in [0, 4]:
        stat, p, cv = coint(
            y, x, trend=terms, maxlag=k, autolag=None)
        print("EG", terms, k, stat, p, cv[1])
e = OLS(y, np.column_stack([x, np.ones(200)])).fit().resid
for terms in ["n", "c", "ct"]:
    result = adfuller(e, maxlag=4, regression=terms, autolag=None)
    print("NOT EG inference", terms, result[0], result[1])

stat, p, cv, saved = adfuller(
    s, maxlag=4, regression="ct", autolag=None, store=True)
epsilon = saved.resols.resid
# Diagnostic only: examine epsilon and its ACF as well.
lm, lm_p, f, f_p = acorr_breusch_godfrey(saved.resols, nlags=8)
print("Residual BG diagnostic", lm, lm_p)

Recognize low power and invalid inputs

A stationary process close to a unit root can look like a random walk over the available history. For an AR(1) deviation with coefficient 0.99, the conditional mean takes log(0.5)/log(0.99) = 68.97 observations to halve. A 100-observation sample spans only about 1.45 such half-lives. This calculation illustrates slow adjustment; it is not an ADF power estimate or a minimum-sample rule.

  • Non-rejection can reflect low power, misspecified deterministic terms or insufficient history. Rejection can coexist with changing variance, breaks or unstable hedge ratios. Diagnose those features separately; KPSS reverses the stationarity null and can provide another view, but disagreement is not resolved by taking a vote.
  • Constant series, exact deterministic fits, nonfinite values, excessive lag counts and rank-deficient regressions do not support an ordinary finite test statistic. Stop and inspect the data rather than reporting an artificial zero p-value.
  • Nearly identical price series can make cointegration regressions numerically unstable. A software result of negative infinity with p = 0 can be a collinearity convention, not compelling evidence of a tradable spread. Check scaling, duplicate instruments and residual variance.
  • Freeze the hedge ratio and trading rule before evaluation on later data. Rolling refits change the object being tested. ADF does not estimate transaction costs, fill quality or a profitable entry threshold.

Separate TradingView trade review from the statistical test

TradingView exports report tabs separately: List of Trades contains trade records, while Performance Summary contains performance metrics. Pineify Backtest Deep Report parses trade timestamps, prices, quantity and P&L and computes supported metrics such as net profit and profit factor. Those records do not supply both synchronized price histories needed to reconstruct a candidate spread.

  • Keep the original export and reconcile its trades with strategy version, symbol, timeframe, session and cost settings. Obtain the aligned price or spread series separately for the ADF or Engle-Granger calculation.
  • Save the sample dates, transformations, deterministic terms, lag policy, usable rows, test statistic, appropriate p-value or critical values and residual diagnostics. The calculations on this page are independent statistical work, not claimed built-in Backtest Deep Report outputs.
  • Pine Script AI Coding Agent can help draft a chart-side spread definition from explicit symbols, hedge ratio and sampling rules. Review generated code and compare its values with the external statistical dataset before interpreting a result.
Where Pineify fits

Backtest Deep Report

Reconcile TradingView trade records and supported performance metrics before assessing a spread strategy. Run unit-root and cointegration inference separately.

Also useful: Pine Script AI Coding Agent. Specify a spread and its observation timing for a code draft, then verify the generated values against your statistical dataset.

Review Backtest Deep Report

Frequently asked questions

Educational information, not investment advice. All numerical examples use constructed data and are not observed trading performance. A statistical rejection does not guarantee stationarity in future samples or profitable trading.

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.