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
-
Real-Time POC Calculation Recalculates the Point of Control on every bar as new volume accumulates, always reflecting the single most-traded price level in the current session — not a stale, session-end snapshot.
-
Dynamic Value Area Mapping (VAH/VAL) Expands the Value Area outward from the POC bar-by-bar, continuously plotting the exact boundary that contains your specified percentage of total session volume (70% by default).
-
High-Density Node Visualization Renders each row of the profile as a proportionally-sized volume node, so high-density (accepted) and low-density (rejected) price zones are visually distinct at a glance.
-
Session-Anchored, Non-Repainting Structure The profile resets cleanly at the start of each defined session, and values calculated for bars that have already closed never repaint.
How to Use
- Copy the script into TradingView's Pine Editor and click Add to Chart.
- 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.
- 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).
- 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.
The Pine Script V5 Code
//@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.")