Indicator · Pine Script v5 · On the chart

Weighted Volume Profile Pivot Points | Flux Charts

by middlechild0· code credited to fluxchart

This overlay indicator builds a weighted volume profile over a chosen number of bars and plots pivot points with a point of control line on the price chart.

View on GitHub

GitHub stars2on the repository
Pine versionv5runs as is
Lines of code725
Settings32inputs
LicenceNone statedfile: MPL-2.0
Last update5 months ago13 May 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

725 lines of Pine Script v5 from mt5/indicators/pine/wvpp.pine, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

wvpp.pine
// 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
const bool DEBUG = false
const bool OBSettings = false
const int maxBoxesCount = 500
const int maxRowSize = 50
const int priceBoxSize = 5
const int v2Gap = 1
const string v2TextSize = size.small
const int edgeOffset = 25
const int minDistance = 125
const float overlapThresholdPercentage = 0
const int maxDistanceToLastBar = 1750 // Affects Running Time
const int maxOrderBlocks = 30

indicator("Weighted Volume Profile Pivot Points | Flux Charts", shorttitle = "WVPPP | Flux Charts", overlay = true, max_boxes_count = maxBoxesCount, max_labels_count = maxBoxesCount, max_lines_count = maxBoxesCount, max_bars_back = 1005)
showPreviousSessions = input.bool(true, "Show Previous Sessions", group = "General Configuration", display = display.none)
dbgMode = input.string("Order Blocks", "Mode", options = ["Pivots", "Order Blocks"], group = "General Configuration")
analyzeBars = input.int(200, "Analyze Bars", minval = 1, maxval = 1000, group = "General Configuration")
rowCount = input.int(30, "Row Count", minval = 1, maxval = 1000, group = "General Configuration")
volumeWeight = input.string("Normal", "Volume Weighting", ["Recent", "Normal", "Past"], group = "General Configuration", display = display.none)
swingLen = input.int(10, "Swing Length", minval = 1, group = "General Configuration")
keepOldPOC = input.bool(true, "Keep Past POC Lines", group = "General Configuration")

intersectionBody = DEBUG ? input.bool(false, "[DBG] Intersection With Body", group = "General Configuration") : false

