Chop Zone - SamX
// This source code provided free and open-source as defined by the terms of the Mozilla Public License 2.0 (https://mozilla.org/MPL/2.0/)
// with the following additional requirements:
//
// 1. Citations for sources and references must be maintained and updated when appropriate
// 2. Links to strategy references included in indicator tooptips and/or alerts should be retained to give credit to the original source
//             while also providing a freely available source of information on proper use and interpretation
//
// Author:  SamAccountX
//
// Original inspiration came from the built-in Chop Zone indicator (https://www.tradingview.com/chart/?solution=43000589111), 
// however I wanted to make some improvements and add some customization options to better suit varying market conditions.
// 
// WARNING: Please be sure of what you're doing and understand the potential implications before altering any of the settings
//          in the "Advanced Configuration" section!!!
//
//@version=5
indicator(title = "Chop Zone - SamX", format=format.price, precision=2, max_labels_count=500)
// ***  Inputs  ***
// Basic input settings
g_NormalInputs = 'Basic Configuration'
tf = input.timeframe(title="Timeframe", defval="", group=g_NormalInputs)
showGaps = input.bool(title="Show gaps for higher timeframes", defval=true, group=g_NormalInputs, tooltip="If selected, higher TF values will only be returned when " +
         'the higher timeframe bar actually closes.  This helps avoid real-time repaining at the expense of potential resolution clarity. \n\n' +
         'If this option is unchecked, the current bar will be populated with the current higher TF bar\'s value (which will repaint), however ' +
         'historical bars will not repaint.')
src = input.source(title='MA Source', defval=close, group=g_NormalInputs, tooltip="Price source to use for calculating the MA basis for the chop zones.")
len = input.int(title='MA Length', defval=34, minval=1, maxval=500, step=1, group=g_NormalInputs, tooltip="Length to use for calculating the MA basis for the chop zones.")
showAsAngleArea = input.bool(title="Plot calculated angle", defval=false, group=g_NormalInputs, tooltip="If selected, the default bar-style chop zone will be replaced with an " +
         'Area-style chart displaying the actual calculated angles. \n\n' +
         'Note #1:  This works best using the Gradient color settings below. \n\n' +
         'Note #2:  If configured to calculate on a timeframe HIGHER than the current chart timeframe, it is recommended to un-check the "Show gaps for higher timeframes" setting above ' +
         'to avoid display anomolies.')
// Coloring settings
// Gradient settings
g_GradientSettings = "Gradient Color Settings"
colorAsGradient = input.bool(title="Color bars using gradient", defval=false, group=g_GradientSettings)
downColor = input.color(title="Downslope:", defval=color.rgb(255, 0, 0, 0), inline="Gradient", group=g_GradientSettings)
upColor = input.color(title="         Upslope:", defval=color.rgb(0, 255, 0, 0), inline="Gradient", group=g_GradientSettings)
gradientLimit = input.int(title="Gradient Limit", defval=33, minval=1, maxval=89, step=1, group=g_GradientSettings, tooltip="Select the desired gradient threshold angle. This " +
         'will be used in gradient color calculations.  This value will be extrapolated as the absolute distance from 0.  If the calculated angle exceeds this threshold, the resulting ' +
         'bar color will be the equivilent of the closest in-range color.')
// Fixed bar color settings
g_BarColors = 'Bar colors'
// Tier 0 - centered on 0 degrees
colorLevel0 = input.color(title="                       Tier-1 Color:", defval=color.rgb(253, 216, 53, 0), group=g_BarColors)
// Tier 1 - 1x past tier 0
colorLevel1Pos = input.color(title="Tier-2 Color - Positive:", defval=color.rgb(0, 150, 136, 0), group=g_BarColors, inline="tier-2")
colorLevel1Neg = input.color(title="   Negative:", defval=color.rgb(255, 183, 77, 0), group=g_BarColors, inline="tier-2")
// Tier 2 - 1x past tier 1
colorLevel2Pos = input.color(title="Tier-3 Color - Positive:", defval=color.rgb(165, 214, 167, 0), group=g_BarColors, inline="tier-3")
colorLevel2Neg = input.color(title="   Negative:", defval=color.rgb(255, 109, 0, 0), group=g_BarColors, inline="tier-3")
// Tier 3 - 1x past tier 2
colorLevel3Pos = input.color(title="Tier-4 Color - Positive:", defval=color.rgb(67, 160, 71, 0), group=g_BarColors, inline="tier-4")
colorLevel3Neg = input.color(title="   Negative:", defval=color.rgb(233, 30, 99, 0), group=g_BarColors, inline="tier-4")
// Tier 4 - 1x past tier 3
colorLevel4Pos = input.color(title="Tier-5 Color - Positive:", defval=color.rgb(38, 198, 218, 0), group=g_BarColors, inline="tier-5")
colorLevel4Neg = input.color(title="   Negative:", defval=color.rgb(213, 0, 0, 0), group=g_BarColors, inline="tier-5")
//
// Advanced input settings - generally should not be changed...
g_AdvancedInputs = 'Advanced Configuration'
maType = input.string(title='Moving Average Calculation', group=g_AdvancedInputs, options=['Exponential', 'Simple', 'Smoothed', 'Weighted', 'Linear', 'Hull', 'Volume-Weigehted', 'RMA', 'ALMA'], 
           defval='Exponential', tooltip='Type of moving average calculation to use (default is Exponential (EMA)). ALMA uses the standard values for sigma and offset.  \n\n' +
                  'Note: DO NOT CHANGE THIS UNLESS YOU KNOW WHAT YOU\'RE DOING!!!')
