TradingView Backtesting

Mean-Reversion Half-Life: Estimation, Sampling and Failure Cases

Mean-reversion half-life measures how long a fitted model takes to halve a conditional expected deviation from its mean. For x(t) = c + phi × x(t-1) + error(t), with 0 < phi < 1, the exact exponential-decay half-life is log(0.5) / log(phi) sampling intervals. It describes the conditional mean, not the first time a noisy price crosses its mean or a guaranteed trade exit time.

See the decision guide

Fit a level series with a defined equilibrium

Use an equally spaced series whose level has a plausible stable mean: for example, a spread constructed with a specified hedge ratio. Write x(t) = c + phi × x(t-1) + error(t), where future errors have conditional mean zero. When |phi| < 1, the model equilibrium is mu = c / (1 - phi). Iterating the equation gives E[x(t+k) - mu | x(t)] = phi^k × [x(t) - mu]. Stationary variance also requires suitable finite-variance innovations and initialization; a fitted coefficient alone is not a stationarity test.

  • Regress current levels on lagged levels plus a constant. For N observations, use N - 1 aligned pairs: u = x[:-1] and v = x[1:]. Then phi_hat = Σ(u - u_bar)(v - v_bar) / Σ(u - u_bar)^2, and c_hat = v_bar - phi_hat × u_bar.
  • Subtracting u from v gives Δx(t) = c + beta × x(t-1) + error(t), with beta = phi - 1. The two OLS fits agree only when they use the same rows, intercept policy and weights. A negative beta alone is insufficient: beta = -2.2 implies phi = -1.2, an explosive alternating response.
  • The intercept is a per-step offset, not the equilibrium mean. Setting c to zero forces the equilibrium to zero. Demeaning by an estimated full-sample mean also uses estimated information; it does not make the equilibrium known in advance.
  • Specify raw levels, log levels or a spread before fitting. A half-life fitted to returns concerns return persistence, not convergence of the price level. A rolling z-score or changing hedge ratio changes the series being modeled.

Use the logarithm, then classify coefficients outside its domain

For positive phi below one, solve phi^h = 0.5 to obtain h = log(0.5) / log(phi). In the change regression, this is -log(2) / log(1 + beta). Replacing log(1 + beta) with beta gives h_approx = -log(2) / beta, accurate only when beta is close to zero. The fractional h interpolates exponential decay between observations; the first observed step at or below half the initial conditional-mean deviation is ceil(h).

  • At phi = 0.8, exact h is 3.106284 intervals; the shortcut gives 3.465736, about 11.6% larger. At phi = 0.5, exact h is 1 while the shortcut is 1.386294, about 38.6% larger.
  • For phi = -0.8, the expected deviations from an initial value of 1 are -0.8, 0.64, -0.512 and 0.4096. The envelope half-life is 3.106284 intervals, with the first observed magnitude below half at step 4. The sign already changed at step 1.
  • At phi = 0, the positive-coefficient formula tends to zero as a continuous interpolation limit, but sampled data first show the vanished deviation at step 1. Do not report zero elapsed time as an observed event.
Fitted phiConditional-mean responseWhat to report
0 < phi < 1Same-sign geometric decayh = log(0.5) / log(phi); first observed half-threshold at ceil(h)
phi = 0Initial deviation disappears after one stepOne-step loss of memory; do not evaluate log(0)
-1 < phi < 0Sign flips each step while magnitude shrinksEnvelope h = log(0.5) / log(|phi|), explicitly labeled oscillatory
phi = 1Unit-root persistence; c can add driftNo finite decay half-life and no identified finite equilibrium from c / (1 - phi)
phi > 1Deviation growsNo decay half-life
phi ≤ -1Alternating response with constant or growing magnitudeNo decay half-life

Recalculate a complete thirteen-observation example

Use this constructed level series in chronological order: [12, 14, 13, 11, 10, 9, 11, 12, 11, 10, 9, 10, 11]. Assume one observation per trading session, with no missing sessions. These values are a teaching fixture, not market prices or evidence of a tradable relationship. The twelve lagged/current pairs give Σu = 132, Σv = 131, Σu² = 1478 and Σuv = 1456. Therefore Sxx = 1478 - 132² / 12 = 26 and Sxy = 1456 - 132 × 131 / 12 = 15.

  • The fitted conditional-mean deviation retains 57.6923% after one session and 33.2840% after two. Future realized values include new errors and need not follow that path.
  • Using observations 1 through 7 produces phi = 0.571429 and h = 1.238613. Using observations 7 through 13 produces phi = 0.454545 and h = 0.879118. Each smaller window has only six regression pairs and an estimated intercept. These differences illustrate sensitivity, not a change-point test.
