Divergence Backtester - V2
// 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("Divergence Backtester - V2", "DBv2", overlay=true, max_lines_count = 500, max_labels_count = 500)
import HeWhoMustNotBeNamed/mZigzag/12 as zg
import HeWhoMustNotBeNamed/enhanced_ta/14 as eta
import HeWhoMustNotBeNamed/arrays/1 as pa
//*********************** Debug method *************************//
i_start = 0
i_page = 100
i_maxLogSize = 1000
i_showHistory = false
i_showBarIndex = false
var DebugArray = array.new_string(0)
var DebugBarArray = array.new_string(0)
add_to_debug_array(arr, val, maxItems) =>
    array.unshift(arr, str.tostring(val))
    if array.size(arr) > maxItems
        array.pop(arr)

debug(debugMsg) =>
    if barstate.islast or i_showHistory
        barTimeString = str.tostring(year, '0000') + '/' + str.tostring(month, '00') + '/' + str.tostring(dayofmonth, '00') + (timeframe.isintraday ? '-' + str.tostring(hour, '00') + ':' + str.tostring(minute, '00') + ':' + str.tostring(second, '00') : '')

        add_to_debug_array(DebugBarArray, i_showBarIndex ? str.tostring(bar_index) : barTimeString, i_maxLogSize)
        add_to_debug_array(DebugArray, debugMsg, i_maxLogSize)
//*********************** Debug method *************************//

length = input.int(8, 'Length', group='Zigzag')
oscillatorType = input.string("rsi", title="Oscillator", inline="osc", options=["cci", "cmo", "cog", "mfi", "roc", "rsi"], group='Oscillator')
oscLength = input.int(14, title="", inline="osc", group='Oscillator')
supertrendLength = input.int(5, 'History', inline='st', group='Supertrend')
drawSupertrend = input.bool(true, "Draw Zigzag Supertrend", inline='st2', group='Supertrend')
txtSize = input.string(size.tiny, 'Text Size', [size.tiny, size.small, size.normal, size.large, size.huge], inline='txt')
txtColor = input.color(color.white, '', inline='txt')

increment(mtx, row, col, val=1)=>matrix.set(mtx, row, col, matrix.get(mtx, row, col)+val)
gettrendindex(int price, int osc, int trend)=>
    trendFactor = trend > 0 ? 0 : 1
    priceFactor = math.abs(price) > 1? 1 : 0
    oscFactor = math.abs(osc) > 1? 1 : 0
    trendFactor*4 + priceFactor*2 + oscFactor


getSentimentDetails(pDir, oDir, sDir) =>
    sentiment = pDir == oDir ? sDir == pDir or sDir * 2 == -pDir ? -sDir : sDir * 4 : sDir == pDir or sDir == -oDir ? 0 : (math.abs(oDir) > math.abs(pDir) ? sDir : -sDir) * (sDir == oDir ? 2 : 3)
    sentimentSymbol = sentiment == 4 ? '⬆' : sentiment == -4 ? '⬇' : sentiment == 3 ? '↗' : sentiment == -3 ? '↘' : sentiment == 2 ? '⤴' : sentiment == -2 ? '⤵' : sentiment == 1 ? '⤒' : sentiment == -1 ? '⤓' : '▣'
    sentimentColor = sentiment == 4 ? color.green : sentiment == -4 ? color.red : sentiment == 3 ? color.lime : sentiment == -3 ? color.orange : sentiment == 2 ? color.rgb(202, 224, 13, 0) : sentiment == -2 ? color.rgb(250, 128, 114, 0) : color.silver
    sentimentLabel = math.abs(sentiment) == 4 ? 'C' : math.abs(sentiment) == 3 ? 'H' : math.abs(sentiment) == 2 ? 'D' : 'I'
    [sentimentSymbol, sentimentLabel, sentimentColor]


getStatus(int trendIndex, int pivotDir)=>
    trendFactor = int(trendIndex/4)
    remainder = trendIndex % 4
    priceFactor = int(remainder/2)+1
    oscFactor = (remainder % 2)+1
    trendChar = (trendFactor == 0)? 'U' : 'D'
    priceChar = pivotDir > 0? (priceFactor == 2? 'HH' : 'LH') : (priceFactor == 2? 'LL' : 'HL')
    oscChar = pivotDir > 0? (oscFactor == 2? 'HH' : 'LH') : (oscFactor == 2? 'LL' : 'HL')
    trendChar + ' - ' + priceChar + '/'+oscChar

