Imbalance Detector [LuxAlgo]
// This work is licensed under a Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) https://creativecommons.org/licenses/by-nc-sa/4.0/
// © LuxAlgo

//@version=5
indicator("Imbalance Detector [LuxAlgo]"
  , overlay = true
  , max_boxes_count = 500
  , max_labels_count = 500
  , max_lines_count = 500)
//------------------------------------------------------------------------------
//Settings
//-----------------------------------------------------------------------------{
//Fair Value Gaps
show_fvg = input(true, 'Fair Value Gaps (FVG)'
  , inline = 'fvg_css'
  , group = 'Fair Value Gaps')

bull_fvg_css = input.color(#2157f3, ''
  , inline = 'fvg_css'
  , group = 'Fair Value Gaps')

bear_fvg_css = input.color(#ff1100, ''
  , inline = 'fvg_css'
  , group = 'Fair Value Gaps')

fvg_usewidth = input(false, 'Min Width'
  , inline = 'fvg_width'
  , group = 'Fair Value Gaps')

fvg_gapwidth = input.float(0, ''
  , inline = 'fvg_width'
  , group = 'Fair Value Gaps')

fvg_method = input.string('Points', ''
  , options = ['Points', '%', 'ATR']
  , inline = 'fvg_width'
  , group = 'Fair Value Gaps')

fvg_extend = input.int(0, 'Extend FVG'
  , minval = 0
  , group = 'Fair Value Gaps')

//Opening Gaps
show_og = input(true, 'Show Opening Gaps (OG)'
  , inline = 'og_css'
  , group = 'Opening Gaps')

bull_og_css = input.color(#2157f3, ''
  , inline = 'og_css'
  , group = 'Opening Gaps')

bear_og_css = input.color(#ff1100, ''
  , inline = 'og_css'
  , group = 'Opening Gaps')

og_usewidth = input(false, 'Min Width'
  , inline = 'og_width'
  , group = 'Opening Gaps')

og_gapwidth = input.float(0, ''
  , inline = 'og_width'
  , group = 'Opening Gaps')

og_method = input.string('Points', ''
  , options = ['Points', '%', 'ATR']
  , inline = 'og_width'
  , group = 'Opening Gaps')

og_extend = input.int(0, 'Extend OG'
  , minval = 0
  , group = 'Opening Gaps')

//Volume Imbalance
show_vi = input(true, 'Show Volume Imbalances (VI)'
  , inline = 'vi_css'
  , group = 'Volume Imbalance')

bull_vi_css = input.color(#2157f3, ''
  , inline = 'vi_css'
  , group = 'Volume Imbalance')

bear_vi_css = input.color(#ff1100, ''
  , inline = 'vi_css'
  , group = 'Volume Imbalance')

vi_usewidth = input(false, 'Min Width'
  , inline = 'vi_width'
  , group = 'Volume Imbalance')

vi_gapwidth = input.float(0, ''
  , inline = 'vi_width'
  , group = 'Volume Imbalance')

vi_method = input.string('Points', ''
  , options = ['Points', '%', 'ATR']
  , inline = 'vi_width'
  , group = 'Volume Imbalance')

vi_extend = input.int(5, 'Extend VI'
  , minval = 0
  , group = 'Volume Imbalance')

//Dashboard
show_dash = input(false, 'Show Dashboard'
  , group   = 'Dashboard')

dash_loc = input.string('Bottom Right', 'Dashboard Location'
  , options = ['Top Right', 'Bottom Right', 'Bottom Left']
  , group   = 'Dashboard')
  
text_size = input.string('Tiny', 'Dashboard Size'
  , options = ['Tiny', 'Small', 'Normal']
  , group   = 'Dashboard')

//-----------------------------------------------------------------------------}
//Functions
//-----------------------------------------------------------------------------{
n = bar_index
atr = ta.atr(200)

//Detect imbalance and return count over time
imbalance_detection(show, usewidth, method, width, top, btm, condition)=>
    var is_width = true
    var count = 0

    if usewidth
        dist = top - btm

        is_width := switch method
            'Points' => dist > width
            '%' => dist / btm * 100 > width
            'ATR' => dist > atr * width

    is_true = show and condition and is_width
    count += is_true ? 1 : 0
    
    [is_true, count]