periodsIn = input.int(title='Periods', defval=30, minval=1, maxval=300, step=1, group=g_AdvancedInputs, tooltip='Number of candles to check when searching for highest high and lowest low.  \n\n' +
		     'Note: DO NOT CHANGE THIS UNLESS YOU KNOW WHAT YOU\'RE DOING!!!')
spanFactor = input.int(title='Span Length', defval=25, minval=1, maxval=100, step=1, group=g_AdvancedInputs, tooltip='Span length for range calculations. \n\n' +
             'Note: DO NOT CHANGE THIS UNLESS YOU KNOW WHAT YOU\'RE DOING!!!')
periodAvgSrc = input.string(title='Period/Bar Average', defval="hlc3", options=["hl2", "hlc3", "ohlc4", "hlcc4"], tooltip='Method to use to derive the average price for a ' +
             'given bar for use in slope calculations. \n\n' +
             'Note: DO NOT CHANGE THIS UNLESS YOU KNOW WHAT YOU\'RE DOING!!!')
stepSize = input.float(title="Bar Step Size", defval=1.43, minval=.01, maxval=10, step=0.1, group=g_AdvancedInputs, tooltip='Step size to use for chop zone brackets.  Increasing this will result ' +
             'in each bar covering a wider range of angles, while decreasing this will result in each bar covering a narrower range of angles.')
useAltSlopeCalc = input.bool(title='Use alternate slope calculation method', defval=false, group=g_AdvancedInputs, tooltip='Select this to find the MA slope using an alternate calculation method. \n\n' +
             'Note: DO NOT CHANGE THIS UNLESS YOU KNOW WHAT YOU\'RE DOING!!!')
//
// ***  Function definitions  ***
// Smoothed MA
smoothedMovingAvg(src, len) => 
	smma = 0.0
	// TV will complain about the use of the ta.sma function use inside a function saying that it should be called on each calculation, 
	// but since we're only using it once to set the initial value for the smoothed MA (when the previous smma value is NaN - Not a Number)
	// and using the previous smma value for each subsequent iteration, this can be safely ignored
	smma := na(smma[1]) ? ta.sma(src, len) : (smma[1] * (len - 1) + src) / len 
	smma
//
// MA calculation
ma(source, length, type) =>
    switch type
        "Simple" => ta.sma(source, length)
        "Exponential" => ta.ema(source, length)
        "Weighted" => ta.wma(source, length)
        "Volume-Weigehted" => ta.vwma(source, length)
        "Smoothed" => smoothedMovingAvg(source, length)
        "RMA" => ta.rma(source, length)
        "Linear" => ta.linreg(source, length, 0)
        "Hull" => ta.hma(source, length)
        "ALMA" => ta.alma(source, length, 0.85, 6)
//
//  *** Functional code start  ***
//
// Explicitly define our ticker to help ensure that we're always getting ACTUAL price instead of relying on the input
// ticker info and input vars (as they tend to inherit the type from what's displayed on the current chart)
realPriceTicker = ticker.new(prefix=syminfo.prefix, ticker=syminfo.ticker)

avgCalc = switch periodAvgSrc
    "hl2" => hl2
    "hlc3" => hlc3
    "ohlc4" => ohlc4
    "hlcc4" => hlcc4
    => hlc3
    
avg = showGaps ? request.security(symbol=realPriceTicker, timeframe=tf, expression=avgCalc, lookahead=barmerge.lookahead_off, gaps=barmerge.gaps_on) :
                 request.security(symbol=realPriceTicker, timeframe=tf, expression=avgCalc, lookahead=barmerge.lookahead_off, gaps=barmerge.gaps_off)
pi = math.pi
periods = periodsIn
highestHigh = showGaps ? request.security(symbol=realPriceTicker, timeframe=tf, expression=ta.highest(periods), lookahead=barmerge.lookahead_off, gaps=barmerge.gaps_on) :
                         request.security(symbol=realPriceTicker, timeframe=tf, expression=ta.highest(periods), lookahead=barmerge.lookahead_off, gaps=barmerge.gaps_off)
