Indicator · Pine Script v6 · On the chart

Volume Profile [ActiveQuants]

by MarcosACH· code credited to ActiveQuants

Plots volume distribution across price levels over a user-defined historical window.

View on GitHub

GitHub stars3on the repository
Pine versionv6current
Lines of code209
Settings18inputs
LicenceNone statedfile: MPL-2.0
Last update1 year ago8 Jul 2025

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

209 lines of Pine Script v6 from volume_profile.pinescript, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

volume_profile.pinescript
// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © ActiveQuants
// Created by ActiveQuants
// Last updated: May 11, 2025

//@version=6
indicator(title = "Volume Profile [ActiveQuants]", overlay = true, max_boxes_count = 500)

//#region ------ USER INPUTS
// Calculation Settings
int lookback = input.int(defval = 500, title = "Lookback", minval = 1, maxval = 10000, tooltip = "Number of most recent bars to calculate the volume profile.", group = "Calculation Settings", display = display.data_window)
int rowQuantity = input.int(defval = 100, title = "Number of Rows", minval = 1, maxval = 500, tooltip = "Number of rows in the volume profile.", group = "Calculation Settings", display = display.data_window)
int profileWidth = input.int(defval = 50, title = "Profile Width", minval = 1, maxval = 100, tooltip = "Relative length of the rows.", group = "Calculation Settings", display = display.data_window)
int horizontalOffset = input.int(defval = 50, title = "Horizontal Offset", minval = -300, maxval = 500, tooltip = "Distance between the current bar and the POC row.", group = "Calculation Settings", display = display.data_window)
int valueAreaThreshold = input.int(defval = 70, title = "Value Area (%)", minval = 0, maxval = 100, group = "Calculation Settings", display = display.data_window)
string displayType = input.string(defval = "Up/Down", title = "Volume", options = ["Up/Down", "Total"], group = "Calculation Settings", display = display.data_window)
string profilePlacement = input.string(defval = "Right", title = "Profile Placement", options = ["Right", "Left"], group = "Calculation Settings", display = display.data_window)

