Dynamic Liquidity Map [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("Vol Oi HeatMap", overlay = true, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500)

import PineCoders/Time/4 as pct
import RicardoSantos/MathOperator/2 // Thanks again Ricardo!!
import HeWhoMustNotBeNamed/arraymethods/1 // Lord V

oib        = input.bool(defval = false, title = "Use OI For Heatmap")
show       = input.bool(defval = true,  title = "Show Delta")
extend     = input.bool(defval = false, title = "Extend Unviolated Boxes")
showB      = input.bool(defval = true,  title = "Show Borders")
rowscol    = input.int(title = "Rows x Columns", minval = 1, maxval = 22, defval = 22) - 1
fixedRange = input.bool(defval = false, title = "Use Fixed Range", group = "Fixed Range", inline = "FR")
frt        = input.time(timestamp("20 Jul 2023 00:00 +0300") ,title = "Start" ,group = "Fixed Range", inline = "FR"), cond = timeframe.period == "W" or timeframe.period == "M" , TIME = float(time), TIME1 = float(time[1]), rawChange = close - close[1], var float div = 0, var int stime = 0, tf = -1.
pos        = input.color(defval = #6929F2, title = "Pos. Color", inline = "Color")
neg        = input.color(defval = #F24968, title = "Neg. Color", inline = "Color")


determine (a, b, c) =>

    switch a 
        true => b 
        =>      c

expression() => 

    [volume, close, high, low, close - close[1], time]

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


if oib and syminfo.type != "crypto"

    runtime.error("No Open Interest Data Available")

strMax( float ) =>

    mult = timeframe.multiplier / 9 
    
    switch cond
        
        false => str.tostring(math.max(1, math.round(timeframe.multiplier - mult * float ))) 
        =>       na

strChange() => 

    switch 
        timeframe.period == "W" => "D"
        timeframe.period == "M" => "M"
        =>                   na


[oic, oih, oil, oio, trueOic    ] =  request.security_lower_tf(syminfo.ticker + "_OI", "1",
                                     [close-close[1], high, low, open, close], true )

req() => 
    
    cont = switch str.contains(syminfo.ticker, "T.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)


[vol0 , clo0 ,  hi0 , lo0 , clocalc0 , ltf0 ] = request.security_lower_tf(syminfo.tickerid,  "1",  expression(), true)
[vol12, clo12,  hi12, lo12, clocalc12, ltf12] = request.security_lower_tf(syminfo.tickerid,  strMax (1),  expression(), true)
[vol13, clo13,  hi13, lo13, clocalc13, ltf13] = request.security_lower_tf(syminfo.tickerid,  strMax (2),  expression(), true)
[vol14, clo14,  hi14, lo14, clocalc14, ltf14] = request.security_lower_tf(syminfo.tickerid,  strMax (3),  expression(), true)
[vol15, clo15,  hi15, lo15, clocalc15, ltf15] = request.security_lower_tf(syminfo.tickerid,  strMax (4),  expression(), true)
[vol16, clo16,  hi16, lo16, clocalc16, ltf16] = request.security_lower_tf(syminfo.tickerid,  strMax (5),  expression(), true)
[vol17, clo17,  hi17, lo17, clocalc17, ltf17] = request.security_lower_tf(syminfo.tickerid,  strMax (6),  expression(), true)
[vol18, clo18,  hi18, lo18, clocalc18, ltf18] = request.security_lower_tf(syminfo.tickerid,  strMax (7),  expression(), true)
[vol19, clo19,  hi19, lo19, clocalc19, ltf19] = request.security_lower_tf(syminfo.tickerid,  strMax (8),  expression(), true)
[vol20, clo20,  hi20, lo20, clocalc20, ltf20] = request.security_lower_tf(syminfo.tickerid,  strChange(), expression(), true)
[vol21, clo21,  hi21, lo21, clocalc21, ltf21] = request.security_lower_tf(syminfo.tickerid,  strChange(), expression(), true)
oi0                                           = request.security_lower_tf(req(),  "1",  rawChange, true)
oi12                                          = request.security_lower_tf(req(),  strMax (1),  rawChange, true)
oi13                                          = request.security_lower_tf(req(),  strMax (2),  rawChange, true)
oi14                                          = request.security_lower_tf(req(),  strMax (3),  rawChange, true)
oi15                                          = request.security_lower_tf(req(),  strMax (4),  rawChange, true)
oi16                                          = request.security_lower_tf(req(),  strMax (5),  rawChange, true)
oi17                                          = request.security_lower_tf(req(),  strMax (6),  rawChange, true)
oi18                                          = request.security_lower_tf(req(),  strMax (7),  rawChange, true)
oi19                                          = request.security_lower_tf(req(),  strMax (8),  rawChange, true)
oi20                                          = request.security_lower_tf(req(),  strChange(), rawChange, true)
oi21                                          = request.security_lower_tf(req(),  strChange(), rawChange, true)


type dotValues 
    
    float [] Levels
    matrix <float> TickVol
    matrix <float> misc

var dotMat = dotValues.new(
             
             array.new_float(),
             matrix.new<float>(1, 4000, 0), 
             matrix.new<float>(3, 0)
             
             )

method tickSet(matrix<float> id, float value, int column, int calc) => 

    int  = int(dotMat.misc.row(0).first())

    id.set(int, column, nz(nz(id.get(int, column)) + (value / math.max(1, (calc)))))



if barstate.isfirst 
    dotMat.misc.add_col(dotMat.misc.columns(), array.from(0, 0, 1e8))

valArr = array.from( vol12.size(), vol13.size(),
                     vol14.size(), vol15.size(),
                     vol16.size(), vol17.size(),
                     vol18.size(), vol19.size(), vol0.size()
 )

if fixedRange and TIME.over_equal(frt) 
    for i = valArr.size() - 1 to 0
        if valArr.get(i).float().over(0)
            tf := i
            break
  

[VOL, CLO, HI, LO, CLOCALC, LTF] = if fixedRange and TIME.over_equal(frt) and not cond
    
    switch 

        vol0.size().float().over(0) =>      [determine(oib, oi0 , vol0 ), clo0 ,  hi0 , lo0 , clocalc0 , ltf0 ]
        tf.equal(0)                     =>  [determine(oib, oi12, vol12), clo12,  hi12, lo12, clocalc12, ltf12]
        tf.equal(1)                     =>  [determine(oib, oi13, vol13), clo13,  hi13, lo13, clocalc13, ltf13]
        tf.equal(2)                     =>  [determine(oib, oi14, vol14), clo14,  hi14, lo14, clocalc14, ltf14]
        tf.equal(3)                     =>  [determine(oib, oi15, vol15), clo15,  hi15, lo15, clocalc15, ltf15]
        tf.equal(4)                     =>  [determine(oib, oi16, vol16), clo16,  hi16, lo16, clocalc16, ltf16]
        tf.equal(5)                     =>  [determine(oib, oi17, vol17), clo17,  hi17, lo17, clocalc17, ltf17]
        tf.equal(6)                     =>  [determine(oib, oi18, vol18), clo18,  hi18, lo18, clocalc18, ltf18]
        tf.equal(7)                     =>  [determine(oib, oi19, vol19), clo19,  hi19, lo19, clocalc19, ltf19]
        tf.equal(-1)                    =>  [array.from(volume), array.from(close), array.from (high) , array.from(low),array.from (close - close[1]), array.from (time)]


else 
    
    switch 

        not cond and not fixedRange => [determine(oib, oi0, vol0 ), clo0 ,  hi0 , lo0 , clocalc0 , ltf0 ] 
        timeframe.period == "W"     => [determine(oib, oi20, vol20), clo20,  hi20, lo20, clocalc20, ltf20]
        timeframe.period == "M"     => [determine(oib, oi21, vol21), clo21,  hi21, lo21, clocalc21, ltf21]

row                                 =   determine(fixedRange, dotMat.TickVol.rows(), rowscol + 1)


if fixedRange ? TIME.over_equal(frt) : VOL.size().float().over(0)

    if fixedRange

        var calc = last_bar_index - bar_index

        if calc.float().under_equal ( rowscol + 1)

            runtime.error(   "Not enough bar data in the fixed range for the 
                             requested heatmap size (" + str.tostring(rowscol) +"). Reduce the number of rows/columns 
                             in the settings, or lengthen the fixed range period.")


    dotMat.misc.set(1, 0, math.max(dotMat.misc.get(1, 0), high))
    dotMat.misc.set(2, 0, math.min(dotMat.misc.get(2, 0), low ))

    if stime.float().equal(0)
            
        div   := (last_bar_time - time) / (rowscol + 1), stime := time
        
        for i = 0 to 1000
            
            dotMat.Levels.push(close * (1 + (i/1000)))
            dotMat.Levels.push(close * (1 - (i/1000)))
        
        dotMat.Levels.sort(order.ascending)

    for i = 0 to math.min(VOL.size(), CLOCALC.size()) - 1

        highest =        dotMat.Levels.binary_search_leftmost  (HI.get(i))
        lowest  =        dotMat.Levels.binary_search_rightmost (LO.get(i))

        for x = lowest to highest
            
            volx = switch math.sign(CLOCALC.get(i))
                
                1 => determine(oib, VOL.get(i), VOL.get(i)      )
                =>   determine(oib, VOL.get(i), VOL.get(i)  * -1)

            dotMat.TickVol.tickSet(volx      , x, math.abs(highest - lowest) + 1)
            
    for i = 0 to rowscol
        
        if TIME.over_equal(math.round(stime + (div * (i + 1))))
            if TIME1.under(math.round(stime + (div * (i + 1))))
             
                dotMat.misc   .set    (0, 0, dotMat.misc.get(0, 0) + 1)
                dotMat.TickVol.add_row(dotMat.TickVol.rows(), dotMat.TickVol.row(dotMat.TickVol.rows() - 1))

                break
                       
orientation = matrix.new<float>(row, row)

method double_binary_search_leftmost(array <float> id, column)                =>

    n  = id.binary_search_leftmost  (orientation.get(0, column))
    n1 = id.binary_search_rightmost (orientation.get(1, column))

    [n, n1]

method finSetVol(matrix <float> id, int i, int row, int start, int end)       =>
    
    for x = start to end 
        id.set (row, i, id.get(row, i) + dotMat.TickVol.get(i, x))


if barstate.islast

    box.all.flush(), line.all.flush(), label.all.flush()

    calc = (dotMat.misc.get(1, 0) - dotMat.misc.get(2, 0)) / row 
    grid =        matrix.new<box>(row, row)

    for i = 0 to row - 1
        for x = 0 to row - 1
                                    
            grid.set(x, i,
                     box.new(
                                     math.round(stime + (div * i)),
                                     dotMat.misc.get(2, 0) + (calc * (x + 1)), 
                                     math.round(stime + (div * (i + 1))), 
                                     dotMat.misc.get(2, 0) + calc *     (x),
                                     xloc         = xloc.bar_time,  
                                     border_color = #00000000,
                                     border_width = 1,
                                     bgcolor      = #00000000
       
                                 ))


    for i = 0 to row - 1
        
        orientation.set(0, i, grid.get(i, 0).get_top   ())
        orientation.set(1, i, grid.get(i, 0).get_bottom())

    preSet = matrix.new<int>(2, 1000, 0)
    
    for i = 0 to row - 1
        
        [up, dn] = dotMat.Levels.double_binary_search_leftmost (i)
        
        preSet.set(0, i, up)
        preSet.set(1, i, dn)

    finTick  = matrix.new<float>(row, row, 0)
    finTick2 = matrix.new<float>    (row, 1, 0)

    for i = 0 to row - 1
        for x = 0 to row - 1
            finTick .finSetVol(i, x, preSet.get(0, x), preSet.get(1, x))
            

    colMat = matrix.new<color>(row, row)
    
    for i = 0 to grid.rows() - 1
        for x = 0 to grid.columns() - 1

            datax = finTick .get(i, x), datamin = finTick .min(),
                         datamax = finTick .max()
            
            col           = color.new(color.gray, 80), bcol  = color.new(color.gray, 80)
            col2          = color.new(color.gray, 80), calcx =   math.sign    (datax)
            
            col := switch calcx

                0  =>    color.new(color.gray, 80)
                1  =>    color.from_gradient(datax, 0, datamax, color.new(pos, 90), color.new(pos, 50)) 
                -1 =>    color.from_gradient(datax, datamin, 0, color.new(neg , 50), color.new(neg , 90))

            bcol := switch calcx 
                 
                0  =>    color.new(color.gray, 80)
                1  =>    #6929F2
                -1 =>    #F24968


            grid.get(i, x).set_bgcolor(col == color.new(color.gray, 80) ? col2 : col)

            if showB
                grid.get(i, x).set_border_color(col == color.new(color.gray, 80) ? na : bcol)


            if show 
                if finTick.get(i, x).not_equal(0)
                    
                    grid.get(i, x).set_text_color(color.white)
                    grid.get(i, x).set_text(str.tostring(finTick.get(i, x), format.volume))


            if finTick.get(i, x).equal(0)
                grid.get(i, x).set_bgcolor(#00000000)
            
            if extend and x.float().over(0)
                if finTick.get(i, x).equal(finTick.get(i, x - 1))
                    
                    grid.get(i, x)    .set_left(grid.get(i, x - 1).get_left())
                    grid.get(i, x - 1).set_bgcolor      (#00000000)
                    grid.get(i, x - 1).set_border_color (#00000000)
                    grid.get(i, x - 1).set_text_color   (#00000000)
            
            if x.float().equal(grid.columns() - 1)
                grid.get(i, x).set_right(pct.timeFrom("bar", 5, "chart"))
    
    if fixedRange
        line.new(frt, high, frt, low, xloc = xloc.bar_time, color = chart.fg_color, width = 2, extend = extend.both)    

        if not extend 

            if box.all.size() < math.pow(rowscol, 2)
                var tab = table.new(position.top_right, 4, 4, frame_color = color.white, frame_width = 1, border_color = color.white, border_width = 1)
                tab.cell(0, 0, text = "Time Gaps Detected\n True Reducing the Rows x Columns", text_color = color.white, text_size = size.small)