// =============================================================================
// Volume Profile Plus
// =============================================================================
// Author: Baris Tankut (@btankutt)
// Repository: https://github.com/btankutt/smc-pine-suite
// License: MPL 2.0 (see LICENSE-FREE)
// Created: 2026-06-17
// Version: 1.0.3
// - FIX: the file was truncated mid-statement in the ALERTS section, so the
// POC-cross alert promised by the README never compiled. Implemented as
// a relocation-proof cross (both closes tested against the CURRENT POC,
// so a POC jump across a static price cannot fire it), gated on
// barstate.isconfirmed and suppressed on the session-rollover bar. The
// alert is live-only by design - the profile exists only on the latest
// bar.
// - FIX: declared max_bars_back=5000 and clamped the profile span to 4999
// bars - the profile indexes high/low/volume with a dynamic offset (up
// to the 5000-bar lookback input, or a session far longer than that on a
// small chart TF), which otherwise aborts with "offset beyond historical
// buffer" runtime errors. Sessions longer than 5000 bars profile the
// most recent 5000.
// - FIX: Fixed-lookback mode profiled lookback+1 bars (off-by-one), and a
// session's first bar pulled in the previous session's last bar.
// -----------------------------------------------------------------------------
// Session-based (or fixed-lookback) Volume Profile for TradingView:
// - Horizontal volume histogram across price bins
// - Point of Control (POC) - the highest-volume price level
// - Value Area High / Low (VAH / VAL) - the configurable % of volume
// - High / Low Volume Node (HVN / LVN) highlighting
//
// The profile is recomputed on the most recent bar. All drawings are anchored
// to real bars (never to future bar offsets) and the level lines use
// extend.right, so nothing drifts when you scroll or zoom — the same approach
// used by the SMC Toolkit. The histogram is drawn over the most recent bars,
// growing left from the last bar.
//
// Disclaimer: Educational tool, not financial advice. Past performance does
// not guarantee future results. Always backtest and use proper risk management.
// =============================================================================
//@version=6
indicator("Volume Profile Plus [btankutt]", overlay=true, max_boxes_count=500, max_lines_count=500, max_labels_count=500, max_bars_back=5000)
// =============================================================================
// CONSTANTS
// =============================================================================
float MIN_BAR_WIDTH = 1.0 // minimum histogram bar width (in bars)
// Input group names
GRP_CALC = "Calculation"
GRP_NODE = "Volume Nodes"
GRP_DISP = "Display"
// =============================================================================
// USER INPUTS
// =============================================================================
// Group 1 - Calculation
string calc_mode = input.string("Session", "Profile range", options=["Session", "Fixed lookback"], group=GRP_CALC)
string anchor_tf = input.timeframe("D", "Session anchor timeframe", group=GRP_CALC, tooltip="Higher timeframe that defines one session, e.g. D (daily), W (weekly).")
int lookback = input.int(200, "Fixed lookback (bars)", minval=20, maxval=5000, group=GRP_CALC)
int num_rows = input.int(50, "Number of rows", minval=10, maxval=200, group=GRP_CALC, tooltip="More rows = finer price resolution, slower compute.")
float va_pct = input.float(70.0, "Value Area (%)", minval=50.0, maxval=95.0, step=1.0, group=GRP_CALC)
// Group 2 - Volume Nodes
bool node_enabled = input.bool(true, "Highlight HVN / LVN", group=GRP_NODE)
float hvn_pct = input.float(80.0, "HVN threshold (% of POC volume)", minval=50.0, maxval=100.0, step=5.0, group=GRP_NODE)
float lvn_pct = input.float(20.0, "LVN threshold (% of POC volume)", minval=1.0, maxval=50.0, step=5.0, group=GRP_NODE)
// Group 3 - Display
int prof_width = input.int(60, "Profile width (bars)", minval=10, maxval=300, group=GRP_DISP, tooltip="How many bars wide the histogram is, drawn left from the last bar.")
bool show_poc = input.bool(true, "Show POC line", group=GRP_DISP)
bool show_va = input.bool(true, "Show Value Area (VAH/VAL)", group=GRP_DISP)
bool show_va_fill = input.bool(true, "Shade Value Area", group=GRP_DISP)
bool show_labels = input.bool(true, "Show price labels", group=GRP_DISP)
color body_color = input.color(color.new(color.blue, 60), "Profile color", group=GRP_DISP)
color va_color = input.color(color.new(color.blue, 40), "Value Area color", group=GRP_DISP)
color hvn_color = input.color(color.new(color.green, 30), "HVN color", group=GRP_DISP)
color lvn_color = input.color(color.new(color.red, 50), "LVN color", group=GRP_DISP)
color poc_color = input.color(color.new(color.orange, 0), "POC color", group=GRP_DISP)
// =============================================================================
// STATE
// =============================================================================
// Visual handles, cleared and redrawn each time the profile is recomputed.
var array<box> hist_boxes = array.new<box>()
var array<line> level_lines = array.new<line>()
var array<label> level_labels = array.new<label>()
var box va_box = na
// POC of the most recent recompute, persisted for the alert at the bottom.
var float alert_poc = na
// Session anchoring. session_start_index tracks the bar where the current
// anchor period began, so the profile spans exactly one session.
int anchor_time = time(anchor_tf)
bool new_period = na(anchor_time[1]) or anchor_time != anchor_time[1]
var int session_start_index = bar_index
if new_period
session_start_index := bar_index
// =============================================================================
// MAIN — recompute the profile on the latest bar only
// =============================================================================
// Computing on barstate.islast keeps the loop cost bounded. The clearing logic
// is inlined here (not in a function) because Pine v6 forbids reassigning a
// global variable such as `va_box` from inside a user-defined function.
if barstate.islast
// --- Clear the previous recompute's drawings ---
if hist_boxes.size() > 0
for b in hist_boxes
b.delete()
hist_boxes.clear()
if level_lines.size() > 0
for l in level_lines
l.delete()
level_lines.clear()
if level_labels.size() > 0
for lb in level_labels
lb.delete()
level_labels.clear()
if not na(va_box)
va_box.delete()
va_box := na
// How many bars back does the profile reach? Session: bars since the
// session start (inclusive). Fixed: exactly `lookback` bars - the loops
// below run 0..span_bars inclusive, hence the -1. The span is clamped to
// 4999: max_bars_back=5000 is Pine's hard buffer cap, and a session can
// exceed it (e.g. a weekly anchor on a 1m 24/7 chart is ~10k bars) - the
// profile then covers the most recent 5000 bars instead of aborting with
// an "offset beyond historical buffer" runtime error.
int span_bars = calc_mode == "Session" ? (bar_index - session_start_index) : math.min(lookback - 1, bar_index)
span_bars := math.max(math.min(span_bars, 4999), 0)
// Pass 1 — establish the price range over the window.
float hi = high
float lo = low
for i = 0 to span_bars
hi := math.max(hi, high[i])
lo := math.min(lo, low[i])
float price_range = hi - lo
if price_range > 0
float row_h = price_range / num_rows
array<float> bins = array.new<float>(num_rows, 0.0)
// Pass 2 — distribute each bar's volume across the rows its range spans.
for i = 0 to span_bars
int lo_row = int(math.floor((low[i] - lo) / row_h))
int hi_row = int(math.floor((high[i] - lo) / row_h))
lo_row := math.max(0, math.min(num_rows - 1, lo_row))
hi_row := math.max(0, math.min(num_rows - 1, hi_row))
int touched = hi_row - lo_row + 1
float v_share = volume[i] / touched
for r = lo_row to hi_row
bins.set(r, bins.get(r) + v_share)
// Locate the Point of Control (row with the most volume) and total.
int poc_row = 0
float poc_vol = 0.0
float total_vol = 0.0
for r = 0 to num_rows - 1
float v = bins.get(r)
total_vol += v
if v > poc_vol
poc_vol := v
poc_row := r
// Expand outward from the POC until the Value Area % of volume is
// captured, always absorbing the heavier of the two neighbouring rows.
float va_target = total_vol * va_pct / 100.0
float va_vol = bins.get(poc_row)
int up_row = poc_row
int dn_row = poc_row
while va_vol < va_target and (up_row < num_rows - 1 or dn_row > 0)
float up_vol = up_row < num_rows - 1 ? bins.get(up_row + 1) : -1.0
float dn_vol = dn_row > 0 ? bins.get(dn_row - 1) : -1.0
if up_vol >= dn_vol
up_row += 1
va_vol += up_vol
else
dn_row -= 1
va_vol += dn_vol
float poc_price = lo + (poc_row + 0.5) * row_h
float vah_price = lo + (up_row + 1) * row_h
float val_price = lo + dn_row * row_h
// Persist the POC for the alert (independent of line visibility).
alert_poc := poc_price
// X anchors — REAL bars only (no future offsets). right_x is the last
// bar; left_x is the window start. Lines use extend.right to become
// stable horizontal rays; the histogram grows left from the last bar.
int right_x = bar_index
int left_x = bar_index - span_bars
// --- Histogram bars (over the most recent bars, growing left) -------
for r = 0 to num_rows - 1
float v = bins.get(r)
if v > 0
float w_f = prof_width * v / poc_vol
int w = math.max(int(math.round(w_f)), int(MIN_BAR_WIDTH))
float y_bottom = lo + r * row_h
float y_top = y_bottom + row_h
bool in_va = r >= dn_row and r <= up_row
color c = body_color
if node_enabled and v >= poc_vol * hvn_pct / 100.0
c := hvn_color
else if node_enabled and v <= poc_vol * lvn_pct / 100.0
c := lvn_color
else if in_va
c := va_color
box bx = box.new(left=right_x - w, top=y_top, right=right_x, bottom=y_bottom,
border_color=color.new(c, 100), bgcolor=c)
hist_boxes.push(bx)
// --- Value Area shading (over the analyzed range) ------------------
if show_va_fill
va_box := box.new(left=left_x, top=vah_price, right=right_x, bottom=val_price,
border_color=color.new(va_color, 100),
bgcolor=color.new(va_color, 92))
// --- POC / VAH / VAL as horizontal rays (extend.right = stable) ----
if show_poc
line poc_line = line.new(left_x, poc_price, right_x, poc_price,
extend=extend.right, color=poc_color, width=2, style=line.style_solid)
level_lines.push(poc_line)
if show_labels
label poc_lbl = label.new(right_x, poc_price,
"POC " + str.tostring(poc_price, format.mintick),
style=label.style_label_left, color=poc_color,
textcolor=color.white, size=size.small)
level_labels.push(poc_lbl)
if show_va
line vah_line = line.new(left_x, vah_price, right_x, vah_price,
extend=extend.right, color=va_color, width=1, style=line.style_dashed)
line val_line = line.new(left_x, val_price, right_x, val_price,
extend=extend.right, color=va_color, width=1, style=line.style_dashed)
level_lines.push(vah_line)
level_lines.push(val_line)
if show_labels
label vah_lbl = label.new(right_x, vah_price,
"VAH " + str.tostring(vah_price, format.mintick),
style=label.style_label_left, color=color.new(va_color, 0),
textcolor=color.white, size=size.small)
label val_lbl = label.new(right_x, val_price,
"VAL " + str.tostring(val_price, format.mintick),
style=label.style_label_left, color=color.new(va_color, 0),
textcolor=color.white, size=size.small)
level_labels.push(vah_lbl)
level_labels.push(val_lbl)
// =============================================================================
// ALERTS
// =============================================================================
// Fire when price crosses the current profile's POC. This alert is LIVE-ONLY
// by design: the profile exists only on the latest bar, so alert_poc is na
// across history and a refreshed chart shows no past crosses. A genuine
// cross means the PRICE PATH traversed the current level between the last
// two closes - ta.cross(close, alert_poc) would also fire when a recompute
// RELOCATES the POC across a static close (one heavy-volume bar can move the
// POC to the other side of price), so both closes are tested against the
// CURRENT level instead. The flag is additionally gated on
// barstate.isconfirmed (final tick only) and suppressed on an anchor-period
// rollover, where the profile resets into the new session's range.
bool poc_crossed = not na(alert_poc) and (close - alert_poc) * (close[1] - alert_poc) < 0
alertcondition(poc_crossed and not new_period and barstate.isconfirmed, "POC cross", "Price crossed the Volume Profile POC")
Overview
Volume Profile Plus identifies the Point of Control (POC) as the highest-volume price level, computes the Value Area (VAH/VAL) using a configurable percentage of volume, and highlights High Volume Nodes (HVN) and Low Volume Nodes (LVN). The profile recomputes on the latest bar using the selected calculation mode and session anchor timeframe. Main inputs adjust profile range, number of rows, value area percentage, node thresholds, and display options for lines, fills, and labels. It draws purely as a visual analysis tool on the chart with no trade signals or position sizing.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Profile range
string
Session
calc_mode
Session anchor timeframe
timeframe
D
anchor_tf
Fixed lookback (bars)
int
200
lookback
Number of rows
int
50
num_rows
Value Area (%)
float
70.0
va_pct
Highlight HVN / LVN
bool
true
node_enabled
HVN threshold (% of POC volume)
float
80.0
hvn_pct
LVN threshold (% of POC volume)
float
20.0
lvn_pct
Profile width (bars)
int
60
prof_width
Show POC line
bool
true
show_poc
Show Value Area (VAH/VAL)
bool
true
show_va
Shade Value Area
bool
true
show_va_fill
Show all 18 settings
Setting
Type
Default
Variable
Show price labels
bool
true
show_labels
Profile color
color
color.new(color.blue, 60)
body_color
Value Area color
color
color.new(color.blue, 40)
va_color
HVN color
color
color.new(color.green, 30)
hvn_color
LVN color
color
color.new(color.red, 50)
lvn_color
POC color
color
color.new(color.orange, 0)
poc_color
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.
The indicator detects liquidity sweeps across key price levels and calculates a cumulative volume delta proxy. It uses pivot high and low lookbacks to identify swing points, marking sweeps when price breaks beyond a level and quickly closes back inside. Featur
Calculates higher-timeframe volume profiles to identify Value Area High (VAH), Value Area Low (VAL), and Point of Control (POC) levels directly on the price chart. It flags potential reversal setups and Change in State of Delivery (CISD) patterns relative to v
Copilot is an order flow range-reversal indicator designed to overlay trading setups directly on the price chart. It evaluates four criteria: market context (sideways vs trending regime based on ATR height), structure (double top or double bottom retests), abs
Supply and Demand Visible Range identifies supply, demand, and equilibrium zones across the visible chart range. It accumulates high, low, and volume data starting from the leftmost visible bar, optionally requesting lower timeframe intrabar data for finer res
This script identifies and visualizes bullish and bearish order blocks on the price chart. It detects low-volatility consolidation ranges followed by breakout moves accompanied by high trading volume. Users can select between Simple, Relative, or Weighted volu
This indicator identifies and visualizes key support and resistance heatmap zones directly on the price chart. Using closed one-hour price data, it evaluates multi-scale pivots, liquidity clusters, relative volume, and round-number resonances to score structur