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

// @version=5

// @description Measures pivots in an oscillator and flags if they are above a configurable size. Uses absolute size rather than just highest/lowest in a candle range.
// Can return confirmation of pivots by momentum and by price. Can return divergent peaks and dips. Can filter by oscillator value. 

library(title='OscillatorPivots', overlay=false)


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                                            INPUTS                                               |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

float in_oscPivotSize = input.float(minval=1, defval=10, group="General",  title="Oscillator Pivot Size", tooltip="Absolute cumulative change in the oscillator value that will trigger a pivot when it is observed both before and after. There is no maximum value hardcoded, but since most oscillators are fitted 0-100, a sensible maximum might be 50.")
string in_oscMode = input.string(defval="RSI", options=["RSI","Stoch %K","Stoch %D"], tooltip="Choose the example oscillator to be plotted and checked for pivots.")
string in_displayMode = input.string(defval="f_osc_PivotsAndShoulders", title="Function to display", group="Display", options=["f_osc_Pivots","f_osc_PivotsLite","f_osc_PivotsAndShoulders"])

float osc = switch in_oscMode  // Derive the oscillator value according to what the user chose.
    "RSI" => ta.rsi(close,14)
    "Stoch %K" => ta.stoch(close, high, low, 14)
    "Stoch %D" => ta.sma(ta.stoch(close, high, low, 14), 3)

float in_oscDipRecovery = input.float(defval=30, title="Dip Recovery Threshold", group="f_osc_PivotsAndShoulders", minval=0, maxval=100, tooltip="The oscillator must be above this value before a dip can be confirmed by momentum. To bypass this setting, use a very low value such as zero.")
float in_oscPeakRecovery = input.float(defval=70, title="Peak Recovery Threshold", minval=0, maxval=100, tooltip="The oscillator must be below this value before a peak can be confirmed by momentum. To bypass this setting, use a very high value such as 100.")
int in_priceBuffer = input.int(defval=10, minval=1, maxval=100, title="ATR % Buffer for Price Confirmation",  tooltip="The close must exceed the high/low by this percentage of ATR in order to count.")

in_oscExcludePeakAbove = input.float(defval=80, minval=0, maxval=100, step=5, title="Exclude Peaks Above", tooltip="Exclude peak if the highest oscillator value is above this value. For example, if Stoch is very high maybe you don't want to short.")
in_oscExcludePeakBelow = input.float(defval=20, minval=0, maxval=100, step=5, title="Exclude Peaks Below", tooltip="Exclude peak if the highest oscillator value is below this value. For example, if RSI is oversold maybe you don't want to short.")
in_oscExcludeDipAbove = input.float(defval=80, minval=0, maxval=100, step=5, title="Exclude Dips Above", tooltip="Exclude dip if the lowest oscillator value is above this value. For example, if RSI is overbought maybe you don't want to long.")
in_oscExcludeDipBelow = input.float(defval=20, minval=0, maxval=100, step=5, title="Exclude Dips Below", tooltip="Exclude dip if the lowest oscillator value is below this value. For example, if Stoch is very low maybe you don't want to long.")


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                                           PIVOTS                                                |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

// @function f_osc_Pivots() Measures pivots in an oscillator value. Uses the total change in the Y axis, instead of a simple Williams pivot over a defined number of bars.
// In other words, it measures the size of the actual pivot, not just whether it happens to be the highest/lowest value in a range. 
// Measures the absolute, cumulative change both before and after the pivot, to avoid flagging mere kinks in trends.
// The advantage is that absolute pivot size is, in some cases, precisely what we care about. A disadvantage is that it can take an arbitrary, perhaps long, time to confirm.
// You can configure the threshold size of the pivot so that it finds large or small pivots.
// Always returns a pivot high after a pivot low, then another pivot high and so on, in order. It never returns a high followed by a high, which simple indicators based on the ta.pivot() function can do.
// @param float _oscPivotSize This is the user setting for what counts as a big enough change to be a pivot.
// @param float _osc This is the oscillator float value.
// @param float _osc1In Optional: the oscillator float value from one HTF bar back. Use this parameter if you are running the function on a timeframe higher than the chart timeframe. An example of using my HighTimeframeSampling library to provide the values to do this is given below the function. For normal usage, just leave this out.
// @param float _osc2In Optional: the oscillator float value from two HTF bars back. For normal usage, just leave this out.
// @returns Information about the pivot that is likely to be useful in further calculations: 
//      confirmPeak, confirmDip - whether the pivot was confirmed this bar
//      peakBarsBack, dipBarsBack - how many *chart* bars back the actual peak or dip was
//      peakOsc, dipOsc - the value of the oscillator at the peak/dip
// It also returns some internal variables, which are plotted in this library only for an understanding of how the function works.
//      debug_peakStartLevel, debug_dipStartLevel - The level of the currently active peak/dip


export f_osc_Pivots(float _oscPivotSize, float _osc, float _osc1In=na, float _osc2In=na) =>
    var bool _trailUp = true, var bool _trailDown = true  // 
    var bool _confirmPeak = false, var bool _confirmDip = false
    // This form of declaration might look strange but it saves us endless "..or persist" logic. 
    // We can define these variables here because they are not *modified* outside this function, even if they are output.
    var float _peakStartLevel = na, _peakStartLevel := _peakStartLevel[1], var float _dipStartLevel = na, _dipStartLevel := _dipStartLevel[1]  // The level of an active candidate pivot
    var int _peakStartIndex = na, _peakStartIndex := _peakStartIndex[1], var int _dipStartIndex = na, _dipStartIndex := _dipStartIndex[1]  // The bar_index of an active candidate pivot
    var int _peakBarsBack = na, _peakBarsBack := _peakBarsBack[1], var int _dipBarsBack = na, _dipBarsBack := _dipBarsBack[1]  // How many bars have elapsed from the pivot to its confirmation
    var float _peakOsc = na, _peakOsc := _peakOsc[1], var float _dipOsc = na, _dipOsc := _dipOsc[1]  // The level of a confirmed pivot
    // Assign the HTF versions of the oscillator value if necessary.
    // Note: we don't need HTF versions of the peak and dip levels because we're only looking for a change from the previous bar and the previous chart bar works fine for this. Those variables are derived from the oscillator, which itself is fixed for the HTF. So it magically just works.
    float _osc1 = na(_osc1In) ? _osc[1] : _osc1In, float _osc2 = na(_osc2In) ? _osc[2] : _osc2In
    // PEAKS
    bool _justPeaked = (_osc < _osc1) and (_osc1 >= _osc2) ? true : false  // Is there a pivot peak of any size, as yet unconfirmed, in the oscillator?
    if _justPeaked and _trailDown
        _peakStartLevel := na(_peakStartLevel) ? _osc1 : (_osc1 > _peakStartLevel) ? _osc1 : _peakStartLevel  // If starting level is na or higher than peak starting level, store current level as starting level for peak.
        _trailDown := _trailUp and _trailDown ? true : not _trailUp ? true : false // We can only start trailing down (=looking to confirm a peak) if we are not now trailing up, because we must alternate.
        _peakStartIndex := na(_peakStartIndex) ? bar_index : (_peakStartLevel > _peakStartLevel[1]) ? bar_index : _peakStartIndex  // Store bar_index when we get a peak. Reset if we get a higher peak, otherwise persist.
    if _trailDown and _osc < (_peakStartLevel - _oscPivotSize)
        _confirmPeak := true  // If we dropped far enough, confirm the peak.
        _peakStartLevel := na  // Unset starting level for peak.
        _trailDown := false, _trailUp := true // Reverse direction.
    else
        _confirmPeak := false  // So this variable is only true for the bar that confirmed.
    if _confirmPeak
        _peakBarsBack := bar_index + 1 - _peakStartIndex  // How far back was the peak? Add 1 because we stored the bar_index one bar after the actual peak.
        _peakStartIndex := na  // Unset on same-direction confirmation, after the barsBack has been calculated.
        _peakOsc := na(_peakStartLevel[1]) ? _osc1 : math.max (_osc1, _peakStartLevel[1])  // What was the oscillator value at the peak? Extra logic for the case where we confirm a peak in one bar.
        _peakStartLevel := na  // We don't need it after a peak has been confirmed
    else
        _peakBarsBack := na  // Unset just to be tidy
    
    // DIPS
    bool _justDipped = (_osc > _osc1) and (_osc1 <= _osc2) ? true : false
    if _justDipped and _trailUp
        _dipStartLevel := na(_dipStartLevel) ? _osc1 : (_osc1 < _dipStartLevel) ? _osc1 : _dipStartLevel
        _trailUp := _trailUp and _trailDown ? true : not _trailDown ? true : false
        _dipStartIndex := na(_dipStartIndex) ? bar_index : (_dipStartLevel < _dipStartLevel[1]) ? bar_index : _dipStartIndex 
    if _trailUp and _osc > (_dipStartLevel + _oscPivotSize)
        _confirmDip := true
        _dipStartLevel := na
        _trailUp := false, _trailDown := true
    else
        _confirmDip := false
    if _confirmDip
        _dipBarsBack := bar_index + 1 - _dipStartIndex
        _dipStartIndex := na
        _dipOsc := na(_dipStartLevel[1]) ? _osc1 : math.min(_osc1, _dipStartLevel[1])
        _dipStartLevel := na
    else
        _dipBarsBack := na

    // Output the variables
    [_confirmPeak, _confirmDip, _peakBarsBack, _dipBarsBack, _peakOsc, _dipOsc, _peakStartLevel, _dipStartLevel]