draw_zg_line(idx1, idx2, zigzaglines, zigzaglabels, valueMatrix, directionMatrix, ratioMatrix,  divergenceMatrix, doubleDivergenceMatrix,
                                         barArray, trendArray, lineColor, lineWidth, lineStyle) =>
    if matrix.rows(valueMatrix) > 2
        idxLen1 = matrix.rows(valueMatrix)-idx1
        idxLen2 = matrix.rows(valueMatrix)-idx2
        lastValues = matrix.row(valueMatrix, idxLen1)
        llastValues = matrix.row(valueMatrix, idxLen2)
        
        lastDirections = matrix.row(directionMatrix, idxLen1)
        lastRatios = matrix.row(ratioMatrix, idxLen1)
        lastDivergence = matrix.row(divergenceMatrix, idxLen1)
        lastDoubleDivergence = matrix.row(doubleDivergenceMatrix, idxLen1)

        y1 = array.get(lastValues, 0)
        y2 = array.get(llastValues, 0)
        x1 = array.get(barArray, idxLen1)
        x2 = array.get(barArray, idxLen2)
        zline = line.new(x1=x1, y1=y1, x2=x2, y2=y2, color=lineColor, width=lineWidth, style=lineStyle)
        currentDir = y1 > y2? 1 : -1
        
        priceDir = array.get(lastDirections, 0)
        oscDir = array.get(lastDirections, 1)
        trendDir = array.get(trendArray, idxLen1)
        trendIndex = gettrendindex(priceDir, oscDir, trendDir)
        trendLabel = getStatus(trendIndex, currentDir)
        [sentimentSymbol, sentimentLabel, sentimentColor] = getSentimentDetails(priceDir, oscDir, trendDir)

        labelStyle = currentDir > 0? label.style_label_down : label.style_label_up
        zlabel = label.new(x=x1, y=y1, yloc=yloc.price, color=sentimentColor, style=labelStyle, text=sentimentSymbol + ' ' + trendLabel, 
                                     textcolor=color.black, size = size.small, tooltip=sentimentLabel)
        if array.size(zigzaglines) > 0
            lastLine = array.get(zigzaglines, array.size(zigzaglines)-1)
            if line.get_x2(lastLine) == x2 and line.get_x1(lastLine) <= x1
                pa.pop(zigzaglines)
                pa.pop(zigzaglabels)
        
        pa.push(zigzaglines, zline, 500)
        pa.push(zigzaglabels, zlabel, 500)

draw(matrix<float> valueMatrix, matrix<int> directionMatrix, matrix<float> ratioMatrix,  matrix<int> divergenceMatrix, matrix<int> doubleDivergenceMatrix, array<int> barArray, array<int> trendArray,
                 bool newZG, bool doubleZG, color lineColor = color.blue, int lineWidth = 1, string lineStyle = line.style_solid)=>
    var zigzaglines = array.new_line(0)
    var zigzaglabels = array.new_label(0)
    if(newZG)
        if doubleZG
            draw_zg_line(2, 3, zigzaglines, zigzaglabels, valueMatrix, directionMatrix, ratioMatrix, divergenceMatrix, doubleDivergenceMatrix, barArray, trendArray,
                             lineColor, lineWidth, lineStyle)

        if matrix.rows(valueMatrix) >= 2
            draw_zg_line(1, 2, zigzaglines, zigzaglabels, valueMatrix, directionMatrix, ratioMatrix,  divergenceMatrix, doubleDivergenceMatrix, barArray, trendArray,
                             lineColor, lineWidth, lineStyle)
    [valueMatrix, directionMatrix, ratioMatrix, divergenceMatrix, doubleDivergenceMatrix, barArray, zigzaglines, zigzaglabels]

    

indicatorHigh = array.new_float()
indicatorLow = array.new_float()
indicatorLabels = array.new_string()

