ABC on Recursive Zigzag [Trendoscope]
// This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0) https://creativecommons.org/licenses/by-nc-sa/4.0/
// © Trendoscope Pty Ltd
//                                       ░▒             
//                                  ▒▒▒   ▒▒      
//                              ▒▒▒▒▒     ▒▒      
//                      ▒▒▒▒▒▒▒░     ▒     ▒▒          
//                  ▒▒▒▒▒▒           ▒     ▒▒          
//             ▓▒▒▒       ▒        ▒▒▒▒▒▒▒▒▒▒▒  
//   ▒▒▒▒▒▒▒▒▒▒▒ ▒        ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒         
//   ▒  ▒       ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░        
//   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░▒▒▒▒▒▒▒▒         
//   ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒                       
//    ▒▒▒▒▒         ▒▒▒▒▒▒▒                            
//                 ▒▒▒▒▒▒▒▒▒                           
//                ▒▒▒▒▒ ▒▒▒▒▒                          
//               ░▒▒▒▒   ▒▒▒▒▓      ████████╗██████╗ ███████╗███╗   ██╗██████╗  ██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
//              ▓▒▒▒▒     ▒▒▒▒      ╚══██╔══╝██╔══██╗██╔════╝████╗  ██║██╔══██╗██╔═══██╗██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔════╝
//              ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒        ██║   ██████╔╝█████╗  ██╔██╗ ██║██║  ██║██║   ██║███████╗██║     ██║   ██║██████╔╝█████╗ 
//             ▒▒▒▒▒       ▒▒▒▒▒       ██║   ██╔══██╗██╔══╝  ██║╚██╗██║██║  ██║██║   ██║╚════██║██║     ██║   ██║██╔═══╝ ██╔══╝  
//            ▒▒▒▒▒         ▒▒▒▒▒      ██║   ██║  ██║███████╗██║ ╚████║██████╔╝╚██████╔╝███████║╚██████╗╚██████╔╝██║     ███████╗
//             ▒▒             ▒                        
//@version=5
import HeWhoMustNotBeNamed/DrawingTypes/2 as dr
import HeWhoMustNotBeNamed/DrawingMethods/2
import HeWhoMustNotBeNamed/ZigzagTypes/5 as zg
import HeWhoMustNotBeNamed/ZigzagMethods/6

import HeWhoMustNotBeNamed/utils/1 as ut

import HeWhoMustNotBeNamed/FibRatios/1 as fibs

indicator("ABC on Recursive Zigzag [Trendoscope]", "ABC-RZ[Trendoscope]", overlay = true, max_lines_count=500, max_labels_count=500, max_bars_back = 1000)
theme = input.string('Dark', title='Theme', options=['Light', 'Dark'], group='Generic Settings',
         tooltip='Chart theme settings. Line and label colors are generted based on the theme settings. If dark theme is selected, '+
         'lighter colors are used and if light theme is selected, darker colors are used.', display=display.none)

zigzagLength = input.int(13, step=5, minval=3, title='Length', group='Zigzag', tooltip='Zigzag length for level 0 zigzag', display = display.none)
depth = input.int(200, "Depth", step=25, maxval=500, group='Zigzag', tooltip='Zigzag depth refers to max number of pivots to show on chart', display = display.none)
minimumZigZagLevel = input.int(0, "Minimum Zigzag Level", group='Zigzag', minval = 0, tooltip = 'Minimum zigzag level to consider for pattern scanning', display=display.none)

indicators = matrix.new<float>()
indicatorNames = array.new<string>()

