Volume profile zones
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © mickes

//@version=5
indicator("Volume profile zones", overlay = true, max_boxes_count = 500, max_labels_count = 500, max_lines_count = 500)

_timeNow = timenow
var _lightTheme = color.r(chart.bg_color) == 255
var _intMax = 2147483647
var _intMin = -2147483648
var _maxUnixTime = 2147483647000

_barVolumeProfileLevels = input.int(25, "Levels", minval = 6, group = "Volume profile")
_lookback = input.int(12, "Lookback", minval = 1, maxval = 150, group = "Volume profile", tooltip = "Set for viewing previous prices volume profile. If set high, all profiles might not be able to draw (because of TradingViews limitations on drawings) but can still be usefull for more potential zones. NOTE: if the range candle only covers one or two candlea from the viewing timeframe the profile is not included in the lookback")
_barVolumeProfileTimeFrame = input.timeframe('60', "Volume profile data source", group = "Volume profile")
_timeFrame = input.timeframe('M', "Range", group = "Volume profile")
_barVolumeProfileValueAreaPercentage = input.int(68, "Value area %", 0, 100, group = "Volume profile")
_barVolumeProfileBackgroundColor = input.color(color.new(color.blue, 90), "Background", group = "Volume profile")
_showVolumeProfileInfoLabel = input.bool(false, "Show info label", tooltip = "Shows the POC's value in relation to the avarage POC, the volume in relation to the mean volume (the same comparison to the previous volume profile is in parentheses), the touches and the score (and given scores)", group = "Volume profile")
_visualizeRange = input.bool(true, "Visualize range", tooltip = "Visualize the active range, usefull for when the data from lower time frame is not enough (for the volume prrofile); if this happens try to increase the time frame for the volume profile data source. If the range once was active the backround will not change", group = "Volume profile")
_showScore = input.bool(true, "Show score", tooltip = "Visualize the score position, the score and the relation to the scores over and under (%) (for zone calculation) in the volume profile", group = "Volume profile")

_barVolumeProfilePocColor = input.color(color.red, "Color", group = "Point Of Controll", inline = "poc")
_barVolumeProfilePocWidth = input.int(2, "Width", group = "Point Of Controll", inline = "poc")

_barVolumeProfileValueAreaUpColor = input.color(color.new(color.blue, 30), "Value area", group = "Up color", inline = "upcolor")
_barVolumeProfileValueAreaDownColor = input.color(color.new(color.orange, 30), "Value area", group = "Down color", inline = "downcolor")
_barVolumeProfileUpColor = input.color(color.new(color.blue, 75), "Volume", group = "Up color", inline = "upcolor")
_barVolumeProfileDownColor = input.color(color.new(color.orange, 75), "Volume", group = "Down color", inline = "downcolor")

_zoneType = input.string("Support or resistance", "Type", ["Support or resistance", "Number"], group = "Zones")
_zonesToShow = input.int(3, "Zones", group = "Zones")
_supportZones = input.int(0, "Support zones", group = "Zones", minval = 0)
_resistanceZones = input.int(2, "Resistance zones", group = "Zones", minval = 0)
_pocLineColor = input.color(color.black, "POC", group = "Zones", inline = "pocline")
_pocLineWidth = input.int(2, "Width", group = "Zones", inline = "pocline")
_zoneLinefillSupport = input.color(color.new(color.green, 90), "Linefill support color", group = "Zones")
_zoneLinefillResistance = input.color(color.new(color.red, 90), "Linefill resistance color", group = "Zones")
_showZoneStopLoss = input.bool(false, "Show stop loss", group = "Zones", tooltip = "Shows a line for stop loss (one ATR from the zone)")
_showZoneInfoLabel = input.bool(true, "Show info label", group = "Zones", tooltip = "Shows a zone label showing the number (number according to rating in parentheses), start/end times, volume to mean, value area part size, touches, retests and breakouts (false breakouts in parentheses), score ('/' maximum score) and given scores")
_showZoneTouchLabel = input.bool(true, "Show zone touch label", group = "Zones", tooltip = "Shows 'T' (for touch), 'R' (for retest), 'B' (for breakout) and 'FB' (for false breakout) on zone/price.\n\n- Touch: marked on the first bar that enters a zone\n- Retest: if price enters and exits a zone without price leaving the zone in the other direction. Marked on the highest/lowest bar in the retest\n- Breakout: marked on the bar that exits the zone\n- False breakout: marked on the highest/lowest bar that exits the zone")
_skipZones = input.string("", "Skip zones", tooltip = "Comma Separated Values (CSV) with no spaces, e.g. 1,2,3,4,5", group = "Zones")

_zoneRetestsLookback = input.int(10, "Lookback", group = "Retests", minval = 0, maxval = 500)
_zoneRetestsAtrFactorTolerance = input.float(0, "ATR factor tolerance", group = "Retests", minval = 0, tooltip = "If a 'retest' has happened within a 'tolerance' of a range of this factor times the ATR, it will count")

_zoneBreaksLookback = input.int(20, "Lookback", group = "Breakouts", minval = 0, maxval = 500)

_zoneFalseBreaksLookback = input.int(20, "Lookback", group = "False breakouts", minval = 0, maxval = 500)

_barsInZoneScores = input.string("10,9,8,7,6,5,4,3,2,1", "Bars in zone", group = "Scores", tooltip = "Comma Separated Values (CSV) with no spaces, top score first, e.g. 5,4,3,2,1")
_virginPocScore = input.float(30, "Virgin POC", group = "Scores", tooltip = "Score that's applied to all profiles that fulfills all the conditions")
_valueAreaPartScores = input.string("10,9,8,7,6,5,4,3,2,1", "Value area part", group = "Scores", tooltip = "Comma Separated Values (CSV) with no spaces, top score first, e.g. 5,4,3,2,1")
_valuAreaVolumeScores = input.string("10,9,8,7,6,5,4,3,2,1", "VA Volume", group = "Scores", tooltip = "Comma Separated Values (CSV) with no spaces, top score first, e.g. 5,4,3,2,1")

_closestPriceScores = input.string("10,9,8,7,6,5,4,3,2,1", "Scores", group = "Closest price", tooltip = "Comma Separated Values (CSV) with no spaces, top score first, e.g. 5,4,3,2,1")
_closestPriceScorePercentageLimit = input.int(80, "Limit %", minval = 0, maxval = 100, group = "Closest price", tooltip = "For the score to be given the profile must be in the earlier profiles (by the percentage given)")

_reactionScore = input.float(10, "Score", group = "Zone reaction score", tooltip = "Score that's applied to all profiles that fulfills all the conditions")
_reactionLookahead = input.int(5, "Lookahead", group = "Zone reaction score", minval = 0, tooltip = "Number of bars to look for a zone base reaction")
_reactionAtrFactor = input.float(.4, "ATR factor", group = "Zone reaction score", minval = 0, tooltip = "ATR (length 14) factor that's needed per bar (average) for the score to be applied")

_pivotScore = input.float(10, "Score", group = "Pivot score", tooltip = "Score that's applied to all profiles that fulfills all the conditions. Even if the period contains multiple pivots only one score is given")
_pivotBars = input.int(50, "Length", group = "Pivot score")
_showPivots = input.bool(true, "Show", group = "Pivot score")

_priceAwayScoreFactor = input.float(3, "Score factor", group = "Price away score", tooltip = "The score is multiplied with the risk/reward that's given")
_priceAwayScoreMargin = input.float(10, "Margin from base", group = "Price away score", minval = 1, tooltip = "Margin from the base at witch to start. 'Minval' is limited to 1 because profiles can have incomple candles (not have a candle at all on the last time of the range)")
_priceAwayScores = input.string("10,9,8,7,6,5,4,3,2,1", "Scores", group = "Price away score", tooltip = "Comma Separated Values (CSV) with no spaces, top score first, e.g. 5,4,3,2,1")
_barsAwayScores = input.string("10,9,8,7,6,5,4,3,2,1", "Scores", group = "Price away score", tooltip = "Comma Separated Values (CSV) with no spaces, top score first, e.g. 5,4,3,2,1")

_showSummary = input.bool(true, "Show summary", group = "summary")
_showSummaryInfo = input.bool(false, "Show info", group = "summary")

_alertFrequency = input.string("Once per bar close", "Frequency", ["All", "Once per bar", "Once per bar close"], group = "Alert")
_alertOn = input.string("None", "Alert", ["None", "All", "Entry/crossing", "Retest", "Breakout", "Breakout and retest", "False breakouts"], group = "Alert", tooltip = "Must be set if you want to create an alert. 'Breakout and retest' also includes 'Breakout and false breakout'. NOTE: If you pause and resume an alert new zones will be used! Alerts are not effected by if a touch is removed after it happens (like a breakout that becomes a false breakout) and if a touch (e.g. a breakout) has started before the alert is created the user will be notified anyway. At the moment alerts can only be set if 'Last range bar index' is set (this is due to var/varip and unknown pine history error)")
_alertDirection = input.string("All", "Direction", ["All","Bull", "Bear"], group = "Alert")
_alertDelay = input.int(0, "Delay", minval = 0, tooltip = "Use if you want to be notified a little bit later (number of bars). If a touch (e.g. a breakout) of the same type happens between the previous touch and the delay it will not be alerted!", group = "Alert")

_activeBarIndexDefaultTime = 2147483647000 // max unix time
_activeBarIndexTime = input.time(_activeBarIndexDefaultTime, "Bar index label", group = "Advanced", tooltip = "Change this to show one specific profile. Put the time in a range and it will show (usefull for viewing older profiles and specific onces)")
_lastRangeBarIndex = input.int(-1, "Last range bar index", group = "Advanced", tooltip = "If set to not -1 will define the latest range bar (if you can figure out the bar index or chech the from end box). Touches after the active period will be shown but the original 'touch' will never be shown")
_lastRangeBarIndexFromEnd = input.bool(true, "Last range bar index from end", group = "Advanced", tooltip = "Set if you want to use the 'last range bar index' as bars from end. Set to '-1' to deactivate")

_barVolumeProfileBullVolumeStartIndex = 0
_barVolumeProfileBearVolumeStartIndex = _barVolumeProfileLevels
_barVolumeProfilePricesStartIndex = _barVolumeProfileLevels * 2
_barVolumeProfileMetaDataLowPriceIndex = _barVolumeProfilePricesStartIndex
_barVolumeProfileMetaDataHighPriceIndex = _barVolumeProfilePricesStartIndex + 1
_barVolumeProfileMetaDataPriceWidthIndex = _barVolumeProfilePricesStartIndex + 2
_barVolumeProfileMetaDataTimeIndex = _barVolumeProfilePricesStartIndex + 3
_barVolumeProfileMetaDataStartBarIndexIndex = _barVolumeProfilePricesStartIndex + 4
_barVolumeProfileMetaDataEndBarIndexIndex = _barVolumeProfilePricesStartIndex + 5
_barVolumeProfileMetaDataPocIndex = _barVolumeProfilePricesStartIndex + 6
_barVolumeProfileMetaDataPocVolumeIndex = _barVolumeProfilePricesStartIndex + 7
_barVolumeProfileMetaDataValueAreaLowIndex = _barVolumeProfilePricesStartIndex + 8
_barVolumeProfileMetaDataValueAreaHighIndex = _barVolumeProfilePricesStartIndex + 9
_barVolumeProfileMetaDataStartTimeIndex = _barVolumeProfilePricesStartIndex + 10
_barVolumeProfileMetaDataEndTimeIndex = _barVolumeProfilePricesStartIndex + 11

_metaDataLowPriceIndex = 0
_metaDataHighPriceIndex = 1
_metaDataPriceWidthIndex = 2
_metaDataTimeIndex = 3
_scoreMetaDataReactionBullTrend = 0
_scoreMetaDataReactionBearTrend = 1

_atr = ta.atr(14)

type interval
    int Start
    int End
    bool IncompleteCandles
    bool PreviousIncompleteCandles
    int StartTime = time
    int EndTime = -time_close
    int PreviousStartTime = time
    int PreviousEndTime = time_close
    int PreviousStart
    int PreviousEnd
type zoneInfo
    int BarIndex
    float Price
    bool IsLabelUp
    int StartBarIndex
    int EndBarIndex
    int Length
type volumeProfile
    float Poc
    float Volume
    int StartTime
    int EndTime
    int Start
    int End
    float PriceWidth
    float Low
    float High
    float ValueAreaLow
    float ValueAreaHigh
    float UpVolume
    float DownVolume
    array<zoneInfo> Breakouts
    array<int> BreakoutIndices
    array<zoneInfo> Retests
    array<int> RetestIndices
    array<zoneInfo> FalseBreakouts
    array<int> FalseBreakoutIndices
    array<int> TouchIndices
    int BarsInZone = 0 // almost the same as touches, but every bar
    array<int> Scores // meta data used by scores
    array<string> ScoresGiven
    float Score
    array<line> ZoneLines
    array<linefill> ZoneLinefills
    array<label> ZoneLabels
    label InfoLabel
    array<line> Lines
    array<linefill> Linefills
    array<box> Boxes
    int LatestBullRetestConfirmationBarIndex = -1
    int LatestBearRetestConfirmationBarIndex = -1
    int LatestBullFalseBreakoutConfirmationBarIndex = -1
    int LatestBearFalseBreakoutConfirmationBarIndex = -1
    int LatestBullBreakoutConfirmationBarIndex = -1
    int LatestBearBreakoutConfirmationBarIndex = -1
    int LatestBullTouchConfirmationBarIndex = -1
    int LatestBearTouchConfirmationBarIndex = -1
    bool Incomplete = false
    float MinimumBeforeTouch = 2147483647
    float MaximumBeforeTouch = -2147483648
    bool IsTouched = false
    int BarsAwayUntilFirstTouch = 0
    float PriceAwayUntilFirstTouch = 0
    float ValueAreaVolume = 0
    box AreaBox
type volumeProfilesSummary
    float AvaragePoc = 0.
    float AvarageVolume = 0.
    int Count = 0
type scoreMetaData
    float Max = 0
    float MaxScore
    float MaxScoreEnd
type rangeBar
    bool Active = false
    bool HasBeenActive = false
    bool LastActiveBar = false
    int LastActiveBarTimeClose = -1
    int StartTime = -1

var _lowerTimeFrameValues = matrix.new<float>()
var _barVolumeProfile = matrix.new<float>(150, (_barVolumeProfileLevels * 4), 0.)

var _scores = matrix.new<float>(_lookback, 4, 0) // row, score, score from end, total score
var _pivots = array.new<int>()
var _summary = matrix.new<string>(3, 2) // zones, vpoc, score std dev

var _interval = interval.new()

var _volumeProfiles = array.new<volumeProfile>()
var _volumeProfilesVolumes = array.new<float>()
var _volumeProfilesPocs = array.new<float>()

var _singleZone = _activeBarIndexTime != _activeBarIndexDefaultTime
var _singleLookback = _lookback == 1

var _scoreMetaData = scoreMetaData.new()
var _zones = array.new<volumeProfile>()

var _alertVolumeProfiles = array.new<volumeProfile>()

unixTimeToString(unixTime) =>
    timeString = str.format_time(unixTime, "yyyy-MM-dd HH:mm:ss", syminfo.timezone)

    timeString
getMetaData(m) =>
    metaData = array.new_float(_barVolumeProfileLevels)
    rows = matrix.rows(m)

    if rows > 0
        lows = matrix.col(m, 2)
        highs = matrix.col(m, 3)

        min = array.min(lows)
        max = array.max(highs)
        barSize = max - min
        priceWidth = barSize / _barVolumeProfileLevels
        
        array.set(metaData, _metaDataLowPriceIndex, min)
        array.set(metaData, _metaDataHighPriceIndex, max)
        array.set(metaData, _metaDataPriceWidthIndex, priceWidth)

    metaData
getPriceLevels(metaData) =>
    priceLevels = array.new_float(_barVolumeProfileLevels)

    lowPriceHigherTime = array.get(metaData, _metaDataLowPriceIndex)
    priceWidth = array.get(metaData, _metaDataPriceWidthIndex)
    for i = 0 to _barVolumeProfileLevels - 1
        price = lowPriceHigherTime + ((priceWidth) * i)
        array.set(priceLevels, i, price)

    priceLevels
setBarVolumePriceLevels(row, metaData) =>
    priceLevels = getPriceLevels(metaData)
    for column = 0 to _barVolumeProfileLevels - 1
        price = array.get(priceLevels, column)
        matrix.set(_barVolumeProfile, row, column + (_barVolumeProfileLevels * 3), price)
setMetaData(row, metaData) =>
    lowPrice = array.get(metaData, _metaDataLowPriceIndex)
    highPrice = array.get(metaData, _metaDataHighPriceIndex)
    priceWidth = array.get(metaData, _metaDataPriceWidthIndex)

    matrix.set(_barVolumeProfile, row, _barVolumeProfileMetaDataLowPriceIndex, lowPrice)
    matrix.set(_barVolumeProfile, row, _barVolumeProfileMetaDataHighPriceIndex, highPrice)
    matrix.set(_barVolumeProfile, row, _barVolumeProfileMetaDataPriceWidthIndex, priceWidth)
setMetaDataAfter(row, startBarIndex, endBarIndex, startTime, endTime) =>
    matrix.set(_barVolumeProfile, row, _barVolumeProfileMetaDataStartBarIndexIndex, startBarIndex)
    matrix.set(_barVolumeProfile, row, _barVolumeProfileMetaDataEndBarIndexIndex, endBarIndex)
    matrix.set(_barVolumeProfile, row, _barVolumeProfileMetaDataStartTimeIndex, startTime)
    matrix.set(_barVolumeProfile, row, _barVolumeProfileMetaDataEndTimeIndex, endTime)

    rowArray = matrix.row(_barVolumeProfile, row)
    bullVolumes = array.slice(rowArray, _barVolumeProfileBullVolumeStartIndex, _barVolumeProfileBearVolumeStartIndex)
    bearVolumes = array.slice(rowArray, _barVolumeProfileBearVolumeStartIndex, _barVolumeProfileBearVolumeStartIndex + _barVolumeProfileLevels)
    totalVolumes = array.copy(bullVolumes)

    for [i, bearVolume] in bearVolumes
        totalVolume = array.get(bullVolumes, i)
        totalVolume += bearVolume
        array.set(totalVolumes, i, totalVolume)

    pocVolume = array.max(totalVolumes)
    pocIndex = array.indexof(totalVolumes, pocVolume)
    lowPrice = array.get(rowArray, _barVolumeProfileMetaDataLowPriceIndex)
    priceWidth = array.get(rowArray, _barVolumeProfileMetaDataPriceWidthIndex)
    highPrice = array.get(rowArray, _barVolumeProfileMetaDataHighPriceIndex)
    poc = lowPrice + ((priceWidth) * pocIndex)
    matrix.set(_barVolumeProfile, row, _barVolumeProfileMetaDataPocIndex, poc)
    matrix.set(_barVolumeProfile, row, _barVolumeProfileMetaDataPocVolumeIndex, pocVolume)

    // calculate value area
    totalVolume = array.sum(totalVolumes)
    maxValueAreaVolume = totalVolume * (_barVolumeProfileValueAreaPercentage / 100.)
    toI = _barVolumeProfileLevels - 1
    fromI = pocIndex + 1
    if pocIndex == _barVolumeProfileLevels - 1
        fromI := pocIndex
    if (fromI - toI) > fromI
        toI := 0

    up = pocIndex + 1, down = pocIndex - 1, valueAreaLow = poc, valueAreaHigh = poc + priceWidth, valueAreaVolume = pocVolume
    for unused = fromI to toI
        if valueAreaVolume >= maxValueAreaVolume
            break

        volumeUp = 0.
        if up <= _barVolumeProfileLevels - 1
            volumeUp := array.get(totalVolumes, up)
            valueAreaHigh := lowPrice + (priceWidth * up)
        volumeDown = 0.
        if down >= 0
            volumeDown := array.get(totalVolumes, down)
            valueAreaLow := lowPrice + (priceWidth * down)

        if volumeUp == 0
          and volumeDown == 0
            break

        if volumeUp >= volumeDown
            valueAreaVolume += volumeUp
            up += 1
        else
            valueAreaVolume += volumeDown
            down -= 1

    matrix.set(_barVolumeProfile, row, _barVolumeProfileMetaDataValueAreaLowIndex, valueAreaLow)
    matrix.set(_barVolumeProfile, row, _barVolumeProfileMetaDataValueAreaHighIndex, valueAreaHigh)
setInterval() =>
    if time >= _interval.EndTime // new interval
        _interval.PreviousStartTime := _interval.StartTime
        _interval.PreviousEndTime := _interval.EndTime
        _interval.PreviousStart := _interval.Start
        _interval.PreviousEnd := _interval.End

        _interval.StartTime := time(_timeFrame)
        _interval.EndTime := time_close(_timeFrame)

        if _interval.PreviousIncompleteCandles
            _interval.PreviousIncompleteCandles := false
            _interval.Start := bar_index - 1 // previous incomplete candles were drawn later
        else
            _interval.Start := bar_index

        _interval.IncompleteCandles := false

    _interval.End := bar_index // always update to latest
getData(active) =>
    // must be called in lower time frame
    bullVolume = 0.
    bearVolume = 0.
    lowPrice = 0.
    highPrice = 0.
    openTime = 0

    if active
        lowPrice := low
        highPrice := high

        if close >= open // green candle
            bullVolume := volume
        else
            bearVolume := volume

        openTime := time

    [bullVolume, bearVolume, lowPrice, highPrice, openTime]
getVolumeProfileMatrix(active, previousActive, previousTimeClose, incomplete) =>
    m = matrix.new<float>()

    incompleteCandles = previousTimeClose < _interval.PreviousEndTime
      and time_close > _interval.PreviousEndTime

    activeAndIntervalCloseTime = active and (time_close == _interval.EndTime)
    activeAndAndLastActiveIncompleteCandlesNonRealtimeBar = active and ((barstate.islast and timenow > _interval.EndTime) and not barstate.isrealtime) // incomplete candles and closed range
    previousActiveAndIncompleteCandles = previousActive and incompleteCandles // incomplete candles and previous bar was active
    incompleteAndLastBar = barstate.islast and incomplete

    if (activeAndIntervalCloseTime or activeAndAndLastActiveIncompleteCandlesNonRealtimeBar or previousActiveAndIncompleteCandles)
      or incompleteAndLastBar
        m := matrix.copy(_lowerTimeFrameValues)

        removeRowIndices = array.new<int>()
        rows = matrix.rows(m)
        toI = rows > 0 ? rows - 1 : na
        for row = 0 to toI
            openTime = matrix.get(m, row, 4)

            if incompleteCandles
                if openTime > _interval.PreviousEndTime
                    array.push(removeRowIndices, row) // remove from returned matrix later
                else if not incompleteAndLastBar
                    matrix.remove_row(_lowerTimeFrameValues, toI - row) // remove from 'next' matrix, all but the current once
            else if not incompleteAndLastBar
                matrix.remove_row(_lowerTimeFrameValues, 0)

        removedRows = 0
        for removeRowIndex in removeRowIndices
            removedRow = matrix.remove_row(m, removeRowIndex - removedRows)
            removedRows += 1

        _interval.IncompleteCandles := incompleteCandles

        if matrix.rows(m) == 0
            runtime.error("Cannot fetch enough data for volume profile (try increasing the volume profile data source)")
    m
isActive(lastRangeBarIndex, lastRangeBarIndexFromEnd) =>
    bar = rangeBar.new()
    lastBarIndex = lastRangeBarIndex != -1 ? lastRangeBarIndexFromEnd ? last_bar_index - lastRangeBarIndex : lastRangeBarIndex : last_bar_index
    var lastActiveBarCloseTime = _maxUnixTime
    var hasBeenActive = false

    if not _singleZone
        bar.Active := bar_index > lastBarIndex - _lookback
          and bar_index <= lastBarIndex
          
        if bar_index == lastBarIndex
            bar.LastActiveBar := true
            lastActiveBarCloseTime := time_close
    else // one zone selected to show
        bar.Active := _activeBarIndexTime >= time
          and _activeBarIndexTime < time_close
        if bar.Active
            lastActiveBarCloseTime := time_close

    bar.LastActiveBarTimeClose := lastActiveBarCloseTime

    if bar.Active
        hasBeenActive := true
    if hasBeenActive
      and not bar.Active
        bar.HasBeenActive := true

    bar.StartTime := time

    bar
setVolumeProfileData(active, bullVolumes, bearVolumes, lowPrices, highPrices, openTimes) =>
    if active
        toI = array.size(bullVolumes) - 1
        if toI < 0
            toI := na
        for i = 0 to toI
            matrix.add_row(_lowerTimeFrameValues, 0, array.from(
              array.get(bullVolumes, i),
              array.get(bearVolumes, i),
              array.get(lowPrices, i),
              array.get(highPrices, i),
              array.get(openTimes, i)))
inRangeShare(lowPriceLimit, highPriceLimit, lowPrice, highPrice) =>
    lowPriceShareInRange = 0.
    highPriceShareInRange = 0.
    isInRange = false
    if lowPrice <= highPriceLimit
      and highPrice >= lowPriceLimit
        lowPriceShareInRange := highPriceLimit - math.max(lowPrice, lowPriceLimit)
        highPriceShareInRange := highPrice - math.max(lowPriceLimit, lowPrice)
        isInRange := true

    inRange = math.min(lowPriceShareInRange, highPriceShareInRange)
    rangeWidth = highPriceLimit - lowPriceLimit
    barSize = highPrice - lowPrice

    wholeBarInRange = (barSize <= rangeWidth and (inRange > 0 or barSize == 0)) and isInRange
    share = 0.
    switch wholeBarInRange
        false =>
            if barSize > 0
                share := inRange / barSize
        true =>
            share := 1

    if share > 1
        runtime.error("Share is to large " + str.tostring(lowPriceShareInRange) + "\n" + str.tostring(highPriceShareInRange) + "\n" + str.tostring(share) + "\n" + str.tostring(inRange) + "\n" + str.tostring(barSize))

    if na(share)
        runtime.error("Share is na " + str.tostring(lowPriceShareInRange) + "\n" + str.tostring(highPriceShareInRange) + "\n" + str.tostring(share) + "\n" + str.tostring(inRange) + "\n" + str.tostring(barSize))

    share
getVolumes(lowPrice, highPrice, bullVolume, bearVolume, metaData) =>
    volumeMatrix = matrix.new<float>()
    priceLevels = getPriceLevels(metaData)
    priceWidth = array.get(metaData, _metaDataPriceWidthIndex)

    for row = 0 to array.size(priceLevels) - 1
        priceLevel = array.get(priceLevels, row)

        lowPriceHigherTime = priceLevel
        highPriceHigherTime = priceLevel + priceWidth

        share = inRangeShare(lowPriceHigherTime, highPriceHigherTime, lowPrice, highPrice)

        if share > 0
            bullVolumeShare = bullVolume * share
            bearVolumeShare = bearVolume * share

            matrix.add_row(volumeMatrix, 0, array.from(
              row,
              bullVolumeShare,
              bearVolumeShare,
              lowPrice,
              highPrice,
              bullVolume,
              bearVolume))

    volumeMatrix
updateBarVolumeProfile(row, volumeMatrix) =>
    for rowArray in volumeMatrix
        column = math.round(array.get(rowArray, 0))
        bullVolume = array.get(rowArray, 1)
        bearVolume = array.get(rowArray, 2)

        bullColumn = column + _barVolumeProfileBullVolumeStartIndex
        bearColumn = column + _barVolumeProfileBearVolumeStartIndex

        matrixBullVolume = matrix.get(_barVolumeProfile, row, bullColumn)
        matrixBullVolume += bullVolume
        matrix.set(_barVolumeProfile, row, bullColumn, matrixBullVolume)

        matrixBearVolume = matrix.get(_barVolumeProfile, row, bearColumn)
        matrixBearVolume += bearVolume
        matrix.set(_barVolumeProfile, row, bearColumn, matrixBearVolume)
volumeProfileScoreString(profile, i) =>
    scores = ""
    for score in profile.ScoresGiven
        scores += "\n" + score

    string scoreString = na
    if str.length(scores) > 0
        profiles = array.size(_volumeProfiles)
        scoreString := str.format("\nScore ({0}): {1,number,#.#}/{2,number,#.#}{3}{4}", i == 0 ? "highest" : str.format("top {0,number,#}%", ((i + 1) / profiles) * 100) , profile.Score, _scoreMetaData.Max, str.length(scores) == 0 ? "" : ":", scores)

    scoreString
volumeProfileInfoLabel(i, profile, start, end, lowPrice, highPrice, poc, totalVolume, upVolume, downVolume) =>
    infoLabel = label.new(start + ((end - start) / 2), 0, "")
    profile.InfoLabel := infoLabel
updateVolumeProfileInfoLabel(profile, i) =>
    var calls = 0

    pocPreviousPercentage = 0.
    pocPercentage = 0.
    volumePreviousPercentage = 0.
    volumePercentage = 0.

    averageVolume = array.sum(_volumeProfilesVolumes) / array.size(_volumeProfilesVolumes)
    averagePoc = array.sum(_volumeProfilesPocs) / array.size(_volumeProfilesPocs)

    previousVolumeProfile = i > 0 ? array.get(_volumeProfiles, i - 1) : volumeProfile.new()
    pocPreviousPercentage := ((profile.Poc / previousVolumeProfile.Poc) - 1) * 100
    pocPercentage := ((profile.Poc / averagePoc) - 1) * 100
    
    volumePreviousPercentage := ((profile.Volume / previousVolumeProfile.Volume) - 1) * 100
    volumePercentage := ((profile.Volume / averageVolume) - 1) * 100

    location = calls % 2 == 0 ? profile.High : profile.Low
    txt = str.format("POC: {0,number,##.#}% ({1,number,##.#}%)\n", pocPercentage, pocPreviousPercentage)
      + str.format("Volume: {0,number,##.#}% ({1,number,##.#}%)\n", volumePercentage, volumePreviousPercentage)
      + str.tostring(profile.UpVolume, format.volume) + "x" + str.tostring(profile.DownVolume, format.volume) + " (" + str.tostring(profile.Volume, format.volume) + ")"
      + volumeProfileScoreString(profile, i)

    color labelColor = na
    if volumePercentage > 0
        labelColor := color.new(color.green, 40)
    else if volumePercentage < 0
        labelColor := color.new(color.red, 40)
    else if volumePercentage == 0
        labelColor := color.new(color.gray, 40)
        txt := str.tostring(profile.Volume, format.volume)

    label.set_text(profile.InfoLabel, txt)
    label.set_style(profile.InfoLabel, location == profile.High ? label.style_label_down : label.style_label_up)
    label.set_y(profile.InfoLabel, location)
    label.set_color(profile.InfoLabel, labelColor)
    
    if i == array.size(_volumeProfiles) - 1
        calls := 0
    else
        calls += 1
updateVolumeProfileInfoLabels() =>
    for [i, profile] in _volumeProfiles
        updateVolumeProfileInfoLabel(profile, i)
