LocalLimit
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Electrified (electrifiedtrading)

// @version=5
// @description Calculates the local upper or local lower limit for a given series.  Applying multiple passes produces what appears like support or resistance levels.
library('LocalLimit', true)

export type Limit
    float value
    int trend

export type Level
    Limit upper
    Limit lower

///////////////////////////////////////////////////
// @function Produces the recent local upper limit for a given series.
// @param src The source series to derive from.
// @param padding Gives some extra room when lowering the level.
export upper(
  series float src,
  float padding = 0) =>
    value = src[0]
    valuePrev = src[1]
    var limit = value
    var int trend = 0
    var int phase = 0

    phase := if not na(value) and (value > limit or na(limit))
        trend := +1
        limit := value
        0
    else
        switch phase
            0 => 1
            1 => value > valuePrev ? 2 : 1
            2 =>
                if value < valuePrev
                    trend := -1
                    limit := math.min(limit, valuePrev + nz(padding))
                    0
                else
                    2
    limit
///////////////////////////////////////////////////

///////////////////////////////////////////////////
// @function Produces the recent local lower limit for a given series.
// @param src The source series to derive from.
// @param padding Gives some extra room when raising the level.
export lower(
  series float src,
  float padding = 0) =>
    value = src[0]
    valuePrev = src[1]
    var limit = value
    var int trend = 0
    var int phase = 0

    phase := if not na(value) and (value < limit or na(limit))
        trend := -1
        limit := value
        0
    else
        switch phase
            0 => 1
            1 => value < valuePrev ? 2 : 1
            2 =>
                if value > valuePrev
                    trend := +1
                    limit := math.max(limit, valuePrev - nz(padding))
                    0
                else
                    2
    limit
///////////////////////////////////////////////////

///////////////////////////////////////////////////
// @function Determines the local trend of a series that and persists the trend if the value is unchanged.
// @param src The source series to derive from.
// @return +1 when the trend is up; -1 when the trend is down; 0 if the trend is not yet established.
export trend(series float src) =>
    var s = 0
    if src > src[1]
        s := +1
    if src < src[1]
        s := -1
    s
///////////////////////////////////////////////////

///////////////////////////////////////////////////
// @function Creates a Limit object that contains the value and the trend.
// @param src The source series to derive from.
export valueAndTrend(series float src) =>
    Limit.new(src, trend(src))
///////////////////////////////////////////////////

///////////////////////////////////////////////////
// @function Creates a Level object representing the local upper and lower limits.
// @param hiSrc The source to discover the upper limit from.
// @param loSrc The source to discover the lower limit from.
// @param padding Gives some extra room when raising the lower level and lowering the upper level.
export of(
  series float hiSrc = high,
  series float loSrc = low,
  float padding = 0) =>
    hi = valueAndTrend(upper(hiSrc, padding))
    lo = valueAndTrend(lower(loSrc, padding))
    Level.new(hi, lo)
///////////////////////////////////////////////////

///////////////////////////////////////////////////
// @function Creates a Level object representing the local upper and lower limits.
// @param level The source levels to expand of.
// @param padding Gives some extra room when raising the lower level and lowering the upper level.
export of(
  series Level level,
  float padding = 0) =>
    hi = valueAndTrend(upper(level.upper.value, padding))
    lo = valueAndTrend(lower(level.lower.value, padding))
    Level.new(hi, lo)
///////////////////////////////////////////////////

limit0 = of()
limit1 = of(limit0, 0.1)
limit2 = of(limit1, 0.5)

hColor = color.yellow
lColor = color.blue

plot(limit0.upper.value, "H0", hColor, 1)
plot(limit1.upper.value, "H1", hColor, 2)
plot(limit2.upper.value, "H2", hColor, 3)

plot(limit0.lower.value, "L0", lColor, 1)
plot(limit1.lower.value, "L1", lColor, 2)
plot(limit2.lower.value, "L3", lColor, 3)
