Pine Script V5 Volume Profile Free / Open Source

Session Volume Profile

Overview

Price tells you where the market has been. Volume tells you where the market agreed it was worth being there. A volume profile reorganizes traded volume by price rather than by time, revealing exactly which levels absorbed the most participation during a session — and which levels price moved through quickly, with little acceptance.

That distribution is the closest real-time proxy retail traders have to how market makers themselves define fair value. The Point of Control (POC) marks the single price that traded the most volume — the level the market has, so far, agreed represents efficient value. The Value Area — typically the range containing 70% of total session volume — marks the broader zone of general acceptance. Price outside that range represents comparative rejection: the market moved through it without building the participation to justify staying there. Session Volume Profile calculates this distribution live, recalculating the POC and Value Area boundaries with every new bar of volume.

Features of This Indicator

How to Use

  1. Copy the script into TradingView's Pine Editor and click Add to Chart.
  2. Set the Profile Session input to the window you want profiled — a full trading day, a specific session, or any custom range in exchange time.
  3. Watch the profile build in real time: the solid line marks the POC, and the shaded band between the dashed lines marks the Value Area (VAH/VAL).
  4. Treat price moving outside the Value Area as a rejection signal, and price returning into it as the market re-accepting that range as fair value.
Pro Tip: The POC is not fixed — it's the level the market has currently agreed represents fair value for the volume traded so far, and it shifts as new volume accumulates. When price later returns to a prior session's POC and holds, that level frequently behaves as a dynamic support/resistance magnet, since it marks the price the market previously found most efficient to transact at.

The Pine Script V5 Code

session-volume-profile.pine
//@version=5
indicator("Session Volume Profile — Crypto Indicator Pro", shorttitle="SVP [CIP]", overlay=true, max_boxes_count=500, max_lines_count=500)

// ============================================================================
// SESSION VOLUME PROFILE
// Builds a live, session-anchored volume distribution across a fixed number
// of price rows, then derives the Point of Control (POC) -- the price level
// with the highest traded volume -- and the Value Area High/Low (VAH/VAL),
// the boundary of the range containing a user-defined percentage of total
// session volume. This distribution is how market makers gauge where the
// market has accepted fair value (high-volume nodes) versus rejected price
// quickly (low-volume nodes). The profile recalculates every bar and is
// fully non-repainting once a session has closed.
// ============================================================================

// ---------------------------- INPUTS ----------------------------
sessionString = input.session("0000-2400", "Profile Session (Exchange Time)", group="Session")
rows          = input.int(24, "Number of Volume Rows", minval=10, maxval=100, group="Profile")
valueAreaPct  = input.float(70.0, "Value Area %", minval=50.0, maxval=95.0, group="Profile")
profileWidth  = input.int(30, "Profile Node Max Width (bars)", minval=5, maxval=100, group="Display")
pocColor      = input.color(color.new(color.yellow, 0), "POC Color", group="Display")
vaColor       = input.color(color.new(color.blue, 60), "Value Area Color", group="Display")
nodeColor     = input.color(color.new(color.gray, 70), "Volume Node Color", group="Display")

// ---------------------------- SESSION DETECTION ----------------------------
inSession  = not na(time(timeframe.period, sessionString))
newSession = inSession and not inSession[1]

// ---------------------------- SESSION DATA STORAGE ----------------------------
var array<float> barHigh = array.new<float>()
var array<float> barLow  = array.new<float>()
var array<float> barVol  = array.new<float>()

if newSession
    array.clear(barHigh)
    array.clear(barLow)
    array.clear(barVol)

if inSession
    array.push(barHigh, high)
    array.push(barLow, low)
    array.push(barVol, volume)

// ---------------------------- PERSISTENT STATE ----------------------------
var array<box>   nodeBoxes      = array.new<box>()
var array<float> rowVolume      = array.new<float>(rows, 0.0)
var line         pocLine        = na
var line         vahLine        = na
var line         valLine        = na
var float        pocPriceStored = na