// OB Settings
showInvalidated = OBSettings ? input.bool(true, "Show Historic Zones", group = "Order Blocks", display = display.none) : true
OBRendering = OBSettings ? input.bool(true, "Show Order Blocks", group = "Order Blocks", display = display.none) : false
orderBlockVolumetricInfo = OBSettings ? input.bool(true, "Volumetric Info", group = "Order Blocks", inline="EV", display = display.none) : true
obEndMethod = OBSettings ? input.string("Wick", "Zone Invalidation", options = ["Wick", "Close"],  group = "Order Blocks", display = display.none) : "Wick"
combineOBs = DEBUG ? input.bool(true, "Combine Zones", group = "Order Blocks", display = display.none) : true
maxATRMult = DEBUG ? input.float(3.5,"Max Atr Multiplier", group = "Order Blocks") : 3.5
zoneCount = OBSettings ? input.string("Low", 'Zone Count', options = ["High", "Medium", "Low", "One"], tooltip = "Number of Order Block Zones to be rendered. Higher options will result in older Order Blocks shown.",  group = "Order Blocks", display = display.none) : "Low"
bullOrderBlockColor = OBSettings ? input(#08998180, 'Bullish', inline = 'obColor', group = 'Order Blocks', display = display.none) : #08998180
bearOrderBlockColor = OBSettings ? input(#f2364680, 'Bearish', inline = 'obColor', group = 'Order Blocks', display = display.none) : #f2364680

bullishOrderBlocks = zoneCount == "One" ? 1 : zoneCount == "Low" ? 3 : zoneCount == "Medium" ? 5 : 10
bearishOrderBlocks = zoneCount == "One" ? 1 : zoneCount == "Low" ? 3 : zoneCount == "Medium" ? 5 : 10

timeframe1Enabled = true
timeframe1 = ""

highlightSessions = input.bool(true, "Highlight Sessions", group = "Style", display = display.none)
dbgStyling = DEBUG ? input.string("v2", "[DBG] Styling", ["v1", "v2"], group = "Style", display = display.none) : "v2"
alignSetting = input.string("Left", "Align To", options = ["Left", "Right"], group = "Style", display = display.none)
POCEnabled = input.bool(true, "POC Line        ", inline = "POC", group = "Style", display = display.none)
POCLineStyle = input.string("____", "    ", options = ["____", "----", "...."], group = "Style", inline = "POC", display = display.none)
POCLineWidth = input.int(2, "", options = [1,2,3], group = "Style", inline = "POC", display = display.none)
POCLineColor = input.color(color.yellow, "", group = "Style", inline = "POC", display = display.none)

bullishColor = input.color(#089981, "Bullish", inline = "color", group = "Style", display = display.none)
bearishColor = input.color(#f23646, "Bearish", inline = "color", group = "Style", display = display.none)
textColor = input.color(#ffffff80, "Text Color", group = "Style")

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
volumeBarsPlace = DEBUG ? input.string("Left", "Show Volume Bars At", options = ["Left", "Right"], group = "Style", inline = "volumebars") : "Left"
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

// ____ TYPES END ____

var timeframeInfo[] timeframeInfos = array.from(newTimeframeInfo(1, timeframe1, timeframe1Enabled))
var bullishOrderBlocksList = array.new<orderBlockInfo>(0)
var bearishOrderBlocksList = 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 = size.normal, xloc = xlocType, extend = extend.none, bgcolor = colorWithTransparency(boxColor, transparencyX), text_color = textColor, text_halign = text.align_center, border_color = #00000000)

renderOrderBlock (orderBlock ob) =>
    orderBlockInfo info = ob.info
    ob.isRendered := true
    orderColor = ob.info.obType == "Bull" ? bullOrderBlockColor : bearOrderBlockColor

    if OBRendering 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)
        moveBox(ob.orderBoxText, volumeBarsLeftSide ? maxEndX : info.startTime, info.top, volumeBarsLeftSide ? info.startTime + zoneSize : startX, 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

            curEndXHigh = int(math.ceil((info.obHighVolume / info.obVolume) * (maxEndX - startX) + startX))
            curEndXLow = int(math.ceil((info.obLowVolume / info.obVolume) * (maxEndX - startX) + startX))

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

            moveBox(ob.orderBoxNegative, mirrorVolumeBars ? startX : curEndXHigh, info.bottom, mirrorVolumeBars ? curEndXLow : 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 and barstate.isconfirmed
        [top, btm] = findOBSwings(swingLen)
        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

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]

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


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()

[bullishOrderBlocksListTimeframe1, bearishOrderBlocksListTimeframe1] = getTFData(timeframeInfos.get(0), timeframe1)

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

type rowType
    float top
    float bottom
    int startIndex
    int endIndex
    float totalVolume = 0
    float bullVolume = 0
    float bearVolume = 0

    box bullBox = na
    box priceBox = na
    box bearBox = na

type volumeProfile
    array<rowType> rows
    line POCLine = na
    box highlightBox = na
    int edgeIndex = na
    int startIndex = na
    float vpTop = na
    float vpBottom = na
    int vpStart = na
    int vpEnd = na

type barType
    float o = open
    float c = close
    float h = high
    float l = low
    float v = volume

var float validPivotHigh = na
var float validPivotLow = na
var int distanceToLastVP = 999

var volumeProfile latestVolumeProfile = na

if not na(latestVolumeProfile)
    distanceToLastVP := bar_index - latestVolumeProfile.vpStart

pvHigh = ta.pivothigh(swingLen, swingLen)
pvLow = ta.pivotlow(swingLen, swingLen)

var float vph = na
var float vpl = na

vph := nz(pvHigh, vph)
vpl := nz(pvLow, vpl)

if dbgMode == "Pivots"
    if (alignSetting == "Left" and distanceToLastVP > minDistance) or (alignSetting == "Right" and distanceToLastVP > minDistance)
        validPivotHigh := vph
        validPivotLow := vpl

else if dbgMode == "Order Blocks"
    if (alignSetting == "Left" and distanceToLastVP > minDistance) or (alignSetting == "Right" and distanceToLastVP > minDistance)
        if bullishOrderBlocksList.size() > 0
            curOB = bullishOrderBlocksList.get(0)
            validPivotHigh := curOB.bottom
        
        if bearishOrderBlocksList.size() > 0
            curOB = bearishOrderBlocksList.get(0)
            validPivotLow := curOB.top

pivotsChanged = false

if ((validPivotHigh != validPivotHigh[1]) or (validPivotLow != validPivotLow[1])) and (not na(validPivotHigh)) and (not na(validPivotLow)) and math.abs(validPivotHigh - validPivotLow) > atr * 5
    pivotsChanged := true

float top = validPivotHigh
float bottom = validPivotLow

float step = (top - bottom) / rowCount
float gap = rowCount < 100 ? (step / 3.0) : 0

var line oldLine = na
var box oldBox = na
var int latestEdgeIndex = na

if pivotsChanged
    if not na(latestVolumeProfile)
        latestVolumeProfile.vpEnd := bar_index
        oldBox := latestVolumeProfile.highlightBox
        if not keepOldPOC
            line.delete(latestVolumeProfile.POCLine)
        latestEdgeIndex := latestVolumeProfile.edgeIndex
        oldLine := latestVolumeProfile.POCLine

    latestVolumeProfile := volumeProfile.new()
    latestVolumeProfile.vpStart := bar_index
    latestVolumeProfile.vpTop := top
    latestVolumeProfile.vpBottom := bottom
    latestVolumeProfile.rows := array.new<rowType>(rowCount)
    for x = 0 to rowCount - 1
        rowTop = top - step * x
        latestVolumeProfile.rows.set(x, rowType.new(rowTop, rowTop - step))

doIntersect (candleTop, candleBottom, rowType row) =>
    if (candleBottom > row.top)
        false
    else if (candleTop < row.bottom)
        false
    else
        true

var float maxVolumeSize = 0
var float minVolumeSize = 0

f_lin_interpolate(x0, y0, x1, y1, x) =>
    y0 + (x - x0) * (y1 - y0) / (x1 - x0)

renderRow (startIndex, flip, rowType row) =>
    totalSize = f_lin_interpolate(minVolumeSize, 1, maxVolumeSize, maxRowSize, row.totalVolume)
    bullSize = nz(math.round((row.bullVolume / row.totalVolume) * totalSize), 0)
    bearSize = nz(math.round((row.bearVolume / row.totalVolume) * totalSize), 0)
    bullPercent = str.tostring(bullSize * 100.0 / totalSize, "#.##") + "%"
    bearPercent = str.tostring(100.0 - str.tonumber(str.replace(bullPercent,"%","")), "#.##") + "%"
    rowPrice = (row.top + row.bottom) / 2.0

    if dbgStyling == "v2"
        row.startIndex := startIndex - bullSize
        if flip
            row.startIndex := startIndex + bullSize
    else
        row.startIndex := startIndex
    
    row.endIndex := row.startIndex
    if dbgStyling == "v1"
        if bullSize + bearSize > 0
            if not flip
                row.endIndex := row.startIndex + bullSize + bearSize
                row.bullBox := box.new(row.startIndex, row.top - gap / 2.0, row.startIndex + bullSize, row.bottom + gap / 2.0, bgcolor = bullishColor, border_color = color.black, border_width = 0)
                if bearSize > 0
                    row.bearBox := box.new(row.startIndex + bullSize, row.top - gap / 2.0, row.startIndex + bullSize + bearSize, row.bottom + gap / 2.0, bgcolor = bearishColor, border_color = color.black, border_width = 0)
            else
                row.endIndex := row.startIndex - bullSize - bearSize
                row.bullBox := box.new(row.startIndex, row.top - gap / 2.0, row.startIndex - bullSize, row.bottom + gap / 2.0, bgcolor = bullishColor, border_color = color.black, border_width = 0)
                if bearSize > 0
                    row.bearBox := box.new(row.startIndex - bullSize, row.top - gap / 2.0, row.startIndex - bullSize - bearSize, row.bottom + gap / 2.0, bgcolor = bearishColor, border_color = color.black, border_width = 0)
    else if dbgStyling == "v2"
        if not flip
            row.endIndex := row.startIndex + bullSize + v2Gap + priceBoxSize + v2Gap + bearSize
            if bullSize > 0
                row.bullBox := box.new(row.startIndex, row.top - gap / 2.0, row.startIndex + bullSize, row.bottom + gap / 2.0, text = bullPercent, text_size = v2TextSize, text_color = textColor, bgcolor = color.new(bullishColor, 50), border_color = bullishColor, border_width = 1)
            row.priceBox := box.new(row.startIndex + bullSize + v2Gap, row.top - gap / 2.0, row.startIndex + bullSize + v2Gap + priceBoxSize, row.bottom + gap / 2.0, text = str.tostring(rowPrice, format.mintick), text_color = textColor, text_size = v2TextSize, bgcolor = color.new(color.blue, 50), border_width = 1)    
            if bearSize > 0
                row.bearBox := box.new(row.startIndex + bullSize + v2Gap + priceBoxSize + v2Gap, row.top - gap / 2.0, row.startIndex + bullSize + v2Gap + priceBoxSize + v2Gap + bearSize, row.bottom + gap / 2.0, text = bearPercent, text_size = v2TextSize, text_color = textColor, bgcolor = color.new(bearishColor, 50), border_color = bearishColor, border_width = 1)
        else
            row.endIndex := row.startIndex - bullSize - v2Gap - priceBoxSize - v2Gap - bearSize
            if bullSize > 0
                row.bullBox := box.new(row.startIndex, row.top - gap / 2.0, row.startIndex - bullSize, row.bottom + gap / 2.0, text = bullPercent, text_size = v2TextSize, text_color = textColor, bgcolor = color.new(bullishColor, 50), border_color = bullishColor, border_width = 1)
            row.priceBox := box.new(row.startIndex - bullSize - v2Gap, row.top - gap / 2.0, row.startIndex - bullSize - v2Gap - priceBoxSize, row.bottom + gap / 2.0, text = str.tostring(rowPrice, format.mintick), text_color = textColor, text_size = v2TextSize, bgcolor = color.new(color.blue, 50), border_width = 1)    
            if bearSize > 0
                row.bearBox := box.new(row.startIndex - bullSize - v2Gap - priceBoxSize - v2Gap, row.top - gap / 2.0, row.startIndex - bullSize - v2Gap - priceBoxSize - v2Gap - bearSize, row.bottom + gap / 2.0, text = bearPercent, text_size = v2TextSize, text_color = textColor, bgcolor = color.new(bearishColor, 50), border_color = bearishColor, border_width = 1)

curIndex = bar_index

const float weightImpact = 0.85

if ((bar_index > last_bar_index - maxDistanceToLastBar and barstate.isconfirmed and showPreviousSessions) or (barstate.islast and not showPreviousSessions)) and (not na(latestVolumeProfile))
    for i = 0 to analyzeBars
        for j = 0 to latestVolumeProfile.rows.size() - 1
            curRow = latestVolumeProfile.rows.get(j)
            vol = volume[i]
            
            if volumeWeight == "Recent"
                vol := ((vol * weightImpact) / (i + 1)) + (vol * (1.0 - weightImpact))
            
            if volumeWeight == "Past"
                vol := ((vol * weightImpact) * ((i + 1) / analyzeBars)) + (vol * (1.0 - weightImpact))
            
            if doIntersect(intersectionBody ? math.max(open[i], close[i]) : high[i], intersectionBody ? math.min(open[i], close[i]) : low[i], curRow)
                if close[i] > open[i]
                    curRow.bullVolume += vol
                else
                    curRow.bearVolume += vol
                curRow.totalVolume += vol
    //
    maxVolumeSize := 0
    minVolumeSize := latestVolumeProfile.rows.get(0).totalVolume
    rowType maxVolRow = na
    for i = 0 to latestVolumeProfile.rows.size() - 1
        curRow = latestVolumeProfile.rows.get(i)
        maxVolumeSize := math.max(maxVolumeSize, curRow.totalVolume)
        if curRow.totalVolume == maxVolumeSize
            maxVolRow := curRow
        minVolumeSize := math.min(minVolumeSize, curRow.totalVolume)

    int startIndex = na
    if alignSetting == "Left"
        startIndex := (curIndex - analyzeBars + 1)
        if not na(latestEdgeIndex)
            startIndex := math.max(startIndex, latestEdgeIndex + edgeOffset)
    else
        startIndex := (curIndex + 75)
        if not na(latestEdgeIndex)
            startIndex := math.max(startIndex, latestEdgeIndex + 75)

    latestVolumeProfile.startIndex := startIndex

    for i = 0 to latestVolumeProfile.rows.size() - 1
        curRow = latestVolumeProfile.rows.get(i)
        
        box.delete(curRow.bearBox)
        box.delete(curRow.priceBox)
        box.delete(curRow.bullBox)
        renderRow(startIndex, alignSetting == "Left" ? false : true, curRow)
        if alignSetting == "Left"
            latestVolumeProfile.edgeIndex := math.max(nz(latestVolumeProfile.edgeIndex, 0), curRow.endIndex)
        else
            latestVolumeProfile.edgeIndex := math.max(nz(latestVolumeProfile.edgeIndex, 0), curRow.startIndex)
    
    if not na(oldLine)
        oldLine.set_extend(extend.none)
        if alignSetting == "Left"
            oldLine.set_xy2(startIndex, oldLine.get_y2())
        else
            oldLine.set_xy1(latestEdgeIndex, oldLine.get_y2())
            oldLine.set_xy2(latestVolumeProfile.edgeIndex, oldLine.get_y2())

    if not na(oldBox)
        oldBox.set_right(startIndex)
    
    line.delete(latestVolumeProfile.POCLine)
    box.delete(latestVolumeProfile.highlightBox)

    if highlightSessions and alignSetting == "Left"
        latestVolumeProfile.highlightBox := box.new(startIndex, latestVolumeProfile.vpTop, nz(latestVolumeProfile.vpEnd, bar_index), latestVolumeProfile.vpBottom, bgcolor = color.new(color.blue, 80), border_width = 0)

    maxRowMid = (maxVolRow.top + maxVolRow.bottom) / 2.0
    if POCEnabled and maxVolRow.totalVolume > 0
        if alignSetting == "Left"
            latestVolumeProfile.POCLine := line.new(maxVolRow.endIndex, maxRowMid, bar_index, maxRowMid, extend = extend.right, width = POCLineWidth, color = POCLineColor, style = POCLineStyle == "____" ? line.style_solid : POCLineStyle == "----" ? line.style_dashed : line.style_dotted)
        else
            latestVolumeProfile.POCLine := line.new(bar_index, maxRowMid, maxVolRow.endIndex, maxRowMid, extend = extend.left, width = POCLineWidth, color = POCLineColor, style = POCLineStyle == "____" ? line.style_solid : POCLineStyle == "----" ? line.style_dashed : line.style_dotted)

Overview

Volume can be weighted toward recent, normal, or past bars, and swing length, row count, and alignment change how the profile is calculated and drawn. A mode switch selects pivots or order blocks, with optional past point of control lines, session highlighting, and zone styling. It does not place trades or set exits.

Details

Published by

middlechild0 on GitHub

Code credit

fluxchart, as written in the script's header

Type

Indicator, Pine Script v5

Draws

On the price chart

Category

Volume, Support & resistance, SMC / ICT / price action, Sessions & time

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 Previous SessionsbooltrueshowPreviousSessions
ModestringOrder BlocksdbgMode
Analyze Barsint200analyzeBars
Row Countint30rowCount
Volume WeightingstringNormalvolumeWeight
Swing Lengthint10swingLen
Keep Past POC LinesbooltruekeepOldPOC
[DBG] Intersection With Bodyboolfalse[DBG] Intersection With Body
Show Historic ZonesbooltrueShow Historic Zones
Show Order BlocksbooltrueShow Order Blocks
Volumetric InfobooltrueVolumetric Info
Zone InvalidationstringWickZone Invalidation
Show all 32 settings
SettingTypeDefaultVariable
Combine ZonesbooltrueCombine Zones
Max Atr Multiplierfloat3.5Max Atr Multiplier
Zone CountstringLowZone Count
Bullishcolor#08998180Bullish
Bearishcolor#f2364680Bearish
Highlight SessionsbooltruehighlightSessions
[DBG] Stylingstringv2[DBG] Styling
Align TostringLeftalignSetting
POC Line booltruePOCEnabled
string____POCLineStyle
POCLineWidthint2POCLineWidth
POCLineColorcolorcolor.yellowPOCLineColor
Bullishcolor#089981bullishColor
Bearishcolor#f23646bearishColor
Text Colorcolor#ffffff80textColor
Extend Zonesint15Extend Zones
DynamicbooltrueDynamic
Combined TextboolfalseCombined Text
Show Volume Bars AtstringLeftShow Volume Bars At
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 8 weeks ago · Custom licence GitHub repository of Moon Dev GEX Sweep & CVD

CRT State Engine

by alboogycOdR
6 stars

CRT State Engine is an overlay indicator that maps New York-timed CRT models, unmitigated order blocks, market structure, liquidity pools, and volume profile on the price chart. It can draw session boxes for Asia, London, London lunch, and New York and optiona

Indicator v5 SMC / ICT / price actionSessions & time
Updated 7 months ago · None stated GitHub repository of CRT State Engine

Smart Money Concepts

by TamTH-Dev
2 stars

This indicator maps market structure and session activity directly on the price chart. It detects and labels internal and swing structures with customizable confluence filters and candle coloring. Additionally, it highlights major trading killzones including N

Indicator v5 SMC / ICT / price actionSessions & time Repaints
Updated 5 weeks ago · None stated GitHub repository of Smart Money Concepts
1 stars

Advanced Market Theory calculates Market Profile, TPO, and Hybrid profiles directly on the price chart. It identifies key structural levels including the Point of Control, Value Area High and Low boundaries, Initial Balance range projections, single prints, po

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 11 months ago · None stated GitHub repository of Advanced Market Theory

VP

by dearvn
257 stars

This indicator calculates and plots a session-based volume profile directly on the price chart. It maps volume distribution across price levels, highlighting key structural reference points including the Point of Control, Value Area High, and Value Area Low. C

Indicator v5 VolumeSupport & resistance
Updated 6 weeks ago · None stated GitHub repository of VP

More indicators

Trade Your Charts With a Regulated Broker

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

Compare brokers