Indicator · Pine Script v6 · On the chart

Air Pocket Profile [FEELS]

by VolodymyrFilias

Air Pocket Profile builds a range-distributed volume profile over a user-set lookback and draws it on the price chart, extending it by a projection length.

View on GitHub

GitHub stars4on the repository
Pine versionv6current
Lines of code621
Settings29inputs
LicenceMIT
Last update4 months ago11 Jun 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

621 lines of Pine Script v6 from indicators/air-pocket-profile.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

air-pocket-profile.pine
//@version=6
indicator("Air Pocket Profile [FEELS]", shorttitle="Air Pocket Profile", overlay=true, max_boxes_count=500, max_lines_count=50, max_labels_count=100, max_bars_back=5000)

//--- inputs
grpCore    = "Profile"
lookback   = input.int(400, "Lookback (bars)", minval=80, maxval=1000, group=grpCore, tooltip="Number of historical bars used to build the volume profile. The profile is a range-distributed approximation, each bar spreads its volume across the price rows it spans.")
rowCount   = input.int(90, "Resolution (rows)", minval=30, maxval=140, group=grpCore, tooltip="Vertical resolution of the profile, more rows means finer detail")
freshBars  = input.int(100, "Fresh window (bars)", minval=10, maxval=500, group=grpCore, tooltip="Volume traded within this many recent bars counts as fresh")
projection = input.int(25, "Projection (bars)", minval=0, maxval=200, group=grpCore, tooltip="How far the profile extends past the last bar")

grpPocket  = "Air Pockets"
pocketThr  = input.float(0.18, "Air pocket threshold", minval=0.02, maxval=0.6, step=0.02, group=grpPocket, tooltip="An air pocket is a price range where very little volume traded. Price tends to move through these ranges fast with little resistance. Rows with volume below this fraction of the peak count as an air pocket.")
minPocket  = input.int(2, "Min pocket size (rows)", minval=1, maxval=20, group=grpPocket, tooltip="An air pocket must span at least this many rows to be drawn")
showPocket = input.bool(true, "Show air pockets", group=grpPocket)
labelZones = input.bool(true, "Label zones", group=grpPocket, tooltip="Prints a text tag on each air pocket so its meaning is clear at a glance")

grpNode    = "Volume Nodes"
showNodes  = input.bool(true, "Show high volume nodes", group=grpNode, tooltip="A high volume node (HVN) is a price shelf where a lot of volume traded. Price tends to slow down and react around these levels.")
nodeCount  = input.int(3, "Node count", minval=1, maxval=8, group=grpNode, tooltip="How many high volume nodes to mark, the volume anchor is always shown separately")
showPoc    = input.bool(true, "Show volume anchor", group=grpNode, tooltip="The volume anchor is the single heaviest price row in the profile, drawn as a glow line. It is the strongest reference level on the chart.")

grpSig     = "Signals"
showAccRej = input.bool(false, "Acceptance / rejection marks", group=grpSig, tooltip="Off by default. Marks bars where price touched a volume node. Acceptance means price stayed and traded around the node, rejection means price was pushed away from it.")
arWindow   = input.int(5, "Reaction window (bars)", minval=2, maxval=30, group=grpSig, tooltip="Bars looked at after a node touch to decide acceptance or rejection")
showTransit= input.bool(true, "Air pocket transit marks", group=grpSig, tooltip="Draws a small check mark on bars that travelled fast through an air pocket, confirming there was little resistance there. Can be turned off.")
transitSz  = input.string("Small", "Transit mark size", options=["Tiny", "Small", "Normal", "Large"], group=grpSig, tooltip="Size of the air pocket transit check mark")
transitPos = input.string("Above bar", "Transit mark position", options=["Above bar", "Inside pocket"], group=grpSig, tooltip="Above bar places the check over the price bar, inside pocket places it at the centre of the air pocket it crossed")
transitGap = input.int(10, "Transit min spacing (bars)", minval=1, maxval=100, group=grpSig, tooltip="Minimum bars between two transit marks on the same air pocket, prevents clusters of marks")
showShift  = input.bool(true, "Anchor shift arrow", group=grpSig, tooltip="Shows whether the volume anchor has migrated up or down over recent bars")
shiftMode  = input.string("Direction only", "Anchor shift display", options=["Direction only", "Direction with percent"], group=grpSig, tooltip="Direction only shows a simple rising or falling tag. Direction with percent also shows how far the anchor moved over the shift window.")
shiftBars  = input.int(60, "Anchor shift window (bars)", minval=10, maxval=400, group=grpSig, tooltip="How far back to compare the anchor position when measuring migration")

