Bull/Bear Candle % Oscillator
// 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
// Spencer G.
// 
// This script will create a plot with a shaded area indirectly representing the
// percentage of candles that were either bullish or bearish from a given sample
// length. The script has 2 modes: 'classic', and 'range'*.
//
//      classic - Candle weights are all the same (1); bullish candles are 
//              candles that had a close above the open, bearish otherwise. The
//              oscillator calculation proceeds in 5 steps:
//              1) Find the number of bullish candles in the sample length
//              2) Find the number of bearish candles in the sample length
//              3) Add together (1) and (2) for a total sum
//              4) Determine % bullish and bearish values by dividing (1) and (2)
//              by the sum in (3) individually (multiply each by 100%).
//              5) Subtract the bearish percent from bullish percent, plot.
//      
//       range† - Candle weights are modified by a specific range of the candle;
//              the process for calculating the values of bullish and bearish 
//              candles differs slightly compared to 'classic'. For bullish 
//              candles, the upper wick (high - close) is subtracted from the 
//              total range of that candle for its weight. This upper wick value
//              is then added to the weights of bearish candles. The same is 
//              true for the lower wicks being subtracted from the total range 
//              of bearish candles and being added to the weights of bullish 
//              candles.
//
//              Bullish Weight: bullish candle low to close + lower wicks of 
//                              bearish candles
//              Bearish Weight: bearish candle high to close + upper wicks of 
//                              bullish candles
//
// * There is an additional option for the user to modify the weights of the 
// candles in either mode by the volume. Volumetric weight is calculated by 
// dividing the current candle's volume by the sum of all volume in the sample 
// length, then multiplying the value derived by each candle (1 for classic, 
// specific candle ranges otherwise) by this weight.
//
// † When 'Volume Weighted' is enabled, this script will employ the 
// request.security_lower_tf function to request the highs, lows, and volume of 
// all candles at the specified resolution**. The volumetric weight of the upper 
// and lower wicks will then be determined by the volume of all the lower time 
// frame candles as follows:
//  
//      upper wicks - volume is added to the upper wick weight when the low of a 
//                  lower timeframe candle is greater than the close of a 
//                  bullish candle at the current resolution. This volume is then
//                  subtracted from the current bullish candle's volume.***
//
//      lower wicks - volume is added to the lower wick weight when the high of 
//                  a lower timeframe candle is less than the close of a bearish
//                  candle at the current resolution. This volume is then 
//                  subtracted from the current bearish candle's volume.***
//
// The limitation of this implementation would be that the volumetric weight of
// wicks may be 0 if all lower timeframe candles have lows or highs that are 
// within the current candle body at the chart's resolution.
//
// ** NOTE: The default resolution is 1 minute candles. FOR HIGHER TIME FRAMES, 
// I RECOMMEND USING RESOLUTIONS THAT THE CURRENT RESOLUTION OF THE CHART IS 
// DIVISIBLE BY. I believe that using resolutions for which the current chart 
// resolution is not divisible by may yield unreliable results.
//
// *** In the original design of this script, low and high values used in these 
// statements are the default source values used, this has since been modified 
// so that the user may decide which OHLC values to use when determining wick 
// weights.
//
// This will create an oscillator that fluctuates between 100 and -100 where 
// above 0 means there were more bullish candles and below 0 means there were 
// more bearish candles in the sample. Data produced by this oscillator is 
// normalized around the 50% value. If there is an even 50/50 split between
// bullish and bearish candles, then the value produced by this oscillator will
// be 0. This oscillator is an indirect representation of the percentage of
// candles in a sample that were bullish or bearish. To find that percentage
// using this oscillator, perform the following:
//
//  TYPE = oscillator value >= 0 ? bullish : bearish
//  50% + ((|oscillator value| / 100) * 50)% of candles in the sample were {TYPE}
//

//@version=5
indicator("Bull/Bear Candle % Oscillator", overlay = false, max_bars_back = 300)

// CONSTANT VALUES
var string UPPER_SWITCH = "upper"
var string LOWER_SWITCH = "lower"

i_SampleLength = input.int(defval = 21, title = "Sample Length", minval = 1, 
  tooltip = "Defines how may candles will be backtraced to calculate a "
  + "percentage for.")
  
i_BullishColor = input.color(defval = color.green, title = "Bull Color", 
  inline = "Colors")
i_NeutralColor = input.color(defval = color.white, title = "Neutral Color", 
  inline = "Colors")
i_BearishColor = input.color(defval = color.red, title = "Bear Color", 
  tooltip = "These define the colors used to create the color gradient that fills "
  + "the plot line to the zero line in both positive and negative directions.", 
  inline = "Colors")
  
i_InvertColors = input.bool(defval = false, title = "Invert Colors")
  