[oscHigh, _, _] = eta.oscillator(oscillatorType, oscLength, oscLength, oscLength, high)
[oscLow, _, _] = eta.oscillator(oscillatorType, oscLength, oscLength, oscLength, low)
array.push(indicatorHigh, math.round(oscHigh,2))
array.push(indicatorLow, math.round(oscLow,2))
array.push(indicatorLabels, oscillatorType+str.tostring(oscLength))


[valueMatrix, directionMatrix, ratioMatrix, divergenceMatrix, doubleDivergenceMatrix, barArray, trendArray, supertrendDir, supertrend, newZG, doubleZG] = 
                                                         zg.calculate(length, array.from(high, low), indicatorHigh, indicatorLow, supertrendLength = supertrendLength)

nextDirection = matrix.rows(directionMatrix) > 0? matrix.row(directionMatrix, matrix.rows(directionMatrix)-1) : array.new_int()
lastDirection = matrix.rows(directionMatrix) > 1? matrix.row(directionMatrix, matrix.rows(directionMatrix)-2) : array.new_int()
llastDirection = matrix.rows(directionMatrix) > 2? matrix.row(directionMatrix, matrix.rows(directionMatrix)-3) : array.new_int()
lllastDirection = matrix.rows(directionMatrix) > 3? matrix.row(directionMatrix, matrix.rows(directionMatrix)-4) : array.new_int()

var pivotHighStats = matrix.new<int>(64, 3, 0)
var pivotLowStats = matrix.new<int>(64, 3, 0)

var pivotHighRatios = matrix.new<float>(64, 3, 0)
var pivotLowRatios = matrix.new<float>(64, 3, 0)

currentTotalTrendIndex = 0
nextTotalTrendIndex = 0
currentDir = matrix.rows(directionMatrix) > 0? matrix.get(directionMatrix, matrix.rows(directionMatrix)-1, 0) : 0
if(array.size(lllastDirection) > 0)
    priceDirection = array.get(lastDirection, 0)
    priceRatio = matrix.get(ratioMatrix, matrix.rows(ratioMatrix)-2, 0)

    currentPriceDirection = array.get(lastDirection, 0)
    currentOscDirection = array.get(lastDirection, 1)
    currentTrend = array.get(trendArray, array.size(trendArray)-2)

    nextPriceDirection = array.get(nextDirection, 0)
    nextOscDirection = array.get(nextDirection, 1)
    nextTrend = array.get(trendArray, array.size(trendArray)-1)

    lastPriceDirection = array.get(llastDirection, 0)
    lastOscDirection = array.get(llastDirection, 1)
    lastTrend = array.get(trendArray, array.size(trendArray)-3)

    llastPriceDirection = array.get(lllastDirection, 0)
    llastOscDirection = array.get(lllastDirection, 1)
    llastTrend = array.get(trendArray, array.size(trendArray)-4)

    colLast = math.abs(priceDirection) %2

    nextTrendIndex = gettrendindex(nextPriceDirection, nextOscDirection, nextTrend)
    currentTrendIndex = gettrendindex(currentPriceDirection, currentOscDirection, currentTrend)
    lastTrendIndex = gettrendindex(lastPriceDirection, lastOscDirection, lastTrend)
    llastTrendIndex = gettrendindex(llastPriceDirection, llastOscDirection, llastTrend)

    totalIndex = lastTrendIndex*8 + llastTrendIndex
    currentTotalTrendIndex := currentTrendIndex*8 + lastTrendIndex
    nextTotalTrendIndex := nextTrendIndex*8 + currentTrendIndex

    matrixToSet = math.sign(priceDirection) > 0? pivotHighStats : pivotLowStats
    ratioMatrixToSet = math.sign(priceDirection) > 0? pivotHighRatios : pivotLowRatios
    increment(matrixToSet, totalIndex, colLast)
    increment(ratioMatrixToSet, totalIndex, colLast, priceRatio)
    increment(matrixToSet, totalIndex, 2)
    increment(ratioMatrixToSet, totalIndex, 2, priceRatio)


