4C Volume w/ Relative Volume at Time
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © FourC
//@version=5

// Credits to @author=e2e4mfck , @author=LucF, and TradingView as a large part of the RVOL code is from their indicator: 'Relative Volume At Time' 
// Credit also goes to // @author LazyBear. Portions of the Volume indicator is adapted from - HawkEye Volume Indicator [LazyBear] 

MAX_BARS_BACK = 5000
indicator('4C Volume w/ Relative Volume at Time', '4C Vol+RVOL', false, max_labels_count=500, max_bars_back=MAX_BARS_BACK)

////////VOLUME Indicator with moving average

_10 = input(true, '═══════════  "Hawkeye" Volume  ════════════')

length = input(200)
range_1 = high - low
rangeAvg = ta.sma(range_1, length)
mov1 = input(true, title='Enable Simple Moving Avg')
mov2 = input.int(20, minval=1, title='Vol Moving Avg Length')

volumeA = ta.sma(volume, length)
divisor = input(3.6)

high1 = high[1]
low1 = low[1]
mid1 = hl2[1]

u1 = mid1 + (high1 - low1) / divisor
d1 = mid1 - (high1 - low1) / divisor

r_enabled1 = range_1 > rangeAvg and close < d1 and volume > volumeA
r_enabled2 = close < mid1
r_enabled = r_enabled1 or r_enabled2

g_enabled1 = close > mid1
g_enabled2 = range_1 > rangeAvg and close > u1 and volume > volumeA
g_enabled3 = high > high1 and range_1 < rangeAvg / 1.5 and volume < volumeA
g_enabled4 = low < low1 and range_1 < rangeAvg / 1.5 and volume > volumeA
g_enabled = g_enabled1 or g_enabled2 or g_enabled3 or g_enabled4

gr_enabled1 = range_1 > rangeAvg and close > d1 and close < u1 and volume > volumeA and volume < volumeA * 1.5 and volume > volume[1]
gr_enabled2 = range_1 < rangeAvg / 1.5 and volume < volumeA / 1.5
gr_enabled3 = close > d1 and close < u1
gr_enabled = gr_enabled1 or gr_enabled2 or gr_enabled3

v_color = gr_enabled ? color.gray : g_enabled ? color.rgb(0,255,0) : r_enabled ? color.rgb(255,0,0) : color.blue
plot(volume, style=plot.style_columns, color=v_color, title = 'Volume')

mov3 = ta.sma(volume, mov2)
plot(mov3 and mov1 ? mov3 : na, title='Volume Moving Avg', color=color.new(color.aqua, 0), linewidth=2)

////////Relative Volume at Time 

// —————————— Constants and inputs {
// ————— Color constants for input default values.
var color c_BULL_BRIGHT = color.new(color.blue, 0)
var color c_BEAR_BRIGHT = color.new(color.purple, 0)

// ————— String constants used in input options.
// Boolean inputs.
var string ON = 'On'
var string OFF = 'Off'
// Calculation modes.
var string CM1 = 'Cumulate from Beginning of TF to Current Bar'
var string CM2 = 'Point-to-Point Bars at Same Offset from Beginning of TF'
// TF selection modes.
var string TF1 = 'Auto-Steps Timeframe'
var string TF2 = 'Fixed Timeframe'
var string TF3 = 'Time (only works with intraday timeframes)'
// Threshold modes.
var string TM1 = 'Fixed Value'
var string TM2 = 'Standard Deviation Channel From Fixed Lookback'
var string TM3 = 'High/Low Channel From Beginning of TF'
var string TM4 = 'High/Low Channel From Beginning of Past Volume Lookback'
var string TM5 = 'High/Low Channel From Fixed Lookback'
// Volatility filter modes.
var string VT1 = 'All'
var string VT2 = 'High only'
var string VT3 = 'Low only'

// ————— Inputs

_20 = input(true, '══════════  Relative Volume Colors  ═══════════')
color i_c_bullBright = input(c_BULL_BRIGHT, 'High Relative Volume')
color i_c_bearBright = input(c_BEAR_BRIGHT, 'Low/Avg Relative Volume')