visulize(volumes, row, incomplete) =>
    start = math.round(array.get(volumes, _barVolumeProfileMetaDataStartBarIndexIndex))
    end = math.round(array.get(volumes, _barVolumeProfileMetaDataEndBarIndexIndex))
    startTime = math.round(array.get(volumes, _barVolumeProfileMetaDataStartTimeIndex))
    endTime = math.round(array.get(volumes, _barVolumeProfileMetaDataEndTimeIndex))
    lowPrice = array.get(volumes, _barVolumeProfileMetaDataLowPriceIndex)
    highPrice = array.get(volumes, _barVolumeProfileMetaDataHighPriceIndex)
    pocVolume = array.get(volumes, _barVolumeProfileMetaDataPocVolumeIndex)
    priceWidth = array.get(volumes, _barVolumeProfileMetaDataPriceWidthIndex)
    valueAreaLow = array.get(volumes, _barVolumeProfileMetaDataValueAreaLowIndex)
    valueAreaHigh = array.get(volumes, _barVolumeProfileMetaDataValueAreaHighIndex)

    poc = array.get(volumes, _barVolumeProfileMetaDataPocIndex)

    boxes = array.new<box>()
    lines = array.new<line>()
    linefills = array.new<linefill>()

    valueAreaVolume = 0.
    xFactor = (end - start) * (float(2) / 3)
    for i = 0 to _barVolumeProfileLevels - 1
        bullVolume = array.get(volumes, _barVolumeProfileBullVolumeStartIndex + i)
        bearVolume = array.get(volumes, _barVolumeProfileBearVolumeStartIndex + i)
        totalVolume = bullVolume + bearVolume
        price = lowPrice + (priceWidth * i)

        volumeShare = totalVolume / pocVolume
        bullShare = nz((bullVolume / totalVolume) * volumeShare)
        bearShare = nz((bearVolume / totalVolume) * volumeShare)

        rightUp = start + math.round(bullShare * xFactor)
        rightDown = rightUp + math.round(bearShare * xFactor)

        left = start
        top = (price + priceWidth) - (priceWidth * 0.07)
        right = rightUp
        bottom = price + (priceWidth * 0.07)

        upBoxColor = _barVolumeProfileValueAreaUpColor
        downBoxColor = _barVolumeProfileValueAreaDownColor
        if price < valueAreaLow
          or price >= valueAreaHigh
            upBoxColor := _barVolumeProfileUpColor
            downBoxColor := _barVolumeProfileDownColor

        else
            valueAreaVolume += totalVolume

        if left != right
            if price == poc
                upLowLine = line.new(left, bottom, right, bottom, color = upBoxColor)
                upHighLine = line.new(left, top, right, top, color = upBoxColor)
                upLeftLine = line.new(left, bottom, left, top, color = upBoxColor)
                upRightLine = line.new(right, bottom, right, top, color = upBoxColor)
                array.push(lines, upLowLine), array.push(lines, upHighLine), array.push(lines, upLeftLine), array.push(lines, upRightLine)

                boxishLinefill = linefill.new(upLowLine, upHighLine, upBoxColor)
                array.push(linefills, boxishLinefill)
            else
                upBox = box.new(left, top, right, bottom, border_color = color.black, bgcolor = upBoxColor, border_width = 0)
                array.push(boxes, upBox)

        left := rightUp
        right := rightDown
        
        if left != right
            if price == poc
                downLowLine = line.new(left, bottom, right, bottom, color = downBoxColor)
                downHighLine = line.new(left, top, right, top, color = downBoxColor)
                downLeftLine = line.new(left, bottom, left, top, color = downBoxColor)
                downRightLine = line.new(right, bottom, right, top, color = downBoxColor)
                array.push(lines, downLowLine), array.push(lines, downHighLine), array.push(lines, downLeftLine), array.push(lines, downRightLine)

                boxishLinefill = linefill.new(downLowLine, downHighLine, downBoxColor)
                array.push(linefills, boxishLinefill)
            else
                downBox = box.new(left, top, right, bottom, bgcolor = downBoxColor, border_width = 0)
                array.push(boxes, downBox)

    valueAreaLowLine = line.new(start, valueAreaLow, end, valueAreaLow, color = color.blue, width = 2)
    valueAreaHighLine = line.new(start, valueAreaHigh, end, valueAreaHigh, color = color.blue, width = 2)
    pocLine = line.new(start, poc + (priceWidth / 2), end, poc + (priceWidth / 2), color = _barVolumeProfilePocColor, width = _barVolumeProfilePocWidth)
    array.push(lines, valueAreaLowLine), array.push(lines, valueAreaHighLine), array.push(lines, pocLine)

    upVolume = array.sum(array.slice(volumes, 0, _barVolumeProfileLevels))
    downVolume = array.sum(array.slice(volumes, _barVolumeProfileLevels, _barVolumeProfileLevels * 2))
    totalVolume = array.sum(array.slice(volumes, 0, _barVolumeProfileLevels * 2))

    profile = volumeProfile.new(poc + (priceWidth / 2), totalVolume, startTime, endTime, start, end, priceWidth, lowPrice, highPrice, valueAreaLow, valueAreaHigh, upVolume, downVolume) // add price width to value area low to match
    profile.Breakouts := array.new<zoneInfo>()
    profile.BreakoutIndices := array.new<int>()
    profile.Retests := array.new<zoneInfo>()
    profile.RetestIndices := array.new<int>()
    profile.FalseBreakouts := array.new<zoneInfo>()
    profile.FalseBreakoutIndices := array.new<int>()
    profile.TouchIndices := array.new<int>()
    profile.Scores := array.new<int>()
    profile.ScoresGiven := array.new<string>()
    profile.ZoneLines := array.new<line>()
    profile.ZoneLinefills := array.new<linefill>()
    profile.ZoneLabels := array.new<label>()
    profile.Lines := lines
    profile.Linefills := linefills
    profile.Boxes := boxes
    profile.Incomplete := incomplete
    profile.ValueAreaVolume := valueAreaVolume

    areaBox = box.new(start, highPrice, end, lowPrice, bgcolor = _barVolumeProfileBackgroundColor, border_width = 0, text_valign = text.align_top, text_halign = text.align_right, text_size = size.tiny, text_color = _lightTheme ? color.black : color.gray)
    array.push(boxes, areaBox)
    profile.AreaBox := areaBox

    array.push(_volumeProfiles, profile)
    array.push(_volumeProfilesVolumes, totalVolume)
    array.push(_volumeProfilesPocs, poc)

    if _showVolumeProfileInfoLabel
        volumeProfileInfoLabel(row, profile, start, end, lowPrice, highPrice, poc, totalVolume, upVolume, downVolume)
sort(m, col, order) =>
    if matrix.rows(m) > 0
        matrix.sort(m, col, order)
removeLatestProfileIfIncomplete() =>
    volumeProfiles = array.size(_volumeProfiles)
    if volumeProfiles == _lookback
        profile = array.get(_volumeProfiles, volumeProfiles - 1)
        if profile.Incomplete
            for profileLine in profile.Lines
                line.delete(profileLine)
            for profileLinefill in profile.Linefills
                linefill.delete(profileLinefill)
            for profileBox in profile.Boxes
                box.delete(profileBox)
            label.delete(profile.InfoLabel)
            array.remove(_volumeProfiles, volumeProfiles - 1)

        sort(_scores, 0, order.ascending) // sort scores by row
updateProfilesIfNeeded() =>
    volumeProfiles = array.size(_volumeProfiles)
    if volumeProfiles == _lookback
        profile = array.remove(_volumeProfiles, 0)
        for profileLine in profile.Lines
            line.delete(profileLine)
        for profileLinefill in profile.Linefills
            linefill.delete(profileLinefill)
        for profileBox in profile.Boxes
            box.delete(profileBox)
        label.delete(profile.InfoLabel)

        sort(_scores, 0, order.ascending) // sort scores by row

        for i = 1 to _lookback - 1
            matrix.set(_scores, i, 0, i - 1) // make every score drop one row

        matrix.add_row(_scores, _lookback, array.from(_lookback - 1, 0, 0, 0)) // add empty row for new profile
        matrix.remove_row(_scores, 0) // remove earliest score
volumeProfile(m, metaData, incomplete) =>
    rows = matrix.rows(m)

    if rows > 0
        updateProfilesIfNeeded()

        bullVolumes = matrix.col(m, 0)
        bearVolumes = matrix.col(m, 1)
        lowPrices = matrix.col(m, 2)
        highPrices = matrix.col(m, 3)

        var row = 0
        setBarVolumePriceLevels(row, metaData)
        setMetaData(row, metaData)
        priceLevels = getPriceLevels(metaData)

        for [i, bullVolume] in bullVolumes
            bearVolume = array.get(bearVolumes, i)
            lowPrice = array.get(lowPrices, i)
            highPrice = array.get(highPrices, i)

            volumeMatrix = getVolumes(lowPrice, highPrice, bullVolume, bearVolume, metaData)
            updateBarVolumeProfile(row, volumeMatrix)

        if _interval.IncompleteCandles
            setMetaDataAfter(row, _interval.PreviousStart, _interval.PreviousEnd, _interval.PreviousStartTime, _interval.PreviousEndTime)
            _interval.IncompleteCandles := false
            _interval.PreviousIncompleteCandles := true
        else
            setMetaDataAfter(row, _interval.Start, _interval.End, _interval.StartTime, _interval.EndTime)
            _interval.PreviousIncompleteCandles := false

        visulize(matrix.row(_barVolumeProfile, row), row, incomplete)

        row += 1
isSupport(price) =>
    price < high
skipZone(i) =>
    var skips = str.split(_skipZones, ',')

    skip = array.indexof(skips, str.tostring(i))

    skip != -1
closestIsInRange(indices, types, index) =>
    size = array.size(indices)
    inRange = false

    if size > 0
        leftIndex = array.binary_search_leftmost(indices, index)
        left = array.get(types, leftIndex)
               
        if index >= left.StartBarIndex
          and index <= left.EndBarIndex
            // earlier bars
            inRange := true
        else
            // later bars
            rightIndex = array.binary_search_rightmost(indices, index)
            zoneInfo right = na
            if rightIndex < size
                right := array.get(types, rightIndex)
            else
                right := zoneInfo.new(-1, 0, false, last_bar_index + 1, last_bar_index + 2)

            if index >= right.StartBarIndex
              and index <= right.EndBarIndex
                inRange := true

    inRange