i_CalculationType = input.string(defval = "Classic", title = "Calculation Type",
  options = ["Classic", "Range"], tooltip = "This setting changes the "
  + "behavior of the script regarding how the % bullish/bearish candles is "
  + "performed. There are 2 settings:\n\nClassic - Counters for keeping track of "
  + "the % of bull/bear candles rely on candle color alone.\n\nRange - Bullish "
  + "candles will be weighted based on how large the range is from low to close"
  + " plus the Lower Wicks of bearish candles in the sample. Bearish candles "
  + "will be weighted based on how large the range is from the high to close "
  + "plus the Upper Wicks of bullish candles in the sample.")
  
i_VolumeWeighted = input.bool(defval = false, title = "Volume Weighted", 
  tooltip = "Modifies the % calculation by assigning weight to each candle (or "
  + "relevant partitions*) based upon its volume.\n\n*For the 'Range' setting, "
  + "this will cause the script to analyze the weights of 'counter wicks' on each"
  + " candle. Counter Wicks will be: Upper Wicks for bullish candles, and Lower "
  + "Wicks for bearish candles. The volume weight for these wicks will be "
  + "analyzed at the specified 'Slice' resolution, and calculated as follows:\n\n"
  + "Bullish Candles - for every lower timeframe candle where the low is greater "
  + "than the candle's close at the current resolution, volume weight is shifted "
  + "onto the upper wick.\n\nBearish Candles - for every lower timeframe candle "
  + "where the high is less than the candle's close at the current resolution, "
  + "volume weight is shifted onto the lower wick.")
  
i_SliceResolution = input.timeframe(defval = "1", title = "Wick Slice Resolution", 
  tooltip = "*Range & Volume Weighted Enabled Only*\n\nDefines the lower timeframe "
  + "resolution that will be used to determine the volumetric weight of upper "
  + "and lower wicks for candles at the current resolution. It is important to"
  + "note that PineScrypt will restrict the number of lower resolution candles "
  + "utilized to 100,000 total. This script uses 3 request.security_lower_tf "
  + "calls: 1 minute slices on a 1D chart will cause this script to not perform "
  + "any calculations past 23 days prior.\n\n**It is recommended to make this "
  + "value one that the current resolution is divisible by for more reliable "
  + "results.**")
  
i_UpperWickSource = input.source(defval = low, title = "Limiting Value: Upper Wicks", 
  inline = "WICK SOURCES")
  
i_LowerWickSource = input.source(defval = high, title = "Lower Wicks", 
  inline = "WICK SOURCES", tooltip = "*Range & Volume Weighted Enabled Only*\n\n"
  + "These 'limiting' values will determine which OHLC value of intrabar candles "
  + "will be used against the current chart resolution's close to attribute "
  + "volumetric weight to the upper and lower wicks of that candle.\n\nThe "
  + "default values of 'low' and 'high' ensure that this script will only "
  + "attribute the volume of intrabar candles where 100% of the price action "
  + "had occurred in the current resolution candle's wicks.")
  
i_RestrictMinMaxData = input.bool(defval = false, title = "Restrict Min/Max Data", 
  inline = "Restriction")
  
i_MinMaxRestrictVal = input.int(defval = 100, title = "Restriction:", 
  minval = 20, tooltip = "Reduces the number of candles to back test "
  + "for defining the min/max % of Bearish/Bullish candles (aids in the gradient "
  + "coloration).\n\nIf 'false', this script will back test the whole way to the "
  + "beginning of viewable historic data.", inline = "Restriction")
  
i_DisplayMinMaxGuide = input.bool(defval = false, title = "Display Min/Max Guide",
  tooltip = "Displays the guidelines that marks what the value of the Minimum %"
  + " and Maximum % Bearish/Bullish candles that is currently being used for "
  + "gradient coloration.")
  
i_ShowConvertedNormalVal = input.bool(defval = false, 
  title = "Show Converted Normalized Value", tooltip = "Plots a line that "
  + "represents the 'Bull/Bear ±% Line' value converted from the normalized "
  + "value to the actual percent of bullish and bearish candles. Minimum value "
  + "that this line will reach is 50, maximum value is 100. The color used to"
  + "represent bullish and bearish candles correspond to the input values for "
  + "'Bull/Bear Color' (not affected by 'Invert Colors')\n\nEq: 50% + "
  + "((|SIGNAL| / 100) * 50)%")

