Initial balance 3 Session
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Vanitati
//@version=5
indicator('Initial balance 3 Sessions with retests', shorttitle='Initial balance 3S', overlay=true)

// ---- GLOBAL CONSTANTS AND VARIABLES ----
is_allowed_timeframe = timeframe.isintraday and (timeframe.multiplier == 1 or timeframe.multiplier == 5 or timeframe.multiplier == 15 or timeframe.multiplier == 30)
var alert_triggered = false
var box[] imbalancedDownBoxes = array.new_box()
var box[] imbalancedUpBoxes = array.new_box()
var int[] imbalancedDownAges = array.new_int()
var int[] imbalancedUpAges = array.new_int()
extension = extend.right

// ---- INPUTS ----
// Session Time modifiers
showNYSession = input.bool(defval=true, title='NY', group='enable/disable sessions')
showLondonSession = input.bool(defval=true, title='London', group='enable/disable sessions')
showTokyoSession = input.bool(defval=true, title='Tokyo', group='enable/disable sessions')
NY_hour = input.int(defval=15, title='NY IB', minval=0, maxval=23, group='Initial balance hour offset')
London_hour = input.int(defval=10, title='London IB', minval=0, maxval=23, group='Initial balance hour offset')
Tokyo_hour = input.int(defval=2, title='Tokyo IB', minval=0, maxval=23, group='Initial balance hour offset')

// Initial Balance Settings
extendLines = input.bool(defval=false, title='Extend Lines', group='Initial balance settings')
onlyExtendNY = input.bool(defval=true, title='Only Extend NY Hour', group='Initial balance settings')
extendMidpointOnly = input.bool(defval=true, title='Only extend mid line', group='Initial balance settings')
showVertLine = input.bool(defval=true, title='Show Vertical Line', group='Initial balance settings')
extensionHours = input.int(defval=3, title='Extension Hours', minval=0, group='Initial balance settings')
highColor = input.color(defval=color.green, title='High Line', group='Initial balance settings')
lowColor = input.color(defval=color.red, title='Low Line', group='Initial balance settings')
midColor = input.color(defval=color.rgb(120, 134, 121), title='Midpoint Line', group='Initial balance settings')
vertLineColor = input.color(defval=color.blue, title='Vertical Line', group='Initial balance settings')

// Imbalance Settings
showImbalances = input.bool(defval=true, title='Show Imbalances', group='Imbalance settings')
penetrationRatio = input.float(defval=0.001, minval=0, maxval=1, step=0.1, title="Fill ratio", group="Imbalance settings")
imbalancedDownColor = input.color(color.rgb(120,120,120,60), title="Imbalance Below", group="Imbalance settings")
imbalancedUpColor = input.color(color.rgb(120,120,120,60), title="Imbalance above", group="Imbalance settings")


