Indicator · Pine Script v5 · On the chart

Supply and Demand MTF

by mikeyhu70· code credited to fluxchart

Finds supply and demand zones from strong momentum candles and draws them as boxes on the price chart, with an option to pull zones from additional timeframes.

View on GitHub

GitHub stars1on the repository
Pine versionv5runs as is
Lines of code606
Settings38inputs
LicenceNone statedfile: MPL-2.0
Last update6 months ago15 Apr 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

606 lines of Pine Script v5 from Supply and Demand/Supply and Demand MTF.txt, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

Supply and Demand MTF.txt
// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © fluxchart

//https://www.tradingview.com/script/yq3DJDMc-Supply-Demand-MTF-Flux-Charts/

//@version=5
const bool DEBUG = true
const int maxBoxesCount = 500
const float overlapThresholdPercentage = 0.0
int maxDistanceToLastBar = 1250 // Affects Running Time
const int maxSDZones = 30
const int minZoneSize = 10
const int RETEST_COOLDOWN   = 5
const int minDistanceBetweenZones = 5
const float maxZoneSizeATR = 1.5

indicator(title = 'Supply & Demand (MTF)', shorttitle = "Supply and Demand (MTF)", overlay = true, max_boxes_count = maxBoxesCount, max_labels_count = maxBoxesCount, max_lines_count = maxBoxesCount, max_bars_back = 2000, dynamic_requests = true)

maxDistanceString   = input.string("Normal", "Max Distance To Last Bar", options = ["High", "Normal", "Low"],  group = "General Configuration", display = display.none)
sdEndMethod = input.string("Close", "Zone Invalidation", options = ["Wick", "Close"],  group = "General Configuration", display = display.none)
combineSDs = DEBUG ? input.bool(true, "Combine Zones", group = "General Configuration", display = display.none) : true
momentumBodyMult = DEBUG ? input.float(0.6, "Momentum Body Mult", step = 0.1, group = "General Configuration") : 0.5
momentumCount = DEBUG ? input.int(3,"Momentum Count", group = "General Configuration") : 4
momentumSpan = DEBUG ? input.int(3, "Momentum Span", group = "General Configuration") : 4
zoneCount = input.string("High", 'Zone Count', options = ["High", "Medium", "Low", "One"], tooltip = "Number of S&D Zones to be rendered. Higher options will result in older S&Ds shown.",  group = "General Configuration", display = display.none)
//zoneCount = "High"
retestsEnabled  = input.bool(false, "Retests", inline = "rb", group = "General Configuration", display = display.none)
breaksEnabled   = input.bool(false, "Breaks", inline = "rb", group = "General Configuration", display = display.none)
showInvalidated = input.bool(false, "Show Historic Zones", group = "General Configuration", display = display.none)
autoMode = input.bool(true, "Auto Mode (1H / 2H / 4H / D / W)", group = "General Configuration", display = display.none)
bullSDZoneColor = input(color.rgb(99, 226, 243), 'Demand', inline = 'sdColor', group = 'General Configuration', display = display.none)
bearSDZoneColor = input(color.rgb(192, 199, 98, 36), 'Supply', inline = 'sdColor', group = 'General Configuration', display = display.none)

// Auto Mode — per-timeframe toggles (only used when Auto Mode is on)
autoCurrentTFEnabled = input.bool(true,  "Current TF", group = "Auto Mode Timeframes", inline = "atf1")
auto30MEnabled       = input.bool(false, "30M",        group = "Auto Mode Timeframes", inline = "atf2")
auto1HEnabled        = input.bool(true,  "1H",         group = "Auto Mode Timeframes", inline = "atf3")
auto2HEnabled        = input.bool(true,  "2H",         group = "Auto Mode Timeframes", inline = "atf4")
auto4HEnabled        = input.bool(true,  "4H",         group = "Auto Mode Timeframes", inline = "atf5")
autoDEnabled         = input.bool(true,  "D",          group = "Auto Mode Timeframes", inline = "atf6")
autoWEnabled         = input.bool(true,  "W",          group = "Auto Mode Timeframes", inline = "atf7")


demandZones = zoneCount == "One" ? 1 : zoneCount == "Low" ? 3 : zoneCount == "Medium" ? 5 : 30
supplyZones = zoneCount == "One" ? 1 : zoneCount == "Low" ? 3 : zoneCount == "Medium" ? 5 : 30