// Appearance
int rowsBorderWidth = input.int(defval = 1, title = "Rows Border Width", minval = 1, maxval = 10, group = "Appearance", inline = "rows", display = display.data_window)
color rowsBorderColor = input.color(defval = color.new(color.black, 50), title = "Color", group = "Appearance", inline = "rows")
bool gradientColors = input.bool(defval = true, title = "Gradient Colors", group = "Appearance")
color vaUpColor = input.color(defval = #26A69A, title = "Value Area Up / Down", group = "Appearance", inline = "va color")
color vaDownColor = input.color(defval = #EF5350, title = "/", group = "Appearance", inline = "va color")
color pUpColor = input.color(defval = color.new(#4a4a4a, 50), title = "Profile Up / Down", group = "Appearance", inline = "p color")
color pDownColor = input.color(defval = color.new(#dbdbdb, 50), title = "/", group = "Appearance", inline = "p color")
color totalColor = input.color(defval = #5088ef, title = "Total Volume Color", group = "Appearance")
bool enablePoc = input.bool(defval = true, title = "POC", group = "Appearance", inline = "poc")
color pocColor = input.color(defval = #ff5d00, title = "Color", group = "Appearance", inline = "poc")
int pocLineWidth = input.int(defval = 2, title = "Width", minval = 1, maxval = 10, group = "Appearance", inline = "poc", display = display.data_window)
//#endregion

//#region ----- VARIABLES
// Two arrays: one for the down portion (left) and one for the up portion (right)
var array<box> upBoxes = array.new_box(rowQuantity)
var array<box> downBoxes = array.new_box(rowQuantity)

var array<float> upVol = array.new_float(rowQuantity)
var array<float> downVol = array.new_float(rowQuantity)
var array<float> sumVol = array.new_float(rowQuantity)

// Array of levels, from lowest to highest (row boundaries)
var array<float> levels = array.new_float(rowQuantity + 1)

var float pocBottom = na
var float pocTop = na
var int pocLeft = na
//#endregion

//#region ----- CALCULATIONS
float highestVal = ta.highest(high, lookback)
float lowestVal = ta.lowest(low, lookback)

if barstate.isfirst
    for i = 0 to rowQuantity - 1
        array.set(upBoxes, i, box.new(na, na, na, na))
        array.set(downBoxes, i, box.new(na, na, na, na))

if barstate.islast
    // Determine the vertical levels
    for i = 0 to rowQuantity
        array.set(levels, i, lowestVal + i / rowQuantity * (highestVal - lowestVal))
    
    // Reset upVol, downVol, and sumv arrays to 0 for each row
    for j = 0 to rowQuantity - 1
        array.set(upVol, j, 0.0)
        array.set(downVol, j, 0.0)
        array.set(sumVol, j, 0.0)
    
    // Accumulate bullish/bearish volume in each row
    for k = 0 to lookback
        float barHigh = high[k]
        float barLow = low[k]
        bool isBull = close[k] >= open[k]
        float barRange = barHigh - barLow
        if barRange > 0
            for j = 0 to rowQuantity - 1
                float lvlBottom = array.get(levels, j)
                float lvlTop = array.get(levels, j + 1)
                float overlap = math.max(0, math.min(barHigh, lvlTop) - math.max(barLow, lvlBottom))
                if overlap > 0
                    float fraction = overlap / barRange
                    float volToAdd = volume[k] * fraction
                    if isBull
                        array.set(upVol, j, array.get(upVol, j) + volToAdd)
                    else
                        array.set(downVol, j, array.get(downVol, j) + volToAdd)
    
    // Calculate total volume per row and overall total volume
    float totalVolume = 0.0
    for j = 0 to rowQuantity - 1
        float rowVol = array.get(upVol, j) + array.get(downVol, j)
        array.set(sumVol, j, rowVol)
        totalVolume += rowVol
    float maxVol = array.max(sumVol)
    
    // Find the POC row (first row with volume equal to maxVol)
    int pocIndex = na
    for j = 0 to rowQuantity - 1
        if array.get(sumVol, j) == maxVol
            pocIndex := j
            break
    
    float pocFraction = 1  // POC volume sum is the maximum volume of all rows, so it represents 100% of the largest row
    int pocRowWidth = math.round(lookback * profileWidth / 100 * pocFraction)
    
    // Determine the Value Area: expand from POC until cumulative volume >= valueAreaThreshold (%) of totalVolume
    int lowerBound = pocIndex
    int upperBound = pocIndex
    float valueAreaVolume = array.get(sumVol, pocIndex)
    float targetVolume = totalVolume * valueAreaThreshold / 100
    
    // Expand while we haven't reached the target and while boundaries allow
    while valueAreaVolume < targetVolume and (lowerBound > 0 or upperBound < rowQuantity - 1)
        bool extendUp = false
        bool extendDown = false
        if lowerBound > 0 and upperBound < rowQuantity - 1
            // Choose the adjacent row with the higher volume
            if array.get(sumVol, lowerBound - 1) >= array.get(sumVol, upperBound + 1)
                extendUp := true
            else
                extendDown := true
        else if lowerBound > 0
            extendUp := true
        else if upperBound < rowQuantity - 1
            extendDown := true
        
        if extendUp
            lowerBound := lowerBound - 1
        if extendDown
            upperBound := upperBound + 1
        
        // Recalculate cumulative volume in the contiguous block
        valueAreaVolume := 0.0
        for j = lowerBound to upperBound
            valueAreaVolume += array.get(sumVol, j)
    
    var array<bool> insideValueArea = array.new_bool(rowQuantity)
    for j = 0 to rowQuantity - 1
        array.set(insideValueArea, j, (j >= lowerBound and j <= upperBound))
    
    for j = 0 to rowQuantity - 1
        float lvlBottom = array.get(levels, j)
        float lvlTop = array.get(levels, j + 1)
        
        float upV = array.get(upVol, j)
        float downV = array.get(downVol, j)
        float totalVol = upV + downV
        
        // Scale the row width by its fraction of the maximum row volume
        float fraction = (maxVol > 0) ? (totalVol / maxVol) : 0.0
        int rowWidth = math.round(lookback * profileWidth / 100 * fraction)
        
        float upFrac = (totalVol > 0) ? (upV / totalVol) : 0.0
        float downFrac = (totalVol > 0) ? (downV / totalVol) : 0.0
        int upBoxWidth = math.round(rowWidth * upFrac)
        int dnBoxWidth = math.round(rowWidth * downFrac)
        
        int boxRight = profilePlacement == "Right" ? bar_index + horizontalOffset + pocRowWidth : bar_index - horizontalOffset - pocRowWidth
        int boxLeft = profilePlacement == "Right" ? boxRight - rowWidth : boxRight + rowWidth
        
        // Down box coordinates (left portion)
        int dnBoxLeft = boxLeft
        int dnBoxRight = profilePlacement == "Right" ? dnBoxLeft + dnBoxWidth : dnBoxLeft - dnBoxWidth

        // Up box coordinates (immediately to the right of down box)
        int upBoxLeft = dnBoxRight
        int upBoxRight = profilePlacement == "Right" ? upBoxLeft + upBoxWidth : upBoxLeft - upBoxWidth
        
        bool isInside = array.get(insideValueArea, j)
        
        color upBoxColor = isInside ? vaUpColor : pUpColor
        color dnBoxColor = isInside ? vaDownColor : pDownColor
        color totalBoxColor = isInside ? totalColor : color.new(totalColor, 60)

        if gradientColors
            upBoxColor := isInside ? color.from_gradient(totalVol / array.max(sumVol), 0, 1, color.new(vaUpColor, 100), color.new(vaUpColor, 0)) : color.from_gradient(totalVol / array.max(sumVol), 0, 1, color.new(pUpColor, 100), color.new(pUpColor, 0))
            dnBoxColor := isInside ? color.from_gradient(totalVol / array.max(sumVol), 0, 1, color.new(vaDownColor, 100), color.new(vaDownColor, 0)) : color.from_gradient(totalVol / array.max(sumVol), 0, 1, color.new(pDownColor, 100), color.new(pDownColor, 0))
            totalBoxColor := isInside ? color.from_gradient(totalVol / array.max(sumVol), 0, 1, color.new(totalColor, 100), color.new(totalColor, 0)) : color.from_gradient(totalVol / array.max(sumVol), 0, 1, color.new(color.new(totalColor, 80), 100), color.new(color.new(totalColor, 80), 0))

        box dnBox = array.get(downBoxes, j)
        box.set_left(dnBox, dnBoxLeft)
        box.set_right(dnBox, displayType == "Up/Down" ? dnBoxRight : upBoxRight)
        box.set_bottom(dnBox, lvlBottom)
        box.set_top(dnBox, lvlTop)
        box.set_border_width(dnBox, rowsBorderWidth)
        box.set_border_color(dnBox, rowsBorderColor)
        box.set_bgcolor(dnBox, displayType == "Up/Down" ? dnBoxColor : totalBoxColor)
        
        if displayType == "Up/Down"
            box upBox = array.get(upBoxes, j)
            box.set_left(upBox, upBoxLeft)
            box.set_right(upBox, upBoxRight)
            box.set_bottom(upBox, lvlBottom)
            box.set_top(upBox, lvlTop)
            box.set_border_width(upBox, rowsBorderWidth)
            box.set_border_color(upBox, rowsBorderColor)
            box.set_bgcolor(upBox, upBoxColor)
        
        // Check if this row is the POC (max volume)
        if totalVol == maxVol
            pocBottom := lvlBottom
            pocTop := lvlTop
            pocLeft := boxLeft
            
    if not na(pocBottom) and not na(pocTop) and not na(pocLeft) and enablePoc
        float pocPrice = (pocBottom + pocTop) / 2
        line pocLine = line.new(x1 = profilePlacement == "Right" ? pocLeft - 1 : pocLeft, y1 = pocPrice, x2 = profilePlacement == "Right" ? pocLeft : pocLeft + 1, y2 = pocPrice, extend = profilePlacement == "Right" ? extend.left : extend.right, color = pocColor, width = pocLineWidth)
        line.delete(pocLine[1])

Overview

Divides price range into equal-height rows and aggregates volume in each. Highlights the Point of Control (highest volume row) with a bold line and the Value Area (containing 70% of volume) above and below the POC with green/red colors. Rows use gradient coloring based on volume strength. Supports total or up/down volume display and left/right placement. Helps identify support/resistance and market sentiment on the price chart.

Details

Published by

MarcosACH on GitHub

Code credit

ActiveQuants, as written in the script's header

Draws

On the price chart

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

pinescripttrading-algorithmstrading-indicatorstradingvewpinescript-indicatorstechnical-analysistradingviewtechnical-analysis-indicatorspinescript-algorithms

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Lookbackint500lookback
Number of Rowsint100rowQuantity
Profile Widthint50profileWidth
Horizontal Offsetint50horizontalOffset
Value Area (%)int70valueAreaThreshold
VolumestringUp/DowndisplayType
Profile PlacementstringRightprofilePlacement
Rows Border Widthint1rowsBorderWidth
Colorcolorcolor.new(color.black, 50)rowsBorderColor
Gradient ColorsbooltruegradientColors
Value Area Up / Downcolor#26A69AvaUpColor
/color#EF5350vaDownColor
Show all 18 settings
SettingTypeDefaultVariable
Profile Up / Downcolorcolor.new(#4a4a4a, 50)pUpColor
/colorcolor.new(#dbdbdb, 50)pDownColor
Total Volume Colorcolor#5088eftotalColor
POCbooltrueenablePoc
Colorcolor#ff5d00pocColor
Widthint2pocLineWidth
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.

Similar Pine Scripts

This overlay indicator draws a fixed-range volume profile on the price chart, splitting traded activity into up and down volume at each price level. It also plots a sentiment profile, a developing point of control, and value-area high and low around a configur

Indicator v5 VolumeSupport & resistance
Updated 2 months ago · MIT GitHub repository of Volume Profile and Indicator by DGT

MGIC-tracker

by Allen1212
303 stars

The MGIC-tracker is a TradingView indicator that overlays ARK Invest trade records onto stock price charts. It plots buy and sell labels at the exact timestamp of each transaction, showing share volume and ETF weight. Users select a specific ETF type via setti

Indicator v4 Support & resistanceVolume
Updated 2 years ago · None stated GitHub repository of MGIC-tracker
228 stars

Comprehensive Volume Profile is an overlay indicator that calculates and plots volume distribution across specified time ranges such as daily sessions, weekly, monthly, or custom bar lengths. It identifies key structural price levels including the Point of Con

Indicator v6 VolumeSupport & resistance
Updated 3 months ago · None stated GitHub repository of Comprehensive Volume Profile

Moon Dev GEX Arrows

by moondevonyt
92 stars

This overlay indicator plots option strike walls and an optional session VWAP on the price chart for SPY or ES. It spaces major strikes by the selected instrument, draws a set number of strikes on each side, and marks wall-trap and sweep events after a cooldow

Indicator v5 Support & resistanceSessions & time
Updated 2 months ago · Custom licence GitHub repository of Moon Dev GEX Arrows

More Volume scripts

Related on ForexR

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers