Volatility patterns / quantifytools
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © quantifytools

//@version=5
indicator("Volatility patterns", overlay=true, max_boxes_count = 500, max_lines_count = 500)


//          Inputs



//Range inputs
groupRangeSettings = "Range pattern settings"

i_minInfluence = input.int(6, "Influence time", group = groupRangeSettings)
i_compressRequirement = input.int(4, "Min. compressed pivots", group = groupRangeSettings)
i_compressPiv = input.int(2, "Pivot length", group = groupRangeSettings)

//Bar inputs
groupBarSettings = "Candle pattern settings"
i_minInfluenceIndBar = input.int(6, "Influence time", group = groupBarSettings)
i_indBarSensitivity = input.int(2, "Indecision candle sensitivity", group = groupBarSettings, minval=1, maxval=3)

//Visual inputs
groupVisual= "Visuals"

indBarPatternCol = input.color(color.yellow, "Candle", group=groupVisual, inline="Pattern")
indBarPatternColoring = input.color(color.rgb(255, 235, 59, 46), "Influence", group=groupVisual, inline="Pattern")

rangePatternCol = input.color(color.orange, "Range", group=groupVisual, inline="Pattern2")
rangePatternColoring = input.color(color.rgb(255, 170, 59, 53), "Influence", group=groupVisual, inline="Pattern2")

i_indBarSize = input.int(5, "Candle width", group = groupVisual)

//Table inputs
groupTable = "Table"

tablePos = input.string("Top right", "Table position", options=["Top right", "Bottom right", "Bottom left", "Top left"], group=groupTable)
tableSize = input.int(2, "Table size", maxval=5, minval=1, group=groupTable)


//          Initializing counters

//Tracking highs/lows
var float prevHigh = 0
var float prevLow = 0

//Reset candle index for volatility coloring
var int resetIndexRange = 0
var int resetIndexIndBar = 0

//Compressed turns counters
var int compressCount = 0
var int compressCount2 = 0

//Volatility metric counters
var float indBarPeriods = 0
var float rangePeriods = 0

var float indBarPeakVol = 0
var float rangePeakVol = 0

var float indBarCumVol = 0
var float rangeCumVol = 0

var float indBarCumInfluence = 0
var float rangeCumInfluence = 0


//          Candle patterns

//Defining relative close
rangePos = (close - low) / (high - low)

//Defining volatility and volatility MA
volatility = high - low
smaVolatility = ta.sma(volatility, 20)

//Volatility multiplier (volatility relative to volatility SMA 20)
volatilityMultiplier = (volatility / smaVolatility)

//Balanced engulfing highs & lows
lowSpread = (low[1] - low)
highSpread = (high - high[1])
balancedHl = (highSpread <= lowSpread * 1.1) and (lowSpread <= highSpread * 1.1)

//Volatility regime
volatilityRegime = ta.sma(volatilityMultiplier, 10)

//indBar candle types
inBar = high < high[1] and low > low[1]
indecisionEngulf = volatilityMultiplier <= 0.70 and rangePos >= 0.40 and rangePos <= 0.60 and balancedHl 
lowVolHighVol = volatilityRegime < 0.70 and volatilityMultiplier >= 1.5

//Grouping indBar candle types
anyIndBar = (inBar or indecisionEngulf or lowVolHighVol) and barstate.isconfirmed

//Defining indBar Pattern
indBarPattern = i_indBarSensitivity == 1 ? anyIndBar : i_indBarSensitivity == 2 ? anyIndBar and (anyIndBar[1] or anyIndBar[2]) : anyIndBar and anyIndBar[1]

//Set reset candle index for indBar
if indBarPattern
    resetIndexIndBar := bar_index + i_minInfluenceIndBar

//Volatility coloring for indBar
indBarColoring = bar_index <= resetIndexIndBar 


//          Compressed range #1 (higher lows, lower highs)

//Defining pivots
pivLegs = i_compressPiv
pivHigh = ta.pivothigh(high, pivLegs, pivLegs)
pivLow = ta.pivotlow(low, pivLegs, pivLegs)

//Populating trackers
if pivLow
    prevLow := low[pivLegs]

if pivHigh
    prevHigh := high[pivLegs]

//Valid compression scenarios
lhPivot = prevHigh < prevHigh[1] and pivHigh
hlPivot = prevLow > prevLow[1] and pivLow

//Populating compress counter
if lhPivot or hlPivot
    compressCount += 1

