DEV Community

Lawrence Liu
Lawrence Liu

Posted on • Originally published at supa.is

TradingView Pine Script RSI Divergence Indicator: Build One That Actually Works (2026)

Originally published on supa.is

TradingView Pine Script RSI Divergence Indicator: Build One That Actually Works (2026)

Most RSI divergence indicators on TradingView's library are either too noisy or miss real setups entirely. I built my own in Pine Script v6 — here's exactly how.


RSI divergence is one of those concepts that sounds simple in textbooks but turns into a mess when you try to code it. The price makes a new high, RSI doesn't — bullish exhaustion, right? In theory, yes. In practice, every public RSI divergence indicator I tested on TradingView either painted signals on every other candle or missed the divergences that actually mattered.

I trade USDJPY on daily charts using a momentum-based system. After months of squinting at RSI and price action trying to spot divergences manually, I decided to automate it. This tutorial walks through building an RSI divergence indicator from scratch in Pine Script v6 — one that uses proper pivot detection, filters out noise, and gives you alerts you can actually trade on.

If you've already gone through my Pine Script moving average crossover tutorial, you'll recognize the approach: start simple, then layer in the filters that make it production-ready.


What Is RSI Divergence (and Why Should You Care)?

RSI divergence happens when price and the RSI oscillator disagree about momentum:

  • Bullish divergence: Price makes a lower low, but RSI makes a higher low → selling momentum is weakening, potential reversal up
  • Bearish divergence: Price makes a higher high, but RSI makes a lower high → buying momentum is weakening, potential reversal down

There's also hidden divergence (continuation signals), but we'll start with regular divergence since it's what most traders look for.

Why I use it: RSI divergence doesn't predict when a reversal will happen — it signals that the current move is running out of steam. Combined with other signals (like moving average structure), it helps me avoid entering trades right before a reversal. On USDJPY, I've found bearish divergence on the daily chart to be particularly reliable around seasonal turning points.


Step 1: Basic RSI Divergence Indicator (The Naive Approach)

Let's start with what most tutorials give you, so you can see why it doesn't work well. Open the Pine Editor on TradingView and paste:

//@version=6
indicator("RSI Divergence — Basic", overlay=false)

// --- Inputs ---
rsiLen    = input.int(14, "RSI Length", minval=2)
src       = input.source(close, "Source")

// --- RSI Calculation ---
rsiValue = ta.rsi(src, rsiLen)
plot(rsiValue, "RSI", color=color.purple, linewidth=2)
hline(70, "Overbought", color=color.red, linestyle=hline.style_dotted)
hline(30, "Oversold", color=color.green, linestyle=hline.style_dotted)

// --- Naive Divergence: Compare Current Bar to N Bars Ago ---
lookback = input.int(14, "Lookback Bars", minval=5)

// Bullish: price lower low, RSI higher low
bullDiv = (low < low[lookback]) and (rsiValue > rsiValue[lookback]) and (rsiValue < 40)
// Bearish: price higher high, RSI lower high
bearDiv = (high > high[lookback]) and (rsiValue < rsiValue[lookback]) and (rsiValue > 60)

plotshape(bullDiv, "Bull Div", shape.triangleup, location.bottom, color.green, size=size.small)
plotshape(bearDiv, "Bear Div", shape.triangledown, location.top, color.red, size=size.small)
Enter fullscreen mode Exit fullscreen mode

Add it to your chart. You'll immediately see the problem: signals everywhere. On a USDJPY daily chart, this fires 2-3 times per week. Most are noise because comparing to "N bars ago" is arbitrary — the price N bars ago might not even be a meaningful swing point.

The fix: We need to compare actual pivot highs and lows, not just arbitrary lookback points.


Step 2: Pivot-Based RSI Divergence (The Right Way)

This is the core improvement. Instead of comparing to a fixed lookback, we detect actual swing highs and swing lows using ta.pivothigh() and ta.pivotlow(), then compare the RSI values at those pivots:

//@version=6
indicator("RSI Divergence — Pivot Based", overlay=false, max_lines_count=500)

// ─── 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)
maxBars    = input.int(60, "Max Bars Between Pivots", minval=10, maxval=200)
src        = input.source(close, "Source")

// ─── RSI ────────────────────────────────────────────
rsiValue = ta.rsi(src, rsiLen)
plot(rsiValue, "RSI", color=color.new(color.purple, 0), linewidth=2)
hline(70, "Overbought", color=color.red, linestyle=hline.style_dotted)
hline(30, "Oversold", color=color.green, linestyle=hline.style_dotted)

// ─── Pivot Detection ────────────────────────────────
// Pivots are confirmed `pivotRight` bars ago
pivotLowPrice  = ta.pivotlow(low, pivotLeft, pivotRight)
pivotHighPrice = ta.pivothigh(high, pivotLeft, pivotRight)

