// =============================================================================
// MTF Divergence Scanner
// =============================================================================
// Author: Baris Tankut (@btankutt)
// Repository: https://github.com/btankutt/smc-pine-suite
// License: MPL 2.0 (see LICENSE-FREE)
// Created: 2026-06-17
// Version: 1.1.0
// - FIX: HTF flags are now requested with the confirmed-bar idiom
// (expr[1] + lookahead_on): every chart bar sees the signal of the last
// CLOSED HTF bar, identically on historical and realtime bars. The
// previous call (live flags + lookahead_off) let the developing HTF bar
// print signals that could vanish on refresh. Chart-TF slots instead use
// the engine evaluated directly in chart scope - a "same TF?" test must
// not live inside a request.security expression, because Pine duplicates
// the expression's dependency chain into the REQUESTED context.
// - FIX: signals are reduced to their rising edge - one HTF divergence
// previously drew a label (and could fire an alert) on EVERY chart bar
// mapped to that HTF period (e.g. 240 duplicates on a 1m chart).
// - FIX: labels, table state and alert flags gated on barstate.isconfirmed
// (house rule) - no intrabar phantom output.
// - FIX: slots set BELOW the chart timeframe are neutralized and marked
// "(low TF)" in the table - request.security samples one lower-TF bar
// per chart bar and would silently drop signals.
// - ta.rsi / ta.macd now run unconditionally; the oscillator input only
// selects which series is read (consistent-calculation rule).
// -----------------------------------------------------------------------------
// Multi-timeframe RSI / MACD divergence detector for TradingView:
// - Regular divergence (reversal) and hidden divergence (continuation)
// - Up to three timeframes scanned simultaneously via request.security
// - On-chart labels plus a per-timeframe status table
//
// Divergences are detected from confirmed oscillator pivots (locked in
// piv_right bars after they form), higher-timeframe flags are read from the
// last CLOSED HTF bar only, and all output is gated on barstate.isconfirmed -
// each signal prints exactly once, does not flicker intrabar, and survives a
// chart refresh.
//
// Disclaimer: Educational tool, not financial advice. Past performance does
// not guarantee future results. Always backtest and use proper risk management.
// =============================================================================
//@version=6
indicator("MTF Divergence Scanner [btankutt]", overlay=true, max_labels_count=500)
// =============================================================================
// CONSTANTS
// =============================================================================
int TABLE_ROWS = 4 // header + up to 3 timeframes
// Input group names
GRP_OSC = "Oscillator"
GRP_PIV = "Pivots"
GRP_DIV = "Divergence Types"
GRP_TF = "Timeframes"
GRP_DSP = "Display"
// =============================================================================
// USER INPUTS
// =============================================================================
// Group 1 - Oscillator
string osc_type = input.string("RSI", "Oscillator", options=["RSI", "MACD"], group=GRP_OSC)
int rsi_len = input.int(14, "RSI length", minval=2, maxval=100, group=GRP_OSC)
int macd_fast = input.int(12, "MACD fast length", minval=1, group=GRP_OSC)
int macd_slow = input.int(26, "MACD slow length", minval=2, group=GRP_OSC)
int macd_sig = input.int(9, "MACD signal length", minval=1, group=GRP_OSC)
// Group 2 - Pivots
int piv_left = input.int(5, "Pivot left", minval=1, maxval=50, group=GRP_PIV)
int piv_right = input.int(5, "Pivot right", minval=1, maxval=50, group=GRP_PIV)
// Group 3 - Divergence types
bool show_regular = input.bool(true, "Regular divergence (reversal)", group=GRP_DIV)
bool show_hidden = input.bool(false, "Hidden divergence (continuation)", group=GRP_DIV)
// Group 4 - Timeframes (blank = current chart timeframe)
bool tf1_on = input.bool(true, "Timeframe 1", inline="tf1", group=GRP_TF)
string tf1 = input.timeframe("", "", inline="tf1", group=GRP_TF)
bool tf2_on = input.bool(true, "Timeframe 2", inline="tf2", group=GRP_TF)
string tf2 = input.timeframe("60", "", inline="tf2", group=GRP_TF)
bool tf3_on = input.bool(true, "Timeframe 3", inline="tf3", group=GRP_TF)
string tf3 = input.timeframe("240", "", inline="tf3", group=GRP_TF)
// Group 5 - Display
bool show_labels = input.bool(true, "Show on-chart labels", group=GRP_DSP)
bool show_table = input.bool(true, "Show status table", group=GRP_DSP)
color bull_color = input.color(color.new(color.green, 0), "Bullish color", group=GRP_DSP)
color bear_color = input.color(color.new(color.red, 0), "Bearish color", group=GRP_DSP)
// =============================================================================
// DIVERGENCE ENGINE
// =============================================================================
// Returns four flags for the timeframe in which it is evaluated:
// [regular_bull, regular_bear, hidden_bull, hidden_bear]
// Regular bullish: price lower low, oscillator higher low (reversal up)
// Regular bearish: price higher high, oscillator lower high (reversal down)
// Hidden bullish: price higher low, oscillator lower low (uptrend pullback)
// Hidden bearish: price lower high, oscillator higher high (downtrend pullback)
calc_div() =>
// ta.* calls stay unconditional (consistent-calculation rule): both
// oscillators are computed every bar; the input only selects one.
float rsi_val = ta.rsi(close, rsi_len)
[_m, _s, macd_hist] = ta.macd(close, macd_fast, macd_slow, macd_sig)
float osc = osc_type == "RSI" ? rsi_val : macd_hist
float pl = ta.pivotlow(osc, piv_left, piv_right)
float ph = ta.pivothigh(osc, piv_left, piv_right)
var float prev_pl_osc = na
var float prev_pl_prc = na
var float prev_ph_osc = na
var float prev_ph_prc = na
bool reg_bull = false
bool reg_bear = false
bool hid_bull = false
bool hid_bear = false
if not na(pl)
float prc = low[piv_right]
if not na(prev_pl_osc)
if prc < prev_pl_prc and pl > prev_pl_osc
reg_bull := true
if prc > prev_pl_prc and pl < prev_pl_osc
hid_bull := true
prev_pl_osc := pl
prev_pl_prc := prc
if not na(ph)
float prc = high[piv_right]
if not na(prev_ph_osc)
if prc > prev_ph_prc and ph < prev_ph_osc
reg_bear := true
if prc < prev_ph_prc and ph > prev_ph_osc
hid_bear := true
prev_ph_osc := ph
prev_ph_prc := prc
[reg_bull, reg_bear, hid_bull, hid_bear]
// =============================================================================
// PER-TIMEFRAME PROCESSING
// =============================================================================
// Resolve a blank timeframe to the chart's own timeframe.
resolve_tf(string tf) =>
tf == "" ? timeframe.period : tf
// Confirmed-flag wrapper for the HTF requests: the flags are read one bar
// back ([1]) and requested with lookahead_on - the canonical confirmed-bar
// idiom. Every chart bar inside HTF bar N sees the flags of the CLOSED bar
// N-1, identically on historical and realtime bars: no lookahead leak, no
// intrabar flicker, no shift on refresh.
// IMPORTANT: nothing chart-dependent may appear inside a request.security
// expression. Pine duplicates the expression's whole dependency chain into
// the REQUESTED context (where timeframe.period resolves to the requested
// TF), so a "same TF?" flag computed at chart scope would silently
// re-evaluate to true inside the request and cancel the offset. The
// chart-vs-HTF choice is therefore made AFTER the requests, below.
calc_div_conf() =>
[rb, rn, hb, hn] = calc_div()
[rb[1], rn[1], hb[1], hn[1]]
// Last-seen signal text per timeframe, for the status table.
var string tf1_state = "—"
var string tf2_state = "—"
var string tf3_state = "—"
// Draw labels for one timeframe's flags and return whether anything fired.
// An HTF flag stays true for every chart bar inside the signalling HTF
// period, so each flag is reduced to its rising edge first - one divergence,
// one label, one alert. (Consecutive same-type pivots cannot occupy adjacent
// HTF bars - pivotlow/high need piv_left strictly-worse neighbours - so the
// edge cannot swallow a second signal.) All output is additionally gated on
// barstate.isconfirmed: no intrabar phantoms, realtime matches history.
process_tf(string tfname, bool enabled, bool rb, bool rn, bool hb, bool hn) =>
bool rb_new = rb and not rb[1]
bool rn_new = rn and not rn[1]
bool hb_new = hb and not hb[1]
bool hn_new = hn and not hn[1]
bool bull_fired = (show_regular and rb_new) or (show_hidden and hb_new)
bool bear_fired = (show_regular and rn_new) or (show_hidden and hn_new)
if enabled and show_labels and barstate.isconfirmed
if bull_fired
string txt = (show_regular and rb_new ? "Reg" : "Hid") + " Bull " + tfname
label.new(bar_index, low, txt, yloc=yloc.belowbar,
style=label.style_label_up, color=bull_color,
textcolor=color.white, size=size.small)
if bear_fired
string txt = (show_regular and rn_new ? "Reg" : "Hid") + " Bear " + tfname
label.new(bar_index, high, txt, yloc=yloc.abovebar,
style=label.style_label_down, color=bear_color,
textcolor=color.white, size=size.small)
[bull_fired and barstate.isconfirmed, bear_fired and barstate.isconfirmed]
// --- Evaluate each timeframe ------------------------------------------------
// The engine evaluated directly in chart scope - used for slots set to the
// chart's own timeframe, where the [1] offset would only add a bar of lag.
// This call site keeps its own var state, independent of the requests.
[cd_rb, cd_rn, cd_hb, cd_hn] = calc_div()
// Same-TF detection stays in chart scope and is applied AFTER the requests -
// see the calc_div_conf note for why it must not enter the expression.
bool tf1_chart = resolve_tf(tf1) == timeframe.period
bool tf2_chart = resolve_tf(tf2) == timeframe.period
bool tf3_chart = resolve_tf(tf3) == timeframe.period
// A slot BELOW the chart TF cannot be scanned reliably: request.security
// samples one lower-TF bar per chart bar, silently dropping one-bar signal
// pulses, and historical vs realtime sampling differ. Such slots are
// neutralized (no flags) and marked "(low TF)" in the status table.
bool tf1_low = timeframe.in_seconds(resolve_tf(tf1)) < timeframe.in_seconds(timeframe.period)
bool tf2_low = timeframe.in_seconds(resolve_tf(tf2)) < timeframe.in_seconds(timeframe.period)
bool tf3_low = timeframe.in_seconds(resolve_tf(tf3)) < timeframe.in_seconds(timeframe.period)
[q1b, q1n, q1hb, q1hn] = request.security(syminfo.tickerid, resolve_tf(tf1), calc_div_conf(), lookahead=barmerge.lookahead_on)
[q2b, q2n, q2hb, q2hn] = request.security(syminfo.tickerid, resolve_tf(tf2), calc_div_conf(), lookahead=barmerge.lookahead_on)
[q3b, q3n, q3hb, q3hn] = request.security(syminfo.tickerid, resolve_tf(tf3), calc_div_conf(), lookahead=barmerge.lookahead_on)
// Chart-TF slots take the direct chart-context flags; lower-TF slots are
// neutralized; HTF slots take the confirmed request flags.
bool r1b = tf1_chart ? cd_rb : tf1_low ? false : q1b
bool r1n = tf1_chart ? cd_rn : tf1_low ? false : q1n
bool h1b = tf1_chart ? cd_hb : tf1_low ? false : q1hb
bool h1n = tf1_chart ? cd_hn : tf1_low ? false : q1hn
bool r2b = tf2_chart ? cd_rb : tf2_low ? false : q2b
bool r2n = tf2_chart ? cd_rn : tf2_low ? false : q2n
bool h2b = tf2_chart ? cd_hb : tf2_low ? false : q2hb
bool h2n = tf2_chart ? cd_hn : tf2_low ? false : q2hn
bool r3b = tf3_chart ? cd_rb : tf3_low ? false : q3b
bool r3n = tf3_chart ? cd_rn : tf3_low ? false : q3n
bool h3b = tf3_chart ? cd_hb : tf3_low ? false : q3hb
bool h3n = tf3_chart ? cd_hn : tf3_low ? false : q3hn
[bull1, bear1] = process_tf(tf1 == "" ? "Chart" : tf1, tf1_on, r1b, r1n, h1b, h1n)
[bull2, bear2] = process_tf(tf2 == "" ? "Chart" : tf2, tf2_on, r2b, r2n, h2b, h2n)
[bull3, bear3] = process_tf(tf3 == "" ? "Chart" : tf3, tf3_on, r3b, r3n, h3b, h3n)
if tf1_on and bull1
tf1_state := "Bull"
if tf1_on and bear1
tf1_state := "Bear"
if tf2_on and bull2
tf2_state := "Bull"
if tf2_on and bear2
tf2_state := "Bear"
if tf3_on and bull3
tf3_state := "Bull"
if tf3_on and bear3
tf3_state := "Bear"
// =============================================================================
// STATUS TABLE
// =============================================================================
var table panel = table.new(position.top_right, 2, TABLE_ROWS, border_width=1)
state_color(string s) =>
s == "Bull" ? bull_color : s == "Bear" ? bear_color : color.new(color.gray, 40)
if show_table and barstate.islast
table.cell(panel, 0, 0, "Timeframe", text_color=color.white, bgcolor=color.new(color.gray, 0), text_size=size.small)
table.cell(panel, 1, 0, "Last Div", text_color=color.white, bgcolor=color.new(color.gray, 0), text_size=size.small)
table.cell(panel, 0, 1, tf1_on ? (tf1 == "" ? "Chart" : tf1) + (tf1_low ? " (low TF)" : "") : "off", text_color=color.white, bgcolor=color.new(color.black, 30), text_size=size.small)
table.cell(panel, 1, 1, tf1_on ? tf1_state : "—", text_color=color.white, bgcolor=state_color(tf1_on ? tf1_state : "—"), text_size=size.small)
table.cell(panel, 0, 2, tf2_on ? (tf2 == "" ? "Chart" : tf2) + (tf2_low ? " (low TF)" : "") : "off", text_color=color.white, bgcolor=color.new(color.black, 30), text_size=size.small)
table.cell(panel, 1, 2, tf2_on ? tf2_state : "—", text_color=color.white, bgcolor=state_color(tf2_on ? tf2_state : "—"), text_size=size.small)
table.cell(panel, 0, 3, tf3_on ? (tf3 == "" ? "Chart" : tf3) + (tf3_low ? " (low TF)" : "") : "off", text_color=color.white, bgcolor=color.new(color.black, 30), text_size=size.small)
table.cell(panel, 1, 3, tf3_on ? tf3_state : "—", text_color=color.white, bgcolor=state_color(tf3_on ? tf3_state : "—"), text_size=size.small)
// =============================================================================
// ALERTS
// =============================================================================
bool any_bull = (tf1_on and bull1) or (tf2_on and bull2) or (tf3_on and bull3)
bool any_bear = (tf1_on and bear1) or (tf2_on and bear2) or (tf3_on and bear3)
alertcondition(any_bull, "Bullish divergence", "Bullish divergence detected on at least one timeframe")
alertcondition(any_bear, "Bearish divergence", "Bearish divergence detected on at least one timeframe")
alertcondition(any_bull or any_bear, "Any divergence", "A divergence was detected on at least one timeframe")
Overview
MTF Divergence Scanner scans up to three timeframes simultaneously using request.security with confirmed-bar logic to identify reversal or continuation signals. The indicator plots divergence labels directly on the price chart and displays a status table showing detected patterns per timeframe. Users configure the oscillator type along with pivot lengths, individual timeframe visibility, colors, and display options. Signals appear only on confirmed bars and survive chart refreshes, though they depend on higher-timeframe data that may include lookahead for live updates.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Oscillator
string
RSI
osc_type
RSI length
int
14
rsi_len
MACD fast length
int
12
macd_fast
MACD slow length
int
26
macd_slow
MACD signal length
int
9
macd_sig
Pivot left
int
5
piv_left
Pivot right
int
5
piv_right
Regular divergence (reversal)
bool
true
show_regular
Hidden divergence (continuation)
bool
false
show_hidden
Timeframe 1
bool
true
tf1_on
tf1
timeframe
tf1
Timeframe 2
bool
true
tf2_on
Show all 19 settings
Setting
Type
Default
Variable
tf2
timeframe
60
tf2
Timeframe 3
bool
true
tf3_on
tf3
timeframe
240
tf3
Show on-chart labels
bool
true
show_labels
Show status table
bool
true
show_table
Bullish color
color
color.new(color.green, 0)
bull_color
Bearish color
color
color.new(color.red, 0)
bear_color
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.
This indicator plots an adaptive RSI on the price chart for gold. It calculates RSI with adaptive lookback and filters signals using EMA trend, higher timeframe bias, slope threshold, ATR volatility, and structure confirmation near swings. Signals appear as bu
Calculates a multi-speed RSI system across six lengths to identify Change of Character (CHoCH) market structure shifts. The indicator overlays signals on the main chart, combining volume filter thresholds, OBV divergence analysis, anticipation proximity marker
This indicator scans multiple tickers simultaneously to detect regular bullish and bearish MACD divergences filtered by long-term trend direction (150 EMA versus 600 EMA). It evaluates pivot highs and lows in the MACD line against price action across configura
This overlay indicator looks for regular price divergences against a set of oscillators, using pivot highs and lows. It compares those pivots with RSI, MACD and its histogram, stochastic, CCI, momentum, OBV, a directional oscillator, volume-weighted MACD, Chai
The RSI Divergence Scanner calculates the Relative Strength Index and identifies regular and hidden bullish or bearish divergences against price using pivot high and low points. It plots divergence lines and customizable labels directly on the price chart. Inp
This indicator detects regular and hidden divergences across multiple technical factors plotted directly on the price chart. It evaluates price highs and lows against peaks and troughs identified by a five-bar fractal mechanism on RSI, RSI Stochastic, MACD, re