// Call the function (to use HTF example, comment this out)
[confirmPeak, confirmDip, peakBarsBack, dipBarsBack, peakOsc, dipOsc, debug_peakStartLevel, debug_dipStartLevel] = f_osc_Pivots(in_oscPivotSize, osc)


// 🧪🧪🧪 =========== HTF EXAMPLE STARTS ===========

// // Example of using values from higher timeframes. Note that because the library fixes the HTF values they are exactly one HTF bar late by design. Consult the explanation in the HighTimeframeSampling library for more information.
// in_HTF = input.timeframe(defval="", title="EXAMPLE High Timeframe")
// float HTF_osc = request.security(syminfo.tickerid, in_HTF, ta.rsi(close,14), barmerge.gaps_off, barmerge.lookahead_off)  // Raw HTF oscillator value. Fixed to RSI for this example.
// import SimpleCryptoLife/HighTimeframeSampling/2 as SCL_HTF  // Get some.
// float HTF_osc1 = SCL_HTF.f_HTF_Float(in_HTF, HTF_osc, 10, 1, true), float HTF_osc2 = SCL_HTF.f_HTF_Float(in_HTF, HTF_osc, 10, 2, true)  // Can we fix it? Yes we can.
// plot(HTF_osc1, style=plot.style_circles, title="HTF_osc1", color=color.new(color.orange,30), linewidth=10), plot(HTF_osc2, style=plot.style_circles, title="HTF_osc2", color=color.new(color.blue,30), linewidth=5)  // If you're bored you can follow the logic through the magic of circles.
// [confirmPeak, confirmDip, peakBarsBack, dipBarsBack, peakOsc, dipOsc, debug_peakStartLevel, debug_dipStartLevel] = f_osc_Pivots(in_oscPivotSize, HTF_osc, HTF_osc1, HTF_osc2)  // Get the stuff from the thing.
// osc := HTF_osc  // So you don't have to comment out the plots later. But do comment out the other call to the function just below here.

// 🧪🧪🧪 =========== HTF EXAMPLE ENDS ===========



// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                                      MINIMALIST PIVOTS                                          |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

// @function f_osc_PivotsLite() This is exactly the same as f_osc_Pivots() except that it returns only the boolean values to tell you if there is a confirmed dip or peak.
// Note that because the optional parameters are the final ones, you do not need to supply the parameter names in the function call as long as you supply them in the correct order.
// @param float _oscPivotSize: the user setting for what counts as a big enough change to be a pivot.
// @param float _osc: the oscillator float value.
// @param float _osc1In Optional: the oscillator float value from one HTF bar back. Use this parameter if you are running the function on a timeframe higher than the chart timeframe. For normal usage, just leave this out.
// @param float _osc2In Optional: the oscillator float value from two HTF bars back. For normal usage, just leave this out.
// @returns confirmPeak, confirmDip - whether the pivot was confirmed this bar

export f_osc_PivotsLite(float _oscPivotSize, float _osc, float _osc1In=na, float _osc2In=na) =>
    var bool _trailUp = true, var bool _trailDown = true, var bool _confirmPeak = false, var bool _confirmDip = false
    var float _peakStartLevel = na, _peakStartLevel := _peakStartLevel[1], var float _dipStartLevel = na, _dipStartLevel := _dipStartLevel[1]
    var int _peakStartIndex = na, _peakStartIndex := _peakStartIndex[1], var int _dipStartIndex = na, _dipStartIndex := _dipStartIndex[1]
    var int _peakBarsBack = na, _peakBarsBack := _peakBarsBack[1], var int _dipBarsBack = na, _dipBarsBack := _dipBarsBack[1]
    var float _peakOsc = na, _peakOsc := _peakOsc[1], var float _dipOsc = na, _dipOsc := _dipOsc[1]
    float _osc1 = na(_osc1In) ? _osc[1] : _osc1In, float _osc2 = na(_osc2In) ? _osc[2] : _osc2In
    bool _justPeaked = (_osc < _osc1) and (_osc1 >= _osc2) ? true : false
    if _justPeaked and _trailDown
        _peakStartLevel := na(_peakStartLevel) ? _osc1 : (_osc1 > _peakStartLevel) ? _osc1 : _peakStartLevel, _trailDown := _trailUp and _trailDown ? true : not _trailUp ? true : false, _peakStartIndex := na(_peakStartIndex) ? bar_index : (_peakStartLevel > _peakStartLevel[1]) ? bar_index : _peakStartIndex
    if _trailDown and _osc < (_peakStartLevel - _oscPivotSize)
        _confirmPeak := true, _peakStartLevel := na, _trailDown := false, _trailUp := true
    else
        _confirmPeak := false 
    if _confirmPeak
        _peakBarsBack := bar_index + 1 - _peakStartIndex, _peakStartIndex := na,  _peakOsc := na(_peakStartLevel[1]) ? _osc1 : math.max (_osc1, _peakStartLevel[1]), _peakStartLevel := na
    else
        _peakBarsBack := na
    bool _justDipped = (_osc > _osc1) and (_osc1 <= _osc2) ? true : false
    if _justDipped and _trailUp
        _dipStartLevel := na(_dipStartLevel) ? _osc1 : (_osc1 < _dipStartLevel) ? _osc1 : _dipStartLevel, _trailUp := _trailUp and _trailDown ? true : not _trailDown ? true : false, _dipStartIndex := na(_dipStartIndex) ? bar_index : (_dipStartLevel < _dipStartLevel[1]) ? bar_index : _dipStartIndex 
    if _trailUp and _osc > (_dipStartLevel + _oscPivotSize)
        _confirmDip := true, _dipStartLevel := na, _trailUp := false, _trailDown := true
    else
        _confirmDip := false
    if _confirmDip
        _dipBarsBack := bar_index + 1 - _dipStartIndex, _dipStartIndex := na, _dipOsc := na(_dipStartLevel[1]) ? _osc1 : math.min(_osc1, _dipStartLevel[1]), _dipStartLevel := na
    else
        _dipBarsBack := na
    [_confirmPeak, _confirmDip]

[confirmPeakLite, confirmDipLite] = f_osc_PivotsLite(in_oscPivotSize, osc)


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                                       PIVOTS AND SHOULDERS                                      |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