pivotLowRSI    = ta.pivotlow(rsiValue, pivotLeft, pivotRight)
pivotHighRSI   = ta.pivothigh(rsiValue, pivotLeft, pivotRight)

// ─── Track Previous Pivots ──────────────────────────
var float prevPivotLowPrice  = na
var int   prevPivotLowBar    = na
var float prevPivotLowRSI    = na

var float prevPivotHighPrice = na
var int   prevPivotHighBar   = na
var float prevPivotHighRSI   = na

// ─── Bullish Divergence (price lower low, RSI higher low) ───
bullDiv = false
if not na(pivotLowPrice)
    currBar = bar_index - pivotRight
    currPriceLow = pivotLowPrice
    currRSILow   = pivotLowRSI

    if not na(prevPivotLowPrice)
        barDiff = currBar - prevPivotLowBar
        if barDiff <= maxBars and barDiff > 0
            // Price: lower low | RSI: higher low
            if currPriceLow < prevPivotLowPrice and currRSILow > prevPivotLowRSI
                bullDiv := true
                // Draw line on RSI pane
                line.new(prevPivotLowBar, prevPivotLowRSI, currBar, currRSILow,
                     color=color.green, width=2, style=line.style_solid)

    prevPivotLowPrice := currPriceLow
    prevPivotLowBar   := currBar
    prevPivotLowRSI   := currRSILow

// ─── Bearish Divergence (price higher high, RSI lower high) ───
bearDiv = false
if not na(pivotHighPrice)
    currBar = bar_index - pivotRight
    currPriceHigh = pivotHighPrice
    currRSIHigh   = pivotHighRSI

    if not na(prevPivotHighPrice)
        barDiff = currBar - prevPivotHighBar
        if barDiff <= maxBars and barDiff > 0
            // Price: higher high | RSI: lower high
            if currPriceHigh > prevPivotHighPrice and currRSIHigh < prevPivotHighRSI
                bearDiv := true
                line.new(prevPivotHighBar, prevPivotHighRSI, currBar, currRSIHigh,
                     color=color.red, width=2, style=line.style_solid)

    prevPivotHighPrice := currPriceHigh
    prevPivotHighBar   := currBar
    prevPivotHighRSI   := currRSIHigh

// ─── Signals ────────────────────────────────────────
plotshape(bullDiv, "Bullish Divergence", shape.labelup, location.bottom,
     color=color.green, text="Bull", textcolor=color.white, size=size.small)
plotshape(bearDiv, "Bearish Divergence", shape.labeldown, location.top,
     color=color.red, text="Bear", textcolor=color.white, size=size.small)

// ─── Alerts ─────────────────────────────────────────
alertcondition(bullDiv, "Bullish RSI Divergence", "RSI bullish divergence detected")
alertcondition(bearDiv, "Bearish RSI Divergence", "RSI bearish divergence detected")
Enter fullscreen mode Exit fullscreen mode

What changed and why:

  1. Pivot detectionta.pivothigh() and ta.pivotlow() find actual swing points, not arbitrary lookbacks. A pivot low with pivotLeft=5, pivotRight=5 means the bar was lower than the 5 bars on either side. This is a real turning point.

  2. Bar distance limit — The maxBars parameter prevents comparing pivots that are 200 bars apart. Divergences lose meaning over very long distances.

  3. Visual lines — Green/red lines drawn between the RSI pivot points make divergence immediately visible.

  4. Alert conditions — You can set TradingView alerts that fire when divergence is detected. If you're on the Plus plan or higher, these run server-side so you don't need to keep the browser open.

On USDJPY daily, this version fires roughly 1-2 signals per month — a dramatic reduction from the naive approach's 2-3 per week, and the signals actually correspond to meaningful swing points.


Step 3: Adding RSI Zone Filters (Reducing False Signals)

Not all divergences are created equal. A bearish divergence when RSI is at 55 is much weaker than one at 75. Let's add zone filters:

// ─── Add these inputs at the top ────────────────────
rsiOBLevel = input.int(60, "Min RSI for Bearish Div", minval=50, maxval=90)
rsiOSLevel = input.int(40, "Max RSI for Bullish Div", minval=10, maxval=50)

// ─── Modify the divergence conditions ───────────────
// In the bullish divergence block, add:
if currPriceLow < prevPivotLowPrice and currRSILow > prevPivotLowRSI
    if currRSILow < rsiOSLevel  // Only in oversold territory
        bullDiv := true
        // ... line drawing code

// In the bearish divergence block, add:
if currPriceHigh > prevPivotHighPrice and currRSIHigh < prevPivotHighRSI
    if currRSIHigh > rsiOBLevel  // Only in overbought territory
        bearDiv := true
        // ... line drawing code
Enter fullscreen mode Exit fullscreen mode