//Market session settings
NYbox = input.session(title='New York Box',group="Market session settings", defval='1500-0000')
Londonbox = input.session(title='London Box',group="Market session settings", defval='1000-1500')
Tokyobox = input.session(title='Tokyo Box',group="Market session settings", defval='0200-1000')
LondonBGC = input(title='London',group="Market session settings", defval=color.new(#40f7f7, 92))
NYBGC = input(title='NY',group="Market session settings", defval=color.new(#ff412c, 92))
TokyoBGC = input(title='Tokyo',group="Market session settings", defval=color.new(#fc39ff, 92))

// ---- UTILITY FUNCTIONS ----
isWithinHour(session_hour) =>
    current_hour = hour(time)
    current_hour == session_hour
hourlyOpen = request.security(syminfo.tickerid, "60", open)
hourlyClose = request.security(syminfo.tickerid, "60", close)
isWithinAnySession = isWithinHour(London_hour) or isWithinHour(NY_hour) or isWithinHour(Tokyo_hour)

// ---- MAIN LOGIC FOR INITIAL BALANCE ----
draw_lines_for_hour(hour, session_name) =>
    hourly_high = request.security(syminfo.tickerid, '60', high)
    hourly_low = request.security(syminfo.tickerid, '60', low)
    hourly_hour = request.security(syminfo.tickerid, '60', hour)
    current_minute = minute(time)
    is_live_bar = barstate.isrealtime
    closing_selected_hour = is_live_bar ? hourly_hour == hour + 1 and hourly_hour[1] == hour : hourly_hour[1] == hour and hourly_hour[2] != hour

    // Calculate the extension in candles based on timeframe
    extensionCandles = extensionHours * 60 / timeframe.multiplier

    if current_minute > 0 and current_minute < 60 and hourly_hour == hour + 1
        na
    else
        var float captured_high = na
        var float captured_low = na
        if closing_selected_hour
            captured_high := hourly_high[1]
            captured_low := hourly_low[1]
        midpoint = (captured_high + captured_low) / 2
        endPointHighLow = extendLines and (not onlyExtendNY or (onlyExtendNY and hour == NY_hour)) ? bar_index + extensionCandles : bar_index
        endPointMid = (extendLines or extendMidpointOnly) and (not onlyExtendNY or (onlyExtendNY and hour == NY_hour)) ? bar_index + extensionCandles : bar_index
        if closing_selected_hour
            length = timeframe.multiplier == 1 ? 61 : timeframe.multiplier == 5 ? 13 : timeframe.multiplier == 15 ? 5 : timeframe.multiplier == 30 ? 3 : 0
            line.new(x1=bar_index - length + 1, y1=captured_high, x2=endPointHighLow, y2=captured_high, color=highColor, width=1)
            line.new(x1=bar_index - length + 1, y1=captured_low, x2=endPointHighLow, y2=captured_low, color=lowColor, width=1)
            line.new(x1=bar_index - length + 1, y1=midpoint, x2=endPointMid, y2=midpoint, color=midColor, width=1, style=line.style_dotted)
            label.new(x=bar_index - length + 1, y=captured_high, text=session_name, color=color.rgb(255, 255, 255, 100), textcolor=color.rgb(255, 255, 255, 4), size=size.small)
            line.new(x1=bar_index - length + 1, y1=captured_high, x2=bar_index - length + 1, y2=captured_low, color=vertLineColor, width=1, style=line.style_dotted)
            line.new(x1=bar_index, y1=captured_high, x2=bar_index, y2=captured_low, color=vertLineColor, width=1, style=line.style_dotted)
// ---- MAIN LOGIC FOR IMBALANCES----
// Define age limits based on timeframe
var int age_limit = 0
if (timeframe.multiplier == 1)
    age_limit := 2000
else if (timeframe.multiplier == 5)
    age_limit := 500

// ---- UTILITY FUNCTIONS FOR IMBALANCES ----

drawImbalance() =>
    if high[3] < low[1]
        array.push(imbalancedDownBoxes, box.new(left=bar_index - 2, bottom=high[3], right=bar_index - 1, top=low[1], bgcolor=imbalancedDownColor, border_color=imbalancedDownColor, extend=extension))
        array.push(imbalancedDownAges, 0)
    if low[3] > high[1]
        array.push(imbalancedUpBoxes, box.new(left=bar_index - 2, top=low[3], right=bar_index - 1, bottom=high[1], bgcolor=imbalancedUpColor, border_color=imbalancedUpColor, extend=extension))
        array.push(imbalancedUpAges, 0)
// Imbalance session filtering within global time
if not barstate.isrealtime and showImbalances
    if isWithinHour(London_hour) and showLondonSession
        drawImbalance()
    if isWithinHour(NY_hour) and showNYSession
        drawImbalance()
    if isWithinHour(Tokyo_hour) and showTokyoSession
        drawImbalance()
// Imbalance Boxes Logic
if array.size(imbalancedUpBoxes) > 0
    for i = array.size(imbalancedUpBoxes) - 1 to 0 by 1
        imbalancedBox = array.get(imbalancedUpBoxes, i)
        top = box.get_top(imbalancedBox)
        bottom = box.get_bottom(imbalancedBox)
        invalidationLimit = (top - bottom) * penetrationRatio
        age = array.get(imbalancedUpAges, i)
        if high >= bottom + invalidationLimit or high >= top or age > age_limit
            box.delete(imbalancedBox)
            array.remove(imbalancedUpBoxes, i)
            array.remove(imbalancedUpAges, i)
        else
            array.set(imbalancedUpAges, i, age + 1)
if array.size(imbalancedDownBoxes) > 0
    for i = array.size(imbalancedDownBoxes) - 1 to 0 by 1
        imbalancedBox = array.get(imbalancedDownBoxes, i)
        top = box.get_top(imbalancedBox)
        bottom = box.get_bottom(imbalancedBox)
        invalidationLimit = (top - bottom) * penetrationRatio
        age = array.get(imbalancedDownAges, i)
        if low <= top - invalidationLimit or low <= bottom or age > age_limit
            box.delete(imbalancedBox)
            array.remove(imbalancedDownBoxes, i)
            array.remove(imbalancedDownAges, i)
        else
            array.set(imbalancedDownAges, i, age + 1)
//----MAIN LOGIC FOR SESSION BOXES----
is_newbar(sess) =>
    t = time('D', sess)
    na(t[1]) and not na(t) or t[1] < t
is_session(sess) =>
    not na(time('D', sess))
processSession(sess, bgColor) =>
    newbar = is_newbar(sess)
    session = is_session(sess)
    if session
        lowValue = float(na)
        lowValue := if session
            if newbar
                low
            else
                math.min(lowValue[1], low)
        else
            lowValue[1]
        highValue = float(na)
        highValue := if session
            if newbar
                high
            else
                math.max(highValue[1], high)
        else
            highValue[1]
        startValue = int(na)
        startValue := if session
            if newbar
                time
            else
                math.min(startValue[1], time)
        else
            na
        endValue = int(na)
        endValue := if session
            if newbar
                time_close
            else
                math.max(endValue[1], time_close)
        else
            na
        sessBox = if newbar
            box.new(left=startValue, bottom=lowValue, right=endValue, top=highValue, xloc=xloc.bar_time, border_style=line.style_solid, border_color=bgColor, bgcolor=bgColor)
        if not newbar
            box.set_right(sessBox[1], endValue)
            box.set_top(sessBox[1], highValue)
            box.set_bottom(sessBox[1], lowValue)
//Drawing session Boxes
if showLondonSession
    processSession(Londonbox, LondonBGC)
if showNYSession
    processSession(NYbox, NYBGC)
if showTokyoSession
    processSession(Tokyobox, TokyoBGC)
// Drawing Session Lines
if is_allowed_timeframe
    if showNYSession
        draw_lines_for_hour(NY_hour, 'New York')
    if showLondonSession
        draw_lines_for_hour(London_hour, 'London')
    if showTokyoSession
        draw_lines_for_hour(Tokyo_hour, 'Tokyo')