RSI divergence is one of the most requested indicators and one of the most loosely defined. Everyone agrees on the picture: price makes a new low, RSI doesn't. Turning that picture into code forces you to answer questions the picture skips. What is a low? How far apart can two lows be? When exactly is the divergence known?
This guide answers them with a complete Pine Script v6 indicator that detects regular bullish and bearish divergence on confirmed pivots, draws it, and sends alerts, without repainting. It's a tool for study, not a trading signal on its own.
What counts as divergence
| Type | Price | RSI | Usually read as |
|---|---|---|---|
| Regular bullish | Lower low | Higher low | Selling momentum fading |
| Regular bearish | Higher high | Lower high | Buying momentum fading |
| Hidden bullish | Higher low | Lower low | Possible continuation of an uptrend |
| Hidden bearish | Lower high | Higher high | Possible continuation of a downtrend |
This indicator handles the two regular types. Hidden divergence uses the same machinery with the comparisons flipped, covered at the end.
Turning the idea into rules
- Swings are pivots on RSI. A pivot low is an RSI value lower than the N values on each side. We use 5 bars left and 5 right.
- Compare consecutive pivots. When a new RSI pivot low is confirmed, compare it with the previous one. The price comparison uses the bar lows at the same two bars.
- Limit the distance. Pivots more than 60 bars apart are too far to be related; ignore that pair.
- Signal at confirmation. A pivot needs 5 bars on its right, so the divergence is known 5 bars after the pivot. That's when we signal.
Rule 4 is what keeps the indicator honest. Scripts that draw the signal on the pivot bar itself look sharper in hindsight, but on a live chart they show it five bars after the fact anyway.
The complete indicator
//@version=6
indicator("RSI divergence (confirmed pivots)", shorttitle = "RSI div", max_lines_count = 200)
rsiLen = input.int(14, "RSI length", minval = 2)
leftBars = input.int(5, "Pivot bars to the left", minval = 1)
rightBars = input.int(5, "Pivot bars to the right", minval = 1)
maxGap = input.int(60, "Max bars between pivots", minval = 5)
r = ta.rsi(close, rsiLen)
// A pivot is confirmed rightBars bars after it forms: signals arrive late, but they never move.
pl = ta.pivotlow(r, leftBars, rightBars)
ph = ta.pivothigh(r, leftBars, rightBars)
var float prevLowRsi = na
var float prevLowPrice = na
var int prevLowBar = na
var float prevHighRsi = na
var float prevHighPrice = na
var int prevHighBar = na
bullDiv = false
bearDiv = false
if not na(pl)
pivotBar = bar_index - rightBars
pivotPrice = low[rightBars]
if not na(prevLowRsi) and pivotBar - prevLowBar <= maxGap
// Regular bullish: price makes a lower low, RSI a higher low.
bullDiv := pivotPrice < prevLowPrice and pl > prevLowRsi
if bullDiv
line.new(prevLowBar, prevLowRsi, pivotBar, pl, color = color.new(color.teal, 0), width = 2)
prevLowRsi := pl
prevLowPrice := pivotPrice
prevLowBar := pivotBar
if not na(ph)
pivotBar = bar_index - rightBars
pivotPrice = high[rightBars]
if not na(prevHighRsi) and pivotBar - prevHighBar <= maxGap
// Regular bearish: price makes a higher high, RSI a lower high.
bearDiv := pivotPrice > prevHighPrice and ph < prevHighRsi
if bearDiv
line.new(prevHighBar, prevHighRsi, pivotBar, ph, color = color.new(color.red, 0), width = 2)
prevHighRsi := ph
prevHighPrice := pivotPrice
prevHighBar := pivotBar
plot(r, "RSI", color = color.new(color.purple, 0), linewidth = 2)
hline(70, "Overbought", color = color.gray, linestyle = hline.style_dashed)
hline(30, "Oversold", color = color.gray, linestyle = hline.style_dashed)
plotshape(bullDiv, "Bullish divergence", shape.labelup, location.bottom, color.new(color.teal, 0), text = "Bull", textcolor = color.white, size = size.tiny)
plotshape(bearDiv, "Bearish divergence", shape.labeldown, location.top, color.new(color.red, 0), text = "Bear", textcolor = color.white, size = size.tiny)
alertcondition(bullDiv, "Bullish RSI divergence", "Bullish RSI divergence confirmed on {{ticker}} ({{interval}})")
alertcondition(bearDiv, "Bearish RSI divergence", "Bearish RSI divergence confirmed on {{ticker}} ({{interval}})")
How it works
Pivots. ta.pivotlow(r, 5, 5) returns the RSI value of a pivot low on the bar where it becomes confirmed, five bars after it formed, and na on every other bar. So not na(pl) means "a pivot low was just confirmed", and the pivot itself sits at bar_index - rightBars.
Remembering the previous pivot. The var variables keep the RSI value, the price low and the bar index of the last pivot from one confirmation to the next. Each new pivot is compared with them, then replaces them.
Price at the pivot. low[rightBars] is the bar low at the pivot bar. Comparing lows at the RSI pivots is a common, simple definition. A stricter version would also require a price pivot near the same bar.
Drawing. When a divergence is found, line.new() connects the two RSI pivots in the indicator pane, and a label marks the bar where the divergence was confirmed. Nothing is drawn retroactively except the line, which joins two bars that were already in the past when it was drawn.
The v6 detail that breaks many divergence scripts
A lot of published divergence code was written for older versions of Pine and looks like this:
inRange = plFound and ta.barssince(plFound[1]) <= 60
In Pine Script v6, and and or are lazy: the right-hand side is only evaluated when the left-hand side doesn't already decide the result. Here ta.barssince() would only run on bars where plFound is true, so its internal count skips most bars and returns nonsense. The script compiles; the logic is simply wrong.
The rule: call every function that keeps history (ta.barssince, ta.valuewhen, ta.crossover, ta.highest and the rest) on every bar, assign the result to a variable at the top level, and combine plain variables with and. The script above avoids the problem by storing pivot data in var variables instead of searching back through history.
Adding hidden divergence
Hidden divergence reverses the comparisons. In the pivot-low block, a hidden bullish divergence is pivotPrice > prevLowPrice and pl < prevLowRsi: price makes a higher low while RSI makes a lower low. In the pivot-high block, hidden bearish is pivotPrice < prevHighPrice and ph > prevHighRsi. Add a variable for each, compute it next to the regular one, and give it its own plot and alert condition.
Making it more selective
- Zone filter: only count bullish divergence when the earlier RSI low was below 30, and bearish when the earlier high was above 70.
- Trend filter: only show bullish divergence when price is above a long moving average, or when the higher-timeframe trend agrees.
- Trigger: wait for a confirming event after the divergence, such as a close above the high of the pivot bar, before considering a trade.
Using it responsibly
Divergence describes momentum that is slowing. It doesn't say when, or whether, price will turn, and in strong trends it can appear several times in a row while the trend continues. Treat a confirmed divergence as a prompt to look closer, not as an order. If you build a strategy on it, test it with costs and look at enough trades before trusting the result. Because a pivot is only confirmed a few bars after it forms, any divergence script that marks it earlier will repaint.
For the indicator itself, see the RSI guide; the same pivot logic works on the stochastic oscillator or MACD by swapping the series.
Sources and further reading
- Pine Script v6 reference: ta.pivotlow() and ta.pivothigh() — how pivots are confirmed after the right-hand bars.
- Pine Script User Manual: Lines and boxes — drawing the divergence lines.
- Migration guide to Pine Script version 6 — including the switch to lazy evaluation of
andandor.
Try it with Edgoro
Describe your exact definition — "regular and hidden RSI(14) divergence on 5/5 pivots no more than 60 bars apart, bullish only below 30, alert when the next bar closes above the pivot bar's high" — and Edgoro writes the Pine Script v6 indicator for you. Build your divergence indicator with Edgoro.
Divergence is not a reliable reversal signal on its own. Indicators describe the past; trading involves substantial risk of loss.