Historic Volume/Market Profiles
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © SamRecio

//@version=5
indicator(title = "Historic Volume/Market Profiles", shorttitle ="HVMP", overlay = true, max_lines_count = 500, max_boxes_count = 500, max_labels_count = 500, max_bars_back = 5000)

tf = input.timeframe("D", title = "Timeframe", inline = "0", group = "PROFILE SETTINGS")

vap = input.float(70, title = "Value Area %", group = "PROFILE SETTINGS")/100
lb_days = input.int(5, title = "# of Profiles", maxval = 20, minval = 1, tooltip = "Max: 20 \nLarge Display = Less Granular Profiles\nSmall Display = More Granular Profiles")
mp = input.bool(false, title = "Calculate As Market Profile", group = "PROFILE SETTINGS", tooltip = "Calculations will distribue a 1 instead of the candle's volume.")

disp_size = input.int(-20, minval = -500,maxval = 500,title  = "Display Size   ", inline = "3", group = "DISPLAY SETTINGS", tooltip = "The entire range of your profile will scale to fit inside this range.\n When Positive, the profile will display from the start of the day. \n When Negative, the profile will display from the end of the day.\nNotes:\n-This value is # bars away from your profile's Axis.\n-The farther from 0 this value is, the more granular your (horizontal) view will be. This does not change the Profiles' value; because of this, sometimes the POC looks tied with other values widely different. The POC CAN be tied to values close to it, but if the value is far away it is likely to just be a visual constraint. \n AUTO SCALE - Fits the profile within the lookback period.")
auto_size = input.bool(true, title = "Auto-Scale", inline = "3", group = "DISPLAY SETTINGS")

hi_width = input.int(10, maxval = 100, minval = 1,title = "[HVN] Analysis Width %      ↕", group = "High/Low Volume Nodes", tooltip = "[HVN] = High Volume Node\nAnalysis Width % = % of profile to take into account when determining what is a High Volume Node and what is Not.")*0.01
lo_width = input.int(10, maxval = 100, minval = 1, title = "[LVN]  Analysis Width %      ↕", group = "High/Low Volume Nodes", tooltip = "[LVN] = Low Volume Node\nAnalysis Width % = % of profile to take into account when determining what is a Low Volume Node and what is Not.")*0.01