// Get Wick Vol(ume) is a helper function which will assess how much volume had
// occurred in the wicks at the current candle of the chart's resolution.
//
// @params:
//      lowerTFArr - (float[]) The array that contains all the prices at either 
//                 the high or low of all candles in the lower resolution.
//          volArr - (float[]) The array that contains all the volumes 
//                 associated with each candle in the lower resolution.
//            type - (string) Specifies which boolean case/calculation to be 
//                 used for determining volume of wicks. (must be 'upper' or 
//                 'lower')
//
// @return:
//      volsum - (float) The volume determined to be in the requested wick at 
//             the current chart resolution's candle.
f_GetWickVol(_lowerTFValArr, _volArr, string _type) =>
    float volsum = 0
    int arrSize = array.size(_lowerTFValArr)
    int count = 0

    // Padding for empty case
    if nz(arrSize) == 0
        na
    else
        // Iterate through the lower resolution candles.
        while count < arrSize
            value = array.get(_lowerTFValArr, count)
            vol = array.get(_volArr, count)
            switch(_type) 
                // Upper Wicks -> value must be above close to be counted
                "upper" => 
                    if value > close
                        volsum := volsum + vol
                // Lower Wicks -> value must be below close to be counted
                "lower" => 
                    if value < close
                        volsum := volsum + vol
            count := count + 1
        // return
        volsum

// Convert Normalized Value is a simple helper function to take the SIGNALPERCENT
// value and convert it from its normalized form to the actual % bullish and 
// bearish candles that this script aims to represent. This will reveal the true
// percentage of bullish and bearish candles, optionally plotted on the chart.
f_ConvertNormalizedValue(_val) =>
    50 + ((math.abs(_val) / 100) * 50)

// Gradient color endpoints
var color BULLCOLOR = i_InvertColors ? i_BearishColor : i_BullishColor
var color BEARCOLOR = i_InvertColors ? i_BullishColor : i_BearishColor

// Modifiable Array to hold historical values of the SIGNALPERCENT below.
// When utilized as a queue, this prevents the use of a for-loop at each candle.
var float[] MODARR = array.new_float(i_MinMaxRestrictVal + 1, na)

// Place holders for the maximum and minimum values of the MODARR
var float HIGHERMOD = 0
var float LOWERMOD = 0

// SignalPercent is the main result of the script: oscillates [-100, 100]
// BULL/BEAR GRADIENT COLOR will fluctuate between i_BullColor - i_NeutralColor -
// i_BearColor based on how close SIGNALPERCENT is to HIGHER/LOWERMOD OR the 
// math.max/min of MODARR (i_RestrictMinMaxData enabled)
float SIGNALPERCENT = na
color BULLGRADIENTCOLOR = na
color BEARGRADIENTCOLOR = na

// Grab lower resolution data: volume, upperwick source, lowerwick source
float[] VOLUMELOWTF = 
  request.security_lower_tf(syminfo.tickerid, i_SliceResolution, volume)
float[] UPPERWICKSOURCELOWTF = 
  request.security_lower_tf(syminfo.tickerid, i_SliceResolution, i_UpperWickSource)
float[] LOWERWICKSOURCELOWTF = 
  request.security_lower_tf(syminfo.tickerid, i_SliceResolution, i_LowerWickSource)

