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

//@version=5
indicator("Rolling VWAP", "RVWAP", true)

// Rolling VWAP
// v3, 2022.07.24

// This code was written using the recommendations from the Pine Script™ User Manual's Style Guide:
//   https://www.tradingview.com/pine-script-docs/en/v5/writing/Style_guide.html


import PineCoders/ConditionalAverages/1 as pc



// ———————————————————— 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 VWAP. The default is the average of the high, low and close prices."
string TT_WINDOW  = "By default, the time period used to calculate the RVWAP 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_STDEV   = "The multiplier for the standard deviation bands offset above and below the RVWAP. Example: 1.0 is 100% of the offset value. 
  \n\nNOTE: A value of 0.0 will hide the bands."
string TT_TABLE   = "Displays the time period of the rolling window."


// ————— Inputs
float   srcInput                = input.source(hlc3, "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.gray,   "",         inline = "21", group = GRP2)
color   infoBoxTxtColorInput    = input.color(color.white,  "T",        inline = "21", group = GRP2)

string  GRP3                    = '═════════  Deviation Bands  ═════════'
float   stdevMult1              = input.float(0.0, "Bands Multiplier 1", group = GRP3, inline = "31", minval = 0.0, step = 0.5, tooltip = TT_STDEV)
float   stdevMult2              = input.float(0.0, "Bands Multiplier 2", group = GRP3, inline = "32", minval = 0.0, step = 0.5, tooltip = TT_STDEV)
float   stdevMult3              = input.float(0.0, "Bands Multiplier 3", group = GRP3, inline = "33", minval = 0.0, step = 0.5, tooltip = TT_STDEV)
color   stdevColor1             = input.color(color.green,  "",          group = GRP3, inline = "31")
color   stdevColor2             = input.color(color.yellow, "",          group = GRP3, inline = "32")
color   stdevColor3             = input.color(color.red,    "",          group = GRP3, inline = "33")

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 RVWAP 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)


// @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.")

// RVWAP + stdev bands
int   timeInMs     = fixedTfInput ? minsInput + hoursInput + daysInput : timeStep()

float sumSrcVol    = pc.totalForTimeWhen(srcInput * volume, timeInMs, true, minBarsInput)
float sumVol       = pc.totalForTimeWhen(volume, timeInMs, true, minBarsInput)
float sumSrcSrcVol = pc.totalForTimeWhen(volume * math.pow(srcInput, 2), timeInMs, true, minBarsInput)

float rollingVWAP  = sumSrcVol / sumVol

float variance     = sumSrcSrcVol / sumVol - math.pow(rollingVWAP, 2)
variance          := math.max(0, variance)

float stDev        = math.sqrt(variance)

float upperBand1   = rollingVWAP + stDev * stdevMult1
float lowerBand1   = rollingVWAP - stDev * stdevMult1

float upperBand2   = rollingVWAP + stDev * stdevMult2
float lowerBand2   = rollingVWAP - stDev * stdevMult2

float upperBand3   = rollingVWAP + stDev * stdevMult3
float lowerBand3   = rollingVWAP - stDev * stdevMult3

plot(rollingVWAP, "Rolling VWAP", color.orange)

p1 = plot(stdevMult1 != 0 ? upperBand1 : na, "Upper Band 1", stdevColor1)
p2 = plot(stdevMult1 != 0 ? lowerBand1 : na, "Lower Band 1", stdevColor1)

p3 = plot(stdevMult2 != 0 ? upperBand2 : na, "Upper Band 2", stdevColor2)
p4 = plot(stdevMult2 != 0 ? lowerBand2 : na, "Lower Band 2", stdevColor2)

p5 = plot(stdevMult3 != 0 ? upperBand3 : na, "Upper Band 3", stdevColor3)
p6 = plot(stdevMult3 != 0 ? lowerBand3 : na, "Lower Band 3", stdevColor3)

fill(p1, p2, color.new(color.green, 95), "Bands Fill")
fill(p3, p4, color.new(color.green, 95), "Bands Fill")
fill(p5, p6, color.new(color.green, 95), "Bands Fill")

// Display of time period.
var table tfDisplay = table.new(infoBoxYPosInput + "_" + infoBoxXPosInput, 1, 1)
if showInfoBoxInput and barstate.islastconfirmedhistory
    table.cell(tfDisplay, 0, 0, tfString(timeInMs), bgcolor = infoBoxColorInput, text_color = infoBoxTxtColorInput, text_size = infoBoxSizeInput)
// }