// @function f_osc_PivotsAndShoulders() Measures pivots in an oscillator value. Uses the total change in the Y axis, instead of a simple Williams pivot over a defined number of bars.
// In other words, it measures the size of the actual pivot, not just whether it happens to be the highest/lowest value in a range. 
// Measures the absolute, cumulative change both before and after the pivot, to avoid flagging mere kinks in trends.
// The advantage is that absolute pivot size is, in some cases, precisely what we care about. A disadvantage is that it can take an arbitrary, perhaps long, time to confirm.
// You can configure the threshold size of the pivot so that it finds large or small pivots.
// The function distinguishes between four types of pivot:
//      + Pivots. Simple peaks or dips in momentum.
//      + Pivot shoulders. These are smaller pivots directly after a major pivot. The lead-in is too small to count as a major pivot but the exit is the same size.
//      + Pivots confirmed by price. A momentum pivot that is also confirmed by the close in price.
//      + Pivot shoulders confirmed by price. A momentum pivot shoulder that is also confirmed by the close in price.
//      Each of the pivot types is either a dip or a peak, giving eight confirmation variables in total.
// Simple pivots confirmed by momentum only are always returned in sequence, i.e., high-low-high-low. The function with default values never returns a high followed by a high, which simple indicators based on the ta.pivot() function can do.
//      However, shoulders have no sequencing at all. And price confirmations have no sequencing, because you never know if a pivot in momentum is going to be confirmed by price.
//      If you exclude pivots based on the oscillator level, they are removed from the sequence, i.e. you can have a dip followed by a dip, or peak by a peak.
// The function automatically delays confirming pivots if another divergent pivot appears in the meantime. Shoulders can suppress unconfirmed simple pivots.
// @param float _oscPivotSize: The user setting for what counts as a big enough change to be a pivot.
// @param float _high: The High of the current bar. Used for price confirmation.
// @param float _low: The Low of the current bar. Used for price confirmation.
// @param float _open: The Open of the current bar. Used for price confirmation.
// @param float _close: The Close of the current bar. Used for price confirmation.
// @param float _osc: The oscillator float value.
// @param float _osc1In  Optional: the oscillator float value from one HTF bar back. Use this parameter if you are running the function on a timeframe higher than the chart timeframe. An example of using my HighTimeframeSampling library to provide the values to do this is given below the function. For normal usage, just leave this out.
// @param float _osc2In  Optional: the oscillator float value from two HTF bars back. For normal usage, just leave this out.
// @param float _oscDipRecovery  Optional: After a dip, the oscillator has to recover above this threshold (if it ever went below it) in order for the dip to qualify.
// @param float _oscPeakRecovery  Optional: After a peak, the oscillator has to recover below this threshold (if it ever went above it) in order for the peak to qualify.
// @param float _priceBuffer  Optional: A percentage of ATR which is added (or subtracted) to the price that must be exceeded in order to confirm a pivot with price. If either _priceBuffer or _atr are not supplied, this defaults to zero.
// @param float _atr  Optional: The current ATR of the asset.
// @param float _oscExcludePeakAbove  Optional: Exclude peak if the highest oscillator value is above this value. For example, if Stoch is very high maybe you don't want to short.
// @param float _oscExcludePeakBelow  Optional: Exclude peak if the highest oscillator value is below this value. For example, if RSI is oversold maybe you don't want to short.
// @param float _oscExcludeDipAbove  Optional: Exclude dip if the lowest oscillator value is above this value. For example, if RSI is overbought maybe you don't want to long.
// @param float _oscExcludeDipBelow  Optional: Exclude dip if the lowest oscillator value is below this value. For example, if Stoch is very low maybe you don't want to long.
// @returns two arrays: float[] _arrayFloats and bool[] _arrayBools, which can be expanded to the following variables (in order):
// 0 float peakBarsBack_M  How many bars back the peak was confirmed by momentum. This variable stays accurate and is reset at the next peak confirmation. 
// 1 float peakBarsBack_P  How many bars back the peak was confirmed by price. This variable has a non-na value only for the confirming bar.
// 2 float peakShoulderBarsBack_M  How many bars back the peak shoulder was confirmed by momentum. This variable stays accurate and is reset at the next peak shoulder confirmation. 
// 3 float peakShoulderBarsBack_P  How many bars back the peak shoulder was confirmed by price. This variable has a non-na value only for the confirming bar.
// 4 float oscPeak  The oscillator value at the peak/peak shoulder. This variable has a non-na value only on the bar when a peak/peak shoulder is confirmed by momentum or price. If you want to track the oscillator values for these four kinds of peaks separately, combine this value with the confirmation bools. 
// 5 float dipBarsBack_M  How many bars back the dip was confirmed by momentum. This variable stays accurate and is reset at the next dip confirmation. 
// 6 float dipBarsBack_P  How many bars back the dip was confirmed by price. This variable has a non-na value only for the confirming bar.
// 7 float dipShoulderBarsBack_M  How many bars back the dip shoulder was confirmed by momentum. This variable stays accurate and is reset at the next dip shoulder confirmation. 
// 8 float dipShoulderBarsBack_P  How many bars back the dip shoulder was confirmed by price. This variable has a non-na value only for the confirming bar.
// 9 float oscDip  The oscillator value at the peak/peak shoulder. This variable has a non-na value only on the bar when a peak/peak shoulder is confirmed by momentum or price. If you want to track the oscillator values for these four kinds of peaks separately, combine this value with the confirmation bools. 
// 10 float _closestPivotLevel  The closest pivot start level, or the value of the oscillator when we confirm a pivot and until we start tracking again. Provided in case anyone else wants to use it for post-processing.
// 0 bool confirmPeak_M  True for the bar when a peak is confirmed by momentum.
// 1 bool confirmPeak_P  True for the bar when a peak is confirmed by price.
// 2 bool confirmPeakShoulder_M  True for the bar when a peak shoulder is confirmed by momentum.
// 3 bool confirmPeakShoulder_P  True for the bar when a peak shoulder is confirmed by price.
// 4 bool confirmDip_M  True for the bar when a dip is confirmed by momentum.
// 5 bool confirmDip_P  True for the bar when a dip is confirmed by price.
// 6 bool confirmDipShoulder_M  True for the bar when a dip shoulder is confirmed by momentum.
// 7 bool confirmDipShoulder_P  True for the bar when a dip shoulder is confirmed by price.


