Band-Zigzag Based Trend Follower
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © HeWhoMustNotBeNamed
//                                       ░▒             
//                                  ▒▒▒   ▒▒      
//                              ▒▒▒▒▒     ▒▒      
//                      ▒▒▒▒▒▒▒░     ▒     ▒▒          
//                  ▒▒▒▒▒▒           ▒     ▒▒          
//             ▓▒▒▒       ▒        ▒▒▒▒▒▒▒▒▒▒▒  
//   ▒▒▒▒▒▒▒▒▒▒▒ ▒        ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒         
//   ▒  ▒       ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░        
//   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░▒▒▒▒▒▒▒▒         
//   ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒                       
//    ▒▒▒▒▒         ▒▒▒▒▒▒▒                            
//                 ▒▒▒▒▒▒▒▒▒                           
//                ▒▒▒▒▒ ▒▒▒▒▒                          
//               ░▒▒▒▒   ▒▒▒▒▓      ████████╗██████╗ ███████╗███╗   ██╗██████╗  ██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
//              ▓▒▒▒▒     ▒▒▒▒      ╚══██╔══╝██╔══██╗██╔════╝████╗  ██║██╔══██╗██╔═══██╗██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔════╝
//              ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒        ██║   ██████╔╝█████╗  ██╔██╗ ██║██║  ██║██║   ██║███████╗██║     ██║   ██║██████╔╝█████╗ 
//             ▒▒▒▒▒       ▒▒▒▒▒       ██║   ██╔══██╗██╔══╝  ██║╚██╗██║██║  ██║██║   ██║╚════██║██║     ██║   ██║██╔═══╝ ██╔══╝  
//            ▒▒▒▒▒         ▒▒▒▒▒      ██║   ██║  ██║███████╗██║ ╚████║██████╔╝╚██████╔╝███████║╚██████╗╚██████╔╝██║     ███████╗
//             ▒▒             ▒                        
//@version=5
indicator("Band-Zigzag Based Trend Follower", "BZZ-TF", overlay = true, max_lines_count = 500, max_labels_count = 500)
import HeWhoMustNotBeNamed/ta/1 as eta
import HeWhoMustNotBeNamed/arrays/1 as pa

bandType = input.string('Bollinger Bands', '', ['Bollinger Bands', 'Keltner Channel', 'Donchian Channel'], group='Bands', inline='b1')
maType = input.string('sma', '', options=['sma', 'ema', 'hma', 'rma', 'wma', 'vwma', 'highlow', 'linreg', 'median'], inline = 'b1', group='Bands')
maLength = input.int(20, '', minval = 5, step=5, inline = 'b1', group='Bands')
multiplier = input.float(2.0, '', step=0.5, minval=0.5, group='Bands', inline='b1', tooltip = 'Band parameters let you select type of band/channel, moving average base, length and multiplier')
useTrueRange = input.bool(true, 'Use True Range (For KC only)', group='Bands', tooltip = 'Used only for keltener channel calculation')
sticky = input.bool(true, 'Sticky', group='Bands', tooltip = 'Sticky bands avoids changing the band unless there is breach. Meaning, band positions only change when price is outside the band.')
useAdaptive = input.bool(true, 'Adaptive', group='Bands', tooltip = 'When selected, lower band will not decrease when pivot high is in motion and upper band will not increase when pivot low in motion')

useLatestPivot = input.bool(true, 'Use Recent Pivot Confirmation', group='Confirmation', tooltip = 'Also use current unconfirmed pivot for trend confirmation')
continueWhenNoConfirmation = input.bool(false, 'Nullify Trend If no confirmation', group='Confirmation', tooltip = 'If selected, trend is set to neither if no confirmation obtained. Otherwise, previous trend is carried forward')

showBands = input.bool(true, 'Bands', group='Display', inline='d')
showZigzag = input.bool(true, 'Zigzag', group='Display', inline='d')
colorCandles = input.bool(true, 'Color Bars', group='Display', inline='d')

float upper = 0.0
float lower = 0.0
float middle = 0.0

type Pivot
    float price
    int bar
    int bartime
    int direction
    float percentB
    float ratio = 0

type Drawing
    line zigzagLine
    label zigzagLabel

unshift(array<Pivot> zigzag, Pivot p)=>
    if(array.size(zigzag) > 1)
        lastPivot = array.get(zigzag, 0)
        llastPivot = array.get(zigzag, 1)
        p.direction := p.price*p.direction > llastPivot.price*p.direction ? p.direction*2 : p.direction
        p.ratio := math.abs(p.price - lastPivot.price)/math.abs(llastPivot.price - lastPivot.price)

    array.unshift(zigzag, p)
    if(array.size(zigzag)>15)
        array.pop(zigzag)

shift(array<Drawing> drawings)=>
    del = array.shift(drawings)
    line.delete(del.zigzagLine)
    label.delete(del.zigzagLabel)

unshift(array<Drawing> drawings, Drawing drawing, int maxItems)=>
    array.unshift(drawings, drawing)
    if(array.size(drawings) > maxItems)
        del = array.pop(drawings)
        line.delete(del.zigzagLine)
        label.delete(del.zigzagLabel)