// Manual timeframe slots — ignored when Auto Mode is enabled (Auto uses: Current / 30M / 1H / 2H / 4H / D / W)
timeframe1Enabled = input.bool(true,  title = "", group = "Timeframes", inline = "timeframe1", display = display.none, tooltip = "Ignored when Auto Mode is on")
timeframe1 = input.timeframe("",   title = "", group = "Timeframes", inline = "timeframe1", display = display.none)
timeframe2Enabled = input.bool(false, title = "", group = "Timeframes", inline = "timeframe2", display = display.none)
timeframe2 = input.timeframe("30", title = "", group = "Timeframes", inline = "timeframe2", display = display.none)
timeframe3Enabled = input.bool(false, title = "", group = "Timeframes", inline = "timeframe3", display = display.none)
timeframe3 = input.timeframe("60", title = "", group = "Timeframes", inline = "timeframe3", display = display.none)
timeframe4Enabled = input.bool(false, title = "", group = "Timeframes", inline = "timeframe4", display = display.none)
timeframe4 = input.timeframe("120",  title = "", group = "Timeframes", inline = "timeframe4", display = display.none)
timeframe5Enabled = input.bool(false, title = "", group = "Timeframes", inline = "timeframe5", display = display.none)
timeframe5 = input.timeframe("240",  title = "", group = "Timeframes", inline = "timeframe5", display = display.none)
timeframe6Enabled = input.bool(false, title = "", group = "Timeframes", inline = "timeframe6", display = display.none)
timeframe6 = input.timeframe("D",   title = "", group = "Timeframes", inline = "timeframe6", display = display.none)
timeframe7Enabled = input.bool(false, title = "", group = "Timeframes", inline = "timeframe7", display = display.none)
timeframe7 = input.timeframe("W",   title = "", group = "Timeframes", inline = "timeframe7", display = display.none)

// Auto mode overrides: Current TF / 30M / 1H / 2H / 4H / D / W  (manual inputs ignored when autoMode = true)
eff1Enabled = autoMode ? autoCurrentTFEnabled : timeframe1Enabled
eff1TF      = autoMode ? ""    : timeframe1
eff2Enabled = autoMode ? auto30MEnabled       : timeframe2Enabled
eff2TF      = autoMode ? "30"  : timeframe2
eff3Enabled = autoMode ? auto1HEnabled        : timeframe3Enabled
eff3TF      = autoMode ? "60"  : timeframe3
eff4Enabled = autoMode ? auto2HEnabled        : timeframe4Enabled
eff4TF      = autoMode ? "120" : timeframe4
eff5Enabled = autoMode ? auto4HEnabled        : timeframe5Enabled
eff5TF      = autoMode ? "240" : timeframe5
eff6Enabled = autoMode ? autoDEnabled         : timeframe6Enabled
eff6TF      = autoMode ? "D"   : timeframe6
eff7Enabled = autoMode ? autoWEnabled         : timeframe7Enabled
eff7TF      = autoMode ? "W"   : timeframe7

