Divergence between an oscillator and price is one of the most-used concepts in technical analysis. When price makes a lower low but RSI makes a higher low, that is a potential bullish divergence signal. The idea is simple. The correct implementation in TradingView Pine Script is not, because it requires confirmed pivot detection — and most publicly available divergence indicators get the confirmation logic wrong, producing signals that look perfect in hindsight and fire unreliably in real time.
This tutorial builds a complete RSI divergence detector in Pine Script v6 from first principles. Every design decision is explained, especially the ones that look like they introduce lag, because that lag is what makes the indicator honest.
Why divergence detection is hard to get right
Divergence requires comparing two swing points: the current pivot and the previous one. The problem is that you cannot know a bar is a pivot high until enough subsequent bars have formed with lower highs. Pine Script's ta.pivothigh() and ta.pivotlow() functions handle this correctly by requiring a lookback on both the left and right side of the potential pivot. A pivot high with leftbars=5 and rightbars=5 is only confirmed 5 bars after the actual pivot bar, once 5 lower highs have formed to its right.
//@version=6
indicator("Pivot confirmation lag", overlay=false)
// ta.pivothigh(source, leftbars, rightbars) returns the HIGH of a pivot
// when BOTH the left and right look-back bars have been confirmed.
// With leftbars=5 and rightbars=5, the pivot is only known 5 bars AFTER
// the actual pivot bar. This lag is correct — you cannot know a bar is a
// pivot high until enough subsequent lower bars have formed.
ph = ta.pivothigh(high, 5, 5)
// ph is na on bars that are not confirmed pivots.
// On bars where a pivot is confirmed, it holds the pivot's high value.
plot(ph, "Pivot High", color=color.red, style=plot.style_circles, linewidth=3)This confirmation delay is not a flaw. It is the only way to avoid repainting. Any divergence detector that signals on the current bar without this delay is using data that does not exist yet — it is peeking at whether the current bar will eventually form a valid pivot. The signal looks perfect in backtests because the indicator had access to future data when placing it. In live trading, the signal appears and then disappears when the pivot fails to confirm. This is the exact issue the repainting guide describes, and it is why the confirmed-pivot approach is the only one worth building.
The full divergence detector
The code below detects both bullish and bearish RSI divergence using confirmed pivots. When a new confirmed pivot low forms, it checks whether this low is lower than the previous confirmed pivot low while the RSI at the same bar is higher than the RSI at the previous pivot. If both are true, it draws a label at the pivot bar. Labels are placed at bar_index - pivotRight to mark the actual pivot bar, not the current bar where the confirmation happened.
//@version=6
indicator("RSI Divergence Detector (non-repainting)", overlay=false)
// ── Inputs ────────────────────────────────────────────────────────────────
rsiLen = input.int(14, "RSI Length", minval=2)
pivotLeft = input.int(5, "Pivot lookback left", minval=1)
pivotRight = input.int(5, "Pivot lookback right", minval=1)
lookback = input.int(50, "Max bars to look back for prior pivot", minval=10)
// ── RSI ───────────────────────────────────────────────────────────────────
rsi = ta.rsi(close, rsiLen)
// ── Confirmed pivot highs and lows ───────────────────────────────────────
// These return na on non-pivot bars and the pivot price on confirmed pivot bars.
// The signal is delayed by pivotRight bars — that delay is intentional and correct.
pivotHigh = ta.pivothigh(high, pivotLeft, pivotRight)
pivotLow = ta.pivotlow(low, pivotLeft, pivotRight)
// ── Bullish divergence: price makes a lower low, RSI makes a higher low ──
// We search backward for the most recent prior confirmed pivot low and compare.
var float prevLowPrice = na
var float prevLowRsi = na
var int prevLowBar = na
if not na(pivotLow)
// A new confirmed pivot low has formed. Before we update the stored values,
// check if this low is lower than the previous one while RSI is higher —
// that is the bullish divergence condition.
bool bullDiv = not na(prevLowPrice) and
(bar_index - prevLowBar) <= lookback and
low[pivotRight] < prevLowPrice and
rsi[pivotRight] > prevLowRsi
if bullDiv
// Plot the label at the pivot bar (pivotRight bars back), not the current bar.
label.new(bar_index - pivotRight, low[pivotRight] - ta.atr(14)[pivotRight],
"Bull Div", style=label.style_label_up, color=color.new(color.green, 20),
textcolor=color.white, size=size.small)
// Store this pivot's values for comparison with the next one.
prevLowPrice := pivotLow
prevLowRsi := rsi[pivotRight]
prevLowBar := bar_index - pivotRight
// ── Bearish divergence: price makes a higher high, RSI makes a lower high ─
var float prevHighPrice = na
var float prevHighRsi = na
var int prevHighBar = na
if not na(pivotHigh)
bool bearDiv = not na(prevHighPrice) and
(bar_index - prevHighBar) <= lookback and
high[pivotRight] > prevHighPrice and
rsi[pivotRight] < prevHighRsi
if bearDiv
label.new(bar_index - pivotRight, high[pivotRight] + ta.atr(14)[pivotRight],
"Bear Div", style=label.style_label_down, color=color.new(color.red, 20),
textcolor=color.white, size=size.small)
prevHighPrice := pivotHigh
prevHighRsi := rsi[pivotRight]
prevHighBar := bar_index - pivotRight
// ── Plot RSI for reference ────────────────────────────────────────────────
plot(rsi, "RSI", color=color.aqua)
hline(70, "Overbought", color.red, linestyle=hline.style_dashed)
hline(30, "Oversold", color.green, linestyle=hline.style_dashed)
hline(50, "Midline", color.gray, linestyle=hline.style_dotted)Walking through the key design decisions
The var keyword on prevLowPrice, prevLowRsi, and prevLowBar is essential. Without it, these variables would reset to na on every bar. Withvar, they retain their value across bars and only update when a new confirmed pivot arrives. This is the correct way to build state that persists across the series, as explained in the var and varip guide.
The lookback parameter limits how far back the detector searches for a prior pivot. Without this cap, the detector might compare a pivot today against one from two years ago and flag a divergence that spans too wide a range to be meaningful. Fifty bars is a reasonable default but should be adjusted based on the timeframe and how frequently the instrument forms meaningful pivots.
The RSI comparison uses rsi[pivotRight] — the RSI valueat the pivot bar, not at the current bar. This is the value that corresponds to the price being compared. Using the current RSI value instead would compare apples and oranges: the price swing at a bar five days ago versus the RSI reading today.
The ATR offset on the label position (ta.atr(14)[pivotRight]) places the label a volatility-appropriate distance below the pivot low or above the pivot high. This keeps labels readable across instruments with different price scales.
Extending to MACD divergence
The same structure works for any oscillator. Replace rsi with the MACD histogram or line: compute it with ta.macd(), then compare the oscillator value at each confirmed pivot the same way. The pivot logic is identical regardless of which oscillator you use.
Building this without writing it by hand
The divergence detector above is roughly 60 lines, but the conceptual complexity is much higher. Getting the pivot confirmation lag right, thevar state management correct, the comparison indexing accurate, and the label placement precise is exactly where most hand-written divergence scripts develop subtle bugs. PineScripter generates indicators like this from a plain-English description — "detect RSI bullish divergence using confirmed pivot lows with a 5-bar lookback on each side, limit comparison to the last 50 bars, mark with a label at the pivot bar" — and the output uses confirmed pivots by default. If something does not compile, the built-in error loop corrects it without you relaying error messages back and forth. For a comparison of how this differs from using a general AI model, see why ChatGPT struggles with Pine Script.
Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.