Pivot Pattern Boundaries [cajole]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © cajole
//@version=5
// hat tip to 'Liquidity by makuchaku & eFe', daughter of 'Pivots MTF [LucF]' for the basic architecture.
indicator("Pivot Pattern Boundaries [cajole]", shorttitle='Pivot boundaries', overlay = true, max_lines_count=500)
i_prd         = input.int(3, minval=1, title="Pivot Period", inline='pvt')
i_ATRtol     = input.float(0.125, minval=0, maxval=1, inline='pvt',    
			 title="Tolerance [ATR]", tooltip="Fraction of ATR used to define when 2 prices are equal. ATR calculated over 'expiry' window.") 
i_labelPvts= input.bool(false,     title="Label every pivot", group='Plot', inline='pvt')
i_levelPersist= input.bool(false,     title="Let levels persist after crossing", group='Plot', inline='pvt')
i_showLines= input.bool(defval=true, title='Show lines:',   group='Plot', inline='col')
i_pvtTopColor = input.color(defval=color.new(color.fuchsia, 30),  title='Resistance', group='Plot', inline='col')
i_pvtBtmColor = input.color(defval=color.new(color.fuchsia, 30),     title='Support', group='Plot', inline='col')
i_showTol     = input.bool(defval=true, title='Shade tolerance',    group='Plot', inline='col')
i_minTouches  = input.int(defval=2, title='Min. touches', group='Plot', inline='tch')
i_minLength   = input.int(defval=25, title='Min. length', tooltip='Lines shorter than this will be hidden.', group='Plot', inline='tch')
i_expiry  =     input.int(defval=200, title='Level expiry', tooltip='Max. no. of bars to search for min. touches before giving up', group='Plot', inline='tch')
i_pvtStyle = input.string(defval=line.style_solid,    title='Line style', group='Plot', inline='lines',
			     options=[line.style_solid, line.style_dotted, line.style_dashed, line.style_arrow_left, 
		    	 line.style_arrow_right, line.style_arrow_both])
i_pvtLThick   = input.int(defval=2,    title='Pivot line thickness', group='Plot', inline='lines')
i_maxHiLines  = input.int(defval=50,   title='Max. resistance lines', minval=1, maxval=500, group='Plot')
i_maxLoLines  = input.int(defval=50,   title='Max. support lines', minval=1, maxval=500, group='Plot')
 
// Transform inputs
//tol = i_tolPct/100 * ta.median(close, 200) // Resolution below which 2 prices / pivots considered the same. 
//tol = i_tolPct/100 * (ta.highest(high, 50) - ta.lowest(low, 50)) // Resolution below which 2 prices / pivots considered the same. 
tol = i_ATRtol * ta.atr(i_expiry) // Resolution below which 2 prices / pivots considered the same. 

// FUNCTIONS
_levelCurrentlyActive(_level, _linesD, _linesU, _touchesX) => 
    // Check all lines in array _lines for y1 == y2 == _level. 
    _levelFound = 0
    if array.size(_linesU) > 0
        for i = 0 to array.size(_linesU)-1 by 1
            _touchX	= array.get(_touchesX, i)
            _lineD 	= array.get(_linesD, i)
            _lineU 	= array.get(_linesU, i)
            _levelD	= line.get_y1(_lineD)
            _levelU	= line.get_y1(_lineU)
            if (bar_index - _touchX) < i_expiry
                _lvl = (_levelD + _levelU) / 2
                _tol = tol*2
                if  (math.abs(_level - _lvl) <= _tol or math.abs(_level - _lvl) <= _tol)
                    _levelFound := 1
                    break
    _levelFound 

