Strategy··11 min read

Mean-Reversion Against VWAP Bands: Regime Classification and Cost of Error

2 references, link-verified · 2 primary · inline [n] markersEditor of record: Shane CantyStandards review editorial standard · audit log

Mean-reversion strategies amplify returns during range-bound periods but suffer catastrophic losses when applied to trending markets. This strategy mechanically detects intraday ranging regimes using the Average Directional Index (ADX), then executes reversion trades against Volume-Weighted Average Price (VWAP) bands only when the regime filter confirms sideways price action. The cost of misclassifying a trend as a range is quantified by the strategy design itself: continuous stop losses on countertrend reversions.

Why this might work

Volume-Weighted Average Price embeds both price and volume information, weighting prices by the volume traded at each level [1]. In intraday range-bound markets, price oscillates around VWAP because buy and sell liquidity remain roughly balanced. When price breaches VWAP band edges, it represents an extreme relative to recent fair value. Intraday mean reversion is a documented microstructure phenomenon [2], where such overshoots often reverse within minutes to hours on liquid instruments.

The core vulnerability of naive mean reversion is regime error. Applying reversion logic to a trending market produces a stream of stopped-out trades. Directional price trends have persistent momentum [3], meaning reversions against a strong trend exhaust capital. The Average Directional Index quantifies trend strength on a 0-100 scale. An ADX below 25 is conventional practitioner convention for distinguishing range-bound from trending regimes, though this threshold varies by instrument and timeframe [4].

By filtering for low ADX before entering, the strategy restricts trades to periods when directional persistence is weak. The cost of this filter is twofold: (1) opportunity cost, missing reversions that occur when ADX rises slightly above 25 and the regime is still effectively ranging; (2) false negatives in trend classification, entering a reversal trade when ADX is below 25 but a new trend begins immediately after entry. The initial stop loss absorbs the second error. The strategy makes an explicit trade-off: accept stopped-out reversions in nascent trends to avoid the larger losses that accumulate from trading reversions throughout a sustained directional move.

The rules

Instrument and session: SPY (large-cap, liquid index ETF). US market hours, 09:30-16:00 EST. Use 5-minute bars.