if(barstate.islast)
    startRow = 2
    var statsTable = table.new(position=position.top_right, columns=10, rows=64+startRow, border_color = color.black, border_width = 2)
    table.clear(statsTable, 0, 0, 9, 64+startRow-1)
    phSortIndices = array.sort_indices(matrix.col(pivotHighStats, 2), order.descending)
    
    phColStart = currentDir > 0 ? 0 : 5
    
    table.cell(statsTable, phColStart, 0, 'Pivot High Projection', text_color = txtColor, text_size = txtSize, bgcolor = color.maroon)
    table.cell(statsTable, phColStart, 1, 'Last Two Pivots', text_color = txtColor, text_size = txtSize, bgcolor = color.maroon)
    table.cell(statsTable, phColStart+2, 1, 'HH', text_color = txtColor, text_size = txtSize, bgcolor = color.maroon)
    table.cell(statsTable, phColStart+3, 1, 'LH', text_color = txtColor, text_size = txtSize, bgcolor = color.maroon)
    table.cell(statsTable, phColStart+4, 1, 'T', text_color = txtColor, text_size = txtSize, bgcolor = color.maroon)
    table.merge_cells(statsTable, phColStart, 0, phColStart+4, 0)
    table.merge_cells(statsTable, phColStart, 1, phColStart+1, 1)

    hlr = startRow
    for i=0 to 63
        si = array.get(phSortIndices, i)
        lastTrendIndex = int(si/8)
        llastTrendIndex = si%8

        lastPivotStatus = getStatus(lastTrendIndex, -1)
        llastPivotStatus = getStatus(llastTrendIndex, 1)
        hhStats = matrix.get(pivotHighStats, si, 0)
        lhStats = matrix.get(pivotHighStats, si, 1)
        tStats = matrix.get(pivotHighStats, si, 2)

        hhRatio = math.round(matrix.get(pivotHighRatios, si, 0)/hhStats, 3)
        lhRatio = math.round(matrix.get(pivotHighRatios, si, 1)/lhStats, 3)
        tRatio = math.round(matrix.get(pivotHighRatios, si, 2)/tStats, 3)

        highlight = math.sign(currentDir) < 0 ? nextTotalTrendIndex == si  : currentTotalTrendIndex == si

        hhTooltip = 'Average Ratio - '+str.tostring(hhRatio)
        lhTooltip = 'Average Ratio - '+str.tostring(lhRatio)
        tTooltip = 'Average Ratio - '+str.tostring(tRatio)

        if(hhStats != 0 and lhStats != 0)
            table.cell(statsTable, phColStart, hlr, llastPivotStatus, text_color = txtColor, text_size = txtSize, bgcolor = color.new(color.lime, 60))
            table.cell(statsTable, phColStart+1, hlr, lastPivotStatus, text_color = txtColor, text_size = txtSize, bgcolor = color.new(color.orange, 60))
            table.cell(statsTable, phColStart+2, hlr, str.tostring(hhStats)+' - '+str.tostring(hhRatio), text_color = txtColor, text_size = txtSize, bgcolor = color.new(color.green, highlight ? 50 : 90), tooltip = hhTooltip)
            table.cell(statsTable, phColStart+3, hlr, str.tostring(lhStats)+' - '+str.tostring(lhRatio), text_color = txtColor, text_size = txtSize, bgcolor = color.new(color.red, highlight? 50 : 90), tooltip = lhTooltip)
            table.cell(statsTable, phColStart+4, hlr, str.tostring(tStats)+' - '+str.tostring(tRatio), text_color = txtColor, text_size = txtSize, bgcolor = color.from_gradient(hhStats/tStats, 0, 1, color.red, color.green), tooltip = tTooltip)
            hlr+=1


    plColStart = currentDir < 0 ? 0 : 5
    table.cell(statsTable, plColStart, 0, 'Pivot Low Projection', text_color = txtColor, text_size = txtSize, bgcolor = color.maroon)
    table.cell(statsTable, plColStart, 1, 'Last Two Pivots', text_color = txtColor, text_size = txtSize, bgcolor = color.maroon)
    table.cell(statsTable, plColStart+2, 1, 'LL', text_color = txtColor, text_size = txtSize, bgcolor = color.maroon)
    table.cell(statsTable, plColStart+3, 1, 'HL', text_color = txtColor, text_size = txtSize, bgcolor = color.maroon)
    table.cell(statsTable, plColStart+4, 1, 'T', text_color = txtColor, text_size = txtSize, bgcolor = color.maroon)
    table.merge_cells(statsTable, plColStart, 0, plColStart+4, 0)
    table.merge_cells(statsTable, plColStart, 1, plColStart+1, 1)
    plSortIndices = array.sort_indices(matrix.col(pivotLowStats, 2), order.descending)

    llr = startRow
    for i=0 to 63
        si = array.get(plSortIndices, i)
        lastTrendIndex = int(si/8)
        llastTrendIndex = si%8

        lastPivotStatus = getStatus(lastTrendIndex, 1)
        llastPivotStatus = getStatus(llastTrendIndex, -1)
        llStats = matrix.get(pivotLowStats, si, 0)
        hlStats = matrix.get(pivotLowStats, si, 1)
        tStats = matrix.get(pivotLowStats, si, 2)

        llRatio = math.round(matrix.get(pivotLowRatios, si, 0)/llStats, 3)
        hlRatio = math.round(matrix.get(pivotLowRatios, si, 1)/hlStats, 3)
        tRatio = math.round(matrix.get(pivotLowRatios, si, 2)/tStats, 3)

        highlight = math.sign(currentDir) > 0 ? nextTotalTrendIndex == si  : currentTotalTrendIndex == si
        llTooltip = 'Average Ratio - '+str.tostring(llRatio)
        hlTooltip = 'Average Ratio - '+str.tostring(hlRatio)
        tTooltip = 'Average Ratio - '+str.tostring(tRatio)

        if(llStats != 0 and hlStats != 0)
            table.cell(statsTable, plColStart, llr, llastPivotStatus, text_color = txtColor, text_size = txtSize, bgcolor = color.new(color.orange, 60))
            table.cell(statsTable, plColStart+1, llr, lastPivotStatus, text_color = txtColor, text_size = txtSize, bgcolor = color.new(color.lime, 60))
            table.cell(statsTable, plColStart+2, llr, str.tostring(llStats)+' - '+str.tostring(llRatio), text_color = txtColor, text_size = txtSize, bgcolor = color.new(color.red, highlight? 50 : 90), tooltip=llTooltip)
            table.cell(statsTable, plColStart+3, llr, str.tostring(hlStats)+' - '+str.tostring(hlRatio), text_color = txtColor, text_size = txtSize, bgcolor = color.new(color.green, highlight? 50 : 90), tooltip = hlTooltip)
            table.cell(statsTable, plColStart+4, llr, str.tostring(tStats)+' - '+str.tostring(tRatio), text_color = txtColor, text_size = txtSize, bgcolor = color.from_gradient(llStats/tStats, 0, 1, color.green, color.red), tooltip = tTooltip)
            llr+=1

