Rolling MACD
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © chervolino

//@version=5

indicator("Rolling MACD", "RMACD")

// Rolling MACD
// v1.1


// ———————————————————— Constants and Inputs {

// ————— Constants
int MS_IN_MIN   = 60 * 1000
int MS_IN_HOUR  = MS_IN_MIN  * 60
int MS_IN_DAY   = MS_IN_HOUR * 24

string TT_SRC     = "The source used to calculate the MACD."
string TT_WINDOW  = "By default, the time period used to calculate the RMACD automatically adjusts with the chart's timeframe.
  Check this to use a fixed-size time period instead, which you define with the following three values."
string TT_MINBARS = "The minimum number of last values to keep in the moving window, even if these values are outside the time period.
  This avoids situations where a large time gap between two bars would cause the time window to be empty."
string TT_TABLE   = "Displays the time period of the rolling window."


// ————— Inputs
float   srcInput                = input.source(close, "Source", tooltip = TT_SRC)

string  GRP2                    = '═══════════   Time Period   ═══════════'
bool    fixedTfInput            = input.bool(false,         "Use a fixed time period", group = GRP2, tooltip = TT_WINDOW)
int     daysInput               = input.int(1,              "Days",                    group = GRP2, minval = 0, maxval = 90) * MS_IN_DAY
int     hoursInput              = input.int(0,              "Hours",                   group = GRP2, minval = 0, maxval = 23) * MS_IN_HOUR
int     minsInput               = input.int(0,              "Minutes",                 group = GRP2, minval = 0, maxval = 59) * MS_IN_MIN
bool    showInfoBoxInput        = input.bool(true,          "Show time period",        group = GRP2)
string  infoBoxSizeInput        = input.string("small",     "Size ",    inline = "21", group = GRP2, options = ["tiny", "small", "normal", "large", "huge", "auto"])
string  infoBoxYPosInput        = input.string("bottom",    "↕",        inline = "21", group = GRP2, options = ["top", "middle", "bottom"])
string  infoBoxXPosInput        = input.string("left",      "↔",        inline = "21", group = GRP2, options = ["left", "center", "right"])
color   infoBoxColorInput       = input.color(color.black,   "",         inline = "21", group = GRP2)
color   infoBoxTxtColorInput    = input.color(color.white,  "T",        inline = "21", group = GRP2)

string  GRP4                    = '════════  Minimum Window Size  ════════'
int     minBarsInput            = input.int(10, "Bars", group = GRP4, tooltip = TT_MINBARS) 
// }

// ———————————————————— Functions {

// @function    Determines a time period from the chart's timeframe.
// @returns     (int) A value of time in milliseconds that is appropriate for the current chart timeframe. To be used in the RMACD calculation.

timeStep() =>
    int tfInMs = timeframe.in_seconds() * 1000
    float step =
      switch
        tfInMs <= MS_IN_MIN        => MS_IN_HOUR
        tfInMs <= MS_IN_MIN  * 5   => MS_IN_HOUR * 4
        tfInMs <= MS_IN_HOUR       => MS_IN_DAY  * 1
        tfInMs <= MS_IN_HOUR * 4   => MS_IN_DAY  * 3
        tfInMs <= MS_IN_HOUR * 12  => MS_IN_DAY  * 7
        tfInMs <= MS_IN_DAY        => MS_IN_DAY  * 30.4375
        tfInMs <= MS_IN_DAY  * 7   => MS_IN_DAY  * 90
        => MS_IN_DAY * 365
    int result = int(step)

totalForTimeWhen(src, ms, cond, minBars) =>
    var float[] sources = array.new_float(0)
    var int[]   times   = array.new_int(0)
    if cond
        array.push(sources, src)
        array.push(times,   time)
    if array.size(sources) > 0
        while time - array.get(times, 0) >= ms and array.size(sources) > minBars
            array.shift(sources)
            array.shift(times)
    float result = array.sum(sources)


// @function    Produces a string corresponding to the input time in days, hours, and minutes.
// @param       (series int) A time value in milliseconds to be converted to a string variable. 
// @returns     (string) A string variable reflecting the amount of time from the input time.
tfString(int timeInMs) =>
    int s  = timeInMs / 1000
    int m  = s / 60
    int h  = m / 60
    int tm = math.floor(m % 60)
    int th = math.floor(h % 24)
    int d  = math.floor(h / 24)
    string result = 
      switch
        d == 30 and th == 10 and tm == 30 => "1M"
        d == 7  and th == 0  and tm == 0  => "1W"
        =>
            string dStr = d  ? str.tostring(d)  + "D "  : ""
            string hStr = th ? str.tostring(th) + "H "  : ""
            string mStr = tm ? str.tostring(tm) + "min" : ""
            dStr + hStr + mStr
// }



// ———————————————————— Calculations and Plots {

// Stop the indicator on charts with no volume.
if barstate.islast and ta.cum(nz(volume)) == 0
    runtime.error("No volume is provided by the data vendor.")


int   timeInMs     = fixedTfInput ? minsInput + hoursInput + daysInput : timeStep()


z = input.int(45, title='Inventory Retracement Percentage %', maxval=100)
rv = math.abs(close - open) < z / 100 * math.abs(high - low)
x = low + z / 100 * math.abs(high - low)
y = high - z / 100 * math.abs(high - low)
x_1    = totalForTimeWhen(x, timeInMs, true, minBarsInput)
y_1    = totalForTimeWhen(y, timeInMs, true, minBarsInput)
sl = rv == 1 and high > y_1 and close < y_1 and open < y_1 
ss = rv == 1 and low < x_1 and close > x_1 and open > x_1
li = sl ? y_1 : ss ? x_1 : (x_1 + y_1) / 2

clo_1=totalForTimeWhen(srcInput, timeInMs, true, minBarsInput)
macd = clo_1 - li
macd_pre = clo_1[1] - li[1]

msignal = ta.ema(macd, 9)
signal = ta.ema(msignal, 9)
plotColor = macd > 0 ? macd > macd_pre ? color.lime : color.green : macd < macd_pre ? color.maroon : color.red
plot(msignal,color=plotColor, style =  plot.style_columns)
plot(signal, color=color.white, style =  plot.style_line)

is_long_short_text = msignal > signal? "Long" : "Short"
is_bullish_bearish_text = msignal > 0 ? "Bullish" : "Bearish"
is_long_short_css = msignal > signal? color.green : color.red
is_bullish_bearish_css = msignal > signal? color.green : color.red
// Display of time period.
var table tfDisplay = table.new(infoBoxYPosInput + "_" + infoBoxXPosInput, 3, 3)
if showInfoBoxInput and barstate.islastconfirmedhistory
    table.cell(tfDisplay, 1, 0, tfString(timeInMs), bgcolor = infoBoxColorInput, text_color = infoBoxTxtColorInput, text_size = infoBoxSizeInput)
    table.cell(tfDisplay, 1, 1, is_long_short_text, bgcolor = infoBoxColorInput, text_color = is_long_short_css, text_size = infoBoxSizeInput)
    table.cell(tfDisplay, 1, 2, is_bullish_bearish_text, bgcolor = infoBoxColorInput, text_color = is_bullish_bearish_css, text_size = infoBoxSizeInput)
// }