CalculationResultInterpretation
phi_hat = 15 / 260.576923Estimated one-session persistence
beta_hat = phi_hat - 1-0.423077Slope in the change regression
c_hat = 131 / 12 - (15 / 26) × 114.570513Intercept, not the equilibrium
mu_hat = c_hat / (1 - phi_hat)10.803030Fitted equilibrium level
h = log(0.5) / log(15 / 26)1.260161 sessionsFractional exponential-decay half-life
ceil(h)2 observations aheadFirst sampled conditional-mean magnitude at or below half

Run the OLS calculation without external Python packages

This Python 3 example calculates both regressions and classifies the resulting coefficient. It rejects nonfinite values, fewer than four observations and constant lagged levels. The four-observation requirement only leaves residual degrees of freedom for the two fitted coefficients; it is not an adequate sample-size recommendation. Supply already synchronized, equally spaced observations and a positive interval in your chosen time unit.

Standard-library Python; exact sequence above. No market download, trading signal or confidence interval is implied.python
from math import ceil, fsum, isclose, isfinite, log


def decay(phi, interval=1.0):
    if not isfinite(phi) or not isfinite(interval) or interval <= 0:
        raise ValueError("Finite coefficient and positive interval required")
    if phi == 0:
        return {"kind": "one-step memory loss", "half_life": None,
                "first_sample_step": 1, "first_sample_time": interval}
    if abs(phi) >= 1:
        return {"kind": "no decay", "half_life": None,
                "first_sample_step": None, "first_sample_time": None}
    h = log(0.5) / log(abs(phi))
    step = ceil(h)
    return {"kind": "monotone" if phi > 0 else "oscillatory envelope",
            "half_life": h * interval, "first_sample_step": step,
            "first_sample_time": step * interval}


def fit_ar1(values, interval=1.0):
    s = [float(z) for z in values]
    if len(s) < 4 or not all(isfinite(z) for z in s):
        raise ValueError("Need at least four finite observations")
    u, v = s[:-1], s[1:]
    n = len(u)
    ubar, vbar = fsum(u) / n, fsum(v) / n
    xx = fsum((z - ubar)**2 for z in u)
    if xx == 0:
        raise ValueError("Constant lagged levels: slope unidentified")
    phi = fsum((a - ubar) * (b - vbar)
               for a, b in zip(u, v)) / xx
    c = vbar - phi * ubar
    differences = [b - a for a, b in zip(u, v)]
    dbar = fsum(differences) / n
    beta = fsum((a - ubar) * (d - dbar)
                for a, d in zip(u, differences)) / xx
    assert isclose(beta, phi - 1, rel_tol=1e-10, abs_tol=1e-12)
    result = {"pairs": n, "c": c, "phi": phi, "beta": beta,
              "mu": c / (1 - phi) if abs(phi) < 1 else None}
    result.update(decay(phi, interval))
    return result


series = [12, 14, 13, 11, 10, 9, 11, 12, 11, 10, 9, 10, 11]
for name, sample in [("all", series), ("first", series[:7]),
                     ("last", series[6:])]:
    print(name, fit_ar1(sample))

for phi in [0.8, 0.5, 0.0, -0.8, 1.0, 1.2, -1.0, -1.2]:
    print("coefficient case", phi, decay(phi))

full = fit_ar1(series)
assert isclose(full["phi"], 15 / 26)
assert isclose(full["half_life"], 1.2601614337478544)
assert decay(0.5)["first_sample_step"] == 1
assert decay(0.0)["first_sample_step"] == 1
assert decay(-0.8)["kind"] == "oscillatory envelope"
assert decay(1.0)["half_life"] is None
assert decay(-1.2)["half_life"] is None

Connect sampling frequency to the Ornstein-Uhlenbeck process