// Ignore introductory data
if bar_index > i_SampleLength

    body = close - open
    cRange = high - low
    vol = volume
    
    bullOrBear = body >= 0
    
    upperWick = bullOrBear ? high - close : high - open
    lowerWick = bullOrBear ? open - low : close - low
    
    // BullRange - (high - low) - (high - close)
    // BearRange - (high - low) - (close - low)
    bullRange = cRange - upperWick
    bearRange = cRange - lowerWick
    
    // If classic, wick volumes are not considered, determine wick volume by 
    // lower resolution candles otherwise
    upperWickVol = i_CalculationType == "Classic" ? 0 : 
      f_GetWickVol(UPPERWICKSOURCELOWTF, VOLUMELOWTF, UPPER_SWITCH)
    lowerWickVol = i_CalculationType == "Classic" ? 0 : 
      f_GetWickVol(LOWERWICKSOURCELOWTF, VOLUMELOWTF, LOWER_SWITCH)
    
    // Value of candle:
    //      classic - 1
    //        range - if bullish candle: bullRange, bearRange otherwise
    value = 
      i_CalculationType == "Classic" ? 1 : bullOrBear ? bullRange : bearRange
    
    // Find total volume during the sample length
    sampleVolSum = math.sum(vol, i_SampleLength)
    
    // For standard volumetric weight, divide current candle's volume by the 
    // sample volume, if not weighted return 1
    standardVolWeight = i_VolumeWeighted ? vol / sampleVolSum : 1
    
    classicValues = value * standardVolWeight
    
    // After determining wick volume (if weighted/range) subtract the associated 
    // wick volume from current candle volume: this is the volume weight for 
    // this candle:
    //      Bullish -> value * adjustedBullVol
    //      Bearish -> value * adjustedBearVol
    adjustedBullVol = i_VolumeWeighted ? (vol - upperWickVol) / sampleVolSum : 1
    adjustedBearVol = i_VolumeWeighted ? (vol - lowerWickVol) / sampleVolSum : 1
    
    // Determine the proportion of the wick volumes
    bullWickVolWeight = i_VolumeWeighted ? lowerWickVol / sampleVolSum : 1
    bearWickVolWeight = i_VolumeWeighted ? upperWickVol / sampleVolSum : 1
    
    // For complex/counter values, calculate based on if the current candle is 
    // bullish or bearish:
    // 
    // Bullish - Counter value is upperWick * bearWickVolWeight
    //
    // Bearish - Counter value is lowerWick * bullWickVolWeight
    complexValue = 
      bullOrBear ? value * adjustedBullVol : value * adjustedBearVol
    counterValue = 
      bullOrBear ? upperWick * bearWickVolWeight : lowerWick * bullWickVolWeight
    
    // Multi-purpose summation ternary declaration:
    // If the current candle is:
    //      Bullish - TernaryValA is added to bullish totals, TernaryValB is 
    //              added to bearish totals.
    //      Bearish - TernaryValB is added to bullish totals, TernaryValA is 
    //              added to bearish totals.
    //
    // If the calculation type is:
    //      classic - TernaryValA will be the classicValues calculated above,
    //              TernaryValB will be 0.
    //        range - TernaryValA will be the complexValue calculated above,
    //              TernaryValB will be the counterValue calculated above.
    ternaryValA = i_CalculationType == "Classic" ? classicValues : complexValue
    ternaryValB = i_CalculationType == "Classic" ? 0 : counterValue
    
    bullSum = math.sum(bullOrBear ? ternaryValA : ternaryValB, i_SampleLength)
    bearSum = math.sum(not bullOrBear ? ternaryValA : ternaryValB, i_SampleLength)
    
    // SIGNALPERCENT Calculation
    totalSum = bullSum + bearSum
    
    bullPercent = (bullSum / totalSum) * 100
    bearPercent = (bearSum / totalSum) * 100
    
    SIGNALPERCENT := bullPercent - bearPercent
    
    // Treat MODARR like a queue
    if i_RestrictMinMaxData
        array.unshift(MODARR, SIGNALPERCENT)
        if array.size(MODARR) > i_MinMaxRestrictVal
            throwAwayVal = array.pop(MODARR)
    
    // Determine the change in HIGHER/LOWERMOD based on SIGNALPERCENT
    HIGHERMOD := i_RestrictMinMaxData ? array.max(MODARR) : 
      SIGNALPERCENT > HIGHERMOD ? SIGNALPERCENT : HIGHERMOD
    LOWERMOD := i_RestrictMinMaxData ? array.min(MODARR) : 
      SIGNALPERCENT < LOWERMOD ? SIGNALPERCENT : LOWERMOD
    
    // Generate the color gradient based on the SIGNALPERCENT's relation to 
    // HIGHER/LOWERMOD
    BULLGRADIENTCOLOR := 
      color.from_gradient(SIGNALPERCENT, 0, HIGHERMOD, i_NeutralColor, BULLCOLOR)
    BEARGRADIENTCOLOR := 
      color.from_gradient(math.abs(SIGNALPERCENT), 0, math.abs(LOWERMOD), 
      i_NeutralColor, BEARCOLOR)

NORMALCONVERT = 
  i_ShowConvertedNormalVal ? f_ConvertNormalizedValue(SIGNALPERCENT) : na

SIGNALCOLOR = SIGNALPERCENT > 0 ? BULLGRADIENTCOLOR : BEARGRADIENTCOLOR

SIGNALPLOT = plot(SIGNALPERCENT, title = "Bull/Bear ±% Line", color = color.white, 
  linewidth = 2)
ZEROPLOT = plot(0, title = "Zero Line", color = color.white)
    
fill(plot1 = ZEROPLOT, plot2 = SIGNALPLOT, color = SIGNALPERCENT > 0 ?
  BULLGRADIENTCOLOR : BEARGRADIENTCOLOR, title = "Bull/Bear Gradient")
  
plot(NORMALCONVERT, title = "Converted Normalized Value Line", 
  color = SIGNALPERCENT > 0 ? i_BullishColor : i_BearishColor)

plot(HIGHERMOD, title = "Higher Mod Value", color = BULLCOLOR, 
  display = i_DisplayMinMaxGuide ? display.all : display.none)
plot(LOWERMOD, title = "Lower Mod Value", color = BEARCOLOR, 
  display = i_DisplayMinMaxGuide ? display.all : display.none)
  
  
alertcondition(SIGNALPERCENT[1] > 0 and SIGNALPERCENT < 0, title = "Bull/Bear "
  + "% Line Bearish Flip", message = "Bull/Bear % Line below 0, more bearish "
  + "candles detected in current sample.")
alertcondition(SIGNALPERCENT[1] < 0 and SIGNALPERCENT > 0, title = "Bull/Bear "
  + "% Line Bullish Flip", message = "Bull/Bear % Line above 0, more bullish "
  + "candles detected in current sample.")