// ---------------------------- PROFILE CALCULATION (recomputed every in-session bar) ----------------------------
if inSession and array.size(barHigh) > 0
    sessHigh  = array.max(barHigh)
    sessLow   = array.min(barLow)
    rangeSpan = sessHigh - sessLow

    if rangeSpan > 0
        rowHeight = rangeSpan / rows
        array.fill(rowVolume, 0.0)

        // Distribute each stored bar's volume into the row containing its typical price
        for i = 0 to array.size(barHigh) - 1
            typicalPrice = (array.get(barHigh, i) + array.get(barLow, i)) / 2
            rowIdx = int(math.min(rows - 1, math.max(0, math.floor((typicalPrice - sessLow) / rowHeight))))
            array.set(rowVolume, rowIdx, array.get(rowVolume, rowIdx) + array.get(barVol, i))

        // ---------------- POINT OF CONTROL ----------------
        maxRowVol = array.max(rowVolume)
        pocRow    = array.indexof(rowVolume, maxRowVol)
        pocPrice  = sessLow + (pocRow + 0.5) * rowHeight
        pocPriceStored := pocPrice

        // ---------------- VALUE AREA (expand outward from POC until target volume is captured) ----------------
        totalVolume    = array.sum(rowVolume)
        targetVolume   = totalVolume * (valueAreaPct / 100)
        capturedVolume = array.get(rowVolume, pocRow)
        vahRow = pocRow
        valRow = pocRow

        while capturedVolume < targetVolume and (vahRow < rows - 1 or valRow > 0)
            volAbove = vahRow < rows - 1 ? array.get(rowVolume, vahRow + 1) : -1.0
            volBelow = valRow > 0 ? array.get(rowVolume, valRow - 1) : -1.0
            if volAbove >= volBelow
                vahRow += 1
                capturedVolume += volAbove
            else
                valRow -= 1
                capturedVolume += volBelow

        vahPrice = sessLow + (vahRow + 1.0) * rowHeight
        valPrice = sessLow + valRow * rowHeight

        // ---------------- CLEAR PREVIOUS BAR'S DRAWING OBJECTS ----------------
        if array.size(nodeBoxes) > 0
            for i = array.size(nodeBoxes) - 1 to 0
                box.delete(array.get(nodeBoxes, i))
            array.clear(nodeBoxes)
        if not na(pocLine)
            line.delete(pocLine)
        if not na(vahLine)
            line.delete(vahLine)
        if not na(valLine)
            line.delete(valLine)

        // ---------------- DRAW VOLUME NODES (histogram anchored at session start) ----------------
        sessionStartBar = bar_index - array.size(barHigh) + 1

        for r = 0 to rows - 1
            rVol = array.get(rowVolume, r)
            if rVol > 0
                nodeTop     = sessLow + (r + 1) * rowHeight
                nodeBottom  = sessLow + r * rowHeight
                nodeLen     = int(math.max(1, (rVol / maxRowVol) * profileWidth))
                isValueArea = r >= valRow and r <= vahRow
                newBox = box.new(left=sessionStartBar, top=nodeTop, right=sessionStartBar + nodeLen, bottom=nodeBottom,
                     border_color=color.new(color.gray, 100), bgcolor=isValueArea ? vaColor : nodeColor)
                array.push(nodeBoxes, newBox)

        // ---------------- DRAW POC / VAH / VAL LINES ----------------
        pocLine := line.new(sessionStartBar, pocPrice, bar_index, pocPrice, color=pocColor, width=2, style=line.style_solid)
        vahLine := line.new(sessionStartBar, vahPrice, bar_index, vahPrice, color=vaColor, width=1, style=line.style_dashed)
        valLine := line.new(sessionStartBar, valPrice, bar_index, valPrice, color=vaColor, width=1, style=line.style_dashed)

// ---------------------------- POC CROSS ALERTS ----------------------------
crossUpPOC   = ta.crossover(close, pocPriceStored)
crossDownPOC = ta.crossunder(close, pocPriceStored)
alertcondition(crossUpPOC, title="Price Crossed Above POC", message="Price crossed above the session Point of Control.")
alertcondition(crossDownPOC, title="Price Crossed Below POC", message="Price crossed below the session Point of Control.")
Risk Notice: This script is provided for educational and research purposes only and does not constitute financial advice. Always backtest on a demo account before any live deployment. See our full Institutional Risk Disclaimer for details.

03 Related Modules

TREND ANALYSIS

Multi-Timeframe Trend

Merges directional data from fractional intervals to macro daily feeds, filtering noise.

View Module
SCALPING MODULE

High-Frequency Scalper

A low-latency script tuned for rapid, algorithmic micro-execution.

View Module
LIQUIDITY TARGETS

Fair Value Gaps (FVG)

Scans multi-timeframe price imbalances and liquidity delivery voids across asset feeds.

View Module

04 Related Reading

Why Price Keeps Reversing Exactly at the POC (and How to Trade the Bounce)

Category: Market Intelligence
Read Guide →

How to Set Up Volume Profile on Crypto When There's No Trading Session

Category: Technical Guides
Read Guide →

Best Time of Day to Trade Crypto for Maximum Volume (With a Volume Map)

Category: Market Intelligence
Read Guide →

What Is a 'Naked POC' and Why Does Price Always Come Back to It?

Category: Technical Guides
Read Guide →