Indicator · Pine Script v5 · On the chart

Volumized Order Blocks 2026-3

by mikeyhu70· code credited to fluxchart

A Pine Script v5 indicator that draws bullish and bearish order block zones on the price chart with volume shown inside, and adds lower-timeframe blocks within range.

View on GitHub

GitHub stars1on the repository
Pine versionv5runs as is
Lines of code691
Settings25inputs
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

691 lines of Pine Script v5 from Order Block/Volumized Order Block 2026-3.txt, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

Volumized Order Block 2026-3.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

//@version=5

//- Chart TF ≤ 14400s (4H and below) → LTF runs
//- Chart TF > 14400s (6H, 8H, 12H, 16H, daily, weekly…) → LTF skipped

//The ltfMaxChartSecs input lets you raise that ceiling if needed (e.g. set to 21600 for 6H on SPXL), but the default of 14400 is the safe line for
//long-history assets like SPY/QQQ.

const bool DEBUG = true
const int maxBoxesCount = 500
const float overlapThresholdPercentage = 0
const int maxDistanceToLastBar = 1750 // Affects Running Time
const int maxOrderBlocks = 30

indicator(title = 'Volumized Order Blocks 2026-3', overlay = true, max_boxes_count = maxBoxesCount, max_labels_count = maxBoxesCount, max_lines_count = maxBoxesCount, max_bars_back = 5000)

showInvalidated = input.bool(false, "Show Historic Zones", group = "General Configuration", display = display.none)
OBsEnabled = true
orderBlockVolumetricInfo = input.bool(true, "Volumetric Info", group = "General Configuration", inline="EV", display = display.none)
obEndMethod = input.string("Close", "Zone Invalidation", options = ["Wick", "Close"],  group = "General Configuration", display = display.none)
combineOBs = DEBUG ? input.bool(true, "Combine Zones", group = "General Configuration", display = display.none) : true
maxATRMult = DEBUG ? input.float(1.618,"Max Atr Multiplier", group = "General Configuration") : 3.5
swingLength = input.int(5, 'Swing Length', minval = 3, tooltip="Swing length is used when finding order block formations. Smaller values will result in finding smaller order blocks.",group = "General Configuration", display = display.none)

zoneCount = input.string("3", 'Zone Count', options = ["High", "Medium", "Low", "3", "2", "One"], tooltip = "Number of Order Block Zones to be rendered. Higher options will result in older Order Blocks shown.",  group = "General Configuration", display = display.none)

bullOrderBlockColor = input(color.rgb(26, 169, 145, 87), 'Bullish', inline = 'obColor', group = 'General Configuration', display = display.none)
bearOrderBlockColor = input(color.rgb(184, 41, 127, 50), 'Bearish', inline = 'obColor', group = 'General Configuration', display = display.none)

// LTF Configuration
ltfEnabled = input.bool(true, "Show LTF Blocks Within Range", group = "LTF Configuration", display = display.none)
ltfAutoEnabled = input.bool(true, "Auto (2 TFs Lower)", group = "LTF Configuration", tooltip = "Automatically selects the 2 timeframes below the current chart timeframe. E.g. on 4H → searches 2H and 1H.", display = display.none)
ltfTimeframe = input.timeframe("15", "Lower Timeframe (Manual)", group = "LTF Configuration", display = display.none)
ltfThresholdPct = input.float(15.0, "Price Threshold %", minval = 0.1, maxval = 50.0, step = 0.5, group = "LTF Configuration", tooltip = "Only show LTF order blocks within this % of current price", display = display.none)
ltfProximityPct = input.float(5.0, "Zone Proximity %", minval = 0.1, maxval = 50.0, step = 0.5, group = "LTF Configuration", tooltip = "Suppress an LTF block if it is within this % of price distance from any existing zone of the same type (0 = overlapping only).", display = display.none)
ltfLookbackBars = input.int(500, "LTF Lookback Bars", minval = 50, maxval = 1750, step = 50, group = "LTF Configuration", tooltip = "How many recent lower-TF bars to scan for order blocks. Smaller = faster. ~410 bars ≈ 1 year of 4H data. Reduce this to fix crashes on long-history assets like SPY on daily charts.", display = display.none)
ltfBullColor = input(color.rgb(0, 255, 200, 60), 'LTF Bullish', inline = 'ltfColor', group = 'LTF Configuration', display = display.none)
ltfBearColor = input(color.rgb(255, 50, 160, 50), 'LTF Bearish', inline = 'ltfColor', group = 'LTF Configuration', display = display.none)

// Auto LTF: map current timeframe → 1st and 2nd lower timeframe strings.
// Use string detection for D/W/M because timeframe.in_seconds() can return 0 for monthly
// and may be unreliable for session-based timeframes on some symbols (e.g. SPY).
_tfPeriod  = timeframe.period
_isDaily   = str.contains(_tfPeriod, "D")
_isWeekly  = str.contains(_tfPeriod, "W")
_isMonthly = str.contains(_tfPeriod, "M")
_ltfSecs   = _isMonthly ? 2592000 : _isWeekly ? 604800 : _isDaily ? 86400 : timeframe.in_seconds(_tfPeriod)

