Two sided mean deviation Indicator [SQT]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © SwissQuantTrader

//@version=5
indicator(title="Two sided mean deviation Indicator [SQT]", shorttitle="Mean Deviation [SQT]", precision=2, overlay=false)

// user parameters
last_n_observations = input.int(defval=50, title="Use N candles", minval=5, maxval=1000, step=5, tooltip="How many candles to include for calculating the return means and standard deviations to derive the lower and upper bounds. The return means and standard deviations for up and down moves are calculated separately.")
sigmas = input.float(defval=2, title="Sigma bounds", minval=0.3, maxval=5, step=0.1, tooltip="How many standard deviations (sigmas) should the lower and upper bounds be away from the mean return. The return means and standard deviations for up and down moves are calculated separately.")
decay = input.string(defval="equally weighted", title="Decay", options=["equally weighted", "age weighted"], tooltip='How the lower and upper bounds decay back to their mean, if "equally weighted" then the bounds decay with equally weighing the observations, if "age weighted" then the bounds decay with giving more weight to more recent observations.')
// hard parameters
sum_and_ema = 3
source = close
weight = 20

// create array with latest N down percent changes (equally weighted)
var downs = array.new_float(size=last_n_observations)
if (source - source[1]) / source[1] < 0
    array.shift(id=downs)
    array.push(id=downs, value=100 * ((source - source[1]) / source[1]))
// create array with latest N down percent changes (age weighted)
var downs_forget = array.new_float(size=last_n_observations)
for i = 0 to last_n_observations - 1
    array.shift(id=downs_forget)
    array.push(id=downs_forget, value=array.get(id=downs, index=last_n_observations - i - 1) * ((last_n_observations - i) / weight))

// create array with latest N up percent changes (equally weighted)
var ups = array.new_float(size=last_n_observations)
if (source - source[1]) / source[1] > 0
    array.shift(id=ups)
    array.push(id=ups, value=100 * ((source - source[1]) / source[1]))
// create array with latest N up percent changes (age weighted)
var ups_forget = array.new_float(size=last_n_observations)
for i = 0 to last_n_observations - 1
    array.shift(id=ups_forget)
    array.push(id=ups_forget, value=array.get(id=ups, index=last_n_observations - i - 1) * ((last_n_observations - i) / weight))

// instantiate lower and upper bounds
var lower = float(x=0)
var upper = float(x=0)
// calculate lowere and upper bounds
if decay == "equally weighted"
    lower := array.avg(id=downs) - (array.stdev(id=downs) * sigmas)
    upper := array.avg(id=ups) + (array.stdev(id=ups) * sigmas)
else
    lower := array.avg(id=downs_forget) - (array.stdev(id=downs_forget) * sigmas)
    upper := array.avg(id=ups_forget) + (array.stdev(id=ups_forget) * sigmas)
// plot the lower and upper bounds
lower_plot = plot(series=lower, color=#FF00FF)
upper_plot = plot(series=upper, color=#FF00FF)

// fill the area between the lower and upper bounds
fill(plot1=upper_plot, plot2=lower_plot, color=color.new(color=#FF00FF, transp=90))

// create and plot signal line as sum and EMA from last 'sum_and_ema' values
signal = 100 * ((source - source[1]) / source[1])
signal := math.sum(source=signal, length=sum_and_ema)
signal := ta.ema(source=signal, length=sum_and_ema)
plot(series=signal, color=#00FFFF, linewidth=2)

// plot a horizontal line at 0
hline(price=0, color=color.gray, linestyle=hline.style_dashed, linewidth=1)