textColor        = input.color(#ffffffcc, "Text Color",        group = "Style")
zoneTransparency = input.int(70, "Zone Transparency", minval = 0, maxval = 100, step = 5, group = "Style", tooltip = "0 = fully opaque, 100 = fully transparent")
rightBarOffset   = input.int(50, "Extend Right (bars)", minval = 0, group = "Style", tooltip = "Extend active zones to the right by this many bars")
labelsAtSameLevel   = DEBUG ? input.bool(true, "[DBG] Place Labels At Same Level", group = "Style") : true
labelsAtSameLevelBreak = false

atr = ta.atr(20)
averageBodySize = ta.sma(math.abs(close - open), 20)

maxDistanceToLastBar := maxDistanceString == "Low" ? 150 : maxDistanceString == "Normal" ? 500 : 1250

type sdZoneInfo
    float top
    float bottom
    string sdType
    int startTime
    int breakTime
    int guid
    string timeframeStr
    bool disabled = false
    string combinedTimeframesStr = na
    bool combined = false

type sdZone
    sdZoneInfo info
    bool isRendered = false

    box sdBox = na

    line sdBoxLineTop = na
    line sdBoxLineMiddle = na
    line sdBoxLineBottom = na
    //
    box sdBoxText = na

type retestLabelContainer
    int guid
    array<label> labels

createSDZone (sdZoneInfo sdZoneInfoF) =>
    sdZone newSDZone = sdZone.new(sdZoneInfoF)
    newSDZone

safeDeleteSDZone (sdZone sdZoneF) =>
    sdZoneF.isRendered := false

    box.delete(sdZoneF.sdBox)
    box.delete(sdZoneF.sdBoxText)

    line.delete(sdZoneF.sdBoxLineTop)
    line.delete(sdZoneF.sdBoxLineMiddle)
    line.delete(sdZoneF.sdBoxLineBottom)

type timeframeInfo
    int index = na
    string timeframeStr = na
    bool isEnabled = false

    sdZoneInfo[] demandZonesList = na
    sdZoneInfo[] supplyZonesList = na

newTimeframeInfo (index, timeframeStr, isEnabled) =>
    newTFInfo = timeframeInfo.new()
    newTFInfo.index := index
    newTFInfo.isEnabled := isEnabled
    newTFInfo.timeframeStr := timeframeStr

    newTFInfo

// ____ TYPES END ____

var timeframeInfo[] timeframeInfos = array.from(newTimeframeInfo(1, eff1TF, eff1Enabled), newTimeframeInfo(2, eff2TF, eff2Enabled), newTimeframeInfo(3, eff3TF, eff3Enabled), newTimeframeInfo(4, eff4TF, eff4Enabled), newTimeframeInfo(5, eff5TF, eff5Enabled), newTimeframeInfo(6, eff6TF, eff6Enabled), newTimeframeInfo(7, eff7TF, eff7Enabled))
var demandZonesList = array.new<sdZoneInfo>(0)
var supplyZonesList = array.new<sdZoneInfo>(0)
var breakLabels = map.new<int, label>()
var retestLabels = map.new<int, retestLabelContainer>()

var int oldestBarTime = na
if bar_index == last_bar_index - maxDistanceToLastBar
    oldestBarTime := time

var allSDZonesList = array.new<sdZone>(0)

moveLine(_line, _x, _y, _x2) =>
    line.set_xy1(_line, _x,  _y)
    line.set_xy2(_line, _x2, _y)

moveBox (_box, _topLeftX, _topLeftY, _bottomRightX, _bottomRightY) =>
    box.set_lefttop(_box, _topLeftX, _topLeftY)
    box.set_rightbottom(_box, _bottomRightX, _bottomRightY)

isTimeframeLower (timeframe1F, timeframe2F) =>
    timeframe.in_seconds(timeframe1F) < timeframe.in_seconds(timeframe2F)

getMinTimeframe (timeframe1F, timeframe2F) =>
    if isTimeframeLower(timeframe1F, timeframe2F)
        timeframe1F
    else
        timeframe2F

getMaxTimeframe (timeframe1F, timeframe2F) =>
    if isTimeframeLower(timeframe1F, timeframe2F)
        timeframe2F
    else
        timeframe1F

formatTimeframeString (formatTimeframe) =>
    timeframeF = formatTimeframe == "" ? timeframe.period : formatTimeframe
    
    if str.contains(timeframeF, "D") or str.contains(timeframeF, "W") or str.contains(timeframeF, "S") or str.contains(timeframeF, "M")
        timeframeF
    else
        seconds = timeframe.in_seconds(timeframeF)
        if seconds >= 3600
            hourCount = int(seconds / 3600)
            str.tostring(hourCount) + " Hour" + (hourCount > 1 ? "s" : "")
        else
            timeframeF + " Min"

colorWithTransparency (colorF, transparencyX) =>
    color.new(colorF, color.t(colorF) * transparencyX)

createSDBox (boxColor, transparencyX = 1.0, xlocType = xloc.bar_time) =>
    box.new(na, na, na, na, xloc = xlocType, extend = extend.none, bgcolor = colorWithTransparency(boxColor, transparencyX), text_color = textColor, text_halign = text.align_right, text_valign = text.align_bottom, text_size = size.small, border_color = boxColor)

renderSDZone (sdZone sd) =>
    sdZoneInfo info = sd.info
    
    sd.isRendered := true
    
    sdColor = sd.info.sdType == "Demand" ? bullSDZoneColor : bearSDZoneColor

    int zoneSize = na
    if na(info.breakTime)
        zoneSize := (time + 1) - info.startTime
    else
        zoneSize := (info.breakTime - info.startTime)

    render = true
    if zoneSize < timeframe.in_seconds(info.timeframeStr) * minZoneSize * 1000
        render := false
    if info.startTime < nz(oldestBarTime, time)
        render := false


    if render and (showInvalidated or (na(sd.info.breakTime)))
        sdColorT = color.new(sdColor, zoneTransparency)
        sd.sdBox := createSDBox(sdColorT, 1.0)
        if sd.info.combined
            sd.sdBox.set_bgcolor(color.new(sdColor, math.min(100, zoneTransparency + 10)))

        startX = info.startTime
        rightOffsetMs = na(info.breakTime) ? rightBarOffset * timeframe.in_seconds() * 1000 : 0
        endX    = info.startTime + zoneSize + rightOffsetMs
        maxEndX = info.startTime + zoneSize / 2

        float middlePoint = (info.top + info.bottom) / 2
        moveBox(sd.sdBox, info.startTime, info.top, endX, info.bottom)

        sd.sdBoxLineMiddle := line.new(info.startTime, middlePoint, endX, middlePoint, xloc = xloc.bar_time, color = textColor, style = line.style_dotted)

        sd.sdBoxText := createSDBox(color.new(color.white, 100))
        moveBox(sd.sdBoxText, maxEndX, middlePoint, endX, info.bottom)
        SDText = (na(sd.info.combinedTimeframesStr) ? formatTimeframeString(sd.info.timeframeStr) : sd.info.combinedTimeframesStr) + " " + sd.info.sdType
        //box.set_text(sd.sdBoxText, SDText)
        boxText = na(sd.info.combinedTimeframesStr) ? formatTimeframeString(sd.info.timeframeStr) : sd.info.combinedTimeframesStr
        //if DEBUG
        //    boxText += " | " + str.tostring(sd.info.guid)
        box.set_text(sd.sdBoxText, boxText)
        

areaOfSD (sdZoneInfo SDInfoF) =>
    float XA1 = SDInfoF.startTime
    float XA2 = na(SDInfoF.breakTime) ? time + 1 : SDInfoF.breakTime
    float YA1 = SDInfoF.top
    float YA2 = SDInfoF.bottom
    float edge1 = math.sqrt((XA2 - XA1) * (XA2 - XA1) + (YA2 - YA2) * (YA2 - YA2))
    float edge2 = math.sqrt((XA2 - XA2) * (XA2 - XA2) + (YA2 - YA1) * (YA2 - YA1))
    float totalArea = edge1 * edge2
    totalArea

doSDsTouch (sdZoneInfo SDInfo1, sdZoneInfo SDInfo2) =>
    float XA1 = SDInfo1.startTime
    float XA2 = na(SDInfo1.breakTime) ? (time + 1) : SDInfo1.breakTime
    float YA1 = SDInfo1.top + atr / 100
    float YA2 = SDInfo1.bottom - atr / 100

    float XB1 = SDInfo2.startTime
    float XB2 = na(SDInfo2.breakTime) ? (time + 1) : SDInfo2.breakTime
    float YB1 = SDInfo2.top + atr / 100
    float YB2 = SDInfo2.bottom - atr / 100
    float intersectionArea = math.max(0, math.min(XA2, XB2) - math.max(XA1, XB1)) * math.max(0, math.min(YA1, YB1) - math.max(YA2, YB2))
    float unionArea = areaOfSD(SDInfo1) + areaOfSD(SDInfo2) - intersectionArea
    
    float overlapPercentage = (intersectionArea / unionArea) * 100.0

    if overlapPercentage > overlapThresholdPercentage
        true
    else
        false

isSDValid (sdZoneInfo SDInfo) =>
    valid = true
    if SDInfo.disabled
        valid := false
    valid

clampSDZone (sdZoneInfo sdZoneF) =>
    sdZoneSize = sdZoneF.top - sdZoneF.bottom
    if sdZoneSize > atr * maxZoneSizeATR
        diff = sdZoneSize - (atr * maxZoneSizeATR)
        sdZoneF.top -= diff / 2
        sdZoneF.bottom += diff / 2

combineSDsFunc () =>
    if allSDZonesList.size() > 0
        lastCombinations = 999
        while lastCombinations > 0
            lastCombinations := 0
            for i = 0 to allSDZonesList.size() - 1
                curSD1 = allSDZonesList.get(i)
                for j = 0 to allSDZonesList.size() - 1
                    curSD2 = allSDZonesList.get(j)
                    if i == j
                        continue
                    if not isSDValid(curSD1.info) or not isSDValid(curSD2.info)
                        continue
                    if curSD1.info.sdType != curSD2.info.sdType
                        continue
                    if doSDsTouch(curSD1.info, curSD2.info)
                        curSD1.info.disabled := true
                        curSD2.info.disabled := true

                        sdZone newSD = createSDZone(sdZoneInfo.new(math.max(curSD1.info.top, curSD2.info.top), math.min(curSD1.info.bottom, curSD2.info.bottom), curSD1.info.sdType))
                        newSD.info.startTime := math.min(curSD1.info.startTime, curSD2.info.startTime)
                        newSD.info.breakTime := math.max(nz(curSD1.info.breakTime), nz(curSD2.info.breakTime))
                        newSD.info.breakTime := newSD.info.breakTime == 0 ? na : newSD.info.breakTime
                        newSD.info.guid := newSD.info.startTime
                        newSD.info.timeframeStr := curSD1.info.timeframeStr
                        clampSDZone(newSD.info)
                        
                        newSD.info.combined := true
                        if timeframe.in_seconds(curSD1.info.timeframeStr) != timeframe.in_seconds(curSD2.info.timeframeStr)
                            newSD.info.combinedTimeframesStr := (na(curSD1.info.combinedTimeframesStr) ? formatTimeframeString(curSD1.info.timeframeStr) : curSD1.info.combinedTimeframesStr) + " & " + (na(curSD2.info.combinedTimeframesStr) ? formatTimeframeString(curSD2.info.timeframeStr) : curSD2.info.combinedTimeframesStr)
                        allSDZonesList.unshift(newSD)
                        lastCombinations += 1


reqSeq (timeframeStr) =>
    if timeframe.in_seconds(timeframeStr) == timeframe.in_seconds()
        [demandZonesList, supplyZonesList]
    else
        [demandZonesListF, supplyZonesListF] = request.security(syminfo.tickerid, timeframeStr, [demandZonesList, supplyZonesList])
        [demandZonesListF, supplyZonesListF]

getTFData (timeframeInfo timeframeInfoF, timeframeStr) =>
    if timeframeInfoF.isEnabled
        [demandZonesListF, supplyZonesListF] = reqSeq(timeframeStr)
        [demandZonesListF, supplyZonesListF]
    else
        [na, na]

handleTimeframeInfo (timeframeInfo timeframeInfoF, demandZonesListF, supplyZonesListF) =>
    if timeframeInfoF.isEnabled
        timeframeInfoF.demandZonesList := demandZonesListF
        timeframeInfoF.supplyZonesList := supplyZonesListF

handleSDZonesFinal () =>
    if DEBUG
        log.info("Demand Count " + str.tostring(demandZonesList.size()))
        log.info("Supply Count " + str.tostring(supplyZonesList.size()))
        log.info("All " + str.tostring(allSDZonesList.size()))
        log.info("Max " + str.tostring(demandZones))

    if allSDZonesList.size() > 0
        for i = 0 to allSDZonesList.size() - 1
            safeDeleteSDZone(allSDZonesList.get(i))
    allSDZonesList.clear()    

    for i = 0 to timeframeInfos.size() - 1
        curTimeframe = timeframeInfos.get(i)
        if not curTimeframe.isEnabled
            continue
        if curTimeframe.demandZonesList.size() > 0
            for j = 0 to math.min(curTimeframe.demandZonesList.size() - 1, demandZones - 1)
                sdZoneInfoF = curTimeframe.demandZonesList.get(j)
                sdZoneInfoF.timeframeStr := curTimeframe.timeframeStr
                allSDZonesList.unshift(createSDZone(sdZoneInfo.copy(sdZoneInfoF)))

        if curTimeframe.supplyZonesList.size() > 0
            for j = 0 to math.min(curTimeframe.supplyZonesList.size() - 1, supplyZones - 1)
                sdZoneInfoF = curTimeframe.supplyZonesList.get(j)
                sdZoneInfoF.timeframeStr := curTimeframe.timeframeStr
                allSDZonesList.unshift(createSDZone(sdZoneInfo.copy(sdZoneInfoF)))

    if combineSDs
        combineSDsFunc()

    if allSDZonesList.size() > 0
        for i = 0 to allSDZonesList.size() - 1
            curSD = allSDZonesList.get(i)
            if isSDValid(curSD.info)
                renderSDZone(curSD)

bodySize = math.abs(close - open)
getMomentumCandleCount (lastBars, reqMult) =>
    bearishCnt = 0
    bullishCnt = 0
    for i = 0 to lastBars - 1
        if bodySize[i] >= averageBodySize * reqMult
            if close[i] > open[i]
                bullishCnt += 1
            else
                bearishCnt += 1
    [bullishCnt, bearishCnt]

[bullishMomentum, bearishMomentum] = getMomentumCandleCount(momentumSpan, momentumBodyMult)

var int lastDemandZone = 0
var int lastSupplyZone = 0
// Find Supply & Demand
if bar_index > last_bar_index - maxDistanceToLastBar
    if bullishMomentum >= momentumCount and bar_index - lastDemandZone > minDistanceBetweenZones
        lastDemandZone := bar_index
        newSDZone = sdZoneInfo.new(high[momentumSpan + 1], low[momentumSpan + 1], "Demand", time[momentumSpan + 1], na, time[momentumSpan + 1])
        clampSDZone(newSDZone)
        demandZonesList.unshift(newSDZone)
        if demandZonesList.size() > maxSDZones
            demandZonesList.pop()
    if bearishMomentum >= momentumCount and bar_index - lastSupplyZone > minDistanceBetweenZones
        lastSupplyZone := bar_index
        newSDZone = sdZoneInfo.new(high[momentumSpan + 1], low[momentumSpan + 1], "Supply", time[momentumSpan + 1], na, time[momentumSpan + 1])
        clampSDZone(newSDZone)
        supplyZonesList.unshift(newSDZone)
        if supplyZonesList.size() > maxSDZones
            supplyZonesList.pop()

    // Invalidation
    if demandZonesList.size() > 0
        for i = demandZonesList.size() - 1 to 0
            currentSD = demandZonesList.get(i)
        
            if na(currentSD.breakTime) 
                if (sdEndMethod == "Wick" ? low : math.min(open, close)) < currentSD.bottom
                    currentSD.breakTime := time

    if supplyZonesList.size() > 0
        for i = supplyZonesList.size() - 1 to 0
            currentSD = supplyZonesList.get(i)

            if na(currentSD.breakTime) 
                if (sdEndMethod == "Wick" ? high : math.max(open, close)) > currentSD.top
                    currentSD.breakTime := time

[demandZonesListTimeframe1, supplyZonesListTimeframe1] = getTFData(timeframeInfos.get(0), eff1TF)
[demandZonesListTimeframe2, supplyZonesListTimeframe2] = getTFData(timeframeInfos.get(1), eff2TF)
[demandZonesListTimeframe3, supplyZonesListTimeframe3] = getTFData(timeframeInfos.get(2), eff3TF)
[demandZonesListTimeframe4, supplyZonesListTimeframe4] = getTFData(timeframeInfos.get(3), eff4TF)
[demandZonesListTimeframe5, supplyZonesListTimeframe5] = getTFData(timeframeInfos.get(4), eff5TF)
[demandZonesListTimeframe6, supplyZonesListTimeframe6] = getTFData(timeframeInfos.get(5), eff6TF)
[demandZonesListTimeframe7, supplyZonesListTimeframe7] = getTFData(timeframeInfos.get(6), eff7TF)

var lastRetestIndexSupply = 0
var lastRetestIndexDemand = 0

float renderRetestLabelBuyside = na
int renderRetestLabelBuysideGUID = na

float renderRetestLabelSellside = na
int renderRetestLabelSellsideGUID = na

float renderBreakLabelBuyside = na
int renderBreakLabelBuysideGUID = na

float renderBreakLabelSellside = na
int renderBreakLabelSellsideGUID = na

var disabledDuplicateTF = false
// Disable Duplicate Timeframes
if not disabledDuplicateTF
    disabledDuplicateTF := true
    for i = 0 to timeframeInfos.size() - 1
        for j = 0 to timeframeInfos.size() - 1
            if i == j
                continue
            timeframeInfo1 = timeframeInfos.get(i)
            timeframeInfo2 = timeframeInfos.get(j)
            if timeframeInfo1.isEnabled and timeframeInfo2.isEnabled and timeframe.in_seconds(timeframeInfo1.timeframeStr) == timeframe.in_seconds(timeframeInfo2.timeframeStr)
                timeframeInfo1.isEnabled := false

if barstate.isconfirmed and bar_index > last_bar_index - maxDistanceToLastBar
    handleTimeframeInfo(timeframeInfos.get(0), demandZonesListTimeframe1, supplyZonesListTimeframe1)
    handleTimeframeInfo(timeframeInfos.get(1), demandZonesListTimeframe2, supplyZonesListTimeframe2)
    handleTimeframeInfo(timeframeInfos.get(2), demandZonesListTimeframe3, supplyZonesListTimeframe3)
    handleTimeframeInfo(timeframeInfos.get(3), demandZonesListTimeframe4, supplyZonesListTimeframe4)
    handleTimeframeInfo(timeframeInfos.get(4), demandZonesListTimeframe5, supplyZonesListTimeframe5)
    handleTimeframeInfo(timeframeInfos.get(5), demandZonesListTimeframe6, supplyZonesListTimeframe6)
    handleTimeframeInfo(timeframeInfos.get(6), demandZonesListTimeframe7, supplyZonesListTimeframe7)
    handleSDZonesFinal()

    // Breaks    

    if allSDZonesList.size() > 0
        for i = 0 to allSDZonesList.size() - 1
            curZone = allSDZonesList.get(i)
            if curZone.info.disabled
                continue
            if not showInvalidated and not na(curZone.info.breakTime)
                continue
            if na(curZone.info.breakTime)
                continue
            if time - curZone.info.startTime < minZoneSize * timeframe.in_seconds(curZone.info.timeframeStr) * 1000
                continue
            if curZone.info.startTime < nz(oldestBarTime, time)
                continue
            
            if time == curZone.info.breakTime
                if curZone.info.sdType == "Supply"
                    if curZone.info.breakTime - curZone.info.startTime > minZoneSize * timeframe.in_seconds() * 1000
                        renderBreakLabelBuyside := curZone.info.bottom
                        renderBreakLabelBuysideGUID := curZone.info.guid
                else
                    if curZone.info.breakTime - curZone.info.startTime > minZoneSize * timeframe.in_seconds() * 1000
                        renderBreakLabelSellside := curZone.info.top
                        renderBreakLabelSellsideGUID := curZone.info.guid
    
    // Retests
    if allSDZonesList.size() > 0
        for i = 0 to allSDZonesList.size() - 1
            curZone = allSDZonesList.get(i)
            
            if curZone.info.disabled
                continue
            if not showInvalidated and not na(curZone.info.breakTime)
                continue
            if not na(curZone.info.breakTime)
                continue
            if time - curZone.info.startTime < minZoneSize * timeframe.in_seconds(curZone.info.timeframeStr) * 1000
                continue
            if curZone.info.startTime < nz(oldestBarTime, time)
                continue
            
            middleLine = (curZone.info.bottom + curZone.info.top) / 2.0
            if curZone.info.sdType == "Supply" and bar_index - lastRetestIndexSupply > RETEST_COOLDOWN
                if high > curZone.info.bottom
                    renderRetestLabelBuyside := curZone.info.top
                    renderRetestLabelBuysideGUID := curZone.info.guid
                    lastRetestIndexSupply := bar_index
            else if curZone.info.sdType == "Demand" and bar_index - lastRetestIndexDemand > RETEST_COOLDOWN
                if low < curZone.info.top
                    renderRetestLabelSellside := curZone.info.bottom
                    renderRetestLabelSellsideGUID := curZone.info.guid
                    lastRetestIndexDemand := bar_index

//plotshape(not na(renderRetestLabelBuyside) and retestsEnabled ? renderRetestLabelBuyside : na, "", shape.labeldown, color = bearSDZoneColor, text = "R", location = labelsAtSameLevel ? location.absolute : location.abovebar, textcolor = color.white, size = size.small)
//plotshape(not na(renderRetestLabelSellside) and retestsEnabled ? renderRetestLabelSellside : na, "", shape.labelup, color = bullSDZoneColor, text = "R", location = labelsAtSameLevel ? location.absolute : location.belowbar, textcolor = color.white, size = size.small)

// Retests

if not na(renderRetestLabelBuyside) and retestsEnabled
    newLabel = label.new(bar_index, renderRetestLabelBuyside, style = label.style_label_down, color = bearSDZoneColor, text = "R", textcolor = color.white, size = size.small)
    //label.new(bar_index, renderRetestLabelSellside, style = label.style_label_up, color = bullSDZoneColor, text = "R", textcolor = color.white, size = size.small)
    if na(retestLabels.get(renderRetestLabelBuysideGUID))
        newContainer = retestLabelContainer.new(renderRetestLabelBuysideGUID)
        newContainer.labels := array.new<label>()
        newContainer.labels.push(newLabel)
        retestLabels.put(renderRetestLabelBuysideGUID, newContainer)
    else
        retestLabels.get(renderRetestLabelBuysideGUID).labels.push(newLabel)

if not na(renderRetestLabelSellside) and retestsEnabled
    newLabel = label.new(bar_index, renderRetestLabelSellside, style = label.style_label_up, color = bullSDZoneColor, text = "R", textcolor = color.white, size = size.small)
    if na(retestLabels.get(renderRetestLabelSellsideGUID))
        newContainer = retestLabelContainer.new(renderRetestLabelSellsideGUID)
        newContainer.labels := array.new<label>()
        newContainer.labels.push(newLabel)
        retestLabels.put(renderRetestLabelSellsideGUID, newContainer)
    else
        retestLabels.get(renderRetestLabelSellsideGUID).labels.push(newLabel)


if retestLabels.keys().size() > 0
    for i = 0 to retestLabels.keys().size() - 1
        curKey = retestLabels.keys().get(i)
        foundKey = false
        if allSDZonesList.size() > 0
            for j = 0 to allSDZonesList.size() - 1
                if allSDZonesList.get(j).info.guid == curKey
                    if allSDZonesList.get(j).info.disabled
                        continue
                    if not showInvalidated and not na(allSDZonesList.get(j).info.breakTime)
                        continue
                    if time - allSDZonesList.get(j).info.startTime < minZoneSize * timeframe.in_seconds(allSDZonesList.get(j).info.timeframeStr) * 1000
                        continue
                    if allSDZonesList.get(j).info.startTime < nz(oldestBarTime, time)
                        continue
                    foundKey := true
                    break
        if not foundKey
            for j = 0 to retestLabels.get(curKey).labels.size() - 1
                label.delete(retestLabels.get(curKey).labels.get(j))

// Breaks
if not na(renderBreakLabelBuyside) and breaksEnabled
    breakLabels.put(renderBreakLabelBuysideGUID, label.new(bar_index, renderBreakLabelBuyside, style = label.style_label_up, color = color.blue, text = "B", textcolor = color.white, size = size.small))

if not na(renderBreakLabelSellside) and breaksEnabled
    breakLabels.put(renderBreakLabelSellsideGUID, label.new(bar_index, renderBreakLabelSellside, style = label.style_label_down, color = color.blue, text = "B", textcolor = color.white, size = size.small))

if breakLabels.keys().size() > 0
    for i = 0 to breakLabels.keys().size() - 1
        curKey = breakLabels.keys().get(i)
        foundKey = false
        if allSDZonesList.size() > 0
            for j = 0 to allSDZonesList.size() - 1
                if allSDZonesList.get(j).info.guid == curKey
                    if allSDZonesList.get(j).info.disabled
                        continue
                    foundKey := true
                    break
        if not foundKey
            label.delete(breakLabels.get(curKey))

alertcondition(not na(renderRetestLabelBuyside) and barstate.isconfirmed, "Supply Zone Retest @ {{ticker}}", "Supply Zone Retest @ {{ticker}}")
alertcondition(not na(renderRetestLabelSellside) and barstate.isconfirmed, "Demand Zone Retest @ {{ticker}}", "Demand Zone Retest @ {{ticker}}")

alertcondition(not na(renderBreakLabelBuyside) and barstate.isconfirmed, "Supply Zone Break @ {{ticker}}", "Supply Zone Break @ {{ticker}}")
alertcondition(not na(renderBreakLabelSellside) and barstate.isconfirmed, "Demand Zone Break @ {{ticker}}", "Demand Zone Break @ {{ticker}}")

Overview

Settings control momentum body multiple, momentum count and span, how far back zones are searched, whether Wick or Close invalidates a zone, zone combining, zone count, and optional markers for retests and breaks. Historic invalidated zones can be shown. It marks areas only and gives no entry or exit rules.

Details

Published by

mikeyhu70 on GitHub

Code credit

fluxchart, as written in the script's header

Draws

On the price chart

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Settings

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

SettingTypeDefaultVariable
Max Distance To Last BarstringNormalmaxDistanceString
Zone InvalidationstringClosesdEndMethod
Combine ZonesbooltrueCombine Zones
Momentum Body Multfloat0.6Momentum Body Mult
Momentum Countint3Momentum Count
Momentum Spanint3Momentum Span
Zone CountstringHighzoneCount
RetestsboolfalseretestsEnabled
BreaksboolfalsebreaksEnabled
Show Historic ZonesboolfalseshowInvalidated
Auto Mode (1H / 2H / 4H / D / W)booltrueautoMode
Demandcolorcolor.rgb(99, 226, 243)bullSDZoneColor
Show all 38 settings
SettingTypeDefaultVariable
Supplycolorcolor.rgb(192, 199, 98, 36)bearSDZoneColor
Current TFbooltrueautoCurrentTFEnabled
30Mboolfalseauto30MEnabled
1Hbooltrueauto1HEnabled
2Hbooltrueauto2HEnabled
4Hbooltrueauto4HEnabled
DbooltrueautoDEnabled
WbooltrueautoWEnabled
timeframe1Enabledbooltruetimeframe1Enabled
timeframe1timeframetimeframe1
timeframe2Enabledboolfalsetimeframe2Enabled
timeframe2timeframe30timeframe2
timeframe3Enabledboolfalsetimeframe3Enabled
timeframe3timeframe60timeframe3
timeframe4Enabledboolfalsetimeframe4Enabled
timeframe4timeframe120timeframe4
timeframe5Enabledboolfalsetimeframe5Enabled
timeframe5timeframe240timeframe5
timeframe6Enabledboolfalsetimeframe6Enabled
timeframe6timeframeDtimeframe6
timeframe7Enabledboolfalsetimeframe7Enabled
timeframe7timeframeWtimeframe7
Text Colorcolor#ffffffcctextColor
Zone Transparencyint70zoneTransparency
Extend Right (bars)int50rightBarOffset
[DBG] Place Labels At Same Levelbooltrue[DBG] Place Labels At Same Level
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

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

DE-tracker

by Allen1212
303 stars

This indicator plots labels on the TradingView price chart to mark ARK Invest's buy and sell trades for the DE stock. Buy transactions appear in green with X1 or X2 multipliers showing the number of ARK ETFs involved on the same day; sells appear in red. The m

Indicator v4 Support & resistanceSMC / ICT / price action
Updated 2 years ago · None stated GitHub repository of DE-tracker

This Pine Script indicator plots a ZigZag on the price chart. It identifies reversal points when price moves more than a set percentage from the last pivot. The script calculates forecasts using zigzag data and labels detected geometric patterns including harm

Indicator v4 TrendSupport & resistance

Golden Bullet Strategy

by TradersPost
228 stars

The Golden Bullet Strategy is an overlay signal indicator based on Smart Money Concepts and time-based liquidity models. It identifies liquidity sweeps of a designated reference session range during a specified hunt window. It then confirms Market Structure Sh

Indicator v6 SMC / ICT / price actionSessions & time
Updated 3 months ago · None stated GitHub repository of Golden Bullet Strategy

Open Source Fractal

by cantolab
183 stars

This indicator implements the Fractal Model concept to plot market structure shifts, liquidity sweeps, and Change in State of Delivery (CISD) levels. It overlays custom higher-timeframe candle previews, open prices, and time markers directly on the price chart

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 2 months ago · MIT GitHub repository of Open Source Fractal

Swing Failure Signals

by Ganador1
154 stars

This indicator calculates pivot highs and pivot lows to detect liquidity sweeps and Swing Failure Patterns. It identifies when price breaks past recent key swing highs or lows within a configurable historical window, then looks for a quick reversal or change i

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 2 months ago · Apache-2.0 GitHub repository of Swing Failure Signals

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