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
- During each session the script stores every bar's high, low and volume in arrays (bounded by "Max bars stored per session").
- The session range, from the lowest low to the highest high, is split into equal price rows.
- 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.
- POC = the row with the largest total.
- 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.
- 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.
- 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
| Input | Default | What it changes |
|---|---|---|
| Session to draw | Current session | Developing or completed profile. |
| Price rows | 30 | Detail of the profile. More rows = finer detail, more boxes. |
| Value area (% of volume) | 70 | Width of the value area. |
| Max bars stored per session | 3000 | Memory limit; older bars of a very long session are dropped. |
| Profile width (bars) | 40 | Visual length of the largest row. |
| Extend POC / VAH / VAL to the right | on | Projects the levels past the last bar. |
| Plot previous-session history | off | Step 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}}")