grpStyle   = "Style"
profCol    = input.color(color.rgb(78, 92, 122), "Profile fill", group=grpStyle)
freshCol   = input.color(color.rgb(38, 222, 130), "Fresh volume", group=grpStyle)
pocketCol  = input.color(color.rgb(120, 170, 255), "Air pocket", group=grpStyle)
nodeCol    = input.color(color.rgb(255, 167, 38), "Volume node", group=grpStyle)
pocCol     = input.color(color.rgb(255, 120, 40), "Volume anchor", group=grpStyle)
profContr  = input.float(2.2, "Profile contrast", minval=1.0, maxval=4.0, step=0.2, group=grpStyle, tooltip="Higher values fade weak volume rows harder and keep only the strong shelves visible")

grpDash    = "Dashboard"
showDash   = input.bool(true, "Show dashboard", group=grpDash)
dashPos    = input.string("Top Right", "Position", options=["Top Right", "Middle Right", "Bottom Right", "Top Left", "Bottom Left"], group=grpDash)
dashSize   = input.string("Normal", "Text size", options=["Small", "Normal", "Large"], group=grpDash)

//--- window bounds, evaluated every bar
winHigh = ta.highest(high, lookback)
winLow  = ta.lowest(low, lookback)
var bool hasVol = false
if not na(volume) and volume > 0
    hasVol := true

//--- lightweight anchor estimate, computed every bar with a coarse 24 bin profile
//--- this feeds a stable shift history, the on screen anchor uses the full resolution profile
f_coarseAnchor() =>
    int cb = 24
    float ch = (winHigh - winLow) / cb
    float result = na
    if ch > 0
        float[] cv = array.new<float>(cb, 0.0)
        for i = 0 to lookback - 1
            float bh = high[i]
            float bl = low[i]
            float bv = volume[i]
            if not na(bh) and not na(bl)
                float wgt = hasVol ? (na(bv) ? 0.0 : bv) : (bh - bl)
                int rLo = math.max(int(math.floor((bl - winLow) / ch)), 0)
                int rHi = math.min(int(math.floor((bh - winLow) / ch)), cb - 1)
                if rHi >= rLo
                    float inv = 1.0 / (rHi - rLo + 1)
                    for r = rLo to rHi
                        array.set(cv, r, array.get(cv, r) + wgt * inv)
        int topR = 0
        float topV = 0.0
        for r = 0 to cb - 1
            if array.get(cv, r) > topV
                topV := array.get(cv, r)
                topR := r
        result := winLow + ch * topR + ch * 0.5
    result

float coarseAnchor = f_coarseAnchor()
var float[] anchHist = array.new<float>()
if barstate.isconfirmed and not na(coarseAnchor)
    array.push(anchHist, coarseAnchor)
    if array.size(anchHist) > 500
        array.shift(anchHist)

//--- persistent state
var float[] rowVol   = array.new<float>()
var float[] rowFresh = array.new<float>()
var box[]   rowBox   = array.new<box>()
var box[]   pocketBx = array.new<box>()
var label[] pocketLb = array.new<label>()
var line[]  nodeLn   = array.new<line>()
var label[] nodeLb   = array.new<label>()
var line    pocGlowA = na
var line    pocGlowB = na
var line    pocLine  = na
var label   pocLabel = na
var table   dash     = na
var string  dashPosPrev = ""
var float[] hvnLvls  = array.new<float>()
var float[] pkTops   = array.new<float>()
var float[] pkBots   = array.new<float>()
var label[] markBuf  = array.new<label>()

//--- helpers
f_pos() =>
    switch dashPos
        "Top Right"    => position.top_right
        "Middle Right" => position.middle_right
        "Bottom Right" => position.bottom_right
        "Top Left"     => position.top_left
        "Bottom Left"  => position.bottom_left
        => position.top_right

f_size() =>
    switch dashSize
        "Small" => size.small
        "Large" => size.large
        => size.normal

f_tsize() =>
    switch transitSz
        "Tiny"   => size.tiny
        "Small"  => size.small
        "Large"  => size.large
        => size.normal

f_vol(float v) =>
    av = math.abs(v)
    av >= 1e9 ? str.tostring(v / 1e9, "#.#") + "B" : av >= 1e6 ? str.tostring(v / 1e6, "#.#") + "M" : av >= 1e3 ? str.tostring(v / 1e3, "#.#") + "K" : str.tostring(v, "#")

