Tutorial

RSI Divergence in Pine Script: Detecting It Correctly

Divergence needs two confirmed pivots, and a confirmed pivot arrives several bars late. Any implementation without that lag is reading data it did not have.

13 min read

RSI divergence is easy to see on a chart and surprisingly hard to code, and the difficulty is not about Pine Script. It is that divergence is defined by comparing two turning points, a turning point can only be identified once you know what came after it, and so a correctly written divergence indicator always signals late. Most published ones do not signal late, which tells you something specific about how they were written.

Short answer

Detect divergence by finding confirmed pivots with ta.pivotlow() or ta.pivothigh() on the RSI, then comparing the current pivot with the previous one and comparing price at the same two bars. Bullish divergence is a higher RSI low against a lower price low; bearish is a lower RSI high against a higher price high. Because ta.pivotlow() requires bars on both sides, the signal necessarily appears several bars after the pivot it refers to. This article covers the coding mechanics and makes no claim about what divergence predicts.

Key facts

  • A pivot needs confirmation from bars on both sides, so ta.pivotlow(source, left, right) returns na until the right-hand bars have printed.
  • A divergence signal therefore appears at least "right" bars after the pivot bar it describes, and no setting removes that delay.
  • Bullish divergence is a higher low in RSI against a lower low in price, compared at two confirmed pivot bars.
  • Comparing RSI at its pivot against price at the same bar index is essential; comparing RSI pivots against price pivots found independently compares different bars.
  • A maximum lookback between the two pivots is needed, or the indicator will compare pivots that are hundreds of bars apart.
  • Plotting a line between the two pivots requires storing the earlier pivot’s bar index, which means persistent state via var.
  • Any implementation that signals on the pivot bar itself, with no delay, on historical data is using information that was not available at that time.
  • TradingView ships a built-in Divergence Indicator, which is a useful reference point for comparing your own output.
TypePriceRSICompared at
Bullish (regular)Lower lowHigher lowTwo confirmed RSI pivot lows
Bearish (regular)Higher highLower highTwo confirmed RSI pivot highs
Bullish (hidden)Higher lowLower lowTwo confirmed RSI pivot lows
Bearish (hidden)Lower highHigher highTwo confirmed RSI pivot highs

Why the lag is the whole problem

Divergence compares two turning points. A turning point is a bar that is lower than the bars around it, and "around it" includes bars that come after. So identifying a low requires waiting for those later bars. This is not a limitation of Pine Script or of TradingView; it is what the word low means when applied to a moment in a series that is still being produced.

ta.pivotlow(source, left, right) encodes exactly that. It returns a value only once the specified number of bars exists on both sides, and returns na otherwise. With a right-hand requirement of five, a pivot at bar 100 is not knowable until bar 105. When your divergence condition then compares that pivot to a previous one, the earliest a signal can exist is bar 105, referring to something that happened at bar 100.

Now consider what an indicator looks like if it ignores this. On historical bars, every bar already has bars after it, so an implementation that examines a bar and its neighbours will find pivots instantly and appear to signal at the exact turning point. In real time the same code has no bars to the right, so it either signals nothing or signals on the developing bar and changes its mind. That gap between the historical appearance and the live behaviour is the single most misleading thing about published divergence indicators, and it is worth being able to spot.

The honest framing is that the lag is the cost of the signal being real. An indicator that appears to identify turning points as they happen is not faster, it is reporting something it could not have known. Accepting a few bars of delay is what makes the output the same in backtest and in real time, which is the only property that makes a backtest worth reading.

Comparing at the same bars

The most common logic error in divergence code is comparing pivots found in two different series independently. If you find RSI pivot lows and separately find price pivot lows, you have two lists of bars that do not necessarily coincide, and comparing them compares the wrong things. RSI might have turned at bar 100 while price turned at bar 97, and the comparison silently becomes meaningless.

The fix is to anchor on one series and read the other at the same bar. Find the pivot in RSI, note which bar it occurred on, then read the price low at that same bar. Now both halves of the comparison refer to a single moment, which is what the definition of divergence actually requires. Anchoring on RSI rather than price is the convention, since the indicator is what you are claiming diverges.

This is also why you need the bar index of the previous pivot rather than just its value. To draw the connecting line, and to check the two pivots are not absurdly far apart, you need to know where the earlier one was. That means storing it, which means var, because ordinary variables reset every bar. Our guide to var and varip covers why.

pine
//@version=6
indicator("RSI divergence", overlay = false)

rsiLength = input.int(14, "RSI length",                 minval = 1)
pivotLeft = input.int(5,  "Pivot bars to the left",      minval = 1)
pivotRight = input.int(5, "Pivot bars to the right",     minval = 1)
maxGap    = input.int(60, "Maximum bars between pivots", minval = 5)
minGap    = input.int(5,  "Minimum bars between pivots", minval = 1)