showZoneInfo(zoneInfos, txt, labelColor, minTransparancy, maxTransparancy, zoneLabels, int from = na) =>
    size = array.size(zoneInfos)
    if size > 0
        sorted = matrix.new<float>(size, 2)
        for [i, touch] in zoneInfos
            matrix.set(sorted, i, 0, i)
            matrix.set(sorted, i, 1, touch.Length)
        matrix.sort(sorted, 1, order.descending)
        
        sortedRows = matrix.col(sorted, 0)
        for [i, touch] in zoneInfos
            if touch.EndBarIndex < from
                continue
        
            lengthIndex = array.indexof(sortedRows, i)
            style = touch.IsLabelUp ? label.style_label_up : label.style_label_down
            transparancy = maxTransparancy
            if size > 1
                transparancy := maxTransparancy - ((((size - 1) - lengthIndex) * ((maxTransparancy - minTransparancy)) / (size - 1)))

            touchLabel = label.new(touch.BarIndex, touch.Price, txt, tooltip = str.format("bar length: {0}", touch.Length), style = style, color = color.new(labelColor, transparancy))

            array.push(zoneLabels, touchLabel)
showZoneTouch(profile) =>
    for touchIndex in profile.TouchIndices
        breaksAlso = closestIsInRange(profile.BreakoutIndices, profile.Breakouts, touchIndex)
        retestsAlso = closestIsInRange(profile.RetestIndices, profile.Retests, touchIndex)
        falseBreakoutAlso = closestIsInRange(profile.FalseBreakoutIndices, profile.FalseBreakouts, touchIndex)
            
        if not breaksAlso
         and not retestsAlso
         and not falseBreakoutAlso
            touchLabel = label.new(touchIndex, 0, "T", yloc = yloc.abovebar, color = color.new(color.white, 70))
            array.push(profile.ZoneLabels, touchLabel)
zoneTouchLabels(profile, int from = na) =>
    showZoneInfo(profile.Breakouts, "B", color.red, 40, 70, profile.ZoneLabels, from)
    showZoneInfo(profile.Retests, "R", color.blue, 40, 70, profile.ZoneLabels, from)
    showZoneInfo(profile.FalseBreakouts, "FB", color.orange, 40, 70, profile.ZoneLabels, from)

    if na(from)
        showZoneTouch(profile)
valueAreaPercentage(profile) =>
    ((profile.ValueAreaHigh - profile.ValueAreaLow) / (profile.High - profile.Low)) * 100
drawZone(profile, shown, entierVolume, support, index, scoreIndex, valueAreaPercentage) =>
    averageVolume = array.sum(_volumeProfilesVolumes) / array.size(_volumeProfilesVolumes)
    volumePercentage = ((profile.Volume / averageVolume) - 1) * 100

    valueAreaLowLine = line.new(last_bar_index, profile.ValueAreaLow, last_bar_index + 1, profile.ValueAreaLow, color = color.black, extend = extend.both)
    valueAreaHighLine = line.new(last_bar_index, profile.ValueAreaHigh, last_bar_index + 1, profile.ValueAreaHigh, color = color.black, extend = extend.both)
    zoneLinefill = linefill.new(valueAreaLowLine, valueAreaHighLine, support ? _zoneLinefillSupport : _zoneLinefillResistance)
    array.push(profile.ZoneLines, valueAreaLowLine)
    array.push(profile.ZoneLines, valueAreaHighLine)
    array.push(profile.ZoneLinefills, zoneLinefill)

    pocLine = line.new(bar_index, profile.Poc, bar_index + 1, profile.Poc, color = _pocLineColor, width = _pocLineWidth, style = line.style_dashed, extend = extend.both)
    array.push(profile.ZoneLines, pocLine)

    if _showZoneStopLoss
        switch support
            false =>
                stopLossLine = line.new(last_bar_index + 10, profile.ValueAreaHigh + _atr, bar_index + 20, profile.ValueAreaHigh + _atr, color = color.black)
                array.push(profile.ZoneLines, stopLossLine)
            true =>
                stopLossLine = line.new(last_bar_index + 10, profile.ValueAreaLow - _atr, bar_index + 20, profile.ValueAreaLow - _atr, color = color.black)
                array.push(profile.ZoneLines, stopLossLine)

    if _showZoneInfoLabel
        string txt = na
        if not _singleZone // only show scores and volume to mean if multiple profiles
            scoreString = volumeProfileScoreString(profile, scoreIndex)
            txt := str.format("{0} ({1})\nStart: {2}\nEnd: {3}\nVolume to mean: {4,number,##.#}%\nVA part: {5,number,##.#}%\nTouches: {6}\nRetests: {7}\nBreakouts: {8} ({9}){10}", index + 1, scoreIndex + 1, unixTimeToString(profile.StartTime), unixTimeToString(profile.EndTime), volumePercentage, valueAreaPercentage, array.size(profile.TouchIndices), array.size(profile.Retests), array.size(profile.BreakoutIndices), array.size(profile.FalseBreakouts), scoreString)
        else
            txt := str.format("Start: {0}\nEnd: {1}\nVA part: {2,number,##.#}%\nTouches: {3}\nRetests: {4}\nBreakouts: {5} ({6})", unixTimeToString(profile.StartTime), unixTimeToString(profile.EndTime), valueAreaPercentage, array.size(profile.TouchIndices), array.size(profile.Retests), array.size(profile.BreakoutIndices), array.size(profile.FalseBreakouts))

        zoneInfoLabel = label.new(last_bar_index + 10 + (shown % 2 ? 0 : 20), profile.ValueAreaHigh, txt, color = support ? color.new(color.green, 40) : color.new(color.red, 40))
        array.push(profile.ZoneLabels, zoneInfoLabel)

    if _showZoneTouchLabel
        zoneTouchLabels(profile)

    array.push(_zones, profile)
clearZones() =>
    for profile in _zones
        for zoneLine in profile.ZoneLines
            line.delete(zoneLine)
        array.clear(profile.ZoneLines)
        
        for zoneLinefill in profile.ZoneLinefills
            linefill.delete(zoneLinefill)
        array.clear(profile.ZoneLinefills)

        for zoneLabel in profile.ZoneLabels
            label.delete(zoneLabel)
        array.clear(profile.ZoneLabels)
drawZones() =>
    array.clear(_zones)

    entierVolume = array.sum(_volumeProfilesVolumes)

    if not _singleZone
        supportZones = 0, resistanceZones = 0
        volumeProfiles = array.size(_volumeProfiles)
        toI = volumeProfiles - 1 < 0 ? na : volumeProfiles - 1
        for i = 0 to toI
            scoreIndex = math.round(matrix.get(_scores, i, 0))

            if skipZone(scoreIndex)
                continue

            profile = array.get(_volumeProfiles, scoreIndex)

            support = isSupport(profile.ValueAreaLow)

            switch _zoneType
                "Support or resistance" =>
                    if support
                        if supportZones >= _supportZones
                          and resistanceZones >= _resistanceZones
                            break
                        else if supportZones >= _supportZones
                            continue
                    else
                        if resistanceZones >= _resistanceZones
                          and supportZones >= _supportZones
                            break
                        else if resistanceZones >= _resistanceZones
                            continue
                "Number" =>
                    if supportZones + resistanceZones == _zonesToShow
                        break

            if support
                supportZones += 1
            else
                resistanceZones += 1

            drawZone(profile, supportZones + resistanceZones, entierVolume, support, scoreIndex, i, valueAreaPercentage(profile))

        matrix.set(_summary, 0, 0, "zones:")
        matrix.set(_summary, 0, 1, str.tostring(supportZones) + "/" + str.tostring(resistanceZones))
    else
        
        profile = array.get(_volumeProfiles, 0)

        support = isSupport(profile.ValueAreaLow)

        profile.Score := matrix.get(_scores, 0, 1)
        drawZone(profile, 0, entierVolume, support, 0, 0, valueAreaPercentage(profile))
summaryTable() =>
    var rows = matrix.rows(_summary)
    if rows > 0
        var t = table.new(position.bottom_right, 2, rows, bgcolor = color.teal)
        table.clear(t, 0, 0, 1, rows - 1)
    
        for row = 0 to rows - 1
            if _showSummaryInfo
                info = matrix.get(_summary, row, 0)
                table.cell(t, 0, row, info, text_size = size.small)
        
            value = matrix.get(_summary, row, 1)
            table.cell(t, 1, row, value, text_size = size.small)
getVolumeProfiles() =>
    array<volumeProfile> volumeProfiles = na

    alertVolumeProfiles = array.size(_alertVolumeProfiles)
    if alertVolumeProfiles > 0
        volumeProfiles := _alertVolumeProfiles
    else
        volumeProfiles := _volumeProfiles

    volumeProfiles
setBreaks(volumeProfiles) =>
    for profile in volumeProfiles
        // /
        if low > profile.ValueAreaHigh
          and low[1] < profile.ValueAreaHigh
            for i = 1 to _zoneBreaksLookback + 2
                if high[i] < profile.ValueAreaLow
                    if bar_index - i > profile.End
                        breakSize = array.size(profile.BreakoutIndices)
                        breakValue = -1
                        if breakSize != 0
                            breakIndex = array.binary_search_leftmost(profile.BreakoutIndices, bar_index) 
                            breakValue := array.get(profile.BreakoutIndices, breakIndex)

                        if breakValue < bar_index - i // did not break before
                            profile.LatestBullBreakoutConfirmationBarIndex := bar_index
                            array.push(profile.Breakouts, zoneInfo.new(bar_index, profile.ValueAreaLow, true, bar_index - i, bar_index, i - 1))
                            array.push(profile.BreakoutIndices, bar_index)

                        while true
                            falseBreakSize = array.size(profile.FalseBreakoutIndices)
                            if falseBreakSize != 0
                                falseBreakIndex = array.binary_search_leftmost(profile.FalseBreakoutIndices, bar_index) 
                                falseBreakValue = array.get(profile.FalseBreakoutIndices, falseBreakIndex)

                                if falseBreakValue >= bar_index - i
                                    array.remove(profile.FalseBreakouts, falseBreakIndex)
                                    array.remove(profile.FalseBreakoutIndices, falseBreakIndex) // also registered as a false breakout
                                    continue
                            break

                    break
                else if low[i] > profile.ValueAreaHigh // already added
                    break

        // \
        if high < profile.ValueAreaLow
          and high[1] > profile.ValueAreaLow
            for i = 1 to _zoneBreaksLookback + 2
                if low[i] > profile.ValueAreaHigh
                    if bar_index - i > profile.End
                        breakSize = array.size(profile.BreakoutIndices)
                        breakValue = -1
                        if breakSize != 0
                            breakIndex = array.binary_search_leftmost(profile.BreakoutIndices, bar_index) 
                            breakValue := array.get(profile.BreakoutIndices, breakIndex)

                        if breakValue < bar_index - i // did not break before
                            profile.LatestBearBreakoutConfirmationBarIndex := bar_index
                            array.push(profile.Breakouts, zoneInfo.new(bar_index, profile.ValueAreaLow, true, bar_index - i, bar_index, i - 1))
                            array.push(profile.BreakoutIndices, bar_index)


                        while true
                            falseBreakSize = array.size(profile.FalseBreakoutIndices)
                            if falseBreakSize != 0
                                falseBreakIndex = array.binary_search_leftmost(profile.FalseBreakoutIndices, bar_index) 
                                falseBreakValue = array.get(profile.FalseBreakoutIndices, falseBreakIndex)

                                if falseBreakValue >= bar_index - i
                                    array.remove(profile.FalseBreakouts, falseBreakIndex)
                                    array.remove(profile.FalseBreakoutIndices, falseBreakIndex) // also registered as a false breakout
                                    continue
                            break
                            
                    break
                else if high[i] < profile.ValueAreaLow // already added
                    break