_40 = input(true, '═════════════ Settings  ═════════════')
string i_calcMode = input.string(CM1, 'Calculation Mode', options=[CM1, CM2])
int i_period = input.int(5, 'Past Volume Lookback in TF units', minval=1)
string i_tfType = input.string(TF1, 'Define Timeframes Units Using', options=[TF1, TF2, TF3])
string i_tf = input.timeframe('D', '  Fixed')
int i_hour = input.int(09, '  Time (hour)', minval=0, maxval=23)
int i_minute = input.int(30, '  Time (minute)', minval=0, maxval=59)
string i_thresholdMode = input.string(TM1, 'Threshold Mode', options=[TM1, TM2, TM3, TM4, TM5])
float i_thresholdFixedVal = input.float(2.0, '  Fixed Threshold', minval=1, step=0.1)
int i_thresholdLookback = input.int(100, '  Channel Lookback', minval=1)
float i_thresholdStdevFac = input.float(1.0, '  StDev Factor', minval=0, step=0.1)
int i_avgPeriod = input.int(14, 'Period of RelVol Moving Average', minval=1)
int i_fastVolatility = input.int(7, 'High Volatility When Short-term ATR', minval=1)
int i_slowVolatility = input.int(40, '  Is Higher Than Long-term ATR', minval=2)

// }

// —————————— Functions {
// ————— Returns 1 when `_c` boolean is true, 0 if false.
f_01(_c) =>
    _c ? 1 : 0

// ————— Converts current chart timeframe into a float minutes value.
f_chartTfInMinutes() =>
    float _resInMinutes = timeframe.multiplier * (timeframe.isseconds ? 1. / 60 : timeframe.isminutes ? 1. : timeframe.isdaily ? 60. * 24 : timeframe.isweekly ? 60. * 24 * 7 : timeframe.ismonthly ? 60. * 24 * 30.4375 : na)
    _resInMinutes

// ————— Returns `_tf` timeframe in float minutes.
f_tfInMinutes(_tf) =>
    // _tf: TF (in `timeframe.period` string format) to be converted in float minutes.
    // Dependency: f_chartTfInMinutes()
    request.security(syminfo.tickerid, _tf, f_chartTfInMinutes(), lookahead=barmerge.lookahead_on)

// ————— Given a `_res` timeframe in float minutes, returns the ideal timeframe to use in calcs.
f_tfAutoSteps(_tf) =>
    // _tf: chart's TF in fractional minutes from which to derive a HTF.
    _tf <= 0.02 ? '1' : _tf <= 1 ? '60' : _tf < 60. * 24 ? '1D' : _tf == 60. * 24 ? '1W' : _tf < 60. * 24 * 30.4375 ? '1M' : '12M'

// ————— Function rounding OHLC values to tick precision.
f_ohlcRoundedToTick() =>
    [math.round(open / syminfo.mintick) * syminfo.mintick, math.round(high / syminfo.mintick) * syminfo.mintick, math.round(low / syminfo.mintick) * syminfo.mintick, math.round(close / syminfo.mintick) * syminfo.mintick]
[o, h, l, c] = f_ohlcRoundedToTick()

// —————————— Bar up/dn states.
// ————— Returns true when a bar is considered to be an up bar.
f_barUp() =>
    // Dependencies: `o` and `c`, which are the `open` and `close` values rounded to tick precision.
    // Account for the normal "close > open" condition, but also for zero movement bars when their close is higher than previous close.
    _barUp = c > o or c == o and c > nz(c[1], c)
    _barUp
// ————— Returns true when a bar is considered to be a down bar.
f_barDn() =>
    // Dependencies: `o` and `c`, which are the `open` and `close` values rounded to tick precision.
    // Account for the normal "close < open" condition, but also for zero movement bars when their close is lower than previous close.
    _barDn = c < o or c == o and c < nz(c[1], c)
    _barDn