base = input.string('ABC Extension', 'Base', ['ABC Extension', 'BC Retracement'], 'Base on which entry, stop and target are calculated', group='ABC', display = display.none)
entryRatio = input.float(0.3, 'Entry Ratio', group='ABC', minval=0.1, step=0.1, display = display.none, tooltip = 'Entry ratio for the calculation of entry level')
targetRatio = input.float(1.0, 'Target Ratio', group='ABC', display = display.none, tooltip = 'Target Ratio for the calculation of target level')
stopRatio = input.float(0.0, 'Stop Ratio', group='ABC', maxval=0.0, step=0.1, display = display.none, tooltip = 'Stop Ratio for the calculation of stop level')
logScale = input.bool(false, 'Log Scale', group='ABC', display = display.none, tooltip = 'Use log scale for scanning and targets')
useClosePricesForEntry = input.bool(true, 'Entry', group='Use Close Prices', display = display.none, tooltip = 'Use close prices for tracking', inline = 'ucp')
useClosePricesForTarget = input.bool(true, 'Target', group='Use Close Prices', display = display.none, tooltip = 'Use close prices for tracking', inline = 'ucp')
useClosePricesForStop = input.bool(true, 'Stop', group='Use Close Prices', display = display.none, tooltip = 'Use close prices for tracking', inline = 'ucp')
useClosePricesForRetest = input.bool(true, 'Retest', group='Use Close Prices', display = display.none, tooltip = 'Use close prices for tracking', inline = 'ucp')

tradeConditionTooltip = 'any - no filter\ntrend - Both A and B pivots in the direction of trend. Example, HH, and HL for long signal and LH and LL for short signal\n'+
                             'reverse - Both A and B pivots in the opposite direction of trend. Example, LH, and LL for long signal and HH and HL for short signal\n'+
                             'contracting - Consider only if both A and B pivots are either LH or HL\nexpanding - Consider only if both A and B pivots are either HH or LL'

tradeCondition = input.string('any', 'Trade Condition', ['any', 'trend', 'reverse', 'contracting', 'expanding'], group='ABC', display = display.none, tooltip = tradeConditionTooltip)

condition = tradeCondition == 'any'? 0:
                 tradeCondition == 'trend'? 1:
                 tradeCondition == 'reverse'? 2:
                 tradeCondition == 'contracting'? 3: 4

baseVal = base == 'ABC Extension'? 1 : 2


var zg.Zigzag zigzag = zg.Zigzag.new(zigzagLength, depth)
zigzag.calculate(array.from(high, low), indicators, indicatorNames)

type ABCProperties
    int base = 1
    float entryRatio = 0.23
    float targetRatio = 1.0
    float stopRatio = -0.1
    bool logScale = false
    bool useClosePricesForEntry = true
    bool useClosePricesForTarget = false
    bool useClosePricesForStop = true
    bool useClosePricesForRetest = false
    int condition = 0

type ABCDrawing
    dr.Line ab
    dr.Line bc
    dr.Line ac
    dr.Label a
    dr.Label b
    dr.Label c
    dr.Label abcRatio
    dr.Box entryBox
    dr.Box targetBox

type ABC
    int id
    int direction
    zg.Pivot a
    zg.Pivot b
    zg.Pivot c
    color patternColor
    ABCProperties properties
    float entryPrice
    float stopPrice
    float targetPrice
    int status = 0
    ABCDrawing drawing

initialiseCounts(int numberOfStatus)=>
    countMap = map.new<int, int>()
    for i=0 to numberOfStatus-1
        countMap.put(i, 0)
    countMap

var array<ABC> abcdPatterns = array.new<ABC>()
var array<ABC> oldPatterns = array.new<ABC>()

var map<int, int> bullishCounts = initialiseCounts(3)
var map<int, int> bearishCounts = initialiseCounts(3)

var int bullishRetests = 0
var int bearishRetests = 0

method calculateTargets(ABC this)=>
    this.entryPrice := this.properties.base == 1 ?
         fibs.extension(this.a.point.price, this.b.point.price, this.c.point.price, this.properties.entryRatio, this.properties.logScale) :
         fibs.retracement(this.b.point.price, this.c.point.price, this.properties.entryRatio, this.properties.logScale)

    this.targetPrice := this.properties.base == 1 ?
         fibs.extension(this.a.point.price, this.b.point.price, this.c.point.price, this.properties.targetRatio, this.properties.logScale) :
         fibs.retracement(this.b.point.price, this.c.point.price, this.properties.targetRatio, this.properties.logScale)

    this.stopPrice := this.properties.base == 1 ?
         fibs.extension(this.a.point.price, this.b.point.price, this.c.point.price, this.properties.stopRatio, this.properties.logScale) :
         fibs.retracement(this.b.point.price, this.c.point.price, this.properties.stopRatio, this.properties.logScale)
    this