//Detect if bullish imbalance is filled and return count over time
bull_filled(condition, btm)=>
    var btms = array.new_float(0)
    var count = 0

    if condition
        array.unshift(btms, btm)
    
    size = array.size(btms)

    for i = (size > 0 ? size-1 : na) to 0
        value = array.get(btms, i)

        if low < value
            array.remove(btms, i)      
            count += 1 

    count

//Detect if bearish imbalance is filled and return count over time
bear_filled(condition, top)=>
    var tops = array.new_float(0)
    var count = 0
    
    if condition
        array.unshift(tops, top)
    
    size = array.size(tops)
    
    for i = (size > 0 ? size-1 : na) to 0
        value = array.get(tops, i)

        if high > value
            array.remove(tops, i)      
            count += 1 

    count

//Set table data cells
set_cells(tb, column, bull_filled, bull_count, bear_filled, bear_count, bull_css, bear_css, table_size)=>
    table.cell(tb, column, 1, str.tostring(bull_count)
      , text_color = bull_css
      , text_size = table_size
      , text_halign = text.align_left)

    table.cell(tb, column, 2, str.format('{0,number,percent}', bull_filled / bull_count)
      , text_color = bull_css
      , text_size = table_size
      , text_halign = text.align_left)

    table.cell(tb, column, 3, str.tostring(bear_count)
      , text_color = bear_css
      , text_size = table_size
      , text_halign = text.align_left)

    table.cell(tb, column, 4, str.format('{0,number,percent}', bear_filled / bear_count)
      , text_color = bear_css
      , text_size = table_size
      , text_halign = text.align_left)

//-----------------------------------------------------------------------------}
//Volume Imbalances
//-----------------------------------------------------------------------------{
//Bullish
bull_gap_top = math.min(close, open)
bull_gap_btm = math.max(close[1], open[1])

[bull_vi, bull_vi_count] = imbalance_detection(
  show_vi
  , vi_usewidth
  , vi_method
  , vi_gapwidth
  , bull_gap_top
  , bull_gap_btm
  , open > close[1] and high[1] > low and close > close[1] and open > open[1] and high[1] < bull_gap_top)

bull_vi_filled = bull_filled(bull_vi[1], bull_gap_btm[1])

//Bearish
bear_gap_top = math.min(close[1], open[1])
bear_gap_btm = math.max(close, open)

[bear_vi, bear_vi_count] = imbalance_detection(
  show_vi
  , vi_usewidth
  , vi_method
  , vi_gapwidth
  , bear_gap_top
  , bear_gap_btm
  , open < close[1] and low[1] < high and close < close[1] and open < open[1] and low[1] > bear_gap_btm)

bear_vi_filled = bear_filled(bear_vi[1], bear_gap_top[1])

if bull_vi
    box.new(n-1, bull_gap_top, n + vi_extend, bull_gap_btm, border_color = bull_vi_css, bgcolor = na, border_style = line.style_dotted)
    
if bear_vi
    box.new(n-1, bear_gap_top, n + vi_extend, bear_gap_btm, border_color = bear_vi_css, bgcolor = na, border_style = line.style_dotted)

//-----------------------------------------------------------------------------}
//Opening Gaps
//-----------------------------------------------------------------------------{
//Bullish
[bull_og, bull_og_count] = imbalance_detection(
  show_og
  , og_usewidth
  , og_method
  , og_gapwidth
  , bull_gap_top
  , bull_gap_btm
  , low > high[1])

bull_og_filled = bull_filled(bull_og, bull_gap_btm)

//Bullish
[bear_og, bear_og_count] = imbalance_detection(
  show_og
  , og_usewidth
  , og_method
  , og_gapwidth
  , bear_gap_top
  , bear_gap_btm
  , high < low[1])

bear_og_filled = bear_filled(bear_og, bear_gap_top)

if bull_og
    box.new(n-1, bull_gap_top, n + og_extend, bull_gap_btm, border_color = na, bgcolor = color.new(bull_og_css, 50), text = 'OG', text_color = chart.fg_color)

if bear_og
    box.new(n-1, bear_gap_top, n + og_extend, bear_gap_btm, border_color = na, bgcolor = color.new(bear_og_css, 50), text = 'OG', text_color = chart.fg_color)

//-----------------------------------------------------------------------------}
//Fair Value Gaps
//-----------------------------------------------------------------------------{
//Bullish
[bull_fvg, bull_fvg_count] = imbalance_detection(
  show_fvg
  , fvg_usewidth
  , fvg_method
  , fvg_gapwidth
  , low
  , high[2]
  , low > high[2] and close[1] > high[2] and not (bull_og or bull_og[1]))