draw(valueMatrix, directionMatrix, ratioMatrix,  divergenceMatrix, doubleDivergenceMatrix, barArray, trendArray, newZG, doubleZG)
plot(drawSupertrend? supertrend:na, color=supertrendDir>0? color.green:color.red, style=plot.style_linebr)

//************************************************************ Print debug message on table ********************************************************/
var debugTable = table.new(position=position.bottom_left, columns=2, rows=i_page + 1, border_width=1)
if array.size(DebugArray) > 0
    table.cell(table_id=debugTable, column=0, row=0, text=i_showBarIndex ? 'Bar Index' : 'Bar Time', bgcolor=color.teal, text_color=txtColor, text_size=size.normal)
    table.cell(table_id=debugTable, column=1, row=0, text='Debug Message', bgcolor=color.teal, text_color=txtColor, text_size=size.normal)
    for i = 0 to math.min(array.size(DebugArray) - 1 - i_start, i_page - 1) by 1
        table.cell(table_id=debugTable, column=0, row=i + 1, text=array.get(DebugBarArray, i + i_start), bgcolor=color.black, text_color=txtColor, text_size=size.normal)
        table.cell(table_id=debugTable, column=1, row=i + 1, text=array.get(DebugArray, i + i_start), bgcolor=color.black, text_color=txtColor, text_size=size.normal)
//************************************************************ Finish Printing ********************************************************/