Why this matters: From my experience trading USDJPY, divergences in the "middle zone" (RSI 40-60) are noise about 70% of the time. The indicator should only alert you when RSI is in meaningful territory — below 40 for bullish divergences, above 60 for bearish. These thresholds are adjustable in settings.


Step 4: Adding Hidden Divergence (Continuation Signals)

Hidden divergence signals trend continuation rather than reversal:

  • Hidden bullish: Price makes a higher low, RSI makes a lower low → uptrend continuing
  • Hidden bearish: Price makes a lower high, RSI makes a higher high → downtrend continuing

Add this to the pivot detection section:

// ─── Input toggle ───────────────────────────────────
showHidden = input.bool(true, "Show Hidden Divergence")

// ─── Hidden Bullish (higher low price, lower low RSI) ───
hiddenBullDiv = false
if showHidden and not na(pivotLowPrice)
    currBar = bar_index - pivotRight
    currPriceIdx = pivotLowPrice
    currRSIIdx   = pivotLowRSI

    if not na(prevPivotLowPrice)
        barDiff = currBar - prevPivotLowBar
        if barDiff <= maxBars and barDiff > 0
            if currPriceIdx > prevPivotLowPrice and currRSIIdx < prevPivotLowRSI
                hiddenBullDiv := true
                line.new(prevPivotLowBar, prevPivotLowRSI, currBar, currRSIIdx,
                     color=color.lime, width=1, style=line.style_dashed)

// ─── Hidden Bearish (lower high price, higher high RSI) ───
hiddenBearDiv = false
if showHidden and not na(pivotHighPrice)
    currBar = bar_index - pivotRight
    currPriceIdx = pivotHighPrice
    currRSIIdx   = pivotHighRSI

    if not na(prevPivotHighPrice)
        barDiff = currBar - prevPivotHighBar
        if barDiff <= maxBars and barDiff > 0
            if currPriceIdx < prevPivotHighPrice and currRSIIdx > prevPivotHighRSI
                hiddenBearDiv := true
                line.new(prevPivotHighBar, prevPivotHighRSI, currBar, currRSIIdx,
                     color=color.orange, width=1, style=line.style_dashed)

// ─── Hidden Divergence Plots ────────────────────────
plotshape(hiddenBullDiv, "Hidden Bull", shape.diamond, location.bottom,
     color=color.lime, text="H-Bull", textcolor=color.white, size=size.tiny)
plotshape(hiddenBearDiv, "Hidden Bear", shape.diamond, location.top,
     color=color.orange, text="H-Bear", textcolor=color.white, size=size.tiny)
Enter fullscreen mode Exit fullscreen mode

Hidden divergence is less popular but I find it useful as a confirmation tool. If I'm already in a USDJPY long based on my momentum system and I see a hidden bullish divergence, it gives me confidence to hold the position rather than taking early profits.


Step 5: Combining RSI Divergence With Moving Averages

RSI divergence alone isn't a trading system — it's a filter. In my own setup, I combine it with moving average structure (covered in detail in my MA crossover tutorial):

The combo logic:

  • Bullish divergence + price above 200 EMA = high-confidence buy setup
  • Bearish divergence + price below 200 EMA = high-confidence sell setup
  • Divergence against the trend = lower confidence, use tighter stops

Here's how to add a trend filter to the indicator:

// ─── Trend Context ──────────────────────────────────
trendMA   = input.int(200, "Trend MA Length", minval=50)
trendLine = ta.ema(close, trendMA)
upTrend   = close > trendLine
downTrend = close < trendLine

// ─── Modify alert conditions for trend-aligned signals ───
trendAlignedBull = bullDiv and upTrend
trendAlignedBear = bearDiv and downTrend

alertcondition(trendAlignedBull, "Trend-Aligned Bull Div",
     "Bullish RSI divergence WITH uptrend — high confidence")
alertcondition(trendAlignedBear, "Trend-Aligned Bear Div",
     "Bearish RSI divergence WITH downtrend — high confidence")
Enter fullscreen mode Exit fullscreen mode

This is where Pine Script indicators start becoming actual trading tools. The trend filter alone cuts out about half the signals — and from what I've seen on my USDJPY charts, it cuts the wrong half.


Step 6: Setting Up Alerts

The whole point of building a custom indicator is to get alerts without staring at charts all day. If you've followed my TradingView backtest settings guide, you know I'm a big fan of automating what can be automated.

To set alerts:

  1. Add the indicator to your chart
  2. Click the Alert button (clock icon) or press Alt+A
  3. Under "Condition", select your indicator name
  4. Choose the specific alert condition (e.g., "Trend-Aligned Bull Div")
  5. Set notification method: app push, email, webhook, or SMS
  6. For webhooks (Telegram bots, etc.), the alert message is customizable

