ProEA Lab · free & open-source indicator

Volume Profile, explained properly.

VP Lite draws where the market actually traded — not when. One histogram, three levels, two modes, and this page is the complete manual: how to read it, the exact playbooks traders build on these levels, and what it honestly cannot do.

Get the indicator — free ↓
00 · GET IT — FREE & OPEN-SOURCE

Paste it into TradingView — about thirty seconds

  1. On any TradingView chart, open Pine Editor (bottom toolbar). A free account works.
  2. Delete the sample code and paste the full source below.
  3. Press Add to chart. Done — save the script to keep it.
⤓ Or download the .pine file
volume-profile-lite.pine · Pine v6 · open-source
//@version=6
// ─────────────────────────────────────────────────────────────────────────────
// Volume Profile Lite — ProEA Lab
// Free & open-source volume-at-price: the value-area histogram with POC,
// VAH and VAL, in Session or Rolling mode.
//
// Honest by design:
//  · Engine = bar-distribution: each bar's volume is spread across its
//    [low, high] range into price rows — an approximation, not tick data.
//  · The profile redraws as the live bar develops; closed-bar history is
//    read as-is and never rewritten.
//  · Context, not signals — it shows where volume traded, never what price
//    will do.
//
// © ProEA Lab — published open-source on TradingView.
// ─────────────────────────────────────────────────────────────────────────────
indicator("Volume Profile Lite — ProEA Lab", "VP Lite", overlay = true, max_boxes_count = 200, max_lines_count = 50, max_labels_count = 20)

// ───────────────────────── Inputs ─────────────────────────
gP = "Profile"
inMode     = input.string("Rolling", "Mode", options = ["Rolling", "Session"], group = gP, tooltip = "Rolling = the last N bars. Session = the current day's profile.")
inLookback = input.int(200, "Rolling lookback (bars)", minval = 20, maxval = 500, group = gP)
inBins     = input.int(60, "Rows (bins)", minval = 10, maxval = 120, group = gP, tooltip = "Price resolution of the histogram.")
inVApct    = input.float(70, "Value Area %", minval = 50, maxval = 90, group = gP) / 100.0

gS = "Style"
inAnchor   = input.string("Right", "Profile side", options = ["Right", "Left"], group = gS, tooltip = "Right = profile to the right of the last bar. Left = overlays the start of the window.")
inWidth    = input.int(60, "Profile width (bars)", minval = 10, maxval = 150, group = gS)
inShowLvls = input.bool(true, "Show POC / VAH / VAL lines", group = gS)
inShowTags = input.bool(true, "Show price tags", group = gS)

// ───────────────────────── Fixed Aurora palette ─────────────────────────
color cVA  = #5B8DEF       // value-area rows
color cPOC = #FFD24A       // point of control
color cOut = #3A4257       // rows outside the value area
color cTxt = #C9CEDA
color cPnl = #0E1118
color cInk = #0B0E14

// ───────────────────────── History buffer ─────────────────────────
// 2000 bars covers a full 1-minute session (~1440 bars); the Session loop is
// hard-capped to the same depth so lower timeframes degrade gracefully.
max_bars_back(high, 2000)
max_bars_back(low, 2000)
max_bars_back(volume, 2000)

float rHi = ta.highest(high, inLookback)
float rLo = ta.lowest(low, inLookback)

// session tracking
bool newDay = ta.change(time("D")) != 0
var float sHi = high
var float sLo = low
var int   sStart = bar_index
if newDay
    sHi := high
    sLo := low
    sStart := bar_index
else
    sHi := math.max(sHi, high)
    sLo := math.min(sLo, low)

