RSI Impact Heat Map [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
indicator("RSI Impact Heat Map [Trendoscope]", "RSIHM[Trendoscope]", overlay = false)

rsiSource = input.source(close, 'Source', inline='r', group='RSI')
rsiLength = input.int(14, '', 5, 200, 5, inline='r', group='RSI', tooltip = 'RSI configuration')
rsiTrigger = input.string('Crossover', 'Trigger', ['Crossover', 'Crossunder'], inline='rt', group = 'RSI')
rsiTriggerValue = input.int(70, '', 0, 100, 5, inline='rt', group = 'RSI', tooltip = 'Trigger for which we need to plot the impact')

impactDuration = input.int(20, 'Duration', 0, 500, 10, group='Impact', tooltip = 'Duration for which impact needs to be measured')
reference = input.string('ATR', 'Reference', ['ATR', 'Percentage'], group='Impact', tooltip='Reference on which the impact is measured')

matrixSize = input.int(50, 'Heat Map Size', 10, 90, 10, group='Display', tooltip = 'Number of rows, columns for heat map matrix')
outliersPercentile = input.int(95, 'Outliers Percentile', 50, 100, 5, group='Display', tooltip = 'Percentile to filter outliers. Value of 95 means, 95th percentile is considered as max displacement')

backgroundColor = input.color(color.rgb(0,0,0,0), 'Background', group='Display', tooltip = 'Background color')
heatmapColor = input.color(color.red, 'Heatmap', group='Display', tooltip = 'Heatmap color')
type Event
    float xValue
    float yValue

var array<Event> events = array.new<Event>()

rsi = ta.rsi(rsiSource, rsiLength)
trigger = rsiTrigger == 'Crossover' ? ta.crossover(rsi, rsiTriggerValue) : ta.crossunder(rsi, rsiTriggerValue)
atr = ta.atr(rsiLength)

highest = ta.highest(impactDuration-1)
lowest = ta.lowest(impactDuration-1)

var array<float> displacements = array.new<float>()

if(trigger[impactDuration])
    price = close[impactDuration]
    positiveDisplacement = math.abs(highest-price)
    negativeDisplacement = math.abs(price-lowest)
    xValue = reference == 'ATR'? positiveDisplacement/atr[impactDuration] : reference == 'Percentage' ? positiveDisplacement/price : positiveDisplacement
    yValue = reference == "ATR"? negativeDisplacement/atr[impactDuration] : reference == 'Percentage' ? negativeDisplacement/price : negativeDisplacement
    array.push(displacements, xValue)
    array.push(displacements, yValue)

    Event event = Event.new(xValue, yValue)
    array.push(events, event)

if(barstate.isfirst)
    var titleTable = table.new(position.top_center, 2, 3, color.maroon, color.maroon, 1, color.maroon, 1)
    title = 'Impact of RSI('+str.tostring(rsiLength)+') '+rsiTrigger+' '+str.tostring(rsiTriggerValue)+ ' after '+str.tostring(impactDuration)+ ' bars measured in terms of '+reference
    table.cell(titleTable, 0, 0, title, text_color = color.white)

if(barstate.islast)
    matrix<int> counts = matrix.new<int>(matrixSize+1, matrixSize+1, 0)
    maxRange = array.percentile_linear_interpolation(displacements, outliersPercentile)
    for event in events
        xIndex = math.min(int(event.xValue*matrixSize/maxRange), matrixSize)
        yIndex = matrixSize - math.min(int(event.yValue*matrixSize/maxRange), matrixSize)
        matrix.set(counts, xIndex, yIndex, matrix.get(counts, xIndex, yIndex)+1)

    var heatmap = table.new(position.middle_center, matrixSize+1, matrixSize+1, backgroundColor, color.yellow, 1, backgroundColor, 0)
    table.clear(heatmap, 0, 0, matrixSize, matrixSize)
    maxCount = matrix.max(counts)
    totalCount = array.size(events)
    sums = array.new<int>(4,0)
    for [i, columns] in counts
        for [j, count] in columns
            countPercent = int(count*90/maxCount)
            sumIndex = 2*(i < ((matrixSize+1)/2) ? 0 : 1) + (j < ((matrixSize+1)/2)? 0: 1)
            array.set(sums, sumIndex, array.get(sums, sumIndex)+count)
            xRange = 'Positive Displaecment : ' + (reference == 'Percentage'? str.tostring(maxRange*i*100/matrixSize, format.percent) : (str.tostring(maxRange*i/matrixSize, format.mintick) + 'X'))
                                              + ' - '+ ((i==matrixSize)? '' : 
                                             (reference == 'Percentage'? str.tostring(maxRange*(i+1)*100/matrixSize, format.percent) : (str.tostring(maxRange*(i+1)/matrixSize, format.mintick) + 'X')))
            yRange = 'Negative Displacement : ' + (reference == 'Percentage'? str.tostring(maxRange*(matrixSize-j)*100/matrixSize, format.percent) : (str.tostring(maxRange*(matrixSize-j)/matrixSize, format.mintick) + 'X'))
                                              + ' - '+ ((j==matrixSize)? '' : 
                                             (reference == 'Percentage'? str.tostring(maxRange*(matrixSize-j+1)*100/matrixSize, format.percent) : (str.tostring(maxRange*(matrixSize-j+1)/matrixSize, format.mintick) + 'X')))
            tooltip = 'Count '+str.tostring(count)+'/'+str.tostring(totalCount)+'\n'+xRange+'\n'+yRange+'\n('+str.tostring(i)+','+str.tostring(j)+')'
            table.cell(heatmap, i, j, '', width=1, height = 1, text_color=color.yellow, bgcolor = color.new(heatmapColor, countPercent==0? 100 : 90-countPercent), text_size = size.tiny, tooltip = tooltip)

    var positions = array.from(position.top_left, position.bottom_left, position.top_right, position.bottom_right)
    for [i, sum] in sums
        tab = table.new(array.get(positions, i), 1,1, color.teal, color.teal, 1, color.teal, 1)
        table.cell(tab, 0, 0, str.tostring(sum), text_color = color.white, text_size = size.large)
        
