Historical Pattern Matcher [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/FibRatios/1 as fibs


indicator("Historical Pattern Matcher [Trendoscope]", "HPM[Trendoscope]", overlay = true, max_lines_count=500, max_labels_count=500, max_bars_back = 1000)
zigzagLength = input.int(13, step=5, minval=3, title='Length', group='Zigzag', tooltip='Zigzag length', display = display.none)
numberOfPivots = input.int(6, "Number of Pivots", group='Zigzag', minval = 5, tooltip = 'Number of pivots to match for pattern', display = display.none)

projectionBullishColor = input.color(color.lime, 'Bullish Colors', inline='bullc', display = display.none, group='Display')
labelBullishColor = input.color(color.green, '', inline='bullc', display = display.none, tooltip = 'Projection and label colors for bullish projections', group='Display')
projectionBearishColor = input.color(color.orange, 'Bearish Colors', inline='bearc', display = display.none, group='Display')
labelBearishColor = input.color(color.red, '', inline='bearc', display = display.none, tooltip = 'Projection and label colors for bearish projections', group='Display')
type PatternData
    float[] ratios
    array<zg.Pivot> lastOccurence

method tostring(PatternData this)=>
    strValue = "Last Occurence : "+str.format_time(this.lastOccurence.get(1).point.bartime) +
                 "\nLast retracement :"+str.tostring(this.ratios.last()) +
                 "\nMax retracement :"+str.tostring(this.ratios.max()) +
                 "\nMin retracement :"+str.tostring(this.ratios.min()) +
                 "\nMedian retracement :"+str.tostring(this.ratios.median()) +
                 "\nNumber of Occurences :"+str.tostring(this.ratios.size())
    strValue
type PatternDrawing
    array<dr.Line> currentPattern
    array<dr.Line> lastOccurence
    dr.Box projectionRange
    dr.Label projectionDetails
    dr.Line medianProjection

depth = numberOfPivots + 2
useRealTimeBars = true

offset = useRealTimeBars? 0 : 1

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

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

dr.LineProperties bullishPatternProperties = dr.LineProperties.new(xloc.bar_time, color = labelBullishColor, width = 2)
dr.LineProperties bearishPatternProperties = dr.LineProperties.new(xloc.bar_time, color = labelBearishColor, width = 2)

dr.LineProperties bullishProjectionProperties = dr.LineProperties.new(color = labelBullishColor, style = line.style_arrow_right, width = 1)
dr.LineProperties bearishProjectionProperties = dr.LineProperties.new(color = labelBearishColor, style = line.style_arrow_right, width = 1)

dr.BoxProperties bullishBoxProperties = dr.BoxProperties.new(color.new(projectionBullishColor, 80), color.new(projectionBullishColor, 90))
dr.BoxProperties bearishBoxProperties = dr.BoxProperties.new(color.new(projectionBearishColor, 80), color.new(projectionBearishColor, 90))
dr.LabelProperties bullishlblProperties = dr.LabelProperties.new(textcolor = chart.fg_color, style = label.style_label_lower_left, color = color.new(labelBullishColor, 90))
dr.LabelProperties bearishLblProperties = dr.LabelProperties.new(textcolor = chart.fg_color, style = label.style_label_upper_left, color = color.new(labelBearishColor, 90))
method getMatchString(array<zg.Pivot> this, startIndex=1)=>
    matchstring = ''
    for i=startIndex to this.size()-2
        for j=i+1 to this.size()-1
            matchstring+= this.get(i).point.price > this.get(j).point.price ? '1' : '0'
    matchstring

method ratios(array<zg.Pivot> this)=>
    ratios = array.new<float>()
    for pivot in this
        ratios.push(pivot.ratio)
    ratios

method tostring(array<float> this)=> str.tostring(this)
method tostring(array<int> this)=> str.tostring(this)
method update(map<string, PatternData> this, array<zg.Pivot> pivots)=>
    matchstring = pivots.getMatchString()

    PatternData currentObj = na
    if(this.contains(matchstring))
        currentObj := this.get(matchstring)
    else
        currentObj := PatternData.new(array.new<float>())
        this.put(matchstring, currentObj)

    currentObj.ratios.push(pivots.get(0).ratio)
    currentObj.lastOccurence := pivots

method draw(array<zg.Pivot> pivots, PatternData patternInfo, int startIndex=1)=>
    var drawing = PatternDrawing.new(array.new<dr.Line>(), array.new<dr.Line>(), dr.Box.new(),
                     dr.Label.new(), dr.Line.new())
    drawing.currentPattern.clear()
    drawing.lastOccurence.clear()
    drawing.medianProjection.delete()
    drawing.projectionDetails.delete()
    currentDir = pivots.get(startIndex).dir
    for i=startIndex to pivots.size()-2
        drawing.currentPattern.push(pivots.get(i).point.createLine(pivots.get(i+1).point, currentDir < 0? bullishPatternProperties : bearishPatternProperties))
    for i=1 to patternInfo.lastOccurence.size()-2
        drawing.lastOccurence.push(patternInfo.lastOccurence.get(i).point.createLine(patternInfo.lastOccurence.get(i+1).point, currentDir < 0? bullishPatternProperties : bearishPatternProperties))
    drawing.currentPattern.draw()
    drawing.lastOccurence.draw()
    ratioMin = patternInfo.ratios.min()
    ratioMax = patternInfo.ratios.max()

    lastPrice = pivots.get(startIndex+1).point.price
    currentPrice = pivots.get(startIndex).point.price
    

    lastBar = pivots.get(startIndex+1).point.bar
    currentBar = pivots.get(startIndex).point.bar
    priceMin = fibs.retracement(lastPrice, currentPrice, ratioMin)
    priceMax = fibs.retracement(lastPrice, currentPrice, ratioMax)

    barMin = int(currentBar + ratioMin*(currentBar-lastBar))+1
    barMax = int(currentBar + ratioMax*(currentBar-lastBar))+1

    drawing.projectionRange.p1 := dr.Point.new(priceMin, barMin)
    drawing.projectionRange.p2 := dr.Point.new(priceMax, barMax)
    drawing.projectionRange.properties := currentDir < 0? bullishBoxProperties : bearishBoxProperties
    drawing.projectionRange.draw()

    drawing.projectionDetails.lblText := patternInfo.tostring()
    drawing.projectionDetails.point := dr.Point.new(priceMax, barMax)
    drawing.projectionDetails.properties := currentDir < 0? bullishlblProperties : bearishLblProperties
    drawing.projectionDetails.draw()

    ratioMedium = patternInfo.ratios.median()
    priceMedian = fibs.retracement(lastPrice, currentPrice, ratioMedium)
    barMedian = int(currentBar + ratioMedium*(currentBar-lastBar))+1
    drawing.medianProjection := pivots.get(startIndex).point.createLine(dr.Point.new(priceMedian, barMedian), currentDir < 0? bullishProjectionProperties : bearishProjectionProperties)
    drawing.medianProjection.draw()

var patternMap = map.new<string, PatternData>()

var lastPatternBar = 0
if zigzag.flags.newPivot and zigzag.zigzagPivots.size() == depth
    lastConfirmedPivot = zigzag.zigzagPivots.get(1)
    if(lastConfirmedPivot.point.bar > lastPatternBar)
        snapshot = zigzag.zigzagPivots.copy()
        snapshot.shift()
        patternMap.update(snapshot)
        lastPatternBar := lastConfirmedPivot.point.bar

        currentSnapshot = zigzag.zigzagPivots.copy()
        currentSnapshot.pop()
        matchstring = currentSnapshot.getMatchString()
        if(patternMap.contains(matchstring))
            patternInfo = patternMap.get(matchstring)
            currentSnapshot.draw(patternInfo)

    latestSnapshot = zigzag.zigzagPivots.copy()
    latestSnapshot.pop()
    latestSnapshot.pop()
    latestMatch = latestSnapshot.getMatchString(0)
    if(patternMap.contains(latestMatch))
        patternInfo = patternMap.get(latestMatch)
        latestSnapshot.draw(patternInfo, 0)


