SOS/SOW Scores
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © benheller21

//@version=5
indicator("SOS/SOW Scores", overlay=true, precision=2, max_labels_count=500)


////////// SOS/SOW ////////////
var display = "SOS/SOW Display Settings"
var vol = "Volume Settings"
var candles = "Candle Settings"
var swing = "Swing Level Settings"
var alerts = "Alert Management"

//Display Settings
displayWeakSOS = input.bool(title="Display Weak SOS/SOW?", defval=true, group = display)
displayModerateSOS = input.bool(title="Display Moderate SOS/SOW?", defval=true, group = display)
displayStrongSOS = input.bool(title="Display Strong SOS/SOW?", defval=true, group = display)
minWeakSOS = input.float(title="Minimum value for weak SOS/SOW", defval=1.5, step = 0.1, minval = 0, group = display)
minModerateSOS = input.float(title="Minimum value for moderate SOS/SOW", defval=4, step = 0.1, group = display)
minStrongSOS = input.float(title="Minimum value for strong SOS/SOW", defval=6.5, step= 0.1, group = display)
displayLabels = input.bool(title="Display Score Labels?", defval=true, group = display)

//Volume
volMALen = input.int(defval=200, title="Volume Moving Average Length", step=1, group=vol, minval=1,
     tooltip= "Moving average length used to compare volume to")
volWeight = input.float(title="Volume Weight", defval=2, step = 0.1, group = vol, 
     tooltip = "Volume MA multiple is multiplied by this number and added to SOS/SOW score")

//Candles
hammerFibLevel = input.float(title="Hammer Body % Level", defval=0.66, minval=0, maxval=1, step=0.01, group=candles,
     tooltip= "Entire candle body must be below this value from bottom to top wick")
hammerWeight = input.float(title="Hammer Weight", defval=0.5, step=0.1, group = candles,
     tooltip = "Score that a candle gets for qualifying as a hammer")
wickSizeWeight = input.float(title="Wick Size Weight", defval=2, step = 0.1, group = candles,
     tooltip = "Size of the wick (as a % of ATR) is multiplied by this number")
colorBoostWeight = input.float(title="Color Boost Weight", defval=0.5, step = 0.1, group=candles,
     tooltip = "Additional score that a hammer candle gets if it closes green (bullish hammer) or red (bearish hammer)")
engulfingWeight = input.float(title="Engulfing Weight", defval=1.5, step = 0.1, group=candles,
     tooltip = "Score that a candle gets for qualifying as an engulfing candle")

//Swing Levels
swingLookback = input.int(title="Swing High/Low Period", defval=8, minval=1, step=1, group=swing,
     tooltip= "Current candle must be highest/lowest in this number of most recent candles")
prevCandleSwing = input.bool(title="Swing High/Low on either of last 2 candles?", defval=true, group=swing)

//Alert Options
alertWeakSOS = input.bool(title = "Send alerts for Weak SOS", defval = false, group = alerts)
alertModSOS = input.bool(title = "Send alerts for Moderate SOS", defval = false, group = alerts)
alertStrongSOS = input.bool(title = "Send alerts for Strong SOS", defval = true, group = alerts)
alertWeakSOW = input.bool(title = "Send alerts for Weak SOW", defval = false, group = alerts)
alertModSOW = input.bool(title = "Send alerts for Moderate SOW", defval = false, group = alerts)
alertStrongSOW = input.bool(title = "Send alerts for Strong SOW", defval = true, group = alerts)

////////////// SOS/SOW Calculations ///////////////

//Swing Levels
swingHigh = high == ta.highest(high, swingLookback) or (prevCandleSwing ? high[1] == ta.highest(high, swingLookback) : na)
swingLow  = low == ta.lowest(low, swingLookback) or (prevCandleSwing ? low[1] == ta.lowest(low, swingLookback) : na)

//Valid volume SOS/SOW?
volAvg = (volMALen > 1) ? ta.sma(volume, volMALen) : volume[1]

//Volume Score
volScore = (volume / volAvg) * volWeight

//Hammer candle fib calculations
bullFib = (low - high) * hammerFibLevel + high
bearFib = (high - low) * hammerFibLevel + low

//Determine which price source closes or opens highest/lowest for hammers
lowestBody  = close < open ? close : open
highestBody = close > open ? close : open

//Hammer ATR filter
atr = ta.atr(14)

//Valid hammer candle?
bullHammer = lowestBody >= bullFib
bearHammer = highestBody <= bearFib

//Wick Size Score
bearHammerWickScore = ((high - highestBody) / atr) * wickSizeWeight
bullHammerWickScore = ((math.abs(low - lowestBody)) / atr) * wickSizeWeight