bull_fvg_filled = bull_filled(bull_fvg, high[2])

//Bearish
[bear_fvg, bear_fvg_count] = imbalance_detection(
  show_fvg
  , fvg_usewidth
  , fvg_method
  , fvg_gapwidth
  , low[2]
  , high
  , high < low[2] and close[1] < low[2] and not (bear_og or bear_og[1]))

bear_fvg_filled = bear_filled(bear_fvg, low[2])

if bull_fvg
    avg = math.avg(low, high[2])
    
    box.new(n-2, low, n + fvg_extend, high[2], border_color = na, bgcolor = color.new(bull_fvg_css, 80))
    line.new(n-2, avg, n + fvg_extend, avg, color = bull_fvg_css)
    
if bear_fvg
    avg = math.avg(low[2], high)

    box.new(n-2, low[2], n + fvg_extend, high, border_color = na, bgcolor = color.new(bear_fvg_css, 80))
    line.new(n-2, avg, n + fvg_extend, avg, color = bear_fvg_css)

//-----------------------------------------------------------------------------}
//Dashboard
//-----------------------------------------------------------------------------{
var table_position = dash_loc == 'Bottom Left' ? position.bottom_left 
  : dash_loc == 'Top Right' ? position.top_right 
  : position.bottom_right

var table_size = text_size == 'Tiny' ? size.tiny 
  : text_size == 'Small' ? size.small 
  : size.normal

var table tb = na

//Set table legends
if barstate.isfirst and show_dash
    tb := table.new(table_position, 5, 7
      , bgcolor = #1e222d
      , border_color = #373a46
      , border_width = 1
      , frame_color = #373a46
      , frame_width = 1)
    
    if show_fvg
        table.cell(tb, 2, 0, '〓 FVG', text_color = color.white, text_size = table_size)

    if show_og
        table.cell(tb, 3, 0, '◼ OG', text_color = color.white, text_size = table_size)

    if show_vi
        table.cell(tb, 4, 0, '⬚ VI', text_color = color.white, text_size = table_size)
    
    //Set vertical legends
    table.merge_cells(tb, 0, 0, 1, 0)

    table.cell(tb, 0, 1, 'Bullish', text_color = #089981, text_size = table_size)
    
    table.cell(tb, 1, 1, 'Frequency', text_color = #089981, text_size = table_size, text_halign = text.align_left)
    
    table.cell(tb, 1, 2, 'Filled', text_color = #089981, text_size = table_size, text_halign = text.align_left)
    
    table.merge_cells(tb, 0, 1, 0, 2)
    
    table.cell(tb, 0, 3, 'Bearish', text_color = #f23645, text_size = table_size)
    
    table.cell(tb, 1, 3, 'Frequency', text_color = #f23645, text_size = table_size, text_halign = text.align_left)
    
    table.cell(tb, 1, 4, 'Filled', text_color = #f23645, text_size = table_size, text_halign = text.align_left)

    table.merge_cells(tb, 0, 3, 0, 4)

//Set dashboard data
if barstate.islast and show_dash
    if show_fvg
        set_cells(tb, 2, bull_fvg_filled, bull_fvg_count, bear_fvg_filled, bear_fvg_count, bull_fvg_css, bear_fvg_css, table_size)

    if show_og
        set_cells(tb, 3, bull_og_filled, bull_og_count, bear_og_filled, bear_og_count, bull_og_css, bear_og_css, table_size)
        
    if show_vi
        set_cells(tb, 4, bull_vi_filled, bull_vi_count, bear_vi_filled, bear_vi_count, bull_vi_css, bear_vi_css, table_size)

//-----------------------------------------------------------------------------}
//Alerts
//-----------------------------------------------------------------------------{
//FVG
alertcondition(bull_fvg, 'Bullish FVG', 'Bullish FVG detected')
alertcondition(bear_fvg, 'Bearish FVG', 'Bearish FVG detected')

//OG
alertcondition(bull_og, 'Bullish OG', 'Bullish OG detected')
alertcondition(bear_og, 'Bearish OG', 'Bearish OG detected')

//VI
alertcondition(bull_vi, 'Bullish VI', 'Bullish VI detected')
alertcondition(bear_vi, 'Bearish VI', 'Bearish VI detected')

//-----------------------------------------------------------------------------}