Indicator · Pine Script v5 · On the chart

Support/Resistence MTF

by obisin

Support/Resistence MTF indicator plots support and resistance levels on the price chart by detecting pivots across multiple configurable timeframes.

View on GitHub

Repaints

GitHub stars2on the repository
Pine versionv5runs as is
Lines of code352
Settings26inputs
LicenceMIT
Last update1 year ago6 Sep 2025

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

352 lines of Pine Script v5 from sr_mtf.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

sr_mtf.pine
// This source code is subject to the terms of the Mozilla Public License 2.0 -- © obisin-ai
//@version=5
indicator("Support/Resistence MTF", shorttitle="S/R MTF", overlay=true, max_labels_count=500, max_lines_count=500)

// ════════════════════════════════════════
// INPUTS
// ════════════════════════════════════════
calc_bars_count = input.int(250, title="Bars to analyze per timeframe", group="General")
pivot_lookback = input.int(10, title="Pivot Lookback Period", minval=5, maxval=20, group="General")
merge_threshold = input.float(0.75, title="Merge Threshold %", minval=0.1, maxval=1.0, step=0.05, tooltip="Percentage range within which nearby levels will be merged into a single level.", group="General")

// Timeframe inputs
res1 = input.timeframe("15", title="Resolution 1", group="Timeframes")
include1 = input.bool(true, title="Include Resolution 1", group="Timeframes")
res2 = input.timeframe("60", title="Resolution 2", group="Timeframes")
include2 = input.bool(true, title="Include Resolution 2", group="Timeframes")
res3 = input.timeframe("360", title="Resolution 3", group="Timeframes")
include3 = input.bool(true, title="Include Resolution 3", group="Timeframes")
res4 = input.timeframe("720", title="Resolution 4", group="Timeframes")
include4 = input.bool(true, title="Include Resolution 4", group="Timeframes")
res5 = input.timeframe("D", title="Resolution 5", group="Timeframes")
include5 = input.bool(false, title="Include Resolution 5", group="Timeframes")

// Visual settings
min_strength = input.int(3, title="Minimum Line Strength", minval=1, group="Visual")
show_labels = input.bool(true, title="Show Strength Labels", group="Visual")
show_price_labels = input.bool(true, title="Show Price on Lines", group="Visual")
highlight_nearest = input.bool(true, title="Highlight Nearest Levels", group="Visual")
nearest_count = input.int(1, title="Number of Nearest Levels", minval=1, maxval=5, group="Visual")
min_line_width = input.int(3, title="Minimum Line Width", minval=1, maxval=10, group="Visual")


// Advanced features
use_volume_weight = input.bool(false, title="Use Volume Weighting", group="Advanced")
detect_clusters = input.bool(false, title="Detect Level Clusters", group="Advanced")
show_broken_levels = input.bool(true, title="Show Recently Broken Levels", group="Advanced")
broken_transparency = input.int(65, title="Broken Level Transparency", minval=50, maxval=100, group="Advanced")

// Table settings
display_table = input.bool(true, title="Display Info Table", group="Table")
table_position = input.string("bottom_right", title="Table Position", 
     options=["top_left", "top_center", "top_right", "bottom_left", "bottom_center", "bottom_right"], group="Table")
max_table_rows = input.int(20, title="Max Table Rows", minval=5, maxval=50, group="Table")

// ════════════════════════════════════════
// ENHANCED FUNCTIONS
// ════════════════════════════════════════

// rounding function with dynamic step sizing
round_to_dynamic_step(value) =>
    //step sizing based on price magnitude
    magnitude = math.pow(10, math.floor(math.log10(math.abs(value))))
    
    step = if value < 1
        0.0001
    else if value < 10
        0.01
    else if value < 100
        0.1
    else if value < 1000
        1
    else if value < 10000
        10
    else if value < 100000
        50
    else
        magnitude / 200
    
    math.round(value / step) * step

// Structure to hold level information
type LevelInfo
    float price
    int count = 1
    float volume_weight = 0
    int first_seen_bar = 0
    int status = 0  // 0=untested, 1=respected, 2=broken
    float total_volume = 0
    array<string> timeframes
    int last_touch_bar = 0

// Arrays to store level information
var array<LevelInfo> levels = array.new<LevelInfo>()
var array<line> active_lines = array.new<line>()
var array<label> active_labels = array.new<label>()