//Reseting compression counter on failed reaction
if high > prevHigh or low < prevLow
    compressCount := 0

//Defining compressed range
rangePattern = compressCount == i_compressRequirement and barstate.isconfirmed

//Set bar index for volatility color reset
if rangePattern
    resetIndexRange := bar_index + i_minInfluence

//Reset counters when bar index is met
if bar_index == resetIndexRange 
    resetIndexRange := 0
    compressCount := 0



//          Compressed range #2 (low volatility pivots)

//Calculating bench mark for measuring how close a pivot is to an average pivot. Exclude pre and post market due to inherent low volatility.
pivChange = (pivHigh or pivLow) and session.ispremarket == false and session.ispostmarket == false ? ( (prevHigh / prevLow) - 1) * 100 : na
pivChangeSma = ta.sma(pivChange, 20)
pivChangeMultiplier = pivChange / pivChangeSma

//Populating counters
if (pivHigh or pivLow) and pivChangeMultiplier <= 0.50
    compressCount2 += 1

if (pivHigh or pivLow) and pivChangeMultiplier >= 0.50
    compressCount2 := 0

//Defining compressed range scenario #2
rangePattern2 = compressCount2 == i_compressRequirement and barstate.isconfirmed

//Set reset candle index for compressed range #2
if rangePattern2
    resetIndexRange := bar_index + i_minInfluence

//Volatility influence time
rangeColoring = bar_index <= resetIndexRange 


//          Metrics

//Bars since influence period false
barssinceInbInfluenceFalse = ta.barssince(indBarColoring == false) 

//Bars since influence period false
barssinceRangeInfluenceFalse = ta.barssince(rangeColoring == false)

//Populating cumulative counters
if rangeColoring and na(volatilityMultiplier) == false
    rangeCumVol += volatilityMultiplier
    rangeCumInfluence += 1

if indBarColoring and na(volatilityMultiplier) == false
    indBarCumVol += volatilityMultiplier
    indBarCumInfluence += 1

//Peak relative volatility
peakRangeVol = barssinceRangeInfluenceFalse > 0 ? ta.highest(volatilityMultiplier, barssinceRangeInfluenceFalse) : na
peakIndBarVol = barssinceInbInfluenceFalse > 0 ? ta.highest(volatilityMultiplier, barssinceInbInfluenceFalse) : na

//When indecision bar volatility influence time is over, populate peak volatility and indecision bar counters
if indBarColoring[1] and indBarColoring == false and na(volatilityMultiplier) == false
    indBarPeakVol += peakIndBarVol[1]
    indBarPeriods += 1

//When range volatility influence time is over, populate peak volatility and range pattern counters
if rangeColoring[1] and rangeColoring == false  and na(volatilityMultiplier) == false
    rangePeakVol += peakRangeVol[1]
    rangePeriods += 1

//Avg peak volatility
avgIndBarPeak = math.round(indBarPeakVol / indBarPeriods, 2)
avgRangePeak = math.round(rangePeakVol / rangePeriods, 2)

avgIndBarVol = math.round(indBarCumVol / indBarCumInfluence, 2)
avgRangeVol = math.round(rangeCumVol / rangeCumInfluence, 2)


//          Table

//Table position and size
tablePosition = tablePos == "Top right" ? position.top_right : tablePos == "Bottom right" ? position.bottom_right : tablePos == "Bottom left" ? position.bottom_left : position.top_left
tableTextSize = tableSize == 1 ? size.tiny : tableSize == 2 ? size.small : tableSize == 3 ? size.normal : tableSize == 4 ? size.huge : size.large

//Initializing table
var metricTable = table.new(position = tablePosition, columns = 50, rows = 50, bgcolor = color.new(color.black, 80), border_width = 3, border_color=color.rgb(23, 23, 23))

//Divider symbol
div = "｜"

//Populating table
table.cell(table_id = metricTable, column = 0, row = 0, text = "Candle patterns: " + str.tostring(indBarPeriods), text_color=color.yellow, text_halign = text.align_center, text_size=tableTextSize)
table.cell(table_id = metricTable, column = 0, row = 1, text = "Range patterns: " + str.tostring(rangePeriods), text_color=color.orange, text_halign = text.align_center, text_size=tableTextSize)