setRetests(volumeProfiles) =>
    for profile in volumeProfiles
        highZoneLimit = profile.ValueAreaHigh + (_atr * _zoneRetestsAtrFactorTolerance)

        // \/
        if low > highZoneLimit
          and low[1] < highZoneLimit
            int lowestBarIndex = na
            for i = 1 to _zoneRetestsLookback + 1
                if low[i] < profile.ValueAreaLow
                    break

                if low[i] < low[bar_index - lowestBarIndex]
                      or na(lowestBarIndex)
                    lowestBarIndex := bar_index - i

                if low[i] > highZoneLimit
                    if bar_index - i <= profile.End
                        break
                    
                    exists = array.includes(profile.RetestIndices, lowestBarIndex)
                    if exists
                        break

                    profile.LatestBearRetestConfirmationBarIndex := bar_index
                    array.push(profile.Retests, zoneInfo.new(lowestBarIndex, low[bar_index - lowestBarIndex] - _atr, true, bar_index - i, bar_index, i - 1))
                    array.push(profile.RetestIndices, lowestBarIndex)
                    break

        lowZoneLimit = profile.ValueAreaLow - (_atr * _zoneRetestsAtrFactorTolerance)
        // /\
        if high < lowZoneLimit
          and high[1] > lowZoneLimit
            int highestBarIndex = na
            for i = 1 to _zoneRetestsLookback + 1
                if high[i] > profile.ValueAreaHigh
                    break

                if high[i] > high[bar_index - highestBarIndex]
                      or na(highestBarIndex)
                    highestBarIndex := bar_index - i

                if high[i] < lowZoneLimit
                    if bar_index - i <= profile.End
                        break
                    
                    exists = array.includes(profile.RetestIndices, highestBarIndex)
                    if exists
                        break

                    profile.LatestBullRetestConfirmationBarIndex := bar_index
                    array.push(profile.Retests, zoneInfo.new(highestBarIndex, high[bar_index - highestBarIndex] + _atr, false, bar_index - i, bar_index, i - 1))
                    array.push(profile.RetestIndices, highestBarIndex)
                    break
setFalseBreaks(volumeProfiles) =>
    for profile in volumeProfiles
        //   /'\    \
        //  /   \    \  /'\
        // /      or  \/   \ 
        if high < profile.ValueAreaLow
          and high[1] > profile.ValueAreaLow
            lowBreak = false, highBreak = false, highestBarIndex = bar_index
            for i = 1 to _zoneFalseBreaksLookback + 1
                if high[i] > profile.ValueAreaHigh
                    highBreak := true

                if low[i] > profile.ValueAreaHigh
                    lowBreak := true

                if high[i] > high[bar_index - highestBarIndex]
                    highestBarIndex := bar_index - i

                if highBreak
                  and high[i] < profile.ValueAreaLow
                    if bar_index - i <= profile.End
                        break
                    
                    exists = array.includes(profile.FalseBreakoutIndices, highestBarIndex)
                    if exists
                        break

                    profile.LatestBullFalseBreakoutConfirmationBarIndex := bar_index
                    array.push(profile.FalseBreakouts, zoneInfo.new(highestBarIndex, high[bar_index - highestBarIndex], false, bar_index - i, bar_index, i - (bar_index - highestBarIndex)))
                    array.push(profile.FalseBreakoutIndices, highestBarIndex)
                    if lowBreak
                        while true
                            breakSize = array.size(profile.BreakoutIndices)
                            if breakSize != 0
                                breakIndex = array.binary_search_leftmost(profile.BreakoutIndices, highestBarIndex) 
                                breakValue = array.get(profile.BreakoutIndices, breakIndex)

                                if breakValue >= bar_index - i
                                    array.remove(profile.Breakouts, breakIndex)
                                    array.remove(profile.BreakoutIndices, breakIndex) // also registered as a breakout
                                    profile.LatestBearBreakoutConfirmationBarIndex := -1
                                    continue
                            break
                    break

                if high[i] < profile.ValueAreaLow // already added
                    break
        // \          /\   /
        //  \   /    /  \./
        //   \./ or /
        if low > profile.ValueAreaHigh
          and low[1] < profile.ValueAreaHigh
            lowBreak = false, highBreak = false, lowestBarIndex = bar_index
            for i = 1 to _zoneFalseBreaksLookback + 1
                if low[i] < profile.ValueAreaLow
                    lowBreak := true
   
                if high[i] < profile.ValueAreaLow
                    highBreak := true

                if low[i] < low[bar_index - lowestBarIndex]
                    lowestBarIndex := bar_index - i

                if lowBreak
                  and low[i] > profile.ValueAreaHigh
                    if bar_index - i <= profile.End
                        break
                    
                    exists = array.includes(profile.FalseBreakoutIndices, lowestBarIndex)
                    if exists
                        break

                    profile.LatestBearFalseBreakoutConfirmationBarIndex := bar_index
                    array.push(profile.FalseBreakouts, zoneInfo.new(lowestBarIndex, low[bar_index - lowestBarIndex], true, bar_index - i, bar_index, i - (bar_index - lowestBarIndex)))
                    array.push(profile.FalseBreakoutIndices, lowestBarIndex)
                    if highBreak
                        while true
                            breakSize = array.size(profile.BreakoutIndices)
                            if breakSize != 0
                                breakIndex = array.binary_search_leftmost(profile.BreakoutIndices, lowestBarIndex) 
                                breakValue = array.get(profile.BreakoutIndices, breakIndex)

                                if breakValue >= bar_index - i
                                    array.remove(profile.Breakouts, breakIndex)
                                    array.remove(profile.BreakoutIndices, breakIndex) // also registered as a breakout
                                    profile.LatestBullBreakoutConfirmationBarIndex := -1
                                    continue
                            break
                    break

                if low[i] > profile.ValueAreaHigh // already added
                    break
setTouches(volumeProfiles) =>
    for [i, profile] in volumeProfiles
        lowPrice = profile.ValueAreaLow
        highPrice = profile.ValueAreaHigh

        touchBullTrend = (low < highPrice and high > lowPrice)
          and ((low[1] < lowPrice and high[1] < lowPrice) or (bar_index - 1 == profile.End or bar_index == profile.End))
        touchBearTrend = (low < highPrice and high > lowPrice)
          and ((low[1] > highPrice and high[1] > highPrice) or (bar_index - 1 == profile.End or bar_index == profile.End))

        if touchBullTrend
          or touchBearTrend
            if bar_index > profile.End
                array.push(profile.TouchIndices, bar_index)
                if touchBullTrend
                    profile.LatestBullTouchConfirmationBarIndex := bar_index
                else
                    profile.LatestBearTouchConfirmationBarIndex := bar_index
toScoreArray(arr) =>
    var scoreStrings = str.split(arr, ',')

    var numScores = array.size(scoreStrings)
    scores = array.new<float>(numScores)
    for [i, scoreString] in scoreStrings
        scoreFloat = str.tonumber(scoreString)
        array.set(scores, i, scoreFloat)

    scores
updateScore(addedScore, row, source, bool end = true, int scoreNum = -1, int scores = -1) =>
    profile = array.get(_volumeProfiles, row)

    score = matrix.get(_scores, row, end ? 2 : 1)
    score += addedScore
    matrix.set(_scores, row, end ? 2 : 1, score)

    scoreString = scoreNum != -1 and scores != -1 ? str.format("({0}/{1} ({2,number,#.#}))", scoreNum, scores, addedScore) : str.format("({0,number,#.#})", addedScore)
    txt = str.format("{0} {1}", scoreString, source)

    array.push(profile.ScoresGiven, txt)
updateScores(m, scores, source, int scoreIndex = 1, int rowIndex = 0, bool skipZero = true, int factorIndex = -1) =>
    allScores = array.copy(scores)
    scoresTotalSize = array.size(allScores)
    scoreNum = 1

    rows = matrix.rows(m)
    toI = rows > 0 ? rows - 1 : na
    for i = 0 to toI
        scoresSize = array.size(allScores)
        if scoresSize == 0
            break // no more scores to give out

        addedScore = array.get(allScores, 0)
        value = matrix.get(m, i, scoreIndex), nextValue = 0.0
        if i != toI
            nextValue := matrix.get(m, i + 1, scoreIndex)

        if na(value)
          or value == _intMax
          or value == _intMin
          or (skipZero and value == 0)
            break
        
        row = math.round(matrix.get(m, i, rowIndex))

        if row == _intMax
          or row == _intMin
            break

        if factorIndex != -1
            factor = matrix.get(m, i, factorIndex)
            addedScore := addedScore * factor

        if addedScore != 0
            updateScore(addedScore, row, source, scoreNum = scoreNum, scores = scoresTotalSize)

        if value != nextValue
            array.remove(allScores, 0)
            scoreNum += 1
getVolumeProfileTouches(profile) =>
    highTouch = high > profile.ValueAreaLow
      and high < profile.ValueAreaHigh
    lowTouch = low > profile.ValueAreaLow
      and low < profile.ValueAreaHigh
    barBreaksTouch = high > profile.ValueAreaHigh
      and low < profile.ValueAreaLow

    [highTouch, lowTouch, barBreaksTouch]
isInRange(value, min, max) =>
    inRange = false

    if value >= min
      and value <= max
        inRange := true

    inRange
setBarsInZone() =>
    for [i, profile] in _volumeProfiles
        if profile.End == bar_index
            continue

        [highTouch, lowTouch, barBreaksTouch] = getVolumeProfileTouches(profile)

        if highTouch
          or lowTouch
          or barBreaksTouch
            profile.BarsInZone += 1
addToMaxScore(score) =>
    var called = false
    if not called
        _scoreMetaData.Max += score
        called := true