add_or_update_level(price, vol_weight, tf) =>
    merged = false
    merge_range = price * merge_threshold / 100
    
    // Check if we can merge with existing level
    if array.size(levels) > 0
        for i = 0 to array.size(levels) - 1
            level = array.get(levels, i)
            if math.abs(level.price - price) <= merge_range
                // Merge: weighted average price
                total_weight = level.count + 1
                level.price := (level.price * level.count + price) / total_weight
                level.count += 1
                level.volume_weight += vol_weight
                level.total_volume += vol_weight
                if na(level.timeframes)
                    level.timeframes := array.new<string>()
                array.push(level.timeframes, tf)
                merged := true
                break
    
    // Add new level if not merged
    if not merged
        new_level = LevelInfo.new()
        new_level.price := price
        new_level.volume_weight := vol_weight
        new_level.total_volume := vol_weight
        new_level.first_seen_bar := bar_index
        new_level.timeframes := array.new<string>()
        array.push(new_level.timeframes, tf)
        array.push(levels, new_level)

//pivot collection with volume weighting
collect_pivots_enhanced(tf, include) =>
    [ph, pl, vol, high_vol, low_vol] = request.security(syminfo.tickerid, tf, [ta.pivothigh(pivot_lookback, pivot_lookback), ta.pivotlow(pivot_lookback, pivot_lookback), volume, volume[pivot_lookback], volume[pivot_lookback]], lookahead=barmerge.lookahead_on, calc_bars_count=calc_bars_count)
    
    if include
        if not na(ph)
            rounded_ph = round_to_dynamic_step(ph)
            add_or_update_level(rounded_ph, use_volume_weight ? high_vol : 1, tf)
            
        if not na(pl)
            rounded_pl = round_to_dynamic_step(pl)
            add_or_update_level(rounded_pl, use_volume_weight ? low_vol : 1, tf)

// Check if levels have been broken      
// Update level status based on recent behavior
check_broken_levels() =>
    if array.size(levels) > 0
        lookback_bars = 50
        
        for i = 0 to array.size(levels) - 1
            level = array.get(levels, i)
            
            respected_recently = false
            broken_recently = false
            
            for j = 1 to math.min(lookback_bars, bar_index)
                high_j = high[j]
                low_j = low[j]
                close_j = close[j]
                
                // Level touched from below and rejected
                if low_j < level.price and high_j >= level.price * 0.998 and close_j < level.price
                    respected_recently := true
                    level.last_touch_bar := bar_index - j
                    
                // Level touched from above and rejected  
                if high_j > level.price and low_j <= level.price * 1.002 and close_j > level.price
                    respected_recently := true
                    level.last_touch_bar := bar_index - j
                    
                // Level clearly broken
                if (close_j > level.price * 1.005 and close[j+1] > level.price * 1.005) or 
                   (close_j < level.price * 0.995 and close[j+1] < level.price * 0.995)
                    broken_recently := true
            
            // Update level status
            if respected_recently and not broken_recently
                level.status := 1  // Respected
            else if broken_recently and not respected_recently  
                level.status := 2  // Broken
            else
                level.status := 0  // Untested

// Clear previous lines and labels
clear_visuals() =>
    for line in active_lines
        line.delete(line)
    array.clear(active_lines)
    
    for lbl in active_labels
        label.delete(lbl)
    array.clear(active_labels)


