Strongest Supports And Resistances
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © mertcancucen

//@version=5
indicator("Strongest Supports And Resistances", "SSAR", true, max_bars_back = 480)


var GRP = "SR Parameters"
var GRP1 = "SR Power Factors"
var GRP2 = "SR Visualization"

closeness_ = input.float(0.005, "Closeness", tooltip = "Higer the value, far away the lines from candles", group = GRP, maxval = 0.5, minval = 0.00001, step = 0.001)
similarity_ = input.float(0.5, "Similarity", tooltip = "Line similarity threshold. Higher the value, more different the lines", group = GRP, maxval = 1, minval = 0.1, step = 0.05)
minHitCount = input.int(6, "Minimum Hit Count", tooltip = "Required amount of hits for an SR line to have a strength", group = GRP, maxval = 30, minval = 2, step = 1)
lookBack = input.int(40, "Look Back", tooltip = "How many bars from the last you want to count in", group = GRP, maxval = 100, minval = 10, step = 1)
maxBarBack_ = input.int(150, "Max Bar Back", tooltip = "How many bars back are counted for hits", group = GRP, maxval = 400, minval = 10, step = 1)
useLogarithmic = input(true, "Logarithmic", "If the graph is logarithmic, make it true", group = GRP)
dontCountAfter = input.time(0, "Hide bars after", "To test the algorithm, you can hide some latest bars", group = GRP)
UTC = input.float(3.0, title = "UTC Timezone offset", group = GRP)


countPower = input.float(1.0, "Hit Factor", tooltip = "Coefficient for line hits on candles", inline= "1", group = GRP1, maxval = 2, minval = 0.1, step = 0.1)

weightLatest = input(false, "Weight Latest Hit", "SR lines tend to have more close to latest candles", inline= "11", group = GRP1)
weightLatestPower = input.float(0.3, "Factor", inline= "11", group = GRP1, maxval = 0.75, minval = 0.1, step = 0.05)

weightDuration = input(false, "Weight Longer Hit", "If an SR line hits spread through chart, it gets more valueable", inline= "21", group = GRP1)
weightDurationPower = input.float(0.3, "Factor", inline= "21", group = GRP1, maxval = 0.75, minval = 0.1, step = 0.05)

weightVolume = input(true, "Weight Volume Hit", "If an SR line hits on more volume, it gets more valuable ", inline= "31", group = GRP1)
weightVolumePower = input.float(0.3, "Factor", inline= "31", group = GRP1, maxval = 0.75, minval = 0.1, step = 0.05)

weightStreak = input(true, "Weight Max Streak", "If an SR line doesn't get broken, it gets more valuable ", inline= "41", group = GRP1)
weightStreakPower = input.float(0.3, "Factor", inline= "41", group = GRP1, maxval = 0.75, minval = 0.1, step = 0.05)

totalLines = input.int(15, "Total Lines Shown", tooltip = "Count of strongest lines shown", group = GRP2, maxval = 40, minval = 5, step = 1)
showWickLines = input(true, "Show Through Wicks", "SR lines that are drawn through wicks", inline= "41", group = GRP2)
showBodyLines = input(true, "Show Through Open-Closes", "SR lines that are drawn through opens and closes", inline= "51", group = GRP2)

