Fair Value Gap [JacobMagleby]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © JacobMagleby

//@version=5
indicator(title = "Fair Value Gap [JacobMagleby]", shorttitle = "Fair Value Gap [JacobMagleby] - Version 1.0.0", overlay = true)

// { <CONSTANTS>

COSMETIC_GROUP = "Cosmetic Settings"
INVALIDATION_GROUP = "Invalidation Settings"

// } <CONSTANTS>



// { <INPUTS>

invalidationOutcome = input.string(
 title = "Invalidation Outcome",
  defval = "Stop Updating Gap",
   options = ["Stop Updating Gap", "Delete Gap"],
     group = INVALIDATION_GROUP)

invalidationMethod = input.string(
 title = "Invalidation Method",
  defval = "Gap Fill",
   options = ["Gap Fill", "Number Of Breakouts", "Age Of Gap"],
     group = INVALIDATION_GROUP)

maximumBreaks = input.int(
 title = "Breakouts Until Invalidation(If Enabled)",
  defval = 1,
   group = INVALIDATION_GROUP)

maximumAge = input.int(
 title = "Bars Until Invalidation(If Enabled)",
  defval = 50,
   group = INVALIDATION_GROUP)

bullishBorderColor = input.color(
 title = "Bullish Border Color",
  defval = color.green,
   group = COSMETIC_GROUP)

bearishBorderColor = input.color(
 title = "Bearish Border Color",
  defval = color.red,
   group = COSMETIC_GROUP)

midLineColor = input.color(
 title = "Middle Line Color",
  defval = color.blue,
   group = COSMETIC_GROUP)

bullishBackgroundColor = input.color(
 title = "Bullish Background Color",
  defval = color.new(color.green, 50),
   group = COSMETIC_GROUP)

bearishBackgroundColor = input.color(
 title = "Bearish Background Color",
  defval = color.new(color.red, 50),
   group = COSMETIC_GROUP)

borderLineWidth = input.int(
 title = "Border Line Width",
  defval = 2,
   group = COSMETIC_GROUP)
   
midLineWidth = input.int(
 title = "Middle Line Width",
  defval = 2,
   group = COSMETIC_GROUP)

// } <INPUTS>



// { <USER DEFINED TYPES>

type fairValueGap
    line topLine
    line botLine
    line midLine
    linefill bg
    float topPrice
    float botPrice
    float midPrice
    int phase
    string kind
    int age
    int flips

// } <USER DEFINED TYPES>



// { <CALCULATIONS>

bullishGapFilled          = false
bearishGapFilled          = false
bullishGapPartiallyFilled = false
bearishGapPartiallyFilled = false
gapBreakoutUp             = false
gapBreakoutDown           = false
bullishGapInvalidated     = false
bearishGapInvalidated     = false
newBullishGap             = false
newBearishGap             = false
priceInsideBullishGap     = false
priceInsideBearishGap     = false

bullishGap = low > high[2]
bearishGap = high < low[2]

var fvgArray = array.new<fairValueGap>()