//Color Filter
bullHammerColorBoost = (close > open) ? colorBoostWeight: 0
bearHammerColorBoost = (close < open) ? colorBoostWeight: 0

//Hammer Score
bullHammerScore = (bullHammer ? hammerWeight: 0) + bullHammerWickScore + bullHammerColorBoost
bearHammerScore = (bearHammer ? hammerWeight: 0) + bearHammerWickScore + bearHammerColorBoost

//Gapping calculations for engulfing candles
gap = math.abs(open - close[1])
var gapList = array.new_float()
array.push(gapList, gap)
stDev = array.stdev(gapList)
allowance = stDev * 2

//Valid engulfing candle?
bull_ec = close[1] < open[1] and open <= (close[1] + allowance) and close >= open[1]
bear_ec = close[1] > open[1] and open >= (close[1] - allowance) and close <= open[1]

//Engulfing Score
bull_ecScore = bull_ec ? engulfingWeight: 0
bear_ecScore = bear_ec ? engulfingWeight: 0

//Total Candle Score
bullCandleScore = bull_ecScore + bullHammerScore + bullHammerColorBoost + bullHammerWickScore
bearCandleScore = bear_ecScore + bearHammerScore + bearHammerColorBoost + bearHammerWickScore

//SOS/SOW Logic
validSOS = (bull_ec or bullHammer) and swingLow
validSOW = (bear_ec or bearHammer) and swingHigh

if validSOS and validSOW
    validSOS := false
    validSOW := false

bodySize = math.abs(open-close)
if bodySize / atr > 2.5
    validSOS := false
    validSOW := false
sosScore = bullCandleScore + volScore
sowScore = bearCandleScore + volScore

weakSOS = sosScore >= minWeakSOS and sosScore < minModerateSOS
moderateSOS = sosScore >= minModerateSOS and sosScore < minStrongSOS
strongSOS = sosScore >= minStrongSOS
weakSOW = sowScore >= minWeakSOS and sowScore < minModerateSOS
moderateSOW = sowScore >= minModerateSOS and sowScore < minStrongSOS
strongSOW = sowScore >= minStrongSOS

bool sos_toDisplay = false
if validSOS and displayWeakSOS and weakSOS
    sos_toDisplay := true

else if validSOS and displayModerateSOS and moderateSOS
    sos_toDisplay := true
else if validSOS and displayStrongSOS and strongSOS
    sos_toDisplay := true 
bool sow_toDisplay = false
if validSOW and displayWeakSOS and weakSOW
    sow_toDisplay := true
else if validSOW and displayModerateSOS and moderateSOW
    sow_toDisplay := true
else if validSOW and displayStrongSOS and strongSOW
    sow_toDisplay := true 

color sosColor = strongSOS ? color.new(color.green, 0) : moderateSOS ? color.new(color.green, 50) : color.new(color.green, 80)
color sowColor = strongSOW ? color.new(color.red, 0) : moderateSOW ? color.new(color.red, 50) : color.new(color.red, 80)

plotshape(sos_toDisplay, title="SOS", style=shape.circle, color=sosColor, location=location.belowbar, size=size.tiny)
plotshape(sow_toDisplay, title="SOW", style=shape.circle, color=sowColor, location=location.abovebar, size=size.tiny)

if sos_toDisplay and displayLabels
    label.new(x=bar_index, y=low, yloc=yloc.belowbar, text=str.tostring(sosScore, '#.#'), 
     style=label.style_label_up, color=sosColor, textcolor=color.white, size=size.normal)    
else if sow_toDisplay and displayLabels
    label.new(x=bar_index, y=high, yloc=yloc.abovebar, text=str.tostring(sowScore, '#.#'), 
     style=label.style_label_down, color=sowColor, textcolor=color.white, size=size.normal)

//Alerts
bool sos_alert = false
bool sow_alert = false
if alertWeakSOS and weakSOS and validSOS
    sos_alert := true
else if alertModSOS and moderateSOS and validSOS
    sos_alert := true
else if alertStrongSOS and strongSOS and validSOS
    sos_alert := true
if alertWeakSOW and weakSOW and validSOW
    sow_alert := true
else if alertModSOW and moderateSOW and validSOW
    sow_alert := true
else if alertStrongSOW and strongSOW and validSOW
    sow_alert := true
alertcondition(sos_alert, "SOS Alert", "SOS detected on {{ticker}} {{interval}}")
alertcondition(sow_alert, "SOW Alert", "SOW detected on {{ticker}} {{interval}}")

//