// Given a _touches array identifying # touches of each line
// a _linesUP array identifying UPPER BOUND price pivots,
// a _linesDN array identifying LOWER BOUND price pivots,
// a _touchesY array identifying bar_index of each last touch
// a _touchesY array identifying level of each line (midpt of _lines._level, for convenience and labelling)',
// update the _lines array for the current bar.
// OR
// delete the line if it has persisted with only 1 touch for too long (defined by i_expiry).
_controlLine(_linesU, _linesD, _touches, _touchesX, _touchesY, _colour, tol, highOrLow) =>
    isPH = highOrLow > 0 // boundaries get treated differently below
    isPL = highOrLow < 0 // boundaries get treated differently beloww
    
    if array.size(_linesU) > 0
        for i = array.size(_linesU)-1 to 0 by 1
            _lineU 	= array.get(_linesU, i)
            _lineD 	= array.get(_linesD, i)
            _NtouchPrevX = array.get(_touchesX, i)
            _NtouchPrev  = array.get(_touches , i)
            _levelU	= line.get_y1(_lineU)
            _levelD	= line.get_y1(_lineD) 
            _x1	    = line.get_x1(_lineU)
            _x2	    = line.get_x2(_lineU)
            
            // line update expired?
			if (bar_index - _NtouchPrevX) > i_expiry 
			 or barstate.islast
			    // delete if no boundary developed
                if (_NtouchPrev < i_minTouches) or (_x2 - _x1) < i_minLength
                    line.delete(_lineU)
                    line.delete(_lineD)
                    array.remove(_linesU,   i)
                    array.remove(_linesD,   i)
                    array.remove(_touches,  i)
                    array.remove(_touchesX, i)
                    array.remove(_touchesY, i)
                    // here is an opportunity to label Ntouches...
                else
                    line.set_x2(_lineD, _NtouchPrevX) // pull bar back to the last touch
                    line.set_x2(_lineU, _NtouchPrevX) // pull bar back to the last touch
                    if i_labelPvts
                        label.new(_x2, _levelD, str.tostring(_NtouchPrev), style=label.style_none, 
                         yloc=yloc.abovebar, size=size.small)
	            continue
            
			// line already crossed? (if so, then prev. iteration of this code did not update _x2)
            _levelAlreadyCrossed = (bar_index > _x2) 
			if _levelAlreadyCrossed // and (_x2 - _x1) > i_minLength)
				continue
            
            // HERE I COULD PROGRAM A 1.25H RULE / EXPLORE THE TRUE BREAKOUT LEVELS.
            // price crossed over high pivot?
            mul = 2 // 2 because the uncertainty applies both to the pivot and to the current bar
            _xOver = (low     < (_levelD - tol*mul) and high > (_levelU + tol*mul) and high[1] < (_levelD - tol*mul))
                 or  (high[1] < (_levelU + tol*mul) and low > (_levelU + tol*mul))
                 
            // price crossed under low pivot?
            _xUnder =  (high > (_levelU + tol*mul) and low < (_levelD - tol*mul) and low[1] > (_levelU + tol*mul))
                  or (low[1] < (_levelD - tol*mul) and high > (_levelU + tol*mul))
                  
            // adjust active lines
            if i_levelPersist or not (_xOver or _xUnder)
                line.set_x2(_lineD, bar_index + 1) // stretch line over to the next bar
                line.set_x2(_lineU, bar_index + 1) // stretch line over to the next bar
                // future feature: move lines to average of pivots FIRST NEED TO SWITCH TO PIVOT BASED TRIGGER OF THE CODE
//                line.set_y1(_lineD, (_levelD + pvt) / 2) // needs to handle hi/lo
//                line.set_y2(_lineD, bar_index + 1) 
//                line.set_y1(_lineU, 
//                line.set_y2(_lineU, 
            	            
	        // line touched? 
	        _touched = (math.abs(high - _levelD) <= tol or math.abs(high - _levelU) <= tol)
	             or    (math.abs( low - _levelD) <= tol or math.abs( low - _levelU) <= tol)
            _Ntouch = _touched ? _NtouchPrev + 1 : _NtouchPrev
	        if _touched and not (_xOver or _xUnder)
                array.set(_touchesX, i, bar_index) 
                array.set(_touches , i, _NtouchPrev + 1) 

            if i_showTol
                // visualize the tolerance of 'touches'
                linefill.new(_lineU, _lineD, _colour)
                1
            else
                // show only the inner line [THE LOGIC THAT WORKS HERE IS BACKWARDS? BUG?]
                line.set_color(_lineU, isPH ? _colour : color.new(color.white, 100))
                line.set_color(_lineD, isPL ? _colour : color.new(color.white, 100))
                1

// PREP
var line[]  loLinesDN 	= array.new_line()  // up and down lines, for linefill()
var line[]  loLinesUP 	= array.new_line()
var line[]  hiLinesDN 	= array.new_line() 
var line[]  hiLinesUP 	= array.new_line()
var int[]   loTouches	= array.new_int() // # of times price touched this level
var int[]   hiTouches	= array.new_int()
var float[] loTouchesY	= array.new_float() // Y position of pivot touches
var float[] hiTouchesY	= array.new_float()
var int[]   loTouchesX	= array.new_int() // bar_index of last touches
var int[]   hiTouchesX	= array.new_int()

// CALCULATIONS
pl = ta.pivotlow( i_prd, i_prd) 
ph = ta.pivothigh(i_prd, i_prd)
// define pivots as rounded to the nearest 'tol' and tick
ph := math.round_to_mintick(  math.ceil(ph / tol) * tol)
pl := math.round_to_mintick(math.floor( pl / tol) * tol)
plotshape(i_labelPvts ? pl : na, text="L",  style=shape.labeldown, color=na, textcolor=color.new(i_pvtBtmColor,0), location=location.belowbar, offset = -i_prd)
plotshape(i_labelPvts ? ph : na, text="H",  style=shape.labeldown, color=na, textcolor=color.new(i_pvtTopColor,0), location=location.abovebar, offset = -i_prd)

// Add pivot low to lines, if new 
if pl 
    plu = pl + tol
    pld = pl - tol
    if not _levelCurrentlyActive(pl, loLinesDN, loLinesUP, loTouchesX)
        _loLineUP = line.new(x1=bar_index[i_prd], y1=plu, x2=bar_index, y2=plu, xloc=xloc.bar_index, color=i_pvtBtmColor, style=i_pvtStyle, width=i_pvtLThick)
        _loLineDN = line.new(x1=bar_index[i_prd], y1=pld, x2=bar_index, y2=pld, xloc=xloc.bar_index, color=i_pvtBtmColor, style=i_pvtStyle, width=i_pvtLThick)
    
        if array.size(loLinesUP) >= i_maxLoLines
            line.delete(array.shift(loLinesDN))
            line.delete(array.shift(loLinesUP))
            array.shift(loTouches)
            array.shift(loTouchesX)
            array.shift(loTouchesY)
            
        array.push(loLinesDN, _loLineUP)
        array.push(loLinesUP, _loLineDN)
        array.push(loTouches, 1)
        array.push(loTouchesY, pl)
        array.push(loTouchesX, bar_index)

// Add pivot high to lines, if new
if ph 
    phu = ph + tol
    phd = ph - tol
    if not _levelCurrentlyActive(ph, hiLinesDN, hiLinesUP, hiTouchesX)
        _hiLineUP = line.new(x1=bar_index[i_prd], y1=phu, x2=bar_index, y2=phu, xloc=xloc.bar_index, color=i_pvtTopColor, style=i_pvtStyle, width=i_pvtLThick)
        _hiLineDN = line.new(x1=bar_index[i_prd], y1=phd, x2=bar_index, y2=phd, xloc=xloc.bar_index, color=i_pvtTopColor, style=i_pvtStyle, width=i_pvtLThick)
            
        if array.size(hiLinesUP) >= i_maxHiLines
            line.delete(array.shift(hiLinesUP))
            line.delete(array.shift(hiLinesDN))
            array.shift(hiTouches)
            array.shift(hiTouchesX)
            array.shift(hiTouchesY)
            
        array.push(hiLinesUP,   _hiLineUP)
        array.push(hiLinesDN,   _hiLineDN)
        array.push(hiTouches, 1)
        array.push(hiTouchesY, ph)
        array.push(hiTouchesX, bar_index)

_controlLine(loLinesUP, loLinesDN, loTouches, loTouchesX, loTouchesY, i_pvtBtmColor, tol, -1)
_controlLine(hiLinesUP, hiLinesDN, hiTouches, hiTouchesX, hiTouchesY, i_pvtTopColor, tol,  1)

if barstate.islast
    1
    // combine neighbouring lines?
    // remove very short pivots inside longer ones

// todo -- add a summary of the bars
if 0
    var table t = table.new(position.top_right, 1, 5, frame_color=color.black)
    table.cell(t, 0, 0, " ",                           bgcolor = color.new(color.white, 70), text_color=color.red,   text_halign = text.align_right, text_size=size.small)
    table.cell(t, 0, 1, array.join(hiTouchesY, ", "),  bgcolor = color.new(color.white, 70), text_color=color.new(i_pvtTopColor, 0), text_halign = text.align_right, text_size=size.small)
    table.cell(t, 0, 2, array.join(hiTouches, ', '),   bgcolor = color.new(color.white, 70), text_color=color.new(i_pvtTopColor, 0), text_halign = text.align_right, text_size=size.small)
    table.cell(t, 0, 3, array.join(loTouchesY, ", "),  bgcolor = color.new(color.white, 70), text_color=color.new(i_pvtBtmColor, 0), text_halign = text.align_right, text_size=size.small)
    table.cell(t, 0, 4, array.join(loTouches, ', '),   bgcolor = color.new(color.white, 70), text_color=color.new(i_pvtBtmColor, 0), text_halign = text.align_right, text_size=size.small)