setZoneReactionScore() =>
    addToMaxScore(_reactionScore)
    var previousProfileIndex = 0
    profiles = array.size(_volumeProfiles)
    if profiles > previousProfileIndex
        profile = array.get(_volumeProfiles, previousProfileIndex)// + 1)
        endHistoryIndex = bar_index - profile.End
        if endHistoryIndex == _reactionLookahead
            atrFactorSize = _atr * _reactionAtrFactor

            averageReactionSizeBull = (high - profile.ValueAreaHigh) / _reactionLookahead
            averageReactionSizeBear = (profile.ValueAreaLow - low) / _reactionLookahead

            previousProfile = volumeProfile.new()
            previousProfile.Scores := array.new<int>()
            if previousProfileIndex > 1
                previousProfile := array.get(_volumeProfiles, previousProfileIndex - 1)

            reaction = math.max(averageReactionSizeBull, averageReactionSizeBear)
            reactionTrendCode = reaction == averageReactionSizeBull ? _scoreMetaDataReactionBullTrend : _scoreMetaDataReactionBearTrend
            if reaction > atrFactorSize
              and not array.includes(previousProfile.Scores, reactionTrendCode) // same trend on previous profile
                updateScore(_reactionScore, previousProfileIndex, "Base reaction", end = false)

            array.push(profile.Scores, reactionTrendCode)
            previousProfileIndex += 1
setValueAreaVolumeScoreEnd() =>
    addToMaxScore(array.max(toScoreArray(_valuAreaVolumeScores)))
    values = matrix.new<float>(_lookback, 2, _intMin) // row, volume
    for [i, profile] in _volumeProfiles
        matrix.set(values, i, 0, i)
        matrix.set(values, i, 1, profile.ValueAreaVolume)

    sort(values, 1, order.descending)
    updateScores(values, toScoreArray(_valuAreaVolumeScores), "VA Volume")
setClosestPriceScoreEnd() =>
    addToMaxScore(array.max(toScoreArray(_closestPriceScores)))
    values = matrix.new<float>(_lookback, 3, _intMax) // row, poc price to current price ratio, factor
    for [i, profile] in _volumeProfiles
        matrix.set(values, i, 0, i)

        priceDiff = math.abs(close - profile.Poc)
        matrix.set(values, i, 1, priceDiff)

        if i > (_lookback - 1) * (_closestPriceScorePercentageLimit * 0.01)
            matrix.set(values, i, 2, 0)
        else
            matrix.set(values, i, 2, ((_lookback - 1) - i) / (_lookback - 1))

    sort(values, 1, order.ascending)
    updateScores(values, toScoreArray(_closestPriceScores), "Closest price", factorIndex = 2)
setValueAreaPartScoreEnd() =>
    addToMaxScore(array.max(toScoreArray(_valueAreaPartScores)))
    values = matrix.new<float>(_lookback, 2, _intMax) // row, va part
    for [i, profile] in _volumeProfiles
        valueAreaPart = (profile.ValueAreaHigh - profile.ValueAreaLow) / (profile.High - profile.Low)

        matrix.set(values, i, 0, i)
        matrix.set(values, i, 1, valueAreaPart)

    sort(values, 1, order.ascending)
    updateScores(values, toScoreArray(_valueAreaPartScores), "VA part")
setBarsInZoneScoreEnd() =>
    addToMaxScore(array.max(toScoreArray(_barsInZoneScores)))
    values = matrix.new<float>(_lookback, 2, _intMax) // row, bars
    virginPocs = 0 // for summary table
    for [i, profile] in _volumeProfiles
        if profile.BarsInZone == 0
            virginPocs += 1
            continue // score given sepparetly

        matrix.set(values, i, 0, i)
        matrix.set(values, i, 1, profile.BarsInZone)

    sort(values, 1, order.ascending)
    updateScores(values, toScoreArray(_barsInZoneScores), "Bars in zone", skipZero = false)

    matrix.set(_summary, 1, 0, "virgin zones (POCs):")
    matrix.set(_summary, 1, 1, str.tostring(virginPocs))
setVirginPocScoreEnd() =>
    s = ""
    for [i, profile] in _volumeProfiles
        if profile.BarsInZone == 0
          and i != _lookback - 1
            score = _virginPocScore * ((_lookback - 1) - i) / (_lookback - 1) // more weigth on earler profiles
            s += str.format("\n{0} {1}", i, score)

            updateScore(score, i, "Virgin POC")
setPivotScoreEnd() =>
    addToMaxScore(_pivotScore)
    toPivots = array.size(_pivots) - 1
    jj = 0, previousJj = 0 // skip outside of range pivots
    end = false
    for [i, profile] in _volumeProfiles
        if end
            break
        for j = jj to toPivots
            if j > toPivots
                end := true
                break // last pivot

            pivot = array.get(_pivots, j)
            if isInRange(pivot, profile.Start, profile.End)
                if j == previousJj // set one score, even if multiple breaks
                    updateScore(_pivotScore, i, "Base pivot")
                jj += 1
                continue
            else
                break // not inside the profile, move on

        previousJj := jj
setPriceAwayAndBarsAway() =>
    for [i, profile] in _volumeProfiles
        if profile.End == bar_index
          or profile.IsTouched
            continue

        if low < profile.MinimumBeforeTouch
            profile.MinimumBeforeTouch := low

        if high > profile.MaximumBeforeTouch
            profile.MaximumBeforeTouch := high

        zoneWidth = profile.ValueAreaHigh - profile.ValueAreaLow
        priceAwayWidth = profile.MinimumBeforeTouch < profile.ValueAreaLow ? profile.ValueAreaLow - profile.MinimumBeforeTouch : profile.MaximumBeforeTouch - profile.ValueAreaHigh
        riskRewardRatio = priceAwayWidth / zoneWidth

        profile.PriceAwayUntilFirstTouch := riskRewardRatio
        profile.BarsAwayUntilFirstTouch := bar_index - profile.End

        if bar_index < profile.End + _priceAwayScoreMargin
            continue

        [highTouch, lowTouch, barBreaksTouch] = getVolumeProfileTouches(profile)

        if highTouch
          or lowTouch
          or barBreaksTouch
            profile.IsTouched := true
setPriceAwayScoreEnd() =>
    addToMaxScore(array.max(toScoreArray(_priceAwayScores)))
    values = matrix.new<float>(_lookback, 2, _intMin) // row, price away
    for [i, profile] in _volumeProfiles
        if profile.PriceAwayUntilFirstTouch < 0
            continue
        matrix.set(values, i, 0, i)
        matrix.set(values, i, 1, profile.PriceAwayUntilFirstTouch)
    sort(values, 1, order.descending)
    updateScores(values, toScoreArray(_priceAwayScores), "Price away")
setBarsAwayScoreEnd() =>
    addToMaxScore(array.max(toScoreArray(_barsAwayScores)))
    values = matrix.new<float>(_lookback, 2, _intMin) // row, bars away
    for [i, profile] in _volumeProfiles
        if na(profile.BarsAwayUntilFirstTouch)
            continue
        matrix.set(values, i, 0, i)
        matrix.set(values, i, 1, profile.BarsAwayUntilFirstTouch)

    sort(values, 1, order.descending)
    updateScores(values, toScoreArray(_barsAwayScores), "Bars away")//, scoreIndex = 1)
setScores() =>
    setBarsInZone()
    setPriceAwayAndBarsAway()

    // odd enhancers:
    setZoneReactionScore()
setEndScores() =>
    // odd enhancers:
    setVirginPocScoreEnd()
    setBarsInZoneScoreEnd()
    setValueAreaPartScoreEnd()
    setValueAreaVolumeScoreEnd()
    setClosestPriceScoreEnd()
    setPivotScoreEnd()
    setPriceAwayScoreEnd()
    setBarsAwayScoreEnd()
setTotalScores() =>
    for i = 0 to matrix.rows(_scores) - 1
        matrix.set(_scores, i, 0, i)

        score = matrix.get(_scores, i, 1)
        scoreEnd = matrix.get(_scores, i, 2)
        matrix.set(_scores, i, 2, 0) // reset end scores

        totalScore = score + scoreEnd
        matrix.set(_scores, i, 3, totalScore)
sortScoresByScore() =>
    sort(_scores, 3, order.descending)

    standardDeviation = array.stdev(matrix.col(_scores, 0))
    matrix.set(_summary, 2, 0, "score std dev %:")
    matrix.set(_summary, 2, 1, str.format("{0,number,##.#}%", (standardDeviation / _scoreMetaData.Max) * 100))
validateAlert(alert) =>
    if alert
      and _lastRangeBarIndex == -1
        runtime.error("At the moment alerts can only be set if 'Last range bar index' is set (this is due to var/varip and unknown pine history error).")
validateActive(active) =>
    if active
      and bar_index == 0
        runtime.error("Is active at bar 0, probably not enough data available (choose shorter lookback, narrower range or view at a higher timeframe)")
showPivots(isLow, barIndex, price) =>
    var float previousPivotPrice = na
    var int previousPivotBarIndex = na

    if isLow
        label.new(barIndex, price, "▼", style = label.style_label_up, color = color.new(color.red, 70))

        if not na(previousPivotBarIndex)
            line.new(previousPivotBarIndex, previousPivotPrice, barIndex, price, color = color.new(color.orange, 80), style = line.style_dashed)

        previousPivotBarIndex := barIndex
        previousPivotPrice := price
    else
        label.new(barIndex, price, "▲", color = color.new(color.green, 70))

        if not na(previousPivotBarIndex)
            line.new(previousPivotBarIndex, previousPivotPrice, barIndex, price, color = color.new(color.green, 80), style = line.style_dashed)

        previousPivotBarIndex := barIndex
        previousPivotPrice := price
setPivots(active) =>
    pivotLow = ta.pivotlow(low, _pivotBars, _pivotBars)
    pivotHigh = ta.pivothigh(high, _pivotBars, _pivotBars)

    if active
        var int firstActiveBarIndex = na
        if na(firstActiveBarIndex)
            firstActiveBarIndex := bar_index

        pivotBarIndex = bar_index - _pivotBars

        if (not na(pivotHigh) or not na(pivotLow))
          and pivotBarIndex >= firstActiveBarIndex
            if _showPivots
                lowPivot = not na(pivotLow)
                price = not na(pivotLow) ? low[bar_index - pivotBarIndex] : high[bar_index - pivotBarIndex]
                showPivots(lowPivot, pivotBarIndex, price)

            array.push(_pivots, pivotBarIndex)