if(bandType == 'Bollinger Bands')
    [bmiddle, bupper, blower] = eta.bb(close, maType, maLength, multiplier, sticky)
    upper := bupper
    lower := blower
    middle := bmiddle

if(bandType == 'Keltner Channel')
    [kmiddle, kupper, klower] = eta.kc(close, maType, maLength, multiplier, useTrueRange, sticky)
    upper := kupper
    lower := klower
    middle := kmiddle

if(bandType == 'Donchian Channel')
    [dmiddle, dupper, dlower] = eta.dc(maLength, false, close, sticky)
    upper := dupper
    lower := dlower
    middle := dmiddle

var zigzag = array.new<Pivot>()

var trend = 0
var currentTrend = 0
var currentDir = 0

if(array.size(zigzag) > 1)
    recentPivot = array.get(zigzag, 0)
    lastPivot = array.get(zigzag, 1)

    currentDir := recentPivot.direction
    currentTrend := recentPivot.ratio > recentPivot.percentB? int(math.sign(recentPivot.direction)) : -int(math.sign(recentPivot.direction))
    trend := lastPivot.ratio > lastPivot.percentB? int(math.sign(lastPivot.direction)) : -int(math.sign(lastPivot.direction))

var uupper = nz(upper)
var llower = nz(lower)

plot(currentDir, 'CurrentDir', display = display.data_window)
uupper := currentDir >= 0 or not useAdaptive? upper : math.min(upper, uupper)
llower := currentDir <= 0 or not useAdaptive? lower : math.max(lower, llower)

percentBHigh = (high - llower)/(uupper - llower)
percentBLow = (uupper - low)/(uupper - llower)


addPivot(zigzag, direction)=>
    lastRemoved = false
    price = direction > 0? high : low
    percentB = direction > 0? percentBHigh : percentBLow
    Pivot p = Pivot.new(price, bar_index, time, int(direction), percentB)
    if(array.size(zigzag) == 0)
        unshift(zigzag, p)
    else
        lastPivot = array.get(zigzag, 0)
        lastPivotDirection = math.sign(lastPivot.direction)
        if (lastPivotDirection != p.direction)
            unshift(zigzag, p)
        if(lastPivotDirection == p.direction and lastPivot.price*direction < p.price*direction)
            array.remove(zigzag, 0)
            unshift(zigzag, p)
            lastRemoved := true
    lastRemoved
       

add_pivots(zigzag, pHigh, pLow)=>
    count = 0
    removeLast = false
    if(array.size(zigzag) == 0)
        if(pHigh)
            addPivot(zigzag, 1)
            count+=1
        if(pLow)
            addPivot(zigzag, -1)
            count+=1
    else
        lastPivot = array.get(zigzag, 0)
        lastDir = math.sign(lastPivot.direction)
        if(pHigh and lastDir == 1)  or (pLow and lastDir == -1)
            removeLast := addPivot(zigzag, lastDir)
            count := removeLast ? count+1 : count
        if(pHigh and (lastDir == -1)) or (pLow and (lastDir == 1))
            addPivot(zigzag, -lastDir)
            count+=1

    [count, removeLast]

draw_zigzag(zigzag, count, removeLast)=>
    var zigzagDrawings = array.new<Drawing>()
    if(removeLast and array.size(zigzagDrawings) > 0)
        shift(zigzagDrawings)
    if(array.size(zigzag) > count and count >= 1)
        for i=count to 1
            startPivot = array.get(zigzag, count)
            endPivot = array.get(zigzag, count-1)
            startDirection = startPivot.direction
            endDirection = endPivot.direction
            lineColor = endDirection == 2? color.green : endDirection == -2? color.red : color.silver
            txt = str.tostring(endPivot.ratio*100, format.percent) + "/" + str.tostring(endPivot.percentB*100, format.percent)
            
            ln = line.new(startPivot.bartime, startPivot.price, endPivot.bartime, endPivot.price, xloc.bar_time, color=lineColor, width = 2)
            lbl = label.new(endPivot.bartime, endPivot.price, txt, xloc.bar_time, yloc.price, lineColor, endPivot.direction > 0? label.style_label_down : label.style_label_up, textcolor = color.black)
            
            
            Drawing dr = Drawing.new(ln, lbl)
            unshift(zigzagDrawings, dr, 500)
    zigzagDrawings


pHigh = high >= uupper
pLow = low <= llower
[count, removeLast] = add_pivots(zigzag, pHigh, pLow)

if(showZigzag)
    draw_zigzag(zigzag, count, removeLast)

plot(showBands? uupper:na, 'Upper', color.green, 0, plot.style_linebr)
plot(showBands? llower:na, 'Lower', color.red, 0, plot.style_linebr)

var combinedTrend = 0
combinedTrend := trend>0 and (not useLatestPivot or not (currentTrend < 0))? 1 :
                 trend < 0 and (not useLatestPivot or not (currentTrend > 0))? -1 : (continueWhenNoConfirmation? 0: combinedTrend)

candleColor = combinedTrend > 0? color.green : combinedTrend < 0 ? color.red : color.silver
plot(combinedTrend, "Trend", candleColor, display = display.data_window)
barcolor(colorCandles? candleColor : na)

