Indicator · Pine Script v6 · On the chart

MTF Divergence Scanner

by btankutt

MTF Divergence Scanner detects regular and hidden divergences in RSI or MACD oscillators across multiple timeframes.

View on GitHub

Repaints

GitHub stars2on the repository
Pine versionv6current
Lines of code280
Settings19inputs
LicenceMPL-2.0
Last update3 months ago6 Jul 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

280 lines of Pine Script v6 from indicators/03-mtf-divergence/mtf-divergence.pine, published under MPL-2.0. Keep the author's credit and licence lines when you reuse it.

mtf-divergence.pine
// =============================================================================
// 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.

Details

Published by

btankutt on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Oscillators & momentum, Divergence, SMC / ICT / price action

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

  • Reads higher-timeframe data with lookahead on, so the history can show signals that were not there in real time

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
OscillatorstringRSIosc_type
RSI lengthint14rsi_len
MACD fast lengthint12macd_fast
MACD slow lengthint26macd_slow
MACD signal lengthint9macd_sig
Pivot leftint5piv_left
Pivot rightint5piv_right
Regular divergence (reversal)booltrueshow_regular
Hidden divergence (continuation)boolfalseshow_hidden
Timeframe 1booltruetf1_on
tf1timeframetf1
Timeframe 2booltruetf2_on
Show all 19 settings
SettingTypeDefaultVariable
tf2timeframe60tf2
Timeframe 3booltruetf3_on
tf3timeframe240tf3
Show on-chart labelsbooltrueshow_labels
Show status tablebooltrueshow_table
Bullish colorcolorcolor.new(color.green, 0)bull_color
Bearish colorcolorcolor.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.

Similar Pine Scripts

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

Indicator v6 Oscillators & momentumDivergence

USI-CHoCH

by skipp-dev
1 stars

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

Indicator v6 SMC / ICT / price actionOscillators & momentum
Updated 2 days ago · None stated GitHub repository of USI-CHoCH
257 stars

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

Indicator v5 DivergenceOscillators & momentum Repaints
Updated 6 weeks ago · None stated GitHub repository of Snag - Best Futures V3

RSI Divergence Scanner

by TradersPost
218 stars

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

Indicator v6 Oscillators & momentumDivergence
Updated 3 months ago · None stated GitHub repository of RSI Divergence Scanner

Price Divergence

by capissimo
114 stars

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

Indicator v4 DivergenceOscillators & momentum
Updated 1 year ago · None stated GitHub repository of Price Divergence

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers