Price Range Volume Profile [Pt]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © PtGambler

//@version=5

indicator("Price Range Volume Profile [Pt]", "PRVP [Pt]", true, max_bars_back = 5000, max_lines_count = 500, max_boxes_count = 500,max_labels_count = 500, explicit_plot_zorder = true)

group_volume_profile    = 'Volume Profile  ---------------------------------------------------------'
group_extra             = 'Extra  ------------------------------------------------------------------'

priceRange        = input.float(2, 'Price Range, % distance', minval = 0.5, step = 0.5, group = group_volume_profile, confirm = true, inline = 'pr') /100
ps                = input.float(0, 'Profile Step size (tick)', inline = 'mintick', step = 0.1, group = group_volume_profile, confirm = true, tooltip = 'Choose 0 for auto step size')

var initial_open_price = 0.
if bar_index == 0
    initial_open_price := open

if ps == 0
    ps     := math.round_to_mintick(initial_open_price / 2000)
else
    ps     := math.round_to_mintick(ps)

lookbackLength    = input.int(3000, 'Historical bars Lookback', minval = 100, maxval = 20000, step = 100 , group = group_volume_profile, confirm = true, inline = 'lb', tooltip = 'Do not exceed 10000 if you are not subscribed to Premium TradingView account')
fullRangeLB       = input.bool(true, '└ Full Historical Lookback', group = group_volume_profile, confirm = true)
 
if lookbackLength > last_bar_index and last_bar_index > 10000
    runtime.error('Historical bars Lookback period exceeds your TradingView subscription limit. Please reduce.')

