Pine Script RSI/MACD Divergence: Harder to Code
You spot an obvious divergence on the chart, flip back through history, and four out of five look right. The moment you translate that same logic into Pine Script — TradingView's built-in strategy scripting language — and paste it in, the alert either stays silent, fires before the bar closes, or shows up five bars later than your eye caught it.
The problem isn't you and it isn't Pine's syntax. Divergence is a thing that lives in two different worlds — **retrospective inspection** and **per-bar real-time judgement** — and those worlds don't line up. Your eye can scan the whole chart backwards and cherry-pick a pivot; a script standing on the current bar only knows what's happened up to that point. This post won't re-explain what divergence means (the RSI and MACD guides already do), and it won't hand you a "stable divergence strategy" — that needs backtesting and parameter search, more than one article can honestly promise. It covers three things only: where the eye-vs-code gap actually lives, what the minimum viable Pine implementation looks like, and the traps waiting for you once the alert leaves the script.
Gap one: the "previous high" your eye sees isn't visible to the script yet
The way your eye judges divergence is to scan left, pick a high that clearly stands out, then check whether today's high beats it and whether RSI followed. That move rests on a very unfriendly assumption for a script: you already know that point was a high.
The script's only real tool is ta.pivothigh(source, leftBars, rightBars), and the semantics of that function are "leftBars bars to the left are all lower, and rightBars bars to the right are all lower." That means it has to wait rightBars bars past the pivot before it can confirm anything — the pivot position you receive on the current bar is actually something that happened rightBars bars ago. That's design, not a bug.
Put another way: if you want to compare "is this pivot high lower than the previous pivot high," the earliest you can do it is the bar on which this pivot just got confirmed — which is rightBars bars later than the true high. left and right are typically set equal (3 to 5 are common values); larger values mean longer lag but cleaner signals.
Gap two: signals before barstate.isconfirmed don't count
RSI, MACD, and their friends recalculate on the current bar as the close ticks. What you see as "RSI 68" right now might read 71 or 65 a second later — so on a single bar your divergence check can flip back and forth and fire an alert each time. This is repainting, the trap that Pine tutorials warn about repeatedly and that Pine users still fall into.
There is exactly one fix: gate the "is this condition true" check on barstate.isconfirmed (meaning the bar has closed and the value won't change again). Or, more conservatively, judge off the previous bar with close[1], rsi[1] — that's and barstate.isconfirmed or and not barstate.isrealtime in Pine.
This is the same class of problem as the one covered in Why Pine Script Alerts Don't Remember Anything: every bar in Pine is an independent calculation, and if you don't explicitly tell it "wait until this bar settles," it will happily push signals built from mid-bar values that disappear seconds later.
Gap three: Pine doesn't automatically remember the "previous pivot"
Every call to ta.pivothigh() answers "does this bar have a newly confirmed pivot" — it returns the pivot value if yes and na if not. It does not store "the previous pivot value and where it happened."
To judge divergence you need at least two pivot highs to compare: the previous one and the current one. Pine gives you two ways to do it:
- Hold it yourself in a
var: declarevar float lastHigh = na, and whenever a new pivot comes in, move the old value out and put the new one in. ta.valuewhen(cond, source, occurrence): ask directly "what was source the previous time cond was true."
Both work. ta.valuewhen is shorter but slower; the var version is more direct and easier to debug. The next section demos the var approach so it lines up with the code in tvsbot's existing RSI Indicator Complete Guide and MACD Indicator Complete Guide, which you can just paste in.
Minimum viable implementation: RSI bullish divergence
First, the shape we're after: price makes a new low but RSI does not — that's a bullish divergence, hinting that downside momentum is fading.
Set up the inputs and the RSI value:
//@version=5
indicator("RSI Bullish Divergence (minimal)", overlay=true)
rsiLen = input.int(14, "RSI length")
left = input.int(5, "Pivot left bars")
right = input.int(5, "Pivot right bars")
rsi = ta.rsi(close, rsiLen)
left and right are usually set to the same value — 5 here, meaning a pivot needs 5 bars on each side that are higher/lower than it; in other words, a 5-bar confirmation lag. Then find the pivots and remember the previous one:
// pivot low confirms only after 'right' more bars; what you get here is a low from 'right' bars ago
pl = ta.pivotlow(low, left, right)
pl_rsi = ta.pivotlow(rsi, left, right)
// Store the previously confirmed pivots so the current one has something to compare to
var float prevPriceLow = na
var float prevRsiLow = na
var is Pine's persistent-variable declaration: it initializes exactly once on the first bar, and every subsequent bar can overwrite it. Then judge the divergence and plot it:
bullishDiv = false
if not na(pl) and not na(pl_rsi) and not na(prevPriceLow) and not na(prevRsiLow)
// price lower, RSI higher → bullish divergence
bullishDiv := pl < prevPriceLow and pl_rsi > prevRsiLow
// If this bar has a new confirmed pivot, promote "current" into "previous"
if not na(pl)
prevPriceLow := pl
if not na(pl_rsi)
prevRsiLow := pl_rsi
// Only plot on bar close so intermediate values don't paint and disappear
plotshape(bullishDiv and barstate.isconfirmed, "Bull Div",
shape.triangleup, location.belowbar,
color.new(color.green, 0), size = size.small)
This code is **minimum viable**, not "ready for live trading." It will miss detections and it will misfire — that's inherent to divergence detection, not a defect of the implementation. What to do before it touches an account is covered in the next section and the FAQ.
Three implementation traps, side by side
Eighty percent of the bugs in a divergence script fall into these three rows. The left column is the naive way, the right column is the version that works.
| Situation | Common (broken) approach | The right way | |
|---|---|---|---|
| Finding the previous high | Use something like high[10] to guess a fixed lookback | Use ta.pivothigh for a confirmed pivot, then use var to remember the previous one | |
| Firing before the bar closes | Fire as soon as the condition holds, no barstate.isconfirmed | Wrap the condition in and barstate.isconfirmed so each bar counts once | |
| Alert frequency | Use alertcondition and let the default fire every time the condition holds | alert() with alert.freq_once_per_bar_close — at most one per bar |
The table is here for you to compare against, not to memorize — every row solves the same problem: **the current bar's values keep changing, so any "compare to the past" logic has to wait for that bar to settle before judging**. Skip any one of these three habits and your backtest will look pretty while the live alerts start firing wildly.
If you actually want the alert to go out, don't use alertcondition
alertcondition() is the old API — Pine's original way of "raising an alert" — and it forces you to open a matching alert in the TradingView chart UI, pick a condition, and wire up the webhook. A webhook here is the channel that automatically pushes the alert payload to your own server; the message body itself is filled in inside that alert dialog.
Using it for divergence detection has two limits. The message can't carry dynamic values (like "how much higher is this RSI than the previous one"); and the trigger frequency isn't written in Pine — you have to pick it in the Create Alert dialog's Frequency dropdown. TradingView's Help Center page "Differences between alert frequencies" lists four options verbatim: Once only / Once per bar / Once per bar close / Once per minute or every time.
The newer approach is alert(): the message can be assembled as a runtime string inside Pine, and the frequency is specified with a Pine constant (for example alert.freq_once_per_bar_close). If you're going to wire this into automated ordering, reach for it directly:
if bullishDiv and barstate.isconfirmed
msg = "RSI bullish divergence | price=" + str.tostring(pl) +
" prev=" + str.tostring(prevPriceLow) +
" rsi=" + str.tostring(pl_rsi, "#.##")
alert(msg, alert.freq_once_per_bar_close)
`alert.freq_once_per_bar_close` means "at most one alert per bar, and only at the moment the bar closes." That's the most conservative setting divergence signals can run under — before you connect this to an execution layer (the [TradingView Webhook Complete Tutorial](/blog/en/tradingview-webhook-tutorial) covers the setup), run it under this setting for a couple of days first and watch what comes through.
The honest section: divergence isn't a guaranteed reversal, and scripted detection adds another lag
Statistically, divergence is a pattern of momentum exhaustion, not a promise that price will reverse. Continuous divergences can persist for a long time — RSI keeps diverging while price keeps walking its direction — and depending on the book you read, that's called either "hidden divergence" or "failed divergence."
Scripted detection of "a confirmed divergence pivot" typically arrives 3 to 8 bars later than what a chart-watcher would call, and divergence as a signal is already lagging by nature. These two lags stack:
- From the eye seeing the pivot to the script confirming it:
rightbars (5 is common). - From Pine firing the alert to your execution endpoint receiving the webhook: another few seconds to a dozen.
- From the execution endpoint sending a market order to the fill landing: one more round of slippage.
By the time the signal reaches you, price might have started to reverse — or might just keep going. Any automated divergence strategy needs a second confirmation condition (moving-average cross, volume expansion) to filter with; without that step, divergence turns into "your loudest source of noise" rather than a signal.
This post won't hand you backtest numbers or parameter recommendations — we haven't run the sweep across pairs and timeframes, and any number written here would just be a guess. Take the minimum viable implementation above into TradingView's Strategy Tester yourself, then compare "with the divergence filter" against "without it" on the same instrument to see how much long-term expected value actually shifts. If it shifts, keep going. If it doesn't, retreat to plain RSI/MACD crosses — coarser signals, but they hold up more consistently.
Once the divergence alert fires, do you enter?
A single divergence signal on its own isn't enough to decide on, which is the first question of the tree. Every branch below can be dropped without breaking the flow, but skipping any one degrades win rate.
barstate.isconfirmed)?FAQ
Parameters and timeframes
What should I set left and right to?
Can I detect divergence on the 1-minute timeframe?
Signals and execution
MACD divergence or RSI divergence — pick one?
ta.pivotlow(rsi, left, right) for a pivot on the MACD line from ta.macd(). MACD tends to fire slightly earlier than RSI but also has more false signals. Some traders only enter when both diverge simultaneously.Once the alert goes out, what should the execution side worry about?
alert.freq_once_per_bar_close gives you double insurance.Would the ta.valuewhen version be better?
ta.valuewhen(not na(pl), pl, 1) means "the value of pl the previous time pl was not na." Getting "the previous" instead of "the current" makes off-by-one bugs hard to trace. During debugging, the var version is easier to follow.Get started
Wire your RSI/MACD divergence alerts into automated orders — with your own API keys, a dry-run stage to observe for two days before going live, risk limits you set yourself, and every incoming signal auditable one by one in the dashboard's signals page.
Start free- RSI Indicator Complete Guide — Beyond 30/70
- MACD Indicator Complete Guide — The Real Use of Golden/Death Crosses
- Why Pine Script Alerts Don't Remember Anything (and Why That Breaks Naive Automation)
- TradingView Webhook Complete Tutorial — From Zero to Automated Orders
- You Generated a Pine Script Strategy With AI — Here's What It Takes to Actually Trade It