For dX = kappa × (mu - X) dt + sigma dW with kappa > 0, the Ornstein-Uhlenbeck conditional mean decays as exp(-kappa × elapsed time). Exact observations separated by a fixed interval Δ follow an AR(1) transition with phi = exp(-kappa × Δ) and c = mu × (1 - phi). Thus kappa = -log(phi) / Δ and the half-life in clock units is log(2) / kappa = Δ × log(0.5) / log(phi). A scalar OU model with positive real kappa cannot generate a negative one-step phi.

  • Under the same correctly specified OU model, phi = 0.8 at one-minute spacing becomes 0.8^5 = 0.32768 at five-minute spacing. Half-life changes from 3.106284 one-minute intervals to 0.621257 five-minute intervals; both equal 3.106284 minutes. The first available five-minute observation is already beyond that fractional half-life.
  • The Euler approximation uses phi ≈ 1 - kappa × Δ, giving beta ≈ -kappa × Δ. This explains the -log(2) / beta shortcut. An exact discrete regression does not require this approximation.
  • Observed estimates at different frequencies need not agree. Microstructure noise, aggregation, changing sessions and model misspecification can change the fit. Report trading-session units separately from elapsed calendar days; overnight and weekend gaps break uniform clock spacing.
  • For irregular timestamps, use the actual gaps in an appropriate continuous-time likelihood or resample under a documented rule. Treating every irregular pair as one identical interval gives an ambiguous time scale.

Check uncertainty before using half-life in a strategy

A finite-sample AR coefficient can be biased, particularly when persistence is high and the mean is estimated. The nonlinear transform amplifies small changes near one: phi = 0.95, 0.98 and 0.99 imply half-lives of 13.513407, 34.309618 and 68.967564 intervals. More decimal places do not repair weak identification. An equilibrium estimated as c / (1 - phi) is also unstable near one.

  • Prespecify the estimation window and compare reasonable neighboring windows. Save sample dates, number of usable pairs, intercept policy, transformations and residual diagnostics. Inspect residual serial correlation and changing variance; persistent residuals can indicate that AR(1) is too simple.
  • Estimate uncertainty with a method suitable for the fitted process and its persistence. For a valid coefficient interval wholly inside (0, 1), transform both endpoints with the exact log formula. If the interval reaches one or includes nondecaying values, a finite bounded half-life interval is not justified. Do not silently discard invalid bootstrap fits.
  • A point estimate below one does not reject a unit root. Assess stationarity, structural breaks and spread construction separately. If a hedge ratio was fitted, retain that estimation step in inference and out-of-sample evaluation.
  • Keep future data out of rolling estimates. A shorter half-life is a model property, not evidence of larger net returns. Entry thresholds, costs, stop rules and execution still require a separately specified backtest.
  • A first-passage time asks when a random path first reaches a boundary. It depends on the starting distance, volatility and boundary, and has a distribution. Half-life only measures conditional-mean decay; it is neither an expected first crossing nor a deterministic holding period.

Keep spread estimation separate from trade-report review

TradingView exports data from the selected Strategy Tester tab. List of Trades records transactions; Performance Summary records performance metrics. Pineify Backtest Deep Report parses trade timestamps, prices, quantity and P&L and calculates supported metrics including net profit and profit factor. A list of completed trades does not contain the regularly sampled spread history needed for this AR(1) fit.

  • Obtain the synchronized price or spread series separately. Preserve the observation calendar, price adjustment policy, hedge ratio and strategy version. Reconcile the trade export with the same symbol, timeframe and test dates.
  • Run the half-life calculation independently and attach its inputs, coefficient classification and window comparison to your research notes. This page does not claim that Backtest Deep Report contains an AR(1) or OU half-life estimator.
  • Pine Script AI Coding Agent can help draft a chart-side spread or strategy from explicit rules. Review the generated code, check sampling and compare its values with the external dataset before relying on the backtest.
Where Pineify fits

Backtest Deep Report

Review TradingView trade records and supported performance metrics alongside a separately computed half-life estimate.

Also useful: Pine Script AI Coding Agent. Draft a spread or strategy from declared observation and trading rules, then verify its generated values and behavior.

Review Backtest Deep Report

Frequently asked questions

Educational information, not investment advice. The sequence and numerical examples are constructed illustrations, not observed trading performance. A fitted mean-reversion model does not guarantee convergence, a particular exit time 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.