OI Visible Range Ladder [Kioseff Trading]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © KioseffTrading

//@version=5
indicator("OI Visible Range [Kioseff Trading]", max_lines_count = 500, max_labels_count = 500, overlay= true, max_polylines_count = 100, max_boxes_count = 500)

import RicardoSantos/MathOperator/2

showBull      = input.bool  (defval = true, title = "Up OI Up Price  "    , group = "Profiles", inline = "Up")
showBull2     = input.bool  (defval = true, title = "Down OI Up Price"  , group = "Profiles", inline = "Up")
showBear      = input.bool  (defval = true, title = "Up OI Down Price"  , group = "Profiles", inline = "Dn")
showBear2     = input.bool  (defval = true, title = "Down OI Down Price", group = "Profiles", inline = "Dn")

ROWS          = input.int   (defval = 2000, maxval = 9998, minval = 10, step = 10, title = "Profile Rows", group = "Profile Settings")
factor        = input.float (defval = 3 , minval = 1.1, step = 0.1, title = "Scaling Factor", group = "Profile Settings")
cumu          = input.float (defval = 70, title = "Value Area", minval = 5, maxval = 95, group = "Profile Settings")
offset        = 1 
curve         = input.bool  (defval = false, title = "Curved Profiles", group = "Profile Settings")
same          = input.bool  (defval = false, title = "Plot Bull VP and Bear VP At Same X-Point", group = "Profile Settings")

showDelta     = input.bool  (defval = true, title = "Show Summed OI", group = "Summed Values", inline = "D") 
mergeDelta    = input.bool  (defval = false, title = "Merge Columns (Highest Values Displayed)", group = "Summed Values", inline = "D") 
deltaRows     = input.int   (defval = 50,   title = "Rows", group = "Summed Values", inline = "D", maxval = 500, minval = 2)
showVals      = input.bool  (defval = true, title = "Show Summed Values", group = "Summed Values")
dTranspMin    = input.int   (defval = 100, minval = 0, maxval = 100, title = "Lower Value Gradient Transparency"  , group = "Summed Values")
dTranspMax    = input.int   (defval = 100, minval = 0, maxval = 100, title = "Higher Value Gradient Transparency" , group = "Summed Values")
dTranspGre    = input.int   (defval = 25 , minval = 0, maxval = 100, title = "Highest Value Pure Transparency", group = "Summed Values")

