Average Volume Profile
// 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("Average Volume Profile", shorttitle = "AVP", overlay = true, max_lines_count = 500, max_boxes_count = 500, max_bars_back = 5000)

tf = input.timeframe("D", title = "Timeframe", inline = "0", group = "PROFILE SETTINGS")
mdr = input.float(4, minval = 1,maxval = 4,title = "Sensitivity ", inline = "1", group = "PROFILE SETTINGS", tooltip = "Higher = More Granular\nLower = Less Granular")
vap = input.float(70, title = "Value Area %", inline = "1_1", group = "PROFILE SETTINGS")/100
disp_size = input.int(-50, 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.\nNotes:\n-This value is # bars away from your profile's axis.\n-The larger this value is, the more granular your (horizontal) view will be. This does not change the Profiles' value; because of this, sometimes the HAV looks tied with other values widely different. The HAV 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-This Value CAN be negative")
prof_offset = input.int(50, minval = -500,maxval = 500, title  = "Display Offset", inline = "4", group = "DISPLAY SETTINGS", tooltip = "Offset your profile's axis (Left/Right) to customize your display to fit your style.\nNotes:\n-Zero = Current Bar\n-This Value CAN be negative")
extend_day = input.bool(false, title = "Display Current HAV/VAH/VAL", inline = "5", group = "Additional Data Displays")
hist = input.bool(false, title = "Display Historical HAV/VAH/VAL", inline = "6", group = "Additional Data Displays")
poc_color = input.color(#03afff, title = "Highest Avg Volume 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")
sp_color = input.color(#014f74, title  = "Lowest Avg Volume Color", group = "Colors")
vl_color = input.color(color.aqua, title = "Volume Line Color")

//Error messages
//no volume, by checking if volume == na
if na(volume)
    runtime.error("No Volume Data. Please Use a Ticker with Volume Data.")
//checking if we have the bar index of the last time the selscte timeframe changed. if not we're not able to calculate the granularity.
if barstate.islast and na(ta.valuewhen(timeframe.change(tf),bar_index,1))
    runtime.error("Profile Timeframe is Too Large. Please Increase Chart Timeframe, or Decrease Profile Timeframe.")

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

//Below is a modified version of my volume profile calculation from "Volume/Market Profile"
//This version handles 3 arrays in parallel holding values of a volume profile, market profile, and the average of the 2
vol_prof(_tf,_mdr) =>
    tf_change = timeframe.change(_tf)                                                                          
    var main = array.new_float(na)    
    var mp_main = array.new_float(na)
    var avg = array.new_float(na)                                                                         
    var float base = na                                                                                        
    var float roof = na                                                                                         
    max_array_dol_range = syminfo.mintick*(_mdr*100)                                                           
    change_dif = nz(ta.valuewhen(tf_change,bar_index,0) - ta.valuewhen(tf_change,bar_index,1),1)                
    rng_get = ta.valuewhen(tf_change,ta.highest(high,change_dif) - ta.lowest(low,change_dif),0)                 
    tick_size = rng_get>max_array_dol_range?math.ceil(rng_get/max_array_dol_range)*syminfo.mintick:syminfo.mintick    
    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 = volume/candle_index
    
    //Start
    if tf_change                        
        array.clear(main) 
        array.clear(mp_main)
        array.clear(avg)              
        base := c_lo                    
        roof := c_hi                    
        for i = 0 to candle_index-1     
            array.push(main,tick_vol)
            array.push(mp_main,1)
            array.push(avg,nz(tick_vol/1))   
    //Expand Down
    down_dif = math.abs((base - c_lo)/tick_size)    
    if c_lo < base                                  
        for i = 1 to down_dif                       
            array.unshift(main,0)
            array.unshift(mp_main,0)
            array.unshift(avg,0)                   
        base := c_lo                               
    z_point = math.abs((base - c_lo)/tick_size)     
        
    //Expand Up
    up_dif = math.abs((roof - c_hi)/tick_size)      
    if c_hi > roof                                  
        for i = 1 to up_dif                        
            array.push(main,0)
            array.push(mp_main,0)
            array.push(avg,0)                      
        roof := c_hi                                
        
    //Input Values
    for i = 0 to array.size(main)                                  
        if (i >= z_point) and (i <= (z_point + candle_index)-1)     
            v = array.get(main,int(i))
            mpv = array.get(mp_main,int(i))                              
            array.set(main,int(i),v + tick_vol)
            array.set(mp_main,int(i),mpv + 1)                     
            array.set(avg,int(i),nz((v + tick_vol)/(mpv + 1)))
    max_index = math.round(math.avg(array.indexof(avg,array.max(avg)),array.lastindexof(avg,array.max(avg)))) 
    poc = base + (tick_size*max_index) 
    //Value Zones
    max_vol = array.sum(avg)*vap                                                  
    vol_count = max_index >=0?array.get(avg, max_index):0.0                       
    up_count = max_index                                                            
    down_count = max_index                                                          
    for x = 0 to array.size(avg)-1                                                
        if vol_count >= max_vol                                                     
            break                                                                   
        uppervol = up_count<array.size(avg)-1?array.get(avg, up_count + 1):na    
        lowervol = down_count>0?array.get(avg, 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)                   
    pd_vah = ta.valuewhen(tf_change,vah[1],0)          
    pd_val = ta.valuewhen(tf_change,val[1],0)           
    pd_poc = ta.valuewhen(tf_change,poc[1],0)           
    [poc,vah,val,pd_poc,pd_vah,pd_val,avg,base,tick_size,up_count,down_count,max_index,roof,candle_index]

[poc,vah,val,pd_poc,pd_vah,pd_val,avg,base,tick_size,up_count,down_count,mv,roof,ci] = vol_prof(tf,mdr)

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
//Determining how many bars are inbetween tf changes
day_len = ta.valuewhen(timeframe.change(tf),bar_index,0)-ta.valuewhen(timeframe.change(tf),bar_index,1)
//Drawing the profile
var profile = array.new_line(na)
var box_profile = array.new_box(na)	

if array.size(profile) > 0
	for i = 0 to array.size(profile) - 1
		line.delete(array.get(profile, i))
        if i == (array.size(profile) - 1)
            array.clear(profile)
if array.size(box_profile) > 0
	for i = 0 to array.size(box_profile) - 1
		box.delete(array.get(box_profile, i))
        if i == (array.size(box_profile) - 1)
            array.clear(box_profile)
min_array = array.copy(avg)
if array.size(min_array) > 0
    for i = array.size(min_array)-1 to 0
        if array.get(min_array,i) == 0
            array.remove(min_array,i)
prof_color(_num) =>
    _num==mv?poc_color:
     (_num==up_count or _num==down_count)?var_color:
     array.get(avg,_num) == array.min(min_array)?sp_color:
     (_num>up_count or _num<down_count)?ov_color:
     vaz_color
     
bi_nd = ta.valuewhen(timeframe.change(tf),bar_index,0)
if barstate.islast 
    if array.size(avg) > 0
        for i = 0 to array.size(avg) - 1
            scale = disp_size/array.max(avg)
            scaled = math.round(array.get(avg,i)*scale)
            if ((i>up_count) or (i<down_count)) or (array.size(box_profile) >= 450)
                array.push(profile,line.new(bar_index+prof_offset,base+(i*tick_size),(bar_index+scaled)+prof_offset,base+(i*tick_size), color = (base+(i*tick_size)==round_to(close,tick_size))?vl_color:prof_color(i), style = (i<down_count or i>up_count?line.style_dotted:line.style_solid)))
            else
                array.push(box_profile,box.new(bar_index+prof_offset,base+(i*tick_size),(bar_index+scaled)+prof_offset,base+(i*tick_size), border_color = (base+(i*tick_size)==round_to(close,tick_size))?vl_color:prof_color(i), border_style = (i<down_count or i>up_count?line.style_dotted:line.style_solid), border_width = 1))
//Making moving labels and lines.
if array.size(avg) > 0
    cur = math.abs((round_to(close,tick_size)-base)/tick_size)
    a = math.round(array.get(avg,int(cur)))
    lab0 = label.new(bar_index+prof_offset,round_to(close,tick_size), style = (disp_size<0?label.style_label_left:label.style_label_right), text = "Avg: " + str.tostring(a,format.volume), color = color.new(color.black,100), textcolor = math.round(volume/ci)>a?vl_color:chart.fg_color, tooltip = "Average Volume at Price")
    label.delete(lab0[1])
    mv_lab = label.new(bar_index+prof_offset, base+(mv*tick_size), style = (disp_size<0?label.style_label_left:label.style_label_right), text = "Max Avg: " + str.tostring(array.get(avg,mv),format.volume), color = color.new(color.black,100), textcolor = chart.fg_color)
    label.delete(mv_lab[1])
    sc = disp_size/array.max(avg)
    scd = math.round((volume/ci)*sc)
    vlab = line.new((bar_index+scd)+prof_offset,roof,(bar_index+scd)+prof_offset,base, color = vl_color)
    line.delete(vlab[1])
    lab0_0 = label.new((bar_index+scd)+prof_offset,base, style = label.style_label_up, text = "\nVol: " + str.tostring(math.round(volume/ci),format.volume), color = color.new(color.black,100), textcolor = math.round(volume/ci)>a?vl_color:chart.fg_color, tooltip = "Distributed Volume\nCandle Volume/Candle Range = Distributed Volume")
    label.delete(lab0_0[1])
//Drawing previous day lines at the start of new day
var pd_lines = array.new_line(na)
if timeframe.change(tf) and hist
    array.push(pd_lines,line.new(bar_index-day_len,pd_poc,bar_index-1,pd_poc,color = poc_color, width = 2))
    array.push(pd_lines,line.new(bar_index-day_len,pd_vah,bar_index-1,pd_vah,color = var_color, width = 2))
    array.push(pd_lines,line.new(bar_index-day_len,pd_val,bar_index-1,pd_val,color = var_color, width = 2))
if array.size(pd_lines) > 90
    line.delete(array.get(pd_lines,0))
    array.remove(pd_lines,0)
//drawing current day's lines
if extend_day
    d_poc = line.new(bi_nd,poc,bar_index,poc,color = poc_color, width = 2)
    d_vah = line.new(bi_nd,vah,bar_index,vah,color = var_color, width = 2)
    d_val = line.new(bi_nd,val,bar_index,val,color = var_color, width = 2)
    line.delete(d_poc[1])
    line.delete(d_vah[1])
    line.delete(d_val[1])
//Profile title Label
lab = label.new(bar_index+prof_offset,roof, text = ("Average Volume Profile [") + tf + "]\nGranularity: " + 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_left:text.align_right), text_font_family = font.family_monospace)
label.delete(lab[1])
//Alerts
alertcondition(ta.crossover(close,poc), "Cross-Over Highest Average Volume")
alertcondition(ta.crossunder(close,poc), "Cross-Under Highest Average Volume")
alertcondition(ta.crossover(close,vah), "Cross-Over Value High")
alertcondition(ta.crossunder(close,vah), "Cross-Under Value High")
alertcondition(ta.crossover(close,val), "Cross-Over Value Low")
alertcondition(ta.crossunder(close,val),"Cross-Under Value Low")