if barstate.isconfirmed
    if bullishGap
        newTopLine = line.new(
         x1 = bar_index[2], y1 = low,
          x2 = bar_index, y2 = low,
           color = bullishBorderColor, width = borderLineWidth)
        
        newBotLine = line.new(
         x1 = bar_index[2], y1 = high[2],
          x2 = bar_index, y2 = high[2],
           color = bullishBorderColor, width = borderLineWidth)

        midLine = line.new(
         x1 = bar_index[2], y1 = (low + high[2]) / 2,
          x2 = bar_index, y2 = (low + high[2]) / 2, style = line.style_dashed,
           color = midLineColor, width = midLineWidth)

        bg = linefill.new(newTopLine, newBotLine, bullishBackgroundColor)

        fvg = fairValueGap.new(newTopLine, newBotLine, midLine, bg, low, high[2], (low + high[2]) / 2, 1, "Bullish", 0, 0)
        array.push(fvgArray, fvg)
        newBullishGap := true

    if bearishGap
        newTopLine = line.new(
         x1 = bar_index[2], y1 = low[2],
          x2 = bar_index, y2 = low[2],
           color = bearishBorderColor, width = borderLineWidth)
        
        newBotLine = line.new(
         x1 = bar_index[2], y1 = high,
          x2 = bar_index, y2 = high,
           color = bearishBorderColor, width = borderLineWidth)

        midLine = line.new(
         x1 = bar_index[2], y1 = (low[2] + high) / 2,
          x2 = bar_index, y2 = (low[2] + high) / 2, style = line.style_dashed,
           color = midLineColor, width = midLineWidth)

        bg = linefill.new(newTopLine, newBotLine, bearishBackgroundColor)

        fvg = fairValueGap.new(newTopLine, newBotLine, midLine, bg, low[2], high, (low[2] + high) / 2, -1, "Bearish", 0, 0)
        array.push(fvgArray, fvg)
        newBearishGap := true

    for fvg in fvgArray
        i = array.indexof(fvgArray, fvg)

        line.set_x2(fvg.topLine, bar_index + 1)
        line.set_x2(fvg.botLine, bar_index + 1)
        line.set_x2(fvg.midLine, bar_index + 1)
        
        fvg.age += 1
        
        if invalidationMethod == "Gap Fill"
            if fvg.kind == "Bullish"
                if low <= fvg.botPrice
                    line.delete(fvg.topLine)
                    line.delete(fvg.botLine)
                    line.delete(fvg.midLine)
                    linefill.delete(fvg.bg)
                    array.remove(fvgArray, i)
                    bullishGapFilled := true
                    bullishGapInvalidated := true
                else if low < fvg.topPrice
                    fvg.topPrice := low
                    line.set_y1(fvg.topLine, low)
                    line.set_y2(fvg.topLine, low)
                    fvg.midPrice := (low + fvg.botPrice) / 2
                    line.set_y1(fvg.midLine, fvg.midPrice)
                    line.set_y2(fvg.midLine, fvg.midPrice)
                    bullishGapPartiallyFilled := true
            
            else if fvg.kind == "Bearish"
                if high >= fvg.topPrice
                    line.delete(fvg.topLine)
                    line.delete(fvg.botLine)
                    line.delete(fvg.midLine)
                    linefill.delete(fvg.bg)
                    array.remove(fvgArray, i)
                    bearishGapFilled := true
                    bearishGapInvalidated := true
                else if high > fvg.botPrice
                    fvg.botPrice := high
                    line.set_y1(fvg.botLine, high)
                    line.set_y2(fvg.botLine, high)
                    fvg.midPrice := (high + fvg.topPrice) / 2
                    line.set_y1(fvg.midLine, fvg.midPrice)
                    line.set_y2(fvg.midLine, fvg.midPrice)
                    bearishGapPartiallyFilled := true

        else if invalidationMethod == "Number Of Breakouts"
            if fvg.phase == 1
                if close < fvg.botPrice
                    fvg.phase := -1
                    fvg.flips += 1
                    gapBreakoutDown := true
            
            else if fvg.phase == -1
                if close > fvg.topPrice
                    fvg.phase := 1
                    fvg.flips += 1
                    gapBreakoutUp := true
                
            if fvg.flips >= maximumBreaks
                if invalidationOutcome == "Delete Gap"
                    line.delete(fvg.topLine)
                    line.delete(fvg.botLine)
                    line.delete(fvg.midLine)
                    linefill.delete(fvg.bg)
                array.remove(fvgArray, i)
                if fvg.kind == "Bullish"
                    bullishGapInvalidated := true
                else
                    bearishGapInvalidated := true

        else if invalidationMethod == "Age Of Gap"
            if fvg.age >= maximumAge
                if invalidationOutcome == "Delete Gap"
                    line.delete(fvg.topLine)
                    line.delete(fvg.botLine)
                    line.delete(fvg.midLine)
                    linefill.delete(fvg.bg)
                array.remove(fvgArray, i)
                if fvg.kind == "Bullish"
                    bullishGapInvalidated := true
                else
                    bearishGapInvalidated := true

        priceInGap = close <= fvg.topPrice and close >= fvg.botPrice
        if fvg.kind == "Bullish" and priceInGap
            priceInsideBullishGap := true
        else if fvg.kind == "Bearish" and priceInGap
            priceInsideBearishGap := true

// } <CALCULATIONS>



// { <ALERTS>

alertcondition(
 condition = newBullishGap,
  title = "New Bullish Gap")

alertcondition(
 condition = newBearishGap,
  title = "New Bearish Gap")

alertcondition(
 condition = bullishGapFilled,
  title = "Bullish Gap Completely Filled")

alertcondition(
 condition = bearishGapFilled,
  title = "Bearish Gap Completely Filled")

alertcondition(
 condition = bullishGapPartiallyFilled,
  title = "Bullish Gap Partially Filled")

alertcondition(
 condition = bearishGapPartiallyFilled,
  title = "Bearish Gap Partially Filled")

alertcondition(
 condition = gapBreakoutUp,
  title = "Upside Breakout")

alertcondition(
 condition = gapBreakoutDown,
  title = "Downside Breakout")

alertcondition(
 condition = bullishGapInvalidated,
  title = "Bullish Gap Invalidated")

alertcondition(
 condition = bearishGapInvalidated,
  title = "Bearish Gap Invalidated")

alertcondition(
 condition = priceInsideBullishGap,
  title = "Price Is Inside Bullish Gap")

alertcondition(
 condition = priceInsideBearishGap,
  title = "Price Is Inside Bearish Gap")

// } <ALERTS>