Skip to content

Free TradingView indicator

Session Volume Profile

Volume profile of the current or previous session drawn with boxes, showing the POC and the 70% value area.

Volumev1.0TradingView (Pine Script v6) Non-repainting signals Alerts

What it does

A volume profile shows how much volume traded at each price level, rather than at each point in time. This indicator builds a profile for one daily session and draws it as horizontal boxes starting at the first bar of that session. It highlights three reference levels:

  • POC (point of control): the price row with the most volume.
  • VAH and VAL: the top and bottom of the value area, the band around the POC that holds 70% of the session's volume by default.

You can draw either the developing profile of the current session or the finished profile of the previous one.

How it works

  1. During each session the script stores every bar's high, low and volume in arrays (bounded by "Max bars stored per session").
  2. The session range, from the lowest low to the highest high, is split into equal price rows.
  3. Each bar's volume is spread across the rows its high-low range covers, in proportion to how much of the bar overlaps each row. A bar with no range puts all its volume in one row.
  4. POC = the row with the largest total.
  5. Value area: starting at the POC, the script repeatedly adds whichever neighboring row (above or below) has more volume, until the included rows reach the value area percentage.
  6. When a new session starts, the finished session's profile is calculated once and its POC, VAH and VAL are stored. Alerts use those stored levels, so they do not change afterwards.
  7. Boxes and lines are created once and moved, so the drawing count stays fixed.

How to use it

The previous session's value area is a popular framework for the next session. If price opens inside it, many traders expect rotation between VAH and VAL. If price opens outside and holds, that can be read as acceptance of new prices. The POC often acts as a magnet or as a support or resistance reference.

The developing profile helps on M1 to M15 charts to see where today's business is concentrated. Low-volume rows inside the range often mark areas price passed through quickly.

Settings

InputDefaultWhat it changes
Session to drawCurrent sessionDeveloping or completed profile.
Price rows30Detail of the profile. More rows = finer detail, more boxes.
Value area (% of volume)70Width of the value area.
Max bars stored per session3000Memory limit; older bars of a very long session are dropped.
Profile width (bars)40Visual length of the largest row.
Extend POC / VAH / VAL to the rightonProjects the levels past the last bar.
Plot previous-session historyoffStep lines of past sessions' levels.

Limitations

  • This is a bar-based approximation. Real volume-at-price data would place each trade at its exact price; here each bar's volume is spread evenly across its range.
  • On spot forex, volume is tick volume from one data feed, not exchange volume.
  • The developing profile changes with every new bar by design; only the stored previous-session levels are fixed.
  • Sessions follow the symbol's daily boundary in TradingView, not a custom session window.
  • It requires an intraday chart. On very long sessions with M1 data, raising the row count increases calculation time.

Pine Script source

In TradingView open Pine Editor, paste the code, click Save and then Add to chart.

FXP_SessionVolumeProfile.pine

199 lines

// © FXPIPS ONLINE — https://fxpips.online/tradingview-indicators/session-volume-profile/
// License: MIT. Educational tool — not financial advice. Trading involves substantial risk.
//@version=6
indicator("FXP Session Volume Profile", shorttitle = "FXP Session VP", overlay = true, max_boxes_count = 120, max_lines_count = 10)

// ── Inputs ─────────────────────────────────────────────────────────────
grpP      = "Profile"
modeSel   = input.string("Current session", "Session to draw", options = ["Current session", "Previous session"], group = grpP)
rows      = input.int(30, "Price rows", minval = 5, maxval = 100, group = grpP)
vaPct     = input.float(70.0, "Value area (% of volume)", minval = 10.0, maxval = 95.0, step = 1.0, group = grpP)
maxBars   = input.int(3000, "Max bars stored per session", minval = 50, maxval = 5000, group = grpP)

grpVis    = "Display"
widthBars = input.int(40, "Profile width (bars)", minval = 5, maxval = 300, group = grpVis)
extendLv  = input.bool(true, "Extend POC / VAH / VAL to the right", group = grpVis)
showHist  = input.bool(false, "Plot previous-session POC / VAH / VAL history", group = grpVis)
vaCol     = input.color(color.new(color.blue, 55), "Value area rows", group = grpVis)
outCol    = input.color(color.new(color.gray, 75), "Rows outside value area", group = grpVis)
pocCol    = input.color(color.orange, "POC color", group = grpVis)
edgeCol   = input.color(color.blue, "VAH / VAL color", group = grpVis)