method withinEntry(ABC this)=>
    dir = this.c.point.price > this.b.point.price? -1 : 1
    close*dir < this.entryPrice*dir

method delete(ABCDrawing this)=>
    if(not na(this))
        this.ab.delete()
        this.bc.delete()
        this.ac.delete()
        this.abcRatio.delete()
        this.a.delete()
        this.b.delete()
        this.c.delete()
        this.entryBox.delete()
        this.targetBox.delete()
    else
        log.info('this should not be na')
    this

method draw(ABCDrawing this)=>
    if(not na(this))
        this.ab.draw()
        this.bc.draw()
        this.ac.draw()
        this.abcRatio.draw()
        this.a.draw()
        this.b.draw()
        this.c.draw()
        this.entryBox.draw()
        this.targetBox.draw()
    else
        log.info('this should not be na')
    this

method draw(ABC this)=>
    this.drawing.draw()
    this

method deleteDrawing(ABC this)=>
    if(not na(this.drawing))
        this.drawing.delete()
        this.drawing := na
    this

method createDrawing(ABC this)=>
    if(not na(this.drawing))
        this.drawing.delete()

    dr.LineProperties patternLineProps = dr.LineProperties.new(color=this.patternColor, width = 1, style = line.style_solid)
    ab = this.a.point.createLine(this.b.point, patternLineProps)
    bc = this.b.point.createLine(this.c.point, patternLineProps)
    dr.LineProperties angleLineProps = dr.LineProperties.new(color=this.patternColor, width = 0, style = line.style_dotted)
    ac = dr.Line.new(this.a.point, this.c.point, angleLineProps)
    acMidPoint = dr.Point.new((this.a.point.price+this.c.point.price)/2, (this.a.point.bar + this.c.point.bar)/2, (this.a.point.bartime + this.c.point.bartime)/2)
    abcRatioValue = fibs.retracementRatio(this.a.point.price, this.b.point.price, this.c.point.price)
    ratioLabelProperties = dr.LabelProperties.new(yloc = yloc.price, textcolor = this.patternColor, style = label.style_none)
    abcRatio = dr.Label.new(acMidPoint, str.tostring(abcRatioValue), properties = ratioLabelProperties)
    pivotLabelPropertiesAC = dr.LabelProperties.new(yloc = this.a.point.price < this.b.point.price? yloc.belowbar : yloc.abovebar, textcolor = this.patternColor)
    pivotLabelPropertiesBD = dr.LabelProperties.new(yloc = this.a.point.price > this.b.point.price? yloc.belowbar : yloc.abovebar, textcolor = this.patternColor)
    a = dr.Label.new(this.a.point, 'A', properties = pivotLabelPropertiesAC)
    b = dr.Label.new(this.b.point, 'B', properties = pivotLabelPropertiesBD)
    c = dr.Label.new(this.c.point, 'C', properties = pivotLabelPropertiesAC)

    entryPoint = dr.Point.new(this.entryPrice, this.c.point.bar, this.c.point.bartime)
    barDiff = math.min((this.c.point.bar- this.a.point.bar)/2, 490)

    entryBoxEndPoint = dr.Point.new(this.stopPrice, this.c.point.bar+barDiff)
    targetBoxEndPoint = dr.Point.new(this.targetPrice, this.c.point.bar+barDiff)

    boxPropertiesEntry = dr.BoxProperties.new(this.patternColor, color.new(color.red, 90))
    boxPropertiesTarget = dr.BoxProperties.new(this.patternColor, color.new(color.green, 90))
    entryBox = dr.Box.new(entryPoint, entryBoxEndPoint, boxPropertiesEntry)
    targetBox = dr.Box.new(entryPoint, targetBoxEndPoint, boxPropertiesTarget)
    this.drawing := ABCDrawing.new(ab, bc, ac, a, b, c, abcRatio, entryBox, targetBox)
    this