// ───────────────────────── Pools + header chip ─────────────────────────
var array<box>   g_boxes = array.new<box>()
var array<line>  g_lines = array.new<line>()
var array<label> g_lbls  = array.new<label>()
var table hdr = table.new(position.top_right, 2, 1, border_width = 1, border_color = #00000055)

f_clear() =>
    for b in g_boxes
        box.delete(b)
    array.clear(g_boxes)
    for l in g_lines
        line.delete(l)
    array.clear(g_lines)
    for t in g_lbls
        label.delete(t)
    array.clear(g_lbls)

// ───────────────────────── Build + render on the last bar ─────────────────────────
if barstate.islast
    f_clear()
    bool  sess  = inMode == "Session"
    float gHi   = sess ? sHi : rHi
    float gLo   = sess ? sLo : rLo
    int   loopN = sess ? math.min(bar_index - sStart + 1, 2000) : math.min(inLookback, bar_index + 1)
    float rng   = gHi - gLo
    if rng > 0 and loopN > 0 and not na(volume)
        float binSz = rng / inBins
        array<float> vol = array.new_float(inBins, 0.0)

        // Fast engine: distribute each bar's volume across its [low, high] span,
        // overlap-weighted per row (the same binning contract as the full engine).
        for i = 0 to loopN - 1
            float bl = low[i]
            float bh = high[i]
            float bv = nz(volume[i])
            if bv > 0 and bh >= bl
                if bh == bl
                    int idx = math.max(0, math.min(inBins - 1, int(math.floor((bl - gLo) / binSz))))
                    array.set(vol, idx, array.get(vol, idx) + bv)
                else
                    int b0 = math.max(0, math.min(inBins - 1, int(math.floor((bl - gLo) / binSz))))
                    int b1 = math.max(0, math.min(inBins - 1, int(math.floor((bh - gLo) / binSz))))
                    float span = bh - bl
                    for b = b0 to b1
                        float binLo = gLo + b * binSz
                        float ov = math.min(bh, binLo + binSz) - math.max(bl, binLo)
                        if ov > 0
                            array.set(vol, b, array.get(vol, b) + bv * ov / span)

        // totals + POC
        float maxV = 0.0
        int  pocIdx = 0
        float totV = 0.0
        for b = 0 to inBins - 1
            float v = array.get(vol, b)
            totV += v
            if v > maxV
                maxV := v
                pocIdx := b

        // value area: expand row-by-row around the POC toward the richer side
        float acc = array.get(vol, pocIdx)
        int up = pocIdx
        int dn = pocIdx
        float target = totV * inVApct
        while acc < target and (up < inBins - 1 or dn > 0)
            float vUp = up < inBins - 1 ? array.get(vol, up + 1) : -1.0
            float vDn = dn > 0 ? array.get(vol, dn - 1) : -1.0
            if vUp >= vDn
                up += 1
                acc += math.max(vUp, 0.0)
            else
                dn -= 1
                acc += math.max(vDn, 0.0)

        float vah = gLo + (up + 1) * binSz
        float val = gLo + dn * binSz
        float poc = gLo + (pocIdx + 0.5) * binSz

        int gap   = 2
        int xBase = inAnchor == "Left" ? math.max(0, bar_index - loopN - gap) : bar_index + gap
        int xl    = math.max(0, bar_index - loopN)
        int xrr   = bar_index + inWidth + gap

        // histogram — solid rows, value-area highlight, POC in gold
        if maxV > 0
            for b = 0 to inBins - 1
                float v = array.get(vol, b)
                if v > 0
                    int len = int(math.round(inWidth * v / maxV))
                    if len >= 1
                        float binLo = gLo + b * binSz
                        float binHi = binLo + binSz
                        float ratio = v / maxV
                        bool inVA = b >= dn and b <= up
                        color c = b == pocIdx ? cPOC : inVA ? cVA : cOut
                        int  tp = b == pocIdx ? 0 : math.min(90, inVA ? int(14 + (1 - ratio) * 26) : int(48 + (1 - ratio) * 30))
                        array.push(g_boxes, box.new(xBase, binHi, xBase + len, binLo, border_color = na, bgcolor = color.new(c, tp)))

        // POC / VAH / VAL lines
        if inShowLvls
            array.push(g_lines, line.new(xl, poc, xrr, poc, color = cPOC, width = 2))
            array.push(g_lines, line.new(xl, vah, xrr, vah, color = color.new(cVA, 15), style = line.style_dashed))
            array.push(g_lines, line.new(xl, val, xrr, val, color = color.new(cVA, 15), style = line.style_dashed))

        // price tags
        if inShowTags
            int xt = xrr + 1
            array.push(g_lbls, label.new(xt, poc, "POC " + str.tostring(poc, format.mintick), style = label.style_label_left, color = color.new(cPOC, 8), textcolor = cInk, size = size.small))
            array.push(g_lbls, label.new(xt, vah, "VAH " + str.tostring(vah, format.mintick), style = label.style_label_left, color = color.new(cVA, 30), textcolor = #ffffff, size = size.tiny))
            array.push(g_lbls, label.new(xt, val, "VAL " + str.tostring(val, format.mintick), style = label.style_label_left, color = color.new(cVA, 30), textcolor = #ffffff, size = size.tiny))

        // header chip
        table.cell(hdr, 0, 0, "◆ VP Lite", text_color = cPOC, text_size = size.small, text_halign = text.align_left, bgcolor = cPnl)
        table.cell(hdr, 1, 0, "ProEA Lab", text_color = color.new(cTxt, 40), text_size = size.small, text_halign = text.align_right, bgcolor = cPnl)
01 · WHAT IT DRAWS

Volume stacked by price

Every row is a price zone; the row's length is how much volume traded there. Long rows are prices the market kept coming back to. Short rows are prices it moved through fast.

Volume Profile anatomy: histogram rows, gold POC line, blue value area between dashed VAH and VAL lines
  • POC (gold) — the single most-traded price. The market's fairest price, and its strongest magnet.
  • Value Area (blue) — the band holding ~70% of all volume: where the market did most of its business.
  • VAH / VAL (dashed) — the edges of value. These are the decision zones.
  • Two modes — Rolling (the last N bars) for context, Session (today only) for day trading.
02 · THE THREE READS

Start every session with one question

Where is price relative to the value area? Inside value: the market is balanced — expect rotation between the edges, through the POC. Above value: the market is auctioning higher than fair — the read depends entirely on acceptance. Below value: the exact mirror.

Three panels: price inside the value area means rotation, above value asks acceptance or rejection, below value is the mirror read
03 · THE CORE SKILL

Acceptance vs rejection — everything hangs on this

Acceptance: closes hold beyond the edge and new rows build. Rejection: a poke with thin rows and a fast return inside

ACCEPTANCE

Price CLOSES and HOLDS beyond the edge, and new rows start building out there. Higher (or lower) prices are being accepted as the new fair. Context favors continuation — the old edge becomes the retest reference.

REJECTION

A poke beyond the edge that closes straight back inside, leaving thin rows behind. The auction failed out there. Context favors rotation back toward the POC magnet.

04 · READ THE SHAPE

The silhouette tells you what kind of day it is

Four profile shapes: D balanced, P top-heavy, b bottom-heavy, and thin trend day
  • D-shape — balanced. Rotation rules apply.
  • P-shape — value built high after a drive up; often short-covering. The thin stem below is untested.
  • b-shape — the mirror; often long liquidation.
  • Thin profile — a trend day. The market never built balance — mean-reversion reads are wrong on these days. Do not fade a thin profile.
05 · FIVE PLAYBOOKS

What traders actually build on these levels

These are context structures, not signals — the tool never says buy or sell. Your strategy and your risk rules do.

PLAYBOOK 1 · RESPONSIVE

The edge fade

Setup: balanced (D-shape) profile · price pokes beyond VAH or VAL and fails — closes back inside within a bar or two.

Frame: rotation back toward the POC.

Invalidated by: acceptance — closes and row-building beyond the edge. And never fade a thin trend-day profile.

Edge fade playbook: a failed poke above VAH rotating back toward the POC, in three steps
PLAYBOOK 2 · INITIATIVE

The acceptance continuation

Setup: price closes beyond the edge and HOLDS — pullbacks stay outside old value, fresh rows build beyond it.

Frame: the old edge flips into the retest floor (or ceiling); continuation context in the breakout direction.

Invalidated by: a full close back inside old value — a failed breakout, which itself often travels to the opposite edge.

Acceptance continuation playbook: close and hold above VAH, retest holds, continuation
PLAYBOOK 3 · ROTATION

The POC magnet

Setup: inside a balanced profile, price sits at one edge and both edges keep holding.

Frame: the POC is the statistical center of gravity — rotations tend to pass through it. Use it as a target reference and as the "price agrees / disagrees" line for any system you already trade.

PLAYBOOK 4 · SESSION MODE

The opening read

Setup: the first hour builds the day's initial balance.

Frame: acceptance beyond the early range = potential range-extension (trend) day; repeated rejection = rotation day. Decide which day type you are in BEFORE choosing a playbook.

PLAYBOOK 5 · THE MULTIPLIER

Confluence stacking

Setup: mark your own levels first — prior swings, session highs and lows, round numbers, higher-timeframe zones. Then load the profile.

Frame: where a profile level lands on one of yours, that zone deserves your full attention. This is how professionals actually use volume profile: as the confirming layer, not the whole system.

06 · SETTINGS RECIPES

Set it up for your style

STYLECHARTMODEROWS
ScalpingM1–M5Session80–120 — fine resolution
Day tradingM15Session (+ Rolling 200 on H1 for the bigger balance)60
SwingH4 / DRolling 200–50040–60 — coarser rows, cleaner zones
  • Value Area % — 70 is the classic; 60 tightens the edges (more tests, noisier), 80 widens them (fewer, stronger).
  • If the profile looks ragged, lower the row count. Zones beat noise.
  • Volume honesty: stocks, futures and crypto report real traded volume. Spot forex and CFDs report TICK volume (price updates) — the structure is still informative, but know what you are reading.
07 · THE COMPLETE WORKFLOW

Six steps — one minute once practiced

Six-step workflow: higher timeframe first, let the first hour build, wait for an edge test, judge acceptance or rejection, match the playbook, size by your own risk rules

If the session levels line up with your higher-timeframe levels, treat the zone as first-class. If nothing lines up — the best trade is often none.

READ THIS BEFORE YOU TRADE WITH IT

What this tool does not do

  • It does not predict. It maps where volume traded — never what price will do next.
  • No buy/sell signals, no arrows, and it keeps no win-rate — a context tool has none to keep.
  • Rows are bar-distribution estimates, not tick data. Treat every level as a zone.
  • The live bar is still trading, so the newest rows refine until it closes. Closed history never repaints.
  • Trading risk is entirely yours. This is context for your decisions — not financial advice.

CONTEXT, NOT A SIGNAL · IT SHOWS WHERE VOLUME TRADED, NEVER WHAT PRICE WILL DO · FREE & OPEN-SOURCE · RESEARCH TOOLING, NOT FINANCIAL ADVICE

© 2026 ProEA · All rights reservedBuilt for MT5 · Not affiliated with MetaQuotes

Past performance ≠ future. Backtest is broker-specific.