if timeframe.isdwm
    runtime.error("The session volume profile needs an intraday chart.")

// ── Profile function ───────────────────────────────────────────────────
// Spreads each bar's volume across the rows its high-low range covers, in proportion to the overlap.
// Fills `prof` (size = nRows) and returns [poc, vah, val, bottom, rowHeight, totalVolume].
calcProfile(array<float> hs, array<float> ls, array<float> vs, array<float> prof, int nRows, float vaPercent) =>
    float pocP  = na
    float vahP  = na
    float valP  = na
    float bot   = na
    float step  = na
    float total = 0.0
    array.fill(prof, 0.0)
    int n = array.size(vs)
    if n > 0
        float top = array.max(hs)
        bot  := array.min(ls)
        step := math.max((top - bot) / nRows, syminfo.mintick)
        for i = 0 to n - 1
            float h = array.get(hs, i)
            float l = array.get(ls, i)
            float v = array.get(vs, i)
            total += v
            int rLo = math.max(0, math.min(nRows - 1, math.floor((l - bot) / step)))
            int rHi = math.max(0, math.min(nRows - 1, math.floor((h - bot) / step)))
            if rHi == rLo or h <= l
                array.set(prof, rLo, array.get(prof, rLo) + v)
            else
                for r = rLo to rHi
                    float rBot = bot + r * step
                    float overlap = math.max(0.0, math.min(h, rBot + step) - math.max(l, rBot))
                    array.set(prof, r, array.get(prof, r) + v * overlap / (h - l))
        int poc = array.indexof(prof, array.max(prof))
        int up  = poc
        int dn  = poc
        float acc    = array.get(prof, poc)
        float target = total * vaPercent / 100.0
        while acc < target and (up < nRows - 1 or dn > 0)
            float vUp = up < nRows - 1 ? array.get(prof, up + 1) : -1.0
            float vDn = dn > 0 ? array.get(prof, dn - 1) : -1.0
            if vUp >= vDn
                up  += 1
                acc += vUp
            else
                dn  -= 1
                acc += vDn
        pocP := bot + (poc + 0.5) * step
        vahP := bot + (up + 1) * step
        valP := bot + dn * step
    [pocP, vahP, valP, bot, step, total]

// ── Bar storage (bounded) ──────────────────────────────────────────────
bool newSession = timeframe.change("D")

var array<float> curH = array.new_float()
var array<float> curL = array.new_float()
var array<float> curV = array.new_float()
var array<float> prvH = array.new_float()
var array<float> prvL = array.new_float()
var array<float> prvV = array.new_float()
var array<float> curProf = array.new_float(rows, 0.0)
var array<float> prvProf = array.new_float(rows, 0.0)
var int   curStart = na
var int   prvStart = na
var int   prvEnd   = na
var float prvPoc   = na
var float prvVah   = na
var float prvVal   = na
var float prvBot   = na
var float prvStep  = na
var float prvTotal = 0.0

if na(curStart)
    curStart := bar_index

if newSession and array.size(curV) > 0
    prvH := array.copy(curH)
    prvL := array.copy(curL)
    prvV := array.copy(curV)
    prvStart := curStart
    prvEnd   := bar_index - 1
    array.clear(curH)
    array.clear(curL)
    array.clear(curV)
    curStart := bar_index
    [p0, h0, l0, b0, s0, t0] = calcProfile(prvH, prvL, prvV, prvProf, rows, vaPct)
    prvPoc   := p0
    prvVah   := h0
    prvVal   := l0
    prvBot   := b0
    prvStep  := s0
    prvTotal := t0

array.push(curH, high)
array.push(curL, low)
array.push(curV, nz(volume))
if array.size(curV) > maxBars
    array.shift(curH)
    array.shift(curL)
    array.shift(curV)

// ── Drawing (last bar only, objects reused) ────────────────────────────
var array<box> boxes = array.new_box()
var line pocLine = na
var line vahLine = na
var line valLine = na