rsiValue = ta.rsi(close, rsiLength)

// Confirmed pivot in the RSI. na until pivotRight bars have printed, which
// is the delay that makes the signal honest.
rsiPivotLow = ta.pivotlow(rsiValue, pivotLeft, pivotRight)

// The pivot refers to the bar pivotRight bars back, not the current bar.
pivotBarOffset = pivotRight

// Remember the previous pivot: its RSI value, the price low at the SAME
// bar, and where it was. var is what lets these survive between bars.
var float prevRsiLow   = na
var float prevPriceLow = na
var int   prevPivotBar = na

bullishDivergence = false

if not na(rsiPivotLow)
    currentRsiLow   = rsiPivotLow
    // Read price at the pivot bar, not at the current bar.
    currentPriceLow = low[pivotBarOffset]
    currentPivotBar = bar_index - pivotBarOffset

    if not na(prevRsiLow)
        gap = currentPivotBar - prevPivotBar
        // Higher low in RSI, lower low in price, within a sensible distance.
        bullishDivergence := currentRsiLow > prevRsiLow and currentPriceLow < prevPriceLow and gap >= minGap and gap <= maxGap

    prevRsiLow   := currentRsiLow
    prevPriceLow := currentPriceLow
    prevPivotBar := currentPivotBar

plot(rsiValue, "RSI", color = color.new(color.purple, 0))
hline(30, "Oversold", color = color.new(color.gray, 60))
hline(70, "Overbought", color = color.new(color.gray, 60))

plotshape(bullishDivergence, "Bullish divergence",
     style = shape.labelup, location = location.bottom,
     color = color.new(color.teal, 20), text = "Div")

Note the two lines reading low[pivotBarOffset] and computing bar_index - pivotBarOffset. Both exist because the confirmed pivot describes a bar in the past, not the current one. Forgetting this offset produces code that compiles, runs, and compares the RSI at a turning point against the price several bars later, which is a bug that produces plausible-looking signals and is nearly impossible to spot by eye.

The two bounds that stop nonsense comparisons

Without a maximum gap, the code will happily compare a pivot from three hundred bars ago with the current one and report divergence. Technically the values satisfy the condition. Practically the comparison is meaningless, because divergence is a statement about two turning points in the same move, not about any two lows that ever happened. Sixty bars is a reasonable default and the right value depends on your timeframe.

A minimum gap matters too, for a different reason. Two pivots a few bars apart are often part of the same turning process rather than two separate turns, and comparing them produces noisy signals that flicker. Requiring some minimum separation filters those without changing the logic.

Both bounds are choices rather than corrections, which means they belong in inputs with sensible limits rather than as hardcoded numbers. Exposing them also makes the sensitivity visible: change the maximum gap and watch how many signals appear. If the count changes dramatically, the indicator is more sensitive to that parameter than to the divergence condition itself, which is worth knowing before you draw any conclusion from it.

Drawing the line, and the drawing budget

A divergence is much easier to read when a line connects the two pivots, and drawing it is why you stored the previous pivot’s bar index. The line runs from the earlier pivot to the current one, on the RSI pane, at the two RSI values.

This introduces the drawing limit. Lines are capped per script, the default allowance is well below the maximum, and when it is exhausted the oldest are deleted silently rather than reported. On a long chart with a permissive gap setting, a divergence indicator can produce a great many lines. Set max_lines_count deliberately, and consider whether you need the whole history drawn or only recent ones.

A useful habit here is to draw only on the bar where the signal fires, which the code below does by creating the line inside the divergence condition. Creating or updating a line on every bar is the pattern that exhausts allowances fastest, and it is easy to fall into when you are trying to make a line extend.

pine
//@version=6
// Continuing the script above, with drawing added.
// max_lines_count raised deliberately; 500 is the ceiling.
// indicator("RSI divergence", overlay = false, max_lines_count = 100)

if bullishDivergence
    line.new(prevPivotBar, prevRsiLow, bar_index - pivotBarOffset, rsiPivotLow,
         color = color.new(color.teal, 20), width = 2)

    // Optional: alert on the confirmed bar only, so the signal cannot
    // fire on a value that later changes.
    alert("Bullish RSI divergence on " + syminfo.ticker, alert.freq_once_per_bar_close)

The alert frequency is the important argument. Firing once per bar close means the alert only goes out on confirmed data. The alternative fires while the bar is developing, which for an indicator built on confirmed pivots makes no sense and reintroduces the instability the pivot confirmation was there to avoid.

Hidden divergence is the same code with the comparisons flipped

What is often called hidden or continuation divergence inverts the price comparison. Regular bullish divergence is a higher RSI low against a lower price low. Hidden bullish divergence is a lower RSI low against a higher price low. Structurally it is the same detection: same pivots, same anchoring, same bounds, with one operator reversed.

