//@version=6 // 功能:短期支撑/压力位(当前周期) // 原理: // - 基于枢轴点(Pivot)在当前时间周期内形成的高点/低点,结合 ATR 容差进行“价格聚类”, // 聚合出更稳定的水平价位;用触碰次数过滤弱水平。 // - 每条水平线从“最新K线”向左以虚线绘制,右侧固定显示标签(类型S/R、价格、触碰次数)。 // 主要参数: // - Pivot Left/Right:枢轴确认强度;越大越严格、信号越少。 // - Minimum Touches:最少触碰次数;越大越稳定、越少越敏感。 // - ATR Length / Cluster Tolerance:聚类容差(ATR倍数);越大越易合并为少量更粗水平。 // - Max Visible Levels:图上最多显示的水平数量。 // - Label Size / Label Offset:标签字号与向右偏移的柱数。 // 交易应用(示例,不构成建议): // - 震荡区间:靠近“支撑S”观察反弹做多;靠近“压力R”观察回落做空;以水平外的 ATR 容差作为入场缓冲。 // - 趋势回踩:上升趋势中,回踩最近“支撑S”且收盘未跌破→顺势接回;跌破并收回失败→止损或反手。 // - 突破回测:收盘有效突破“压力R”,回测不跌回→看多延续;跌破“支撑S”并回测不过→看空延续。 // - 风险控制:可用“当前价与最近S/R的距离/ATR 倍数”推导止损间距与仓位;(若需要,可在脚本中扩展显示)。 // 说明:本脚本不使用未来函数;水平会随新枢轴与触碰实时更新。 indicator("Support/Resistance (Current TF)", overlay=true, max_lines_count=200, max_labels_count=100) // Inputs leftBars = input.int(3, "Pivot Left", minval=1) rightBars = input.int(3, "Pivot Right", minval=1) lookbackBars = input.int(500, "Lookback Bars", minval=50, maxval=5000) atrLength = input.int(14, "ATR Length", minval=1) clusterTolATR = input.float(0.25, "Cluster Tolerance (x ATR)", step=0.05, minval=0.05) minTouches = input.int(2, "Minimum Touches to Validate", minval=1) maxLevelsStored = input.int(60, "Max Stored Levels", minval=10, maxval=300) maxVisibleLevels = input.int(10, "Max Visible Levels", minval=1, maxval=50) lineWidth = input.int(2, "Line Width", minval=1, maxval=5) resistanceColor = input.color(color.new(color.red, 0), "Resistance Color") supportColor = input.color(color.new(color.teal, 0), "Support Color") showPriceLabels = input.bool(true, "Show Price Labels", inline="lbl") showTouchesInLbl = input.bool(true, "Touches In Label", inline="lbl") labelSizeOpt = input.string("Tiny", "Label Size", options=["Tiny", "Small", "Normal", "Large", "Huge"], inline="lbl") labelOffsetBars = input.int(1, "Label Offset (bars to right)", minval=1, maxval=500) // Calculations atrValue = ta.atr(atrLength) clusterTolerance = atrValue * clusterTolATR // Level storage var float[] levelPrices = array.new_float() var int[] levelTotalTouches = array.new_int() var int[] levelLastTouchBarIndex = array.new_int() var int[] levelSupportTouches = array.new_int() var int[] levelResistanceTouches = array.new_int() var line[] levelLines = array.new_line() var label[] levelLabels = array.new_label() // helper to map size option to Pine size enum f_label_size(opt) => opt == "Tiny" ? size.tiny : opt == "Small" ? size.small : opt == "Normal" ? size.normal : opt == "Large" ? size.large : size.huge // Utilities f_find_level_index(price, tolerance) => int foundIndex = -1 sz = array.size(levelPrices) if sz > 0 for i = 0 to sz - 1 existing = array.get(levelPrices, i) if math.abs(existing - price) <= tolerance foundIndex := i break foundIndex f_remove_level(idx) => ln = array.get(levelLines, idx) if not na(ln) line.delete(ln) lb = array.get(levelLabels, idx) if not na(lb) label.delete(lb) array.remove(levelPrices, idx) array.remove(levelTotalTouches, idx) array.remove(levelLastTouchBarIndex, idx) array.remove(levelSupportTouches, idx) array.remove(levelResistanceTouches, idx) array.remove(levelLines, idx) array.remove(levelLabels, idx) f_remove_old_levels() => sz = array.size(levelPrices) if sz > 0 // iterate safely in reverse using computed index for k = 0 to sz - 1 i = sz - 1 - k lastBar = array.get(levelLastTouchBarIndex, i) if bar_index - lastBar > lookbackBars f_remove_level(i) f_ensure_capacity() => if array.size(levelPrices) >= maxLevelsStored // Remove the oldest by last touch oldestIdx = 0 oldestBar = array.get(levelLastTouchBarIndex, 0) for i = 1 to array.size(levelPrices) - 1 b = array.get(levelLastTouchBarIndex, i) if b < oldestBar oldestBar := b oldestIdx := i f_remove_level(oldestIdx) f_add_or_update_level(price, isResistance) => idx = f_find_level_index(price, clusterTolerance) if idx == -1 f_ensure_capacity() array.push(levelPrices, price) array.push(levelTotalTouches, 1) array.push(levelLastTouchBarIndex, bar_index) array.push(levelSupportTouches, isResistance ? 0 : 1) array.push(levelResistanceTouches, isResistance ? 1 : 0) array.push(levelLines, na) array.push(levelLabels, na) else prevPrice = array.get(levelPrices, idx) touches = array.get(levelTotalTouches, idx) newTouches = touches + 1 // Re-anchor price by averaging to stabilize the level newPrice = (prevPrice * touches + price) / newTouches array.set(levelPrices, idx, newPrice) array.set(levelTotalTouches, idx, newTouches) array.set(levelLastTouchBarIndex, idx, bar_index) if isResistance r = array.get(levelResistanceTouches, idx) + 1 array.set(levelResistanceTouches, idx, r) else s = array.get(levelSupportTouches, idx) + 1 array.set(levelSupportTouches, idx, s) f_draw_levels() => // Collect candidate indices var int[] candidates = array.new_int() array.clear(candidates) sz = array.size(levelPrices) if sz > 0 for i = 0 to sz - 1 touches = array.get(levelTotalTouches, i) lastBar = array.get(levelLastTouchBarIndex, i) if touches >= minTouches and bar_index - lastBar <= lookbackBars array.push(candidates, i) // Select up to maxVisibleLevels by distance to close var int[] chosen = array.new_int() array.clear(chosen) candSz = array.size(candidates) if candSz > 0 chooseCount = math.min(maxVisibleLevels, candSz) for _ = 0 to chooseCount - 1 if array.size(candidates) == 0 break bestPos = -1 bestDist = 10e10 curSz = array.size(candidates) if curSz > 0 for pos = 0 to curSz - 1 idx = array.get(candidates, pos) price = array.get(levelPrices, idx) d = math.abs(close - price) if d < bestDist bestDist := d bestPos := pos if bestPos != -1 pickedIdx = array.get(candidates, bestPos) array.push(chosen, pickedIdx) array.remove(candidates, bestPos) // Build a quick lookup for chosen to manage create/update/delete of lines var bool[] isChosen = array.new_bool() array.clear(isChosen) if sz > 0 for _ = 0 to sz - 1 array.push(isChosen, false) chosenSz = array.size(chosen) if chosenSz > 0 and sz > 0 for j = 0 to chosenSz - 1 chIdx = array.get(chosen, j) if chIdx >= 0 and chIdx < sz array.set(isChosen, chIdx, true) // Create/Update lines for chosen, delete lines for non-chosen if sz > 0 for i = 0 to sz - 1 ln = array.get(levelLines, i) price = array.get(levelPrices, i) supTouches = array.get(levelSupportTouches, i) resTouches = array.get(levelResistanceTouches, i) isRes = resTouches >= supTouches col = isRes ? resistanceColor : supportColor if array.get(isChosen, i) xRight = bar_index xLeft = math.max(0, bar_index - lookbackBars) if na(ln) ln := line.new(x1=xLeft, y1=price, x2=xRight, y2=price, extend=extend.none, color=col, width=lineWidth) line.set_style(ln, line.style_dotted) array.set(levelLines, i, ln) else line.set_xy1(ln, xLeft, price) line.set_xy2(ln, xRight, price) line.set_extend(ln, extend.none) line.set_color(ln, col) line.set_width(ln, lineWidth) line.set_style(ln, line.style_dotted) // labels lb = array.get(levelLabels, i) if showPriceLabels touches = array.get(levelTotalTouches, i) lblTxt = (isRes ? "R " : "S ") + str.tostring(price, format.price) + (showTouchesInLbl ? " x" + str.tostring(touches) : "") // Always recreate to honor size/offset changes reliably if not na(lb) label.delete(lb) lb := na desiredSize = f_label_size(labelSizeOpt) lblX = bar_index + labelOffsetBars lb := label.new(x=lblX, y=price, text=lblTxt, style=label.style_label_left, color=color.new(col, 85), textcolor=color.white, size=desiredSize) array.set(levelLabels, i, lb) else if not na(lb) label.delete(lb) array.set(levelLabels, i, na) else if not na(ln) line.delete(ln) array.set(levelLines, i, na) lb = array.get(levelLabels, i) if not na(lb) label.delete(lb) array.set(levelLabels, i, na) // Detect pivots on current timeframe ph = ta.pivothigh(high, leftBars, rightBars) pl = ta.pivotlow(low, leftBars, rightBars) if not na(ph) f_add_or_update_level(ph, true) if not na(pl) f_add_or_update_level(pl, false) // Maintenance and drawing f_remove_old_levels() f_draw_levels() // Optional: show nearest support/resistance prices var float nearestSupport = na var float nearestResistance = na // Scan chosen candidates quickly by direction float bestBelow = na float bestAbove = na szAll = array.size(levelPrices) if szAll > 0 for i = 0 to szAll - 1 touches = array.get(levelTotalTouches, i) lastBar = array.get(levelLastTouchBarIndex, i) if touches >= minTouches and bar_index - lastBar <= lookbackBars p = array.get(levelPrices, i) if p <= close bestBelow := na(bestBelow) ? p : math.max(bestBelow, p) if p >= close bestAbove := na(bestAbove) ? p : math.min(bestAbove, p) nearestSupport := bestBelow nearestResistance := bestAbove plot(nearestSupport, title="Nearest Support", color=color.new(supportColor, 60), linewidth=1, style=plot.style_linebr) plot(nearestResistance, title="Nearest Resistance", color=color.new(resistanceColor, 60), linewidth=1, style=plot.style_linebr)