Volume Profile Premium
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © dandrideng

// ══════════════════════════════════════════════════════════════════════════════════════════════════ //
//# * ══════════════════════════════════════════════════════════════════════════════════════════════
//# *
//# * Study       : Volume Profile Premium
//# * Author      : © dandri deng
//# *
//# * Revision History
//# *  Release    : Jun. 03, 2022  First release of Volume Profile Premium indicator
//# *  Update     : Sep. 11, 2022  Fix the bugs of real time volume-profile updates
//# *  Update     : Dec. 30, 2022  Fix the bugs of collecting wrong history data in replay mode
//# *
//# * ══════════════════════════════════════════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════════════════════════════════════════════ //

//@version=5
indicator("Volume Profile Premium", shorttitle="VP-Premium", overlay=true, max_bars_back=500, max_lines_count=500, max_labels_count=500, max_boxes_count=500)

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

print(string txt) => 
	var table t = table.new(position.middle_right, 1, 1)
	table.cell(t, 0, 0, txt, bgcolor = color.yellow)

// ---------------------------------------------------------------------------------------------- //
// volume profile  ------------------------------------------------------------------------------ //

group_volume_profile = "Volume Profile Parameters"
max_lbk = input.int(defval=100, title="max of lookback forward", minval=1, maxval=1200, step=1, group=group_volume_profile)
lower_reso = input.int(defval=5, title="lower resolution (Minute)", minval=1, maxval=300, step=1, group=group_volume_profile)
display = input.string(defval="Buy/Sell", title="display volume profile", options=["Buy/Sell", "Total"], group=group_volume_profile)
num_rows = input.int(defval=30, title="rows of volume profile", minval=5, maxval=150, step=1, group=group_volume_profile)
placement = input.string(defval="Right", title="placement of volume profile", options=["Left", "Right"], group=group_volume_profile)
width = input.int(defval=75, title="width of volume profile", minval=5, maxval=300, step=1, group=group_volume_profile)
offset = input.int(defval=5, title="horizontal offset", minval=5, maxval=300, step=1, group=group_volume_profile)
realtime = input.bool(defval=false, title="Enable realtime volum-profile mode", tooltip="do not use realtime mode in replay mode!", group=group_volume_profile)

if timeframe.multiplier % lower_reso != 0
    runtime.error("The multiplier of current timeframe (" + str.tostring(timeframe.multiplier) + ") can not be divided by that of the lower timeframe (" +  str.tostring(lower_reso) + ")")

//stage one: request bars from lower time frame
[open_sec, high_sec, low_sec, close_sec, vol_sec] = request.security_lower_tf(syminfo.tickerid, str.tostring(lower_reso), [open, high, low, close, volume])
request_size = array.size(open_sec)

var open_arr_var = array.new_float(0)
var high_arr_var = array.new_float(0)
var low_arr_var = array.new_float(0)
var close_arr_var = array.new_float(0)
var vol_arr_var = array.new_float(0)

array.concat(open_arr_var, open_sec)
array.concat(high_arr_var, high_sec)
array.concat(low_arr_var, low_sec)
array.concat(close_arr_var, close_sec)
array.concat(vol_arr_var, vol_sec)

//stage two: volume accumulation in the specific price region
open_arr = array.new_float(0)
high_arr = array.new_float(0)
low_arr = array.new_float(0)
close_arr = array.new_float(0)
vol_arr = array.new_float(0)

slice_size = request_size[1] * (max_lbk - 1) + request_size

var_array_size = array.size(open_arr_var) 
if var_array_size > slice_size
    open_arr := array.slice(open_arr_var, var_array_size - slice_size - 1, var_array_size - 1)
    high_arr := array.slice(high_arr_var, var_array_size - slice_size - 1, var_array_size - 1)
    low_arr := array.slice(low_arr_var, var_array_size - slice_size - 1, var_array_size - 1)
    close_arr := array.slice(close_arr_var, var_array_size - slice_size - 1, var_array_size - 1)
    vol_arr := array.slice(vol_arr_var, var_array_size - slice_size - 1, var_array_size - 1)

if realtime
    array.push(open_arr, open)
    array.push(high_arr, high)
    array.push(low_arr, low)
    array.push(close_arr, close)
    array.push(vol_arr, volume)

price_region = array.new_float(0)
array.concat(price_region, high_arr)
array.concat(price_region, low_arr)
array.sort(price_region, order.descending)

price_region_size = array.size(price_region)
buy_volume = array.new_float(math.max(price_region_size, 1) - 1, 0)
sell_volume = array.new_float(math.max(price_region_size, 1) - 1, 0)

price_array_size = array.size(open_arr)
if price_array_size > 0
    for i = 0 to price_array_size - 1
        o = array.get(open_arr, i)
        h = array.get(high_arr, i)
        l = array.get(low_arr, i)
        c = array.get(close_arr, i)
        v = array.get(vol_arr, i)

        h_idx = array.indexof(price_region, h)
        l_idx = array.lastindexof(price_region, l)
        for j = h_idx to l_idx - 1
            l_rgn = array.get(price_region, j)
            r_rgn  = array.get(price_region, j + 1)
            if c > o
                array.set(buy_volume, j, array.get(buy_volume, j) + (l_rgn - r_rgn) / (h - l) * v)
            else
                array.set(sell_volume, j, array.get(sell_volume, j) + (l_rgn - r_rgn) / (h - l) * v)

//stage three: discretization processing 
buy_profile = array.new_float(num_rows, 0)
sell_profile = array.new_float(num_rows, 0)

highest = ta.highest(high, max_lbk)
lowest = ta.lowest(low, max_lbk)
interval = (highest - lowest) / num_rows