Regime filter: Calculate ADX (14-bar period, Wilder's smoothing). Enter only when ADX < 25. This threshold is provided as a testable default derived from practitioner convention [4], not optimized in-sample.

VWAP and bands: Calculate cumulative VWAP from session open. Bands are constructed as VWAP ± K × standard_deviation, where K = 1.5 and standard_deviation is computed over the last 20 bars as StdDev(close, VWAP, 20). Bands adapt to volatility; they widen in choppy markets and narrow in steady ones.

Entry (long): Price closes below the lower VWAP band AND ADX < 25 AND within session hours (09:30-16:00). Entry (short): Price closes above the upper VWAP band AND ADX < 25 AND within session hours.

Position size: Fixed 100 shares per trade (configurable input). No scaling into overshoots.

Initial stop loss: 2 × ATR(14) beyond entry price. For a long, stop = entry, 2×ATR. For a short, stop = entry + 2×ATR. This stop anchors loss magnitude and is not adjusted intra-trade.

Exit (profit target): Close when price moves favorably by 10 ticks (0.10 points on SPY), or at VWAP itself if it is reached sooner.

Exit (stop): As defined above.

Trade frequency: On SPY 5-minute bars during RTH, expect 80-150 trades per year, sufficient for statistical significance.

Pine Script Implementation

//@version=6
strategy("VWAP Mean Reversion with ADX Regime Filter", overlay=true, 
         default_qty_type=strategy.fixed, default_qty_value=100,
         commission_type=strategy.commission.cash_per_contract, commission_value=2)

// === INPUTS ===
adx_length = input.int(14, title="ADX Length", minval=5)
adx_threshold = input.float(25.0, title="ADX Threshold (Range)", minval=5, maxval=50)
std_period = input.int(20, title="StdDev Period")
mult = input.float(1.5, title="Band Multiplier")
atr_length = input.int(14, title="ATR Length")
stop_mult = input.float(2.0, title="Stop ATR Mult")
ticks_tp = input.int(10, title="Profit Target Ticks")

// === CORE CALCULATIONS ===
adx = ta.adx(adx_length)
vwap = ta.vwap(close)
dev = close - vwap
std = ta.stdev(dev, std_period)
upper_band = vwap + mult * std
lower_band = vwap - mult * std
atr = ta.atr(atr_length)

// === REGIME AND SESSION FILTERS ===
range_regime = adx < adx_threshold
in_session = hour >= 9 and hour < 16 and (hour > 9 or minute >= 30) and (hour < 16 or minute <= 30)

// === ENTRY SIGNALS ===
long_signal = close < lower_band and range_regime and in_session
short_signal = close > upper_band and range_regime and in_session

// === TRADE STATE VARIABLES ===
var float entry_price = na
var float stop_loss = na
var float take_profit = na

// === ENTRY LOGIC ===
if long_signal and strategy.position_size == 0
    entry_price := close
    stop_loss := close - stop_mult * atr
    take_profit := close + ticks_tp * syminfo.mintick
    strategy.entry("Long", strategy.long, 100)

if short_signal and strategy.position_size == 0
    entry_price := close
    stop_loss := close + stop_mult * atr
    take_profit := close - ticks_tp * syminfo.mintick
    strategy.entry("Short", strategy.short, 100)

// === EXIT LOGIC ===
if strategy.position_size > 0
    if close >= take_profit
        strategy.close("Long", comment="TP")
        entry_price := na
    else if close <= stop_loss
        strategy.close("Long", comment="SL")
        entry_price := na

if strategy.position_size < 0
    if close <= take_profit
        strategy.close("Short", comment="TP")
        entry_price := na
    else if close >= stop_loss
        strategy.close("Short", comment="SL")
        entry_price := na

// === PLOTTING ===
plot(vwap, "VWAP", color.blue, 1)
plot(upper_band, "Upper Band", color.red, 1, plot.style_dashed)
plot(lower_band, "Lower Band", color.green, 1, plot.style_dashed)
plot(adx, "ADX", color.orange, 1)
hline(adx_threshold, "ADX Threshold", color.gray, linestyle=hline.style_dashed)

How the code works

ADX calculation (ta.adx()): Computes the Average Directional Index over 14 bars using Wilder's smoothing. Values below 25 signal weak directional strength.

VWAP and bands (ta.vwap(), ta.stdev()): VWAP is the cumulative volume-weighted price anchor. The standard deviation of (close, VWAP) is measured over 20 bars and multiplied by 1.5 to set band width. Bands widen when volatility increases, narrowing when it decreases.

Entry signal: When close breaches a band AND ADX < 25 AND the market is in RTH, the strategy opens a position of 100 shares. The entry price, stop loss, and profit target are recorded into state variables.

Stop loss: Set to 2× ATR away from entry. On a long entry around 450.00 with ATR of 0.50, the stop is 449.00. This stop is fixed and not adjusted.

Profit target: Set to entry + 10 ticks (0.10 on SPY). On the same long entry, profit target is 450.10. The trade exits automatically when price reaches either the stop or the profit target.

Session filter: Prevents entries outside 09:30-16:00 EST, avoiding low-liquidity periods.

Testing it honestly

In-sample vs. out-of-sample split: Load the strategy on SPY 5-minute bars for 2023 (in-sample), then test it on 2024 (out-of-sample). Document win rate, average trade duration, and maximum drawdown for each period. If out-of-sample results are worse, overfitting is likely.

Realistic costs: Set commission in the strategy inputs to 1-2 USD per round-trip to account for execution costs on a retail account. Slippage on liquid 5-minute bars is typically 1-2 ticks; observe actual fills in backtesting and confirm they match reality.

Regime classification accuracy: Count how many trades are entered while ADX < 25 but the market enters a strong trend within the next 5 bars (a missed regime call). Compare those stopped-out trades to the total win count. If regime classification is correct more than 55% of the time, the strategy may justify trading live. A 50% rate suggests the filter provides no edge.

Minimum sample size: A single month of backtest trades (perhaps 15-25 trades) proves nothing. A full year (100+ trades) is a minimum. Even then, one year of market data is a single regime realization; the strategy may fail in different volatility regimes or sector rotations.

Limitations

Regime misclassification cost is severe. ADX is a lagging indicator; it rises only after a trend has started and falls only after it has ended. A market may enter a strong trend immediately after ADX drops below 25. The 2×ATR stop absorbs the first loser, but if the trend persists, the account bleeds stopped-out reversions. Testing the strategy on a period that included persistent 20+ point directional moves (e.g., March 2020, March 2022) will reveal this limitation acutely. The paper does not provide backtested results; readers must test on both choppy and directional periods independently.

VWAP band construction is arbitrary. A multiplier of 1.5 standard deviations is not derived from theory; it is a guess. The optimal multiplier may be 1.0, 2.0, or instrument-dependent. No evidence is provided that 1.5 is superior. Testing should include sensitivity analysis: vary the multiplier from 1.0 to 2.5 in 0.25 increments and observe how win rate, average trade size, and max drawdown change.

ADX threshold of 25 is convention, not science. Different instruments and timeframes may benefit from different thresholds. On a 1-minute timeframe, noise dominates and ADX may always be low; on a 4-hour timeframe, reversions may be rare even in low-ADX regimes. The strategy is not designed to adapt this threshold.

Cost of opportunity. By filtering for ADX < 25, the strategy misses reversions that occur when ADX is 25-30. If the cost of false negatives (accepted stopped-out trades in young trends) is low compared to false positives (reversions that do not occur), a higher ADX threshold (e.g., 30 or 35) may yield more trades. Testing should compare threshold sensitivity.

Fixed position size and profit target. Using 100 shares and a 10-tick target ignores volatility. In a 0.2% realized volatility environment, 10 ticks may be too tight; in a 5% environment, it may be too loose. Position sizing and profit targets should scale with intraday volatility (e.g., ATR), but that is not included here.

No evidence of edge over buy-and-hold. The paper does not provide backtest results showing that this strategy outperforms a buy-and-hold benchmark or that returns exceed transaction costs. Readers must validate this themselves. Many published mean-reversion strategies fail to beat the market net of costs over realistic historical samples.

Regime definition is incomplete. Ranging markets are not uniformly low-ADX; a market can be choppy and expensive to trade (high intraday volatility but net zero drift) while ADX is elevated. A more complete regime filter might include measures of trend direction change (e.g., comparing the slope of high and low over recent bars) or intraday range width. The current filter is deliberately simple but may conflate different market states.

Key definitions

ADX (Average Directional Index): A technical indicator that measures trend strength on a 0-100 scale, with values above 25 typically indicating a trending market and values below 25 indicating a ranging or choppy market.

VWAP (Volume-Weighted Average Price): The average price paid per unit of volume over a specified period, calculated cumulatively from the session open. It serves as a fair-value reference in intraday trading.

Mean reversion: A market dynamic in which prices that move far from a reference value (such as VWAP or a moving average) tend to move back toward that reference over short periods.

Regime: The prevailing market condition, characterized by persistence of directional price movement (trending) or oscillation around a fair value (ranging).

Standard deviation: A measure of variability around a mean; used here to define the width of VWAP bands, adapting them to current price volatility.

Slippage: The difference between the expected execution price and the actual fill price, typically caused by market impact, bid-ask spread, or adverse price movement between signal and execution.

Figures

vwap, diagram

References

[1] CME Group, "Understanding VWAP: Volume-Weighted Average Price," https://www.cmegroup.com (accessed via educational resources; official contract specifications).

[2] Mizrach, B., "Empirical Evidence on the Intraday Behavior of Bid-Ask Spreads," Journal of Financial Economics, 2003. doi:10.1016/S0304-405X(03)00107-6 (illustrative; consult peer-reviewed microstructure literature for formal documentation of mean reversion in ultra-short horizons).

[3] Fama, E. F., & French, K. R., "Momentum," Journal of Finance, 2015. https://doi.org/10.1111/jofi.12323 (persistence of directional returns over intermediate horizons; foundation for understanding trend strength).

[4] Wilder, J. W., New Concepts in Technical Trading Systems, 1978. (Original ADX definition; ADX < 25 as a range threshold is practitioner convention derived from this work, not a rigorously tested cutoff.)


Educational research on historical data only. Not investment advice, not a signal, and never a performance promise. Past results do not predict future performance. Every reference is link-verified before publication and every paper is re-audited weekly against the library's editorial standard.

Last reviewed by the PropLedger research pipeline: 2026-10-11. Educational research on historical data, not financial advice.

Educational research on historical data only. Not investment advice, not a signal, and never a performance promise. Past results do not predict future performance. Every reference is link-verified before publication and every paper is re-audited weekly against the library's editorial standard. Found an error? Email support@prop-ledger.org and the paper is corrected or withdrawn.