//--- render on the last bar only
if barstate.islast
    //--- size the bin buffers to current resolution
    if array.size(rowVol) != rowCount
        array.clear(rowVol)
        array.clear(rowFresh)
        for r = 0 to rowCount - 1
            array.push(rowVol, 0.0)
            array.push(rowFresh, 0.0)
    else
        for r = 0 to rowCount - 1
            array.set(rowVol, r, 0.0)
            array.set(rowFresh, r, 0.0)

    //--- profile box pool
    if array.size(rowBox) != rowCount
        if array.size(rowBox) > 0
            for r = 0 to array.size(rowBox) - 1
                box.delete(array.get(rowBox, r))
        array.clear(rowBox)
        for r = 0 to rowCount - 1
            array.push(rowBox, box.new(na, na, na, na, border_color=na, border_width=0))

    //--- pocket box and label pool, capped to keep within drawing limits
    int pocketMax = 24
    if array.size(pocketBx) != pocketMax
        if array.size(pocketBx) > 0
            for r = 0 to array.size(pocketBx) - 1
                box.delete(array.get(pocketBx, r))
                label.delete(array.get(pocketLb, r))
        array.clear(pocketBx)
        array.clear(pocketLb)
        for r = 0 to pocketMax - 1
            array.push(pocketBx, box.new(na, na, na, na, border_color=na, border_width=0))
            array.push(pocketLb, label.new(na, na, "", style=label.style_none))

    //--- node line and label pool
    if array.size(nodeLn) != nodeCount
        if array.size(nodeLn) > 0
            for r = 0 to array.size(nodeLn) - 1
                line.delete(array.get(nodeLn, r))
                label.delete(array.get(nodeLb, r))
        array.clear(nodeLn)
        array.clear(nodeLb)
        for r = 0 to nodeCount - 1
            array.push(nodeLn, line.new(na, na, na, na, width=2))
            array.push(nodeLb, label.new(na, na, "", style=label.style_none))

    float rowH = (winHigh - winLow) / rowCount

    if rowH > 0
        //--- range weighted volume distribution
        //--- each bar spreads its volume across every row it spans, weighted by an impulse factor
        for i = 0 to lookback - 1
            float bh = high[i]
            float bl = low[i]
            float bo = open[i]
            float bc = close[i]
            float bv = volume[i]
            if not na(bh) and not na(bl) and not na(bc)
                float rng = bh - bl
                float wgt = hasVol ? (na(bv) ? 0.0 : bv) : rng
                float body = rng > 0 ? math.abs(bc - bo) / rng : 0.0
                float impulse = 0.7 + 0.6 * body
                float load = wgt * impulse
                int rLo = int(math.floor((bl - winLow) / rowH))
                int rHi = int(math.floor((bh - winLow) / rowH))
                rLo := math.max(rLo, 0)
                rHi := math.min(rHi, rowCount - 1)
                if rHi >= rLo
                    float inv = 1.0 / (rHi - rLo + 1)
                    bool fresh = i < freshBars
                    for r = rLo to rHi
                        array.set(rowVol, r, array.get(rowVol, r) + load * inv)
                        if fresh
                            array.set(rowFresh, r, array.get(rowFresh, r) + load * inv)

        //--- profile peak and mean, used to push contrast so strong shelves stand out
        float peak = 0.0
        float volSum2 = 0.0
        for r = 0 to rowCount - 1
            float vv2 = array.get(rowVol, r)
            peak := math.max(peak, vv2)
            volSum2 += vv2
        float meanVol = rowCount > 0 ? volSum2 / rowCount : 0.0

        //--- draw the profile fill, contrast pushed so heavy shelves read clearly and weak rows fade out
        int leftX  = math.max(bar_index - lookback + 1, 0)
        int rightX = bar_index + projection
        for r = 0 to rowCount - 1
            box bx     = array.get(rowBox, r)
            float rBot = winLow + rowH * r
            float rTop = rBot + rowH
            float vv   = array.get(rowVol, r)
            float norm = peak > 0 ? vv / peak : 0.0
            color cc = na
            if norm > pocketThr
                float fr   = vv > 0 ? array.get(rowFresh, r) / vv : 0.0
                color base = color.from_gradient(fr, 0.0, 1.0, profCol, freshCol)
                //--- rescale around the mean, rows below average fade hard, rows above stay vivid
                float rel  = meanVol > 0 ? vv / meanVol : 0.0
                float emph = math.pow(math.min(rel / 2.0, 1.0), profContr)
                int tr = int(96 - emph * 70)
                tr := math.max(26, math.min(97, tr))
                cc := color.new(base, tr)
            box.set_left(bx, leftX)
            box.set_right(bx, rightX)
            box.set_top(bx, rTop)
            box.set_bottom(bx, rBot)
            box.set_bgcolor(bx, cc)
            box.set_border_color(bx, na)

        //--- detect air pockets, runs of low volume rows
        int   pocketUsed = 0
        int   runStart   = -1
        int   nearPkRow  = -1
        float nearPkDist = 1e20
        array.clear(pkTops)
        array.clear(pkBots)
        int priceRow = int(math.floor((close - winLow) / rowH))
        priceRow := math.max(0, math.min(rowCount - 1, priceRow))
        for r = 0 to rowCount - 1
            float norm = peak > 0 ? array.get(rowVol, r) / peak : 0.0
            bool isThin = norm <= pocketThr
            if isThin and runStart < 0
                runStart := r
            if (not isThin or r == rowCount - 1) and runStart >= 0
                int runEnd = isThin ? r : r - 1
                int span = runEnd - runStart + 1
                if span >= minPocket and pocketUsed < pocketMax and showPocket
                    box pb = array.get(pocketBx, pocketUsed)
                    label pl = array.get(pocketLb, pocketUsed)
                    float pBot = winLow + rowH * runStart
                    float pTop = winLow + rowH * (runEnd + 1)
                    array.push(pkTops, pTop)
                    array.push(pkBots, pBot)
                    box.set_left(pb, leftX)
                    box.set_right(pb, rightX)
                    box.set_top(pb, pTop)
                    box.set_bottom(pb, pBot)
                    box.set_bgcolor(pb, color.new(pocketCol, 93))
                    box.set_border_color(pb, color.new(pocketCol, 10))
                    box.set_border_width(pb, 2)
                    int midRow = int(math.avg(runStart, runEnd))
                    float pMid = winLow + rowH * midRow + rowH * 0.5
                    if labelZones
                        label.set_xy(pl, leftX + int((rightX - leftX) * 0.5), pMid)
                        label.set_text(pl, "AIR POCKET")
                        label.set_style(pl, label.style_label_center)
                        label.set_color(pl, color.new(pocketCol, 20))
                        label.set_textcolor(pl, color.rgb(10, 14, 26))
                        label.set_size(pl, size.small)
                    else
                        label.set_text(pl, "")
                        label.set_style(pl, label.style_none)
                    pocketUsed += 1
                    float dist = math.abs(midRow - priceRow)
                    if dist < nearPkDist
                        nearPkDist := dist
                        nearPkRow  := midRow
                runStart := -1
        //--- hide unused pocket boxes and labels
        if pocketUsed < pocketMax
            for r = pocketUsed to pocketMax - 1
                box pb = array.get(pocketBx, r)
                label pl = array.get(pocketLb, r)
                box.set_bgcolor(pb, na)
                box.set_border_color(pb, na)
                label.set_text(pl, "")
                label.set_style(pl, label.style_none)

        //--- rank high volume nodes with vertical spacing
        float[] usedY  = array.new<float>()
        float minGap   = rowH * 6.0
        int pocRow = 0
        float pocVal = 0.0
        for r = 0 to rowCount - 1
            if array.get(rowVol, r) > pocVal
                pocVal := array.get(rowVol, r)
                pocRow := r
        float pocPx = winLow + rowH * pocRow + rowH * 0.5
        if showNodes
            array.push(usedY, pocPx)
            array.clear(hvnLvls)
            //--- keep only local peaks, a row heavier than both neighbours, so nodes scale to any timeframe
            float[] peakVol = array.new<float>(rowCount, 0.0)
            for r = 1 to rowCount - 2
                float vc = array.get(rowVol, r)
                float vu = array.get(rowVol, r + 1)
                float vd = array.get(rowVol, r - 1)
                if vc >= vu and vc >= vd and r != pocRow
                    array.set(peakVol, r, vc)
            int placed = 0
            for n = 0 to nodeCount - 1
                line ln = array.get(nodeLn, n)
                label lb = array.get(nodeLb, n)
                float best = 0.0
                int   bestR = -1
                for r = 0 to rowCount - 1
                    if array.get(peakVol, r) > best
                        best := array.get(peakVol, r)
                        bestR := r
                bool shown = false
                if bestR >= 0 and peak > 0 and best / peak > 0.05
                    float price = winLow + rowH * bestR + rowH * 0.5
                    bool clash = false
                    if array.size(usedY) > 0
                        for u = 0 to array.size(usedY) - 1
                            if math.abs(array.get(usedY, u) - price) < minGap
                                clash := true
                    if not clash
                        float fr = best > 0 ? array.get(rowFresh, bestR) / array.get(rowVol, bestR) : 0.0
                        color lc = fr > 0.5 ? freshCol : nodeCol
                        line.set_xy1(ln, leftX, price)
                        line.set_xy2(ln, rightX, price)
                        line.set_color(ln, lc)
                        line.set_style(ln, line.style_solid)
                        label.set_xy(lb, rightX, price)
                        label.set_text(lb, "HVN  " + f_vol(array.get(rowVol, bestR)))
                        label.set_style(lb, label.style_label_left)
                        label.set_color(lb, color.rgb(15, 20, 33))
                        label.set_textcolor(lb, lc)
                        label.set_size(lb, size.small)
                        array.push(usedY, price)
                        array.push(hvnLvls, price)
                        shown := true
                    array.set(peakVol, bestR, 0.0)
                if not shown
                    line.set_xy1(ln, na, na)
                    line.set_xy2(ln, na, na)
                    label.set_text(lb, "")
                    label.set_style(lb, label.style_none)
        else
            //--- hide all node objects when the toggle is off
            if array.size(nodeLn) > 0
                for n = 0 to array.size(nodeLn) - 1
                    line ln = array.get(nodeLn, n)
                    label lb = array.get(nodeLb, n)
                    line.set_xy1(ln, na, na)
                    line.set_xy2(ln, na, na)
                    label.set_text(lb, "")
                    label.set_style(lb, label.style_none)

        //--- anchor shift, compares the current anchor with its recorded position shiftBars ago
        float shiftPct = na
        if showShift
            int hsz = array.size(anchHist)
            if hsz > shiftBars
                float pastAnchor = array.get(anchHist, hsz - 1 - shiftBars)
                float curAnchor  = winLow + rowH * pocRow + rowH * 0.5
                if not na(pastAnchor) and pastAnchor > 0
                    shiftPct := (curAnchor - pastAnchor) / pastAnchor * 100.0

        //--- volume anchor, the single heaviest row, drawn with a layered glow
        float pocPrice = winLow + rowH * pocRow + rowH * 0.5
        if showPoc
            if na(pocGlowA)
                pocGlowA := line.new(na, na, na, na, width=10)
            if na(pocGlowB)
                pocGlowB := line.new(na, na, na, na, width=4)
            if na(pocLine)
                pocLine := line.new(na, na, na, na, width=1)
            if na(pocLabel)
                pocLabel := label.new(na, na, "", style=label.style_label_left)
            line.set_xy1(pocGlowA, leftX, pocPrice)
            line.set_xy2(pocGlowA, rightX, pocPrice)
            line.set_color(pocGlowA, color.new(pocCol, 85))
            line.set_xy1(pocGlowB, leftX, pocPrice)
            line.set_xy2(pocGlowB, rightX, pocPrice)
            line.set_color(pocGlowB, color.new(pocCol, 60))
            line.set_xy1(pocLine, leftX, pocPrice)
            line.set_xy2(pocLine, rightX, pocPrice)
            line.set_color(pocLine, pocCol)
            string anchorTxt = "Anchor  " + f_vol(array.get(rowVol, pocRow))
            if not na(shiftPct) and math.abs(shiftPct) >= 1.0
                bool up = shiftPct > 0
                if shiftMode == "Direction with percent"
                    anchorTxt += up ? "   ▲ rising " + str.tostring(shiftPct, "0.0") + "%" : "   ▼ falling " + str.tostring(shiftPct, "0.0") + "%"
                else
                    anchorTxt += up ? "   ▲ rising" : "   ▼ falling"
            label.set_xy(pocLabel, rightX, pocPrice)
            label.set_text(pocLabel, anchorTxt)
            label.set_color(pocLabel, pocCol)
            label.set_textcolor(pocLabel, color.rgb(12, 16, 28))
            label.set_style(pocLabel, label.style_label_left)
            label.set_size(pocLabel, size.small)
        else
            //--- hide anchor objects when the toggle is off
            if not na(pocGlowA)
                line.set_xy1(pocGlowA, na, na)
                line.set_xy2(pocGlowA, na, na)
            if not na(pocGlowB)
                line.set_xy1(pocGlowB, na, na)
                line.set_xy2(pocGlowB, na, na)
            if not na(pocLine)
                line.set_xy1(pocLine, na, na)
                line.set_xy2(pocLine, na, na)
            if not na(pocLabel)
                label.set_text(pocLabel, "")
                label.set_style(pocLabel, label.style_none)

        //--- dashboard
        if showDash
            float nearPkPrice = nearPkRow >= 0 ? winLow + rowH * nearPkRow + rowH * 0.5 : na
            string pkText = "None"
            color  pkCol  = color.gray
            if not na(nearPkPrice)
                pkText := str.tostring(nearPkPrice, format.mintick) + (nearPkPrice > close ? "  up" : "  down")
                pkCol  := pocketCol

            float normHere = peak > 0 ? array.get(rowVol, priceRow) / peak : 0.0
            string locTxt = normHere <= pocketThr ? "Air pocket" : normHere > 0.6 ? "Volume node" : "Profile"
            color  locCol = normHere <= pocketThr ? pocketCol : normHere > 0.6 ? nodeCol : color.rgb(165, 174, 194)

            float freshHere = 0.0
            float volAll    = 0.0
            for r = 0 to rowCount - 1
                volAll    += array.get(rowVol, r)
                freshHere += array.get(rowFresh, r)
            float freshPct = volAll > 0 ? freshHere / volAll * 100.0 : 0.0

            if na(dash) or dashPosPrev != dashPos
                if not na(dash)
                    table.delete(dash)
                dash := table.new(f_pos(), 2, 7, border_width=1, border_color=color.rgb(45, 53, 72), frame_width=1, frame_color=color.rgb(45, 53, 72))
                dashPosPrev := dashPos

            color hdrBg  = color.rgb(15, 20, 33)
            color cellBg = color.rgb(22, 28, 44)
            color txtCol = color.rgb(165, 174, 194)
            color legCol = color.rgb(120, 130, 150)

            //--- profile balance, share of volume above vs below the anchor
            float volAbove = 0.0
            float volBelow = 0.0
            for r = 0 to rowCount - 1
                if r > pocRow
                    volAbove += array.get(rowVol, r)
                else if r < pocRow
                    volBelow += array.get(rowVol, r)
            float balDen = volAbove + volBelow
            float abovePct = balDen > 0 ? volAbove / balDen * 100.0 : 50.0
            string balTxt = abovePct > 55.0 ? "Heavy above" : abovePct < 45.0 ? "Heavy below" : "Even"
            color  balCol = abovePct > 55.0 or abovePct < 45.0 ? nodeCol : color.rgb(165, 174, 194)

            table.cell(dash, 0, 0, "AIR POCKET PROFILE", text_color=color.white, text_size=f_size(), bgcolor=hdrBg)
            table.cell(dash, 1, 0, "", bgcolor=hdrBg)
            table.merge_cells(dash, 0, 0, 1, 0)

            table.cell(dash, 0, 1, " Price is in", text_color=txtCol, text_size=f_size(), bgcolor=cellBg, text_halign=text.align_left)
            table.cell(dash, 1, 1, locTxt + " ", text_color=locCol, text_size=f_size(), bgcolor=cellBg, text_halign=text.align_right)

            table.cell(dash, 0, 2, " Nearest pocket", text_color=txtCol, text_size=f_size(), bgcolor=cellBg, text_halign=text.align_left)
            table.cell(dash, 1, 2, pkText + " ", text_color=pkCol, text_size=f_size(), bgcolor=cellBg, text_halign=text.align_right)

            table.cell(dash, 0, 3, " Volume anchor", text_color=txtCol, text_size=f_size(), bgcolor=cellBg, text_halign=text.align_left)
            table.cell(dash, 1, 3, str.tostring(pocPrice, format.mintick) + " ", text_color=pocCol, text_size=f_size(), bgcolor=cellBg, text_halign=text.align_right)

            table.cell(dash, 0, 4, " Profile balance", text_color=txtCol, text_size=f_size(), bgcolor=cellBg, text_halign=text.align_left)
            table.cell(dash, 1, 4, balTxt + "  " + str.tostring(abovePct, "0") + "% ", text_color=balCol, text_size=f_size(), bgcolor=cellBg, text_halign=text.align_right)

            table.cell(dash, 0, 5, " Fresh volume", text_color=txtCol, text_size=f_size(), bgcolor=cellBg, text_halign=text.align_left)
            table.cell(dash, 1, 5, str.tostring(freshPct, "0") + "% ", text_color=freshPct > 50.0 ? freshCol : color.white, text_size=f_size(), bgcolor=cellBg, text_halign=text.align_right)

            table.cell(dash, 0, 6, " HVN (heavy shelf)  ·  Anchor (peak volume)  ·  Pocket (thin volume) ", text_color=legCol, text_size=size.tiny, bgcolor=cellBg, text_halign=text.align_left)
            table.cell(dash, 1, 6, "", bgcolor=cellBg)
            table.merge_cells(dash, 0, 6, 1, 6)
        else
            //--- remove the dashboard when the toggle is off
            if not na(dash)
                table.delete(dash)
                dash := na
                dashPosPrev := ""

        //--- behaviour marks, based on confirmed bars to avoid intrabar repainting
        int markMax = 28
        if array.size(markBuf) != markMax
            if array.size(markBuf) > 0
                for r = 0 to array.size(markBuf) - 1
                    label.delete(array.get(markBuf, r))
            array.clear(markBuf)
            for r = 0 to markMax - 1
                array.push(markBuf, label.new(na, na, "", style=label.style_none))
        int markUsed = 0
        float markTol = rowH * 0.6
        //--- acceptance and rejection at volume nodes
        if showAccRej and array.size(hvnLvls) > 0
            for i = arWindow to lookback - 1
                if markUsed < markMax
                    float bh = high[i]
                    float bl = low[i]
                    if not na(bh) and not na(bl)
                        bool touch = false
                        float lvl = na
                        for h = 0 to array.size(hvnLvls) - 1
                            float hl = array.get(hvnLvls, h)
                            if bh >= hl - markTol and bl <= hl + markTol
                                touch := true
                                lvl := hl
                        if touch and not na(lvl)
                            bool wasTouchPrev = false
                            float ph = high[i + 1]
                            float pl2 = low[i + 1]
                            if not na(ph) and not na(pl2)
                                wasTouchPrev := ph >= lvl - markTol and pl2 <= lvl + markTol
                            if not wasTouchPrev
                                int near = 0
                                for k = 1 to arWindow
                                    float kh = high[i - k]
                                    float kl = low[i - k]
                                    if not na(kh) and not na(kl)
                                        if kh >= lvl - markTol and kl <= lvl + markTol
                                            near += 1
                                bool accepted = near >= int(math.ceil(arWindow / 2.0))
                                label lb = array.get(markBuf, markUsed)
                                label.set_xy(lb, bar_index - i, accepted ? bl : bh)
                                label.set_text(lb, accepted ? "A" : "R")
                                label.set_style(lb, accepted ? label.style_label_up : label.style_label_down)
                                label.set_color(lb, accepted ? color.new(nodeCol, 20) : color.new(pocketCol, 20))
                                label.set_textcolor(lb, color.rgb(12, 16, 28))
                                label.set_size(lb, size.tiny)
                                markUsed += 1
        //--- air pocket transit, a bar that covered most of a pocket in one move
        if showTransit and array.size(pkTops) > 0
            int pkn = array.size(pkTops)
            int[] lastMarkBar = array.new<int>(pkn, -100000)
            for i = 0 to lookback - 1
                if markUsed < markMax
                    float bh = high[i]
                    float bl = low[i]
                    if not na(bh) and not na(bl) and bh > bl
                        for p = 0 to pkn - 1
                            float pt = array.get(pkTops, p)
                            float pb2 = array.get(pkBots, p)
                            float pkH = pt - pb2
                            if pkH > 0 and markUsed < markMax
                                float overlap = math.min(bh, pt) - math.max(bl, pb2)
                                if overlap > 0 and overlap >= pkH * 0.7
                                    int thisBar = bar_index - i
                                    if math.abs(thisBar - array.get(lastMarkBar, p)) >= transitGap
                                        array.set(lastMarkBar, p, thisBar)
                                        label lb = array.get(markBuf, markUsed)
                                        bool above = transitPos == "Above bar"
                                        label.set_xy(lb, thisBar, above ? high[i] : math.avg(pt, pb2))
                                        label.set_text(lb, "✓")
                                        label.set_style(lb, above ? label.style_label_down : label.style_label_center)
                                        label.set_color(lb, color.new(pocketCol, 0))
                                        label.set_textcolor(lb, color.rgb(12, 16, 28))
                                        label.set_size(lb, f_tsize())
                                        markUsed += 1
        //--- hide unused marks
        if markUsed < markMax
            for r = markUsed to markMax - 1
                label lb = array.get(markBuf, r)
                label.set_text(lb, "")
                label.set_style(lb, label.style_none)

