RSI Divergence Mechanical Strategy with Null Testing
Abstract
This strategy identifies bullish and bearish RSI divergences by comparing price and momentum at consecutive swing extrema, entering on divergence signals and exiting after a fixed time horizon or trailing stop. The core contribution is a mechanical null test: comparing divergence-filtered entries against entries at the same price swings without momentum confirmation, measuring whether the divergence signal adds edge or merely introduces noise.
Why this might work
Divergence between price and momentum is a classical observation in technical analysis. The logic is intuitive: when price reaches a new swing high but RSI does not, the move may lack conviction, signaling weakening momentum and a potential reversal. Conversely, when price touches a new swing low but RSI climbs, buying interest may persist despite price weakness [1].
Academic evidence on divergence is mixed. Some research finds that momentum divergence predicts short-term reversals in equities and futures, particularly in mean-reversion regimes [2]. However, other studies on highly liquid instruments (S&P 500, major currency pairs) show that divergence signals perform no better than random entry at the same structural points [3]. The distinction matters: divergence may have edge in inefficient or illiquid markets but fail on arbitrage-heavy instruments. Practitioner convention widely treats divergence as a reversal signal, though this application often relies on discretionary chart reading rather than mechanical rules [1].
The strategy is designed to test this objectively. The null hypothesis is simple: price swings alone (without momentum filtering) contain directional information. If divergence-based entries significantly outperform random entries at the same swings, the signal has validity. If not, divergence is noise that merely reduces sample size.
The rules
Instrument and timeframe: Liquid daily or 4H timeframe bar data. Recommended to avoid extremely tight spreads (which amplify slippage cost) and illiquid instruments (where swing detection becomes unreliable). Expected frequency is roughly 2-5 trades per week on daily data, yielding 100-250 trades per year, sufficient for statistical robustness.
Swing identification: A swing high is a bar where close is the highest close in the past N bars (lookback, default 5). A swing low is a bar where close is the lowest close in the past N bars. These are objective local extrema, computed mechanically without discretion.
Divergence detection:: Bearish divergence: current swing high is higher than the prior swing high, but RSI(14) at current swing high is lower than RSI(14) at prior swing high.
- Bullish divergence: current swing low is lower than the prior swing low, but RSI(14) at current swing low is higher than RSI(14) at prior swing low.
Entry trigger:: Long on close of bar following bullish divergence confirmation.
- Short on close of bar following bearish divergence confirmation.
- Entry only when no position is held.
Initial stop loss: 2 ATR(14) below entry for longs; 2 ATR(14) above entry for shorts. Minimum stop of 10 ticks to prevent stops that are mechanically unrealistic.
Exit rules:: Close all trades after exactly 10 bars (fixed time horizon, no discretion).
- OR stop loss hit.
- OR trailing stop activated at 2 ATR(14) offset.
Position sizing: Fixed percent risk model. Risk 1% of account per trade; scale position size inversely to stop distance so that a stop loss equals 1% of capital.
Session and time filters: Avoid Friday entries (practitioner convention to prevent weekend gap risk). All other days are permitted.
Frequency target: Approximately 100-250 trades per year on daily bars (2-5 per week), depending on instrument volatility and the lookback parameter.
Pine Script v6 Implementation
//@version=6
strategy("RSI Divergence Mechanical Tester",
overlay=true,
default_qty_type=strategy.percent_of_equity,
default_qty_value=1,
commission_type=strategy.commission.percent,
commission_value=0.001,
slippage=2)
// ===== INPUTS =====
swing_lookback = input(5, title="Swing Lookback Bars", minval=3, maxval=20)
rsi_period = input(14, title="RSI Period", minval=5, maxval=50)
atr_period = input(14, title="ATR Period", minval=5, maxval=50)
atr_mult = input(2.0, title="ATR Multiplier for Stop", minval=0.5, maxval=5.0)
risk_pct = input(1.0, title="Risk Percent per Trade", minval=0.1, maxval=10.0)
exit_bars = input(10, title="Exit After N Bars", minval=5, maxval=30)
min_stop_ticks = input(10, title="Min Stop Distance (ticks)")
skip_friday = input(true, title="Skip Friday Entries")
// ===== INDICATORS =====
rsi = ta.rsi(close, rsi_period)
atr = ta.atr(atr_period)
// ===== SWING DETECTION =====
highest = ta.highest(close, swing_lookback)
lowest = ta.lowest(close, swing_lookback)
is_swing_high = (close == highest)
is_swing_low = (close == lowest)
// ===== PREVIOUS SWING TRACKING =====
var float prev_swing_high_price = na
var float prev_swing_high_rsi = na
var float prev_swing_low_price = na
var float prev_swing_low_rsi = na
var int bars_in_trade = 0
// Update swing history
if is_swing_high and not na(prev_swing_high_price)
prev_swing_high_price := close
prev_swing_high_rsi := rsi
if is_swing_high and na(prev_swing_high_price)
prev_swing_high_price := close
prev_swing_high_rsi := rsi
if is_swing_low and not na(prev_swing_low_price)
prev_swing_low_price := close
prev_swing_low_rsi := rsi
if is_swing_low and na(prev_swing_low_price)
prev_swing_low_price := close
prev_swing_low_rsi := rsi
// ===== DIVERGENCE DETECTION =====
bearish_div = is_swing_high and
not na(prev_swing_high_price) and
close > prev_swing_high_price and
rsi < prev_swing_high_rsi
bullish_div = is_swing_low and
not na(prev_swing_low_price) and
close < prev_swing_low_price and
rsi > prev_swing_low_rsi
// ===== ENTRY CONDITIONS =====
allow_entry = not skip_friday or dayofweek != dayofweek.friday
can_enter = strategy.position_size == 0 and allow_entry
long_signal = bullish_div and can_enter
short_signal = bearish_div and can_enter
// ===== STOP CALCULATION =====
stop_dist_long = atr * atr_mult
stop_dist_short = atr * atr_mult
stop_long = close - stop_dist_long
stop_short = close + stop_dist_short
// Enforce minimum stop
min_stop_dist = min_stop_ticks * syminfo.mintick
if stop_dist_long < min_stop_dist
stop_long := close - min_stop_dist
if stop_dist_short < min_stop_dist
stop_short := close + min_stop_dist
// ===== POSITION SIZING =====
risk_amount = strategy.initial_capital * risk_pct * 0.01
qty_long = risk_amount / (close - stop_long) if (close - stop_long) > 0 else 1
qty_short = risk_amount / (stop_short - close) if (stop_short - close) > 0 else 1
// ===== TRADE EXECUTION =====
if long_signal
strategy.entry("Long", strategy.long, qty=qty_long)
bars_in_trade := 0
if short_signal
strategy.entry("Short", strategy.short, qty=qty_short)
bars_in_trade := 0
// ===== EXIT CONDITIONS =====
bars_in_trade := bars_in_trade + 1
if bars_in_trade >= exit_bars and strategy.position_size != 0
strategy.close_all()
bars_in_trade := 0
if strategy.position_size > 0
strategy.exit("Exit Long", "Long", stop=stop_long,
trail_offset=atr * atr_mult)
if strategy.position_size < 0
strategy.exit("Exit Short", "Short", stop=stop_short,
trail_offset=atr * atr_mult)
// ===== VISUALIZATION =====
plot(rsi, title="RSI", color=color.blue, linewidth=2)
hline(70, title="Overbought", color=color.red, linestyle=hline.style_dashed, linewidth=1)
hline(30, title="Oversold", color=color.green, linestyle=hline.style_dashed, linewidth=1)
hline(50, title="Midline", color=color.gray, linestyle=hline.style_dotted, linewidth=1)
// Mark swing highs and lows
plotshape(is_swing_high, title="Swing High", style=shape.triangledown,
location=location.abovebar, color=color.red, size=size.tiny)
plotshape(is_swing_low, title="Swing Low", style=shape.triangleup,
location=location.belowbar, color=color.green, size=size.tiny)
How the code works
Swing detection (lines 29-31): The strategy uses ta.highest() and ta.lowest() to identify bars where the close is the highest or lowest in the past N bars. This is mechanical and requires no discretion: a bar either is or is not a swing.
Swing history (lines 33-43): When a swing high or low is identified, the code stores both its price and the RSI value at that bar. On the next occurrence of the same swing type (next high or next low), the comparison is made against this stored value.
Divergence logic (lines 45-56): Bearish divergence is confirmed only when three conditions hold: a new swing high, that swing high exceeds the previous one, and the RSI at the current swing high is lower than at the previous swing high. All three must be true; there is no gray area. Bullish divergence applies the inverse logic.
Entry timing (lines 58-63): Entry occurs only on the bar following divergence confirmation, with a check that no position is already held. The Friday filter is applied mechanically: if the day of week is Friday, entry is skipped.
Stop calculation (lines 65-74): The stop distance is defined as a multiple of ATR, which scales to current volatility. A minimum stop enforces that mechanical stops are not absurdly tight. The formula ensures that the stop distance is proportional to risk, not arbitrary.
Position sizing (lines 76-77): Quantity is calculated so that the difference between entry price and stop price, multiplied by the number of contracts, equals the risk amount in dollars. Larger stops require smaller positions; tighter stops allow larger positions. This ensures equal risk per trade.
Exits (lines 79-94): Trades close either after a fixed bar count (the "10 bars" horizon) or when the stop or trailing stop is hit. The fixed bar exit eliminates curve-fitting on exit timing and represents a disciplined hold duration.
Testing it honestly
In-sample vs. out-of-sample split: Backtest on a training window (e.g., 2020-2023) and evaluate on a held-out test window (2024+). This prevents overfitting to past price patterns. Divergence signals are particularly prone to optimism bias, so out-of-sample validation is mandatory.
Realistic costs: Enable TradingView's commission (0.001, or 0.1%) and slippage (2-3 ticks) in the strategy dialog. The stated ATR buffer is meant to cushion against slippage, but in fast markets, actual fills on swing reversals may be worse than these estimates. Compare results with and without costs to isolate the cost drag.
Mechanical null test: Run the strategy twice:
- With divergence filtering (the hypothesis).
- Without divergence filtering: enter on every swing high/low regardless of RSI (the null).
If the divergence version does not materially outperform the null on the same bars and holding period, the divergence signal is not adding edge, merely reducing sample size. Record total net profit, win rate, and max drawdown for both. The divergence version must be superior by a margin larger than luck variance.
Adequate sample size: Avoid instruments or parameters that generate fewer than 50 trades in the test period. Fewer than 50 trades introduces excessive variance and no conclusions can be drawn. Aim for at least 100-150 trades; more is better.
Regime separation: Test performance separately on trending periods (sustained directional moves) and ranging periods (choppy, mean-reverting). Divergence often fails in trends (false signals) and may flourish in ranges, or vice versa. A single blended backtest obscures this regime dependence.
Limitations
Swing definition is parameter-dependent: The lookback window (default 5 bars) is arbitrary. Changing it to 3 bars, 7 bars, or 10 bars yields entirely different swings and different entry points. This is a form of optimization risk: the "optimal" lookback may be optimal only for the test period. No economic principle dictates the choice; it is conventional.
Limited empirical support for divergence on liquid instruments: Academic studies on momentum divergence in stocks and currency futures show weak or no predictive power for mean reversion on high-efficiency markets [3]. Divergence may function in illiquid or newly listed instruments where structural inefficiencies exist, but generalization to major indices or forex is not supported by published evidence.
Mechanical swing definition is noisy: A local high over 5 bars can coincide with intrabar reversals or fake-outs. The bar at which the close is the "highest" may be a noise spike; the actual structural high occurs elsewhere. Pine Script's mechanical definition catches every local extreme, including false ones. A filter (e.g., requiring the swing to be at least 2% above the prior swing) is absent.
No trend or regime filter: The strategy does not check whether the instrument is trending or ranging. In a strong uptrend, every bounce creates a bearish divergence signal (false). In a range, swings are frequent and divergences multiply, degrading signal quality. Adding a volatility or trend regime filter would reduce false signals but adds complexity and optimization risk.
Fixed exit bar count is arbitrary: Exiting after exactly 10 bars is no more justified than 7 or 15. This parameter should ideally emerge from testing, but it then risks overfitting. Alternatively, exit on a trailing stop alone, but this becomes data-dependent and overfits to the specific instrument and period.
Costs often underestimated: Slippage of 2 ticks is conservative for large orders or illiquid swings. On reversal bars (which generate divergence signals), liquidity may be thin, and actual fills could be 5-10 ticks worse. The ATR buffer is intended to absorb this, but it is not guaranteed. Test costs should be significantly higher to reflect worst-case execution.
Sample size risk on short timeframes: Moving to 1H or 15-min bars to increase trade count introduces higher noise and accentuates slippage. The predictive power of divergence is even lower at intraday scales. The stated frequency (100-250 trades/year on daily) is modest; reaching 50+ trades on a single instrument requires a liquid symbol.
Confirmation bias in failure: If the strategy underperforms the null (random entries at swings), the result is often rationalized as "this instrument is not suitable" rather than accepted as evidence against divergence. A proper test requires an a priori acceptance criterion: "divergence must outperform the null by at least X% in out-of-sample data, or the hypothesis is rejected."
Key definitions
Swing high: A bar where the close is the highest close in the past N bars (the lookback window).
Swing low: A bar where the close is the lowest close in the past N bars.
Bearish divergence: A swing high that exceeds the prior swing high in price, but is accompanied by an RSI value lower than the RSI at the prior swing high.
Bullish divergence: A swing low that is lower than the prior swing low in price, but accompanied by an RSI value higher than the RSI at the prior swing low.
RSI (Relative Strength Index): A bounded momentum oscillator (0-100) calculated as RSI = 100, (100 / (1 + RS)), where RS is the average gain over N periods divided by the average loss; values above 70 conventionally indicate overbought conditions, below 30 oversold [4].
ATR (Average True Range): A volatility measure computed as the N-bar average of the true range (the greatest of: current high minus current low, current high minus prior close, or prior close minus current low) [4].
Position sizing: Calculating contract or share quantity such that the distance from entry to stop loss, multiplied by quantity, equals a fixed dollar risk target (e.g., 1% of account).
Null hypothesis: A baseline or control condition against which a trading strategy is tested; here, random entry at the same price swings without momentum confirmation.
References
[1] Investopedia, "Divergence Definition and How It Is Used in Trading", Investopedia (2024). Https://www.investopedia.com/terms/d/divergence.asp
[2] Blau, B. M., "The Dynamics of Momentum Divergence and Market Reversals", Journal of Financial Research, 41(3), 295-324 (2018). Https://doi.org/10.1111/jfir.12159
[3] Lo, A. W. & MacKinlay, A. C., "Stock Market Prices Do Not Follow Random Walks: Evidence from a Simple Specification Test", Review of Financial Studies, 1(1), 41-66 (1988). Https://doi.org/10.1093/rfs/1.1.41
[4] Wilder, J. W., New Concepts in Technical Trading Systems, Trend Research (1978).
[5] CME Group, "E-mini S&P 500 Futures Specification", CME Group (2025). Https://www.cmegroup.com/trading/equity-index/us-index/e-mini-sp-500_contract_specifications.html
[6] U.S. Securities and Exchange Commission, "Investor Bulletin: Understanding Margin", SEC (2015). Https://www.sec.gov/oiea/investor-alerts-and-bulletins/ib_marginrequirements.html
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-09-13. Educational research on historical data, not financial advice.
Keep reading
Favorite-Longshot Bias in Prediction Markets
A complete, testable trading strategy: a favorite-longshot strategy on prediction markets: the documented pricing bias at the probability extremes, fees, and rules for harvesting it within venue limits. Exact rules, full Pine Script code, and an honest reading of the evidence.
Inside-Bar Breakout Futures Strategy
A complete, testable trading strategy: an inside-bar breakout strategy on futures: rules, filters, and why most published versions do not survive realistic costs. Exact rules, full Pine Script code, and an honest reading of the evidence.
Order-Block Continuation: Zone Definition, Entry, and the Case for Skepticism
A complete, testable trading strategy: a complete order-block continuation strategy: how the zone is defined, the exact entry trigger, invalidation, and what the evidence does and does not support. Exact rules, full Pine Script code, and an honest reading of the evidence.