lowestLow = showGaps ? request.security(symbol=realPriceTicker, timeframe=tf, expression=ta.lowest(periods), lookahead=barmerge.lookahead_off, gaps=barmerge.gaps_on) :
                     request.security(symbol=realPriceTicker, timeframe=tf, expression=ta.lowest(periods), lookahead=barmerge.lookahead_off, gaps=barmerge.gaps_off)
span = spanFactor / (highestHigh - lowestLow) * lowestLow
ema34 = showGaps ? request.security(symbol=realPriceTicker, timeframe=tf, expression=ma(src, len, maType), lookahead=barmerge.lookahead_off, gaps=barmerge.gaps_on) :
                   request.security(symbol=realPriceTicker, timeframe=tf, expression=ma(src, len, maType), lookahead=barmerge.lookahead_off, gaps=barmerge.gaps_off)
ema34_prev = showGaps ? request.security(symbol=realPriceTicker, timeframe=tf, expression=ma(src[1], len, maType), lookahead=barmerge.lookahead_off, gaps=barmerge.gaps_on) :
                         request.security(symbol=realPriceTicker, timeframe=tf, expression=ma(src[1], len, maType), lookahead=barmerge.lookahead_off, gaps=barmerge.gaps_off)

var emaAngle = 0.0
if (useAltSlopeCalc)
    rightX = 1
    rightY = ema34
    leftX = 0
    leftY = ema34_prev
    slope = (rightY - leftY) / (rightX - leftX)
    angleRadians = math.atan(slope)
    angleDegrees = math.todegrees(angleRadians)
    emaAngle := angleDegrees //slope < 0 ? -angleDegrees : angleDegrees
else
    x1_ema34 = 0
    x2_ema34 = 1
    y1_ema34 = 0
    y2_ema34 = (ema34_prev - ema34) / avg * span
    c_ema34 = math.sqrt((x2_ema34 - x1_ema34)*(x2_ema34 - x1_ema34) + (y2_ema34 - y1_ema34)*(y2_ema34 - y1_ema34))
    emaAngle_1 = math.round(180 * math.acos((x2_ema34 - x1_ema34)/c_ema34) / pi)
    emaAngle := y2_ema34 > 0 ? -emaAngle_1 : emaAngle_1
    
// Select color based on inputs and calculated MA angle...
//
// First, we need to calculate our 0-level zone.  Since we need to also include the ACTUAL 0.00 angle as well, we
// have a few options for sizing this zone...  For simplicity, we're going to divide our bar step size by 2 and 
// round to 2 decimal places.  Should rounding be required, we'll always round down.
//
// Unfortunately, TV doesn't have a `floor` function that can round to decimals (only integers), so we'll have to 
// use some clever math gymnastics to accomplish this.
halfStep = math.floor((stepSize / 2) * 100) / 100
var color chopZoneColor = na
if (emaAngle >= 3*stepSize + halfStep)
    chopZoneColor := colorLevel4Pos 
else if (emaAngle < 3*stepSize + halfStep and emaAngle >= 2*stepSize + halfStep)
    chopZoneColor := colorLevel3Pos
else if (emaAngle < 2*stepSize + halfStep and emaAngle >= stepSize + halfStep)
    chopZoneColor := colorLevel2Pos
else if (emaAngle < stepSize + halfStep and emaAngle >= halfStep)
    chopZoneColor := colorLevel1Pos
else if (emaAngle > -halfStep and emaAngle < halfStep)
    // Between -.71 and .71
    chopZoneColor := colorLevel0
else if (emaAngle > -stepSize - halfStep and emaAngle <= -halfStep)
    chopZoneColor := colorLevel1Neg
else if (emaAngle > -2*stepSize - halfStep and emaAngle <= -stepSize - halfStep)
    chopZoneColor := colorLevel2Neg
else if (emaAngle > -3*stepSize - halfStep and emaAngle <= -2*stepSize - halfStep)
    chopZoneColor := colorLevel3Neg
else if (emaAngle <= -3*stepSize - halfStep)
    chopZoneColor := colorLevel4Neg

if (colorAsGradient)
    chopZoneColor := color.from_gradient(emaAngle, -gradientLimit, gradientLimit, downColor, upColor)


plot(showAsAngleArea ? na : 1, title='Chop Zone', color=na(avg) ? na : chopZoneColor, style=plot.style_columns)
plot(showAsAngleArea ? emaAngle : na, title='MA Angle', color=na(avg) ? na : chopZoneColor, style=plot.style_area)
// Label plots - for testing use only
//if not na(avg)
//    label.new(bar_index, 1.25, text=str.tostring(math.round(emaAngle)), style=label.style_label_down)
