Determine Consecutive Candles
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © SolCollector
// BS Computer Science
// This simple script allows the user to show when (and for how many) consecutive
// black or white candles had been experienced by an asset. Can be applied to 
// all time frames. This script now has the ability to recognize runs of:
// higher highs, lower highs, higher lows, and lower lows, and will properly 
// display alerts for each of these cases.
//
// To keep this script simple, it will not feature the usual ASCII-art that has
// been present in my other scripts. This was a simple experiment inspired by 
// something I had found on Twitter regarding Bitcoin experiencing its first 
// 7th weekly black candle throughout the entirety that it has been trading 
// (apply this script to INDEX:BTCUSD using following parameters: offset - true,
// Number of Candles - 7, Black or White - "Black").

//@version=5
indicator("Determine Consecutive Candles", overlay = true)

// INPUT LIST:
//  enableAlert - (onRun/Break) Allows this script to recognize when runs have
//              occured/when they break and displays a message to the user when
//              conditions set by the inputs below are met.
//    indOffset - Determines which index to begin counting at for runs (either
//              0 or 1).
//   showCPlots - Checkbox to show a visual representation of the values that  
//              are used for logic checks in this script (see tooltip).
//      onClose - Determines if this script will be applied to the currently
//              closing candle (false) or most recently closed candle (true).
//   minCandles - The minimum number of candles to needed for consecutive-ness.
// blackOrWhite - Shows runs of black, white, or both candle types on input 
//              given by the user (may also be turned off).
//    HLBool(s) - Boolean switches for showing runs of higher highs, lower highs,
//              higher lows, and lower lows (uses minCandles).
//

i_enableAlertOnRun = input.bool(defval = true, title = "Enable Alerts on Runs",
  tooltip = "Allows alerts to be fired when runs that exceed the specified "
  + "minimum number of candles.*\n\n*This must be turned on *prior* to enabling"
  + " the alert through TradingView.", group = "ALERT SETTINGS")
  
i_enableAlertOnBreak = input.bool(defval = false, title = "Enable Alerts on "
  + "Broken runs", tooltip = "Fires an alert when a prior run that has exceeded "
  + "The specified number of candles has ended.*\n\n*This must be turned on "
  + "*prior* to enabling the alert through TradingView.", group = "ALERT SETTINGS")
  
i_indOffset = input.int(defval = 1, title = "Default Index Starting Value", 
  minval = 0, maxval = 1, tooltip = "Sets the first value that will be used for "
  + "counting runs.\n\nDefault Value: 1st candle in a run will be '1'.",
  group = "BEHAVIOR SETTINGS")
  
i_showCounterPlots = input.bool(defval = false, title = "Show Counter Plots", 
  tooltip = "[Diagnostic tools]:\n\nShows a plot of the active counter variables "
  + "used to determine the number of candles that have occurred in a run of "
  + "Black or White candles. To properly view this, I recommend moving the "
  + "indicator to a separate pane above or below before enabling.", 
  group = "BEHAVIOR SETTINGS")

i_onClose = input.bool(defval = false, title = "Identify On Close",
  tooltip = "Identifies on most recent close if true; current candle otherwise.", 
  group = "BEHAVIOR SETTINGS")
  
i_minCandles = input.int(defval = 7, title = "Min Number of Candles", minval = 2, 
  maxval = 100, tooltip = "Minimum number of candles needed to backtest for " 
  + "consecutive candles.", group = "BEHAVIOR SETTINGS")
  
i_blackOrWhite = input.string(defval = "Black", title = "Candle Type Setting", 
  options = ["Black", "White", "Both", "OFF"], 
  tooltip = "Selects which candle type(s) to look for with this script.", 
  group = "BEHAVIOR SETTINGS")

// Bool Switches for HL Detection
i_HHBool = input.bool(defval = false, title = "HH", group = "BEHAVIOR SETTINGS", 
  inline = "HL SWITCHES")
i_LHBool = input.bool(defval = false, title = "LH", group = "BEHAVIOR SETTINGS", 
  inline = "HL SWITCHES")
i_HLBool = input.bool(defval = false, title = "HL", group = "BEHAVIOR SETTINGS", 
  inline = "HL SWITCHES")
i_LLBool = input.bool(defval = false, title = "LL", 
  tooltip = "HL Detection Switches:\n\nHH - Higher High\nLH - Lower High\nHL - "
  + "Higher Low\nLL - Lower Low", group = "BEHAVIOR SETTINGS", 
  inline = "HL SWITCHES")
  