method update(ABC this, zg.Pivot c)=>
    this.c := c
    alert('ABC Pattern Coordinates Updated')
    this.calculateTargets()

    if(this.withinEntry())
        this.deleteDrawing().
                 createDrawing().
                 draw()
    this
    
method createAbc(zg.Zigzag this, ABCProperties props, color patternColor)=>
    var id = 1
    c = this.zigzagPivots.get(0)
    b = this.zigzagPivots.get(1)
    a = this.zigzagPivots.get(2)
    direction = b.point.price > c.point.price? 1 : -1
    abc = ABC.new(id, direction, a, b, c, patternColor, props)
    id+=1
    abc

method scanAbc(zg.Zigzag this, ABCProperties props)=>
    isAbc = false
            
    if(this.zigzagPivots.size() >= 4)
        c = this.zigzagPivots.get(0)
        b = this.zigzagPivots.get(1)
        a = this.zigzagPivots.get(2)
        aDir = math.abs(a.dir)
        bDir = math.abs(b.dir)
        conditionInLine = props.condition == 0 or
                             (props.condition == 1 and aDir == 1 and bDir == 2) or
                             (props.condition == 2 and aDir == 2 and bDir == 1) or
                             (props.condition == 3 and aDir == 1 and bDir == 1) or
                             (props.condition == 4 and aDir == 2 and bDir == 2)
        ratioInLine = c.ratio >= 0.618 and c.ratio <= 0.786
        if(ratioInLine and conditionInLine)
            existingPattern = false
            isAbc := true
            for p in abcdPatterns
                existingPattern := p.a.point.price == a.point.price and p.b.point.price == b.point.price
                if(existingPattern)
                    if(p.c.point.bar > c.point.bar and p.status == 0)
                        p.update(c)
                    
                    isAbc:=false
                    break
    isAbc

method record(ABC pattern)=>
    countMapToSet = pattern.direction >0? bullishCounts : bearishCounts
    countMapToSet.put(pattern.status, countMapToSet.get(pattern.status)+1)

method removePattern(array<ABC> patterns, int index)=>
    pattern = patterns.remove(index)
    pattern.deleteDrawing()
    pattern.record()
    
method traverse(array<ABC> patterns)=>
    for i = patterns.size() >0? patterns.size()-1: na to 0
        pattern = patterns.get(i)
        baseTarget = pattern.properties.useClosePricesForTarget? close : (pattern.direction > 0? high : low)
        baseStop = pattern.properties.useClosePricesForStop? close : (pattern.direction >0? low : high)
        baseEntry = pattern.properties.useClosePricesForEntry? close : (pattern.direction > 0? high : low)
        baseValueRetest = pattern.properties.useClosePricesForRetest? close : (pattern.direction > 0? low : high)
        baseInvalidation = close
        newStatus = baseTarget*pattern.direction >= pattern.targetPrice*pattern.direction? 2 :
                         baseEntry*pattern.direction >= pattern.entryPrice*pattern.direction? 1: pattern.status
        retested = pattern.status == 1 and baseValueRetest <= pattern.entryPrice

        newStatus := math.max(pattern.status, newStatus)
        closed = (newStatus > 0 and baseStop*pattern.direction <= pattern.stopPrice*pattern.direction) or
             (newStatus == 0 and baseInvalidation*pattern.direction <= pattern.stopPrice*pattern.direction) or
             pattern.status == 2
        increment = newStatus >= pattern.status
        pattern.status := newStatus
        if(closed)
            patterns.removePattern(i)

var properties = ABCProperties.new(baseVal, entryRatio, targetRatio, stopRatio, logScale, useClosePricesForEntry, useClosePricesForTarget, useClosePricesForStop, useClosePricesForRetest, condition)
var themeColors = ut.getColors(theme)

abcdPatterns.traverse()
oldPatterns.traverse()