Pro tip: TradingView's free plan only allows 1 active alert. If you're running multiple indicators, you'll want at least the Plus plan for 5 server-side alerts. I run about 4 alerts across different indicators and timeframes for USDJPY.


Common Mistakes When Coding RSI Divergence

After iterating on this indicator across several months, here's what tripped me up:

1. Not Accounting for Pivot Confirmation Delay

ta.pivothigh(high, 5, 5) confirms a pivot 5 bars after the actual high. Your signal is delayed by pivotRight bars. This is intentional — you can't confirm a swing high until you see bars declining after it. But it means you're never catching the exact top/bottom.

Workaround: I use pivotLeft=5, pivotRight=3 as a compromise — slightly faster confirmation with acceptable reliability.

2. Comparing Price Pivots to RSI Non-Pivots

Some indicators compare price pivot lows to RSI values at the same bar, but don't check if RSI also formed a pivot at that bar. This leads to phantom divergences. In my indicator, I use ta.pivotlow(rsiValue, ...) separately to ensure RSI also has a genuine swing point.

3. No Maximum Distance Between Pivots

Without maxBars, the indicator might compare a pivot from 6 months ago to today's pivot and call it a divergence. Technically correct, practically useless. I cap it at 60 bars (about 3 months on daily charts).

4. Ignoring the Trend

Counter-trend divergences have a much lower win rate. A bullish divergence in a strong downtrend (price well below 200 EMA) often just leads to a brief bounce before the downtrend continues. Always check trend context.


RSI Divergence Settings: What I Use

Setting My Value Why
RSI Length 14 Standard, well-tested
Pivot Left 5 Enough bars for a real swing
Pivot Right 3 Faster confirmation than 5
Max Bars 60 ~3 months on daily
Min RSI (Bear) 60 Filter out mid-range noise
Max RSI (Bull) 40 Only oversold divergences
Trend MA 200 Standard trend reference
Hidden Div On Useful for holding positions

These work for USDJPY on daily timeframes. If you trade crypto or lower timeframes, you'll want to adjust — smaller maxBars, possibly looser RSI thresholds since crypto RSI behaves differently.


FAQ

Does RSI divergence work on all timeframes?

It works on any timeframe, but reliability increases with higher timeframes. On 1-minute or 5-minute charts, you'll get far more false signals because intrabar noise creates meaningless "pivots." I primarily use it on daily charts, and occasionally on 4-hour. Below that, the signal-to-noise ratio drops fast.

Why does my RSI divergence indicator show signals that don't match what I see visually?

Most likely a pivot detection issue. If your indicator uses a fixed lookback (rsiValue[14]) instead of actual pivot detection (ta.pivotlow()), it's comparing arbitrary bars rather than genuine swing points. The pivot-based approach in this tutorial solves that.

Can I use RSI divergence as a standalone trading signal?

I wouldn't recommend it. RSI divergence tells you momentum is weakening — it doesn't tell you when the reversal will happen. A market can stay divergent for weeks. I use it as a filter alongside my momentum strategy: divergence + trend alignment + moving average structure = a trade I might take. Divergence alone = an observation I note.

What's the difference between regular and hidden divergence?

Regular divergence signals potential reversal — momentum disagrees with price at extremes. Hidden divergence signals continuation — during a pullback within a trend, RSI dips lower than the previous pullback but price holds higher (or vice versa for downtrends). Think of regular as "the move is ending" and hidden as "the trend is still alive."

How do I avoid RSI divergence false signals?

Three filters I use: (1) RSI zone filter — only take bullish divergences when RSI is below 40 and bearish above 60, (2) Trend alignment — match divergence direction to the 200 EMA trend, (3) Minimum bar distance — don't compare pivots that are too close together (less than 10 bars) or too far apart (more than 60 bars). Together these cut false signals by roughly 60% in my testing.


Wrapping Up

RSI divergence is one of those indicators that's simple in concept but surprisingly tricky to code properly. The difference between a useful indicator and a noisy one comes down to three things: real pivot detection, zone filtering, and trend context.

The complete Pine Script v6 code in this tutorial gives you all three. Add it to your chart, adjust the settings for your instrument and timeframe, and set alerts so you don't have to babysit the chart.

If you're building a broader Pine Script trading system, start with the moving average crossover as your trend engine, then layer this RSI divergence indicator as a timing filter. That's essentially what I do for USDJPY.

Ready to build your own indicators? TradingView is the platform I use for all my charting and Pine Script development. The free tier gets you started — upgrade when you need server-side alerts.


Affiliate disclosure: Some links in this article are affiliate links. I only recommend tools I actually use. This doesn't affect my opinions or the technical content.

Risk warning: Trading involves substantial risk of loss. RSI divergence is a technical analysis tool, not a guarantee of future results. Never trade with money you can't afford to lose.

Top comments (0)