poc_color = input.color(#ff033e, title = "POC Color", group = "Colors")
var_color = input.color(color.white, title = "Value High/Low Color", group = "Colors")
vaz_color = input.color(color.new(#555555,50), title = "Value Zone Color", group = "Colors")
ov_color = input.color(#555555, title = "Profile Color", group = "Colors")
lv_color = input.color(#801922, title  = "Low Volume Color", group = "Colors")
hv_color = input.color(#2295BF, title = "High Volume Color", group = "Colors")



fix_z(_val) => _val>0?_val:1

round_to(_round,_to) =>
    math.round(_round/_to)*_to

vgroup_pull(_var,_array,_num1,_num2) =>
    _var == 1 and _num1>=_num2?array.get(_array,_num1-_num2):
      _var == 2 and array.size(_array)-1 >= (_num1 + _num2)?array.get(_array,_num1+_num2)
      :0

prof_color(_num,_mv,_a1,_a2,_a3,_a4) =>
    _num==0?na:
     _num==_mv?poc_color:
     (_num==_a3 or _num==_a4)?var_color:
     array.includes(_a1,_num) and array.includes(_a2,_num)?((_num>_a3 or _num<_a4)?ov_color:vaz_color):
     array.includes(_a2,_num)?lv_color:
     array.includes(_a1,_num)?hv_color:
     (_num>_a3 or _num<_a4)?ov_color:
     vaz_color   

var line_array = array.new_line(na)
var box_array = array.new_box(na)

kill_bar = ta.valuewhen(timeframe.change(tf),bar_index,lb_days+1)+1
if array.size(line_array) > 0 
    for i = array.size(line_array)-1 to 0
        ln = array.get(line_array,i)
        lx = line.get_x1(ln)
        if lx <= kill_bar
            line.delete(ln)
            array.remove(line_array,i)
if array.size(box_array) > 0 
    for i = array.size(box_array)-1 to 0
        bx = array.get(box_array,i)
        bl = box.get_left(bx)
        if bl <= kill_bar or na(bl)
            box.delete(bx)
            array.remove(box_array,i)
if array.size(label.all) > 0
    for i = array.size(label.all)-1 to 0
        lb = array.get(label.all,i)
        lx = label.get_x(lb)
        if lx <= kill_bar
            label.delete(lb)
            array.remove(label.all,i)

get_prof(_mp,_tf) => 
    new_calc = timeframe.change(_tf)
    last_new_calc = ta.valuewhen(new_calc,bar_index,1)
    index_num = math.floor(1000/lb_days)-1
    start_time = request.security("",_tf,ta.valuewhen(bar_index == last_bar_index-lb_days,time,0))
    calc_bars = (bar_index - ta.valuewhen(new_calc,bar_index,0))[1]+1
    base = ta.lowest(low,fix_z(calc_bars)+1)[1]
    roof = ta.highest(high,fix_z(calc_bars)+1)[1]
    tick_size = round_to(math.max(((roof - base)/index_num),syminfo.mintick),(syminfo.mintick/100))
    c_hi = round_to(high,tick_size)
    c_lo = round_to(low,tick_size)
    candle_range = c_hi - c_lo
    candle_index = (candle_range/tick_size)+1
    tick_vol = _mp?1:volume/candle_index 
    main = array.new_float(na)
    hvn_points = array.new_int(na) 
    lvn_points = array.new_int(na)
    if new_calc and time >= start_time
        for i = 0 to index_num
            index_price = base + (i*tick_size)
            if index_price >= roof
                break
            float index_sum = 0
            for e = 1 to calc_bars
                if index_price <= c_hi[e] and index_price >= c_lo[e]
                    index_sum := index_sum + tick_vol[e]
            array.push(main,index_sum) 
        max_index = math.round(math.avg(array.indexof(main,array.max(main)),array.lastindexof(main,array.max(main))))
        poc = base + (tick_size*max_index)
        max_vol = array.sum(main)*vap
        vol_count = max_index >=0?array.get(main, max_index):0.0
        up_count = max_index
        down_count = max_index
        if array.size(main) > 0
            for x = 0 to array.size(main)-1
                if vol_count >= max_vol
                    break                                                                   
                uppervol = up_count<array.size(main)-1?array.get(main, up_count + 1):na
                lowervol = down_count>0?array.get(main, down_count - 1):na
                if ((uppervol >= lowervol) and not na(uppervol)) or na(lowervol)
                    vol_count += uppervol
                    up_count += 1
                else                                                                        
                    vol_count += lowervol
                    down_count -= 1
        val = base + (tick_size*down_count)
        vah = base + (tick_size*up_count)
        uc = up_count
        dc = down_count
        

        ///////////////////////////////
        //Cluster ID for Volume Nodes//
        ///////////////////////////////
  
        if array.size(main) > 0
            for i = 0 to array.size(main)-1                     
                _val = array.get(main,i)                        
                ary = array.new_float(na)                               
                for e = 0 to int(array.size(main)*hi_width)                           
                    array.push(ary,vgroup_pull(1,main,i,e))        
                    array.push(ary,vgroup_pull(2,main,i,e))      
                max = array.max(ary)                       
                if _val >= math.avg(max,array.avg(ary))  
                    array.push(hvn_points,i)           

        if array.size(main) > 0    
            for i = 0 to array.size(main)-1
                _val = array.get(main,i)
                ary = array.new_float(na) 
                for e = 0 to int(array.size(main)*lo_width)  
                    array.push(ary,vgroup_pull(1,main,i,e))
                    array.push(ary,vgroup_pull(2,main,i,e))
                min = array.min(ary)
                if _val <= math.avg(min,array.avg(ary))  
                    array.push(lvn_points,i)
        ///_________________________________________
        ///Cluster Merging
        ///‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
        merge_per = 0.02
        if array.size(hvn_points)>0
            for i = 0 to array.size(hvn_points)-1
                hi_found = false
                lo_found = false
                _val = array.get(hvn_points,i)
                for e = int(array.size(main)*merge_per) to 0
                    if array.includes(hvn_points,_val+e)
                        hi_found := true
                    if hi_found
                        array.push(hvn_points,_val+e)
                    if array.includes(hvn_points,_val-e) 
                        lo_found := true
                    if lo_found
                        array.push(hvn_points,_val-e)

        if array.size(lvn_points)>0
            for i = 0 to array.size(lvn_points)-1
                hi_found = false
                lo_found = false
                _val = array.get(lvn_points,i)
                for e = int(array.size(main)*merge_per) to 0
                    if array.includes(lvn_points,_val+e)
                        hi_found := true
                    if hi_found
                        array.push(lvn_points,_val+e)
                    if array.includes(lvn_points,_val-e)
                        lo_found := true
                    if lo_found
                        array.push(lvn_points,_val-e)
        prof_axis = disp_size>0?last_new_calc:bar_index[1]
        display_size = auto_size and disp_size!=0?(disp_size/math.abs(disp_size))*(calc_bars-1):disp_size
        if array.size(main) > 0
            for i = 0 to array.size(main) - 1
                scale = display_size/array.max(main)
                scaled = math.round(array.get(main,i)*scale)
                if ((i>uc) or (i<dc)) and (array.size(line_array) <= 499)
                    array.push(line_array,line.new(prof_axis,base+(i*tick_size[1]),(prof_axis+scaled),base+(i*tick_size[1]), color = prof_color(i,max_index,hvn_points,lvn_points,uc,dc), style = (i<dc or i>uc?line.style_dotted:line.style_solid)))
                else if ((i<=uc) or (i>=dc)) and (array.size(box_array) <= 499)
                    array.push(box_array,box.new(prof_axis,base+(i*tick_size[1]),(prof_axis+scaled),base+(i*tick_size[1]), border_color = prof_color(i,max_index,hvn_points,lvn_points,uc,dc), border_style = (i<dc or i>uc?line.style_dotted:line.style_solid), border_width = 1))
                else if (array.size(line_array) <= 499)
                    array.push(line_array,line.new(prof_axis,base+(i*tick_size[1]),(prof_axis+scaled),base+(i*tick_size[1]), color = prof_color(i,max_index,hvn_points,lvn_points,uc,dc), style = (i<dc or i>uc?line.style_dotted:line.style_solid)))
                else if (array.size(box_array) <= 499)
                    array.push(box_array,box.new(prof_axis,base+(i*tick_size[1]),(prof_axis+scaled),base+(i*tick_size[1]), border_color = prof_color(i,max_index,hvn_points,lvn_points,uc,dc), border_style = (i<dc or i>uc?line.style_dotted:line.style_solid), border_width = 1))
              
        lab = label.new(prof_axis,roof,size = size.small, text = (mp?"Mkt Profile [":"Vol Profile [") + tf + "] " + "\nLB [" + str.tostring(calc_bars) + " Bars]" + "\nGran: " + str.tostring(tick_size,"$#.##########"), style = (disp_size<0?label.style_label_lower_right:label.style_label_lower_left), color = color.rgb(0,0,0,100), textcolor = chart.fg_color, textalign = (disp_size<0?text.align_right:text.align_left), text_font_family = font.family_monospace)

get_prof(mp,tf)

