Angled Volume Profile [Trendoscope]
// This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0) https://creativecommons.org/licenses/by-nc-sa/4.0/
// © Trendoscope Pty Ltd
//                                       ░▒             
//                                  ▒▒▒   ▒▒      
//                              ▒▒▒▒▒     ▒▒      
//                      ▒▒▒▒▒▒▒░     ▒     ▒▒          
//                  ▒▒▒▒▒▒           ▒     ▒▒          
//             ▓▒▒▒       ▒        ▒▒▒▒▒▒▒▒▒▒▒  
//   ▒▒▒▒▒▒▒▒▒▒▒ ▒        ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒         
//   ▒  ▒       ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░        
//   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░▒▒▒▒▒▒▒▒         
//   ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒                       
//    ▒▒▒▒▒         ▒▒▒▒▒▒▒                            
//                 ▒▒▒▒▒▒▒▒▒                           
//                ▒▒▒▒▒ ▒▒▒▒▒                          
//               ░▒▒▒▒   ▒▒▒▒▓      ████████╗██████╗ ███████╗███╗   ██╗██████╗  ██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
//              ▓▒▒▒▒     ▒▒▒▒      ╚══██╔══╝██╔══██╗██╔════╝████╗  ██║██╔══██╗██╔═══██╗██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔════╝
//              ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒        ██║   ██████╔╝█████╗  ██╔██╗ ██║██║  ██║██║   ██║███████╗██║     ██║   ██║██████╔╝█████╗ 
//             ▒▒▒▒▒       ▒▒▒▒▒       ██║   ██╔══██╗██╔══╝  ██║╚██╗██║██║  ██║██║   ██║╚════██║██║     ██║   ██║██╔═══╝ ██╔══╝  
//            ▒▒▒▒▒         ▒▒▒▒▒      ██║   ██║  ██║███████╗██║ ╚████║██████╔╝╚██████╔╝███████║╚██████╗╚██████╔╝██║     ███████╗
//             ▒▒             ▒                        
//@version=5
import HeWhoMustNotBeNamed/LineWrapper/1 as LineWrapper

indicator("Angled Volume Profile [Trendoscope]", "AVP[Trendoscope]", overlay=true, max_lines_count = 500, max_bars_back = 5000)
startX = input.time(0, 'StartX', inline='Start', group='Coordinates', confirm=true)
startY = input.price(0, "StartY", inline='Start', group='Coordinates', confirm=true)

endY = input.price(0, "EndY", inline='End', group='Coordinates', confirm=true)

precision = input.int(1000, 'Step Multiplier', minval=10, step=100, group='Properties', tooltip='Multiplier for min tick to define step between profile lines')
lineColor = input.color(color.yellow, '', inline='l', group='Display')
lineTransparency = input.int(80, '', minval=0, maxval=100, step=5, inline='l', group='Display')
lineStyle = input.string(line.style_dotted, '', [line.style_dotted, line.style_dashed, line.style_solid], inline='l', group='Display', tooltip = 'Volume profile line color, transparency and style')

type VolumeProfileLine
    LineWrapper.Line profileLine
    float profileVolume = 0.0

method update(map<float, VolumeProfileLine> this, startXBar, endXBar, startXValue, endXValue, factor)=>
    vLine = this.contains(startXValue)? this.get(startXValue) : VolumeProfileLine.new(
                 LineWrapper.Line.new(startXBar, startXValue, endXBar, endXValue, color=lineColor, style = lineStyle)
             )
    vLine.profileVolume += volume*factor
    this.put(startXValue, vLine)
    this

roundtoprecision(price, step)=>
    multiplier = 1/step
    int(price*multiplier)/(multiplier*1.0)

method add(array<polyline> this, array<chart.point> points)=>
    this.push(polyline.new(points, false, false, xloc.bar_index, color.new(lineColor, lineTransparency), line_style=lineStyle))
    points.clear()

method flush(array<polyline> this)=>
    while this.size()!=0
        this.pop().delete()

var startXBar = 0

var volumeProfiles = map.new<float, VolumeProfileLine>()

var label startLbl = label.new(startX, startY, "", xloc = xloc.bar_time, style = label.style_circle, size = size.tiny, color = color.maroon, tooltip='Starting Point - Time+Price. Move this to change starting point')
var label endLbl = label.new(last_bar_time, endY, "", xloc = xloc.bar_time, style = label.style_circle, size = size.tiny, color = color.maroon, tooltip = 'Ending Point - Only price can be changed. Ending bar is always the last bar')

var float diff = endY - startY
var float step = syminfo.mintick*precision
if(time == startX)
    startXBar := bar_index
    end = diff + close
    vLine = LineWrapper.Line.new(startXBar, close, last_bar_index, end, color=lineColor, style = lineStyle)
    volumeProfiles.put(close, VolumeProfileLine.new(vLine, 0.0))

if(time >= startX and not na(step))
    endXBar = last_bar_index
    factor = (high-low)/(high-low+math.abs(open-close))
    startDiff = diff*(bar_index-startXBar)/(endXBar-startXBar)
    endDiff = diff*(endXBar-bar_index)/(endXBar-startXBar)
    
    for price = low to high by step
        startXValue = roundtoprecision(price- startDiff, step)
        endXValue = roundtoprecision(price + endDiff, step)
        volumeProfiles.update(startXBar, endXBar, startXValue, endXValue, (price <=math.max(open, close) and price >= math.min(open, close) ? 2 : 1)*factor)

    float maxVolume = na

    var bool draw = true
    if(barstate.islast and draw)
        draw := false
        for volumeProfile in volumeProfiles.values()
            maxVolume := nz(math.max(volumeProfile.profileVolume, maxVolume), volumeProfile.profileVolume)

        array<chart.point> profilePoints = array.new<chart.point>()
        array<polyline> polyLineArrays = array.new<polyline>()
        
        polyLineArrays.flush()
        for volumeProfile in volumeProfiles.values()
            percent = volumeProfile.profileVolume*100/maxVolume
            newEndX = startXBar + math.floor((last_bar_index-startXBar)*percent/100)
            newEndY = volumeProfile.profileLine.get_price(newEndX)
            newStartY = volumeProfile.profileLine.get_y1()
            
            if(percent != 0)
                profilePoints.push(chart.point.from_index(startXBar, newStartY))
                profilePoints.push(chart.point.from_index(newEndX, newEndY))
                profilePoints.push(chart.point.from_index(startXBar, newStartY))

            if(profilePoints.size() > 997)
                polyLineArrays.add(profilePoints)

        polyLineArrays.add(profilePoints)