Indicator · Pine Script v5 · On the chart

Volume Profile Plus

by webcrack4· code credited to algotraderdev

This indicator calculates and plots volume profile histograms, market profile distributions, and key value area levels directly on the main price chart.

View on GitHub

GitHub stars1on the repository
Pine versionv5runs as is
Lines of code495
Settings20inputs
LicenceNone statedfile: MPL-2.0
Last update9 months ago10 Jan 2026

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

495 lines of Pine Script v5 from 34VPplus.pine, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

34VPplus.pine
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © algotraderdev

//@version=5
indicator('Volume Profile Plus', 'VP+', overlay = true, max_bars_back = 1)

import algotraderdev/contrast/1

//#region Inputs & Constants

const string VOLUME_PROFILE = 'Volume Profile'
const string MARKET_PROFILE = 'Market Profile / TPO'
string MODE = input.string(
  VOLUME_PROFILE,
  'Mode',
  options = [VOLUME_PROFILE, MARKET_PROFILE],
  tooltip = 'This setting determines the mode in which the indicator operates.\n\n' +
      'Volume Profile (Default Mode): In this mode, the volume of each bar is taken into consideration to construct ' +
      'the profile.\n\n' + 
      'Market Profile (a.k.a. Time Price Opportunity): In this mode, volume data is disregarded. This indicator ' +
      'analyzes the price distribution solely over time.\n\n' +
      'NOTE: in case the ticker does not have any volume data, this indicator uses the `Market Profile` mode.')
const string RANGE_GROUP = 'TIME RANGE'
const string VISIBLE_RANGE = 'Visible Range'
const string ANCHORED_RANGE = 'Anchored Range'
const string ALL_RANGE = 'All Range'
string RANGE_MODE = input.string(
  VISIBLE_RANGE,
  'Range Mode',
  options = [VISIBLE_RANGE, ANCHORED_RANGE, ALL_RANGE],
  tooltip = 'This setting determines the start time of the volume profile calculation.\n\n' +
      'Visible Range (Default Mode): In this mode, the volume profile calculation begins at the time of the ' +
      'left-most bar displayed in the current viewport. As the user scrolls through the viewport, the volume profile ' +
      'updates automatically.\n\n' +
      'Anchored Range: This mode allows the user to set the start time either by using the "Anchor Time" input box ' +
      'below or by dragging the anchor line on the chart.\n\n' +
      'All Range: In this mode, the volume profile calculation is based on all the historical bars available in the ' +
      'chart.',
  group = RANGE_GROUP)
int ANCHORED_TIME = input.time(
  timestamp('2023-01-01T12:00:00'),
  'Anchor Time',
  tooltip = 'The start time of the volume profile calculation. This setting is only used when the "Range Mode" above ' +
      'is set to "Anchored Range".',
  group = RANGE_GROUP)

const string GRANULARITY_GROUP = 'GRANULARITY'
int NUM_ROWS = input.int(
  300,
  'Number of Rows',
  minval = 100,
  maxval = 2500,
  step = 100,
  tooltip = 'The number of rows to display in the volume profile histogram (Max 2500)',
  group = GRANULARITY_GROUP,
  display = display.none)
int MAX_LTF_BARS = input.int(
  5000,
  'Max Lower Timeframe Bars',
  minval = 1000,
  step = 1000,
  tooltip = 'The max number of lower timeframe bars to use for the volume profile calculation.\n\n' +
      'NOTE: The higher the number, the more time and memory the calculation needs. If you encounter a time limit ' +
      'or memory limit error, please try lowering the number here.',
  group = GRANULARITY_GROUP)
bool USE_SECOND_BASED_TIMEFRAMES = input.bool(
  false,
  'Use Second-Based Timeframes',
  tooltip = 'Whether to use second-based timeframes (e.g. 1 second, 5 seconds) when the chart\'s current timeframe ' +
      'is low (e.g. 5 minutes).\n\n' +
      'NOTE: second-based timeframes are only supported if you\'re subscribed to the TradingView Premium Plan and ' +
      'above.',
  group = GRANULARITY_GROUP)

const string APPEARANCE_GROUP = 'APPEARANCE'
int HORIZONTAL_OFFSET = input.int(
  20,
  'Horizontal Offset',
  step = 5,
  tooltip = 'The distance, measured in the number of bars, between the last bar in the chart and the left of the ' +
      'volume profile histogram.',
  group = APPEARANCE_GROUP,
  display = display.none)