color wickColor = input(#ff00ffaf, "Wick Color", "SR lines that are drawn through wicks", inline= "41", group = GRP2)
color bodyColor = input(#ff7b00, "Body Color", "SR lines that are drawn through opens and closes", inline= "51", group = GRP2)

can_we_start(prd, UTC)=>
    // get UTC time
    int timenow_ = timenow + math.round(UTC * 3600000)
    int time_ = time + math.round(UTC * 3600000)
    
    // calculate D/H/M/S for the bar which we are on and time now
    int daynow_ = math.floor((timenow_ + 86400000 * 3) / 86400000) % 7
    int day_ = math.floor((time_ + 86400000 * 3) / 86400000) % 7
    int hournow_ = math.floor(timenow_ / 3600000) % 24
    int hour_ = math.floor(time_ / 3600000) % 24
    int minutenow_ = math.floor(timenow_ / 60000) % 60
    int minute_ = math.floor(time_ / 60000) % 60
    int secondnow_ = math.floor(timenow_ / 60000) % 60
    int second_ = math.floor(time_ / 60000) % 60
    int hmstime_ = hour_ * 3600000 + minute_ * 60000 + second_ * 1000
    int hmstimenow_ = hournow_ * 3600000 + minutenow_ * 60000 + secondnow_  * 1000
    
    var int starttime = na
    if bar_index > prd and (na(starttime) or time_ < nz(starttime))
        if timeframe.isdaily
            if day_ == daynow_ // session day
                diff = time_ - time_[prd]
                starttime := timenow_ - diff
            if daynow_ > day_ and daynow_ > day_[1] and day_[1] > day_ // weekend
                diff = time_[1] - time_[prd + 1] + (daynow_ - day_[1]) * 86400000
                starttime := timenow_ - diff
        
        if timeframe.isintraday 
            if day_ == daynow_ and day_[1] == daynow_ and hmstime_ > hmstimenow_ and hmstime_[1] <= hmstimenow_ // in session
                diff = time_[1] - time_[prd + 1] + math.round(UTC * 3600000)
                starttime := timenow_ - diff
            if day_ != daynow_ and day_[1] == daynow_ and hmstime_ < hmstimenow_ and hmstime_[1] <= hmstimenow_ // in same day and after session
                diff = time_[1] - time_[prd + 1] + (hmstimenow_ - hmstime_[1]) + math.round(UTC * 3600000)
                starttime := timenow_ - diff
            if day_ == daynow_ and day_[1] + 1 == daynow_ and hmstime_ > hmstimenow_ and hmstime_[1] >= hmstimenow_  // in week (not first day) and before session
                diff = time_[1] - time_[prd + 1] + (24 * 3600000 - hmstime_[1]) + hmstimenow_ + math.round(UTC * 3600000)
                starttime := timenow_ - diff
            if day_ == daynow_ and day_[1] > daynow_ and hmstime_ > hmstimenow_ and hmstime_[1] >= hmstimenow_ // the day after weekend before session
                diff = time_[1] - time_[prd + 1] + 2 * 86400000 + (24 * 3600000 - hmstime_[1]) + hmstimenow_ + math.round(UTC * 3600000)
                starttime := timenow_ - diff
            if daynow_ > day_ and daynow_ > day_[1] and day_[1] > day_ // weekend
                diff = time_[1] - time_[prd + 1] + (daynow_ - day_[1] - 1) * 86400000 + (24 * 3600000 - hmstime_[1]) + hmstimenow_ + math.round(UTC * 3600000)
                starttime := timenow_ - diff
        
        if timeframe.isweekly
            starttime := timenow_ - timeframe.multiplier * prd * 604800000      // 1 week = 604800000ms
        
        if timeframe.ismonthly
            starttime := timenow_ - timeframe.multiplier * prd * 2629743000     // 1 month = 2629743000ms
            
    not na(starttime) and time >= starttime


var similarity = similarity_
var closeness = closeness_
var useBarIndexSet = false
var currentTimeStamp = timenow
var startBarIndex = -1
var startBarTime = -1
var weStarted = false
var maxBarBack = maxBarBack_
if(dontCountAfter <= currentTimeStamp)
    line.new(dontCountAfter, 0, dontCountAfter, 1, xloc.bar_time, extend.both, color.blue, line.style_dashed, 1)

type Line
    float slope
    int x
    float y
    bool isLog

type RSLine
    Line _line
    int[] hitTimes
    int[] crossTimes
    float totalVolume
    float totalCrossVolume
    bool isShadows
    float power
    int barIndex
    int lastCalculation

method checkArraySimilarity(int[] arr1, int[] arr2) =>
    size1 = array.size(arr1)
    size2 = array.size(arr2)
    if(size1 > 0 and size2 > 0)
        bigArr = size1 > size2 ? arr1 : arr2
        smallArr = size1 > size2 ? arr2 : arr1
        
        sameCount = 0
        for i = 0 to (array.size(smallArr) - 1)
            if(array.indexof(bigArr, array.get(smallArr, i)) > -1)
                sameCount := sameCount + 1
        
        if(array.size(bigArr) == 1)
            sameCount == 1
        else if(array.size(bigArr) == 2)
            sameCount == 2
        else if(sameCount > 0 and sameCount >= math.round(array.size(bigArr) * similarity))
            true
        else
            false
    else
        array.size(arr1) == 0 and array.size(arr2) == 0

method checkIfSame(RSLine line1, RSLine line2) =>
    checkArraySimilarity(line1.hitTimes, line2.hitTimes)

method calculateMaxStreakTime(int[] crossTimes) =>
    int timeMax = dontCountAfter == 0 ? currentTimeStamp : dontCountAfter
    int timeMin = startBarTime
    int maxStreak = -1
    if(array.size(crossTimes) == 0)
        maxStreak := timeMax - timeMin
    else
        int[] sortedIndices = array.sort_indices(crossTimes)
        for j = 0 to (array.size(crossTimes) - 1)
            currentStreak = 0
            if(j == 0)
                currentStreak := array.get(crossTimes, array.get(sortedIndices, j)) - timeMin
            else if(j == (array.size(crossTimes) - 1))
                currentStreak := timeMax - array.get(crossTimes, array.get(sortedIndices, j))
            else
                currentStreak := array.get(crossTimes, array.get(sortedIndices, j)) - array.get(crossTimes, array.get(sortedIndices, j - 1))
            if(currentStreak > maxStreak)
                maxStreak := currentStreak
    maxStreak

method calculatePower(RSLine line1) =>
    power = 0.0
    hitCount = array.size(line1.hitTimes) - array.size(line1.crossTimes)
    if(hitCount >= minHitCount)
        power := math.pow(hitCount, countPower)
        if(weightDuration)
            power := power * math.pow(array.max(line1.hitTimes) - array.min(line1.hitTimes), weightDurationPower)
        if(weightLatest)
            power := power * (1 / math.pow(currentTimeStamp - array.max(line1.hitTimes), weightLatestPower)) 
        if(weightStreak)
            power := power * math.pow(calculateMaxStreakTime(line1.crossTimes) , weightStreakPower)
        if(weightVolume and line1.totalVolume - line1.totalCrossVolume > 0)
            power := power * math.pow(line1.totalVolume - line1.totalCrossVolume, weightVolumePower)
    power


method calculateValFor(Line line1, int x) =>
    transition = line1.y - line1.x * line1.slope
    x * line1.slope + transition

method tradingViewPlot(Line line1, float alpha, bool isWick) =>
    x2 = line1.x + 30 * 24 * 60 * 60 * 1000
    y1 = line1.y
    y2 = calculateValFor(line1, x2)
    if(useLogarithmic)
        y1 := math.exp(y1)
        y2 := math.exp(y2)
    lineColor = color.new(isWick ? wickColor : bodyColor, alpha * 100)
    line.new(line1.x, y1, x2, y2, xloc.bar_time, extend.both, color=lineColor, style=line.style_solid, width = isWick ? 1 : 2)

method setTimeForRsLine(RSLine rsLine, int datumTime, float volume, bool isCross) =>
    if(isCross)
        if(array.indexof(rsLine.crossTimes, datumTime) == -1)
            array.push(rsLine.crossTimes, datumTime)
            rsLine.totalCrossVolume := rsLine.totalCrossVolume + volume
    else     
        if(array.indexof(rsLine.hitTimes, datumTime) == -1)
            array.push(rsLine.hitTimes, datumTime)
            rsLine.totalVolume := rsLine.totalVolume + volume

method sortRSLines(RSLine[] rsLines) =>
    powers = array.new_float(0)
    rsLinesWithPower = array.new<RSLine>(0)
    for i = 0 to (array.size(rsLines) - 1)
        rsLine = array.get(rsLines, i)
        rsLine.power := calculatePower(rsLine)
        if(rsLine.power > 0)
            array.push(powers, rsLine.power)  
            array.push(rsLinesWithPower, rsLine)   
        
    sortedIndices = array.sort_indices(powers)
    sortedArray = array.new<RSLine>(0)
    if(array.size(rsLinesWithPower) > 0)
        for i = 0 to (array.size(rsLinesWithPower) - 1)
            array.push(sortedArray, array.get(rsLinesWithPower, array.get(sortedIndices, (array.size(rsLinesWithPower) - 1) - i)))
    sortedArray

method removeCloseLines(RSLine[] lines_) =>
    if(array.size(lines_) <= 1)
        lines_
    else     
        lines = sortRSLines(lines_)
        if(array.size(lines) > 0)
            rsLines = array.new<RSLine>(0)
            for i = 0 to (array.size(lines) - 1)
                found = false
                index = array.size(lines) - i - 1
                for j = 0 to (index - 1)
                    index2 = (index - 1) - j  
                    if(index2 > 0)          
                        if(checkIfSame(array.get(lines, index), array.get(lines, index2)))
                            found := true
                            break
                if(not found)
                    array.push(rsLines, array.get(lines, index))
            sortRSLines(rsLines)
        else 
            lines
    

method calculateMissingRSLineData(RSLine rsLine, bool log) =>
    if(bar_index - rsLine.barIndex < maxBarBack)
        for i = rsLine.lastCalculation + 1 to bar_index
            datumTime = time[bar_index - i]
            lineYValue = calculateValFor(rsLine._line, datumTime)

            datumOpen = open[bar_index - i]
            datumClose = close[bar_index - i]
            datumLow = low[bar_index - i]
            datumHigh = high[bar_index - i]

            if(log)
                datumOpen := math.log(datumOpen)
                datumClose := math.log(datumClose)
                datumLow := math.log(datumLow)
                datumHigh := math.log(datumHigh)
            
            lowLevelLine = 0.0
            highLevelLine = 0.0
            if(lineYValue > 0)
                lowLevelLine := lineYValue * (1 - closeness)
                highLevelLine := lineYValue * (1 + closeness)
            else
                lowLevelLine := lineYValue * (1 + closeness)
                highLevelLine := lineYValue * (1 - closeness)

            if(rsLine.isShadows)
                if((datumLow >= lowLevelLine and datumLow <= highLevelLine) or (datumHigh >= lowLevelLine and datumHigh <= highLevelLine))
                    setTimeForRsLine(rsLine, datumTime, na(volume[bar_index - i]) ? 0 : volume[bar_index - i], false)
                else if((datumLow <= lowLevelLine and datumHigh >= highLevelLine))
                    setTimeForRsLine(rsLine, datumTime, na(volume[bar_index - i]) ? 0 : volume[bar_index - i], true)
            else
                if((datumOpen >= lowLevelLine and datumOpen <= highLevelLine) or (datumClose >= lowLevelLine and datumClose <= highLevelLine))
                    setTimeForRsLine(rsLine, datumTime, na(volume[bar_index - i]) ? 0 : volume[bar_index - i], false)
                else if((datumClose > datumOpen and datumOpen <= lowLevelLine and datumClose >= highLevelLine) or (datumClose <= datumOpen and datumClose <= lowLevelLine and datumOpen >= highLevelLine))
                    setTimeForRsLine(rsLine, datumTime, na(volume[bar_index - i]) ? 0 : volume[bar_index - i], true)
            rsLine.lastCalculation := bar_index
    rsLine

method calculateRSLineData(Line line1, bool useShadows, bool log) =>
    rsLine = RSLine.new(line1, array.new_int(0), array.new_int(0), 0, 0, useShadows, 0.0, bar_index, math.max(1, startBarIndex) - 1)
    calculateMissingRSLineData(rsLine, log)

method findRSLines(bool log) =>
    rsLines = array.new<RSLine>(0)
    for i = math.max(0, bar_index - 1 - lookBack) to math.max(0, bar_index - 1)
        barLow = low[0]
        barHigh = high[0]
        barOpen = open[0]
        barClose = close[0]

        datumLow = low[bar_index - i]
        datumHigh = high[bar_index - i]
        datumOpen = open[bar_index - i]
        datumClose = close[bar_index - i]

        datumOpenTime = time[bar_index - i]
        barOpenTime = time[0]

        if(log)
            barLow := math.log(barLow)
            barHigh := math.log(barHigh)
            barOpen := math.log(barOpen)
            barClose := math.log(barClose)
            datumLow := math.log(datumLow)
            datumHigh := math.log(datumHigh)
            datumOpen := math.log(datumOpen)
            datumClose := math.log(datumClose)

        line1 = Line.new(((datumLow - barLow) / (datumOpenTime - barOpenTime)), barOpenTime, barLow, log)
        line2 = Line.new(((datumHigh - barLow) / (datumOpenTime - barOpenTime)), barOpenTime, barLow, log)
        line3 = Line.new(((datumOpen - barOpen) / (datumOpenTime - barOpenTime)), barOpenTime, barOpen, log)
        line4 = Line.new(((datumClose - barOpen) / (datumOpenTime - barOpenTime)), barOpenTime, barOpen, log)

        line5 = Line.new(((datumLow - barHigh) / (datumOpenTime - barOpenTime)), barOpenTime, barHigh, log)
        line6 = Line.new(((datumHigh - barHigh) / (datumOpenTime - barOpenTime)), barOpenTime, barHigh, log)
        line7 = Line.new(((datumOpen - barClose) / (datumOpenTime - barOpenTime)), barOpenTime, barClose, log)
        line8 = Line.new(((datumClose - barClose) / (datumOpenTime - barOpenTime)), barOpenTime, barClose, log)

        if(showWickLines)
            rsLines.push(calculateRSLineData(line1, true, log))
            rsLines.push(calculateRSLineData(line2, true, log))
            rsLines.push(calculateRSLineData(line5, true, log))
            rsLines.push(calculateRSLineData(line6, true, log))
        if(showBodyLines)
            rsLines.push(calculateRSLineData(line3, false, log))
            rsLines.push(calculateRSLineData(line4, false, log))
            rsLines.push(calculateRSLineData(line7, false, log))
            rsLines.push(calculateRSLineData(line8, false, log))

    removed = removeCloseLines(rsLines)  
    if(array.size(removed) > 0)
        array.slice(removed, 0 , math.min(array.size(removed), math.max(totalLines, 30)))
    else
        removed


var rsLines = array.new<RSLine>(0)

if(not useBarIndexSet)
    if(bar_index > 0)
        openTime = time[bar_index - 1]
        closeTime = time_close[bar_index - 1]
        if(closeTime - openTime <= 60 * 60 * 1000)
            similarity := similarity * 1.6
            closeness := closeness / 4
        else if(closeTime - openTime <= 4 * 60 * 60 * 1000)
            similarity := similarity * 1.35
            closeness := closeness / 3
        else if(closeTime - openTime <= 24 * 60 * 60 * 1000)
            similarity := similarity * 1.2
            closeness := closeness / 2
        else if(closeTime - openTime <= 3 * 24 * 60 * 60 * 1000)
            similarity := similarity * 1.1
            closeness := closeness / 1.5
        useBarIndexSet := true

if(not weStarted)
    weStarted := can_we_start(maxBarBack, UTC)
if(weStarted)
    if(startBarIndex == -1)
        startBarIndex := bar_index
        startBarTime := time

    if(array.size(rsLines) > 1 and (dontCountAfter >= time or dontCountAfter == 0))
        for j = 0 to (array.size(rsLines) - 1)
            rsLine = array.get(rsLines, j)
            if(rsLine.barIndex > bar_index - maxBarBack)
                calculateMissingRSLineData(rsLine, useLogarithmic)

    if(not barstate.islast and (dontCountAfter >= time or dontCountAfter == 0))
        linesForBar = findRSLines(useLogarithmic)
        if(array.size(linesForBar) > 0)
            for j = 0 to (array.size(linesForBar) - 1)
                array.push(rsLines, array.get(linesForBar, j))
            if(bar_index % 20 == 0 or (bar_index % 5 and array.size(rsLines) > 100))
                rsLines := removeCloseLines(rsLines)  
                if(array.size(rsLines) > 0)
                    rsLines := array.slice(rsLines, 0 , math.min(array.size(rsLines), totalLines * 5))

    if(barstate.islast)
        removed = removeCloseLines(rsLines)  
        if(array.size(removed) > 0)
            removed := array.slice(removed, 0 , math.min(array.size(removed), totalLines))
            for i = 0 to (array.size(removed) - 1)
                rsLine = array.get(removed, i)
                tradingViewPlot(rsLine._line, i * 0.8 / array.size(removed), rsLine.isShadows)
        else
            label.new(bar_index, close, "Nothing found. Please adjust some parameters. ex. decrease max bar back parameter to available bar count at your current chart.")
else if(barstate.islast)
    label.new(bar_index, close, "Please decrease max bar back parameter to available bar count at your current chart.")
    

