// 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)