Volume Candlesticks [cajole]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © cajole
// This indicator highlights volume anomalies, with an emphasis on spikes but also dips.

//@version=5
indicator("Volume Candlesticks [cajole]", 'Volume Candlesticks [cj]', overlay = true)
// v1 published (internal marker: 299 -> 302)
// v304 small tweaks
// v306 smooth transition to the anomaly colour.

// CONSTANTS
MODE_CANDLES = 'Candlesticks', MODE_BARS = 'Bar chart', MODE_DOTS = 'None', MODE_DISABLE = 'Off (plots no data at all)'
K_CYAN = color.rgb(26, 212, 255), K_MGTA = color.rgb(255, 128, 255), K_ORNG = color.rgb(230,171,0)

// INPUTS
i_displayMode    = input.string(MODE_CANDLES, 'Display mode (see instructions)', 
 [MODE_CANDLES, MODE_BARS, MODE_DOTS, MODE_DISABLE], inline='v0', group='VCs',
 tooltip='For BOTH Candlesticks and Bars, use a TradingView candlestick chart. Then, go to Settings (4th button from top-right), Symbol (1st option), and uncheck Body, Borders, and Wick.')
n_v  = input.int(50, 'N bars to average volume over', inline='v', group='VCs')
i_2sd_lv =     input.float(1.618, 'Relative volume factor for highlighting', inline='v', group='VCs',
              tooltip = 'This value is taken as a 95% confidence interval on a smooth bell curve for highlighting.')
i_minBarFrac = input.float(0.1, 'Extrapolate bar colour after this fraction of bar time has passed', step=0.05, 
              tooltip='0.1 means about 45 min. of the trading day. Set to zero to disable.')
c_dn = input.color(K_MGTA,  'Low-volume bars',     inline='v2vd') // Kovesi's ternary basis colour. 3rd is (230,171,0).
c_up = input.color(K_CYAN, 'High-volume bars',     inline='v2vu') // Kovesi's ternary basis colour
c_spike = input.color(color.lime,  'Extreme-volume bars', inline='v2ve') 
i_c_upFillDefault  = input.color(color.white, 'Default up fill',            inline='v2upf')
i_c_dnFillDefault  = input.color(color.rgb(0, 0, 0, 100), 'Down fill.',inline='v2dnf')
i_c_BorderDefault  = input.color(color.rgb(255, 255, 255), 'Edges.',            inline='v2edgef')
i_dnFillFixed      = input.bool(true, "Dark chart mode",       inline='v2fill', 
 tooltip = 'When checked (or not), bear (or bull) candle fill is never changed, so that you can distinguish bull and bear bars. Recommendation: keep checked for dark backgrounds.')
i_diagnostics = input.bool(false, 'Add diagnostic scatterplot of log-volume to the chart.',
     tooltip = 'This is intended only for troubleshooting your colour scheme.')
i_spike_prob = input.float(1, 'Special colour for anomalies with <x% probability', inline='v1', step=0.05, 
              tooltip = 'Assuming the N preceding bars form a lognormal distribution, bars at the upper tail of its pdf will be highlighted.')
i_spike_smth = input.float(0.5, 'Smooth anomaly colouring? [%]', -100, 100, 0.2)


// this will be used for a sliding scale of highlights
normal_pdf(_x, _mu, _sigma) => math.exp(-0.5 * math.pow(_x - _mu, 2) / math.pow(_sigma, 2))

// this was used to avoid colouring the last bar if it's incomplete. At halfway to completion that bar would have half volume.
// sessionClosed = barstate.islast ? (timenow > time_close ? 1 : 0) : 1 

// scale up volume for incomplete bar
bar_frac = barstate.islast and (timenow < time_close) 
             ? (timenow - time) / (time_close - time) : 1

// convert volume into a colour scale. 
sd_lv = math.log10(i_2sd_lv)
v = (bar_frac > i_minBarFrac) ? volume / bar_frac : na
lv  = math.log10(v) // Based on log-volume because that quantity is normally distributed, and its mean more robust
avg_lv = ta.ema(lv, n_v)

// convert inputs into visuals: prob_lv goes to 100 for average bars and 0 for extremes
prob_lv = 100 * normal_pdf(lv, avg_lv, sd_lv) 

// Dots mode -- plot disappearing highlighting icons
c_avg_lv_fade = color.new(lv > avg_lv ? c_up : c_dn, prob_lv)
plotshape(i_displayMode==MODE_DOTS,     
         'Volume surge', shape.circle,    location.bottom, c_avg_lv_fade, size = size.tiny, display=display.pane)

// variables to make the code more readable.
isBarChart = i_displayMode==MODE_BARS
isCdlStick = i_displayMode==MODE_CANDLES
isDisabled = i_displayMode==MODE_DISABLE  or  na(prob_lv)
isBullBar  = close > open
isBEARBar  = not isBullBar
is_v_spike = lv > avg_lv and prob_lv < i_spike_prob
upFillFixed= not i_dnFillFixed

// calculate volume-bar colours (note: the 'bottom' and 'top' colours are inverted because we work with probabilities.)
c_grad_fill_avgv = isBullBar  ? (is_v_spike ? c_up : i_c_upFillDefault)  : i_c_dnFillDefault
c_grad_edge      =               is_v_spike ? c_up : i_c_BorderDefault
c_grad_v_anomaly = is_v_spike ? c_spike : (lv > avg_lv ? c_up : c_dn)
loProb = is_v_spike ?           0  - i_spike_smth : i_spike_prob 
hiProb = is_v_spike ? i_spike_prob + i_spike_smth :          100
edge_color = color.from_gradient(prob_lv, loProb, hiProb, c_grad_v_anomaly, c_grad_edge)
fill_color = color.from_gradient(prob_lv, loProb, hiProb, c_grad_v_anomaly, c_grad_fill_avgv)

// disable colouring when no volume was available, or prob_lv calculation failed for some other reason.
edge_color := isDisabled ? i_c_BorderDefault                                     : edge_color
fill_color := isDisabled ? (isBullBar ? i_c_upFillDefault  : i_c_dnFillDefault ) : fill_color

// disable colouring for either bull or bear (user selected), so that the chart distinguishes bull & bear conditions.
fill_color := (isCdlStick and isBullBar and   upFillFixed) ? i_c_upFillDefault : fill_color
fill_color := (isCdlStick and isBEARBar and i_dnFillFixed) ? i_c_dnFillDefault : fill_color

// plot the new bars / candlesticks
plotbar(   isBarChart ? open : na, high, low, close, 'Volume bars', fill_color, display=display.pane)
plotcandle(isCdlStick ? open : na, high, low, close, 'Volume candlesticks',
           color = fill_color, wickcolor = edge_color, bordercolor = edge_color,display=display.pane) 

plot(i_diagnostics ? lv : na, color = edge_color, style=plot.style_circles, display = display.pane)