Band-Zigzag - TrendFollower Strategy [Trendoscope]
// 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
strategy("Band-Zigzag - TrendFollower Strategy [Trendoscope]", "BZSTF[Trendoscope]", overlay=true, initial_capital=1000,
         default_qty_type=strategy.percent_of_equity, default_qty_value=10, commission_type=strategy.commission.percent, 
         pyramiding=1, commission_value=0.05, close_entries_rule="ANY", margin_long=100, margin_short=100,
         max_lines_count=500, max_labels_count=500, max_boxes_count=500, use_bar_magnifier=false)
tradeDirection = input.string(strategy.direction.all, "Trade Direction", options=[strategy.direction.long, strategy.direction.short, strategy.direction.all],
                             group="Trade Settings", tooltip='Use this to filter only long or short trades')
tradeSettingsTooltip = 'Pullback - On first pullback after trend confirmation\nBreakout - On first breakout of trend\Pullback-Breakout - On first pullback and breakout of trend\nAny - As soon as trend change'
tradeType = input.string('Any', 'Trade Type', options=['Pullback', 'Breakout', 'Pullback-Breakout', 'Any'], group='Trade Settings', tooltip=tradeSettingsTooltip)
tradeOnBreakout = tradeType == 'Breakout'
tradeOnPullback = tradeType == 'Pullback'
tradeOnRetest = tradeType == 'Pullback-Breakout'
tradeOnAny = tradeType == 'Any'
useStopLoss = input.bool(false, 'Use Stop Loss', group='Trade Settings', tooltip = 'If set, initial stoploss is used for every trade. Otherwise, trade can only be closed by change of trend. Not required if using Recent Pivot Override')
strategy.risk.allow_entry_in(tradeDirection)

import HeWhoMustNotBeNamed/ta/1 as eta
import HeWhoMustNotBeNamed/arrays/1 as pa
import HeWhoMustNotBeNamed/eHarmonicpatternsExtended/6 as hp

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')
showBands = input.bool(true, 'Display Bands', group='Bands', tooltip = 'If selected, displays bands on chart')

useLatestPivot = input.bool(true, 'Use Recent Pivot Override', group='Trend', inline='t')
currentTrendInvalidationMultiplier = input.float(1.618, '', 0, 100, 1, group='Trend', inline='t',
             tooltip = 'Trend calculation is done on last confirmed pivot. This option provides opportunity to also consider current unconfirmed pivot for defining trend on extreme moves. This will help in avoiding steep drawbacks')

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

var currentTrendForce = 0
var currentTrendExtreme = 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))
    currentTrendForce := recentPivot.ratio > recentPivot.percentB and recentPivot.ratio> currentTrendInvalidationMultiplier? int(math.sign(recentPivot.direction)) : -int(math.sign(recentPivot.direction))
    

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

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)

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 or (useLatestPivot and (currentTrendForce > 0))? 1 :
                 trend < 0 or (useLatestPivot and (currentTrendForce < 0))? -1 : combinedTrend

candleColor = combinedTrend > 0? color.green : combinedTrend < 0 ? color.red : color.silver

plot(trend, 'Last Pivot Trend', trend >0? color.green : trend < 0? color.red : color.silver, display=display.data_window)
plot(currentTrend, 'Current Pivot Trend', currentTrend > 0? color.green : currentTrend < 0? color.red : color.silver, display=display.data_window)
plot(currentTrendForce, 'Force Trend', currentTrendForce >0? color.green : currentTrendForce < 0? color.red : color.silver, display=display.data_window)

plot(combinedTrend, "Combined Trend", candleColor, display = display.data_window)
plot(currentDir, "Current Direction", color=currentDir>0?color.green:currentDir<0?color.red:color.silver, display = display.data_window)

var allowEntry = false
allowEntry := ta.crossover(combinedTrend, 0) or ta.crossunder(combinedTrend, 0)? true : allowEntry

if(combinedTrend > 0)
    strategy.close("Short")
    if((tradeOnBreakout and currentDir > 0) or (tradeOnPullback and currentDir < 0) or tradeOnAny or (currentTrend >0 and trend >0 and currentDir > 0 and tradeOnRetest)) and allowEntry
        allowEntry := false
        strategy.entry("Long", strategy.long)
        if useStopLoss
            strategy.exit("ExitLong", "Long", stop=low - (uupper-llower))

if(combinedTrend < 0)
    strategy.close("Long")
    if((tradeOnBreakout and currentDir < 0) or (tradeOnPullback and currentDir > 0) or tradeOnAny or (currentTrend < 0 and trend < 0 and currentDir < 0 and tradeOnRetest)) and allowEntry
        allowEntry := false
        strategy.entry("Short", strategy.short)
        if useStopLoss
            strategy.exit("ExitShort", "Short", stop=high + (uupper-llower))

if(combinedTrend == 0)
    strategy.close_all()

plot(allowEntry?1:0, 'Allow Entry', display = display.data_window)