//@version=6 indicator("BornInvestor Gap Detector", shorttitle = "BornInvestor Gaps", overlay = true, max_boxes_count = 500, max_labels_count = 500) closeGapsPartially = input.bool(false, "Close Gaps Partially", display = display.data_window) boxLimitInput = input.int(15, "Max Number of Gaps", minval = 1, maxval = 500, display = display.data_window) minimalDeviationTooltip = "Specifies the minimal size of detected gaps, as a percentage of the average high-low range for the last 14 bars." minimalDeviationInput = nz(input.float(30.0, "Minimal Deviation (%)", tooltip = minimalDeviationTooltip, minval=1, maxval=100, display = display.data_window) / 100 * ta.sma(high-low, 14)) limitBoxLengthBoolInput = input.bool(false, "Limit Max Gap Trail Length (bars)", inline = "Length Limit", display = display.data_window) limitBoxLengthIntInput = input.int(300, "", inline = "Length Limit", minval = 1, display = display.data_window) extendGapsRight = input.bool(true, "Extend Gaps to Right", display = display.data_window) showNoGapsWarning = input.bool(true, "Show 'No Gaps Found' Message", display = display.data_window) // Label Customization Group labelGroup = "Gap Size Labels" showGapSizeLabels = input.bool(true, "Show Gap Size Labels", group = labelGroup) maxLabeledGaps = input.int(0, "Max Recent Gaps with % Label", minval = 0, maxval = 50, group = labelGroup, tooltip = "0 = Show labels on all gaps") labelSize = input.string("Small", "Label Size", options = ["Tiny", "Small", "Normal", "Large", "Huge"], group = labelGroup) labelOpacity = input.int(0, "Label Opacity", minval = 0, maxval = 100, step = 5, group = labelGroup, tooltip = "0 = Fully Opaque, 100 = Fully Transparent") labelPosition = input.string("Gap Edge", "Label Position", options = ["Gap Edge", "Gap Center", "Outside Gap"], group = labelGroup) labelOffset = input.int(0, "Label Offset (bars)", minval = -10, maxval = 10, group = labelGroup, tooltip = "Positive = Right, Negative = Left") groupName = "Border and fill colors" colorUpBorderInput = input.color(color.green, "Up Gaps", inline = "Gap Up", group = groupName, display = display.data_window) colorUpBackgroundInput = input.color(color.new(color.green, 85), "", inline = "Gap Up", group = groupName, display = display.data_window) colorDownBorderInput = input.color(color.red, "Down Gaps", inline = "Gap Down", group = groupName, display = display.data_window) colorDownBackgroundInput = input.color(color.new(color.red, 85), "", inline = "Gap Down", group = groupName, display = display.data_window) type AlertInfo int countOpenGap int countClosedGap int countEnteredGap method hasOpenedGap(AlertInfo this) => this.countOpenGap > 0 method hasClosedGap(AlertInfo this) => this.countClosedGap > 0 method hasEnteredGap(AlertInfo this) => this.countEnteredGap > 0 AlertInfo alertInfo = AlertInfo.new(0, 0, 0) type Gap bool isActive bool isBull array boxes bool wasEntered method delete(Gap this) => for _box in this.boxes _box.delete() method partialClose(Gap this) => activeBox = this.boxes.last() activeBox.set_extend(extend.none) top = this.isBull ? activeBox.get_top() : low bottom = this.isBull ? high : activeBox.get_bottom() this.boxes.push(box.new( bar_index, top, bar_index, bottom, this.isBull ? colorDownBorderInput : colorUpBorderInput, bgcolor = this.isBull ? colorDownBackgroundInput : colorUpBackgroundInput)) method fullClose(Gap this) => alertInfo.countClosedGap += 1 activeBox = this.boxes.last() activeBox.set_extend(extend.none) this.isActive := false if closeGapsPartially activeBox.delete() method checkForClose(Gap this) => if this.isActive activeBox = this.boxes.last() top = activeBox.get_top() bot = activeBox.get_bottom() isBull = this.isBull activeBox.set_right(bar_index) // Trigger "entry into gap" alert only once if not this.wasEntered and high >= bot and low <= top this.wasEntered := true alertInfo.countEnteredGap += 1 if (high > bot and isBull) or (low < top and not isBull) if closeGapsPartially this.partialClose() else this.fullClose() bool forceCloseBoxExceededLengthLimit = (limitBoxLengthBoolInput and bar_index - activeBox.get_left() >= limitBoxLengthIntInput) if ((high > top and isBull) or (low < bot and not isBull)) or forceCloseBoxExceededLengthLimit this.fullClose() var allGaps = array.new() // Helper functions for label customization getLabelSize() => switch labelSize "Tiny" => size.tiny "Small" => size.small "Normal" => size.normal "Large" => size.large "Huge" => size.huge => size.small getLabelPosition(bool isGapDown, float gapTop, float gapBottom) => switch labelPosition "Gap Edge" => isGapDown ? gapTop : gapBottom "Gap Center" => (gapTop + gapBottom) / 2 "Outside Gap" => isGapDown ? gapTop + (gapTop - gapBottom) * 0.2 : gapBottom - (gapTop - gapBottom) * 0.2 => isGapDown ? gapTop : gapBottom getLabelStyle(bool isGapDown) => switch labelPosition "Gap Edge" => isGapDown ? label.style_label_down : label.style_label_up "Gap Center" => label.style_label_center "Outside Gap" => isGapDown ? label.style_label_up : label.style_label_down => isGapDown ? label.style_label_down : label.style_label_up // Detect gaps isGapDown = high < low[1] and low[1] - high >= minimalDeviationInput isGapUp = low > high[1] and low - high[1] >= minimalDeviationInput isGap = isGapDown or isGapUp boxBorderColor = isGapDown ? colorDownBorderInput : colorUpBorderInput boxBgcolor = isGapDown ? colorDownBackgroundInput : colorUpBackgroundInput registerNewGap(bool isGapDown) => alertInfo.countOpenGap += 1 gapTop = isGapDown ? low[1] : high[1] gapBottom = isGapDown ? high : low extendMode = extendGapsRight ? extend.right : extend.none newBox = box.new( bar_index - 1, gapTop, bar_index, gapBottom, border_color = boxBorderColor, bgcolor = boxBgcolor, extend = extendMode) allGaps.push(Gap.new(true, isGapDown, array.from(newBox), false)) // Gap size label gapCount = allGaps.size() if showGapSizeLabels and (maxLabeledGaps == 0 or gapCount > gapCount - maxLabeledGaps) gapSize = isGapDown ? (low[1] - high) : (low - high[1]) referencePrice = close[1] gapSizePercent = (gapSize / referencePrice) * 100 labelText = str.tostring(gapSizePercent, "#.##") + "%" labelY = getLabelPosition(isGapDown, gapTop, gapBottom) labelX = bar_index - 1 + labelOffset labelColor = isGapDown ? color.new(color.red, labelOpacity) : color.new(color.green, labelOpacity) textColor = color.new(color.white, labelOpacity) label.new( labelX, labelY, labelText, color = labelColor, textcolor = textColor, style = getLabelStyle(isGapDown), size = getLabelSize()) if allGaps.size() > boxLimitInput allGaps.shift().delete() // Check gaps for gap in allGaps gap.checkForClose() // Register new gap if present if isGap registerNewGap(isGapDown) // Optional 'no gaps found' message if showNoGapsWarning and barstate.islastconfirmedhistory and allGaps.size() == 0 noGapText = "No gaps found on the current chart. \nThe cause could be that some exchanges align the open of new bars on the close of the previous one, resulting in charts with no gaps. Alternatively, your Minimal Deviation might be too high." var infoTable = table.new(position.bottom_right, 1, 1) table.cell(infoTable, 0, 0, text = noGapText, text_color = chart.bg_color, bgcolor = chart.fg_color) // Alerts alertcondition(alertInfo.hasOpenedGap(), "New Gap Appeared", "A new gap has appeared.") alertcondition(alertInfo.hasClosedGap(), "Gap Closed", "A gap was closed.") alertcondition(alertInfo.hasEnteredGap(), "Price Entered Gap", "Price has entered a gap zone.")