// GetOrdinal is a string function that will return the proper ordinal indicator 
// associated with the parameter given to it.
// 
// Ex: [value returned is outside parenthesis in each case]
//
//      _val == 10 -> (10)th    _val == 21 -> (21)st
//      _val == 11 -> (11)th    _val == 31 -> (31)st
//      _val == 12 -> (12)th    _val == 32 -> (32)nd
//      _val == 13 -> (13)th    _val == 33 -> (33)rd
//      _val == 20 -> (20)th    
//
// @param:
//      _val - integer value to grab oridnal indictor for.
//
// @return: 
//      string - one of the following: "st", "nd", "rd", or "th"
//
// This function accounts for the special cases of 11th, 12th, and 13th
//
f_getOrdinal(int _val) =>
    pos = _val % 10
    
    // Eleventh, Twelfth, Thirteenth restriction (also % 100 may be overkill
    // but I believe it's necessary)
    ETT = (_val % 100) - 10
    
    bool ETTRestrict = ETT == 1 or ETT == 2 or ETT == 3
    
    if ETTRestrict
        "th"
    else
        pos == 1 ? "st" : pos == 2 ? "nd" : pos == 3 ? "rd" : "th"
  
// Global variable offset using i_onClose logic
OFFSET = i_onClose ? 1 : 0

// Boolean condition on if the current (or prior) candle is black or white
bool blackOrWhite = close[OFFSET] - open[OFFSET] < 0

bool currHighLower = high[OFFSET] < high[OFFSET + 1]
bool currLowHigher = low[OFFSET] > low[OFFSET + 1]

// Counting vars
var BWcount = 0
var higherHigh = i_indOffset 
var lowerHigh = i_indOffset 
var higherLow = i_indOffset 
var lowerLow = i_indOffset 

BWFlip = (blackOrWhite and not blackOrWhite[1]) or 
  (blackOrWhite[1] and not blackOrWhite)

// Black and White consecutive candle counting logic
if BWFlip and i_indOffset != 1
    BWcount := 0

else if blackOrWhite
    // reset to -1 if previous was a white candle run
    if BWcount >= 0 and (blackOrWhite[1] or i_indOffset == 1)
        BWcount := -1
    else
        BWcount := BWcount - 1
else
    // reset to 1 if previous was a black candle run
    if BWcount <= 0 and (not blackOrWhite[1] or i_indOffset == 1)
        BWcount := 1
    else
        BWcount := BWcount + 1

// Count higher highs/lower highs
if currHighLower
    lowerHigh := lowerHigh + 1
    higherHigh := i_indOffset
else
    lowerHigh := i_indOffset
    higherHigh := higherHigh + 1

// Count higher lows/lower lows
if currLowHigher
    higherLow := higherLow + 1
    lowerLow := i_indOffset
else
    higherLow := i_indOffset
    lowerLow := lowerLow + 1

// Get total number of consecutive candles for each (needs to be unsigned).
absCount = math.abs(BWcount)

// Get runs
isBlackRun = BWcount < 0
isWhiteRun = not isBlackRun

HHRun = higherHigh >= i_minCandles
LHRun = lowerHigh >= i_minCandles
HLRun = higherLow >= i_minCandles
LLRun = lowerLow >= i_minCandles

// Show if count exceeds i_minCandle # and if it is the specified type of run
showBW = absCount >= i_minCandles and (i_blackOrWhite == "Black" ? isBlackRun : 
  i_blackOrWhite == "White" ? isWhiteRun : i_blackOrWhite == "Both")
  
showHH = HHRun and i_HHBool
showLH = LHRun and i_LHBool
showHL = HLRun and i_HLBool
showLL = LLRun and i_LLBool

// String formatting, will properly display ordinal indicator for the number of
// candles on labels + candle type.
BWstrBeg = f_getOrdinal(absCount)

highVal = higherHigh > lowerHigh ? higherHigh : - lowerHigh
lowVal = higherLow > lowerLow ? higherLow : - lowerLow

HVAbs = math.abs(highVal)
LVAbs = math.abs(lowVal)

highStrBeg = f_getOrdinal(HVAbs)
lowStrBeg = f_getOrdinal(LVAbs)

string builtBWString = str.tostring(absCount) + BWstrBeg + " consecutive " + 
  (blackOrWhite ? "black" : "white") + " candle."
  
string builtHighString = str.tostring(HVAbs) + highStrBeg + 
  (highVal > 0 ? " higher high." : " lower high.")
string builtLowString = str.tostring(LVAbs) + lowStrBeg + 
  (lowVal > 0 ? " higher low." : " lower low.")

// Create a new label for each specific run.
label generateBWLabel = 
  showBW ? (label.new(x = bar_index[OFFSET], y = isBlackRun ? low[OFFSET] : 
  high[OFFSET], text = builtBWString, style = isBlackRun ? label.style_label_up : 
  label.style_label_down, textalign = text.align_center, color = color.white)) : 
  na
  