table.cell(table_id = metricTable, column = 1, row = 0, text = " Avg. volatility: " + str.tostring(avgIndBarVol) + "x" + div + "Avg. peak: " + str.tostring(avgIndBarPeak) + "x", text_color=color.white, text_halign = text.align_left, text_size=tableTextSize)
table.cell(table_id = metricTable, column = 1, row = 1, text = " Avg. volatility: " + str.tostring(avgRangeVol) + "x" + div + "Avg. peak: " + str.tostring(avgRangePeak) + "x", text_color=color.white, text_halign = text.align_left, text_size=tableTextSize)


//          Alerts

//Candle pattern
alertcondition(indBarPattern, "Indecision candle pattern", "Indecision candle pattern detected.")

//Range pattern
alertcondition(rangePattern or rangePattern2, "Range pattern", "Range pattern detected.")

//Grouped
alertcondition(indBarPattern or rangePattern or rangePattern2, "Volatility pattern (any)", "Volatility pattern detected.")


//          Plots

//Piv legs lookback
pivlb = pivLegs * 2

//Highest high/lowest low for compressed range highlight
highestRange = ta.highest(high, pivlb * i_compressRequirement)
lowestRange = ta.lowest(low, pivlb * i_compressRequirement)

//If range Pattern condition is met, draw box
if rangePattern2 and rangePattern2[1] == false
    box.new(left=bar_index - (pivlb * i_compressRequirement), top=highestRange, right=bar_index, bottom=lowestRange, bgcolor=color.new(color.yellow, 100), border_color=rangePatternCol, border_width=1, border_style = line.style_dotted, text="", text_color=color.white, text_valign = text.align_bottom)
    
//If range Pattern condition is met, draw box
if rangePattern and rangePattern[1] == false
    box.new(left=bar_index - (pivlb * i_compressRequirement), top=highestRange, right=bar_index, bottom=lowestRange, bgcolor=color.new(color.yellow, 100), border_color=rangePatternCol, border_width=1, border_style = line.style_dotted, text="", text_color=color.white, text_valign = text.align_bottom)
    
//If indBar Pattern condition is met, draw line
if indBarPattern
    line.new(bar_index[0], high[0], bar_index[0], low[0], xloc=xloc.bar_index, color=indBarPatternCol, width=i_indBarSize)

//Volatility color
compressColBorder = rangeColoring ? rangePatternColoring : indBarColoring ? indBarPatternColoring : color.new(color.gray, 100)

//Coloring border only
plotcandle(open, high, low, close, title="Volatility color", color=color.new(color.gray, 100), wickcolor=color.new(color.gray, 100), bordercolor=compressColBorder)

//Plotting calculations
plot( volatilityMultiplier , "Relative volatility (volatility/volatility SMA 20)", display=display.data_window, color=color.white)

plot(0, "==========[ Peak counters ]", display=display.data_window)
plot( rangePeriods , "Compressed range influence periods", display=display.data_window, color=color.orange)
plot( rangePeakVol , "Cumulative peak volatility", display=display.data_window, color=color.orange)
plot( avgRangePeak , "Average peak volatility", display=display.data_window, color=color.orange)
plot( rangeColoring[1] and rangeColoring == false ? rangePeakVol - rangePeakVol[1] : na, "Recent peak volatility", display=display.data_window, color=color.rgb(237, 163, 51))

plot( indBarPeriods , "Indecision bar influence periods", display=display.data_window, color=color.yellow)
plot( indBarPeakVol , "Cumulative peak volatility", display=display.data_window, color=color.yellow)
plot( avgIndBarPeak , "Average peak volatility", display=display.data_window, color=color.yellow)
plot( indBarColoring[1] and indBarColoring == false ? indBarPeakVol - indBarPeakVol[1] : na, "Recent peak volatility", display=display.data_window, color=color.rgb(246, 238, 167))

plot(0, "==========[ Average counters ]", display=display.data_window)
plot( rangeCumInfluence , "Cumulative influence bars", display=display.data_window, color=color.orange)
plot( rangeCumVol , "Cumulative influence bar volatility", display=display.data_window, color=color.orange)
plot( avgRangeVol , "Average range pattern volatility", display=display.data_window, color=color.orange)

plot( indBarCumInfluence , "Cumulative influence bars", display=display.data_window, color=color.yellow)
plot( indBarCumVol , "Cumulative influence bar volatility", display=display.data_window, color=color.yellow)
plot( avgIndBarVol , "Average indecision bar pattern volatility", display=display.data_window, color=color.yellow)