//--- alerts
var float pocLvl    = na
var float pocketTop = na
var float pocketBot = na
if barstate.islast
    float rh = (winHigh - winLow) / rowCount
    if rh > 0 and array.size(rowVol) == rowCount and rowCount > 0
        float pk = 0.0
        for r = 0 to rowCount - 1
            pk := math.max(pk, array.get(rowVol, r))
        int pr = 0
        float pv = 0.0
        for r = 0 to rowCount - 1
            if array.get(rowVol, r) > pv
                pv := array.get(rowVol, r)
                pr := r
        pocLvl := winLow + rh * pr + rh * 0.5
        int prow = int(math.floor((close - winLow) / rh))
        prow := math.max(0, math.min(rowCount - 1, prow))
        float nh = pk > 0 ? array.get(rowVol, prow) / pk : 1.0
        if nh <= pocketThr
            pocketTop := winLow + rh * (prow + 1)
            pocketBot := winLow + rh * prow
        else
            pocketTop := na
            pocketBot := na
bool intoPocket = not na(pocketTop) and close <= pocketTop and close >= pocketBot and not (close[1] <= pocketTop and close[1] >= pocketBot)
bool atPoc = not na(pocLvl) and low <= pocLvl and high >= pocLvl and not (low[1] <= pocLvl and high[1] >= pocLvl)
alertcondition(intoPocket, "Price entered an air pocket", "Price moved into a low volume air pocket on {{ticker}}")
alertcondition(atPoc, "Price reached the volume anchor", "Price reached the volume anchor level on {{ticker}}")

