Market Sessions and TPO (+Forecast)
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// Â© Mxwll Mxwll Capital

//@version=5
indicator("Mxwll Sessions" , overlay = true , max_bars_back = 500, max_lines_count  = 500, max_boxes_count  = 500, max_labels_count = 500)


proj        = input.string(defval = "Bars", title = "Show Projection", options = ["Bars", "Line", "None"])
profileType = input.string(defval = "Bars", options = ["Letters", "Bars", "None"], title = "Profile Type")
mult        = input.int   (defval = 1, title = "Profile Bar Multiplier", minval = 1)
use         = input.bool  (defval = true,  title = "Use ATR in Projection")
removeBor   = input.bool  (defval = false, title = "Remove Box Border Color")
prep        = input.bool  (defval = true, title = "Show Pre-Market")
posp        = input.bool  (defval = true, title = "Show Post-Market")
poc         = input.bool  (defval = true, title = "Show Naked POCs")
showNY      = input       (true, title = '', inline = 'NY', group = 'New York')
NYcol       = input.color (#6929F2, title = '', inline = 'NY', group = 'New York')
showLONDON  = input       (true, title = '', inline = 'LONDON', group = 'LONDON')
LONDONcol   = input.color (#F24968, title = '', inline = 'LONDON', group = 'LONDON')
showAS      = input       (true, title = '', inline = 'ASIA', group = 'ASIA')
AScol       = input.color (#39FF14, title = '', inline = 'ASIA', group = 'ASIA')

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)

oi = request.security(req(), timeframe.period, close - close[1]), var timeLevels = matrix.new<float>(2, 0)

method conditional_add(matrix <float> id) => 
    
    if timeframe.multiplier >= 30
        timeLevels.add_col(timeLevels.columns(), array.from(high, low))
    
    else 
        
        max = 0., min = 1e8
        if timeframe.change("30")
            for i = 0 to math.floor(30 / timeframe.multiplier)
               
                max := math.max(high[i], max)
                min := math.min(low [i], min)
            
            id.add_col(timeLevels.columns(), array.from(max, min))

timeLevels.conditional_add()

type objects 

    box   sessionBox 
    label sessionLabel
    box   preSessionBox
    label preSessionLabel
    box   postSessionBox
    label postSessionLabel 

var objNY = matrix.new<objects>(0, 6), var objLON = matrix.new<objects>(0, 6),
                 var objAS = matrix.new<objects>(0, 6)


method newObject(matrix <objects> id, titleTime, title, color col, bool condition)=>
        
    if condition and last_bar_index - bar_index <= 7000
        
        var timE = 0, var HIGH = high, var LOW = low, var vol = 0., var delta = 0., var oiD = 0.

        if titleTime > titleTime[1] 

            timE := bar_index, HIGH := high, LOW := low, vol := volume, oiD := oi
            calc = math.sign(close - open)
            
            delta := switch calc 
                
                -1 => -volume
                =>     volume 

            id.add_row(id.rows(),

                     array.from(

                             objects.new(sessionBox   =  box.new(bar_index, HIGH, bar_index, LOW, 
                                                         bgcolor      = color.new(col, 90), 
                                                         border_color = removeBor ? na : col 
                                                         )), 

                             objects.new(sessionLabel = label.new(timE, HIGH, title, 
                                                         textcolor = col, 
                                                         color     = #ffffff00, 
                                                         size      = size.tiny)),

                             objects.new(preSessionBox   = box  (na)), 
                             objects.new(preSessionLabel = label(na)), 
                             objects.new(postSessionBox  = box  (na)), 
                             objects.new(postSessionLabel= label(na)) 

                                                         ))

        if titleTime and titleTime == titleTime[1]

            HIGH := math.max(high, HIGH), vol += volume, oiD += oi
            LOW  := math.min(low , LOW) , calc = math.sign(close - open)

            switch calc
                
                -1 => delta -= volume 
                =>    delta += volume

            if id.rows() > 0
                if not na(id.get(id.rows() - 1, 0))

                    id.get(id.rows() - 1, 0).sessionBox.set_top(HIGH)
                    id.get(id.rows() - 1, 0).sessionBox.set_rightbottom(bar_index, LOW)

                    rang1   =  id.get(id.rows() - 1, 0).sessionBox.get_top() - id.get(id.rows() - 1, 0) .sessionBox.get_bottom()
                    rang2   = (id.get(id.rows() - 1, 0).sessionBox.get_top() + id.get(id.rows() - 1, 0).sessionBox.get_bottom()) / 2

                    finrang = rang1 / rang2 * 100

                    if not na(id.get(id.rows() - 1, 1))

                        id.get(id.rows() - 1, 1).sessionLabel.set_xy(int(math.avg(timE, bar_index)), HIGH)
                        id.get(id.rows() - 1, 1).sessionLabel.set_text(title         + "\nRange: "      +
                                               str.tostring(finrang, format.percent) + "\nVol: "        +
                                               str.tostring(vol    , format.volume ) + "\n∆: "          +
                                               str.tostring(delta  , format.volume ) + "\nOI: "         + 
                                               str.tostring(oiD    , format.mintick))
signTime(TIME) => 
    
    int(math.sign(nz(time(timeframe.period, TIME, "America/New_York"))))


[preny, prel, pres]  = switch 
    
    timeframe.multiplier <= 5 =>  ["0630-0931", "0005-0301", "1900-2001"]
    timeframe.multiplier <= 30 => ["0630-0931", "0000-0301", "1900-2001"]
    =>                            ["0600-1000", "0000-0301", "1900-2001"]

[nyT, lonT] = switch 
    
    timeframe.multiplier <= 30 => ['0930-1601', '0300-1131']
    timeframe.multiplier =>       ['0900-1601', '0300-1101']

nytrue     = signTime(nyT    ),      prenytrue   = signTime(preny)
lontrue    = signTime(lonT   ),      prelontrue  = signTime(prel )
astrue     = signTime('2000-0501' ), preaustrue  = signTime(pres )

objNY .newObject(nytrue  ,"New York" , NYcol, showNY), objLON.newObject(lontrue , "London", LONDONcol, showLONDON),
                         objAS .newObject(astrue ,"Asia", AScol, showAS)


method preSessionDraw(matrix <objects> id, int time1, int time2, int time3, color col, bool condition, bool activate) =>
    

    if prep
    
        if last_bar_index - bar_index <= 5000 
             and condition 
             and activate

            if not na(id.get(id.rows() - 1, 0)) 

                var preBar = 0

                preBar := switch not time2[1] and time2

                    true => bar_index
                    =>      preBar == 0 ? bar_index : preBar

                if not time1[1] and time1
                    id.set(id.rows() - 1, 2, 
                                 objects.new(preSessionBox = 
                                         box.new(preBar, high, bar_index, low, border_style = line.style_dashed, 
                                                     border_color = removeBor ? na : col, 
                                                     bgcolor = color.new(col, 90)
                                                     )))
                    id.set(id.rows() - 1, 3, 
                                 objects.new(preSessionLabel = 
                                     label.new(bar_index, high, color = #00000000, text = "OPEN", 
                                                     textcolor = color.white, 
                                                     size      = size.tiny, 
                                                     style     = label.style_label_down
                                                     )))

                if id.rows() > 0 and time3 == 1 
                    if not na(id.get(id.rows() - 1, 0))
                        id.get(id.rows() - 1, 2).preSessionBox  .set_top   (id.get(id.rows() - 1, 0).sessionBox.get_top())
                        id.get(id.rows() - 1, 2).preSessionBox  .set_bottom(id.get(id.rows() - 1, 0).sessionBox.get_bottom())
                        id.get(id.rows() - 1, 3).preSessionLabel.set_y     (id.get(id.rows() - 1, 0).sessionBox.get_top())


[nyP, lonP, nyP1, lonP1] = switch 
    
    timeframe.multiplier <= 30 => ['0930-0931', '0300-0301', "0931-1601", "0301-1130"]
    timeframe.multiplier =>       ['0900-0901', '0300-0301', "0901-1601", "0301-1100"]


objNY .preSessionDraw(signTime(nyP),  signTime(preny), signTime(nyP1), NYcol, showNY, true)
objLON.preSessionDraw(signTime(lonP), signTime(prel ), signTime(lonP1), LONDONcol, showLONDON, true)
objAS .preSessionDraw(signTime("2000-2001"), signTime(pres ), signTime("2001-0501"), AScol, showAS, false)


var string [] strx = array.from(
    
     " A",
     " B",
     " C",
     " D",
     " E",
     " F",
     " G",
     " H",
     " I",
     " J",
     " K",
     " L",
     " M",
     " N",
     " O",
     " P",
     " Q",
     " R",
     " S",
     " T",
     " U",
     " V",
     " W",
     " X",
     " Y",
     " Z",
     " a",
     " b",
     " c",
     " d",
     " e",
     " f",
     " g",
     " h",
     " i",
     " j",
     " k",
     " l",  
     " m",
     " n",
     " o",
     " p",
     " q",
     " r",
     " s",
     " t",
     " u",
     " v",
     " w",
     " x",
     " y",
     " z"

     )


if barstate.isfirst and profileType == "Letters"

    sX = ""

    for i = 0 to 1275
        
        index = i % 52
        sX    := strx.get(index) 
        
        strx.push(sX)

var POCLINES = array.new_line(), earliest = array.new_int(), calcTime = 600 / timeframe.multiplier + 5


method postSessionDraw(matrix <objects> id, int time1, int time2, color col, bool condition, bool activate) => 

    level = array.new_float(), lab = array.new_string(), bsize = array.new_int()
    
    if last_bar_index - bar_index <= 5000 
         and condition 

        if not na(id.get(id.rows() - 1, 0))
            if not time1[1] and time1

                if profileType != "None"

                    calc = (id.get(id.rows() - 1, 0).sessionBox.get_top() - id.get(id.rows() - 1, 0).sessionBox.get_bottom()) / 20
                    index = 0, 
                    for i = 0 to 20

                        level.push(id.get(id.rows() - 1, 0).sessionBox.get_bottom() + (calc * i))

                        switch profileType

                            "Letters" => lab.push("") , bsize.push(0)
                            "Bars"    => bsize.push(0), index := 1 

                    increment = switch 
                        
                        timeframe.multiplier >= 30 => 1
                        =>                            30 / timeframe.multiplier

                    for i = math.ceil(id.get(id.rows() - 1, 0).sessionBox.get_left() / increment) to timeLevels.columns() - 1

                        highest =        level.binary_search_leftmost (timeLevels.row(0).get(i))
                        lowest  =        level.binary_search_rightmost(timeLevels.row(1).get(i))
  
                        for x = lowest to highest 

                            switch profileType

                                "Letters" => lab  .set(x, lab.get(x) + strx.get(i - math.ceil(id.get(id.rows() - 1, 0).sessionBox.get_left() / increment)))
                                "Bars"    => bsize.set(x, bsize.get(x) + 1)
                    
                    nearest = 1000.

                    if profileType == "Letters"

                        for i = 0 to 20 

                            split = str.split(lab.get(i), " ")
                            bsize.set(i, split.size())

                    if bsize.indexof(bsize.max()) != bsize.lastindexof(bsize.max())

                        mid = math.round(bsize.size() / 2)

                        for i = 0 to 20 
                            
                            if math.abs(mid - bsize.get(i)) < nearest and bsize.get(i) == bsize.max()
                                nearest := i

                    for i = index to 20

                        colorCond = switch 
                            
                            nearest == 1000 => bsize.get(i) == bsize.max()
                            =>                 i == nearest
                            

                        COL = switch 

                            colorCond                  => #6929F2
                            bsize.get(i) == 1          => #F24968 
                            =>                          profileType == "Bars" ? col : color.white

                        if colorCond and poc

                            mid = switch profileType 
                                
                                "Letters" => level.get(i) 
                                =>           math.avg(level.get(i), level.get(i - 1))

                            POCLINES.push(line.new(id.get(id.rows() - 1, 0).sessionBox.get_left(), mid, 
                                                         bar_index, mid, 
                                                         color = color.new(color.red, 60), 
                                                         width = 2
                                                         ))


                        switch profileType 
                            "Letters" => 
                                         label.new(id.get(id.rows() - 1, 0).sessionBox.get_left(), level.get(i), lab.get(i), 
                                                          style             = label.style_label_left, 
                                                          textcolor         = COL,
                                                          text_font_family  = font.family_monospace,
                                                          size              = size.tiny,
                                                          color             = #00000000)
                            "Bars"   => 
                                         box.new(
                                             id.get(id.rows() - 1, 0).sessionBox.get_left(), 
                                             level.get(i), 
                                             id.get(id.rows() - 1, 0).sessionBox.get_left() + (bsize.get(i) * mult), 
                                             level.get(i - 1),
                                             bgcolor      = color.new(COL, 80),
                                             border_color = removeBor ? na : color.new(COL, 80)
                                         )

                if activate and posp
                    id.set(id.rows() - 1, 4, 
                                         objects.new(
                                                 postSessionBox = box.new(bar_index, high, bar_index, low, 
                                                                 border_style = line.style_dashed, 
                                                                 border_color = removeBor ? na : col, 
                                                                 bgcolor      = color.new(col, 90)
                                                                 )))
                    id.set(id.rows() - 1, 5, 
                                         objects.new(postSessionLabel = label.new(bar_index, high, color = #00000000, 
                                                                     text      = "CLOSE", 
                                                                     textcolor = color.white, 
                                                                     size      = size.tiny, 
                                                                     style     = label.style_label_down
                                                                     )))

            if activate
                if time2 and id.rows() > 0 
                    if not na(id.get(id.rows() - 1, 0)) and not na(id.get(id.rows() - 1, 2))

                        max = math.max(high, id.get(id.rows() - 1, 0).sessionBox.get_top   ())
                        min = math.min(low,  id.get(id.rows() - 1, 0).sessionBox.get_bottom())

                        id.get(id.rows() - 1, 5).postSessionLabel.set_y     (max),       id.get(id.rows() - 1, 4).postSessionBox  .set_top   (max)
                        id.get(id.rows() - 1, 4).postSessionBox  .set_right (bar_index), id.get(id.rows() - 1, 4).postSessionBox  .set_bottom(min)
                        id.get(id.rows() - 1, 2).preSessionBox   .set_top   (max),       id.get(id.rows() - 1, 2).preSessionBox   .set_bottom(min)
                        id.get(id.rows() - 1, 0).sessionBox      .set_bottom(min),       id.get(id.rows() - 1, 0).sessionBox      .set_top   (max)
                        id.get(id.rows() - 1, 1).sessionLabel    .set_y     (max),       id.get(id.rows() - 1, 3).preSessionLabel .set_y     (max)

lonAf = switch 
    
    timeframe.multiplier <= 30 => "1130-1131"
    =>                            "1100-1101"


[aftny, aftl, afts] = switch 

    timeframe.multiplier <= 15 =>  ["1600-2001", "1130-1245", "0200-0500"]
    timeframe.multiplier <= 30 =>  ["1600-2001", "1130-1300", "0200-0500"]
    =>                             ["1600-2001", "1100-1300", "0200-0500"]



objNY .postSessionDraw(signTime("1600-1601"), signTime(aftny), NYcol, showNY  , true)
objLON.postSessionDraw(signTime(lonAf), signTime(aftl ), LONDONcol, showLONDON, true)
objAS .postSessionDraw(signTime("0500-0501"), signTime(afts ), AScol, showAS  ,false),

var cloMat  = matrix.new<float>(0, calcTime),                var hiMat  = matrix.new<float>(0, calcTime),                   var loMat  = matrix.new<float>(0, calcTime)
var cloMatl = matrix.new<float>(0, calcTime),                var hiMatl = matrix.new<float>(0, calcTime),                   var loMatl = matrix.new<float>(0, calcTime)
var cloMata = matrix.new<float>(0, calcTime),                var hiMata = matrix.new<float>(0, calcTime),                   var loMata = matrix.new<float>(0, calcTime)


method collectData(matrix<float> id, TIME, matrix<float> id2, matrix<float> id3, color col, bool condition) => 

    if proj != "None"
        

        var int columnPlace = -1, var maxPlace = 0 , atr = ta.atr(14)
        cond1   = signTime(TIME), cond2  = cond1[1], var index = 0
        
        var project = array.new_line(), var projectBox = array.new_box(), 
                         var hlMat = matrix.new<line>(2, 0)    

        if cond1 == 1
            
            columnPlace += 1
            maxPlace    := math.max(columnPlace, maxPlace)
            
            if cond2 == 0
            
                id.add_row(id.rows()), id2.add_row(id2.rows()), id3.add_row(id3.rows()), index := bar_index
            
            id.set (id.rows()  - 1, columnPlace, ((close - close[1]) / close[1]))
            id2.set(id2.rows() - 1, columnPlace, (high - math.max(open, close)) / math.max(open, close))
            id3.set(id3.rows() - 1, columnPlace, (low  - math.min(open, close)) / math.min(open, close))
        
        if cond1 != 1 
            
            columnPlace := 0
            
            if project.size() > 0 
                for i = 0 to project.size() - 1
                    project.shift().delete()
            
            if projectBox.size() > 0 
                for i = 0 to projectBox.size() - 1
                    projectBox.shift().delete()
            
            if hlMat.columns() > 0 
                for i = 0 to hlMat.columns() - 1
                    hlMat.get(0, i).delete()
                    hlMat.get(1, i).delete()
                    hlMat      .remove_col()
        
        if barstate.islast 
            
            if not condition or condition and signTime("0300-0501") != 1
            
                finClo = id .submatrix(0, id.rows (), 0, maxPlace + 1)
                finHi  = id2.submatrix(0, id2.rows(), 0, maxPlace + 1)
                finLo  = id3.submatrix(0, id3.rows(), 0, maxPlace + 1)
            
                if cond1 == 1
                    
                    start = bar_index - index
                    
                    if proj == "Line"
                        if project.size() > 0 
                            for i = 0 to project.size() - 1
                                project.shift().delete()
                    
                        project.push(line.new(last_bar_index, close, last_bar_index + 1, close[1] * (1 + finClo.col(start).median()), style = line.style_dotted, color = col))
                        
                        for i = start + 1 to finClo.columns() - 1
                            x = project.last().get_x2()
                            y = project.last().get_y2()
                            project.push(line.new(x, y, x + 1, y * (1 + finClo.col(i).median()), style = line.style_dotted, color = col))
                    
                    if proj == "Bars"
                        
                        if projectBox.size() > 0 
                            
                            for i = 0 to projectBox.size() - 1
                                projectBox.shift().delete()
                        
                        if hlMat.columns() > 0 
                            
                            for i = 0 to hlMat.columns() - 1
                                hlMat.get(0, i).delete()
                                hlMat.get(1, i).delete()
                            for i = 0 to hlMat.columns() - 1
                                hlMat.remove_col()
                        
                        projectBox.push(box.new(last_bar_index, close[1], last_bar_index + 2, close[1] * (1 + finClo.col(start).median()), 
                                                     bgcolor       = color.new(col, 80), 
                                                     border_color = col
                                                     ))
                        if use 
                            
                            switch math.sign(projectBox.last().get_bottom() - projectBox.last().get_top()) 
                                1  => projectBox.last().set_bottom(projectBox.last().get_bottom() + atr)
                                -1 => projectBox.last().set_bottom(projectBox.last().get_bottom() - atr)
                        
                        hlMat.add_col(hlMat.columns(),
                                         array.from(
                                             line.new(
                                                     last_bar_index + 1, 
                                                     math.max(projectBox.last().get_top(), projectBox.last().get_bottom()), 
                                                     last_bar_index + 1,
                                                     math.max(projectBox.last().get_top(), projectBox.last().get_bottom()) * (1 + finHi.col(start).median()),
                                                     color = col
                                         ),
                                             line.new(
                                                     last_bar_index + 1, 
                                                     math.min(projectBox.last().get_top(), projectBox.last().get_bottom()), 
                                                     last_bar_index + 1,
                                                     math.min(projectBox.last().get_top(), projectBox.last().get_bottom()) * (1 + finLo.col(start).median()),
                                                     color = col
                                         )))


                        if start + 1 < finClo.columns() - 2
                            
                            for i = start + 1 to math.min(start + 26, finClo.columns() - 1)
                               
                                x = projectBox.last().get_right ()
                                y = projectBox.last().get_bottom()
                                projectBox.push(box.new(x, y, x + 2, y * (1 + finClo.col(i).median()),  bgcolor = color.new(col, 80), border_color = col))
                                
                                switch math.sign(projectBox.last().get_bottom() - projectBox.last().get_top())
                                    1  => projectBox.last().set_bottom(projectBox.last().get_bottom() + atr)
                                    -1 => projectBox.last().set_bottom(projectBox.last().get_bottom() - atr)
                                
                                hlMat.add_col(hlMat.columns(),
                                             array.from(
                                                 line.new(
                                                         x + 1, 
                                                         math.max(projectBox.last().get_top(), projectBox.last().get_bottom()), 
                                                         x + 1,
                                                         math.max(projectBox.last().get_top(), projectBox.last().get_bottom()) * (1 + finHi.col(i).median()),
                                                         color = col
                                             ),
                                                 line.new(
                                                         x + 1, 
                                                         math.min(projectBox.last().get_top(), projectBox.last().get_bottom()), 
                                                         x + 1,
                                                         math.min(projectBox.last().get_top(), projectBox.last().get_bottom()) * (1 + finLo.col(i).median()),
                                                         color = col
                                             )))




cloMat .collectData(nyP1, hiMat, loMat, NYcol, false), cloMatl.collectData(lonP1, hiMatl, loMatl, LONDONcol, false),
                             cloMata.collectData("2001-0501", hiMata, loMata, AScol, true)
    

method lineDelete (array <line> id) => 

    if id.size() > 0 

        for i = 0 to id.size() - 1
            if id.get(i).get_y1() == id.get(i).get_y2()
                if high >= id.get(i).get_y1() and low <= id.get(i).get_y1()
                    id.get(i).set_y2(id.get(i).get_y2() + syminfo.mintick)

            if id.get(i).get_y1() == id.get(i).get_y2()
                id.get(i).set_x2(bar_index)

method leftCheck(matrix <objects> id) => 
    if id.rows() > 0 
        for i = 0 to id.rows() - 1
            if not na(id.get(i, 2))
                earliest.push(id.get(i, 2).preSessionBox.get_left())


POCLINES.lineDelete()


if barstate.islast 

    objNY .leftCheck(), objLON.leftCheck(),
              objAS .leftCheck()

    lin = line.all, bo = box.all, lab = label.all

    if lin.size() > 0 
        for i = 0 to lin.size() - 1
            if lin.get(i).get_x1() < earliest.min()
                lin.get(i).delete()

    if bo.size() > 0 
        for i = 0 to bo.size() - 1
            if bo.get(i).get_left() < earliest.min()
                bo.get(i).delete()

    if lab.size() > 0 
        for i = 0 to lab.size() - 1
            if lab.get(i).get_x() < earliest.min()
                lab.get(i).delete()