if price_region_size > 1
    for i = 0 to price_region_size - 2
        h = array.get(price_region, i)
        l = array.get(price_region, i + 1)
        bv = array.get(buy_volume, i)
        sv = array.get(sell_volume, i)
        
        r_min = int((highest - h) / interval)
        r_max = int((highest - l) / interval)
        if r_min <= r_max
            for r = r_min to r_max
                if r >= 0 and r <= num_rows - 1
                    r1 = highest - r * interval
                    r2 = highest - (r + 1) * interval
                    up = math.min(r1, h)
                    dn = math.max(r2, l)
                    if up > dn
                        array.set(buy_profile, r, array.get(buy_profile, r) + (up - dn) * bv / (h - l))
                        array.set(sell_profile, r, array.get(sell_profile, r) + (up - dn) * sv / (h - l))
                
total_profile = array.new_float(num_rows, 0)
for r = 0 to num_rows - 1
    array.set(total_profile, r, array.get(buy_profile, r) + array.get(sell_profile, r))
max_profile = array.max(total_profile)

buy_bar = array.new_int(num_rows, 0)
sell_bar = array.new_int(num_rows, 0)
for r = 0 to num_rows - 1
    array.set(buy_bar, r, int(array.get(buy_profile, r) / max_profile * width))
    array.set(sell_bar, r, int(array.get(sell_profile, r) / max_profile * width))


// ---------------------------------------------------------------------------------------------- //
// display settings ----------------------------------------------------------------------------- //

group_display_settings = "Display Settings"
color_hl_line = input.color(defval=color.new(#5d606b, 95), title="lookback region color, lines/region", inline="AA", group=group_display_settings)
color_hl_region = input.color(defval=color.new(#5d606b, 95), title="", inline="AA", group=group_display_settings)
color_buy_profile = input.color(defval=color.new(#1592e6, 30), title="profile color, buys/sells/total", inline="BB", group=group_display_settings)
color_sell_profile = input.color(defval=color.new(#fbc123, 30), title="", inline="BB", group=group_display_settings)
color_total_profile = input.color(defval=color.new(#1592e6, 30), title="", inline="BB", group=group_display_settings)
color_bull_profile = input.color(defval=color.new(#26a69a, 30), title="difference color, bull/bear", inline="CC", group=group_display_settings)
color_bear_profile = input.color(defval=color.new(#ef5350, 30), title="", inline="CC", group=group_display_settings)

var line highest_line = na
var line lowest_line = na

line.delete(highest_line)
line.delete(lowest_line)

if placement == "Right"
    highest_line := line.new(x1=bar_index - max_lbk, y1=highest, x2=bar_index + width + offset, y2=highest, width=2, color=color_hl_line)
    lowest_line := line.new(x1=bar_index - max_lbk, y1=lowest, x2=bar_index + width + offset, y2=lowest, width=2, color=color_hl_line)
else
    highest_line := line.new(x1=bar_index - max_lbk, y1=highest, x2=bar_index, y2=highest, width=2, color=color_hl_line)
    lowest_line := line.new(x1=bar_index - max_lbk, y1=lowest, x2=bar_index, y2=lowest, width=2, color=color_hl_line)
    
linefill.new(highest_line, lowest_line, color_hl_region)
    
all_boxes = box.all
if array.size(all_boxes) > 0
	for i = 0 to array.size(all_boxes) - 1
		box.delete(array.get(all_boxes, i))

left = 0
right = 0
for r = 0 to num_rows - 1
    top = highest - r * interval 
    bottom = highest - (r + 1) * interval
    top := top - interval * 0.15
    bottom := bottom + interval * 0.15
    
    diff_bar = array.get(buy_bar, r) - array.get(sell_bar, r)

    if display == "Buy/Sell"
        if placement == "Right"
            left := array.get(buy_bar, r) - (width + offset)
            right := -(width + offset)
        else
            left := max_lbk
            right := left - array.get(buy_bar, r)
        
        lbx = box.new(left=bar_index - left, top=top, right=bar_index - right, bottom=bottom)
        box.set_bgcolor(lbx, color_buy_profile)
        box.set_border_color(lbx, color_buy_profile)
        
        if placement == "Right"
            right := left
            left := left + array.get(sell_bar, r) 
        else
            left := right
            right := left - array.get(sell_bar, r) 
        
        sbx = box.new(left=bar_index - left, top=top, right=bar_index - right, bottom=bottom)
        box.set_bgcolor(sbx, color_sell_profile)
        box.set_border_color(sbx, color_sell_profile)
    else
        if placement == "Right"
            left := array.get(buy_bar, r) + array.get(sell_bar, r) - (width + offset)
            right := -(width + offset)
        else
            left := max_lbk
            right := left - diff_bar
        
        tbx = box.new(left=bar_index - left, top=top, right=bar_index - right, bottom=bottom)
        box.set_bgcolor(tbx, color_total_profile)
        box.set_border_color(tbx, color_total_profile)
    
    if placement == "Right"
        left := -(width + offset + 1)
        right := -(math.abs(diff_bar) + width + offset + 1)
    else
        left := max_lbk + 1 + math.abs(diff_bar)
        right := max_lbk + 1
        
    clr = diff_bar > 0 ? color_bull_profile : diff_bar < 0 ? color_bear_profile : na
    dbx = box.new(left=bar_index - left, top=top, right=bar_index - right, bottom=bottom)
    box.set_bgcolor(dbx, clr)
    box.set_border_color(dbx, clr)

// ---------------------------------------------------------------------------------------------- //
// end of file ---------------------------------------------------------------------------------- //