Overview

Rows below a fraction of peak volume form air-pocket zones, while the heaviest row is the volume anchor and other peaks can be marked as high-volume nodes. Recent bars can be weighted as fresh volume. Optional marks show node acceptance or rejection, fast transits through pockets, and shifts of the volume anchor. A dashboard summarizes the profile. It does not place trades.

Details

Published by

VolodymyrFilias on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Volume, Support & resistance

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.

Topics

indicatorsliquiditymarket-structurepine-script-v6pinescriptsmart-money-conceptstechnical-analysistradingtradingviewvolume-profile

Settings

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

SettingTypeDefaultVariable
Lookback (bars)int400lookback
Resolution (rows)int90rowCount
Fresh window (bars)int100freshBars
Projection (bars)int25projection
Air pocket thresholdfloat0.18pocketThr
Min pocket size (rows)int2minPocket
Show air pocketsbooltrueshowPocket
Label zonesbooltruelabelZones
Show high volume nodesbooltrueshowNodes
Node countint3nodeCount
Show volume anchorbooltrueshowPoc
Acceptance / rejection marksboolfalseshowAccRej
Show all 29 settings
SettingTypeDefaultVariable
Reaction window (bars)int5arWindow
Air pocket transit marksbooltrueshowTransit
Transit mark sizestringSmalltransitSz
Transit mark positionstringAbove bartransitPos
Transit min spacing (bars)int10transitGap
Anchor shift arrowbooltrueshowShift
Anchor shift displaystringDirection onlyshiftMode
Anchor shift window (bars)int60shiftBars
Profile fillcolorcolor.rgb(78, 92, 122)profCol
Fresh volumecolorcolor.rgb(38, 222, 130)freshCol
Air pocketcolorcolor.rgb(120, 170, 255)pocketCol
Volume nodecolorcolor.rgb(255, 167, 38)nodeCol
Volume anchorcolorcolor.rgb(255, 120, 40)pocCol
Profile contrastfloat2.2profContr
Show dashboardbooltrueshowDash
PositionstringTop RightdashPos
Text sizestringNormaldashSize
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

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

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

MGIC-tracker

by Allen1212
303 stars

The MGIC-tracker is a TradingView indicator that overlays ARK Invest trade records onto stock price charts. It plots buy and sell labels at the exact timestamp of each transaction, showing share volume and ETF weight. Users select a specific ETF type via setti

Indicator v4 Support & resistanceVolume
Updated 2 years ago · None stated GitHub repository of MGIC-tracker
218 stars

Comprehensive Volume Profile is an overlay indicator that calculates and plots volume distribution across specified time ranges such as daily sessions, weekly, monthly, or custom bar lengths. It identifies key structural price levels including the Point of Con

Indicator v6 VolumeSupport & resistance
Updated 3 months ago · None stated GitHub repository of Comprehensive Volume Profile

Moon Dev GEX Arrows

by moondevonyt
92 stars

This overlay indicator plots option strike walls and an optional session VWAP on the price chart for SPY or ES. It spaces major strikes by the selected instrument, draws a set number of strikes on each side, and marks wall-trap and sweep events after a cooldow

Indicator v5 Support & resistanceSessions & time
Updated 8 weeks ago · Custom licence GitHub repository of Moon Dev GEX Arrows

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