label generateHHLabel = 
  showHH ? (label.new(x = bar_index[OFFSET], y = high[OFFSET], 
  text = builtHighString, style = label.style_label_down, 
  textalign = text.align_center, color = color.white)) : na
  
label generateLHLabel = 
  showLH ? (label.new(x = bar_index[OFFSET], y = high[OFFSET], 
  text = builtHighString, style = label.style_label_down, 
  textalign = text.align_center, color = color.white)) : na
  
label generateHLLabel = 
  showHL ? (label.new(x = bar_index[OFFSET], y = low[OFFSET], 
  text = builtLowString, style = label.style_label_up, 
  textalign = text.align_center, color = color.white)) : na
  
label generateLLLabel = 
  showLL ? (label.new(x = bar_index[OFFSET], y = low[OFFSET], 
  text = builtLowString, style = label.style_label_up, 
  textalign = text.align_center, color = color.white)) : na

// Delete the previous labels so there are no overlaps
if showBW and not na(generateBWLabel[1])
    label.delete(generateBWLabel[1])
    
if showHH and not na(generateHHLabel[1])
    label.delete(generateHHLabel[1])
    
if showLH and not na(generateLHLabel[1])
    label.delete(generateLHLabel[1])
    
if showHL and not na(generateHLLabel[1])
    label.delete(generateHLLabel[1])
    
if showLL and not na(generateLLLabel[1])
    label.delete(generateLLLabel[1])

string MSTRSTR = "A run of "

// Generate alert strings for each case (runs/breaks)
string builtBWRunAlertString = 
  MSTRSTR + str.tostring(absCount) + " consecutive " + 
  (blackOrWhite ? "black" : "white") + " candles has been found."
  
string builtBWBreakAlertString = 
  MSTRSTR + str.tostring(absCount[1]) + " consecutive " +
  (blackOrWhite[1] ? "black" : "white") + " candles has just been broken."
  
string builtHighRunAlertString = MSTRSTR + str.tostring(HVAbs) + 
  (highVal > 0 ? " high" : " low") + "er highs has been found."
string builtHighBreakAlertString = MSTRSTR + str.tostring(HVAbs[1]) + 
  (highVal[1] > 0 ? " high" : " low") + "er highs has ended."
string builtLowRunAlertString = MSTRSTR + str.tostring(LVAbs) + 
  (lowVal > 0 ? " high" : " low") + "er lows has been found."
string builtLowBreakAlertString = MSTRSTR + str.tostring(LVAbs[1]) + 
  (lowVal[1] > 0 ? " high" : " low") + "er lows has ended."
  
bool showHigh = showHH or showLH
bool showLow = showHL or showLL
  
alertFreq = i_onClose ? alert.freq_once_per_bar : alert.freq_once_per_bar_close

// Black and White candle alerts
if showBW and i_enableAlertOnRun
    alert(builtBWRunAlertString, alertFreq)

if not showBW and showBW[1] and i_enableAlertOnBreak
    alert(builtBWBreakAlertString, alertFreq)
    
// High/Low switch alerts
// Note: This handles breaks where higher highs are detected immediately and
// a candle with a lower high is detected after the close. Where as lower
// highs are detected after the candle close, and a higher high is detected 
// immediately. The same applies to the lows.
if showHigh and i_enableAlertOnRun
    if HHRun
        alert(builtHighRunAlertString)
    else
        alert(builtHighRunAlertString, alertFreq)

if not showHigh and showHigh[1] and i_enableAlertOnBreak
    if HHRun[1]
        alert(builtHighBreakAlertString, alert.freq_once_per_bar_close)
    else
        alert(builtHighBreakAlertString)

if showLow and i_enableAlertOnRun
    if LLRun
        alert(builtLowRunAlertString)
    else
        alert(builtLowRunAlertString, alertFreq)

if not showLow and showLow[1] and i_enableAlertOnBreak
    if LLRun[1]
        alert(builtLowBreakAlertString, alert.freq_once_per_bar_close)
    else
        alert(builtLowBreakAlertString)

// Delete labels for plot view
labelList = label.all
        
if i_showCounterPlots and not array.size(labelList) == 0
    for i = 0 to array.size(labelList) - 1
        label.delete(array.get(labelList, i))
        
// Optional Plots
plot(i_showCounterPlots and i_blackOrWhite != "OFF" ? BWcount : na,
  title = "BWCount", color = color.red)
plot(i_showCounterPlots and i_HHBool ? higherHigh : na, title = "Higher High", 
  color = color.orange)
plot(i_showCounterPlots and i_LHBool ? lowerHigh : na, title = "Lower High", 
  color = color.yellow)
plot(i_showCounterPlots and i_HLBool ? higherLow : na, title = "Higher Low", 
  color = color.green)
plot(i_showCounterPlots and i_LLBool ? lowerLow : na, title = "Lower Low", 
  color = color.blue)
