Tri-State Supertrend
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © StefanReich

// A supertrend with 3 states: Buy, Sell, Range
// Based on Pivot Point SuperTrend by LonesomeTheblue

//@version=5

indicator("Tri-State Supertrend", overlay=true)

prd = input.int(defval=10, title='Pivot Point Period', minval=1, maxval=50)
Factor = input.float(defval=3, title='ATR Factor', minval=1, step=0.1)
Pd = input.int(defval=10, title='ATR Period', minval=1)
rangeFilter = input.bool(true, "Range filter", tooltip="Filter signals for ranges")
rangeSize = input.float(1.3, "Range size", step=0.1, tooltip="Maximal movement (%) allowed in a range")

showpivot = input(false, 'Show Pivot Points', group="Display")
showlabel = input(defval=true, title='Show Buy/Sell Labels', group="Display")
showsr = input(defval=false, title='Show Support/Resistance', group="Display")
showRanges = input(defval=false, title='Show (non-) trendline for ranges', group="Display")
showSuppressedSignals = input.bool(false, "Show suppressed signals", tooltip="Show buy/sell signals that were suppressed during a range", group="Display")

// get Pivot High/Low
float ph = ta.pivothigh(prd, prd)
float pl = ta.pivotlow(prd, prd)

// draw Pivot Points if "showpivot" is enabled
plotshape(ph and showpivot, title="High", text='H', style=shape.labeldown, color=na, textcolor=color.new(color.red, 0), location=location.abovebar, offset=-prd, display=display.all-display.status_line)
plotshape(pl and showpivot, title="Low", text='L', style=shape.labeldown, color=na, textcolor=color.new(color.lime, 0), location=location.belowbar, offset=-prd, display=display.all-display.status_line)

// calculate the Center line using pivot points
var float center = na
float lastpp = ph ? ph : pl ? pl : na
if lastpp
    if na(center)
        center := lastpp
        center
    else
        //weighted calculation
        center := (center * 2 + lastpp) / 3
        center

// upper/lower bands calculation
atr = ta.atr(Pd)
Up = center - Factor * atr
Dn = center + Factor * atr

plotchar(atr/close*100, "ATR (%)", "", display=display.data_window) 

// get the trend
float TUp = na
float TDown = na
Trend = 0
TUp := close[1] > TUp[1] ? math.max(Up, TUp[1]) : Up
TDown := close[1] < TDown[1] ? math.min(Dn, TDown[1]) : Dn
Trend := close > TDown[1] ? 1 : close < TUp[1] ? -1 : nz(Trend[1], 1)
Trailingsl = Trend == 1 ? TUp : TDown

// check and plot the signals
bsignal = Trend == 1 and Trend[1] == -1
ssignal = Trend == -1 and Trend[1] == 1

//get S/R levels using Pivot Points
float resistance = na
float support = na
support := pl ? pl : support[1]
resistance := ph ? ph : resistance[1]

// if enabled then show S/R levels
plot(showsr and support ? support : na, color=showsr and support ? color.lime : na, style=plot.style_circles, offset=-prd, title="Support", display=display.all-display.status_line)
plot(showsr and resistance ? resistance : na, color=showsr and resistance ? color.red : na, style=plot.style_circles, offset=-prd, title="Resistance", display=display.all-display.status_line)

// max function with direction (direction > 0 for max, direction < 0 for min)
directionalMax(direction, a, b) =>
    d = math.sign(direction)
    d*math.max(a*d, b*d)

var bool inRange = false   // Are we in a range?

// Furthest price move in % since last buy/sell signal (suppressed or non-suppressed)
// This is the central value that determines whether we are in a range.
var float crest = 0        

var float entryPrice = na  // Entry price at last buy/sell signal

// Record last trend change
trendChange = Trend != Trend[1]
lastTrendChange = bar_index-ta.barssince(trendChange)

// Calculate current price move in direction of trend
currentMove = Trend*(directionalMax(Trend, low, high)/entryPrice-1)*100

// Update crest value if currentMove is above it
if currentMove > crest
    crest := currentMove

// Core range detection logic. Uses crest[1] because crest may already be pertaining to the new trend
// The check for currentMove makes sure to ignore the trend beginning at the very first bar in the history
inRange_calc = not na(currentMove) and crest[1] <= rangeSize

plotchar(currentMove, "currentMove", "", display=display.data_window) 
plotchar(crest, "crest", "", display=display.data_window) 

if rangeFilter and barstate.isconfirmed
    // On buy/sell signal, check if last trend was an actual trend or range-like
    if bsignal or ssignal
        inRange := inRange_calc

        // Reset variables to record new trend
        entryPrice := close
        crest := 0

    // End range as soon as a trend is of notable size (this will trigger a buy/sell signal)
    if crest[1] > rangeSize
        inRange := false

rsignal = inRange and not inRange[1] // Range signal occurs when inRange turns true
isBuy = Trend > 0 and not inRange    // Suppress buy signal when in range
isSell = Trend < 0 and not inRange   // Suppress sell signal when in range
buy = isBuy and isBuy[1] == false    // Actual filtered buy signal (only true for one bar when the trend begins)
sell = isSell and isSell[1] == false // Actual filtered sell signal (only true for one bar when the trend begins)

// Plot the trend line - green for buy, red for sell, gray for range
rangeColor = color.gray
linecolor = inRange ? rangeColor : Trend == 1 and nz(Trend[1]) == 1 ? color.lime : Trend == -1 and nz(Trend[1]) == -1 ? color.red : na
plot(not inRange or showRanges ? Trailingsl : na, color=linecolor, linewidth=2, title='Trendline', style=plot.style_linebr, display=display.all-display.status_line)

// Show buy/sell labels
plotshape(buy and showlabel ? Trailingsl : na, title='Buy', text='Buy', location=location.absolute, style=shape.labelup, size=size.tiny, color=inRange ? rangeColor : color.lime, textcolor=color.black, display=display.all-display.status_line)
plotshape(sell and showlabel ? Trailingsl : na, title='Sell', text='Sell', location=location.absolute, style=shape.labeldown, size=size.tiny, color=inRange ? rangeColor : color.red, textcolor=color.white, display=display.all-display.status_line)

// [Non-suppressed mode] Show range labels, either above or below the bar, depending on what the last trend was (looks better this way)
plotshape(rsignal and showlabel and not showSuppressedSignals and Trend[1] > 0 ? Trailingsl : na, title='Range', text='Range', location=location.absolute, style=shape.labeldown, size=size.tiny, color=rangeColor, textcolor=color.white, display=display.all-display.status_line)
plotshape(rsignal and showlabel and not showSuppressedSignals and Trend[1] < 0  ? Trailingsl : na, title='Range', text='Range', location=location.absolute, style=shape.labelup, size=size.tiny, color=rangeColor, textcolor=color.white, display=display.all-display.status_line)

// Show signals suppressed during range if selected
showSupp = showlabel and showSuppressedSignals and inRange and not rsignal
plotshape(bsignal and showSupp ? Trailingsl : na, title='Buy (suppressed)', text='Buy\n(suppressed)', location=location.absolute, style=shape.labelup, size=size.tiny, color=rangeColor, textcolor=color.white, display=display.all-display.status_line)
plotshape(ssignal and showSupp ? Trailingsl : na, title='Sell (suppressed)', text='Sell\n(suppressed)', location=location.absolute, style=shape.labeldown, size=size.tiny, color=rangeColor, textcolor=color.white, display=display.all-display.status_line)

plotshape(rsignal and showlabel and showSuppressedSignals and Trend[1] > 0 ? Trailingsl : na, title='Range (suppressed sell)', text='Range\n(suppressed sell)', location=location.absolute, style=shape.labeldown, size=size.tiny, color=rangeColor, textcolor=color.white, display=display.all-display.status_line)
plotshape(rsignal and showlabel and showSuppressedSignals and Trend[1] < 0  ? Trailingsl : na, title='Range (suppressed Buy)', text='Range\n(suppressed buy)', location=location.absolute, style=shape.labelup, size=size.tiny, color=rangeColor, textcolor=color.white, display=display.all-display.status_line)

// If you embed this indicator into a strategy, you can use this variable to always query
// the Tri-State Supertrend's current state (1 for buy, -1 for sell, 0 for range)
tristateTrend = inRange ? 0 : Trend
plotchar(tristateTrend, "Tri-State Trend", "", display=display.all-display.status_line)

// Alerts
alertcondition(buy, title='Buy Signal', message='Buy Signal')
alertcondition(sell, title='Sell Signal', message='Sell Signal')
alertcondition(rsignal, title='Sell Signal', message='Range Signal')
alertcondition(buy or sell or rsignal, title='Trend Changed', message='Trend Changed')