volumeProfile     = input.bool(true, 'Volume Profile', inline='ZZ', group = group_volume_profile)
nonVaColor        = input.color(color.new(#3fdcff, 30), '', inline='ZZ', group = group_volume_profile)
show_poc          = input.bool(true, 'Point of Control (POC)', inline = 'poc', group = group_volume_profile)
poc_color         = input.color(color.orange, '', inline = 'poc', group = group_volume_profile)

profileWidth      = input.int(300, 'Profile Width', minval = 25, maxval = 300                      , group = group_volume_profile, inline = 'pw')
horizontalOffset  = input.int(40, 'Horizontal Offset', minval = 0 , maxval = 200                   , group = group_volume_profile, inline = 'ho')
applyBackColor    = input.bool(false, 'Background Fill of Profile Range'            , inline = 'BG', group = group_volume_profile)
backgroundColor   = input.color(color.new(#2962ff, 90), ''                         , inline = 'BG', group = group_volume_profile)
timeWeighted      = input.bool(false, 'Time Weighted Profile', group = group_volume_profile, inline = 'tw')
timeWeight_pow    = input.int(2, '| Power', group = group_volume_profile, inline = 'tw')
smoothing         = input.bool(false, 'Smooth filter', group = group_volume_profile, inline = 'sm')
sm_len            = input.int(3, '| Length', group = group_volume_profile, inline = 'sm')
 
show_table = input.bool(true,"Show Settings", group= group_extra, inline="Table")
tableLocation = input.string(defval='Bottom right', options=['Top left', 'Top center', 'Top right', 'Bottom left', 'Bottom center', 'Bottom right'], title='| Location', group=group_extra, inline="Table")
tablePosition = tableLocation == 'Top left' ? position.top_left : tableLocation == 'Top center' ? position.top_center : tableLocation == 'Top right' ? position.top_right : tableLocation == 'Bottom left' ? position.bottom_left : tableLocation == 'Bottom center' ? position.bottom_center : position.bottom_right
font_size = input.string('Normal', "    └ Font Size", options = ['Auto', 'Tiny', 'Small', 'Normal', 'Large'], group= group_extra, inline = 'font')

stbar = fullRangeLB ? 0 : last_bar_index - lookbackLength

bs_lastSession = nz(ta.barssince(session.isfirstbar))

var a_pv     = array.new_box()
var m_pvol   = matrix.new<float>(1,2)
var m_pvol_sm = matrix.new<float>(1,2)
var poclvl = 0.

noError = true

b_lvlblw = false
lvlblw = 0

bpl = low
bph = high
nzVol = nz(volume)

var ath = 0.0
var atl = 0.0

var pb = low
var stpb = low

if bar_index > stbar
    ath := high > nz(ath[1]) or ath <= 0 ? high : nz(ath[1])
    atl := low < nz(atl[1]) or atl <= 0 ? low : nz(atl[1])
    atl := atl - ((atl - pb) % ps)

prh    = math.min(math.round_to_mintick(close + (close*priceRange)), ath)
prl     =  math.max(math.round_to_mintick(close - (close*priceRange)), atl)

prl := prl - ((prl - pb) % ps)

// Functions  ----------------------------------------------------------------------------------- 

f_drawLabelX(_x, _y, _text, _xloc, _yloc, _color, _style, _textcolor, _size, _textalign, _tooltip) =>
    var id = label.new(_x, _y, _text, _xloc, _yloc, _color, _style, _textcolor, _size, _textalign, _tooltip)
    label.set_xy(id, _x, _y)
    label.set_text(id, _text)
    label.set_tooltip(id, _tooltip)
    label.set_textcolor(id, _textcolor)
    id

f_drawLineX(_x1, _y1, _x2, _y2, _xloc, _extend, _color, _style, _width) =>
    var id = line.new(_x1, _y1, _x2, _y2, _xloc, _extend, _color, _style, _width)
    line.set_xy1(id, _x1, _y1)
    line.set_xy2(id, _x2, _y2)
    line.set_color(id, _color)
    id

f_drawBoxX(_left, _top, _right, _bottom, _border_color, _border_width, _border_style, _bgcolor) =>
    var id = box.new(_left, _top, _right, _bottom, _border_color, _border_width, _border_style, bgcolor = _bgcolor)
    box.set_left(id, _left)
    box.set_top(id, _top)
    box.set_right(id, _right)
    box.set_bottom(id, _bottom)
    id

f_tfIsIntraday(_res) =>
    [intraday, daily, weekly, monthly] = request.security(syminfo.tickerid, _res, [timeframe.isintraday, timeframe.isdaily, timeframe.isweekly, timeframe.ismonthly])
    check = intraday ? "Intraday" : daily ? "Daily" : weekly ? "Weekly" : monthly ? "Monthly" : "Error" 
    check

// Main Calculations and data collection ----------------------------------------------------

stpb := bpl > bph[1] ? bph[1] : bpl
if bpl > atl[1]
    stpb := stpb - ((stpb - pb) % ps)
else if bpl < atl[1]
    b_lvlblw := true
    lvlblw := math.ceil((atl[1] - stpb) / ps)
    stpb := atl

stlvl = math.floor((stpb - atl) / ps)
endlvl = math.ceil((bph - atl) / ps)
blvl = endlvl - stlvl + 1

tlvl = nz(matrix.rows(m_pvol))

tw_factor = timeWeighted ? math.pow(math.max((bar_index - stbar), 0) / (last_bar_index - stbar),timeWeight_pow) : 1

if bar_index >= stbar and bar_index < stbar + 2
    for lvl = 0 to blvl - 1
        pl = pb + ps * lvl
        matrix.add_row(m_pvol)
        matrix.set(m_pvol, lvl, 0, pl)
        matrix.set(m_pvol, lvl, 1, volume[1] / blvl * tw_factor)

if barstate.isconfirmed and bar_index > stbar + 1 and nzVol

    if b_lvlblw
        for lvl = 1 to lvlblw
            pl = atl[1] - ps * lvl
            matrix.add_row(m_pvol, 0)
            matrix.set(m_pvol, 0, 0, pl)

    for lvl = stlvl to endlvl
        if lvl >= 0
            if lvl < tlvl
                matrix.set(m_pvol, lvl, 1, nz(matrix.get(m_pvol, lvl, 1)) + nzVol / blvl * tw_factor)

            else if lvl >= tlvl
                pl = atl[1] + ps * lvl
                matrix.add_row(m_pvol)
                matrix.set(m_pvol, lvl, 0, pl)
                matrix.set(m_pvol, lvl, 1, nz(matrix.get(m_pvol, lvl, 1)) + nzVol / blvl * tw_factor)

    if smoothing
        m_pvol_sm := matrix.copy(m_pvol)
        for i = 0 to matrix.rows(m_pvol_sm)-1
            sum_vol = 0.0
            count = 0
            for j = -sm_len/2 to sm_len/2
                if i+j >= 0 and i+j < matrix.rows(m_pvol_sm)
                    sum_vol := sum_vol + nz(matrix.get(m_pvol, i+j, 1))
                    count := count + 1
            matrix.set(m_pvol_sm, i, 1, sum_vol / count)

vpl  = math.max(math.floor((prl - atl) / ps), 0)
vph = math.min(math.floor((prh - atl) / ps), matrix.rows(m_pvol)-1)
vplvl = vph - vpl

// Error handling ----------------------------------------------------------

option1 = '- Increase Profile Step size (tick)\n'
option2 = '- Reduce Price Range, % distance\n'
error_text = ''

total_boxes = vplvl

if total_boxes > 500
    noError := false

if barstate.islast and not noError
    error_text := 'There are too many levels (' + str.tostring(vplvl) + ') shown on the volume profile. \nIt must be <500. Considering the following options: \n' + option1 + option2
    f_drawLabelX(last_bar_index, close, error_text, xloc.bar_index, yloc.price, color.new(color.red,90), label.style_label_left, chart.fg_color, size.large, text.align_left, '')

// Plotting Profile -------------------------------------------------------

if barstate.islast and nzVol and noError

    if array.size(a_pv) > 0
        for i = 0 to array.size(a_pv) - 1
            box.delete(array.shift(a_pv))
 
    maxVol = 0.

    for lvl = vpl to vph - 1
        maxVol := math.max(maxVol, smoothing ? nz(matrix.get(m_pvol_sm, lvl, 1)) : nz(matrix.get(m_pvol, lvl, 1)))
 
    for lvl = vpl to vph - 1
        lvlvol = smoothing ? nz(matrix.get(m_pvol_sm, lvl, 1)) : nz(matrix.get(m_pvol, lvl, 1))
        if volumeProfile
            lvlCol =  nonVaColor
            stind = bar_index + profileWidth + horizontalOffset - math.ceil(profileWidth * (lvlvol / maxVol))
            endind   = bar_index + profileWidth + horizontalOffset
            array.push(a_pv, box.new(stind, atl + (lvl + 0.9) * ps, endind, atl + (lvl + 0.1) * ps, lvlCol, bgcolor = lvlCol))
        if lvlvol == maxVol
            poclvl := atl + (lvl + 0.5) * ps
    
    if show_poc
        f_drawLineX(bar_index, poclvl, bar_index + profileWidth + horizontalOffset, poclvl, xloc.bar_index, extend.left, poc_color, line.style_solid, 2)

    if applyBackColor
        f_drawBoxX(math.max(stbar, bar_index-5000), prh, bar_index, prl, backgroundColor, 1, line.style_dotted, backgroundColor)

// Table --------------------------------------------------------------------------------------------------------------

var displayTable = table.new(tablePosition, 1, 9, border_width=1)

f_fillCell(_row, _column,series string _cellText) =>
    switch font_size
        "Auto" => table.cell(displayTable, _column, _row, _cellText, bgcolor=color.new(chart.bg_color,90), text_color=color.white, text_halign = text.align_left, text_valign = text.align_center, text_size = size.auto)
        "Tiny" => table.cell(displayTable, _column, _row, _cellText, bgcolor=color.new(chart.bg_color,90), text_color=color.white, text_halign = text.align_left, text_valign = text.align_center, text_size = size.tiny)
        "Small" => table.cell(displayTable, _column, _row, _cellText, bgcolor=color.new(chart.bg_color,90), text_color=color.white, text_halign = text.align_left, text_valign = text.align_center, text_size = size.small)
        "Normal" => table.cell(displayTable, _column, _row, _cellText, bgcolor=color.new(chart.bg_color,90), text_color=color.white, text_halign = text.align_left, text_valign = text.align_center, text_size = size.normal)
        "Large" => table.cell(displayTable, _column, _row, _cellText, bgcolor=color.new(chart.bg_color,90), text_color=color.white, text_halign = text.align_left, text_valign = text.align_center, text_size = size.large)

if barstate.islast and show_table
    f_fillCell(0, 0, '% range: ' + str.tostring(priceRange*100) + "%")
    f_fillCell(1, 0, 'Tick size: ' + str.tostring(ps))
    f_fillCell(2, 0, 'VP levels: ' + str.tostring(vplvl) + ' (500 Max)')
    f_fillCell(3, 0, 'Lookback length: ' + (fullRangeLB ? 'Full History' : str.tostring(lookbackLength) + ' Bars'))
    f_fillCell(4, 0, 'Time Weighted: ' + (timeWeighted ? 'ON' : 'OFF'))
