//@version=5 indicator("Simple Auto Swing Lines", shorttitle="Simple Auto Swing Lines", overlay=true) // ===== Input Parameters ===== // Pivot detection settings var pivotGroup = "Pivot Detection" pivotStrength = input.int(title="Pivot Strength", defval=5, minval=1, group=pivotGroup, tooltip="Number of bars to check on each side of pivot point") maxBars = input.int(500, "Max Lookback Bars", minval=10, group=pivotGroup) // Line settings var lineGroup = "Line Settings" maxLines = input.int(2, "Max Lines", minval=1, maxval=20, group=lineGroup, tooltip="Maximum number of swing lines PER SIDE (e.g. if 2 is set, 4 lines will be shown)") lineThickness = input.int(1, "Line Thickness", minval=1, maxval=5, group=lineGroup) topLineColor = input.color(color.red, "Resistance Line Color", group=lineGroup) bottomLineColor = input.color(color.green, "Support Line Color", group=lineGroup) showLabels = input.bool(false, "Show Labels", group=lineGroup) extendLeft = input.bool(false, "Extend Left", group=lineGroup, tooltip="Extend lines to the left") extendRight = input.bool(true, "Extend Right", group=lineGroup, tooltip="Extend lines to the right") placeOnBody = input.bool(false, "Place Line on Body", group=lineGroup, tooltip="Place lines on candle body (open/close) instead of wicks (high/low)") // Display settings var displayGroup = "Display Settings" proximityFilter = input.int(0, "Proximity Filter (Ticks)", minval=0, maxval=30000, group=displayGroup, tooltip="Filter out swing lines that are within this distance (in ticks) of older ones. Set to 0 to disable.") candlesBeforeHide = input.int(2, "Candles Before Hide", minval=1, maxval=50, group=displayGroup, tooltip="Number of consecutive candles that must close above/below a swing line before hiding it.") // Fixed appearance settings var lineStyleDict = line.style_solid var hideClipped = true // ===== Data Structures ===== // We'll use a more robust approach with fixed-size arrays to avoid out-of-bounds errors var int maxArraySize = 100 // Maximum number of pivot points to store // Arrays for pivot highs var float[] highPrices = array.new_float(maxArraySize, na) var int[] highBars = array.new_int(maxArraySize, 0) var bool[] highActive = array.new_bool(maxArraySize, false) var bool[] showHighPivot = array.new_bool(maxArraySize, true) // Track if pivot should be shown after proximity filter var int[] highCrossCount = array.new_int(maxArraySize, 0) // Track consecutive closes above pivot var bool[] highClipped = array.new_bool(maxArraySize, false) // Track if pivot was permanently clipped // Arrays for pivot lows var float[] lowPrices = array.new_float(maxArraySize, na) var int[] lowBars = array.new_int(maxArraySize, 0) var bool[] lowActive = array.new_bool(maxArraySize, false) var bool[] showLowPivot = array.new_bool(maxArraySize, true) // Track if pivot should be shown after proximity filter var int[] lowCrossCount = array.new_int(maxArraySize, 0) // Track consecutive closes below pivot var bool[] lowClipped = array.new_bool(maxArraySize, false) // Track if pivot was permanently clipped // Counters for current position in arrays var int highCount = 0 var int lowCount = 0 // ===== Pivot Detection ===== // Detect pivot highs and lows using the same strength for both left and right // Use body values if placeOnBody is enabled, otherwise use wicks highSource = placeOnBody ? math.max(open, close) : high lowSource = placeOnBody ? math.min(open, close) : low ph = ta.pivothigh(highSource, pivotStrength, pivotStrength) pl = ta.pivotlow(lowSource, pivotStrength, pivotStrength) // Add new pivot high if detected if not na(ph) // Use modulo to wrap around the array when it's full pivotIndex = highCount % maxArraySize array.set(highPrices, pivotIndex, ph) array.set(highBars, pivotIndex, bar_index - pivotStrength) array.set(highActive, pivotIndex, true) array.set(highCrossCount, pivotIndex, 0) // Reset cross count for new pivot array.set(highClipped, pivotIndex, false) // Reset clipped status for new pivot highCount := highCount + 1 // Add new pivot low if detected if not na(pl) pivotIndex = lowCount % maxArraySize array.set(lowPrices, pivotIndex, pl) array.set(lowBars, pivotIndex, bar_index - pivotStrength) array.set(lowActive, pivotIndex, true) array.set(lowCrossCount, pivotIndex, 0) // Reset cross count for new pivot array.set(lowClipped, pivotIndex, false) // Reset clipped status for new pivot lowCount := lowCount + 1 // ===== Process Pivot Points ===== // Check for clipped lines and count active lines var int activeHighs = 0 var int activeLows = 0 // Reset active pivot counters activeHighs := 0 activeLows := 0 // Reset show arrays for i = 0 to maxArraySize - 1 array.set(showHighPivot, i, true) array.set(showLowPivot, i, true) // Process high pivots for i = 0 to maxArraySize - 1 price = array.get(highPrices, i) barIdx = array.get(highBars, i) isActive = array.get(highActive, i) // Skip if not a valid pivot or too old if na(price) or (bar_index - barIdx) > maxBars array.set(highActive, i, false) array.set(highCrossCount, i, 0) continue // Only update invalidation on confirmed candle closes. if barstate.isconfirmed // Check if price closed above the pivot (clipped) if close > price // Increment counter for consecutive closes above crossCount = array.get(highCrossCount, i) + 1 array.set(highCrossCount, i, crossCount) // Only hide after required number of candles if crossCount >= candlesBeforeHide array.set(highActive, i, false) array.set(highClipped, i, true) // Mark as permanently clipped array.set(highCrossCount, i, 0) else // Reset counter but don't reactivate if permanently clipped array.set(highCrossCount, i, 0) // Only reactivate if not permanently clipped if not array.get(highActive, i) and not array.get(highClipped, i) and (bar_index - barIdx) <= maxBars array.set(highActive, i, true) // Keep showing the pivot until the required invalidation count is reached. if array.get(highActive, i) activeHighs := activeHighs + 1 // Process low pivots for i = 0 to maxArraySize - 1 price = array.get(lowPrices, i) barIdx = array.get(lowBars, i) isActive = array.get(lowActive, i) // Skip if not a valid pivot or too old if na(price) or (bar_index - barIdx) > maxBars array.set(lowActive, i, false) array.set(lowCrossCount, i, 0) continue // Only update invalidation on confirmed candle closes. if barstate.isconfirmed // Check if price closed below the pivot (clipped) if close < price // Increment counter for consecutive closes below crossCount = array.get(lowCrossCount, i) + 1 array.set(lowCrossCount, i, crossCount) // Only hide after required number of candles if crossCount >= candlesBeforeHide array.set(lowActive, i, false) array.set(lowClipped, i, true) // Mark as permanently clipped array.set(lowCrossCount, i, 0) else // Reset counter but don't reactivate if permanently clipped array.set(lowCrossCount, i, 0) // Only reactivate if not permanently clipped if not array.get(lowActive, i) and not array.get(lowClipped, i) and (bar_index - barIdx) <= maxBars array.set(lowActive, i, true) // Keep showing the pivot until the required invalidation count is reached. if array.get(lowActive, i) activeLows := activeLows + 1 // Apply proximity filter if enabled if proximityFilter > 0 // Check high pivots against each other for i = 0 to maxArraySize - 1 if array.get(highActive, i) and array.get(showHighPivot, i) priceI = array.get(highPrices, i) barI = array.get(highBars, i) // Compare with other active high pivots for j = 0 to maxArraySize - 1 if i != j and array.get(highActive, j) and array.get(showHighPivot, j) priceJ = array.get(highPrices, j) barJ = array.get(highBars, j) // Calculate distance in ticks priceDiff = math.abs(priceI - priceJ) / syminfo.mintick // If pivots are close enough and the current pivot is newer if priceDiff <= proximityFilter and barI > barJ // Hide the newer pivot array.set(showHighPivot, i, false) break // Check low pivots against each other for i = 0 to maxArraySize - 1 if array.get(lowActive, i) and array.get(showLowPivot, i) priceI = array.get(lowPrices, i) barI = array.get(lowBars, i) // Compare with other active low pivots for j = 0 to maxArraySize - 1 if i != j and array.get(lowActive, j) and array.get(showLowPivot, j) priceJ = array.get(lowPrices, j) barJ = array.get(lowBars, j) // Calculate distance in ticks priceDiff = math.abs(priceI - priceJ) / syminfo.mintick // If pivots are close enough and the current pivot is newer if priceDiff <= proximityFilter and barI > barJ // Hide the newer pivot array.set(showLowPivot, i, false) break // ===== Draw Lines and Labels ===== // ===== Drawing Functions ===== // Store lines and labels in arrays so we can delete and recreate them var line[] resistanceLines = array.new_line() var line[] supportLines = array.new_line() var label[] lineLabels = array.new_label() // Clear all previous drawings clearDrawings() => // Clear all lines while array.size(resistanceLines) > 0 line.delete(array.pop(resistanceLines)) while array.size(supportLines) > 0 line.delete(array.pop(supportLines)) // Clear all labels while array.size(lineLabels) > 0 label.delete(array.pop(lineLabels)) // Helper functions to create and store lines and labels drawLine(barIndex, price, lineColor, width, style, isResistance) => // Determine line start and end points based on extend settings lineStart = extendLeft ? barIndex - 500 : barIndex lineEnd = extendRight ? bar_index + 500 : bar_index // Create line with appropriate extension extendType = extendLeft and extendRight ? extend.both : extendLeft ? extend.left : extendRight ? extend.right : extend.none ln = line.new(lineStart, price, lineEnd, price, color=lineColor, width=width, style=style, xloc=xloc.bar_index, extend=extendType) // Create label only if showLabels is enabled if showLabels labelText = isResistance ? "Swing High" : "Swing Low" lbl = label.new(bar_index + 12, price, labelText, color=color.new(color.white, 100), style=label.style_none, textcolor=lineColor, xloc=xloc.bar_index) array.push(lineLabels, lbl) // Store the line in the appropriate array if isResistance array.push(resistanceLines, ln) else array.push(supportLines, ln) ln // Draw lines (always enabled now) // Clear previous drawings first clearDrawings() // Draw resistance lines (most recent first) drawnHighs = 0 // Start from the most recent pivot (highCount - 1) and work backwards highStartIdx = highCount - 1 for j = 0 to maxArraySize - 1 i = (highStartIdx - j) % maxArraySize if i < 0 i := i + maxArraySize price = array.get(highPrices, i) barIdx = array.get(highBars, i) isActive = array.get(highActive, i) showPivot = array.get(showHighPivot, i) if not na(price) and isActive and showPivot and drawnHighs < math.min(maxLines, activeHighs) // Draw line drawLine(barIdx, price, topLineColor, lineThickness, lineStyleDict, true) drawnHighs := drawnHighs + 1 // Draw support lines (most recent first) drawnLows = 0 // Start from the most recent pivot (lowCount - 1) and work backwards lowStartIdx = lowCount - 1 for j = 0 to maxArraySize - 1 i = (lowStartIdx - j) % maxArraySize if i < 0 i := i + maxArraySize price = array.get(lowPrices, i) barIdx = array.get(lowBars, i) isActive = array.get(lowActive, i) showPivot = array.get(showLowPivot, i) if not na(price) and isActive and showPivot and drawnLows < math.min(maxLines, activeLows) // Draw line drawLine(barIdx, price, bottomLineColor, lineThickness, lineStyleDict, false) drawnLows := drawnLows + 1 // ===== Alert Conditions ===== // Variables to track alert conditions var bool resistanceCrossed = false var bool supportCrossed = false // Check for price touching any DRAWN resistance line (swing high) // Only check the lines that are actually being displayed resistanceCrossed := false // Use the same logic as drawing to only check displayed lines drawnHighsForAlert = 0 highStartIdxAlert = highCount - 1 for j = 0 to maxArraySize - 1 i = (highStartIdxAlert - j) % maxArraySize if i < 0 i := i + maxArraySize price = array.get(highPrices, i) barIdx = array.get(highBars, i) isActive = array.get(highActive, i) showPivot = array.get(showHighPivot, i) // Only check lines that would actually be drawn if not na(price) and isActive and showPivot and drawnHighsForAlert < math.min(maxLines, activeHighs) // Check if price touched or crossed the resistance line if (high >= price and low <= price) or (close > price and close[1] <= price) or (close < price and close[1] >= price) resistanceCrossed := true break drawnHighsForAlert := drawnHighsForAlert + 1 // Check for price touching any DRAWN support line (swing low) // Only check the lines that are actually being displayed supportCrossed := false // Use the same logic as drawing to only check displayed lines drawnLowsForAlert = 0 lowStartIdxAlert = lowCount - 1 for j = 0 to maxArraySize - 1 i = (lowStartIdxAlert - j) % maxArraySize if i < 0 i := i + maxArraySize price = array.get(lowPrices, i) barIdx = array.get(lowBars, i) isActive = array.get(lowActive, i) showPivot = array.get(showLowPivot, i) // Only check lines that would actually be drawn if not na(price) and isActive and showPivot and drawnLowsForAlert < math.min(maxLines, activeLows) // Check if price touched or crossed the support line if (high >= price and low <= price) or (close > price and close[1] <= price) or (close < price and close[1] >= price) supportCrossed := true break drawnLowsForAlert := drawnLowsForAlert + 1 // Combined alert for any crossing anyCrossed = resistanceCrossed or supportCrossed // Alert conditions alertcondition(resistanceCrossed, title="Swing High touched", message="Swing HIGH touched") alertcondition(supportCrossed, title="Swing Low touched", message="Swing LOW touched") alertcondition(anyCrossed, title="Any Swing Level touched", message="Swing Level touched")