draw_enhanced_lines() =>
    clear_visuals()
    
    if array.size(levels) > 0
        // Sort levels by strength (count * volume_weight)
        sorted_levels = array.copy(levels)
        n = array.size(sorted_levels)
        for i = 0 to n - 2
            for j = 0 to n - i - 2
                level_a = array.get(sorted_levels, j)
                level_b = array.get(sorted_levels, j + 1)
                strength_a = level_a.count * (use_volume_weight ? level_a.volume_weight : 1)
                strength_b = level_b.count * (use_volume_weight ? level_b.volume_weight : 1)
                if strength_a < strength_b
                    array.set(sorted_levels, j, level_b)
                    array.set(sorted_levels, j + 1, level_a)
        
        // Find max strength for normalization
        max_strength = 0.0
        for level in sorted_levels
            strength = level.count * (use_volume_weight ? level.volume_weight : 1)
            max_strength := math.max(max_strength, strength)
        
        // Identify nearest levels to current price
        nearest_levels = array.new<float>()
        if highlight_nearest
            for level in sorted_levels
                if level.status != 2 or show_broken_levels
                    array.push(nearest_levels, math.abs(close - level.price))
            
            array.sort(nearest_levels, order.ascending)
        
        // Draw lines
        for i = 0 to math.min(array.size(sorted_levels) - 1, 100)
            level = array.get(sorted_levels, i)
            strength = level.count * (use_volume_weight ? level.volume_weight : 1)
            normalized_strength = strength / max_strength
            
            if level.count >= min_strength
                // Determine if this is a nearest level
                is_nearest = false
                if highlight_nearest and array.size(nearest_levels) > 0
                    distance = math.abs(close - level.price)
                    for j = 0 to math.min(nearest_count - 1, array.size(nearest_levels) - 1)
                        if distance == array.get(nearest_levels, j)
                            is_nearest := true
                            break
                
                // Color based on strength and status
                colorStep = max_strength / 6.0
                base_color = if strength <= colorStep 
                    color.green 
                else if strength <= 2 * colorStep 
                    color.aqua 
                else if strength <= 3 * colorStep 
                    color.blue 
                else if strength <= 4 * colorStep 
                    color.orange 
                else if strength <= 5 * colorStep 
                    color.purple 
                else 
                    color.red

                line_style = level.status == 2 ? line.style_solid : line.style_solid

                // Adjust transparency based on status
                transparency = if level.status == 0
                    50  // Untested - semi-transparent
                else if level.status == 2
                    broken_transparency  // Broken - use input
                else
                    0  // Respected - full opacity

                line_color = color.new(base_color, transparency)
                // line_color = level.is_broken ? color.new(base_color, broken_transparency) : base_color
                line_width = int(2 + (12 * (strength / max_strength)))  // 2-14 width range
                
                if is_nearest and level.status != 2
                    line_color := color.yellow
                    line_width := math.max(line_width, 3)
                
                // Create line
                if level.count >= min_strength and line_width >= min_line_width
                    new_line = line.new(x1=bar_index - 50, y1=level.price, x2=bar_index + 50, y2=level.price, color=line_color, width=line_width,extend=extend.both,style=line_style)
                    array.push(active_lines, new_line)
                
                // Add labels
                if show_labels or show_price_labels
                    label_text = ""
                    if show_price_labels
                        label_text += str.tostring(level.price, "#.####")
                    if show_labels
                        label_text += (show_price_labels ? "\n" : "") + "S:" + str.tostring(level.count) + (use_volume_weight ? " V:" + str.tostring(level.volume_weight, "#") : "")
                    if label_text != ""
                        new_label = label.new(x=bar_index + 55, y=level.price, text=label_text,style=label.style_none,textcolor=line_color,size=size.small)
                        array.push(active_labels, new_label)

// Create and populate info table
var table info_table = table.new(position=table_position == "top_left" ? position.top_left : table_position == "top_center" ? position.top_center :table_position == "top_right" ? position.top_right :table_position == "bottom_left" ? position.bottom_left :table_position == "bottom_center" ? position.bottom_center :position.bottom_right, columns=4, rows=max_table_rows + 1, border_width=1)

populate_enhanced_table() =>
    if display_table
        // Clear table
        table.clear(info_table, 0, 0, 3, max_table_rows)
        
        // Headers
        table.cell(info_table, 0, 0, "Price", text_color=color.white, bgcolor=color.black)
        table.cell(info_table, 1, 0, "Strength", text_color=color.white, bgcolor=color.black)
        table.cell(info_table, 2, 0, "TFs", text_color=color.white, bgcolor=color.black)
        table.cell(info_table, 3, 0, "Distance", text_color=color.white, bgcolor=color.black)
        
        // Sort by distance from current price
        sorted_for_table = array.copy(levels)
        // Manual sort by distance
        n_table = array.size(sorted_for_table)
        if n_table > 1
            for i = 0 to n_table - 2
                for j = 0 to n_table - i - 2
                    level_a = array.get(sorted_for_table, j)
                    level_b = array.get(sorted_for_table, j + 1)
                    dist_a = math.abs(close - level_a.price)
                    dist_b = math.abs(close - level_b.price)
                    if dist_a > dist_b
                        array.set(sorted_for_table, j, level_b)
                        array.set(sorted_for_table, j + 1, level_a)
        
        // Populate rows
        row = 1
        for i = 0 to math.min(array.size(sorted_for_table) - 1, max_table_rows - 1)
            level = array.get(sorted_for_table, i)
            if level.count >= min_strength
                distance = close - level.price
                distance_pct = (distance / close) * 100
                
                bg_color = if level.status == 2
                    color.new(color.gray, 70)
                else if distance > 0
                    color.new(color.red, 80)
                else
                    color.new(color.green, 80)
                
                table.cell(info_table, 0, row, str.tostring(level.price, "#.####"), text_color=color.white, bgcolor=bg_color)
                table.cell(info_table, 1, row, str.tostring(level.count), text_color=color.white, bgcolor=bg_color)
                table.cell(info_table, 2, row, str.tostring(array.size(level.timeframes)), text_color=color.white, bgcolor=bg_color)
                table.cell(info_table, 3, row, str.tostring(distance_pct, "#.##") + "%", text_color=color.white, bgcolor=bg_color)
                row += 1

// ════════════════════════════════════════
// MAIN EXECUTION
// ════════════════════════════════════════