export f_osc_PivotsAndShoulders(float _oscPivotSize, float _high, float _low, float _open, float _close, float _osc, float _osc1In=na, float _osc2In=na, float _oscDipRecovery=na, float _oscPeakRecovery=na, float _priceBuffer=0, float _atr=na, float _oscExcludePeakAbove=na, float _oscExcludePeakBelow=na, float _oscExcludeDipAbove=na, float _oscExcludeDipBelow=na) =>

    var bool _trailUp = true, var bool _trailDown = true  // Looking for peaks or dips
    var bool _confirmPeak_M = false, var bool _confirmDip_M = false //  Pivots confirmed by momentum only
    var float _peakStartLevel = na, _peakStartLevel := _peakStartLevel[1], var float _dipStartLevel = na, _dipStartLevel := _dipStartLevel[1] 
    var float _peakStartIndex = na, _peakStartIndex := _peakStartIndex[1], var float _dipStartIndex = na, _dipStartIndex := _dipStartIndex[1]  // These and other variables that should really be ints are floats here so we can add them to a float array to be returned. We only process them using whole numbers so it's ok.
    var float _peakBarsBack_M = na, _peakBarsBack_M := _peakBarsBack_M[1], var float _dipBarsBack_M = na, _dipBarsBack_M := _dipBarsBack_M[1]
    float _osc1 = na(_osc1In) ? _osc[1] : _osc1In, float _osc2 = na(_osc2In) ? _osc[2] : _osc2In
    var float _peakClose = na, _peakClose := _peakClose[1], var float _dipClose = na, _dipClose := _dipClose[1]  // The price close at the time of a pivot
    bool _peakDoReset = false, bool _dipDoReset = false  // Should we reset the pivot this very bar because price shows regular divergence?
    var bool _trailDown_S_0 = na, _trailDown_S_0 := _trailDown_S_0[1], var bool _trailUp_S_0 = na, _trailUp_S_0 := _trailUp_S_0[1]  // Prerequisite trailing state for shoulders
    var bool _trailDown_S = na, _trailDown_S := _trailDown_S[1], var bool _trailUp_S = na, _trailUp_S := _trailUp_S[1]  // Trail state for shoulders only, which have different sequencing.
    var float _peakStartLevel_S = na, _peakStartLevel_S := _peakStartLevel_S[1], var float _dipStartLevel_S = na, _dipStartLevel_S := _dipStartLevel_S[1]  // Starting oscillator value for shoulders only.
    bool _peakDoReset_S = false, bool _dipDoReset_S = false  // Reset the shoulder levels on divergence? True for only that bar, hence not var.
    var float _peakStartIndex_S = na, _peakStartIndex_S := _peakStartIndex_S[1], var float _dipStartIndex_S = na, _dipStartIndex_S := _dipStartIndex_S[1]  // The bar_index of the shoulder pivot.
    var float _peakClose_S = na, _peakClose_S := _peakClose_S[1], var float _dipClose_S = na, _dipClose_S := _dipClose_S[1]  // The price close at the time of a shoulder pivot.
    var bool _confirmPeakShoulder_M = na, _confirmPeakShoulder_M := _confirmPeakShoulder_M[1], var bool _confirmDipShoulder_M = na, _confirmDipShoulder_M := _confirmDipShoulder_M[1]   // Shoulder pivots confirmed by momentum only
    var float _peakShoulderBarsBack_M = na, _peakShoulderBarsBack_M := _peakShoulderBarsBack_M[1], var float _dipShoulderBarsBack_M = na, _dipShoulderBarsBack_M := _dipShoulderBarsBack_M[1]  // How many bars back was the shoulder?
    var float _peakClosePersist = na, _peakClosePersist := _peakClosePersist[1], var float _dipClosePersist = na, _dipClosePersist := _dipClosePersist[1]  // A copy of the peak close to use for measuring if peak shoulders are divergent.

    var float _peak_M_LowCandidate = na, _peak_M_LowCandidate := _peak_M_LowCandidate[1], var float _dip_M_HighCandidate = na, _dip_M_HighCandidate := _dip_M_HighCandidate[1]  // The Low of the peak/High of the dip candle, for confirmations, before confirmation with momentum.
    var float _peak_M_Low = na, _peak_M_Low := _peak_M_Low[1], var float _dip_M_High = na, _dip_M_High := _dip_M_High[1]  // The Low of the peak/High of the dip candle, for confirmations, after confirmation with momentum.
    var float _peakShoulder_M_Low = na, _peakShoulder_M_Low := _peakShoulder_M_Low[1], var float _dipShoulder_M_High = na, _dipShoulder_M_High := _dipShoulder_M_High[1]  // The Low/High of the peak/dip SHOULDER candle, for confirmations, after confirmation with momentum.
    var bool _confirmPeak_P = na, _confirmPeak_P := _confirmPeak_P[1], var bool _confirmDip_P = na, _confirmDip_P := _confirmDip_P[1]  // Confirm a pivot with price (and momentum).
    var bool _confirmPeakShoulder_P = na, _confirmPeakShoulder_P := _confirmPeakShoulder_P[1], var bool _confirmDipShoulder_P = na, _confirmDipShoulder_P := _confirmDipShoulder_P[1]  // Confirm a pivot SHOULDER with price (and momentum).
    var float _oscPeak_M = na, _oscPeak_M := _oscPeak_M[1], var float _oscPeak_S = na, _oscPeak_S := _oscPeak_S[1], var float _oscDip_M = na, _oscDip_M := _oscDip_M[1], var float _oscDip_S = na, _oscDip_S := _oscDip_S[1]  // Internal helper values. _oscPeak and _oscDip are derived from these and they are returned as the oscillator value at the peak/shoulder, or dip/shoulder. We return only two variables rather than 4 or 8 so as not to get too cluttered, because they can be differentiated, if needed, by the client script after calling this function, by combining them with the confirmation variables. They're only true for the confirming bar.
    float _priceConfirmBuffer = nz(_atr) * (_priceBuffer/100)  // The buffer which is added (or subtracted) to the price that must be exceeded in order to confirm a pivot with price. If either ATR or the buffer are not supplied, this defaults to zero.
    var bool _isOscPeakTooHigh = false, var bool _isOscPeakTooLow = false, var bool _isOscDipTooHigh = false, var bool _isOscDipTooLow = false  // One-off variables for out-of-bounds peaks.
    var bool _suppressOutOfBoundsPeak = false, var bool _suppressOutOfBoundsDip = false   // States that persist and supress the *output* of pivots and pivot shoulders but let the internal pivot values continue (so we don't interfere with the order of dips and peaks or break anything else).

    // PEAKS
    bool _justPeaked = (_osc < _osc[1]) and (_osc1 >= _osc2) ? true : false  // Using osc[1] instead of osc1 makes it true ONLY for the FIRST chart bar of the HTF bar after the peak
    if _justPeaked
        _isOscPeakTooHigh := na(_oscExcludePeakAbove) ? false : _osc[1] > _oscExcludePeakAbove ? true : false // We can use osc[1] even for HTF because _justPeaked is only true for the first chart bar of the HTF bar.
        _isOscPeakTooLow := na(_oscExcludePeakBelow) ? false : _osc[1] < _oscExcludePeakBelow ? true : false  // Ignore the TooHigh/TooLow conditions if the input is na.
    else
        _isOscPeakTooHigh := false, _isOscPeakTooLow := false  // Unset just to make sure they're only true for the one bar.
    if _justPeaked and _trailDown
        _peakStartLevel := na(_peakStartLevel) ? _osc1 : (_osc1 > _peakStartLevel) ? _osc1 : _peakStartLevel 
        _peakDoReset := math.max(_open,_close) > _peakClose ? true : false // We have a higher close in price so let's reset some stuff in the next loop
        _trailDown := _trailUp and _trailDown ? true : not _trailUp ? true : false // We can only start trailing down (=looking to confirm a peak) if we are not now trailing up, because we must alternate.
        _peakStartIndex := na(_peakStartIndex) ? bar_index : (_peakStartLevel > _peakStartLevel[1]) ? bar_index : _peakStartIndex  // Store bar_index when we get a peak. Reset if we get a higher peak, otherwise persist.
        _peakClose := math.max(_open[1],_close[1])  // Update this now after we checked for it in deciding whether _peakDoReset was the case, to make sure we were checking the previous value.
    if _peakDoReset
        _peakStartLevel := _osc1  // If we have a divergent peak, reset the peak start level
        _peakStartIndex := bar_index  // ..and the bar index.
    bool _oscPeakIsBelowRecovery = na(_oscPeakRecovery) ? true : _osc < _oscPeakRecovery ? true : false  // Serves both peaks and peak shoulders
    if _trailDown and _osc < (_peakStartLevel - _oscPivotSize) and _oscPeakIsBelowRecovery
        _confirmPeak_M := true  // If we dropped far enough, confirm the peak.
        _oscPeak_M := _peakStartLevel  // Capture this before we unset it. Persists.
        _peakStartLevel := na  // Unset starting level for peak.
        _trailDown := false, _trailUp := true // Reverse direction.
    else
        _confirmPeak_M := false  // So this variable is only true for the bar that confirmed.
    if _confirmPeak_M
        _peakBarsBack_M := bar_index + 1 - _peakStartIndex  // How far back was the peak? Add 1 because we stored the bar_index one bar after the actual peak.
        _peakStartIndex := na  // Unset on same-direction confirmation, after the barsBack has been calculated.
        _peakStartLevel := na  // We don't need it after a peak has been confirmed.
    else
        _peakBarsBack_M +=1 // Maintain _peakBarsBack and reset it only on a new peak.

    // PEAK SHOULDERS
    // Shoulders need a separate logic because they do not alternate peak-dip-peak like pivots do.
    _trailDown_S_0 := _confirmPeak_M[1] ? true : _confirmDip_M ? false : _trailDown_S_0  // Prerequisite for _trailDown_S. Start looking for peak shoulders only after peak confirmation. We can do this because shoulders always start after peaks are confirmed. If they start before peak confirmation, the divergent logic moves the peak itself. This is a prerequisite to the other trailing condition.
    _trailDown_S := _confirmDip_M ? false : _osc >= _peakStartLevel ? false : _justPeaked and _trailDown_S_0 ? true : _trailDown_S
    _peakStartLevel_S := not _trailDown_S ? na : _peakStartLevel_S  // Unset when we're no longer looking for peak shoulders
    _peakClosePersist := _confirmPeak_M ? math.max(_open,_close,_peakClose) : _confirmPeakShoulder_M ? math.max(_peakClosePersist,_peakClose_S) : _peakClosePersist  // Initialise on peak confirmation; Unset on dip confirmation (in the dip section). Reset to the higher of itself and peak shoulder close on confirmation of a peak shoulder.
    if _justPeaked and _trailDown_S and not _trailDown
        _peakStartLevel_S := na(_peakStartLevel_S) ? _osc1 : (_osc1 > _peakStartLevel_S) ? _osc1 : _peakStartLevel_S
        _peakDoReset_S := math.max(_open,_close) > math.max(_peakClose, _peakClose_S) ? true : false  // To be higher than both the original peak and the previous shoulder 
        _peakStartIndex_S := na(_peakStartIndex_S) ? bar_index : (_peakStartLevel_S > _peakStartLevel_S[1]) ? bar_index : _peakStartIndex_S  // Store bar_index when we get a peak. Reset if we get a higher peak, otherwise persist.
        _peakClose_S := math.max(_open[1],_close[1])  // Update this now after we checked for it in deciding whether _peakDoReset_S was the case, to make sure we were checking the previous value.
    _peakClose_S := not _trailDown_S ? na : _peakClose_S  // We should probably unset this
    if _peakDoReset_S
        _peakStartIndex_S := bar_index  // If we have a divergent peak, reset the bar index
        _peakStartLevel_S := _osc1 // ..and the peak start level.
    if _trailDown_S and _osc < (_peakStartLevel_S - _oscPivotSize) and not (_peakClose_S < _peakClosePersist) and _oscPeakIsBelowRecovery
        _confirmPeakShoulder_M := true  // If we dropped far enough, confirm the peak shoulder.
    else
        _confirmPeakShoulder_M := false  // So this variable is only true for the bar that confirmed.
    if _confirmPeakShoulder_M and not _confirmPeak_M
        _peakShoulderBarsBack_M := bar_index + 1 - _peakStartIndex_S  // How far back was the peak shoulder? Add 1 because we stored the bar_index one bar after the actual peak.
        _oscPeak_S := _peakStartLevel_S  // Oscillator value at the time of confirming the shoulder. Capture this before we reset it. Persists.
        _peakStartLevel_S := _osc1  // If we confirm a peak shoulder, reset the peak start level
    else
        _peakShoulderBarsBack_M += 1  // Maintain _peakShoulderBarsBack_M and reset it only on a new peak
    if _confirmPeakShoulder_M or _confirmPeak_M
        _peakStartIndex_S := na  // Unset on ANY same-direction confirmation, after the barsBack has been calculated.
    if _confirmPeakShoulder_M and _confirmPeak_M
        _peakShoulderBarsBack_M := na  // We don't need this set if we're not confirming a shoulder on its own
        _confirmPeakShoulder_M := false  // Cancel the peak shoulder if this is a normal peak 
    _peakStartLevel_S := _confirmPeakShoulder_M ? na : _peakStartLevel_S

    
    // DIPS
    bool _justDipped = (_osc > _osc[1]) and (_osc1 <= _osc2) ? true : false
    if _justDipped
        _isOscDipTooHigh := na(_oscExcludeDipAbove) ? false : _osc[1] > _oscExcludeDipAbove ? true : false
        _isOscDipTooLow := na(_oscExcludeDipBelow) ? false : _osc[1] < _oscExcludeDipBelow ? true : false
    else
        _isOscDipTooHigh := false, _isOscDipTooLow := false
    if _justDipped and _trailUp 
        _dipStartLevel := na(_dipStartLevel) ? _osc1 : (_osc1 < _dipStartLevel) ? _osc1 : _dipStartLevel
        _dipDoReset := math.min(_open,_close) < _dipClose ? true : false
        _trailUp := _trailUp and _trailDown ? true : not _trailDown ? true : false
        _dipStartIndex := na(_dipStartIndex) ? bar_index : (_dipStartLevel < _dipStartLevel[1]) ? bar_index : _dipStartIndex
        _dipClose := math.min(_open[1],_close[1])
    if _dipDoReset
        _dipStartLevel := _osc1  // If we have a divergent dip, reset the dip start level
        _dipStartIndex := bar_index  // ..and the bar index
    bool _oscDipIsAboveRecovery = na(_oscDipRecovery) ? true : _osc > _oscDipRecovery ? true : false
    if _trailUp and _osc > (_dipStartLevel + _oscPivotSize) and _oscDipIsAboveRecovery
        _confirmDip_M := true
        _oscDip_M := _dipStartLevel
        _dipStartLevel := na
        _trailUp := false, _trailDown := true
    else
        _confirmDip_M := false
    if _confirmDip_M
        _dipBarsBack_M := bar_index + 1 - _dipStartIndex
        _dipStartIndex := na
        _dipStartLevel := na
    else
        _dipBarsBack_M += 1

    // DIP SHOULDERS
    _trailUp_S_0 := _confirmDip_M[1] ? true : _confirmPeak_M ? false : _trailUp_S_0
    _trailUp_S := _confirmPeak_M ? false : _osc <= _dipStartLevel ? false : _justDipped and _trailUp_S_0 ? true : _trailUp_S
    _dipStartLevel_S := not _trailUp_S ? na : _dipStartLevel_S
    _dipClosePersist := _confirmDip_M ? math.min(_open,_close,_dipClose) : _confirmDipShoulder_M ? math.min(_dipClosePersist,_dipClose_S) : _dipClosePersist
    if _justDipped and _trailUp_S and not _trailUp
        _dipStartLevel_S := na(_dipStartLevel_S) ? _osc1 : (_osc1 < _dipStartLevel_S) ? _osc1 : _dipStartLevel_S
        _dipDoReset_S := math.min(_open,_close) < math.min(_dipClose, _dipClose_S) ? true : false
        _dipStartIndex_S := na(_dipStartIndex_S) ? bar_index : (_dipStartLevel_S < _dipStartLevel_S[1]) ? bar_index : _dipStartIndex_S
        _dipClose_S := math.min(_open[1],_close[1])
    _dipClose_S := not _trailUp_S ? na : _dipClose_S
    if _dipDoReset_S
        _dipStartIndex_S := bar_index
        _dipStartLevel_S := _osc1
    if _trailUp_S and _osc > (_dipStartLevel_S + _oscPivotSize) and not (_dipClose_S > _dipClosePersist) and _oscDipIsAboveRecovery
        _confirmDipShoulder_M := true
    else
        _confirmDipShoulder_M := false
    if _confirmDipShoulder_M and not _confirmDip_M
        _dipShoulderBarsBack_M := bar_index + 1 - _dipStartIndex_S
        _oscDip_S := _dipStartLevel_S
        _dipStartLevel_S := _osc1
    else
        _dipShoulderBarsBack_M += 1
    if _confirmDipShoulder_M or _confirmDip_M
        _dipStartIndex_S := na
    if _confirmDipShoulder_M and _confirmDip_M
        _dipShoulderBarsBack_M := na
        _confirmDipShoulder_M := false
    _dipStartLevel_S := _confirmDipShoulder_M ? na : _dipStartLevel_S


    // PEAK & PEAK SHOULDER CONFIRMATIONS BY PRICE
    _peak_M_LowCandidate := _justPeaked ? math.max(_low[1],_low) : _peak_M_LowCandidate
    _peak_M_LowCandidate := na(_peak_M_LowCandidate)? na: math.max(_peak_M_LowCandidate,_low)  // Trail the Low up only, unless it's unset in which case keep it unset.
    if _confirmPeak_M
        _peak_M_Low := _peak_M_LowCandidate  // Take a copy on peak confirmation
        _peak_M_LowCandidate := na  // Unset the candidate once we've taken a copy
    _peak_M_Low := _confirmDip_M or _confirmDipShoulder_M ? na : _peak_M_Low  // Unset on opposing confirmation with momentum.
    if _confirmPeakShoulder_M
        _peakShoulder_M_Low := _peak_M_LowCandidate  // Take a copy on peak SHOULDER confirmation. Need a separate variable so that we can output separate peak/shoulder price confirmations.
        _peak_M_LowCandidate := na
    _peakShoulder_M_Low := _confirmDip_M or _confirmDipShoulder_M ? na : _peakShoulder_M_Low
    // This very bar, confirm with price if the close or close[1] is lower than the _peak_M_Low  Separate variables for peaks and peak shoulders. (It doesn't make sense to look back at all the candles for the minimum since if price isn't confirming now or in the next few bars then it shouldn't count).
    _confirmPeak_P := ((_close < (_peak_M_Low - _priceConfirmBuffer)) or (_close[1] < (_peak_M_Low - _priceConfirmBuffer))) // True now or not true at all. Since we're checking every chart bar, [1] on the chart TF should be ok.
    _confirmPeakShoulder_P := (_close < (_peakShoulder_M_Low - _priceConfirmBuffer)) or (_close[1] < (_peakShoulder_M_Low - _priceConfirmBuffer))
    _peak_M_Low := _confirmPeak_P or _confirmPeakShoulder_P ? na : _peak_M_Low  // Unset _peak_M_Low on peak or peak shoulder confirmation with price. 
    _peakShoulder_M_Low := _confirmPeakShoulder_P ? na : _peakShoulder_M_Low  // Unset _peakShoulder_M_Low on peak shoulder confirmation with price.
    float _peakBarsBack_P = _confirmPeak_P ? _peakBarsBack_M : na  // Unlike _peakBarsBack_M, which is running, _peakBarsBack_P has a value only once.
    float _peakShoulderBarsBack_P = _confirmPeakShoulder_P ? _peakShoulderBarsBack_M : na  // Unlike _peakShoulderBarsBack_M, which is running, _peakShoulderBarsBack_P has a value only once.


    // DIP & DIP SHOULDER CONFIRMATIONS BY PRICE
    _dip_M_HighCandidate := _justDipped ? math.min(_high[1],_high) : _dip_M_HighCandidate
    _dip_M_HighCandidate := na(_dip_M_HighCandidate)? na: math.min(_dip_M_HighCandidate,_high)
    if _confirmDip_M
        _dip_M_High := _dip_M_HighCandidate
        _dip_M_HighCandidate := na
    _dip_M_High := _confirmPeak_M or _confirmPeakShoulder_M ? na : _dip_M_High
    if _confirmDipShoulder_M
        _dipShoulder_M_High := _dip_M_HighCandidate
        _dip_M_HighCandidate := na
    _dipShoulder_M_High := _confirmPeak_M or _confirmPeakShoulder_M ? na : _dipShoulder_M_High
    _confirmDip_P := ((_close > (_dip_M_High + _priceConfirmBuffer)) or (_close[1] > (_dip_M_High + _priceConfirmBuffer)))
    _confirmDipShoulder_P := (_close > (_dipShoulder_M_High + _priceConfirmBuffer)) or (_close[1] > (_dipShoulder_M_High + _priceConfirmBuffer))
    _dip_M_High := _confirmDip_P or _confirmDipShoulder_P ? na : _dip_M_High
    _dipShoulder_M_High := _confirmDipShoulder_P ? na : _dipShoulder_M_High
    float _dipBarsBack_P = _confirmDip_P ? _dipBarsBack_M : na
    float _dipShoulderBarsBack_P = _confirmDipShoulder_P ? _dipShoulderBarsBack_M : na

    // CLEANUP
    _peakClosePersist := _confirmDip_M ? na : _peakClosePersist, _dipClosePersist := _confirmPeak_M ? na : _dipClosePersist  // Don't remember what this is for but cleaning up is good.
    _confirmDip_P := _confirmDip_P and _confirmDipShoulder_P ? false : _confirmDip_P, _confirmPeak_P := _confirmPeak_P and _confirmPeakShoulder_P ? false : _confirmPeak_P  // Suppress pivot confirmation with price if we have a pivot shoulder confirmation with price this bar because the pivot had its chance and shoulders are cooler.
    float _oscDip = _confirmDip_M or _confirmDip_P ? _oscDip_M : _confirmDipShoulder_M or _confirmDipShoulder_P ? _oscDip_S : na, float _oscPeak = _confirmPeak_M or _confirmPeak_P ? _oscPeak_M : _confirmPeakShoulder_M or _confirmPeakShoulder_P ? _oscPeak_S : na  // This needs to come after all confirmations.
    var float _closestPivotLevel = na, _closestPivotLevel := not na(_peakStartLevel) ? _peakStartLevel : not na(_dipStartLevel) ? _dipStartLevel : _osc  // The closest pivot start level, or _osc when we confirm a pivot.

    // SUPPRESSION
    _suppressOutOfBoundsPeak := _justDipped ? false : _isOscPeakTooHigh or _isOscPeakTooLow ? true : _suppressOutOfBoundsPeak  // Turn on when we get the out-of-bounds peak. Off when we get any dip (because after that we'll get a peak and we need to re-evaluate it). Must be after all peak and dip calculations.
    _suppressOutOfBoundsDip := _justPeaked ? false : _isOscDipTooHigh or _isOscDipTooLow ? true : _suppressOutOfBoundsDip
    // Define copies of the output variables. This is overkill but I want to be sure there's no unintended effects or edge cases.
    var bool _confirmPeak_M_Out = na, var float _peakBarsBack_M_Out = na, var bool _confirmPeak_P_Out = na, var float _peakBarsBack_P_Out = na, var bool _confirmPeakShoulder_M_Out = na, var float _peakShoulderBarsBack_M_Out = na, var bool _confirmPeakShoulder_P_Out = na, var float _peakShoulderBarsBack_P_Out = na, var float _oscPeak_Out = na, var float _peakStartLevelRunning_Out = na
    var bool _confirmDip_M_Out = na, var float _dipBarsBack_M_Out = na, var bool _confirmDip_P_Out = na, var float _dipBarsBack_P_Out = na, var bool _confirmDipShoulder_M_Out = na, var float _dipShoulderBarsBack_M_Out = na, var bool _confirmDipShoulder_P_Out = na, var float _dipShoulderBarsBack_P_Out = na, var float _oscDip_Out = na, var float _dipStartLevelRunning_Out = na
    if _suppressOutOfBoundsPeak
        _confirmPeak_M_Out := false, _peakBarsBack_M_Out := _peakBarsBack_M_Out[1], _confirmPeak_P_Out := false, _peakBarsBack_P_Out := _peakBarsBack_P_Out[1], _confirmPeakShoulder_M_Out := false, _peakShoulderBarsBack_M_Out := _peakShoulderBarsBack_M_Out[1], _confirmPeakShoulder_P_Out := false, _peakShoulderBarsBack_P_Out := _peakShoulderBarsBack_P_Out[1], _oscPeak_Out := _oscPeak_Out[1]
    else
        _confirmPeak_M_Out := _confirmPeak_M, _peakBarsBack_M_Out := _peakBarsBack_M, _confirmPeak_P_Out := _confirmPeak_P, _peakBarsBack_P_Out := _peakBarsBack_P, _confirmPeakShoulder_M_Out := _confirmPeakShoulder_M, _peakShoulderBarsBack_M_Out := _peakShoulderBarsBack_M, _confirmPeakShoulder_P_Out := _confirmPeakShoulder_P, _peakShoulderBarsBack_P_Out := _peakShoulderBarsBack_P, _oscPeak_Out := _oscPeak
    if _suppressOutOfBoundsDip
        _confirmDip_M_Out := false, _dipBarsBack_M_Out := _dipBarsBack_M_Out[1], _confirmDip_P_Out := false, _dipBarsBack_P_Out := _dipBarsBack_P_Out[1], _confirmDipShoulder_M_Out := false, _dipShoulderBarsBack_M_Out := _dipShoulderBarsBack_M_Out[1], _confirmDipShoulder_P_Out := false, _dipShoulderBarsBack_P_Out := _dipShoulderBarsBack_P_Out[1], _oscDip_Out := _oscDip_Out[1]
    else
        _confirmDip_M_Out := _confirmDip_M, _dipBarsBack_M_Out := _dipBarsBack_M, _confirmDip_P_Out := _confirmDip_P, _dipBarsBack_P_Out := _dipBarsBack_P, _confirmDipShoulder_M_Out := _confirmDipShoulder_M, _dipShoulderBarsBack_M_Out := _dipShoulderBarsBack_M, _confirmDipShoulder_P_Out := _confirmDipShoulder_P, _dipShoulderBarsBack_P_Out := _dipShoulderBarsBack_P, _oscDip_Out := _oscDip
    
    // Return everything in two arrays
    float[] _arrayFloats = array.from(_peakBarsBack_M_Out, _peakBarsBack_P_Out, _peakShoulderBarsBack_M_Out, _peakShoulderBarsBack_P_Out, _oscPeak_Out, _dipBarsBack_M_Out, _dipBarsBack_P_Out, _dipShoulderBarsBack_M_Out, _dipShoulderBarsBack_P_Out, _oscDip_Out, _closestPivotLevel)
    bool[] _arrayBools = array.from(_confirmPeak_M_Out, _confirmPeak_P_Out, _confirmPeakShoulder_M_Out, _confirmPeakShoulder_P_Out, _confirmDip_M_Out, _confirmDip_P_Out, _confirmDipShoulder_M_Out, _confirmDipShoulder_P_Out)
    [_arrayFloats, _arrayBools]