// ————— Function displays a label containing `_txt` at position `_y` in `_color` and at bar `_offset` from last bar. No background is used.
f_print(_txt, _y, _color, _offset) =>
    // Keep track of minimum time between bars (chart's timeframe in ms) using `abs()` so this works with pre-1970 negative times.
    var _tf = 10e15
    _tf := math.min(math.abs(time - nz(time[1])), _tf)
    // Calculate time offset for x position.
    _t = int(time + _tf * _offset)
    // Create the label on the first bar and only update it when on the dataset's last bar; it's more efficient.
    var label _lbl = label.new(_t, _y, _txt, xloc.bar_time, yloc.price, #00000000, label.style_label_left, _color, size.large, text.align_left)
    if barstate.islast
        label.set_xy(_lbl, _t, _y)
        label.set_text(_lbl, _txt)
        label.set_textcolor(_lbl, _color)

// ————— Function appends the `_text` error message to the `_msg` string when `_error` is true.
f_checkError(_error, _msg, _text) =>
    _error ? _msg + _text + '\n' : _msg

// ————— Function returns the farthest usable lookback, givne a user-selected number of periods.
f_optimalLookback(_barNosAtTfTransitions, _userPeriods) =>
    // _barNosAtTfTransitions: Id of array containing bar nos of past TF transitions.
    // _userPeriods          : User-selected number of TF units to look back.
    // Dependency: MAX_BARS_BACK
    int _return = 0
    // Loop from now to past TFs until we find a point back that's too far.
    for _i = _userPeriods - 1 to 0 by 1
        if bar_index - array.get(_barNosAtTfTransitions, _i) > MAX_BARS_BACK - 1
            break
        _return += 1
        _return
    _return

// ————— Function returning the average of past point-to-point volume, i.e., for the bars with the same offset from the beginning of the TF.
f_pastVolPtp(_msSinceStartOfTf, _timeAtFarthest, _periods) =>
    // _msSinceStartOfTf: Bar's offset in milliseconds since the beginning of the TF.
    // _timeAtFarthest  : UTC time at the beginning of the lookback periods, so the farthest back we will be analyzing.
    // _periods         : number of past TFs units to average.
    // Dependency: MAX_BARS_BACK
    // Note that if bars with the same offset from the beginning of the TF are missing in the past TFs, the average will be calculated for a smaller number of TFs.

    // Array holding the cumulative volume for each of the `_periods` TFs in our lookback, 
    // but only counting the bars within the current offset from the beginning of the current TF's beginning.
    // We start with an empty array and add an element containing the cumulative volume for each TF in history we use.
    float[] _volumes = array.new_float(0)
    int _tfNo = 0
    for _i = 1 to MAX_BARS_BACK - 2 by 1
        if time[_i] < _timeAtFarthest or _tfNo >= _periods
            // We are farther back than the beginning of the lookback, or the required qty of TFs has been processed; exit loop.
            break
        if _msSinceStartOfTf == nz(_msSinceStartOfTf[_i])
            // A bar with the same time offset was found; add its volume.
            _tfNo += 1
            // Save the cumulated volume for the TF.
            array.push(_volumes, volume[_i])
    // ————— Return the avg of point-to-point volume.
    float _return = nz(array.avg(_volumes))
    _return

// ————— Function returning the average of cumulative past volume, i.e., 
//       the total volume from the beginning of each TF in the past until the point in that TF
//       corresponding to where the current bar is with regards to the beginning of the current TF.
f_pastVolCum(_msSinceStartOfTf, _timeAtFarthest, _periods, _timesAtTfTransitions) =>
    // _msSinceStartOfTf    : Bar's offset in milliseconds since the beginning of the TF.
    // _timeAtFarthest      : UTC time at the beginning of the lookback periods, so the farthest back we will be analyzing.
    // _periods             : number of past TFs units to average.
    // _timesAtTfTransitions: Array id containing the time at TF transitions, so we know where to reset volume.
    // Dependency: MAX_BARS_BACK

    // Array holding the cumulative volume for each of the `_periods` TFs in our lookback, 
    // but only counting the bars within the current offset from the beginning of the current TF's beginning.
    // We start with an empty array and add an element containing the cumulative volume for each TF in history we use.
    float[] _volumes = array.new_float(0)
    // Holds cumulative volume for the TF being cumulated in the loop.
    float _cumVolTot = 0.
    // Count of historical TFs we have analyzed.
    int _tfNo = 1
    // Non-capped no of user-selected periods.
    var int _userSelectedNoOfPeriods = array.size(_timesAtTfTransitions) - 1
    // Time at beginning of the TF the current bar belongs to; we will skip calcs until we're past that point.
    float _timeAtStartOfCurrentTf = array.get(_timesAtTfTransitions, _userSelectedNoOfPeriods)
    // Time at beginning of the TF the loop is calculating.
    float _timeAtStartOfPastTf = array.get(_timesAtTfTransitions, _userSelectedNoOfPeriods - 1)
    for _i = 1 to MAX_BARS_BACK - 2 by 1
        float _time = time[_i]
        if _time < _timeAtFarthest or _tfNo > _periods
            // We are farther back than the beginning of the lookback, or the required qty of TFs has been processed; exit loop.
            break
        if _time < _timeAtStartOfCurrentTf
            // We are past the current TF; we can calculate cumulative past volume.
            if _time - _timeAtStartOfPastTf <= _msSinceStartOfTf
                // The bar is in our scope; add its volume.
                _cumVolTot += volume[_i]
                if _time == _timeAtStartOfPastTf
                    // ————— We have reached the beginning of one of the TFs in our lookback; update/reset our numbers.
                    // Save the cumulated volume for the TF.
                    array.push(_volumes, _cumVolTot)
                    // Qty of processed TFs.
                    _tfNo += 1
                    // Get next TF's beginning time.
                    _timeAtStartOfPastTf := nz(array.get(_timesAtTfTransitions, math.max(0, _userSelectedNoOfPeriods - _tfNo)))
                    // Reset the TF's cum volume.
                    _cumVolTot := 0.
                    _cumVolTot
    // ————— Return the avg of cumulative past volume.
    float _return = nz(array.avg(_volumes))
    _return

// ————— Function returning true when a high volatility condition is detected.
f_highVolatility() =>
    // Dependencies : i_fastVolatility, i_slowVolatility
    ta.atr(i_fastVolatility) > ta.atr(i_slowVolatility)
// }

// —————————— Calculations {
// ————— Determine timeframe from user selection.
var string tf = i_tfType == TF2 ? i_tf : f_tfAutoSteps(f_chartTfInMinutes())

// ————— Detect new TF.
// Logic for TOD TF units.
var int dailyTodTriggers = 0
float timeMark = timestamp(year, month, dayofmonth, i_hour, i_minute, 00)
float timeDailyClose = request.security(syminfo.tickerid, 'D', time_close, lookahead=barmerge.lookahead_on)
bool beginTime = time == timeMark
bool pastTime = time > timeMark
bool lastBarofDay = time_close == timeDailyClose
bool newday = ta.change(time('D'))
bool atOrPastBeginTime = false
if barstate.isfirst
    atOrPastBeginTime := true
    dailyTodTriggers := 1
    dailyTodTriggers
else if newday
    dailyTodTriggers := 0
    dailyTodTriggers
if not beginTime[1] and beginTime or dailyTodTriggers == 0 and (pastTime or lastBarofDay)
    // The user-defined TOD was reached, or it's the first time in the day that we are past it, 
    // or we are on the day's last bar and no suitable bar has been detected yet.
    dailyTodTriggers += 1
    atOrPastBeginTime := true
    atOrPastBeginTime

// TF resets using user-selected mode.
bool tfReset = nz(i_tfType == TF3 and timeframe.isminutes ? atOrPastBeginTime : ta.change(time(tf)), true)

// ————— Manage TF transitions and cumulate volume of current TF.
// Array holding the time and bar_index at TF transitions.
var float[] timesAtTfTransitions = array.new_float(i_period + 1, time)
var int[] barNosAtTfTransitions = array.new_int(i_period + 1, 0)
// Cumulative volume of current TF.
var float cumVolume = 0.
// Time at point from where we calculate the time offset between the current bar and the beginning of the current TF.
var int timeAtStartOfTf = time
// Handle TF transition calcs.
if tfReset
    // Maintain queues of the time and bar_index at which our last `i_period` TFs begin. We'll need this when calculating the cumulative past volume.
    // The last element always contains the info for the current TF, which won't be used in calcs of past volume. If the period is capped, we won't be using all elements.
    array.push(timesAtTfTransitions, time)
    array.shift(timesAtTfTransitions)
    array.push(barNosAtTfTransitions, bar_index)
    array.shift(barNosAtTfTransitions)
    // Reset cumulative volume from beginning of current TF.
    cumVolume := volume
    cumVolume
else
    // Keep track of total volume from beginning of current TF.
    cumVolume += volume
    cumVolume

// ————— Get new time offsets.
// Cap period if user-selected one causes us to look back too far away.
int cappedPeriod = math.min(i_period, f_optimalLookback(barNosAtTfTransitions, i_period))
// Bar no at beginning of current TF.
int barAtStartOfTf = array.get(barNosAtTfTransitions, i_period)
// Save time on first bar.
var float timeAtBarZero = time
// Time at `cappedPeriod` * TF units back, which is the furthest in past we will look back.
float timeAtFarthestLookback = array.get(timesAtTfTransitions, i_period - cappedPeriod)
// Bar's offset in milliseconds since the beginning of the current TF.
float msSinceStartOfTf = time - array.get(timesAtTfTransitions, i_period)

// ————— Relative volume.
// Get past and current volume as per user-selected calc mode.
float pastVolume = i_calcMode == CM1 ? f_pastVolCum(msSinceStartOfTf, timeAtFarthestLookback, cappedPeriod, timesAtTfTransitions) : f_pastVolPtp(msSinceStartOfTf, timeAtFarthestLookback, cappedPeriod)
float currentVolume = i_calcMode == CM1 ? cumVolume : volume
float relVol = pastVolume == 0 ? 0. : currentVolume / pastVolume

// ————— Directional relvol avg.
float relVolDir = f_barUp() ? relVol : -relVol
float relVolAvgDir = ta.hma(relVolDir, i_avgPeriod)

// ————— Relvol avg.
float relVolAvg = ta.hma(relVol, i_avgPeriod)

// ————— Threshold
// Highs/Lows.
// Bar no at `cappedPeriod` TF units back.
int barAtFarthestLookback = array.get(barNosAtTfTransitions, i_period - cappedPeriod)
int hiLoLookback = i_thresholdMode == TM3 ? bar_index - barAtStartOfTf + 1 : i_thresholdMode == TM4 ? bar_index - barAtFarthestLookback + 1 : i_thresholdLookback
hiLoLookback := int(math.min(math.max(1, nz(hiLoLookback)), MAX_BARS_BACK - 1))
float hiLoHigh = ta.highest(relVol, hiLoLookback)
float hiLoLow = ta.lowest(relVol, hiLoLookback)
// Stdev.
float relVolStDev = ta.stdev(relVol, i_thresholdLookback)
float relVolSma = ta.sma(relVol, i_thresholdLookback)
float relVolStDevHigh = relVolSma + relVolStDev * i_thresholdStdevFac
float relVolStDevLow = math.max(0, relVolSma - relVolStDev * i_thresholdStdevFac)

[thresholdHi, thresholdLo] = if i_thresholdMode == TM1
    [i_thresholdFixedVal, i_thresholdFixedVal]
else if i_thresholdMode == TM2
    [relVolStDevHigh, relVolStDevLow]
else if i_thresholdMode == TM3 or i_thresholdMode == TM4 or i_thresholdMode == TM5
    [hiLoHigh, hiLoLow]

// ————— Error conditions
// Critical errors causing plots to be hidden.
bool chartTfIsTooLow = f_chartTfInMinutes() > f_tfInMinutes(tf)
bool noRelVol = na(relVol)
bool noTfUnits = barAtStartOfTf == 0 and bar_index > MAX_BARS_BACK
bool noVolume = na(volume)
bool dontPlot = chartTfIsTooLow or noRelVol or noTfUnits or noVolume
// Non-critical errors.
bool lookbackIsTooBig = i_period != cappedPeriod

bool cantUseTod = i_tfType == TF3 and not timeframe.isminutes
// }

//RVOL Background Color Plot

color c_pastVolMark = relVol > 1 ? i_c_bullBright : i_c_bearBright
bgcolor(color.new(c_pastVolMark,50), title = 'RVOL Background Fill')






