Directional Volume EStimate from Price Action (RedK D_VESPA)
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © RedKTrader - May 2023

//@version=5
indicator(title='Directional Volume Estimate from Pirce Action v1.0', shorttitle='RedK D_VESPA v1.0', 
  format=format.volume, timeframe='', timeframe_gaps=false, explicit_plot_zorder = true)

// Originally this was based on V.Viewer - it estimates and plots average supply & demand volume based on price action
// it uses the shape of a price bar to estimate the supply vs demand split of the traded volume 
// & plots a moving average of both, and an estimated Average "Net Volume" 
// This provides an insightful way to look at the traded volume compared to the classic volume histogram view
// ---------------------------------
//
// - Supply/Demand calc now includes 2-bar gaps (improved algo)
// - Add option for MA type for calculation
// - Add option to show Net Volume as 3-color bars 
// - Visual simplification and improvements to be less distracting & more actionable
// - options to display/hide main visuals while maintaining the status line consistency (Avg Supply, Avg Demand, Avg Net)     
// - add alerts for NetVol moving into Buy (crosses 0 up) or Sell (crosses 0 down) modes    
// - implement a "sentiment" timeframe - options 1W, 1D, 1H  
// using request.scurity() calls to bring in the data from the HTF and pass to the Calc_VESPA function

// ==============================================================================================
GetAverage(_data, _len, MAOption) =>
    value = switch MAOption
        'SMA' => ta.sma(_data, _len) 
        'EMA' => ta.ema(_data, _len) 
        'RMA' => ta.rma(_data, _len) 
        => 
            ta.wma(_data, _len)

// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
Calc_VESPA(bool VolumeOn, bool GapsOn) =>
    // *********************************************************************************************************************
    // Extrapolate avg estimated Buy & Sell volume per bar
    // How the updated buy/sell average estimated volume algo works:
    // buy volume is assoicated with price up-moves, sell volume is associated with price down-moves
    // so each of the bulls and bears will get the equivalent of the top & bottom wicks, 
    // for up bars, bulls get the value of the "body", else the bears get the "body"
    // open gaps are allocated to bulls or bears depending on the gap direction (if the option is selected)
    // if there's no volume, then this will just reflect the price action split between buyers/sellers  
    // *********************************************************************************************************************
    o = open
    c = close
    h = high
    l = low
    v = na(volume) or not VolumeOn ? 1 : volume

    gap         = o - c[1]
    
    bull_gap    = math.max(gap, 0) 
    bear_gap    = math.abs(math.min(gap, 0))

    body        = math.abs(c - o)
    BarRange    = h - l
    wick        = BarRange - body

    up_bar      = c > o
  
    bull        = wick + (up_bar ? body : 0) + (GapsOn ? bull_gap : 0)
    bear        = wick + (up_bar ? 0 : body) + (GapsOn ? bear_gap : 0)

    ViRange     = bull + bear

    // Rare cases with very low TF's (mainly FOREX) where no price movement occurs, ViRange (including gaps) = 0 
    BScore      = ViRange > 0 ? bull / ViRange : 0.5
    BuyVol      = BScore * v
    SellVol     = v - BuyVol
    
  // Return Estimated Buy & Sell Volume values
    [BuyVol, SellVol]

// *********************************************************************************************************************
// inputs
// *********************************************************************************************************************
length      = input.int(title='Volume Length',    defval=16,  minval=1)
AvgType     = input.string("WMA", "Average type", options = ['SMA', 'EMA', 'RMA', 'WMA'])

smooth      = input.int(title='Smoothing',        defval=8,   minval=1)

VolumeWeighted  = input.bool(true, "Volume Weighted (Keep on)")
GapImpact       = input.bool(true, "2-bar Gap Impact (Keep on)")

ShowNetVolBars  = input.bool(true, "Show NetVol Bars")
ShowNetVolHisto = input.bool(true, "Show NetVol Histogram")

// *********************************************************************************************************************
// variables
// *********************************************************************************************************************

// Calculate estimated Buy & Sell colume 
[B_BuyVol, B_SellVol] = Calc_VESPA(VolumeWeighted, GapImpact)  

// Calc average Buy & Sell vol and NetVol from estimate
demand      = ta.wma(GetAverage(B_BuyVol, length, AvgType), smooth)
supply      = ta.wma(GetAverage(B_SellVol, length, AvgType), smooth)
NetVol      = demand - supply

// *********************************************************************************************************************
// Plots  -- classic volume bars have been removed
// *********************************************************************************************************************