if zigzag.flags.newPivot
    mlzigzag = zigzag
    while(mlzigzag.zigzagPivots.size() >= 3)
        if(mlzigzag.level >= minimumZigZagLevel)
            isAbcd = mlzigzag.scanAbc(properties)
            if(isAbcd)
                patternColor = themeColors.shift()
                alert('New ABC Pattern Detected')
                pattern = mlzigzag.createAbc(properties, patternColor).calculateTargets()
                if(pattern.withinEntry())
                    pattern.createDrawing().draw()
                    abcdPatterns.push(pattern)
                    while(abcdPatterns.size() > 10)
                        last = abcdPatterns.shift()
                        oldPatterns.push(last)
                        last.deleteDrawing()

                themeColors.push(patternColor)
        mlzigzag := mlzigzag.nextlevel()

while(abcdPatterns.size() < 10 and oldPatterns.size() > 0)
    restoreOld = oldPatterns.pop()
    abcdPatterns.unshift(restoreOld)
    restoreOld.draw()

if barstate.islast
    var closedStatsTable = table.new(position.top_right, 6, 3, border_color = chart.bg_color)
    closedStatsTable.clear(0, 0, 5, 2)
    closedStatsTable.cell(0, 0, 'Direction\\Status', text_color=color.white, bgcolor = color.maroon)
    closedStatsTable.cell(1, 0, 'Invalid', text_color=color.white, bgcolor = color.maroon)
    closedStatsTable.cell(2, 0, 'Stopped', text_color=color.white, bgcolor = color.maroon)
    closedStatsTable.cell(3, 0, 'Complete', text_color=color.white, bgcolor = color.maroon)
    closedStatsTable.cell(4, 0, 'Win Ratio', text_color=color.white, bgcolor = color.maroon)
    closedStatsTable.cell(5, 0, 'Risk Reward', text_color=color.white, bgcolor = color.maroon)
    closedStatsTable.cell(0, 1, 'Bullish', text_color=color.white, bgcolor = color.new(color.green, 50))
    closedStatsTable.cell(0, 2, 'Bearish', text_color=color.white, bgcolor = color.new(color.red, 50))

    bullishInvalid = bullishCounts.get(0)
    bullishStopped = bullishCounts.get(1)
    bullishCompleted = bullishCounts.get(2)
    riskReward =(targetRatio - entryRatio)/(entryRatio - stopRatio)
    bullishBgColor = color.new(color.green, 70)
    closedStatsTable.cell(1, 1, str.tostring(bullishInvalid), text_color=color.white, bgcolor = bullishBgColor)
    closedStatsTable.cell(2, 1, str.tostring(bullishStopped), text_color=color.white, bgcolor = bullishBgColor)
    closedStatsTable.cell(3, 1, str.tostring(bullishCompleted), text_color=color.white, bgcolor = bullishBgColor)
    closedStatsTable.cell(4, 1, str.tostring(bullishCompleted*100/(bullishCompleted+bullishStopped), format.percent), text_color=color.white, bgcolor = bullishBgColor)
    closedStatsTable.cell(5, 1, str.tostring(riskReward, '#.##'), text_color=color.white, bgcolor = bullishBgColor)

    bearishInvalid = bearishCounts.get(0)
    bearishStopped = bearishCounts.get(1)
    bearishCompleted = bearishCounts.get(2)
    bearishBgColor = color.new(color.red, 70)
    closedStatsTable.cell(1, 2, str.tostring(bearishInvalid), text_color=color.white, bgcolor = bearishBgColor)
    closedStatsTable.cell(2, 2, str.tostring(bearishStopped), text_color=color.white, bgcolor = bearishBgColor)
    closedStatsTable.cell(3, 2, str.tostring(bearishCompleted), text_color=color.white, bgcolor = bearishBgColor)
    closedStatsTable.cell(4, 2, str.tostring(bearishCompleted*100/(bearishCompleted+bearishStopped), format.percent), text_color=color.white, bgcolor = bearishBgColor)
    closedStatsTable.cell(5, 2, str.tostring(riskReward, '#.##'), text_color=color.white, bgcolor = bearishBgColor)