// Call the function (to use the HTF example, comment this line out):
[arrayFloats, arrayBools] = f_osc_PivotsAndShoulders(_oscPivotSize=in_oscPivotSize, _high=high, _low=low, _open=open, _close=close, _osc=osc, _oscDipRecovery=in_oscDipRecovery, _oscPeakRecovery=in_oscPeakRecovery, _priceBuffer=in_priceBuffer, _atr=ta.atr(14), _oscExcludePeakAbove=in_oscExcludePeakAbove, _oscExcludePeakBelow=in_oscExcludePeakBelow, _oscExcludeDipAbove=in_oscExcludeDipAbove, _oscExcludeDipBelow=in_oscExcludeDipBelow)


// 🧪🧪🧪 =========== HTF EXAMPLE STARTS ===========
// To use the HTF example, uncomment everything in this section

// // There's no error-checking of the timeframe on this end, but there is in the HTF sampling library.
// in_HTF = input.timeframe(defval="", title="EXAMPLE High Timeframe")

// f_sec_HTF_RawValues() =>
//     // This function is only defined so we can call it using security() and get multiple variables at once
//     _raw_high = high, _raw_low = low, _raw_open = open, _raw_close = close, _raw_osc = ta.rsi(close,14), _raw_atr = ta.atr(14)
//     [_raw_high, _raw_low, _raw_open, _raw_close, _raw_osc, _raw_atr]