col_red         = color.new(#ff0000, 00)
col_green       = color.new(#00ff00, 00)
col_hist_red    = color.new(#ef5350, 25)
col_hist_green  = color.new(#089981, 25)

col_gold        = color.new(#ffeb3b, 20)

//plot(v,         title='Volume',         style=plot.style_columns,   color=up_bar ? col_green : col_red, display=display.none)


// ======================================================================================================================
// NetVol Bars Plot
// ======================================================================================================================
nvo = fixnan(supply)                           // fixes NaN values - observed mainly on Renko
nvc = fixnan(demand)
nvh = math.max(nvo, nvc)
nvl = math.min(nvo, nvc)

rising      = ta.change(NetVol) > 0


c_barup     = color.new(#11ff20, 60)
c_bardn     = color.new(#ff1111, 60)
c_bardj     = color.new(#ffffff, 50)

c_barupb    = color.new(#1b5e20, 50)
c_bardnb    = color.new(#981919, 50)
c_bardjb    = color.new(#9598a1, 50)

barcolor    = nvc > nvo and rising ? c_barup : nvc < nvo and not rising ? c_bardn : c_bardj
borcolor    = nvc > nvo and rising ? c_barupb : nvc < nvo and not rising ? c_bardnb : c_bardjb

plotcandle(nvo, nvh, nvl, nvc, 'NetVol Bars', barcolor, barcolor, bordercolor = borcolor, 
  display = ShowNetVolBars ? display.pane : display.none)

hline(0,        title='zero line', linestyle=hline.style_dotted,    color=col_gold, 
  display = ShowNetVolHisto ? display.all : display.none)  //hides with histogram

plot(supply,    title='Supply',         color=col_red,       linewidth=2)
plot(demand,    title='Demand',         color=col_green,     linewidth=2)

// ======================================================================================================================
// Net Volume Histogram Plot
// ======================================================================================================================
c_NetVol = NetVol >= 0 ? col_hist_green : col_hist_red
plot(NetVol,    title='NetVol Histogram',  style=plot.style_columns,      color = c_NetVol, linewidth = 4, 
  display = ShowNetVolHisto ? display.all : display.status_line + display.data_window)


// ======================================================================================================================
// Secondary TF Average Net Volume Plot
// ======================================================================================================================

S_gp = "Secondary TF" 

ShowS_NetVol  = input.bool(false, 'Show Secondary', group = S_gp, inline = "Senti_TF")    // STF plot hidden by default

i_STF         = input.string("Chart", 'TF', options = ['Chart', '1Wk', '1Day' , '1Hr'], group = S_gp,  inline = "Senti_TF")
S_length      = input.int(12, "Length", minval = 1, group = S_gp, inline = "Senti_L")
S_smooth      = input.int(4, "Smooth", minval = 1, group = S_gp, inline = "Senti_L")

STF = switch i_STF
    '1Wk'   => 'W' 
    '1Day'  => 'D' 
    '1Hr'   => '60'
    => 
        timeframe.period

// Error trap here if selected secondary TF is lower than chart      
float chartTF_Mins = timeframe.in_seconds() / 60
float i_STF_Mins  = timeframe.in_seconds(STF) / 60
if chartTF_Mins > i_STF_Mins and ShowS_NetVol
    runtime.error("Secondary timeframe must be equal to, or higher than, the chart's timeframe.")


[S_BuyVol, S_SellVol] = request.security(syminfo.tickerid, STF, Calc_VESPA(VolumeWeighted, GapImpact) )

S_demand      = request.security(syminfo.tickerid, STF, ta.wma(GetAverage(S_BuyVol, S_length, AvgType), S_smooth))
S_supply      = request.security(syminfo.tickerid, STF, ta.wma(GetAverage(S_SellVol, S_length, AvgType), S_smooth))

S_NetVol      = S_demand - S_supply

c_S_NetVol = S_NetVol >= 0 ? color.aqua : color.orange
plot(S_NetVol,    title='Secondary TF NetVol',  color = c_S_NetVol, linewidth = 2, 
  display = ShowS_NetVol ? display.all : display.data_window)

// plot(S_supply, color = color.red)
// plot(S_demand, color = color.green)

// ======================================================================================================================
// Alerts
// ======================================================================================================================

// Primary TF Alerts
Al_NetVol_up    = ta.crossover(NetVol, 0)
Al_NetVol_dn    = ta.crossunder(NetVol, 0)
Al_NetVol_swing = Al_NetVol_up or Al_NetVol_dn

alertcondition(Al_NetVol_up,    "A1.  Avg NetVol Crossing 0 Up",      "Avg NetVol Positive - Bullish Mode Detected!")
alertcondition(Al_NetVol_dn,    "A2.  Avg NetVol Crossing 0 Down",    "Avg NetVol Negative - Bearish Mode Detected!")
alertcondition(Al_NetVol_swing, "A3.  Avg NetVol Crossing 0",         "Avg NetVol Swing - Possible Mode Reversal Detected!")

// Secondary TF Alerts
Al_SNetVol_up    = ta.crossover(S_NetVol, 0)
Al_SNetVol_dn    = ta.crossunder(S_NetVol, 0)
Al_SNetVol_swing = Al_SNetVol_up or Al_SNetVol_dn

alertcondition(Al_SNetVol_up,    "B1.  STF Avg NetVol Crossing 0 Up",      "Secondary TF Avg NetVol Positive - Bullish Mode Detected!")
alertcondition(Al_SNetVol_dn,    "B2.  STF Avg NetVol Crossing 0 Down",    "Secondary TF Avg NetVol Negative - Bearish Mode Detected!")
alertcondition(Al_SNetVol_swing, "B3.  STF Avg NetVol Crossing 0",         "Secondary TF Avg NetVol Swing - Possible Mode Reversal Detected!")