// Maximum chart timeframe (in seconds) at which LTF is active.
// Default 23400 = 390 min (1 RTH session). Safe for SPY/QQQ because TradingView caps
// intraday (minute-string) data differently than daily bars, so 390-min charts don't
// carry the full 30-year history that crashes the request.security calls.
ltfMaxChartSecs = input.int(23400, "LTF Max Chart TF (sec)", minval = 3600, maxval = 86400, step = 3600, group = "LTF Configuration", tooltip = "LTF is disabled when the chart timeframe exceeds this value in seconds. 23400 = 390 min (1 RTH session). 14400 = 4H (more conservative).", display = display.none)
_ltfFeatActive  = ltfEnabled and _ltfSecs <= ltfMaxChartSecs
_autoTF1 = _isMonthly ? "1W" : _isWeekly ? "1D" : _isDaily ? "240" : _ltfSecs >= 14400 ? "120" : _ltfSecs >= 7200 ? "60" : _ltfSecs >= 3600 ? "30" : _ltfSecs >= 1800 ? "15" : _ltfSecs >= 900 ? "5" : _ltfSecs >= 300 ? "3" : "1"
_autoTF2 = _isMonthly ? "1D" : _isWeekly ? "240" : _isDaily ? "120" : _ltfSecs >= 14400 ? "60"  : _ltfSecs >= 7200 ? "30" : _ltfSecs >= 3600 ? "15" : _ltfSecs >= 1800 ? "5"  : _ltfSecs >= 900 ? "3"  : "1"
activeLTF1 = ltfAutoEnabled ? _autoTF1 : ltfTimeframe
activeLTF2 = ltfAutoEnabled ? _autoTF2 : ltfTimeframe

bullishOrderBlocks = zoneCount == "One" ? 1 : zoneCount == "2" ? 2 : zoneCount == "3" ? 3 : zoneCount == "Low" ? 4 : zoneCount == "Medium" ? 6 : 10
bearishOrderBlocks = zoneCount == "One" ? 1 : zoneCount == "2" ? 2 : zoneCount == "3" ? 3 : zoneCount == "Low" ? 4 : zoneCount == "Medium" ? 6 : 10

timeframe1Enabled = true
timeframe1 = ""