// // Get the raw values from the HTF using a single security() call
// [raw_high, raw_low, raw_open, raw_close, raw_osc, raw_atr] = request.security(syminfo.tickerid, in_HTF, f_sec_HTF_RawValues(), barmerge.gaps_off, barmerge.lookahead_off)

// import SimpleCryptoLife/HighTimeframeSampling/2 as SCL_HTF  // Let's fix the "current" HTF data, so it doesn't change and we avoid repainting and other issues.
// float[] HTF_Array = array.from(raw_high, raw_low, raw_open, raw_close, raw_osc, raw_atr)  // Create input array for the HTF matrix
// HTF_Matrix = SCL_HTF.f_HTF_MatrixFloat(_HTF=in_HTF, _sourceArray=HTF_Array, _matrixHistory=3, _useLiveDataOnChartTF=true)  // Fix the array and get historical values. Note that we use the chart TF passthrough.
// // var0 means fixed with no (extra) offset; var1 means fixed and going back [1]; var2 means [2].
// HTF_high0 = matrix.get(HTF_Matrix, 0, 0), HTF_low0 = matrix.get(HTF_Matrix, 0, 1), HTF_open0 = matrix.get(HTF_Matrix, 0, 2), HTF_close0 = matrix.get(HTF_Matrix, 0, 3), HTF_osc0 = matrix.get(HTF_Matrix, 0, 4), HTF_atr0 = matrix.get(HTF_Matrix, 0, 5)
// HTF_osc1 = matrix.get(HTF_Matrix, 1, 4), HTF_osc2 = matrix.get(HTF_Matrix, 2, 4)

