Linear Regression, Volume Weighted
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Electrified (electrifiedtrading) & shr395

// @version=5
// @description Produces deviation lines based upon linear regression of volume adjusted price with the option to clean the data of outliers.
indicator("Linear Regression, Volume Weighted", shorttitle="VWLR", overlay=true)
import Electrified/DataCleaner/6 as Data

var sess_minutes = syminfo.session == session.regular ? 390 : 1440
minutesPerBar = timeframe.isminutes ? timeframe.multiplier : timeframe.isseconds ? timeframe.multiplier / 60 : na

BARS = 'Bars', MINUTES = 'Mins', DAYS = 'Days', LENGTH = "Length"
minutesToLen(simple int len, simple string type) =>
    switch type
        BARS => len
        MINUTES => len / minutesPerBar
        DAYS => timeframe.isintraday ?
          (sess_minutes * len / minutesPerBar) :
          timeframe.isdaily ? len :
          timeframe.isweekly ? len / 7 : 
          timeframe.ismonthly ? len / 30 :
          na
        => na

src = input.source(hlc3, "Source")
len = minutesToLen(
  input.int(251, LENGTH, inline=LENGTH, minval=1),
  input.string(BARS, "", inline=LENGTH, options=[BARS, MINUTES, DAYS], tooltip="The length specifies the how far back to measure the regression.\nLines have a length limit so the lines may not extend all the way back. Use the 'extend' setting to extend further to the left."))
recency = input.float(0.0, "Recency Bias", minval=0.0, step=0.01, tooltip="The higher the number the more weight is given to recent values. Similar to the difference between SMA and WMA.\n\nNote: it may not take much to have an effect.")
multiple = input.float(1.0, "Multiplier", minval=0, step=0.1, tooltip="Value to apply as a multipler to the standard deviation.")

NEITHER = "Neither", RIGHT = "Right", LEFT = "Left", BOTH = "Both"
extendLines = input.string(NEITHER, "Extend Lines", options=[NEITHER, RIGHT, LEFT, BOTH])
extend = switch extendLines
    NEITHER => extend.none
    RIGHT => extend.right
    LEFT => extend.left
    BOTH => extend.both

level2 = input.bool(false, "Draw Level 2", tooltip="When true, shows another set of lines an additional standard deviation out from the level one lines.")
maxDeviation = input.float(4, "Max Deviation", group="Data Cleaning", minval=0, step=0.5, tooltip="The maximum deviation before a value is considered an outlier and removed from the dataset.")


//////////////////////////////////////////////////
calcForData(float[] data, float[] weights) =>

    sumX = 0.0
    sumY = 0.0
	sumXSqr = 0.0
	sumXY = 0.0
	sumMsqr = 0.0
	total = 0.0
    size = array.size(data)
    last = size - 1
	
	for i = 0 to last
		val = array.get(data, i)
        weight = array.get(weights, i) * (1 + recency * i)
        total += weight
        x = (i + 1) * weight
        y = val * weight
		sumX += x
		sumY += y
		sumXSqr += x * x / weight
		sumXY += x * y / weight
	
	slope = (total * sumXY - sumX * sumY) / (total * sumXSqr - sumX * sumX)
	average = sumY / total
	intercept = average - slope * sumX / total
	
    // Get true deviation values.
    devArray = array.new_float(size)
	for n = 0 to last
		array.set(devArray, n, array.get(data, n) - intercept + slope * n)
	
	[slope, average, intercept, intercept + slope * last, devArray]
//////////////////////////////////////////////////


//////////////////////////////////////////////////
calc() =>

    var data = array.new_float()
    var weights = array.new_float()
    array.push(data, src)
    array.push(weights, volume)
    size = array.size(data)
    
    if size > len
        array.shift(data)
        array.shift(weights)
        
        [slope, average, intercept, endValue, devArray] = calcForData(data, weights)
        
    	stddev = if Data.naArrayOutliers(devArray, maxDeviation)
    	    // SEDOND PASS
    	    // Data needs to be cleaned.  Use new na values in devArray to clean the orignal data.
    	    n = array.new_float()
            w = array.new_float()
            for i = 0 to len - 1
                if not na(array.get(devArray, i))
                    array.push(n, array.get(data, i))
                    array.push(w, array.get(weights, i))
            [s, a, i, e, d] = calcForData(n, w)   
            slope := s
            average := a
            intercept := i
            endValue := e
            array.stdev(d)
        else
            // Data was unchanged by cleaning process.  No outilers present.
            array.stdev(devArray)
            
    	[intercept, endValue, stddev * multiple]
        
    else
		[float(na), float(na), float(na)]
//////////////////////////////////////////////////


[startPrice, endPrice, sd] = calc()

// NOTE lines have length limits.
lineBarLimit = 1200
first = if len > lineBarLimit
    d = endPrice - startPrice
    s = d / len
    startPrice += s * (len - lineBarLimit)
    lineBarLimit
else
    len - 1

drawLine(float priceOffset=0, style=line.style_solid, color=color.silver, transp=50) =>
    var line L = na
    var c = color.new(color, transp)
    if na(startPrice)
        L
    else if na(L)
    	L := line.new(time[first], startPrice+priceOffset, time, endPrice+priceOffset, width=1, extend=extend, color=c, style=style, xloc=xloc.bar_time)
    else if not na(startPrice)
        line.set_xy1(L, time[first], startPrice+priceOffset)
    	line.set_xy2(L, time, endPrice+priceOffset)
    	L
    	
drawLines(float priceOffset=0, style=line.style_solid, color=color.silver, transp=50) =>
    drawLine(+priceOffset, style, color, transp)
    drawLine(-priceOffset, style, color, transp)

drawLine(0)
sdHalf = sd / 2

drawLines(sdHalf, line.style_dashed, transp=85)
drawLines(sd, line.style_dashed)

if level2
    drawLines(3 * sdHalf, line.style_dashed, color.orange, 85)
    drawLines(2 * sd, line.style_dashed, color.orange)

