Move Magnitude Visualizer (beta)
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Electrified

//@version=5
indicator("Move Magnitude Visualizer", shorttitle = "MMV", overlay = true, max_bars_back = 2000)

var samples = input.int(1200, "Samples", 50, 2000, 50, tooltip = "The number of bars back to begin sampling. Reduce this number if the script takes to long.")
var maxLen = math.floor(samples * input.float(5, "Max Length (%)", 0.1, 100, 1, tooltip = "The maximum length as a percentage of the total samples.") / 100)
var minLen = input.int(4, "Min Length", 1)

var bar = -1
bar += 1

calculateSD(float[] data, float mean) =>
    sumSquaredDiff = 0.0
    for e in data
        sumSquaredDiff += math.pow(e - mean, 2)
    math.sqrt(sumSquaredDiff / array.size(data))
////    

filteredMean(float[] data, float max) =>
    sum = 0.0
    count = 0
    if max < 0
        for e in data
            if e > max
                sum += e
                count += 1
    else
        for e in data
            if e < max
                sum += e
                count += 1
    [sum, count]


calculateSD(float[] data, float mean, int count, float max) =>
    sumSquaredDiff = 0.0
    if max < 0
        for e in data
            if e > max
                sumSquaredDiff += math.pow(e - mean, 2)
    else
        for e in data
            if e < max
                sumSquaredDiff += math.pow(e - mean, 2)

    math.sqrt(sumSquaredDiff / count)
////    

getDeviation(float d, int len) =>
    var data = array.new_float()
    var mean = 0.0
    var std = 0.0
    // don't calculate or store unnecessary data.
    if d != 0 and bar > last_bar_index - samples
        array.push(data, d)
        if array.size(data) > samples
            array.shift(data)
        m = array.avg(data)
        sign = (m < 0 ? -1 : 1)
        s = calculateSD(data, m) * sign
        max = m + s * 4
        [sum, count] = filteredMean(data, max)
        mean := sum / count
        std := calculateSD(data, mean, count, max) * sign
        
    [mean, std]    
////

getDeviationUp(int len) =>
    lo = low[len*2]
    d = math.max(high[len] - lo, 0) / lo
    getDeviation(d, len)
////

getDeviationDn(int len) =>
    hi = high[len*2]
    d = math.min(low[len] - hi, 0) / hi
    getDeviation(d, len)
////

var d3color = input.color(color.new(color.red, 50), "D3", "Level 3", group = "Deviation Lines")
var d2color = input.color(color.new(color.orange, 50), "D2", "Level 2", group = "Deviation Lines")
var d1color = input.color(color.new(color.yellow, 50), "D1", "Level 2", group = "Deviation Lines")

var upLines = array.new_line(maxLen + 1, na)
var dnLines = array.new_line(maxLen + 1, na)
 
maxUp = 0.0
maxDn = 0.0

for i = minLen to maxLen
    [upM, upS] = getDeviationUp(i)
    [dnM, dnS] = getDeviationDn(i)

    // Minimize processing to recent.
    if bar > last_bar_index - maxLen * 3
      
        upLine_prev = array.get(upLines, i)
        upline_prev_lo = na(upLine_prev) ? na : line.get_y1(upLine_prev)
        upline_prev_hi = na(upLine_prev) ? na : line.get_y2(upLine_prev)
        lo = low[i]
        if high < upline_prev_hi ? lo > upline_prev_lo : false
            if maxUp > (upline_prev_hi - upline_prev_lo) / upline_prev_lo
                line.delete(upLine_prev)
                array.set(upLines, i, na)
            upLine_prev 
        else
            up1 = upM + upS
            up2 = up1 + upS
            up3 = up2 + upS

            up = math.max(high - lo, 0) / lo

            upLine = if up > maxUp
                maxUp := up
                if up > up3
                    line.new(bar - i, lo, bar, high, color = d3color, width = 4)
                else if up > up2
                    line.new(bar - i, lo, bar, high, color = d2color, width = 2)
                else if up > up1
                    line.new(bar - i, lo, bar, high, color = d1color, width = 1, style = line.style_dotted)
                else
                    na
            else
                na
   
            line.delete(upLine_prev)
            array.set(upLines, i, upLine)

        dnLine_prev = array.get(dnLines, i)
        dnLine_prev_hi = na(dnLine_prev) ? na : line.get_y1(dnLine_prev)        
        dnLine_prev_lo = na(dnLine_prev) ? na : line.get_y2(dnLine_prev)
        hi = high[i]
        if low > dnLine_prev_lo ? hi < dnLine_prev_hi : false
            if maxDn < (dnLine_prev_lo - dnLine_prev_hi) / dnLine_prev_hi
                line.delete(dnLine_prev)
                array.set(dnLines, i, na)   
            dnLine_prev
        else
            dn1 = dnM + dnS
            dn2 = dn1 + dnS
            dn3 = dn2 + dnS

            dn = math.min(low - hi, 0) / hi

            dnLine = if dn < maxDn
                maxDn := dn
                if dn < dn3
                    line.new(bar - i, hi, bar, low, color = d3color, width = 4)
                else if dn < dn2
                    line.new(bar - i, hi, bar, low, color = d2color, width = 2)
                else if dn < dn1
                    line.new(bar - i, hi, bar, low, color = d1color, width = 1, style = line.style_dotted)
                else
                    na
            else
                na

            line.delete(dnLine_prev)
            array.set(dnLines, i, dnLine)