// // Call the pivot function with HTF values.
// [arrayFloats, arrayBools] = f_osc_PivotsAndShoulders(_oscPivotSize=in_oscPivotSize, _high=HTF_high0, _low=HTF_low0, _open=HTF_open0, _close=HTF_close0, _osc=HTF_osc0, _osc1In=HTF_osc1, _osc2In=HTF_osc2, _oscDipRecovery=in_oscDipRecovery, _oscPeakRecovery=in_oscPeakRecovery, _priceBuffer=in_priceBuffer, _atr=HTF_atr0, _oscExcludePeakAbove=in_oscExcludePeakAbove, _oscExcludePeakBelow=in_oscExcludePeakBelow, _oscExcludeDipAbove=in_oscExcludeDipAbove, _oscExcludeDipBelow=in_oscExcludeDipBelow)

// 🧪🧪🧪 =========== HTF EXAMPLE ENDS ===========


// Extract all the variables from the arrays. You might only be interested in a subset, in which case you can delete or comment out the relevant lines.
float peakBarsBack_M = array.get(arrayFloats,0)  // How many bars back the peak was confirmed by momentum. This variable stays accurate and is reset at the next peak confirmation. 
float peakBarsBack_P = array.get(arrayFloats,1)  // How many bars back the peak was confirmed by price. This variable has a non-na value only for the confirming bar.
float peakShoulderBarsBack_M = array.get(arrayFloats,2)  // How many bars back the peak shoulder was confirmed by momentum. This variable stays accurate and is reset at the next peak shoulder confirmation. 
float peakShoulderBarsBack_P = array.get(arrayFloats,3)  // How many bars back the peak shoulder was confirmed by price. This variable has a non-na value only for the confirming bar.
float oscPeak = array.get(arrayFloats,4)  // The oscillator value at the peak/peak shoulder. This variable has a non-na value only on the bar when a peak/peak shoulder is confirmed by momentum or price. If you want to track the oscillator values for these four kinds of peaks separately, combine this value with the confirmation bools. 

float dipBarsBack_M = array.get(arrayFloats,5)  // How many bars back the dip was confirmed by momentum. This variable stays accurate and is reset at the next dip confirmation. 
float dipBarsBack_P = array.get(arrayFloats,6)  // How many bars back the dip was confirmed by price. This variable has a non-na value only for the confirming bar.
float dipShoulderBarsBack_M = array.get(arrayFloats,7)  // How many bars back the dip shoulder was confirmed by momentum. This variable stays accurate and is reset at the next dip shoulder confirmation. 
float dipShoulderBarsBack_P = array.get(arrayFloats,8)  // How many bars back the dip shoulder was confirmed by price. This variable has a non-na value only for the confirming bar.
float oscDip = array.get(arrayFloats,9)  // The oscillator value at the peak/peak shoulder. This variable has a non-na value only on the bar when a peak/peak shoulder is confirmed by momentum or price. If you want to track the oscillator values for these four kinds of peaks separately, combine this value with the confirmation bools. 
float closestPivotLevel = array.get(arrayFloats,10)  // The closest level of a relevant confirmed or unconfirmed pivot.

bool confirmPeak_M = array.get(arrayBools, 0)  // True for the bar when a peak is confirmed by momentum.
bool confirmPeak_P = array.get(arrayBools, 1)  // True for the bar when a peak is confirmed by price.
bool confirmPeakShoulder_M = array.get(arrayBools, 2)  // True for the bar when a peak shoulder is confirmed by momentum.
bool confirmPeakShoulder_P = array.get(arrayBools, 3)  // True for the bar when a peak shoulder is confirmed by price.