poc           = input.bool  (defval = false, title = "Show POC", group = "POC Settings")
wid           = input.int   (defval = 1, title ="POC Width", minval = 1, group = "POC Settings")
upCol         = input.color (defval = #14D990, title = "Up OI Up Price Color", group = "Colors", inline = "Up3")
upCol2        = input.color (defval = #F2B807, title = "Dn OI Up Price Color", group = "Colors", inline = "Up3")
dnCol         = input.color (defval = #6929F2, title = "Up OI Dn Price Color", group = "Colors", inline = "Dn3")
dnCol2        = input.color (defval = #F24968, title = "Dn OI Dn Price Color", group = "Colors", inline = "Dn3")

transp        = input.int   (defval = 80, minval = 0, maxval = 100, group = "Colors", title = "Pofile Fill Transparency")
transp2       = input.int   (defval = 0 , minval = 0, maxval = 100, group = "Colors", title = "Pofile Line Transparency")
transp3       = input.int   (defval = 25, minval = 0, maxval = 100, group = "Colors", title = "POC Line Transparency")
transp4       = input.int   (defval = 90, minval = 0, maxval = 100, group = "Colors", title = "Non-VA Fill Transparency")
left          = chart.left_visible_bar_time, right = chart.right_visible_bar_time


if not mergeDelta 
    deltaRows := math.min(deltaRows, 125)

var rightIndex = 0, var leftIndex = 0

if time > left and leftIndex == 0
    leftIndex := bar_index - 1

if time == right 
    rightIndex := bar_index


req() => 
    
    cont = switch str.contains(syminfo.ticker, ".P")
        
        true => syminfo.ticker + "_OI"
        =>      str.endswith(syminfo.ticker, "USDT") ? syminfo.ticker + ".P_OI" : syminfo.ticker + "T.P_OI"

    switch syminfo.type

        "crypto" =>          cont
        =>                   string(na)



oi = request.security(req(), timeframe.period, close - close[1] , ignore_invalid_symbol = false)


if curve 
    ROWS := 60 


type dataStore

    matrix <float> timeVolMat 
    matrix <float> HLmat
    array  <int>   retArr 
    matrix <line>  pocs 
    matrix <float> tickLevels
    array  <float> priceArr

var data = dataStore.new(matrix.new<float>(2, 0), matrix.new<float>(2, 0), array.new_int(), matrix.new<line>(4, 0), priceArr = array.new_float()),                                                                                                                                                                       //kioseff
tLevels  = dataStore.new(tickLevels = matrix.new<float>(5, ROWS, 0.0)) 

method Float (int id) => float(id)

method double_binary_search_leftmost_reg(array <float> id, i) =>


    n  = id .binary_search_leftmost  (data.HLmat.get(0, i))
    n1 = id .binary_search_leftmost  (data.HLmat.get(1, i))

    [n, n1]


method double_binary_search_delta(array<float>id, i, val, val2) => 

    n  = id.binary_search_rightmost(val )
    n1 = id.binary_search_leftmost (val2)

    [n, n1]

method effSwitchReg(matrix<float> id, i, x, div) => 

    if data.priceArr.get(i) == 1

        switch data.retArr.get(i)

            1  => id.set(1, x, id.get(1, x) + data.timeVolMat.get(1, i) / div)
            -1 => id.set(2, x, id.get(2, x) + math.abs(data.timeVolMat.get(1, i) / div))

    if data.priceArr.get(i) == -1 

        switch data.retArr.get(i)

            1  => id.set(3, x, id.get(3, x) + data.timeVolMat.get(1, i) / div)
            -1 => id.set(4, x, id.get(4, x) + math.abs(data.timeVolMat.get(1, i) / div))


lineDraw(y, col) => 

    line.new(left, y, right, y, color = color.new(col, transp3), width =wid , xloc = xloc.bar_time)

method drawPOC(matrix<line>id, idrow, newRowUp, newRowUp2, newRowDn, newRowDn2) => 

    maxUp = idrow.get(newRowUp.indexof(newRowUp.max())), maxUp2 = idrow.get(newRowUp2.indexof(newRowUp2.max()))
    maxDn = idrow.get(newRowDn.indexof(newRowDn.max())), maxDn2 = idrow.get(newRowDn2.indexof(newRowDn2.max()))

    if poc 

        id.add_col(0, array.from(

             lineDraw(maxUp, upCol), lineDraw(maxUp2, upCol2), 
             lineDraw(maxDn, dnCol), lineDraw(maxDn2, dnCol2)))


method setLevels(matrix<float> id) =>

    rows = math.abs(data.HLmat.max() - data.HLmat.min()) / (ROWS - 1)

    for i = 0 to ROWS - 1
        id.set(0, i, data.HLmat.min() + rows * i)

method createPoly(matrix<chart.point> id, array<float> idrow) =>

    index = switch same
        
        false => rightIndex
        =>       leftIndex
    
    if showBull
        polyline.new(id.row(0), fill_color = color.new(upCol , transp), line_color  = color.new(upCol , transp2), curved = curve)
    if showBull2
        polyline.new(id.row(1), fill_color = color.new(upCol2, transp), line_color  = color.new(upCol2, transp2), curved = curve)
    if showBear
        polyline.new(id.row(2), fill_color = color.new(dnCol , transp), line_color  = color.new(dnCol , transp2), curved = curve)
    if showBear2
        polyline.new(id.row(3), fill_color = color.new(dnCol2, transp), line_color  = color.new(dnCol2, transp2), curved = curve)


norm(newRowUp, newRowDn, newRowUp2, newRowDn2, oldMin, oldMax, newRange) => 

    calcs = matrix.new<float>(4, 0)

    for i = 0 to ROWS - 1

        calcUp  = 1 + ((newRowUp .get(i) - oldMin) / (oldMax - oldMin)) * (newRange - 1)
        calcUp2 = 1 + ((newRowUp2.get(i) - oldMin) / (oldMax - oldMin)) * (newRange - 1)

        calcDn  = 1 + ((newRowDn .get(i) - oldMin) / (oldMax - oldMin)) * (newRange - 1)
        calcDn2 = 1 + ((newRowDn2.get(i) - oldMin) / (oldMax - oldMin)) * (newRange - 1)

        calcs.add_col(calcs.columns(), array.from(calcUp, calcUp2, calcDn, calcDn2))

    [calcs.row(0), calcs.row(1), calcs.row(2), calcs.row(3)]

method findValue(matrix <float> id, id2, highest, r1, r2) =>

    for i = 1 to id2.size() - 1
    
        slice = id2.slice(math.max(highest - i, 0), math.min(highest + i + 1, id2.size()))
        if slice.sum() / id2.sum() >= cumu / 100
            
            id.set(r1, 0, math.max(highest - i, 0))
            id.set(r2, 0, math.min(highest + i, id2.size() - 1))
            break

method pushShift (array<chart.point> id, valX, valY1, valY2) => 

    id.push   (chart.point.from_index(valX, valY1))
    id.unshift(chart.point.from_index(valX, valY2))
    

var maxVals = matrix.new<int>(4, 1, 0)


boxDraw(left, top, right, bottom, txt) => 

    txtBox = switch showVals 

        true => str.tostring(txt, format.volume)
        =>      string(na) 


    box.new(left, top, right, bottom, text = txtBox,
         text_color = chart.fg_color),

method appendSums (matrix <box> id, id2, i, startBox2, startBox, endBox, endBox2, middle, calc, calc2, sumUU, sumUD, sumDU, sumDD)  =>

    if not mergeDelta

        id.add_col(id.columns(), array.from(

             boxDraw(startBox2, calc, startBox, calc2, sumUU),
             boxDraw(startBox , calc, middle  , calc2, sumUD),
             boxDraw(middle   , calc, endBox  , calc2, sumDU), 
             boxDraw(endBox   , calc, endBox2 , calc2, sumDD)
             ))

    else 

        if i.Float().equal(0)

            for x = 0 to 2 

                id .remove_row()

        id.add_col(id.columns(), array.from(

             boxDraw(startBox, calc, endBox, calc2, math.max(sumUU, sumUD, sumDU, sumDD))))
        
        id2.push(math.max(sumUU, sumUD, sumDU, sumDD))

method setCoords(matrix<chart.point> id, newRowUp, newRowUp2, newRowDn, newRowDn2, oldMin, oldMax, newRange, timeRow, idrow) =>

    detPoc  = matrix.new<int>(8, 1) 

    highestUp = newRowUp.indexof(newRowUp.max()), highestUp2 = newRowUp2.indexof(newRowUp2.max())
    highestDn = newRowDn.indexof(newRowDn.max()), highestDn2 = newRowDn2.indexof(newRowDn2.max())

    detPoc.findValue(newRowUp , highestUp , 0, 1), detPoc.findValue(newRowUp2, highestUp2, 2, 3)
    detPoc.findValue(newRowDn,  highestDn , 4, 5), detPoc.findValue(newRowDn2, highestDn2, 6, 7)
    
    coordsUpUpVal  = array.new<chart.point>(), coordsUpDnVal  = array.new<chart.point>()
    coordsUpUpVal2 = array.new<chart.point>(), coordsUpDnVal2 = array.new<chart.point>()

    coordsDnUpVal  = array.new<chart.point>(), coordsDnDnVal  = array.new<chart.point>()
    coordsDnUpVal2 = array.new<chart.point>(), coordsDnDnVal2 = array.new<chart.point>()

    [calcUp, calcUp2, calcDn, calcDn2] = norm(newRowUp, newRowUp2, newRowDn, newRowDn2, oldMin, oldMax, newRange)

    for i = 0 to newRowUp.size() - 1

        newSwitchUp      = math.round(leftIndex + offset + calcUp.get(i))
        newXUp           = math.max(newSwitchUp, math.round(leftIndex + offset))

        newSwitchUp2     = math.round(leftIndex + offset + calcUp2.get(i))
        newXUp2          = math.max(newSwitchUp2, math.round(leftIndex + offset))

        [newSwitchDn, newSwitchDn2] = switch same 

            false => [math.round(rightIndex - offset - calcDn.get(i)), math.round(rightIndex - offset - calcDn2.get(i))]
            =>       [math.round(leftIndex  + offset + calcDn.get(i)), math.round(leftIndex  + offset + calcDn2.get(i))]

        [newXDn, newXDn2] = switch same 

            false => [math.min(newSwitchDn, math.round(rightIndex - offset)), math.min(newSwitchDn2, math.round(rightIndex - offset))]
            =>       [math.max(newSwitchDn, math.round(leftIndex  + offset)), math.max(newSwitchDn2, math.round(leftIndex  + offset))]

        if detPoc.get(0, 0).Float().not_equal(0)

            if i.Float().under_equal(detPoc.get(0, 0) - 1)

                coordsUpDnVal.push(chart.point.from_index(newXUp, idrow.get(i)))

        if detPoc.get(1, 0).Float().not_equal(newRowUp.size() - 1)

            if i.Float().over_equal(detPoc.get(1, 0) + 1)

                coordsUpUpVal.push(chart.point.from_index(newXUp, idrow.get(i)))

        if i.Float().over_equal(detPoc.get(0, 0)) and i.Float().under_equal(detPoc.get(1, 0))
    
            id.set(0, i, chart.point.from_index(newXUp, idrow.get(i)))
            maxVals.set(0, 0, math.max(maxVals.get(0, 0), int(calcUp.get(i))))
    
        if detPoc.get(2, 0).Float().not_equal(0)

            if i.Float().under_equal(detPoc.get(2, 0) - 1)

                coordsUpDnVal2.push(chart.point.from_index(newXUp2, idrow.get(i)))


        if detPoc.get(3, 0).Float().not_equal(newRowUp2.size() - 1)

            if i.Float().over_equal(detPoc.get(3, 0) + 1)

                coordsUpUpVal2.push(chart.point.from_index(newXUp2, idrow.get(i)))


        if i.Float().over_equal(detPoc.get(2, 0)) and i.Float().under_equal(detPoc.get(3, 0))

            id.set(1, i, chart.point.from_index(newXUp2, idrow.get(i)))
            maxVals.set(1, 0, math.max(maxVals.get(1, 0), int(calcUp2.get(i))))
        
   
        if detPoc.get(4, 0).Float().not_equal(0)

            if i.Float().under_equal(detPoc.get(4, 0) - 1)

                coordsDnDnVal.push(chart.point.from_index(newXDn, idrow.get(i)))


        if detPoc.get(5, 0).Float().not_equal(newRowDn.size() - 1)

            if i.Float().over_equal(detPoc.get(5, 0) + 1)

                coordsDnUpVal.push(chart.point.from_index(newXDn, idrow.get(i)))

        if i.Float().over_equal(detPoc.get(4, 0)) and i.Float().under_equal(detPoc.get(5, 0))

            id.set(2, i, chart.point.from_index(newXDn, idrow.get(i)))
            maxVals.set(2, 0, math.max(maxVals.get(2, 0), int(calcDn.get(i))))
   
        if detPoc.get(6, 0).Float().not_equal(0)

            if i.Float().under_equal(detPoc.get(6, 0) - 1)

                coordsDnDnVal2.push(chart.point.from_index(newXDn2, idrow.get(i)))


        if detPoc.get(7, 0).Float().not_equal(newRowDn2.size() - 1)

            if i.Float().over_equal(detPoc.get(7, 0) + 1)

                coordsDnUpVal2.push(chart.point.from_index(newXDn2, idrow.get(i)))

        if i.Float().over_equal(detPoc.get(6, 0)) and i.Float().under_equal(detPoc.get(7, 0))

            id.set(3, i, chart.point.from_index(newXDn2, idrow.get(i)))
            maxVals.set(3, 0, math.max(maxVals.get(3, 0), int(calcDn2.get(i))))
    

    index = switch same 

        false => rightIndex 
        =>       leftIndex

    coordsUpDnVal .pushShift(leftIndex, idrow.get(detPoc.get(0, 0)), idrow.min())
    coordsUpUpVal .pushShift(leftIndex, idrow.max(), idrow.get(detPoc.get(1, 0)))

    coordsUpDnVal2.pushShift(leftIndex, idrow.get(detPoc.get(2, 0)), idrow.min())
    coordsUpUpVal2.pushShift(leftIndex, idrow.max(), idrow.get(detPoc.get(3, 0)))

    coordsDnDnVal .pushShift(index,     idrow.get(detPoc.get(4, 0)), idrow.min())
    coordsDnUpVal .pushShift(index,     idrow.max(), idrow.get(detPoc.get(5, 0)))

    coordsDnDnVal2.pushShift(index,     idrow.get(detPoc.get(6, 0)), idrow.min())
    coordsDnUpVal2.pushShift(index,     idrow.max(), idrow.get(detPoc.get(7, 0)))


    id.add_col(id.columns(), 
             array.from( chart.point.from_index(leftIndex, idrow.get(detPoc.get(1, 0))), chart.point.from_index(leftIndex, idrow.get(detPoc.get(3, 0))), 
                         chart.point.from_index(index, idrow.get(detPoc.get(5, 0)))    , chart.point.from_index(index, idrow.get(detPoc.get(7, 0)))))
    id.add_col(0,
             array.from( chart.point.from_index(leftIndex, idrow.get(detPoc.get(0, 0))), chart.point.from_index(leftIndex, idrow.get(detPoc.get(2, 0))), 
                         chart.point.from_index(index, idrow.get(detPoc.get(4, 0)))    , chart.point.from_index(index, idrow.get(detPoc.get(6, 0)))))

    if showBull

        polyline.new(coordsUpDnVal,  fill_color = color.new(upCol, transp4),  line_color  = color.new(upCol, transp2), curved = curve)
        polyline.new(coordsUpUpVal,  fill_color = color.new(upCol, transp4),  line_color  = color.new(upCol, transp2), curved = curve)

    if showBull2

        polyline.new(coordsUpDnVal2, fill_color = color.new(upCol2, transp4), line_color  = color.new(upCol2, transp2), curved = curve)
        polyline.new(coordsUpUpVal2, fill_color = color.new(upCol2, transp4), line_color  = color.new(upCol2, transp2), curved = curve)

    if showBear
        
        polyline.new(coordsDnDnVal, fill_color = color.new(dnCol, transp4), line_color  = color.new(dnCol, transp2), curved = curve)
        polyline.new(coordsDnUpVal, fill_color = color.new(dnCol, transp4), line_color  = color.new(dnCol, transp2), curved = curve)

    if showBear2
        
        polyline.new(coordsDnDnVal2, fill_color = color.new(dnCol2, transp4), line_color  = color.new(dnCol2, transp2), curved = curve)
        polyline.new(coordsDnUpVal2, fill_color = color.new(dnCol2, transp4), line_color  = color.new(dnCol2, transp2), curved = curve)


calcDelta(idrow, newRowUp, newRowUp2, newRowDn, newRowDn2) =>

    if showDelta 

        deltaBoxes  = matrix.new<box>(4, 0), start = data.HLmat.min(), highestVals = array.new_float()
 
        rows        = math.abs(data.HLmat.max() - start) / deltaRows
        middle      = math.round(math.avg(leftIndex, rightIndex))
 
        scale       = (rightIndex - leftIndex) / 30
        gradientMat = matrix.new<float>(4, 0)

        for i = 0 to deltaRows - 1
            
            calc = start + rows * i, calc2 = start + rows * (i + 1)

            [lx1, lx2] = idrow.double_binary_search_delta(i, calc, calc2)

            sumUU = 0.0, sumUD = 0.0, sumDU = 0.0, sumDD = 0.0, 

            for x = lx1 to lx2 

                sumUU += newRowUp.get(x), sumUD += newRowUp2.get(x)
                sumDU += newRowDn.get(x), sumDD += newRowDn2.get(x)

            gradientMat.add_col(gradientMat.columns(), array.from(sumUU, sumUD, sumDU, sumDD))

            startBox2 = math.round(middle - scale * 2.5 ), endBox2 = math.round(middle + scale * 2.5 )
            startBox  = math.round(middle - scale * 1.25), endBox  = math.round(middle + scale * 1.25)
        
            deltaBoxes.appendSums(highestVals, i, startBox2, startBox, endBox, endBox2, middle, calc, calc2, sumUU, sumUD, sumDU, sumDD)


        gradUp = gradientMat.max()
        gradDn = gradientMat.min()


        for i = 0 to deltaBoxes.columns() - 1

            colMat = gradientMat.col(i)
            colMax = colMat.max()

            if mergeDelta 

                finCol = switch colMat.indexof(colMax)
                    
                    0 => upCol 
                    1 => upCol2 
                    2 => dnCol 
                    3 => dnCol2

                
                colLoop = color.from_gradient(colMax, highestVals.min(), highestVals.max(), color.new(finCol, 75), color.new(finCol, 10))

                deltaBoxes.get(0, i).set_bgcolor     (colLoop)
                deltaBoxes.get(0, i).set_border_color(colLoop)

            else  

                for x = 0 to 3

                    finCol = switch 
                    
                        x.Float().equal(0) => upCol
                        x.Float().equal(1) => upCol2
                        x.Float().equal(2) => dnCol
                        =>                    dnCol2
    

                    boxBgcol = switch math.sign(gradientMat.get(x, i))
                    
                        1  => color.from_gradient(gradientMat.get(x, i), gradDn, gradUp, color.new(finCol, dTranspMin), color.new(finCol, dTranspMax))
                        =>    #00000000
    
                    switch gradientMat.get(x, i).equal(colMax) 
                    
                        false => deltaBoxes.get(x, i).set_bgcolor(boxBgcol), deltaBoxes.get(x, i).set_border_color(color.new(finCol , 50))
                        =>       deltaBoxes.get(x, i).set_bgcolor(color.new(finCol, dTranspGre)), deltaBoxes.get(x, i).set_border_color(finCol)
    

        if not mergeDelta

            simRow     = deltaBoxes.row(0)
            difference = simRow.last().get_bottom() - simRow.last().get_top()
            dbcols     = deltaBoxes.columns() - 1
// 
            for i = 0 to 3 

                [X, COLOR, TEXT] = switch 

                    i == 0 => [math.round(math.avg(simRow.last().get_left(), simRow.last().get_right())), upCol, "Up OI \n Up Price"]
                    i == 1 => [math.round(math.avg(deltaBoxes.get(1, dbcols).get_left(), deltaBoxes.get(1, dbcols).get_right())), upCol2, "Down OI \n Up Price"  ]
                    i == 2 => [math.round(math.avg(deltaBoxes.get(2, dbcols).get_left(), deltaBoxes.get(2, dbcols).get_right())), dnCol , "Up OI \n Down Price"  ]
                    =>        [math.round(math.avg(deltaBoxes.get(3, dbcols).get_left(), deltaBoxes.get(3, dbcols).get_right())), dnCol2, "Down OI \n Down Price"]

                label.new(X, simRow.last().get_bottom() + difference * 2, color = COLOR, textcolor = color.white, style = label.style_text_outline, text = TEXT, 
                                         size = size.tiny)


method update(matrix<float> id) => 

    if time >= left and time <= right

        data.timeVolMat.add_col(data.timeVolMat.columns(), array.from(time, oi))
        data.HLmat     .add_col(data.HLmat.columns(), array.from(high, low))
        data.retArr    .push(int(math.sign(oi)))
        data.priceArr  .push(int(math.sign(close - open)))

    if time == right

        id.setLevels()

        timeRow = data.timeVolMat.row(0)
            
        idrow = id.row(0)

        for i = 0 to data.retArr.size() - 1

            [lx1, lx2] = idrow.double_binary_search_leftmost_reg(i)

            div = math.abs(lx1 - lx2) + 1

            for x = lx1 to lx2 

                id.effSwitchReg(i, x, div)
        
        newRange = math.floor((rightIndex - leftIndex) / factor)
        newRowUp = id.row(1), newRowUp2 = id.row(2), newRowDn = id.row(3), newRowDn2 = id.row(4)

        oldMin   = math.min(newRowUp.min(), newRowDn.min())  
        oldMax   = math.max(newRowUp.max(), newRowDn.max())

        coordinates = matrix.new<chart.point>(4, newRowUp.size(), chart.point.from_index(na, na))
        coordinates.setCoords(newRowUp, newRowUp2, newRowDn, newRowDn2, oldMin, oldMax, newRange, timeRow, idrow)
        coordinates.createPoly(idrow)
        data.pocs  .drawPOC(idrow, newRowUp, newRowUp2, newRowDn, newRowDn2)
        calcDelta(idrow, newRowUp, newRowUp2, newRowDn, newRowDn2)

tLevels.tickLevels.update()