setVolumeProfileScores() =>
    for [profileIndex, profile] in _volumeProfiles
        for [scoreIndex, score] in _scores
            row = math.round(array.get(score, 0))
            if row == profileIndex
                profile.Score := matrix.get(_scores, scoreIndex, 3)
                break
getTrendString(bullBarIndex, bearBarIndex) =>
    string trendString = na, string trend = na
    delayString = _alertDelay != 0 ? str.format(", {0} bars delay", _alertDelay) : ""
    if bullBarIndex == bar_index - _alertDelay
        trendString := str.format("(from bull trend{0})", delayString)
        trend := "Bull"
    else if bearBarIndex == bar_index - _alertDelay
        trendString := str.format("(from bear trend{0})", delayString)
        trend := "Bear"

    [trend, trendString]
_alertTexts = array.new<string>()
addAlert(type, latestBullRetestConfifmation, latestBearRetestConfifmation, profile, string extraText = "") =>
    if type == _alertOn
      or _alertOn == "All"
        [trend, trendText] = getTrendString(latestBullRetestConfifmation, latestBearRetestConfifmation)
        if _alertDirection == trend
          or _alertDirection == "All"
            txt = str.format("{0} {1}{2} (zone from {3} {4} to {5} {6}, between {7} and {8})", type, trendText, extraText == "" ? "" : str.format(" {0}", extraText), unixTimeToString(profile.StartTime), syminfo.timezone, unixTimeToString(profile.EndTime), syminfo.timezone, str.tostring(profile.ValueAreaLow), str.tostring(profile.ValueAreaHigh))

            array.push(_alertTexts, txt)
sendAlert() =>
    alerts = array.size(_alertTexts)
    if alerts > 0
        string alertText = na
        for [i, txt] in _alertTexts
            if i == 0
                alertText := txt
            else
                alertText += "\n" + txt

        switch _alertFrequency
            "All" =>
                alert(alertText, alert.freq_all)
            "Once per bar" =>
                alert(alertText, alert.freq_once_per_bar)
            "Once per bar close" =>
                alert(alertText, alert.freq_once_per_bar_close)
alertIfInZone() =>
    for profile in _alertVolumeProfiles
        if profile.LatestBullTouchConfirmationBarIndex == bar_index - _alertDelay
          or profile.LatestBearTouchConfirmationBarIndex == bar_index - _alertDelay
            addAlert("Entry/crossing", profile.LatestBullTouchConfirmationBarIndex, profile.LatestBearTouchConfirmationBarIndex, profile)
alertIfRetest() =>
    for profile in _alertVolumeProfiles
        if profile.LatestBullRetestConfirmationBarIndex == bar_index - _alertDelay
          or profile.LatestBearRetestConfirmationBarIndex == bar_index - _alertDelay
            retests = array.size(profile.Retests)
            retest = array.get(profile.Retests, retests - 1)
            addAlert("Retest", profile.LatestBullRetestConfirmationBarIndex, profile.LatestBearRetestConfirmationBarIndex, profile, str.format("({0} bars)", retest.Length))
alertIfBreakout() =>
    for profile in _alertVolumeProfiles
        if profile.LatestBullBreakoutConfirmationBarIndex == bar_index - _alertDelay
          or profile.LatestBearBreakoutConfirmationBarIndex == bar_index - _alertDelay
            breakouts = array.size(profile.Breakouts)
            breakout = array.get(profile.Breakouts, breakouts - 1)
            addAlert("Breakout", profile.LatestBullBreakoutConfirmationBarIndex, profile.LatestBearBreakoutConfirmationBarIndex, profile, str.format("({0} bars)", breakout.Length))
alertIfBreakoutAndX(profile, latestBullRetestConfifmation, latestBearRetestConfifmation, retestIndices, string source = na) =>
    if latestBullRetestConfifmation == bar_index - _alertDelay
      or latestBearRetestConfifmation == bar_index - _alertDelay // alert only on retest and not on every bar after 
        latestRetestIndex = array.size(retestIndices) - 1
        if latestRetestIndex >= 0 // check has done retest
            latestBreakIndex = array.size(profile.BreakoutIndices) - 1
            if latestBreakIndex >= 0 // check has broken
            
                latestRetestBarIndex = array.get(retestIndices, latestRetestIndex)
                latestBreakoutBarIndex = array.get(profile.BreakoutIndices, latestBreakIndex)
            
                if latestRetestBarIndex > latestBreakoutBarIndex
                    string retestString = na, retests = 1
                    if latestRetestIndex != 0
                        for i = latestRetestIndex - 1 to 0
                            retest = array.get(retestIndices, i)
                            if retest > latestBreakoutBarIndex
                                retests += 1
                            else
                                break

                    if retests == 1
                        retestString := str.format("({0}) ({1} bars from break)", source, latestRetestBarIndex - latestBreakoutBarIndex)
                    else
                        firstRetestBarIndex = array.get(profile.RetestIndices, 0)
                        retestString := str.format("({0}) ({1} {2}s within {3} bars, {4} bars from break)", source, retests, source, latestRetestBarIndex - firstRetestBarIndex, latestRetestBarIndex - latestBreakoutBarIndex, latestBreakoutBarIndex)

                    addAlert("Breakout and retest", latestBearRetestConfifmation, latestBullRetestConfifmation, profile, retestString) // reverse the latest retest/false break because the break is the trend
alertIfBreakoutAndRetest() =>
    for profile in _alertVolumeProfiles
        alertIfBreakoutAndX(profile, profile.LatestBullRetestConfirmationBarIndex, profile.LatestBearRetestConfirmationBarIndex, profile.RetestIndices, "Retest")
        alertIfBreakoutAndX(profile, profile.LatestBullFalseBreakoutConfirmationBarIndex, profile.LatestBearFalseBreakoutConfirmationBarIndex, profile.FalseBreakoutIndices, "false breakout")
alertIfFalseBreakout() =>
    for profile in _alertVolumeProfiles
        if profile.LatestBullFalseBreakoutConfirmationBarIndex == bar_index - _alertDelay
          or profile.LatestBearFalseBreakoutConfirmationBarIndex == bar_index - _alertDelay
            falseBreakouts = array.size(profile.FalseBreakouts)
            falseBreakout = array.get(profile.FalseBreakouts, falseBreakouts - 1)
            addAlert("False breakout", profile.LatestBullFalseBreakoutConfirmationBarIndex, profile.LatestBearFalseBreakoutConfirmationBarIndex, profile, str.format("({0} bars)", falseBreakout.Length))
setAlerts() =>
    switch _alertOn
        "Entry/crossing" =>
            alertIfInZone()
        "Retest" =>
            alertIfRetest()
        "Breakout" =>
            alertIfBreakout()
        "Breakout and retest" =>
            alertIfBreakoutAndRetest()
        "False breakouts" =>
            alertIfFalseBreakout()
        "All" =>
            alertIfInZone()
            alertIfRetest()
            alertIfBreakout()
            alertIfBreakoutAndRetest()
            alertIfFalseBreakout()
setAlertZones() =>
    zones = array.size(_alertVolumeProfiles)
    if zones == 0
        for profile in _zones
            array.push(_alertVolumeProfiles, profile)
GetIndicatorBackground(active, alert) =>
    color background = na

    if alert
        if active
            background := color.new(color.red, 90)
    else if _visualizeRange
      and active
        background := color.new(color.green, 90)

    background
showPastZonesTouchLabels() =>
    for profile in _zones
        zoneTouchLabels(profile, bar_index)
clearScoresGiven() =>
    for profile in _volumeProfiles
        array.clear(profile.ScoresGiven)
showScore() =>
    profiles = array.size(_volumeProfiles)
    for [i, score] in _scores
        if i == profiles
            break
        scoreRow = math.round(array.get(score, 0))
        profile = array.get(_volumeProfiles, scoreRow)
        previousProfileString = i > 0 ? str.format("{0,number,#.#}%>", (-(profile.Score - matrix.get(_scores, i - 1, 3)) / profile.Score) * 100) : ""
        nextProfileString = i < profiles - 1 ? str.format(">{0,number,#.#}%", (1 - (matrix.get(_scores, i + 1, 3) / profile.Score)) * 100) : ""
        box.set_text(profile.AreaBox, str.format("{0}\n{1}{2,number,#.#}{3}", i + 1, previousProfileString, profile.Score, nextProfileString))
rangeBar = request.security(syminfo.tickerid, _timeFrame, isActive(_lastRangeBarIndex, _lastRangeBarIndexFromEnd), lookahead = barmerge.lookahead_on)

alert = _alertOn != "None"
alertAndLastBar = alert
  and barstate.islast
incomplete = rangeBar.LastActiveBarTimeClose != _maxUnixTime
  and rangeBar.LastActiveBarTimeClose > _timeNow
  and time != rangeBar.StartTime // only one bar, don't draw profile
active = rangeBar.Active
previousActive = active[1]
previousTimeClose = time_close[1]

validateAlert(alert)

if incomplete
  and not alertAndLastBar[1]
    removeLatestProfileIfIncomplete()

setPivots(active)

[bullVolumes, bearVolumes, lowPrices, highPrices, openTimes] = request.security_lower_tf(syminfo.tickerid, _barVolumeProfileTimeFrame, getData(active))

if rangeBar.Active
  and not alertAndLastBar[1]
    validateActive(active)
    setInterval()

    setVolumeProfileData(active, bullVolumes, bearVolumes, lowPrices, highPrices, openTimes)

    m = getVolumeProfileMatrix(active, previousActive, previousTimeClose, incomplete)
    metaData = getMetaData(m)
    volumeProfile(m, metaData, incomplete)

volumeProfiles = getVolumeProfiles()
setBreaks(volumeProfiles)
setRetests(volumeProfiles)
setFalseBreaks(volumeProfiles)
setTouches(volumeProfiles)

if not _singleZone
  and not alertAndLastBar[1]
    setScores()

var done = false
if barstate.islast
  and not (done and alert)
    if not (_singleZone or _singleLookback)
        setEndScores()
        setTotalScores()
        sortScoresByScore()
        setVolumeProfileScores()

        clearZones()
        drawZones()

    if _showVolumeProfileInfoLabel
        updateVolumeProfileInfoLabels()

    if not (_singleZone or _singleLookback)
        clearScoresGiven()

        if _showSummary
            summaryTable()

        if _showScore
            showScore()

    done := true

if alertAndLastBar
    setAlertZones()
    setAlerts()
    sendAlert()

if _showZoneTouchLabel
  and rangeBar.HasBeenActive
  and not alertAndLastBar[1]
    showPastZonesTouchLabels()

bgcolor(GetIndicatorBackground(active, alert))