// Reset on new bar
// if barstate.isfirst
//     array.clear(levels)

// Collect pivots from all timeframes
collect_pivots_enhanced(res1, include1)
collect_pivots_enhanced(res2, include2)
collect_pivots_enhanced(res3, include3)
collect_pivots_enhanced(res4, include4)
collect_pivots_enhanced(res5, include5)

// Check for broken levels
if show_broken_levels
    check_broken_levels()

// Draw visualization
if barstate.islast
    draw_enhanced_lines()
    populate_enhanced_table()

Overview

Support/Resistence MTF identifies turning points using ta.pivothigh and ta.pivotlow functions, then consolidates nearby levels into stronger zones based on a merge threshold. Levels are colored according to strength with adaptive line widths and optionally labeled. It includes a table of nearest levels and highlights recently broken levels. Main settings allow customizing analyzed timeframes, lookback periods, and visual options. It draws directly on the price chart and uses lookahead for higher timeframe data.

Details

Published by

obisin on GitHub

Draws

On the price chart

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
Bars to analyze per timeframeint250calc_bars_count
Pivot Lookback Periodint10pivot_lookback
Merge Threshold %float0.75merge_threshold
Resolution 1timeframe15res1
Include Resolution 1booltrueinclude1
Resolution 2timeframe60res2
Include Resolution 2booltrueinclude2
Resolution 3timeframe360res3
Include Resolution 3booltrueinclude3
Resolution 4timeframe720res4
Include Resolution 4booltrueinclude4
Resolution 5timeframeDres5
Show all 26 settings
SettingTypeDefaultVariable
Include Resolution 5boolfalseinclude5
Minimum Line Strengthint3min_strength
Show Strength Labelsbooltrueshow_labels
Show Price on Linesbooltrueshow_price_labels
Highlight Nearest Levelsbooltruehighlight_nearest
Number of Nearest Levelsint1nearest_count
Minimum Line Widthint3min_line_width
Use Volume Weightingboolfalseuse_volume_weight
Detect Level Clustersboolfalsedetect_clusters
Show Recently Broken Levelsbooltrueshow_broken_levels
Broken Level Transparencyint65broken_transparency
Display Info Tablebooltruedisplay_table
Table Positionstringbottom_righttable_position
Max Table Rowsint20max_table_rows
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

947 stars

Moving Average Channel plots separate simple moving averages for upper and lower bands on the price chart. It fills the space between bands with semi-transparent color and colors bars green above the upper band, red below the lower band, or gray in between. Us

Indicator v4 Moving averagesSupport & resistance
Updated 12 days ago · GPL-3.0 GitHub repository of Moving Average Channel

ICT Equal Highs and Lows

by ArunKBhaskar
575 stars

This overlay indicator finds pivot highs and lows on the price chart and compares the last two highs and the last two lows. When the percentage difference between a pair is below the Equal HL threshold, it treats them as equal levels and draws a box. Left and

Indicator v5 SMC / ICT / price actionSupport & resistance
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Equal Highs and Lows
330 stars

This indicator monitors multiple cryptocurrency symbols for trading signals. It calculates and plots entry prices, take profit, and stop loss levels for each symbol including BTCUSDT, ETHUSDT, BNBUSDT and others. Price crossovers trigger buy, sell or close ale

Indicator v5 Support & resistanceScreeners & dashboards
Updated 2 months ago · None stated GitHub repository of Multi Alert Long/Short [Alorse]

This overlay indicator draws a fixed-range volume profile on the price chart, splitting traded activity into up and down volume at each price level. It also plots a sentiment profile, a developing point of control, and value-area high and low around a configur

Indicator v5 VolumeSupport & resistance
Updated 2 months ago · MIT GitHub repository of Volume Profile and Indicator by DGT

BZ-tracker

by Allen1212
303 stars

The BZ-tracker indicator marks ARK Invest buy and sell transactions on stock charts with labels. It plots green labels for purchases and red labels for sales, each annotated with a multiplier such as X1 or X2 to show the number of ARK ETFs trading the stock on

Indicator v4 Support & resistanceScreeners & dashboards
Updated 2 years ago · None stated GitHub repository of BZ-tracker

Function - Linear Regression

by ricardosantos79
261 stars

This indicator fits a linear regression through stored pivot highs and lows and draws the resulting channel on the price chart. A function returns predicted values, maximum and minimum deviations, and the average absolute deviation. Confirmed two-bar pivots up

Indicator v5 TrendSupport & resistance
Updated 4 years ago · None stated GitHub repository of Function - Linear Regression

More Support & resistance scripts

Related on ForexR

Trade Your Charts With a Regulated Broker

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

Compare brokers