bool confirmDip_M = array.get(arrayBools, 4)  // True for the bar when a dip is confirmed by momentum.
bool confirmDip_P = array.get(arrayBools, 5)  // True for the bar when a dip is confirmed by price.
bool confirmDipShoulder_M = array.get(arrayBools, 6)  // True for the bar when a dip shoulder is confirmed by momentum.
bool confirmDipShoulder_P = array.get(arrayBools, 7)  // True for the bar when a dip shoulder is confirmed by price.


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//                                               PLOTS                                             |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|

plot(osc, title="Oscillator", linewidth=2, color=color.white)  // Plot the oscillator value.

// Get the right values from the function that's selected for display
bool showLinesFills = in_displayMode == "f_osc_Pivots"
bool showLabels = in_displayMode == "f_osc_Pivots" or in_displayMode == "f_osc_PivotsAndShoulders"
bool confirmPeakPlot = switch in_displayMode
    "f_osc_Pivots" => confirmPeak
    "f_osc_PivotsLite" => confirmPeakLite
    "f_osc_PivotsAndShoulders" => confirmPeak_M
bool confirmDipPlot = switch in_displayMode
    "f_osc_Pivots" => confirmDip
    "f_osc_PivotsLite" => confirmDipLite
    "f_osc_PivotsAndShoulders" => confirmDip_M

// Confirmation of pivots (shown for all functions)
plotchar(confirmPeakPlot ? osc : na, title="Peak Confirmed", char="☑", color=color.red, size=size.small, location=location.absolute)  // Appears on the bar where the peak was actually confirmed
plotchar(confirmDipPlot ? osc : na, title="Dip Confirmed", char="☑", color=color.green, size=size.small, location=location.absolute)

// Confirmation of pivot shoulders (shown for f_osc_PivotsAndShoulders)
plotchar(confirmPeakShoulder_M and in_displayMode=="f_osc_PivotsAndShoulders" ? osc : na, title="Peak Shoulder Confirmed", char="☑", color=color.orange, size=size.small, location=location.absolute)
plotchar(confirmDipShoulder_M and in_displayMode=="f_osc_PivotsAndShoulders" ? osc : na, title="Dip Shoulder Confirmed", char="☑", color=color.teal, size=size.small, location=location.absolute)

// Confirmation of pivot shoulders with price (shown for f_osc_PivotsAndShoulders)
plotshape(confirmPeak_P and in_displayMode=="f_osc_PivotsAndShoulders", title="Peak Confirmed by Price", style=shape.triangledown, color=color.new(color.red,30), text="Peak\nconfirmed\nby price", size=size.large, textcolor=color.red, location=location.top)
plotshape(confirmPeakShoulder_P and in_displayMode=="f_osc_PivotsAndShoulders", title="Peak Shoulder Confirmed by Price", style=shape.triangledown, color=color.new(color.orange,30), text="Peak shoulder\nconfirmed\nby price", size=size.normal, textcolor=color.orange, location=location.top)
plotshape(confirmDip_P and in_displayMode=="f_osc_PivotsAndShoulders", title="Dip Confirmed by Price", style=shape.triangleup, color=color.new(color.green,30), text="Dip\nconfirmed\nby price", size=size.large, textcolor=color.green, location=location.bottom)
plotshape(confirmDipShoulder_P and in_displayMode=="f_osc_PivotsAndShoulders", title="Dip Shoulder Confirmed by Price", style=shape.triangleup, color=color.new(color.teal,30), text="Dip shoulder\nconfirmed\nby price", size=size.normal, textcolor=color.teal, location=location.bottom)

// Lines and fills for visualising the necessary levels to confirm pivots (shown only for f_osc_Pivots)
debug_peakStartLevelPlot = plot(showLinesFills ? debug_peakStartLevel : na, title="Peak Start Level", linewidth=3, color=color.new(color.red,30), style=plot.style_linebr)
debug_dipStartLevelPlot = plot(showLinesFills ? debug_dipStartLevel : na, title="Dip Start Level", linewidth=3, color=color.new(color.green,30), style=plot.style_linebr)
debug_peakConfirmLevelPlot = plot(na(debug_peakStartLevel) ? na : showLinesFills ? debug_peakStartLevel - in_oscPivotSize : na, title="Peak Confirmation Level", linewidth=1, color=color.new(color.red,90), style=plot.style_linebr)
debug_dipConfirmLevelPlot = plot(na(debug_dipStartLevel) ? na : showLinesFills ? debug_dipStartLevel + in_oscPivotSize : na, title="Dip Confirmation Level", linewidth=1, color=color.new(color.green,90), style=plot.style_linebr)
fill(debug_peakStartLevelPlot, debug_peakConfirmLevelPlot, color=color.new(color.red,80))
fill(debug_dipStartLevelPlot, debug_dipConfirmLevelPlot, color=color.new(color.green,80))
plot(showLinesFills ? peakOsc : na, title="Last Peak Oscillator Value", color=color.orange, linewidth=2)
plot(showLinesFills ? dipOsc : na, title="Last Dip Oscillator Value", color=color.teal, linewidth=2)

// Get the right values for the labels
float peakBarsBackPlot = switch in_displayMode
    "f_osc_Pivots" => peakBarsBack
    "f_osc_PivotsAndShoulders" => peakBarsBack_M
float dipBarsBackPlot = switch in_displayMode
    "f_osc_Pivots" => dipBarsBack
    "f_osc_PivotsAndShoulders" => dipBarsBack_M
float peakOscPlot = switch in_displayMode
    "f_osc_Pivots" => peakOsc
    "f_osc_PivotsAndShoulders" => osc[peakBarsBack_M]
float dipOscPlot = switch in_displayMode
    "f_osc_Pivots" => dipOsc
    "f_osc_PivotsAndShoulders" => osc[dipBarsBack_M]

// Show peak/dip labels(for f_osc_Pivots and f_osc_PivotsAndShoulders). Remember that TradingView has a limit to how many labels can be displayed, so if you don't see any labels, scroll right.
import SimpleCryptoLife/Labels/2 as SCL_Label
peakLabelOffset = int(0 - peakBarsBackPlot)
SCL_Label.labelLast(_condition = showLabels and confirmPeakPlot, _text="Peak (" + str.tostring(peakBarsBackPlot) + ")", _offset=peakLabelOffset, _keepLast=false, _y=peakOscPlot+3, _style=label.style_label_down)

peakShoulderLabelOffset = int(0 - peakShoulderBarsBack_M)
SCL_Label.labelLast(_condition = showLabels and in_displayMode=="f_osc_PivotsAndShoulders" and confirmPeakShoulder_M, _text="Peak\nShoulder (" + str.tostring(peakShoulderBarsBack_M) + ")", _color=color.new(color.orange,50), _textColor=color.white, _offset=peakShoulderLabelOffset, _keepLast=false, _y=osc[peakShoulderBarsBack_M]-3, _style=label.style_label_down)

dipLabelOffset = int(0 - dipBarsBackPlot)
SCL_Label.labelLast(_condition = showLabels and confirmDipPlot, _text="Dip (" + str.tostring(dipBarsBackPlot) + ")", _offset=dipLabelOffset, _keepLast=false, _y=dipOscPlot-3, _style=label.style_label_up)

dipShoulderLabelOffset = int(0 - dipShoulderBarsBack_M)
SCL_Label.labelLast(_condition = showLabels and in_displayMode=="f_osc_PivotsAndShoulders" and confirmDipShoulder_M, _text="Dip\nShoulder (" + str.tostring(dipShoulderBarsBack_M) + ")", _color=color.new(color.orange,50), _textColor=color.white, _offset=dipShoulderLabelOffset, _keepLast=false, _y=osc[dipShoulderBarsBack_M]-3, _style=label.style_label_up)


// ======================================================= //
//                                                         //
//   (╯°□°）╯︵ ǝdʎɥ ǝɥʇ ǝʌǝᴉlǝq ʇ,uoD                     //
//                                                         //
// ======================================================= //