That means you should implement it by reusing the pivot detection rather than writing a second indicator. The pivots are the expensive and error-prone part; the comparison is one line. A single script that detects both, with a boolean input to choose which to display, is less code and much less opportunity for the two versions to drift apart in their pivot handling.

The bearish variants mirror everything with ta.pivothigh() instead of ta.pivotlow(), high instead of low, and the inequalities reversed. It is worth writing all four in one script for exactly the reason above: four scripts that each find pivots slightly differently is how you end up unable to explain why one of them disagrees.

Checking your implementation honestly

Start by plotting the pivots themselves before you plot any divergence. A shape on every confirmed RSI pivot low tells you immediately whether the pivot detection is finding what you expect and how late it arrives. If the pivots are wrong, the divergence built on them cannot be right, and debugging the composite condition first is a common way to waste an evening.

Then verify the offset. Pick a signal, note the bar the line starts and ends on, and check by eye that those are the actual turning points rather than bars a few positions later. This is the single check that catches the missing pivotRight offset, and it takes about ten seconds per signal.

Compare against TradingView’s built-in Divergence Indicator as a sanity reference. It will not agree with you exactly, because it makes its own choices about lookbacks and bounds, but the signals should be broadly in the same places. Wholesale disagreement means one of you is comparing the wrong bars, and it is worth finding out which.

Finally, watch it forward. Note which signals are on the chart now, then come back after a session. Real signals stay put. Anything that appears and then vanishes was based on a developing bar. This is slower than any other check and it is the only one that actually tests the property you care about.

Where a Pine-focused workflow helps

Divergence is one of the clearest examples of a script where a general-purpose chat assistant produces confident, working, subtly wrong code. The two failures repeat: pivots found without a genuine right-hand lookforward, so the indicator repaints, and RSI pivots compared against independently found price pivots, so the comparison is between different bars. Both compile. Both produce charts that look right. Neither is doing what the definition says.

PineScripter is the product we build, and the relevant advantage is that it retrieves the Pine Script manual as context, so the confirmation semantics of ta.pivotlow() come from documentation rather than from a plausible guess. Its edits are line-level, which suits this problem specifically: the fix for a missing offset is one index, and getting that as a diff rather than as a new script means you can see it.

The judgement stays with you, and for divergence the important judgement is about what you accept. A tool can make the code correct. It cannot make divergence predictive, and this article deliberately makes no claim that it is. What correct code buys you is the ability to look at the question honestly instead of at an indicator that quietly knew the future.

Plot the pivots before the divergence, so you can see how late confirmation actually arrives

Frequently asked questions

How do I code RSI divergence in Pine Script?

Find confirmed pivots in the RSI with ta.pivotlow() or ta.pivothigh(), store the previous pivot’s RSI value, the price at that same bar, and its bar index. On each new confirmed pivot, compare the current RSI pivot and the price at that bar against the stored previous pair, and require the gap between them to fall within sensible bounds.

Why does my divergence indicator signal late?

Because it is working correctly. ta.pivotlow() needs bars on both sides to confirm a pivot, so a pivot is only knowable after the right-hand bars print. A signal about a pivot five bars back cannot exist before those five bars exist. Any indicator that signals with no delay on historical data is using information that was not available at the time.

Why do RSI divergence indicators repaint?

Usually because the pivot detection uses the developing bar rather than requiring confirmation from bars to its right. On historical data every bar already has later bars, so the code appears to find pivots instantly. In real time there are no bars to the right, so it signals on incomplete information and then changes.

Should I compare RSI pivots to price pivots?

No. Finding pivots in both series independently gives you two sets of bars that may not coincide, so the comparison is between different moments. Anchor on the RSI pivot and read the price at that same bar index, which is what the definition of divergence requires.

What is the difference between regular and hidden divergence?

The price comparison is inverted. Regular bullish divergence is a higher RSI low against a lower price low. Hidden bullish divergence is a lower RSI low against a higher price low. The pivot detection is identical, so implement both in one script by reusing the pivots and flipping one operator.

Why do my divergence lines disappear from older bars?

The drawing allowance has been exhausted. Lines are capped per script and the default is below the maximum, and when the cap is reached the oldest are deleted with no error. Raise max_lines_count and check you are creating lines only on signal bars rather than updating them every bar.

The practical takeaway

Divergence is a comparison between two confirmed turning points, so a correct implementation signals late and an implementation that does not signal late is reading the future. Anchor on RSI pivots, read price at the same bar index, bound the gap in both directions, plot the pivots before the signal, and judge the result by watching it forward rather than by how well it marks historical turns.

Because the difference between a correct and a repainting divergence indicator is often one index, PineScripter is our product and shows that as a line-level diff rather than a new script. It cannot make divergence predictive, and watching the signals forward on a live chart is still the only test that matters.

Sources

Related reading: building an RSI strategy in Pine Script, repainting explained, persistent state with var and varip, the drawing limits, the RSI calculator.

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.