textColor = input.color(#ffffff80, "Text Color", group = "Style")
labelSizeInput = input.string("Tiny", "Label Text Size", options = ["Tiny", "Small", "Normal", "Large", "Huge"], group = "Style")

selectedTextSize = labelSizeInput == "Tiny" ? size.tiny : 
                   labelSizeInput == "Small" ? size.small : 
                   labelSizeInput == "Normal" ? size.normal : 
                   labelSizeInput == "Large" ? size.large : 
                   size.huge

extendZonesBy = DEBUG ? input.int(15, "Extend Zones", group = "Style", minval = 1, maxval = 30, inline = "ExtendZones") : 15
extendZonesDynamic = DEBUG ? input.bool(true, "Dynamic", group = "Style", inline = "ExtendZones") : true
combinedText = DEBUG ? input.bool(false, "Combined Text", group = "Style", inline = "CombinedColor") : false

// --- MODIFIED: Added variable to shift text to the left ---
textShiftBars = input.int(5, "Text Shift Left (Bars)", group = "Style", tooltip = "Shifts the text label to the left by this many bars")

volumeBarsPlace = "Right" 
mirrorVolumeBars = DEBUG ? input.bool(true, "Mirror Volume Bars", group = "Style", inline = "volumebars") : true

volumeBarsLeftSide = (volumeBarsPlace == "Left")
extendZonesByTime = extendZonesBy * timeframe.in_seconds(timeframe.period) * 1000

atr = ta.atr(10)

type orderBlockInfo
    float top
    float bottom
    float obVolume
    string obType
    int startTime
    float bbVolume
    float obLowVolume
    float obHighVolume
    bool breaker
    int breakTime
    string timeframeStr
    bool disabled = false
    string combinedTimeframesStr = na
    bool combined = false

type orderBlock
    orderBlockInfo info
    bool isRendered = false

    box orderBox = na
    box breakerBox = na

    line orderBoxLineTop = na
    line orderBoxLineBottom = na
    line breakerBoxLineTop = na
    line breakerBoxLineBottom = na
    //
    box orderBoxText = na
    box orderBoxPositive = na
    box orderBoxNegative = na

    line orderSeperator = na
    line orderTextSeperator = na

createOrderBlock (orderBlockInfo orderBlockInfoF) =>
    orderBlock newOrderBlock = orderBlock.new(orderBlockInfoF)
    newOrderBlock

safeDeleteOrderBlock (orderBlock orderBlockF) =>
    orderBlockF.isRendered := false

    box.delete(orderBlockF.orderBox)
    box.delete(orderBlockF.breakerBox)
    box.delete(orderBlockF.orderBoxText)
    box.delete(orderBlockF.orderBoxPositive)
    box.delete(orderBlockF.orderBoxNegative)

    line.delete(orderBlockF.orderBoxLineTop)
    line.delete(orderBlockF.orderBoxLineBottom)
    line.delete(orderBlockF.breakerBoxLineTop)
    line.delete(orderBlockF.breakerBoxLineBottom)
    line.delete(orderBlockF.orderSeperator)
    line.delete(orderBlockF.orderTextSeperator)

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

    orderBlockInfo[] bullishOrderBlocksList = na
    orderBlockInfo[] bearishOrderBlocksList = na

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

    newTFInfo

type obSwing
    int x = na    
    float y = na
    float swingVolume = na
    bool crossed = false

var timeframeInfo[] timeframeInfos = array.from(newTimeframeInfo(1, timeframe1, timeframe1Enabled))
var bullishOrderBlocksList = array.new<orderBlockInfo>(0)
var bearishOrderBlocksList = array.new<orderBlockInfo>(0)

// Separate arrays for LTF requests — use a limited lookback to avoid replaying
// thousands of historical bars for long-history assets like SPY/QQQ on daily charts.
var ltfBullOBList = array.new<orderBlockInfo>(0)
var ltfBearOBList = array.new<orderBlockInfo>(0)

var allOrderBlocksList = array.new<orderBlock>(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"

betterCross(s1, s2) =>
    string ret = na
    if s1 >= s2 and s1[1] < s2
        ret := "Bull"
    if s1 < s2 and s1[1] >= s2
        ret := "Bear"
    ret

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

createOBBox (boxColor, transparencyX = 1.0, xlocType = xloc.bar_time) =>
    box.new(na, na, na, na, text_size = selectedTextSize, xloc = xlocType, extend = extend.none, bgcolor = colorWithTransparency(boxColor, transparencyX), text_color = textColor, text_halign = text.align_right, border_color = #00000000)

renderOrderBlock (orderBlock ob) =>
    orderBlockInfo info = ob.info
    ob.isRendered := true
    isLTFBlock = ltfEnabled and (ob.info.timeframeStr == activeLTF1 or ob.info.timeframeStr == activeLTF2) and timeframe.in_seconds(ob.info.timeframeStr) < timeframe.in_seconds(timeframe.period)
    orderColor = ob.info.obType == "Bull" ? (isLTFBlock ? ltfBullColor : bullOrderBlockColor) : (isLTFBlock ? ltfBearColor : bearOrderBlockColor)

    if OBsEnabled and (not false or not (false and info.breaker)) and not (not showInvalidated and info.breaker)
        ob.orderBox := createOBBox(orderColor, 1.5)
        if ob.info.combined
            ob.orderBox.set_bgcolor(colorWithTransparency(orderColor, 1.1))
        ob.orderBoxText := createOBBox(color.new(color.white, 100))
        if orderBlockVolumetricInfo
            ob.orderBoxPositive := createOBBox(bullOrderBlockColor)
            ob.orderBoxNegative := createOBBox(bearOrderBlockColor)
            ob.orderSeperator := line.new(na,na,na,na,xloc.bar_time,extend.none,textColor,line.style_dashed,1)
            ob.orderTextSeperator := line.new(na,na,na,na,xloc.bar_time,extend.none,textColor,line.style_solid,1)

        zoneSize = extendZonesDynamic ? na(info.breakTime) ? extendZonesByTime : (info.breakTime - info.startTime) : extendZonesByTime
        if na(info.breakTime)
            zoneSize := (time + 1) - info.startTime

        startX = volumeBarsLeftSide ? info.startTime : info.startTime + zoneSize - zoneSize / 3
        maxEndX = volumeBarsLeftSide ? info.startTime + zoneSize / 3 : info.startTime + zoneSize

        moveBox(ob.orderBox, info.startTime, info.top, info.startTime + zoneSize, info.bottom)
        
        // --- MODIFIED: Convert textShiftBars to time and subtract from X coordinates ---
        shiftTime = textShiftBars * timeframe.in_seconds(timeframe.period) * 1000
        moveBox(ob.orderBoxText, startX - shiftTime, info.top, maxEndX - shiftTime, info.bottom)

        percentage = int((math.min(info.obHighVolume, info.obLowVolume) / math.max(info.obHighVolume, info.obLowVolume)) * 100.0)
        OBText = (na(ob.info.combinedTimeframesStr) ? formatTimeframeString(ob.info.timeframeStr) : ob.info.combinedTimeframesStr) + " OB"
        box.set_text(ob.orderBoxText, (orderBlockVolumetricInfo ? str.tostring(ob.info.obVolume, format.volume) + " (" + str.tostring(percentage) + "%)\n" : "") + (combinedText and ob.info.combined ? "[Combined]\n" : "") + OBText)

        if orderBlockVolumetricInfo
            showHighLowBoxText = false

            highVolumeWidth = (info.obHighVolume / info.obVolume) * (maxEndX - startX)
            lowVolumeWidth = (info.obLowVolume / info.obVolume) * (maxEndX - startX)

            curStartXHigh = int(math.floor(maxEndX - highVolumeWidth))
            curStartXLow = int(math.floor(maxEndX - lowVolumeWidth))

            moveBox(ob.orderBoxPositive, curStartXHigh, info.top, maxEndX, (info.bottom + info.top) / 2)
            box.set_text(ob.orderBoxPositive, showHighLowBoxText ? str.tostring(info.obHighVolume, format.volume) : "")

            moveBox(ob.orderBoxNegative, curStartXLow, info.bottom, maxEndX, (info.bottom + info.top) / 2)
            box.set_text(ob.orderBoxNegative, showHighLowBoxText ? str.tostring(info.obLowVolume, format.volume) : "")

            moveLine(ob.orderSeperator, volumeBarsLeftSide ? startX : maxEndX, (info.bottom + info.top) / 2, volumeBarsLeftSide ? maxEndX : startX)

            line.set_xy1(ob.orderTextSeperator, volumeBarsLeftSide ? maxEndX : startX, info.top)
            line.set_xy2(ob.orderTextSeperator, volumeBarsLeftSide ? maxEndX : startX, info.bottom)

findOBSwings(len) =>
    var swingType = 0
    var obSwing top = obSwing.new(na, na)
    var obSwing bottom = obSwing.new(na, na)
    
    upper = ta.highest(len)
    lower = ta.lowest(len)

    swingType := high[len] > upper ? 0 : low[len] < lower ? 1 : swingType

    if swingType == 0 and swingType[1] != 0
        top := obSwing.new(bar_index[len], high[len], volume[len])
    
    if swingType == 1 and swingType[1] != 1
        bottom := obSwing.new(bar_index[len], low[len], volume[len])

    [top, bottom]

findOrderBlocks () =>
    if bar_index > last_bar_index - maxDistanceToLastBar
        [top, btm] = findOBSwings(swingLength)
        useBody = false
        max = useBody ? math.max(close, open) : high
        min = useBody ? math.min(close, open) : low

        // Bullish Order Block
        bullishBreaked = 0

        if bullishOrderBlocksList.size() > 0
            for i = bullishOrderBlocksList.size() - 1 to 0
                currentOB = bullishOrderBlocksList.get(i)
            
                if not currentOB.breaker 
                    if (obEndMethod == "Wick" ? low : math.min(open, close)) < currentOB.bottom
                        currentOB.breaker := true
                        currentOB.breakTime := time
                        currentOB.bbVolume := volume
                else
                    if high > currentOB.top
                        bullishOrderBlocksList.remove(i)
                    else if i < bullishOrderBlocks and top.y < currentOB.top and top.y > currentOB.bottom 
                        bullishBreaked := 1

        if close > top.y and not top.crossed
            top.crossed := true

            boxBtm = max[1]
            boxTop = min[1]
            boxLoc = time[1]

            for i = 1 to (bar_index - top.x) - 1
                boxBtm := math.min(min[i], boxBtm)
                boxTop := boxBtm == min[i] ? max[i] : boxTop
                boxLoc := boxBtm == min[i] ? time[i] : boxLoc

            newOrderBlockInfo = orderBlockInfo.new(boxTop, boxBtm, volume + volume[1] + volume[2], "Bull", boxLoc)
            newOrderBlockInfo.obLowVolume := volume[2]
            newOrderBlockInfo.obHighVolume := volume + volume[1]
            
            obSize = math.abs(newOrderBlockInfo.top - newOrderBlockInfo.bottom)
            if obSize <= atr * maxATRMult
                bullishOrderBlocksList.unshift(newOrderBlockInfo)
                if bullishOrderBlocksList.size() > maxOrderBlocks
                    bullishOrderBlocksList.pop()

        // Bearish Order Block
        
        bearishBreaked = 0

        if bearishOrderBlocksList.size() > 0
            for i = bearishOrderBlocksList.size() - 1 to 0
                currentOB = bearishOrderBlocksList.get(i)

                if not currentOB.breaker 
                    if (obEndMethod == "Wick" ? high : math.max(open, close)) > currentOB.top
                        currentOB.breaker := true
                        currentOB.breakTime := time
                        currentOB.bbVolume := volume
                else
                    if low < currentOB.bottom
                        bearishOrderBlocksList.remove(i)
                    else if i < bearishOrderBlocks and btm.y > currentOB.bottom and btm.y < currentOB.top 
                        bearishBreaked := 1

        if close < btm.y and not btm.crossed
            btm.crossed := true

            boxBtm = min[1]
            boxTop = max[1]
            boxLoc = time[1]

            for i = 1 to (bar_index - btm.x) - 1
                boxTop := math.max(max[i], boxTop)
                boxBtm := boxTop == max[i] ? min[i] : boxBtm
                boxLoc := boxTop == max[i] ? time[i] : boxLoc

            newOrderBlockInfo = orderBlockInfo.new(boxTop, boxBtm, volume + volume[1] + volume[2], "Bear", boxLoc)
            newOrderBlockInfo.obLowVolume := volume + volume[1]
            newOrderBlockInfo.obHighVolume := volume[2]

            obSize = math.abs(newOrderBlockInfo.top - newOrderBlockInfo.bottom)
            if obSize <= atr * maxATRMult
                bearishOrderBlocksList.unshift(newOrderBlockInfo)
                if bearishOrderBlocksList.size() > maxOrderBlocks
                    bearishOrderBlocksList.pop()
    true

// LTF-specific OB detection — identical logic but uses ltfBullOBList/ltfBearOBList
// and ltfLookbackBars so request.security only does real work on recent bars.
// Separate var state in findOBSwingsLTF ensures no cross-contamination with the main swing tracker.
findOBSwingsLTF(len) =>
    var swingType = 0
    var obSwing top = obSwing.new(na, na)
    var obSwing bottom = obSwing.new(na, na)

    upper = ta.highest(len)
    lower = ta.lowest(len)

    swingType := high[len] > upper ? 0 : low[len] < lower ? 1 : swingType

    if swingType == 0 and swingType[1] != 0
        top := obSwing.new(bar_index[len], high[len], volume[len])

    if swingType == 1 and swingType[1] != 1
        bottom := obSwing.new(bar_index[len], low[len], volume[len])

    [top, bottom]

findOrderBlocksLTF () =>
    if bar_index > last_bar_index - ltfLookbackBars
        [top, btm] = findOBSwingsLTF(swingLength)
        useBody = false
        max = useBody ? math.max(close, open) : high
        min = useBody ? math.min(close, open) : low

        bullishBreaked = 0
        if ltfBullOBList.size() > 0
            for i = ltfBullOBList.size() - 1 to 0
                currentOB = ltfBullOBList.get(i)
                if not currentOB.breaker
                    if (obEndMethod == "Wick" ? low : math.min(open, close)) < currentOB.bottom
                        currentOB.breaker := true
                        currentOB.breakTime := time
                        currentOB.bbVolume := volume
                else
                    if high > currentOB.top
                        ltfBullOBList.remove(i)
                    else if i < bullishOrderBlocks and top.y < currentOB.top and top.y > currentOB.bottom
                        bullishBreaked := 1

        if close > top.y and not top.crossed
            top.crossed := true
            boxBtm = max[1]
            boxTop = min[1]
            boxLoc = time[1]
            for i = 1 to (bar_index - top.x) - 1
                boxBtm := math.min(min[i], boxBtm)
                boxTop := boxBtm == min[i] ? max[i] : boxTop
                boxLoc := boxBtm == min[i] ? time[i] : boxLoc
            newOrderBlockInfo = orderBlockInfo.new(boxTop, boxBtm, volume + volume[1] + volume[2], "Bull", boxLoc)
            newOrderBlockInfo.obLowVolume := volume[2]
            newOrderBlockInfo.obHighVolume := volume + volume[1]
            obSize = math.abs(newOrderBlockInfo.top - newOrderBlockInfo.bottom)
            if obSize <= atr * maxATRMult
                ltfBullOBList.unshift(newOrderBlockInfo)
                if ltfBullOBList.size() > maxOrderBlocks
                    ltfBullOBList.pop()

        bearishBreaked = 0
        if ltfBearOBList.size() > 0
            for i = ltfBearOBList.size() - 1 to 0
                currentOB = ltfBearOBList.get(i)
                if not currentOB.breaker
                    if (obEndMethod == "Wick" ? high : math.max(open, close)) > currentOB.top
                        currentOB.breaker := true
                        currentOB.breakTime := time
                        currentOB.bbVolume := volume
                else
                    if low < currentOB.bottom
                        ltfBearOBList.remove(i)
                    else if i < bearishOrderBlocks and btm.y > currentOB.bottom and btm.y < currentOB.top
                        bearishBreaked := 1

        if close < btm.y and not btm.crossed
            btm.crossed := true
            boxBtm = min[1]
            boxTop = max[1]
            boxLoc = time[1]
            for i = 1 to (bar_index - btm.x) - 1
                boxTop := math.max(max[i], boxTop)
                boxBtm := boxTop == max[i] ? min[i] : boxBtm
                boxLoc := boxTop == max[i] ? time[i] : boxLoc
            newOrderBlockInfo = orderBlockInfo.new(boxTop, boxBtm, volume + volume[1] + volume[2], "Bear", boxLoc)
            newOrderBlockInfo.obLowVolume := volume + volume[1]
            newOrderBlockInfo.obHighVolume := volume[2]
            obSize = math.abs(newOrderBlockInfo.top - newOrderBlockInfo.bottom)
            if obSize <= atr * maxATRMult
                ltfBearOBList.unshift(newOrderBlockInfo)
                if ltfBearOBList.size() > maxOrderBlocks
                    ltfBearOBList.pop()
    true

areaOfOB (orderBlockInfo OBInfoF) =>
    float XA1 = OBInfoF.startTime
    float XA2 = na(OBInfoF.breakTime) ? time + 1 : OBInfoF.breakTime
    float YA1 = OBInfoF.top
    float YA2 = OBInfoF.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

doOBsTouch (orderBlockInfo OBInfo1, orderBlockInfo OBInfo2) =>
    float XA1 = OBInfo1.startTime
    float XA2 = na(OBInfo1.breakTime) ? time + 1 : OBInfo1.breakTime
    float YA1 = OBInfo1.top
    float YA2 = OBInfo1.bottom

    float XB1 = OBInfo2.startTime
    float XB2 = na(OBInfo2.breakTime) ? time + 1 : OBInfo2.breakTime
    float YB1 = OBInfo2.top
    float YB2 = OBInfo2.bottom
    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 = areaOfOB(OBInfo1) + areaOfOB(OBInfo2) - intersectionArea
    
    float overlapPercentage = (intersectionArea / unionArea) * 100.0

    if overlapPercentage > overlapThresholdPercentage
        true
    else
        false

isOBValid (orderBlockInfo OBInfo) =>
    valid = true
    if OBInfo.disabled
        valid := false
    valid

combineOBsFunc () =>
    if allOrderBlocksList.size() > 0
        lastCombinations = 999
        while lastCombinations > 0
            lastCombinations := 0
            for i = 0 to allOrderBlocksList.size() - 1
                curOB1 = allOrderBlocksList.get(i)
                for j = 0 to allOrderBlocksList.size() - 1
                    curOB2 = allOrderBlocksList.get(j)
                    if i == j
                        continue
                    if not isOBValid(curOB1.info) or not isOBValid(curOB2.info)
                        continue
                    if curOB1.info.obType != curOB2.info.obType
                        continue
                    if doOBsTouch(curOB1.info, curOB2.info)
                        curOB1.info.disabled := true
                        curOB2.info.disabled := true
                        orderBlock newOB = createOrderBlock(orderBlockInfo.new(math.max(curOB1.info.top, curOB2.info.top), math.min(curOB1.info.bottom, curOB2.info.bottom), curOB1.info.obVolume + curOB2.info.obVolume, curOB1.info.obType))
                        newOB.info.startTime := math.min(curOB1.info.startTime, curOB2.info.startTime)
                        newOB.info.breakTime := math.max(nz(curOB1.info.breakTime), nz(curOB2.info.breakTime))
                        newOB.info.breakTime := newOB.info.breakTime == 0 ? na : newOB.info.breakTime
                        newOB.info.timeframeStr := curOB1.info.timeframeStr

                        newOB.info.obVolume := curOB1.info.obVolume + curOB2.info.obVolume
                        newOB.info.obLowVolume := curOB1.info.obLowVolume + curOB2.info.obLowVolume
                        newOB.info.obHighVolume := curOB1.info.obHighVolume + curOB2.info.obHighVolume
                        newOB.info.bbVolume := nz(curOB1.info.bbVolume, 0) + nz(curOB2.info.bbVolume, 0)
                        newOB.info.breaker := curOB1.info.breaker or curOB2.info.breaker
                        
                        newOB.info.combined := true
                        if timeframe.in_seconds(curOB1.info.timeframeStr) != timeframe.in_seconds(curOB2.info.timeframeStr)
                            newOB.info.combinedTimeframesStr := (na(curOB1.info.combinedTimeframesStr) ? formatTimeframeString(curOB1.info.timeframeStr) : curOB1.info.combinedTimeframesStr) + " & " + (na(curOB2.info.combinedTimeframesStr) ? formatTimeframeString(curOB2.info.timeframeStr) : curOB2.info.combinedTimeframesStr)
                        allOrderBlocksList.unshift(newOB)
                        lastCombinations += 1

reqSeq (timeframeStr) =>
    [bullishOrderBlocksListF, bearishOrderBlocksListF] = request.security(syminfo.tickerid, timeframeStr, [bullishOrderBlocksList, bearishOrderBlocksList])
    [bullishOrderBlocksListF, bearishOrderBlocksListF]

reqSeqLTF (timeframeStr) =>
    [bullF, bearF] = request.security(syminfo.tickerid, timeframeStr, [ltfBullOBList, ltfBearOBList])
    [bullF, bearF]

getTFData (timeframeInfo timeframeInfoF, timeframeStr) =>
    if not isTimeframeLower(timeframeInfoF.timeframeStr, timeframe.period) and timeframeInfoF.isEnabled
        [bullishOrderBlocksListF, bearishOrderBlocksListF] = reqSeq(timeframeStr)
        [bullishOrderBlocksListF, bearishOrderBlocksListF]
    else
        [na, na]

handleTimeframeInfo (timeframeInfo timeframeInfoF, bullishOrderBlocksListF, bearishOrderBlocksListF) =>
    if not isTimeframeLower(timeframeInfoF.timeframeStr, timeframe.period) and timeframeInfoF.isEnabled
        timeframeInfoF.bullishOrderBlocksList := bullishOrderBlocksListF
        timeframeInfoF.bearishOrderBlocksList := bearishOrderBlocksListF


isWithinThresholdPct (float currentPrice, orderBlockInfo obInfo, float thresholdPct) =>
    float midPrice = (obInfo.top + obInfo.bottom) / 2.0
    float pctDiff = math.abs(midPrice - currentPrice) / currentPrice * 100.0
    pctDiff <= thresholdPct

handleOrderBlocksFinal () =>
    if DEBUG
        log.info("Bullish OB Count " + str.tostring(bullishOrderBlocksList.size()))
        log.info("Bearish OB Count " + str.tostring(bearishOrderBlocksList.size()))

    if allOrderBlocksList.size () > 0
        for i = 0 to allOrderBlocksList.size() - 1
            safeDeleteOrderBlock(allOrderBlocksList.get(i))
    allOrderBlocksList.clear()    

    for i = 0 to timeframeInfos.size() - 1
        curTimeframe = timeframeInfos.get(i)
        if not curTimeframe.isEnabled
            continue
        if curTimeframe.bullishOrderBlocksList.size() > 0
            for j = 0 to math.min(curTimeframe.bullishOrderBlocksList.size() - 1, bullishOrderBlocks - 1)
                orderBlockInfoF = curTimeframe.bullishOrderBlocksList.get(j)
                orderBlockInfoF.timeframeStr := curTimeframe.timeframeStr
                allOrderBlocksList.unshift(createOrderBlock(orderBlockInfo.copy(orderBlockInfoF)))

        if curTimeframe.bearishOrderBlocksList.size() > 0
            for j = 0 to math.min(curTimeframe.bearishOrderBlocksList.size() - 1, bearishOrderBlocks - 1)
                orderBlockInfoF = curTimeframe.bearishOrderBlocksList.get(j)
                orderBlockInfoF.timeframeStr := curTimeframe.timeframeStr
                allOrderBlocksList.unshift(createOrderBlock(orderBlockInfo.copy(orderBlockInfoF)))

    if combineOBs
        combineOBsFunc()    

    if allOrderBlocksList.size() > 0
        for i = 0 to allOrderBlocksList.size() - 1
            curOB = allOrderBlocksList.get(i)
            if isOBValid(curOB.info)
                renderOrderBlock(curOB)

findOrderBlocks()
findOrderBlocksLTF()

[bullishOrderBlocksListTimeframe1, bearishOrderBlocksListTimeframe1] = getTFData(timeframeInfos.get(0), timeframe1)
// Conditionally fetch LTF data — avoids request.security on daily/weekly/monthly charts
// which would compute across decades of intraday bars and crash Pine for long-history assets.
getLTFData(string tfStr, bool shouldFetch) =>
    if shouldFetch
        reqSeqLTF(tfStr)
    else
        [array.new<orderBlockInfo>(0), array.new<orderBlockInfo>(0)]

[ltfBullBlocks1, ltfBearBlocks1] = getLTFData(activeLTF1, _ltfFeatActive)
[ltfBullBlocks2, ltfBearBlocks2] = getLTFData(activeLTF2, _ltfFeatActive)

tfToSeconds(string tf) =>
    tf == "" ? timeframe.in_seconds(timeframe.period) : timeframe.in_seconds(tf)

// Returns true if the candidate should be suppressed.
// If an existing zone is the lower-TF one, it is removed and deleted so the candidate can take its place.
ltfCheckConflicts(orderBlockInfo ltfOBInfo) =>
    suppress = false
    int k = 0
    while k < allOrderBlocksList.size() and not suppress
        existingOB = allOrderBlocksList.get(k)
        if isOBValid(existingOB.info) and existingOB.info.obType == ltfOBInfo.obType
            float gap = math.max(0.0, math.max(ltfOBInfo.bottom, existingOB.info.bottom) - math.min(ltfOBInfo.top, existingOB.info.top))
            if gap / close * 100.0 <= ltfProximityPct
                int candidateSecs = tfToSeconds(ltfOBInfo.timeframeStr)
                int existingSecs  = tfToSeconds(existingOB.info.timeframeStr)
                if candidateSecs < existingSecs
                    // Candidate is the lower TF → suppress it
                    suppress := true
                else if existingSecs < candidateSecs
                    // Existing is the lower TF → remove it, keep checking for more conflicts
                    safeDeleteOrderBlock(existingOB)
                    existingOB.info.disabled := true
                    allOrderBlocksList.remove(k)
                    k := k - 1  // compensate for removal so next element isn't skipped
                else
                    // Same TF → keep existing, suppress candidate
                    suppress := true
        k := k + 1
    suppress

renderLTFList(orderBlockInfo[] bullList, orderBlockInfo[] bearList, string tfStr) =>
    if not na(bullList) and bullList.size() > 0
        for j = 0 to bullList.size() - 1
            ltfOBInfo = bullList.get(j)
            ltfOBInfo.timeframeStr := tfStr  // set before conflict check so TF comparison is accurate
            if not ltfOBInfo.breaker and isWithinThresholdPct(close, ltfOBInfo, ltfThresholdPct) and not ltfCheckConflicts(ltfOBInfo)
                newOB = createOrderBlock(orderBlockInfo.copy(ltfOBInfo))
                renderOrderBlock(newOB)
                allOrderBlocksList.unshift(newOB)
    if not na(bearList) and bearList.size() > 0
        for j = 0 to bearList.size() - 1
            ltfOBInfo = bearList.get(j)
            ltfOBInfo.timeframeStr := tfStr
            if not ltfOBInfo.breaker and isWithinThresholdPct(close, ltfOBInfo, ltfThresholdPct) and not ltfCheckConflicts(ltfOBInfo)
                newOB = createOrderBlock(orderBlockInfo.copy(ltfOBInfo))
                renderOrderBlock(newOB)
                allOrderBlocksList.unshift(newOB)

if barstate.isconfirmed
    handleTimeframeInfo(timeframeInfos.get(0), bullishOrderBlocksListTimeframe1, bearishOrderBlocksListTimeframe1)
    handleOrderBlocksFinal()

    if _ltfFeatActive
        if timeframe.in_seconds(activeLTF1) < _ltfSecs
            renderLTFList(ltfBullBlocks1, ltfBearBlocks1, activeLTF1)
        if ltfAutoEnabled and timeframe.in_seconds(activeLTF2) < _ltfSecs
            renderLTFList(ltfBullBlocks2, ltfBearBlocks2, activeLTF2)

Overview

Swing length, maximum ATR multiplier, zone count and invalidation method shape the main zones. A lower timeframe can be chosen automatically, two timeframes lower, or set by hand, with price threshold and zone proximity percentages controlling which lower-timeframe blocks appear.

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
Show Historic ZonesboolfalseshowInvalidated
Volumetric InfobooltrueorderBlockVolumetricInfo
Zone InvalidationstringCloseobEndMethod
Combine ZonesbooltrueCombine Zones
Max Atr Multiplierfloat1.618Max Atr Multiplier
Swing Lengthint5swingLength
Zone Countstring3zoneCount
Bullishcolorcolor.rgb(26, 169, 145, 87)bullOrderBlockColor
Bearishcolorcolor.rgb(184, 41, 127, 50)bearOrderBlockColor
Show LTF Blocks Within RangebooltrueltfEnabled
Auto (2 TFs Lower)booltrueltfAutoEnabled
Lower Timeframe (Manual)timeframe15ltfTimeframe
Show all 25 settings
SettingTypeDefaultVariable
Price Threshold %float15.0ltfThresholdPct
Zone Proximity %float5.0ltfProximityPct
LTF Lookback Barsint500ltfLookbackBars
LTF Bullishcolorcolor.rgb(0, 255, 200, 60)ltfBullColor
LTF Bearishcolorcolor.rgb(255, 50, 160, 50)ltfBearColor
LTF Max Chart TF (sec)int23400ltfMaxChartSecs
Text Colorcolor#ffffff80textColor
Label Text SizestringTinylabelSizeInput
Extend Zonesint15Extend Zones
DynamicbooltrueDynamic
Combined TextboolfalseCombined Text
Text Shift Left (Bars)int5textShiftBars
Mirror Volume BarsbooltrueMirror Volume Bars
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

Moon Dev GEX Sweep & CVD

by moondevonyt
92 stars

The indicator detects liquidity sweeps across key price levels and calculates a cumulative volume delta proxy. It uses pivot high and low lookbacks to identify swing points, marking sweeps when price breaks beyond a level and quickly closes back inside. Featur

Indicator v5 Support & resistanceSMC / ICT / price action
Updated 2 months ago · Custom licence GitHub repository of Moon Dev GEX Sweep & CVD

Calculates higher-timeframe volume profiles to identify Value Area High (VAH), Value Area Low (VAL), and Point of Control (POC) levels directly on the price chart. It flags potential reversal setups and Change in State of Delivery (CISD) patterns relative to v

Indicator v6 VolumeSupport & resistance
Updated 2 months ago · None stated GitHub repository of CandelaCharts - Value Area Reversals

Copilot

by milocaetano
48 stars

Copilot is an order flow range-reversal indicator designed to overlay trading setups directly on the price chart. It evaluates four criteria: market context (sideways vs trending regime based on ATR height), structure (double top or double bottom retests), abs

Indicator v5 SMC / ICT / price actionSupport & resistance
Updated 2 days ago · MIT GitHub repository of Copilot

Supply and Demand Visible Range identifies supply, demand, and equilibrium zones across the visible chart range. It accumulates high, low, and volume data starting from the leftmost visible bar, optionally requesting lower timeframe intrabar data for finer res

Indicator v5 Support & resistanceVolume Repaints
Updated 2 months ago · AGPL-3.0 GitHub repository of Supply and Demand Visible Range [LuxAlgo]
13 stars

This script identifies and visualizes bullish and bearish order blocks on the price chart. It detects low-volatility consolidation ranges followed by breakout moves accompanied by high trading volume. Users can select between Simple, Relative, or Weighted volu

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 6 months ago · MIT GitHub repository of Advanced Order Blocks with Volume

This indicator identifies and visualizes key support and resistance heatmap zones directly on the price chart. Using closed one-hour price data, it evaluates multi-scale pivots, liquidity clusters, relative volume, and round-number resonances to score structur

Indicator v6 Support & resistanceSMC / ICT / price action
Updated 7 weeks ago · MIT GitHub repository of 加密货币1小时关键价位热力图 V2

More SMC / ICT 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