if barstate.islast
    bool useCur = modeSel == "Current session"
    array<float> prof = prvProf
    float dPoc   = prvPoc
    float dVah   = prvVah
    float dVal   = prvVal
    float dBot   = prvBot
    float dStep  = prvStep
    float dTotal = prvTotal
    int   left   = prvStart
    if useCur
        [p1, h1, l1, b1, s1, t1] = calcProfile(curH, curL, curV, curProf, rows, vaPct)
        prof   := curProf
        dPoc   := p1
        dVah   := h1
        dVal   := l1
        dBot   := b1
        dStep  := s1
        dTotal := t1
        left   := math.max(curStart, bar_index - array.size(curV) + 1)
    else if not na(prvStart) and not na(prvEnd)
        left := math.max(prvStart, prvEnd - array.size(prvV) + 1)

    if array.size(boxes) == 0
        for r = 0 to rows - 1
            array.push(boxes, box.new(bar_index, 0.0, bar_index, 0.0, border_color = na, bgcolor = na))

    bool canDraw = not na(left) and not na(dStep) and dTotal > 0
    float maxV = canDraw ? array.max(prof) : 0.0
    for r = 0 to rows - 1
        box b = array.get(boxes, r)
        if canDraw and maxV > 0
            float rBot = dBot + r * dStep
            int w = math.round(widthBars * array.get(prof, r) / maxV)
            bool inVa = rBot >= dVal - dStep * 0.5 and rBot < dVah
            box.set_lefttop(b, left, rBot + dStep)
            box.set_rightbottom(b, left + w, rBot)
            box.set_bgcolor(b, inVa ? vaCol : outCol)
        else
            box.set_bgcolor(b, na)

    if canDraw
        string ext = extendLv ? extend.right : extend.none
        if na(pocLine)
            pocLine := line.new(left, dPoc, bar_index, dPoc, color = pocCol, width = 2)
            vahLine := line.new(left, dVah, bar_index, dVah, color = edgeCol, style = line.style_dashed)
            valLine := line.new(left, dVal, bar_index, dVal, color = edgeCol, style = line.style_dashed)
        line.set_xy1(pocLine, left, dPoc)
        line.set_xy2(pocLine, bar_index, dPoc)
        line.set_xy1(vahLine, left, dVah)
        line.set_xy2(vahLine, bar_index, dVah)
        line.set_xy1(valLine, left, dVal)
        line.set_xy2(valLine, bar_index, dVal)
        line.set_extend(pocLine, ext)
        line.set_extend(vahLine, ext)
        line.set_extend(valLine, ext)

// ── Previous-session levels as series (used for history plots and alerts) ──
plot(showHist ? prvPoc : na, "Prev POC", color = pocCol, style = plot.style_stepline)
plot(showHist ? prvVah : na, "Prev VAH", color = edgeCol, style = plot.style_stepline)
plot(showHist ? prvVal : na, "Prev VAL", color = edgeCol, style = plot.style_stepline)

bool xPoc = ta.cross(close, prvPoc) and barstate.isconfirmed
bool xVah = ta.crossover(close, prvVah) and barstate.isconfirmed
bool xVal = ta.crossunder(close, prvVal) and barstate.isconfirmed

alertcondition(xPoc, "Cross of previous POC", "FXP Session VP: price crossed the previous session POC on {{ticker}} {{interval}}")
alertcondition(xVah, "Close above previous VAH", "FXP Session VP: close above the previous session value area high on {{ticker}} {{interval}}")
alertcondition(xVal, "Close below previous VAL", "FXP Session VP: close below the previous session value area low on {{ticker}} {{interval}}")

Key inputs

Key inputs of Session Volume Profile
InputDefaultDescription
Session to drawCurrent sessionDraw the developing profile of today or the completed profile of the previous session.
Price rows30Number of horizontal price rows the session range is split into (5 to 100).
Value area (% of volume)70Share of session volume contained in the value area.
Max bars stored per session3000Upper limit on bars kept in memory for one session.
Profile width (bars)40Horizontal length of the largest row.
Plot previous-session POC / VAH / VAL historyfalseDraw the prior session's levels as step lines across history.

Educational tools, not advice. Indicators and Expert Advisors describe price; they do not predict it, and past behaviour does not guarantee future results. Test everything on a demo account first and never risk money you cannot afford to lose. See our disclaimer.

Keep exploring

Related tradingview indicators