int MAX_WIDTH = input.int(
  80,
  'Max Width',
  maxval = 500,
  minval = 10,
  step = 10,
  tooltip = 'The maximum width, measured in number of bars, for the volume profile histogram.',
  group = APPEARANCE_GROUP,
  display = display.none)
int HEIGHT_PERCENTAGE_PER_ROW = input.int(
  40,
  'Height % per Row',
  minval = 1,
  maxval = 100,
  tooltip = 'The height of each volume profile histogram row, measured as the percentage of the price range ' +
      'of each price bucket.',
  group = APPEARANCE_GROUP,
  display = display.none)
bool SHOW_DELTA = input.bool(
  false,
  'Show Volume Delta',
  tooltip = 'Whether to show Volume Delta (the net difference between buying and selling volume).',
  group = APPEARANCE_GROUP,
  display = display.none)

const string COLOR_GROUP = 'COLOR'
color COLOR = input.color(#5d606b77, 'Default', group = COLOR_GROUP)
color BUY_COLOR = input.color(#00bcd480, 'Buy Volume', group = COLOR_GROUP)
color SELL_COLOR = input.color(#e91e6380, 'Sell Volume', group = COLOR_GROUP)
color POC_COLOR = input.color(color.yellow, 'POC (Point of Control)', group = COLOR_GROUP)
color VAH_COLOR = input.color(color.blue, 'VAH (Value Area High)', group = COLOR_GROUP)
color VAL_COLOR = input.color(color.blue, 'VAL (Value Area Low)', group = COLOR_GROUP)

const string KEY_PRICES_GROUP = 'POC & Value Area'
int VALUE_AREA_PERCENT = input.int(
  70,
  'Value Area Percent',
  minval = 1,
  maxval = 100,
  tooltip = 'The percentage of the total trading volume that\'s considered as the value area.',
  group = KEY_PRICES_GROUP,
  display = display.none)
bool DISPLAY_VA = input.bool(
  true,
  'Display VAH/VAL',
  group = KEY_PRICES_GROUP,
  display = display.none)
bool DISPLAY_POC = input.bool(
  true,
  'Display POC',
  group = KEY_PRICES_GROUP,
  display = display.none)
bool EXTEND_LEFT = input.bool(
  true,
  'Extend Key Price Lines to the Left',
  tooltip = 'Whether to extend the key price lines (POC, VAH, VAL) to the left of the chart.',
  group = KEY_PRICES_GROUP)

int BASE_OFFSET = HORIZONTAL_OFFSET + MAX_WIDTH
int LABEL_OFFSET = BASE_OFFSET + 2

int START_TIME = switch RANGE_MODE
    VISIBLE_RANGE => chart.left_visible_bar_time
    ANCHORED_RANGE => ANCHORED_TIME
    ALL_RANGE => 0

// If second-based timeframe is enabled, then set the minimal timeframe to 1 second.
// Otherwise, set it to 60 seconds (1 minute).
int MIN_TIMEFRAME_IN_SECONDS = USE_SECOND_BASED_TIMEFRAMES ? 1 : 60

//#endregion

//#region Utils

// @function Identifies the index of the max value in the array.
// @param a The array to find max index for.
// @returns The index of the max value in the array.
maxIndex(float[] a) =>
    if a.size() == 0
        na
    float max = a.first()
    int maxIndex = 0
    for [i, v] in a
        if v > max
            max := v
            maxIndex := i
    maxIndex

//#endregion

//#region Candle

const int DIRECTION_NONE = 0
const int DIRECTION_BUY = 1
const int DIRECTION_SELL = -1

// @type The candle object holds the required information for each candle in the chart.
// @field high The `high` of the candle.
// @field low The `low` of the candle.
// @field volume The `volume` of the candle.
// @field direction Any of DIRECTION_NONE, DIRECTION_BUY, or DIRECTION_BUY.
type Candle
    float high
    float low
    float volume = 1
    int direction

//#endregion

//#region Marker

// @type The Marker type is for highlighting a specific price point in the volume profile.
// @field poly The polyline for highlighting the price point. Note that we're using a `polyline` instead of `line`
// here since there seems to be bug in TradingView that prevents a `line` to be overlaid on top of a `polyline`.
// @field extendedLine A line at the price level that extends to the left of the screen.
// @field label Label for this marker.
type Marker
    polyline poly
    line extendedLine
    label label

// @function Sets the properties of the marker.
// @param name The name of the marker. This is also displayed as part of the label text.
// @param left The bar index of the left x coordinate of the volume profile row.
// @param right The bar index of the right x coordinate of the volume profile row.
// @param price The price / y-coordinate of the marker.
// @param bg The background color of the marker.
// @param height The height of the volume profile row.
method set(Marker this, string name, int left, int right, float price, color bg, float height) =>
    // Create a polyline to highlight the row.
    if not na(this.poly)
        this.poly.delete()
    float bottom = price - height / 2
    float top = bottom + height
    chart.point[] points = array.from(
      chart.point.from_index(left, bottom),
      chart.point.from_index(left, top),
      chart.point.from_index(right, top),
      chart.point.from_index(right, bottom))
    this.poly := polyline.new(points, line_color = bg, fill_color = bg)

    // Create a dotted line and extend it to the left.
    if not na(this.extendedLine)
        this.extendedLine.delete()
    if EXTEND_LEFT
        this.extendedLine := line.new(
          x1 = left,
          y1 = price,
          x2 = right,
          y2 = price,
          extend = extend.left,
          style = line.style_dotted,
          color = bg)

    // Create a label to the right of the row.
    if na(this.label)
        this.label.delete()
    string txt = str.format('{0}: {1}', name, math.round_to_mintick(price))
    this.label := label.new(
      x = bar_index + LABEL_OFFSET,
      y = price,
      text = txt,
      style = label.style_label_left,
      size = size.small,
      color = bg,
      textcolor = bg.contrast(0.6))

    this

//#endregion

//#region VP

// @type The VP (Volume Profile) type is responsible for calculating and visualizing the distribution of
// volumes at various price points.
// @field candles The stored candles based on which the distribution will be calculated.
// @field minPrice The minimum price for all the stored candles.
// @field maxPrice The maximum price for all the stored candles.
// @field step The price difference between adjacent price buckets.
// @field poly The polyline that's used to draw the histogram of the volume profile.
// @field buyPoly The polyline that's used to draw the histogram of the buying volume.
// @field sellPoly The polyline that's used to draw the histogram of the selling volume.
// @field markers Markers that highlight POC, VAH, and VAL.
type VP
    Candle[] candles
    float minPrice
    float maxPrice
    float step
    polyline poly
    polyline buyPoly
    polyline sellPoly
    map<string, Marker> markers

// @function Initializes a new VP instance.
// @returns The initialized VP instance.
method init(VP this) =>
    this.candles := array.new<Candle>()
    this.markers := map.new<string, Marker>()
    this

// @function Gets the bucket index for the given price.
// @param price The price to get index for.
// @returns The bucket index for the price.
method getBucketIndex(VP this, float price) =>
    math.min(math.floor((price - this.minPrice) / this.step), NUM_ROWS - 1)

// @function Gets the bucketed price for the given index
// @param index The bucket index.
// @returns The average price for the bucket.
method getBucketedPrice(VP this, int index) =>
    (index + 0.5) * this.step + this.minPrice

// @function Highlights a specific price / volume bucket.
// @param name The name of the label.
// @param index The index of the bucket to highlight.
// @param left The x coordinate of the left endpoint of the volume profile.
// @param bg The background highlight color.
// @param height The height of each row.
method mark(VP this, string name, float price, int left, color bg, float height) =>
    int right = bar_index + BASE_OFFSET
    Marker marker = this.markers.get(name)
    if na(marker)
        marker := Marker.new().set(name = name, left = left, right = right, price = price, bg = bg, height = height)

// @function Stores a candle for it to be analyzed later.
// Note that this method is expected to be called on every tick. The actual calculation of VP is deferred to only when
// the last bar in the chart is reached, as an optimization.
// @param candle The candle to store.
method store(VP this, Candle candle) =>
    this.candles.push(candle)
    this.minPrice := na(this.minPrice) ? candle.low : math.min(this.minPrice, candle.low)
    this.maxPrice := na(this.maxPrice) ? candle.high : math.max(this.maxPrice, candle.high)
    this.step := (this.maxPrice - this.minPrice) / NUM_ROWS

// @function Draws a histogram for the given coordinates.
// @param xs The x-coordinates for the left of the histogram rows.
// @param baseX The x-coordinate for the base of the histogram.
// @param step The step / delta between adjacent price points.
// @param height The height for each row.
// @param color The color for the histogram.
// @returns The drawn polyline.
method drawHistogram(VP this, int[] xs, int baseX, float step, float height, color color) =>
    // Construct the polyline points.
    chart.point[] points = array.new<chart.point>()
    float gap = (step - height) / 2
    for [i, x] in xs
        float lo = i * step + gap + this.minPrice
        float hi = lo + height
        points.push(chart.point.from_index(index = baseX, price = lo))
        points.push(chart.point.from_index(index = x, price = lo))
        points.push(chart.point.from_index(index = x, price = hi))
        points.push(chart.point.from_index(index = baseX, price = hi))
    polyline.new(points, closed = true, line_color = color, fill_color = color)

// @function Calculates the distribution and visualizes it on the chart.
method update(VP this) =>
    // Calculate the step size for each bucket.
    float step = (this.maxPrice - this.minPrice) / NUM_ROWS

    // Loop through the candles and populate the distribution array.
    float[] dist = array.new_float(NUM_ROWS, 0)
    float[] deltaDist = array.new_float(NUM_ROWS, 0)
    for c in this.candles
        // Calculate the start and end index of the buckets to fill the volume.
        int start = this.getBucketIndex(c.low)
        int end = this.getBucketIndex(c.high)
        int buckets = end - start + 1
        
        float vol = c.volume / buckets
        for i = start to end
            dist.set(i, dist.get(i) + vol)
            deltaDist.set(i, deltaDist.get(i) + vol * c.direction)
    float maxVol = dist.max()

    // Calculate the x coordinate for each row.
    int baseX = bar_index + BASE_OFFSET
    int[] xs = array.new_int()
    for vol in dist
        int width = math.round(vol / maxVol * MAX_WIDTH)
        int x = baseX - width
        xs.push(x)
    float height = HEIGHT_PERCENTAGE_PER_ROW / 100 * step

    // Draw the histogram.
    if not na(this.poly)
        this.poly.delete()
    this.poly := this.drawHistogram(xs, baseX, step, height, COLOR)

    // Draw the delta histograms.
    if SHOW_DELTA
        int[] buyXs = array.new_int()
        int[] sellXs = array.new_int()
        for vol in deltaDist
            int width = math.round(math.abs(vol) / maxVol * MAX_WIDTH)
            int x = baseX - width
            buyXs.push(vol > 0 ? x : baseX)
            sellXs.push(vol < 0 ? x : baseX)
        if not na(this.buyPoly)
            this.buyPoly.delete()
        this.buyPoly := this.drawHistogram(buyXs, baseX, step, height, BUY_COLOR)
        if not na(this.sellPoly)
            this.sellPoly.delete()
        this.sellPoly := this.drawHistogram(sellXs, baseX, step, height, SELL_COLOR)

    // Calculate the cumulative distribution.
    float[] cumdist = dist.copy()
    for i = 1 to cumdist.size() - 1
        cumdist.set(i, cumdist.get(i - 1) + cumdist.get(i))
    float totalVolume = cumdist.last()

    // Highlight VAH and VAL.
    if DISPLAY_VA
        float valPercentile = (100 - VALUE_AREA_PERCENT) / 100 / 2
        float vahPercentile = 1 - valPercentile
        int valIndex = cumdist.binary_search_leftmost(totalVolume * valPercentile)
        this.mark(
          name = 'VAL',
          price = this.getBucketedPrice(valIndex),
          left = xs.get(valIndex),
          bg = VAL_COLOR,
          height = height)

        int vahIndex = cumdist.binary_search_leftmost(totalVolume * vahPercentile)
        this.mark(
          name = 'VAH',
          price = this.getBucketedPrice(vahIndex),
          left = xs.get(vahIndex),
          bg = VAH_COLOR,
          height = height)

    // Highlight POC.
    if DISPLAY_POC
        int pocIndex = maxIndex(dist)
        this.mark(
          name = 'POC',
          price = this.getBucketedPrice(pocIndex),
          left = xs.get(pocIndex),
          bg = POC_COLOR,
          height = height)

    // Create another vertical polyline to mask the rightmost pixel of the volume profile, so that the minimum
    // width for each price bucket can be 0 rather than 1.
    chart.point maxPoint = chart.point.from_index(index = baseX, price = this.maxPrice)
    chart.point minPoint = chart.point.from_index(index = baseX, price = this.minPrice)
    polyline.new(array.from(maxPoint, minPoint), line_color = chart.bg_color)

//#endregion

//#region main

var VP vp = VP.new().init()

// @function Returns a lower timeframe string given the multiplier.
// @param multiplier The multiplier to use.
// @returns The timeframe string.
ltf(simple int multiplier) =>
    timeframe.from_seconds(math.max(MIN_TIMEFRAME_IN_SECONDS, math.round(timeframe.in_seconds() / multiplier)))

simple string ltf1  = timeframe.period
simple string ltf2  = ltf(2)
simple string ltf4  = ltf(4)
simple string ltf8  = ltf(8)
simple string ltf16 = ltf(16)

// Checks a list of lower timeframes to see which one should be used for the volume profile calculation.
// NOTE: unfortunately we cannot use a for-loop to go through an array of timeframe strings since the timeframe
// parameter in `request.security_lower_tf` has to be an `simple string` whereas the type for items in array is
// always `series`, even if the array itself is marked as `const` or `simple`.
bool canProcess = time >= START_TIME and (last_bar_index - bar_index) < MAX_LTF_BARS
bool cp1  = canProcess
bool cp2  = request.security(syminfo.tickerid, ltf2,  canProcess)
bool cp4  = request.security(syminfo.tickerid, ltf4,  canProcess)
bool cp8  = request.security(syminfo.tickerid, ltf8,  canProcess)
bool cp16 = request.security(syminfo.tickerid, ltf16, canProcess)

// A list of candidate candles in various timeframes.
int dir = switch
    close > open => DIRECTION_BUY
    close < open => DIRECTION_SELL
    => DIRECTION_NONE
float vol = MODE == VOLUME_PROFILE ? nz(volume, 100) : 100
Candle candle = Candle.new(high, low, vol, dir)
Candle[] c1  = array.from(candle)
Candle[] c2  = request.security_lower_tf(syminfo.tickerid, ltf2,  candle, ignore_invalid_timeframe = true)
Candle[] c4  = request.security_lower_tf(syminfo.tickerid, ltf4,  candle, ignore_invalid_timeframe = true)
Candle[] c8  = request.security_lower_tf(syminfo.tickerid, ltf8,  candle, ignore_invalid_timeframe = true)
Candle[] c16 = request.security_lower_tf(syminfo.tickerid, ltf16, candle, ignore_invalid_timeframe = true)

var string ltf = na
if na(ltf)
    ltf := switch
        cp16 => ltf16
        cp8  => ltf8
        cp4  => ltf4
        cp2  => ltf2
        cp1  => ltf1

Candle[] cs = switch ltf
    ltf1 => c1
    ltf2 => c2
    ltf4 => c4
    ltf8 => c8
    ltf16 => c16

if not na(cs)
    for c in cs
        vp.store(c)

if barstate.islast
    vp.update()

//#endregion

Overview

Users can switch between standard volume analysis and time price opportunity modes, select visible, anchored, or full chart ranges, and adjust row granularity. It displays the point of control alongside value area high and low lines, with optional volume delta tracking. The indicator provides analytical levels rather than execution signals. High lower-timeframe bar limits or second-based data may trigger script execution limits or require premium platform tiers.

Details

Published by

webcrack4 on GitHub

Code credit

algotraderdev, as written in the script's header

Type

Indicator, Pine Script v5

Draws

On the price chart

Category

Volume, Support & resistance

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.

Settings

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

SettingTypeDefaultVariable
ModestringVOLUME_PROFILEMODE
Range ModestringVISIBLE_RANGERANGE_MODE
Anchor Timetimetimestamp('2023-01-01T12:00:00')ANCHORED_TIME
Number of Rowsint300NUM_ROWS
Max Lower Timeframe Barsint5000MAX_LTF_BARS
Use Second-Based TimeframesboolfalseUSE_SECOND_BASED_TIMEFRAMES
Horizontal Offsetint20HORIZONTAL_OFFSET
Max Widthint80MAX_WIDTH
Height % per Rowint40HEIGHT_PERCENTAGE_PER_ROW
Show Volume DeltaboolfalseSHOW_DELTA
Defaultcolor#5d606b77COLOR
Buy Volumecolor#00bcd480BUY_COLOR
Show all 20 settings
SettingTypeDefaultVariable
Sell Volumecolor#e91e6380SELL_COLOR
POC (Point of Control)colorcolor.yellowPOC_COLOR
VAH (Value Area High)colorcolor.blueVAH_COLOR
VAL (Value Area Low)colorcolor.blueVAL_COLOR
Value Area Percentint70VALUE_AREA_PERCENT
Display VAH/VALbooltrueDISPLAY_VA
Display POCbooltrueDISPLAY_POC
Extend Key Price Lines to the LeftbooltrueEXTEND_LEFT
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
218 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 8 weeks ago · Custom licence GitHub repository of Moon Dev GEX Arrows

More indicators

Trade Your Charts With a Regulated Broker

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

Compare brokers